diff --git a/Grbl_Esp32/src/Machines/3axis_rs485.h b/Grbl_Esp32/src/Machines/3axis_rs485.h
new file mode 100644
index 00000000..75de35e0
--- /dev/null
+++ b/Grbl_Esp32/src/Machines/3axis_rs485.h
@@ -0,0 +1,71 @@
+#pragma once
+// clang-format off
+
+/*
+ 3axis_xyx.h
+ Part of Grbl_ESP32
+
+ This is a general XYZ-axis RS-485 CNC machine. The schematic is quite
+ easy, you basically need a MAX485 wired through a logic level converter
+ for the VFD, and a few pins wired through an ULN2803A to the external
+ stepper drivers. It's common to have a dual gantry for the Y axis.
+
+ Optional limit pins are slightly more difficult, as these require a
+ Schmitt trigger and optocouplers.
+
+ 2020 - Stefan de Bruijn
+
+ Grbl_ESP32 is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Grbl_ESP32. If not, see .
+*/
+
+#define MACHINE_NAME "ESP32_XYZ_RS485"
+#define X_STEP_PIN GPIO_NUM_4 // labeled X
+#define X_DIRECTION_PIN GPIO_NUM_16 // labeled X
+#define Y_STEP_PIN GPIO_NUM_17 // labeled Y
+#define Y_DIRECTION_PIN GPIO_NUM_18 // labeled Y
+#define Y2_STEP_PIN GPIO_NUM_19 // labeled Y2
+#define Y2_DIRECTION_PIN GPIO_NUM_21 // labeled Y2
+#define Z_STEP_PIN GPIO_NUM_22 // labeled Z
+#define Z_DIRECTION_PIN GPIO_NUM_23 // labeled Z
+
+#define SPINDLE_TYPE SPINDLE_TYPE_H2A
+#define VFD_RS485_TXD_PIN GPIO_NUM_13 // RS485 TX
+#define VFD_RS485_RTS_PIN GPIO_NUM_15 // RS485 RTS
+#define VFD_RS485_RXD_PIN GPIO_NUM_2 // RS485 RX
+
+#define X_LIMIT_PIN GPIO_NUM_33
+#define Y_LIMIT_PIN GPIO_NUM_32
+#define Y2_LIMIT_PIN GPIO_NUM_35
+#define Z_LIMIT_PIN GPIO_NUM_34
+
+#ifdef HOMING_CYCLE_0
+ #undef HOMING_CYCLE_0
+#endif
+#define HOMING_CYCLE_0 bit(Z_AXIS) // Z first
+
+#ifdef HOMING_CYCLE_1
+ #undef HOMING_CYCLE_1
+#endif
+#define HOMING_CYCLE_1 (bit(X_AXIS)|bit(Y_AXIS))
+
+#ifdef HOMING_CYCLE_2
+ #undef HOMING_CYCLE_2
+#endif
+
+#define PROBE_PIN GPIO_NUM_14 // labeled Probe
+#define CONTROL_RESET_PIN GPIO_NUM_27 // labeled Reset
+#define CONTROL_FEED_HOLD_PIN GPIO_NUM_26 // labeled Hold
+#define CONTROL_CYCLE_START_PIN GPIO_NUM_25 // labeled Start
+
+// #define VFD_DEBUG_MODE
diff --git a/Grbl_Esp32/src/Machines/4axis_external_driver.h b/Grbl_Esp32/src/Machines/4axis_external_driver.h
index 0b52d1fc..61cb3edd 100644
--- a/Grbl_Esp32/src/Machines/4axis_external_driver.h
+++ b/Grbl_Esp32/src/Machines/4axis_external_driver.h
@@ -51,9 +51,9 @@
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
-#define HUANYANG_TXD_PIN GPIO_NUM_17
-#define HUANYANG_RXD_PIN GPIO_NUM_4
-#define HUANYANG_RTS_PIN GPIO_NUM_16
+#define VFD_RS485_TXD_PIN GPIO_NUM_17
+#define VFD_RS485_RXD_PIN GPIO_NUM_4
+#define VFD_RS485_RTS_PIN GPIO_NUM_16
#define X_LIMIT_PIN GPIO_NUM_34
#define Y_LIMIT_PIN GPIO_NUM_35
diff --git a/Grbl_Esp32/src/Machines/6_pack_stepstick_v1.h b/Grbl_Esp32/src/Machines/6_pack_stepstick_v1.h
index 12371a8f..6c10c6e0 100644
--- a/Grbl_Esp32/src/Machines/6_pack_stepstick_v1.h
+++ b/Grbl_Esp32/src/Machines/6_pack_stepstick_v1.h
@@ -123,9 +123,9 @@
// RS485 In socket #3
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
-#define HUANYANG_TXD_PIN GPIO_NUM_26
-#define HUANYANG_RTS_PIN GPIO_NUM_4
-#define HUANYANG_RXD_PIN GPIO_NUM_16
+#define VFD_RS485_TXD_PIN GPIO_NUM_26
+#define VFD_RS485_RTS_PIN GPIO_NUM_4
+#define VFD_RS485_RXD_PIN GPIO_NUM_16
diff --git a/Grbl_Esp32/src/Report.cpp b/Grbl_Esp32/src/Report.cpp
index 19c98eda..d8c9d720 100644
--- a/Grbl_Esp32/src/Report.cpp
+++ b/Grbl_Esp32/src/Report.cpp
@@ -386,7 +386,7 @@ void report_gcode_modes(uint8_t client) {
strcat(modes_rpt, temp);
sprintf(temp, report_inches->get() ? " F%.1f" : " F%.0f", gc_state.feed_rate);
strcat(modes_rpt, temp);
- sprintf(temp, " S%4.3f", gc_state.spindle_speed);
+ sprintf(temp, " S%d", uint32_t(gc_state.spindle_speed));
strcat(modes_rpt, temp);
strcat(modes_rpt, "]\r\n");
grbl_send(client, modes_rpt);
diff --git a/Grbl_Esp32/src/SettingsDefinitions.cpp b/Grbl_Esp32/src/SettingsDefinitions.cpp
index d9b712a9..bea866aa 100644
--- a/Grbl_Esp32/src/SettingsDefinitions.cpp
+++ b/Grbl_Esp32/src/SettingsDefinitions.cpp
@@ -62,6 +62,7 @@ enum_opt_t spindleTypes = {
{ "HUANYANG", SPINDLE_TYPE_HUANYANG },
{ "BESC", SPINDLE_TYPE_BESC },
{ "10V", SPINDLE_TYPE_10V },
+ { "H2A", SPINDLE_TYPE_H2A },
// clang-format on
};
diff --git a/Grbl_Esp32/src/Spindles/H2ASpindle.cpp b/Grbl_Esp32/src/Spindles/H2ASpindle.cpp
new file mode 100644
index 00000000..9da1a871
--- /dev/null
+++ b/Grbl_Esp32/src/Spindles/H2ASpindle.cpp
@@ -0,0 +1,148 @@
+#include "H2ASpindle.h"
+
+/*
+ H2ASpindle.cpp
+
+ This is for the new H2A H2A VFD based spindle via RS485 Modbus.
+
+ Part of Grbl_ESP32
+ 2020 - Stefan de Bruijn
+
+ Grbl is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Grbl. If not, see .
+
+ WARNING!!!!
+ VFDs are very dangerous. They have high voltages and are very powerful
+ Remove power before changing bits.
+
+ The documentation is okay once you get how it works, but unfortunately
+ incomplete... See H2ASpindle.md for the remainder of the docs that I
+ managed to piece together.
+*/
+
+#include
+
+namespace Spindles {
+ void H2A::default_modbus_settings(uart_config_t& uart) {
+ // sets the uart to 19200 8E1
+ VFD::default_modbus_settings(uart);
+
+ uart.baud_rate = 19200;
+ uart.data_bits = UART_DATA_8_BITS;
+ uart.parity = UART_PARITY_EVEN;
+ uart.stop_bits = UART_STOP_BITS_1;
+ }
+
+ void H2A::direction_command(uint8_t mode, ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 6;
+
+ data.msg[1] = 0x06; // WRITE
+ data.msg[2] = 0x20; // Command ID 0x2000
+ data.msg[3] = 0x00;
+ data.msg[4] = 0x00;
+ data.msg[5] = (mode == SPINDLE_ENABLE_CCW) ? 0x02 : (mode == SPINDLE_ENABLE_CW ? 0x01 : 0x06);
+ }
+
+ void H2A::set_speed_command(uint32_t rpm, ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 6;
+
+ // We have to know the max RPM before we can set the current RPM:
+ auto max_rpm = this->_max_rpm;
+
+ // Speed is in [0..10'000] where 10'000 = 100%.
+ // We have to use a 32-bit integer here; typical values are 10k/24k rpm.
+ // I've never seen a 400K RPM spindle in my life, and they aren't supported
+ // by this modbus protocol anyways... So I guess this is OK.
+ uint16_t speed = (uint32_t(rpm) * 10000L) / uint32_t(max_rpm);
+ if (speed < 0) {
+ speed = 0;
+ }
+ if (speed > 10000) {
+ speed = 10000;
+ }
+
+ data.msg[1] = 0x06; // WRITE
+ data.msg[2] = 0x10; // Command ID 0x1000
+ data.msg[3] = 0x00;
+ data.msg[4] = uint8_t(speed >> 8); // RPM
+ data.msg[5] = uint8_t(speed & 0xFF);
+ }
+
+ H2A::response_parser H2A::get_max_rpm(ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 8;
+
+ // Send: 01 03 B005 0002
+ data.msg[1] = 0x03; // READ
+ data.msg[2] = 0xB0; // B0.05 = Get RPM
+ data.msg[3] = 0x05;
+ data.msg[4] = 0x00; // Read 2 values
+ data.msg[5] = 0x02;
+
+ // Recv: 01 03 00 04 5D C0 03 F6
+ // -- -- = 24000 (val #1)
+ return [](const uint8_t* response, Spindles::VFD* vfd) -> bool {
+ uint16_t rpm = (uint16_t(response[4]) << 8) | uint16_t(response[5]);
+ vfd->_max_rpm = rpm;
+
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "H2A spindle is initialized at %d RPM", int(rpm));
+
+ return true;
+ };
+ }
+
+ H2A::response_parser H2A::get_current_rpm(ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 8;
+
+ // Send: 01 03 700C 0002
+ data.msg[1] = 0x03; // READ
+ data.msg[2] = 0x70; // B0.05 = Get RPM
+ data.msg[3] = 0x0C;
+ data.msg[4] = 0x00; // Read 2 values
+ data.msg[5] = 0x02;
+
+ // Recv: 01 03 0004 095D 0000
+ // ---- = 2397 (val #1)
+
+ // TODO: What are we going to do with this? Update sys.spindle_speed? Update vfd state?
+ return [](const uint8_t* response, Spindles::VFD* vfd) -> bool {
+ uint16_t rpm = (uint16_t(response[4]) << 8) | uint16_t(response[5]);
+ // Set current RPM value? Somewhere?
+ return true;
+ };
+ }
+
+ H2A::response_parser H2A::get_current_direction(ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 6;
+
+ // Send: 01 03 30 00 00 01
+ data.msg[1] = 0x03; // READ
+ data.msg[2] = 0x30; // Command group ID
+ data.msg[3] = 0x00;
+ data.msg[4] = 0x00; // Message ID
+ data.msg[5] = 0x01;
+
+ // Receive: 01 03 00 02 00 02
+ // ----- status
+
+ // TODO: What are we going to do with this? Update sys.spindle_speed? Update vfd state?
+ return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; };
+ }
+}
diff --git a/Grbl_Esp32/src/Spindles/H2ASpindle.h b/Grbl_Esp32/src/Spindles/H2ASpindle.h
new file mode 100644
index 00000000..d022e421
--- /dev/null
+++ b/Grbl_Esp32/src/Spindles/H2ASpindle.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "VFDSpindle.h"
+
+/*
+ H2ASpindle.h
+
+ Part of Grbl_ESP32
+ 2020 - Stefan de Bruijn
+
+ Grbl is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Grbl. If not, see .
+
+*/
+
+namespace Spindles {
+ class H2A : public VFD {
+ protected:
+ void default_modbus_settings(uart_config_t& uart) override;
+
+ void direction_command(uint8_t mode, ModbusCommand& data) override;
+ void set_speed_command(uint32_t rpm, ModbusCommand& data) override;
+
+ response_parser get_max_rpm(ModbusCommand& data) override;
+ response_parser get_current_rpm(ModbusCommand& data) override;
+ response_parser get_current_direction(ModbusCommand& data) override;
+ response_parser get_status_ok(ModbusCommand& data) override { return nullptr; }
+ };
+}
diff --git a/Grbl_Esp32/src/Spindles/H2ASpindle.md b/Grbl_Esp32/src/Spindles/H2ASpindle.md
new file mode 100644
index 00000000..ce5d07da
--- /dev/null
+++ b/Grbl_Esp32/src/Spindles/H2ASpindle.md
@@ -0,0 +1,148 @@
+# P2A/P2B/P2C VFD protocol
+
+The H2A/H2B/H2C user manual can be quite confusing at times about
+how the RS485 protocol should be implemented.
+
+First off, wiring. Use a MAX-485 and wire it like this:
+
+- GND = GND of Arduino
+- A = RS+485
+- B = RS-485
+- VCC = 5V of Arduino
+- DI = TX (can be any D pin, 3.3V or 5V)
+- DE = Wire to RE and some pin (can be any D pin, 3.3V or 5V)
+- RE
+- RO = RX (can be any D pin, 3.3V or 5V)
+
+An ESP32 cannot handle 5V unfortunately. So you either have to use
+a voltage level converter for that, or use a 3.3V RS485 board.
+
+## VFD settings
+
+**ALWAYS** read the manual for VFD's! This is imperative to
+get motor speed etc. all correct. Also, you need to set
+certain settings to get RS485 working correctly, most notably:
+
+- F0.02 = 7 (use rs485)
+- F0.04 = 2 (use rs485)
+- F0.09 = 4 (use rs485)
+- F9.00 = 4 (19200 baud)
+- F9.01 = 0 (this is 8,N,1 parity, for SoftwareSerial) or if you use
+ HardwareSerial (like me) pick whatever suits you. Note the DUE doesn't
+ support SoftwareSerial.
+- F9.02 = 1 (address)
+- F9.05 = 0 (non-std modbus, 1 = std modbus, 2 = ascii)
+- F9.07 = 0 (write ops responded)
+
+Note that 19200,8N1 is more than enough for anything you want
+to throw at a VFD. High baud rates will just get you more
+errors -- BUT you need a high baud rate to keep Marlin happy.
+It's a bit of a trade-off...
+
+Also note 8N1. If you're using SoftwareSerial, you have no options
+and have to use 8N1. If you use Hardware serial (like on a DUE),
+you should set 8E1 (which is the VFD default).
+
+## CRC
+
+The CRC check should be implemented like in the document. They
+often mix up Big Endian and Little Endian unfortunately. The way
+it should be ordered is best described by an example:
+
+ 01: Address of device, usually 1
+ 06: 03 = read, 06 = write, 07 = command
+ 20: Byte 1 of 0x2000 (set command)
+ 00: Byte 2 of 0x2000
+ 00: Byte 1 of 0x0002 (rev run)
+ 02: Byte 2 of 0x0002
+ 03: crc_value & 0xFF
+ CB: (crc_value >> 8) & 0xFF
+
+For reference, I'll just write this packet down as follows:
+`01.06.2000.0002`
+
+# Commands
+
+Most commands simply work with an ID (which they call address)
+to make it confusing, and the number of return values you would
+like to have. For example:
+
+ Send: 01 03 3000 0004
+ // Command 0x3000, number of results = 4
+ Recv: 01 03 0008 0002 0000 0000 0000 D285
+ #bytes #1 #2 #3 #4 CRC
+
+If you want to query the current [running] status, that's
+command 0x3000, and the status is 1 byte, so you might as
+well add `0001` as parameter. There are exceptions here,
+obviously when writing data to the EEPROM or the speed.
+
+I hereby list the most important command sequences, and how
+they work:
+
+## Initialization & status
+
+Get current status: `01 03 3000 0001`. The receive data
+contains 1 value, namely the status. This is an enum with
+the following values:
+
+- 03 = idle
+- 01 = forward running
+- 02 = reverse running
+
+Example:
+
+ Send: 01 03 30 00 00 01 8B 0A
+ Recv: 01 03 00 02 00 02 65 CB
+
+
+Get max RPM (b0.05): `01 03 B005 0002`
+
+Example:
+
+ Max RPM: b0.05
+ Send: 01 03 B0 05 00 02 F2 CA
+ Recv: 01 03 00 04 5D C0 03 F6 D0 21
+ -- -- = 24000
+
+
+Get current RPM (d0.12): `01 03 700C 0002`
+
+Note that current RPM doesn't translate 1:1 to a percentage.
+So, when setting 10% of the max RPM on a 24.000 rpm spindle,
+we would get:
+
+ Send: 01 03 700C 0002 1EC8
+ Recv: 01 03 0004 095D 0000 D149
+ ---- 2397 RPM (~ 10%)
+
+## Running
+
+Forward run: `01 06 2000 0001`
+Backward run: `01 06 2000 0002`
+Stop: `01 06 2000 0006`
+
+Set speed: `01 06 1000 xxxx`
+where xxxx is the speed in 1/100 percent of the max. So,
+that means a value of 10000 is the max speed (`27 10`)
+and a value of 00000 is the min speed (`00 00`).
+
+Note that `01 06` will return the original command, so
+for example:
+
+ Send: 01 06 20 00 00 01 43 CA
+ Recv: 01 06 20 00 00 01 43 CA
+
+# Implementation details
+
+If sending and receiving collides, the checksum will be
+corrupted. And even if it was received by the VFD, it might
+take time to execute. For example, if we have a FWD run, and
+we want to move to a REV run, this takes time. So, it's
+*ALWAYS* a good idea to send a command, and check if the
+value matches the live values.
+
+For example: if we set 1000 RPM, we want to check if the
+current running speed is 1000 RPM - and until this happens,
+we *definitely* want to wait and do nothing. Same for FWD,
+REV, STOP, etc.
diff --git a/Grbl_Esp32/src/Spindles/HuanyangSpindle.cpp b/Grbl_Esp32/src/Spindles/HuanyangSpindle.cpp
index 1dd2aa41..e7b4bf3f 100644
--- a/Grbl_Esp32/src/Spindles/HuanyangSpindle.cpp
+++ b/Grbl_Esp32/src/Spindles/HuanyangSpindle.cpp
@@ -1,3 +1,5 @@
+#include "HuanyangSpindle.h"
+
/*
HuanyangSpindle.cpp
@@ -6,7 +8,8 @@
VFD was a PITA. I am just trying to help the next person.
Part of Grbl_ESP32
- 2020 - Bart Dring
+ 2020 - Bart Dring
+ 2020 - Stefan de Bruijn
Grbl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -26,8 +29,8 @@
==============================================================================
If a user changes state or RPM level, the command to do that is sent. If
- the command is not responded to a message is sent to serial that there was
- a timeout. If the Grbl is in a critical state, an alarm will be generated and
+ the command is not responded to a message is sent to serial that there was
+ a timeout. If the Grbl is in a critical state, an alarm will be generated and
the machine stopped.
If there are no commands to execute, various status items will be polled. If there
@@ -67,7 +70,7 @@
=========================================================================
Commands
- ADDR CMD LEN DATA CRC
+ ADDR CMD LEN DATA CRC
0x01 0x03 0x01 0x01 0x31 0x88 Start spindle clockwise
0x01 0x03 0x01 0x08 0xF1 0x8E Stop spindle
0x01 0x03 0x01 0x11 0x30 0x44 Start spindle counter-clockwise
@@ -84,7 +87,7 @@
==========================================================================
- Setting RPM
+ Setting RPM
ADDR CMD LEN DATA CRC
0x01 0x05 0x02 0x09 0xC4 0xBF 0x0F Write Frequency (0x9C4 = 2500 = 25.00HZ)
@@ -104,362 +107,69 @@
0x01 0x04 0x03 0x07 0x00 0x00 CRC CRC // VFD Temp
Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above)
- TODO:
- Move CRC Calc to task to free up main task
-
-
*/
-#include "HuanyangSpindle.h"
#include
-#define HUANYANG_UART_PORT UART_NUM_2 // hard coded for this port right now
-#define ECHO_TEST_CTS UART_PIN_NO_CHANGE // CTS pin is not used
-#define HUANYANG_BUF_SIZE 127
-#define HUANYANG_QUEUE_SIZE 10 // numv\ber of commands that can be queued up.
-#define RESPONSE_WAIT_TICKS 50 // how long to wait for a response
-#define HUANYANG_MAX_MSG_SIZE 16 // more than enough for a modbus message
-#define HUANYANG_POLL_RATE 200 // in milliseconds betwwen commands
-
-// OK to change these
-// #define them in your machine definition file if you want different values
-#ifndef HUANYANG_ADDR
-# define HUANYANG_ADDR 0x01
-#endif
-
-#ifndef HUANYANG_BAUD_RATE
-# define HUANYANG_BAUD_RATE 9600 // PD164 setting
-#endif
-
namespace Spindles {
- // communication task and queue stuff
- typedef struct {
- uint8_t tx_length;
- uint8_t rx_length;
- bool critical;
- char msg[HUANYANG_MAX_MSG_SIZE];
- } hy_command_t;
+ void Huanyang::default_modbus_settings(uart_config_t& uart) {
+ // sets the uart to 9600 8N1
+ VFD::default_modbus_settings(uart);
- typedef enum : uint8_t {
- READ_SET_FREQ = 0, // The set frequency
- READ_OUTPUT_FREQ = 1, // The current operating frequency
- READ_OUTPUT_AMPS = 2, //
- READ_SET_RPM = 3, // This is the last requested freq even in off mode
- READ_DC_VOLTAGE = 4, //
- READ_AC_VOLTAGE = 5, //
- READ_CONT = 6, // counting value???
- READ_TEMP = 7, //
- } read_register_t;
+ // uart.baud_rate = 9600;
+ // Baud rate is set in the PD164 setting.
+ }
- QueueHandle_t hy_cmd_queue;
+ void Huanyang::direction_command(uint8_t mode, ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 4;
+ data.rx_length = 4;
- static TaskHandle_t vfd_cmdTaskHandle = 0;
+ // data.msg[0] is omitted (modbus address is filled in later)
+ data.msg[1] = 0x03;
+ data.msg[2] = 0x01;
- bool hy_ok = true;
-
- // The communications task
- void vfd_cmd_task(void* pvParameters) {
- static bool unresponsive = false; // to pop off a message once each time it becomes unresponsive
- uint8_t reg_item = 0x00;
- hy_command_t next_cmd;
- uint8_t rx_message[HUANYANG_MAX_MSG_SIZE];
-
- while (true) {
- if (xQueueReceive(hy_cmd_queue, &next_cmd, 0) == pdTRUE) {
- uart_flush(HUANYANG_UART_PORT);
- //report_hex_msg(next_cmd.msg, "Tx: ", next_cmd.tx_length);
- uart_write_bytes(HUANYANG_UART_PORT, next_cmd.msg, next_cmd.tx_length);
-
- uint16_t read_length = uart_read_bytes(HUANYANG_UART_PORT, rx_message, next_cmd.rx_length, RESPONSE_WAIT_TICKS);
-
- if (read_length < next_cmd.rx_length) {
- if (!unresponsive) {
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Spindle RS485 Unresponsive %d", read_length);
- if (next_cmd.critical) {
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Critical Spindle RS485 Unresponsive");
- system_set_exec_alarm(EXEC_ALARM_SPINDLE_CONTROL);
- }
- unresponsive = true;
- }
- } else {
- // success
- unresponsive = false;
- //report_hex_msg(rx_message, "Rx: ", read_length);
- uint32_t ret_value = ((uint32_t)rx_message[4] << 8) + rx_message[5];
- //grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Item:%d value:%05d ", rx_message[3], ret_value);
- }
-
- } else {
- Huanyang::read_value(reg_item); // only this appears to work all the time. Other registers are flakey.
- if (reg_item < 0x03)
- reg_item++;
- else {
- reg_item = 0x00;
- }
- }
- vTaskDelay(HUANYANG_POLL_RATE); // TODO: What is the best value here?
+ switch (mode) {
+ case SPINDLE_ENABLE_CW: data.msg[3] = 0x01; break;
+ case SPINDLE_ENABLE_CCW: data.msg[3] = 0x11; break;
+ default: // SPINDLE_DISABLE
+ data.msg[3] = 0x08;
+ break;
}
}
- // ================== Class methods ==================================
+ void Huanyang::set_speed_command(uint32_t rpm, ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 5;
+ data.rx_length = 5;
- void Huanyang::init() {
- hy_ok = true; // initialize
+ // data.msg[0] is omitted (modbus address is filled in later)
+ data.msg[1] = 0x05;
+ data.msg[2] = 0x02;
- // fail if required items are not defined
- if (!get_pins_and_settings()) {
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Huanyang spindle errors");
- return;
- }
+ uint16_t value = (uint16_t)(rpm * 100 / 60); // send Hz * 10 (Ex:1500 RPM = 25Hz .... Send 2500)
- if (!_task_running) { // init can happen many times, we only want to start one task
- hy_cmd_queue = xQueueCreate(HUANYANG_QUEUE_SIZE, sizeof(hy_command_t));
- xTaskCreatePinnedToCore(vfd_cmd_task, // task
- "vfd_cmdTaskHandle", // name for task
- 2048, // size of task stack
- NULL, // parameters
- 1, // priority
- &vfd_cmdTaskHandle,
- 0 // core
- );
- _task_running = true;
- }
-
- // this allows us to init() again later.
- // If you change certain settings, init() gets called agian
- uart_driver_delete(HUANYANG_UART_PORT);
-
- uart_config_t uart_config = {
- .baud_rate = HUANYANG_BAUD_RATE,
- .data_bits = UART_DATA_8_BITS,
- .parity = UART_PARITY_DISABLE,
- .stop_bits = UART_STOP_BITS_1,
- .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
- .rx_flow_ctrl_thresh = 122,
- };
-
- uart_param_config(HUANYANG_UART_PORT, &uart_config);
-
- uart_set_pin(HUANYANG_UART_PORT, _txd_pin, _rxd_pin, _rts_pin, UART_PIN_NO_CHANGE);
-
- uart_driver_install(HUANYANG_UART_PORT, HUANYANG_BUF_SIZE * 2, 0, 0, NULL, 0);
-
- uart_set_mode(HUANYANG_UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
-
- is_reversable = true; // these VFDs are always reversable
- use_delays = true;
-
- //
- _current_rpm = 0;
- _state = SPINDLE_DISABLE;
-
- config_message();
+ data.msg[3] = (value >> 8) & 0xFF;
+ data.msg[4] = (value & 0xFF);
}
- // Checks for all the required pin definitions
- // It returns a message for each missing pin
- // Returns true if all pins are defined.
- bool Huanyang::get_pins_and_settings() {
-#ifdef HUANYANG_TXD_PIN
- _txd_pin = HUANYANG_TXD_PIN;
-#else
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined HUANYANG_TXD_PIN");
- hy_ok = false;
-#endif
+ Huanyang::response_parser Huanyang::get_status_ok(ModbusCommand& data) {
+ // NOTE: data length is excluding the CRC16 checksum.
+ data.tx_length = 6;
+ data.rx_length = 6;
-#ifdef HUANYANG_RXD_PIN
- _rxd_pin = HUANYANG_RXD_PIN;
-#else
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined HUANYANG_RXD_PIN");
- hy_ok = false;
-#endif
+ // data.msg[0] is omitted (modbus address is filled in later)
+ data.msg[1] = 0x04;
+ data.msg[2] = 0x03;
+ data.msg[3] = reg;
+ data.msg[4] = 0x00;
+ data.msg[5] = 0x00;
-#ifdef HUANYANG_RTS_PIN
- _rts_pin = HUANYANG_RTS_PIN;
-#else
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined HUANYANG_RTS_PIN");
- hy_ok = false;
-#endif
-
- if (laser_mode->get()) {
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Huanyang spindle disabled in laser mode. Set $GCode/LaserMode=Off and restart");
- hy_ok = false;
- }
-
- _min_rpm = rpm_min->get();
- _max_rpm = rpm_max->get();
-
- return hy_ok;
- }
-
- void Huanyang::config_message() {
- grbl_msg_sendf(CLIENT_SERIAL,
- MSG_LEVEL_INFO,
- "Huanyang Tx:%s Rx:%s RTS:%s",
- pinName(_txd_pin).c_str(),
- pinName(_rxd_pin).c_str(),
- pinName(_rts_pin).c_str());
- }
-
- void Huanyang::set_state(uint8_t state, uint32_t rpm) {
- if (sys.abort)
- return; // Block during abort.
-
- bool critical = (sys.state == STATE_CYCLE || state != SPINDLE_DISABLE);
-
- if (_current_state != state) { // already at the desired state. This function gets called a lot.
- set_mode(state, critical); // critical if we are in a job
- set_rpm(rpm);
- if (state == SPINDLE_DISABLE) {
- sys.spindle_speed = 0;
- if (_current_state != state)
- mc_dwell(spindle_delay_spindown->get());
- } else {
- if (_current_state != state)
- mc_dwell(spindle_delay_spinup->get());
- }
+ if (reg < 0x03) {
+ reg++;
} else {
- if (_current_rpm != rpm)
- set_rpm(rpm);
+ reg = 0x00;
}
-
- _current_state = state; // store locally for faster get_state()
-
- sys.report_ovr_counter = 0; // Set to report change immediately
-
- return;
- }
-
- bool Huanyang::set_mode(uint8_t mode, bool critical) {
- if (!hy_ok)
- return false;
-
- hy_command_t mode_cmd;
-
- mode_cmd.tx_length = 6;
- mode_cmd.rx_length = 6;
-
- mode_cmd.msg[0] = HUANYANG_ADDR;
- mode_cmd.msg[1] = 0x03;
- mode_cmd.msg[2] = 0x01;
-
- if (mode == SPINDLE_ENABLE_CW)
- mode_cmd.msg[3] = 0x01;
- else if (mode == SPINDLE_ENABLE_CCW)
- mode_cmd.msg[3] = 0x11;
- else { //SPINDLE_DISABLE
- mode_cmd.msg[3] = 0x08;
-
- if (!xQueueReset(hy_cmd_queue)) {
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD spindle off, queue could not be reset");
- }
- }
-
- add_ModRTU_CRC(mode_cmd.msg, mode_cmd.rx_length);
-
- mode_cmd.critical = critical;
-
- if (xQueueSend(hy_cmd_queue, &mode_cmd, 0) != pdTRUE)
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD Queue Full");
-
- return true;
- }
-
- uint32_t Huanyang::set_rpm(uint32_t rpm) {
- if (!hy_ok)
- return 0;
-
- hy_command_t rpm_cmd;
-
- // apply override
- rpm = rpm * sys.spindle_speed_ovr / 100; // Scale by spindle speed override value (uint8_t percent)
-
- // apply limits
- if ((_min_rpm >= _max_rpm) || (rpm >= _max_rpm))
- rpm = _max_rpm;
- else if (rpm != 0 && rpm <= _min_rpm)
- rpm = _min_rpm;
-
- sys.spindle_speed = rpm;
-
- if (rpm == _current_rpm) // prevent setting same RPM twice
- return rpm;
-
- _current_rpm = rpm;
-
- // TODO add the speed modifiers override, linearization, etc.
-
- rpm_cmd.tx_length = 7;
- rpm_cmd.rx_length = 6;
-
- rpm_cmd.msg[0] = HUANYANG_ADDR;
- rpm_cmd.msg[1] = 0x05;
- rpm_cmd.msg[2] = 0x02;
-
- uint16_t data = (uint16_t)(rpm * 100 / 60); // send Hz * 10 (Ex:1500 RPM = 25Hz .... Send 2500)
-
- rpm_cmd.msg[3] = (data & 0xFF00) >> 8;
- rpm_cmd.msg[4] = (data & 0xFF);
-
- add_ModRTU_CRC(rpm_cmd.msg, rpm_cmd.tx_length);
-
- rpm_cmd.critical = false;
-
- if (xQueueSend(hy_cmd_queue, &rpm_cmd, 0) != pdTRUE)
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD Queue Full");
-
- return rpm;
- }
-
- // This appears to read the control register and will return an RPM running or not.
- void Huanyang::read_value(uint8_t reg) {
- uint16_t ret_value = 0;
- hy_command_t read_cmd;
- uint8_t rx_message[HUANYANG_MAX_MSG_SIZE];
-
- read_cmd.tx_length = 8;
- read_cmd.rx_length = 8;
-
- read_cmd.msg[0] = HUANYANG_ADDR;
- read_cmd.msg[1] = 0x04;
- read_cmd.msg[2] = 0x03;
-
- read_cmd.msg[3] = reg;
-
- read_cmd.msg[4] = 0x00;
- read_cmd.msg[5] = 0x00;
-
- read_cmd.critical = (sys.state == STATE_CYCLE); // only critical if running a job TBD.... maybe spindle on?
-
- add_ModRTU_CRC(read_cmd.msg, read_cmd.tx_length);
-
- if (xQueueSend(hy_cmd_queue, &read_cmd, 0) != pdTRUE)
- grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD Queue Full");
- }
-
- void Huanyang::stop() { set_mode(SPINDLE_DISABLE, false); }
-
- // state is cached rather than read right now to prevent delays
- uint8_t Huanyang::get_state() { return _state; }
-
- // Calculate the CRC on all of the byte except the last 2
- // It then added the CRC to those last 2 bytes
- // full_msg_len This is the length of the message including the 2 crc bytes
- // Source: https://ctlsys.com/support/how_to_compute_the_modbus_rtu_message_crc/
- void Huanyang::add_ModRTU_CRC(char* buf, int full_msg_len) {
- uint16_t crc = 0xFFFF;
- for (int pos = 0; pos < full_msg_len - 2; pos++) {
- crc ^= (uint16_t)buf[pos]; // XOR byte into least sig. byte of crc
- for (int i = 8; i != 0; i--) { // Loop over each bit
- if ((crc & 0x0001) != 0) { // If the LSB is set
- crc >>= 1; // Shift right and XOR 0xA001
- crc ^= 0xA001;
- } else // Else LSB is not set
- crc >>= 1; // Just shift right
- }
- }
- // add the calculated Crc to the message
- buf[full_msg_len - 1] = (crc & 0xFF00) >> 8;
- buf[full_msg_len - 2] = (crc & 0xFF);
+ return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; };
}
}
diff --git a/Grbl_Esp32/src/Spindles/HuanyangSpindle.h b/Grbl_Esp32/src/Spindles/HuanyangSpindle.h
index 6dc341e3..7aacc433 100644
--- a/Grbl_Esp32/src/Spindles/HuanyangSpindle.h
+++ b/Grbl_Esp32/src/Spindles/HuanyangSpindle.h
@@ -1,62 +1,38 @@
#pragma once
+#include "VFDSpindle.h"
+
/*
- HuanyangSpindle.h
+ HuanyangSpindle.h
- Part of Grbl_ESP32
- 2020 - Bart Dring
+ Part of Grbl_ESP32
+ 2020 - Bart Dring
+ 2020 - Stefan de Bruijn
- Grbl is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- Grbl is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with Grbl. If not, see .
+ Grbl is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Grbl. If not, see .
*/
-#include "Spindle.h"
namespace Spindles {
- class Huanyang : public Spindle {
+ class Huanyang : public VFD {
private:
- uint16_t ModRTU_CRC(char* buf, int len);
-
- bool set_mode(uint8_t mode, bool critical);
-
- bool get_pins_and_settings();
-
- uint32_t _current_rpm;
- uint8_t _txd_pin;
- uint8_t _rxd_pin;
- uint8_t _rts_pin;
- uint8_t _state;
- bool _task_running;
-
- public:
- Huanyang() : _task_running(false) {}
-
- Huanyang(const Huanyang&) = delete;
- Huanyang(Huanyang&&) = delete;
- Huanyang& operator=(const Huanyang&) = delete;
- Huanyang& operator=(Huanyang&&) = delete;
-
- void init();
- void config_message();
- void set_state(uint8_t state, uint32_t rpm);
- uint8_t get_state();
- uint32_t set_rpm(uint32_t rpm);
- void stop();
- static void read_value(uint8_t reg);
- static void add_ModRTU_CRC(char* buf, int full_msg_len);
-
- virtual ~Huanyang() {}
+ int reg;
protected:
- uint32_t _min_rpm;
- uint32_t _max_rpm;
+ void default_modbus_settings(uart_config_t& uart) override;
+
+ void direction_command(uint8_t mode, ModbusCommand& data) override;
+ void set_speed_command(uint32_t rpm, ModbusCommand& data) override;
+
+ response_parser get_status_ok(ModbusCommand& data) override;
};
}
diff --git a/Grbl_Esp32/src/Spindles/NullSpindle.cpp b/Grbl_Esp32/src/Spindles/NullSpindle.cpp
index 518e8f59..cad13e40 100644
--- a/Grbl_Esp32/src/Spindles/NullSpindle.cpp
+++ b/Grbl_Esp32/src/Spindles/NullSpindle.cpp
@@ -30,9 +30,15 @@ namespace Spindles {
use_delays = false;
config_message();
}
- uint32_t Null::set_rpm(uint32_t rpm) { return rpm; }
- void Null::set_state(uint8_t state, uint32_t rpm) {}
- uint8_t Null::get_state() { return (SPINDLE_STATE_DISABLE); }
- void Null::stop() {}
- void Null::config_message() { grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "No spindle"); }
+ uint32_t Null::set_rpm(uint32_t rpm) {
+ sys.spindle_speed = rpm;
+ return rpm;
+ }
+ void Null::set_state(uint8_t state, uint32_t rpm) {
+ _current_state = state;
+ sys.spindle_speed = rpm;
+ }
+ uint8_t Null::get_state() { return _current_state; }
+ void Null::stop() {}
+ void Null::config_message() { grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "No spindle"); }
}
diff --git a/Grbl_Esp32/src/Spindles/Spindle.cpp b/Grbl_Esp32/src/Spindles/Spindle.cpp
index cb96dd37..e971437c 100644
--- a/Grbl_Esp32/src/Spindles/Spindle.cpp
+++ b/Grbl_Esp32/src/Spindles/Spindle.cpp
@@ -5,7 +5,7 @@
Part of Grbl_ESP32
- 2020 - Bart Dring
+ 2020 - Bart Dring
Grbl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -35,6 +35,7 @@
#include "Laser.h"
#include "DacSpindle.h"
#include "HuanyangSpindle.h"
+#include "H2ASpindle.h"
#include "BESCSpindle.h"
#include "10vSpindle.h"
@@ -44,9 +45,10 @@ namespace Spindles {
Null null;
PWM pwm;
Relay relay;
- Laser laser;
+ Laser laser;
Dac dac;
Huanyang huanyang;
+ H2A h2a;
BESC besc;
_10v _10v;
@@ -59,6 +61,7 @@ namespace Spindles {
case SPINDLE_TYPE_HUANYANG: spindle = &huanyang; break;
case SPINDLE_TYPE_BESC: spindle = &besc; break;
case SPINDLE_TYPE_10V: spindle = &_10v; break;
+ case SPINDLE_TYPE_H2A: spindle = &h2a; break;
case SPINDLE_TYPE_NONE:
default: spindle = &null; break;
}
diff --git a/Grbl_Esp32/src/Spindles/Spindle.h b/Grbl_Esp32/src/Spindles/Spindle.h
index ba96ff49..d09ede2d 100644
--- a/Grbl_Esp32/src/Spindles/Spindle.h
+++ b/Grbl_Esp32/src/Spindles/Spindle.h
@@ -26,9 +26,9 @@
*/
-#define SPINDLE_STATE_DISABLE 0 // Must be zero.
-#define SPINDLE_STATE_CW bit(0)
-#define SPINDLE_STATE_CCW bit(1)
+#define SPINDLE_STATE_DISABLE 0 // Must be zero.
+#define SPINDLE_STATE_CW bit(4) // matches PL_COND_FLAG_SPINDLE_CW
+#define SPINDLE_STATE_CCW bit(5) // matches PL_COND_FLAG_SPINDLE_CCW
#define SPINDLE_TYPE_NONE 0
#define SPINDLE_TYPE_PWM 1
@@ -38,6 +38,7 @@
#define SPINDLE_TYPE_HUANYANG 5
#define SPINDLE_TYPE_BESC 6
#define SPINDLE_TYPE_10V 7
+#define SPINDLE_TYPE_H2A 8
#include "../Grbl.h"
#include
@@ -68,9 +69,9 @@ namespace Spindles {
virtual ~Spindle() {}
- bool is_reversable;
- bool use_delays; // will SpinUp and SpinDown delays be used.
- uint8_t _current_state;
+ bool is_reversable;
+ bool use_delays; // will SpinUp and SpinDown delays be used.
+ volatile uint8_t _current_state = SPINDLE_DISABLE;
static void select();
};
diff --git a/Grbl_Esp32/src/Spindles/VFDSpindle.cpp b/Grbl_Esp32/src/Spindles/VFDSpindle.cpp
new file mode 100644
index 00000000..46eea6c7
--- /dev/null
+++ b/Grbl_Esp32/src/Spindles/VFDSpindle.cpp
@@ -0,0 +1,466 @@
+/*
+ VFDSpindle.cpp
+
+ This is for a VFD based spindles via RS485 Modbus. The details of the
+ VFD protocol heavily depend on the VFD in question here. We have some
+ implementations, but if yours is not here, the place to start is the
+ manual. This VFD class implements the modbus functionality.
+
+ Part of Grbl_ESP32
+ 2020 - Bart Dring
+ 2020 - Stefan de Bruijn
+
+ Grbl is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Grbl. If not, see .
+
+ WARNING!!!!
+ VFDs are very dangerous. They have high voltages and are very powerful
+ Remove power before changing bits.
+
+ TODO:
+ - We can report spindle_state and rpm better with VFD's that support
+ either mode, register RPM or actual RPM.
+ - Destructor should break down the task.
+ - Move min/max RPM to protected members.
+
+*/
+#include "VFDSpindle.h"
+
+#define VFD_RS485_UART_PORT UART_NUM_2 // hard coded for this port right now
+#define ECHO_TEST_CTS UART_PIN_NO_CHANGE // CTS pin is not used
+#define VFD_RS485_BUF_SIZE 127
+#define VFD_RS485_QUEUE_SIZE 10 // numv\ber of commands that can be queued up.
+#define RESPONSE_WAIT_TICKS 50 // how long to wait for a response
+#define VFD_RS485_POLL_RATE 200 // in milliseconds between commands
+
+// OK to change these
+// #define them in your machine definition file if you want different values
+#ifndef VFD_RS485_ADDR
+# define VFD_RS485_ADDR 0x01
+#endif
+
+namespace Spindles {
+ QueueHandle_t VFD::vfd_cmd_queue = nullptr;
+ TaskHandle_t VFD::vfd_cmdTaskHandle = nullptr;
+
+ // The communications task
+ void VFD::vfd_cmd_task(void* pvParameters) {
+ static bool unresponsive = false; // to pop off a message once each time it becomes unresponsive
+ static int pollidx = 0;
+
+ VFD* instance = static_cast(pvParameters);
+ ModbusCommand next_cmd;
+ uint8_t rx_message[VFD_RS485_MAX_MSG_SIZE];
+
+ while (true) {
+ response_parser parser = nullptr;
+
+ next_cmd.msg[0] = VFD_RS485_ADDR; // Always default to this
+
+ // First check if we should ask the VFD for the max RPM value as part of the initialization. We
+ // should also query this is max_rpm is 0, because that means a previous initialization failed:
+ if (pollidx == 0 || (instance->_max_rpm == 0 && (parser = instance->get_max_rpm(next_cmd)) != nullptr)) {
+ pollidx = 1;
+ next_cmd.critical = true;
+ } else {
+ next_cmd.critical = false;
+ }
+
+ // If we don't have a parser, the queue goes first. During idle, we can grab a parser.
+ if (parser == nullptr && xQueueReceive(vfd_cmd_queue, &next_cmd, 0) != pdTRUE) {
+ // We poll in a cycle. Note that the switch will fall through unless we encounter a hit.
+ // The weakest form here is 'get_status_ok' which should be implemented if the rest fails.
+ switch (pollidx) {
+ case 1:
+ parser = instance->get_current_rpm(next_cmd);
+ if (parser) {
+ pollidx = 2;
+ break;
+ }
+ // fall through intentionally:
+ case 2:
+ parser = instance->get_current_direction(next_cmd);
+ if (parser) {
+ pollidx = 3;
+ break;
+ }
+ // fall through intentionally:
+ case 3:
+ parser = instance->get_status_ok(next_cmd);
+ pollidx = 1;
+
+ // we could complete this in case parser == nullptr with some ifs, but let's
+ // just keep it easy and wait an iteration.
+ break;
+ }
+
+ // If we have no parser, that means get_status_ok is not implemented (and we have
+ // nothing resting in our queue). Let's fall back on a simple continue.
+ if (parser == nullptr) {
+ vTaskDelay(VFD_RS485_POLL_RATE);
+ continue; // main while loop
+ }
+ }
+
+ {
+ // Grabbed the command. Add the CRC16 checksum:
+ auto crc16 = ModRTU_CRC(next_cmd.msg, next_cmd.tx_length);
+
+ next_cmd.tx_length += 2;
+ next_cmd.rx_length += 2;
+
+ // add the calculated Crc to the message
+ next_cmd.msg[next_cmd.tx_length - 1] = (crc16 & 0xFF00) >> 8;
+ next_cmd.msg[next_cmd.tx_length - 2] = (crc16 & 0xFF);
+
+#ifdef VFD_DEBUG_MODE
+ if (parser == nullptr) {
+ report_hex_msg(next_cmd.msg, "RS485 Tx: ", next_cmd.tx_length);
+ }
+#endif
+ }
+
+ // Assume for the worst, and retry...
+ int retry_count = 0;
+ for (; retry_count < MAX_RETRIES; ++retry_count) {
+ // Flush the UART and write the data:
+ uart_flush(VFD_RS485_UART_PORT);
+ uart_write_bytes(VFD_RS485_UART_PORT, reinterpret_cast(next_cmd.msg), next_cmd.tx_length);
+
+ // Read the response
+ uint16_t read_length = uart_read_bytes(VFD_RS485_UART_PORT, rx_message, next_cmd.rx_length, RESPONSE_WAIT_TICKS);
+
+ // Generate crc16 for the response:
+ auto crc16response = ModRTU_CRC(rx_message, next_cmd.rx_length - 2);
+
+ if (read_length == next_cmd.rx_length && // check expected length
+ rx_message[0] == VFD_RS485_ADDR && // check address
+ rx_message[read_length - 1] == (crc16response & 0xFF00) >> 8 && // check CRC byte 1
+ rx_message[read_length - 2] == (crc16response & 0xFF)) { // check CRC byte 1
+
+ // success
+ unresponsive = false;
+ retry_count = MAX_RETRIES + 1; // stop retry'ing
+
+ // Should we parse this?
+ if (parser != nullptr && !parser(rx_message, instance)) {
+#ifdef VFD_DEBUG_MODE
+ report_hex_msg(next_cmd.msg, "RS485 Tx: ", next_cmd.tx_length);
+ report_hex_msg(rx_message, "RS485 Rx: ", read_length);
+#endif
+
+ // Not succesful! Now what?
+ unresponsive = true;
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Spindle RS485 did not give a satisfying response");
+ }
+ } else {
+#ifdef VFD_DEBUG_MODE
+ report_hex_msg(next_cmd.msg, "RS485 Tx: ", next_cmd.tx_length);
+ report_hex_msg(rx_message, "RS485 Rx: ", read_length);
+
+ if (read_length != 0) {
+ if (rx_message[0] != VFD_RS485_ADDR) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 received message from other modbus device");
+ } else if (read_length != next_cmd.rx_length) {
+ grbl_msg_sendf(CLIENT_SERIAL,
+ MSG_LEVEL_INFO,
+ "RS485 received message of unexpected length; expected %d, got %d",
+ int(next_cmd.rx_length),
+ int(read_length));
+ } else {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 CRC check failed");
+ }
+ } else {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 No response");
+ }
+#endif
+
+ // Wait a bit before we retry. Set the delay to poll-rate. Not sure
+ // if we should use a different value...
+ vTaskDelay(VFD_RS485_POLL_RATE);
+ }
+ }
+
+ if (retry_count == MAX_RETRIES) {
+ if (!unresponsive) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Spindle RS485 Unresponsive %d", next_cmd.rx_length);
+ if (next_cmd.critical) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Critical Spindle RS485 Unresponsive");
+ system_set_exec_alarm(EXEC_ALARM_SPINDLE_CONTROL);
+ }
+ unresponsive = true;
+ }
+ }
+
+ vTaskDelay(VFD_RS485_POLL_RATE); // TODO: What is the best value here?
+ }
+ }
+
+ // ================== Class methods ==================================
+ void VFD::default_modbus_settings(uart_config_t& uart) {
+ // Default is 9600 8N1, which is sane for most VFD's:
+ uart.baud_rate = 9600;
+ uart.data_bits = UART_DATA_8_BITS;
+ uart.parity = UART_PARITY_DISABLE;
+ uart.stop_bits = UART_STOP_BITS_1;
+ }
+
+ void VFD::init() {
+ vfd_ok = false; // initialize
+
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Initializing RS485 VFD spindle");
+
+ // fail if required items are not defined
+ if (!get_pins_and_settings()) {
+ vfd_ok = false;
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 VFD spindle errors");
+ return;
+ }
+
+ // this allows us to init() again later.
+ // If you change certain settings, init() gets called agian
+ uart_driver_delete(VFD_RS485_UART_PORT);
+
+ uart_config_t uart_config;
+ default_modbus_settings(uart_config);
+
+ // Overwrite with user defined defines:
+#ifdef VFD_RS485_BAUD_RATE
+ uart_config.baud_rate = VFD_RS485_BAUD_RATE;
+#endif
+#ifdef VFD_RS485_PARITY
+ uart_config.parity = VFD_RS485_PARITY;
+#endif
+
+ uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
+ uart_config.rx_flow_ctrl_thresh = 122;
+
+ if (uart_param_config(VFD_RS485_UART_PORT, &uart_config) != ESP_OK) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 VFD uart parameters failed");
+ return;
+ }
+
+ if (uart_set_pin(VFD_RS485_UART_PORT, _txd_pin, _rxd_pin, _rts_pin, UART_PIN_NO_CHANGE) != ESP_OK) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 VFD uart pin config failed");
+ return;
+ }
+
+ if (uart_driver_install(VFD_RS485_UART_PORT, VFD_RS485_BUF_SIZE * 2, 0, 0, NULL, 0) != ESP_OK) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 VFD uart driver install failed");
+ return;
+ }
+
+ if (uart_set_mode(VFD_RS485_UART_PORT, UART_MODE_RS485_HALF_DUPLEX) != ESP_OK) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "RS485 VFD uart set half duplex failed");
+ return;
+ }
+
+ // Initialization is complete, so now it's okay to run the queue task:
+ if (!_task_running) { // init can happen many times, we only want to start one task
+ vfd_cmd_queue = xQueueCreate(VFD_RS485_QUEUE_SIZE, sizeof(ModbusCommand));
+ xTaskCreatePinnedToCore(vfd_cmd_task, // task
+ "vfd_cmdTaskHandle", // name for task
+ 2048, // size of task stack
+ this, // parameters
+ 1, // priority
+ &vfd_cmdTaskHandle,
+ 0 // core
+ );
+ _task_running = true;
+ }
+
+ is_reversable = true; // these VFDs are always reversable
+ use_delays = true;
+ vfd_ok = true;
+
+ // Initially we initialize this to 0; over time, we might poll better information from the VFD.
+ _current_rpm = 0;
+ _current_state = SPINDLE_DISABLE;
+
+ config_message();
+ }
+
+ // Checks for all the required pin definitions
+ // It returns a message for each missing pin
+ // Returns true if all pins are defined.
+ bool VFD::get_pins_and_settings() {
+ bool pins_settings_ok = true;
+
+#ifdef VFD_RS485_TXD_PIN
+ _txd_pin = VFD_RS485_TXD_PIN;
+#else
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined VFD_RS485_TXD_PIN");
+ pins_settings_ok = false;
+#endif
+
+#ifdef VFD_RS485_RXD_PIN
+ _rxd_pin = VFD_RS485_RXD_PIN;
+#else
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined VFD_RS485_RXD_PIN");
+ pins_settings_ok = false;
+#endif
+
+#ifdef VFD_RS485_RTS_PIN
+ _rts_pin = VFD_RS485_RTS_PIN;
+#else
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined VFD_RS485_RTS_PIN");
+ pins_settings_ok = false;
+#endif
+
+ if (laser_mode->get()) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD spindle disabled in laser mode. Set $GCode/LaserMode=Off and restart");
+ pins_settings_ok = false;
+ }
+
+ _min_rpm = rpm_min->get();
+ _max_rpm = rpm_max->get();
+
+ return pins_settings_ok;
+ }
+
+ void VFD::config_message() {
+ grbl_msg_sendf(CLIENT_SERIAL,
+ MSG_LEVEL_INFO,
+ "VFD RS485 Tx:%s Rx:%s RTS:%s",
+ pinName(_txd_pin).c_str(),
+ pinName(_rxd_pin).c_str(),
+ pinName(_rts_pin).c_str());
+ }
+
+ void VFD::set_state(uint8_t state, uint32_t rpm) {
+ if (sys.abort) {
+ return; // Block during abort.
+ }
+
+ bool critical = (sys.state == STATE_CYCLE || state != SPINDLE_DISABLE);
+
+ if (_current_state != state) { // already at the desired state. This function gets called a lot.
+ set_mode(state, critical); // critical if we are in a job
+ set_rpm(rpm);
+ if (state == SPINDLE_DISABLE) {
+ sys.spindle_speed = 0;
+ if (_current_state != state) {
+ mc_dwell(spindle_delay_spindown->get());
+ }
+ } else {
+ if (_current_state != state) {
+ mc_dwell(spindle_delay_spinup->get());
+ }
+ }
+ } else {
+ if (_current_rpm != rpm) {
+ set_rpm(rpm);
+ }
+ }
+
+ _current_state = state; // store locally for faster get_state()
+
+ sys.report_ovr_counter = 0; // Set to report change immediately
+
+ return;
+ }
+
+ bool VFD::set_mode(uint8_t mode, bool critical) {
+ if (!vfd_ok) {
+ return false;
+ }
+
+ ModbusCommand mode_cmd;
+ mode_cmd.msg[0] = VFD_RS485_ADDR;
+
+ direction_command(mode, mode_cmd);
+
+ if (mode == SPINDLE_DISABLE) {
+ if (!xQueueReset(vfd_cmd_queue)) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD spindle off, queue could not be reset");
+ }
+ }
+
+ mode_cmd.critical = critical;
+ _current_state = mode;
+
+ if (xQueueSend(vfd_cmd_queue, &mode_cmd, 0) != pdTRUE) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD Queue Full");
+ }
+
+ return true;
+ }
+
+ uint32_t VFD::set_rpm(uint32_t rpm) {
+ if (!vfd_ok) {
+ return 0;
+ }
+
+#ifdef VFD_DEBUG_MODE
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Setting spindle speed to %d rpm (%d, %d)", int(rpm), int(_min_rpm), int(_max_rpm));
+#endif
+
+ // apply override
+ rpm = rpm * sys.spindle_speed_ovr / 100; // Scale by spindle speed override value (uint8_t percent)
+
+ // apply limits
+ if ((_min_rpm >= _max_rpm) || (rpm >= _max_rpm)) {
+ rpm = _max_rpm;
+ } else if (rpm != 0 && rpm <= _min_rpm) {
+ rpm = _min_rpm;
+ }
+
+ sys.spindle_speed = rpm;
+
+ if (rpm == _current_rpm) { // prevent setting same RPM twice
+ return rpm;
+ }
+
+ _current_rpm = rpm;
+
+ // TODO add the speed modifiers override, linearization, etc.
+
+ ModbusCommand rpm_cmd;
+ rpm_cmd.msg[0] = VFD_RS485_ADDR;
+
+ set_speed_command(rpm, rpm_cmd);
+
+ rpm_cmd.critical = false;
+
+ if (xQueueSend(vfd_cmd_queue, &rpm_cmd, 0) != pdTRUE) {
+ grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "VFD Queue Full");
+ }
+
+ return rpm;
+ }
+
+ void VFD::stop() { set_mode(SPINDLE_DISABLE, false); }
+
+ // state is cached rather than read right now to prevent delays
+ uint8_t VFD::get_state() { return _current_state; }
+
+ // Calculate the CRC on all of the byte except the last 2
+ // It then added the CRC to those last 2 bytes
+ // full_msg_len This is the length of the message including the 2 crc bytes
+ // Source: https://ctlsys.com/support/how_to_compute_the_modbus_rtu_message_crc/
+ uint16_t VFD::ModRTU_CRC(uint8_t* buf, int msg_len) {
+ uint16_t crc = 0xFFFF;
+ for (int pos = 0; pos < msg_len; pos++) {
+ crc ^= uint16_t(buf[pos]); // XOR byte into least sig. byte of crc.
+
+ for (int i = 8; i != 0; i--) { // Loop over each bit
+ if ((crc & 0x0001) != 0) { // If the LSB is set
+ crc >>= 1; // Shift right and XOR 0xA001
+ crc ^= 0xA001;
+ } else { // Else LSB is not set
+ crc >>= 1; // Just shift right
+ }
+ }
+ }
+
+ return crc;
+ }
+}
diff --git a/Grbl_Esp32/src/Spindles/VFDSpindle.h b/Grbl_Esp32/src/Spindles/VFDSpindle.h
new file mode 100644
index 00000000..34daa9ba
--- /dev/null
+++ b/Grbl_Esp32/src/Spindles/VFDSpindle.h
@@ -0,0 +1,93 @@
+#pragma once
+
+/*
+ VFDSpindle.h
+
+ Part of Grbl_ESP32
+ 2020 - Bart Dring
+ 2020 - Stefan de Bruijn
+
+ Grbl is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Grbl is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Grbl. If not, see .
+*/
+#include "Spindle.h"
+
+#include
+
+namespace Spindles {
+
+ class VFD : public Spindle {
+ private:
+ static const int VFD_RS485_MAX_MSG_SIZE = 16; // more than enough for a modbus message
+ static const int MAX_RETRIES = 3; // otherwise the spindle is marked 'unresponsive'
+
+ bool set_mode(uint8_t mode, bool critical);
+ bool get_pins_and_settings();
+
+ uint8_t _txd_pin;
+ uint8_t _rxd_pin;
+ uint8_t _rts_pin;
+
+ uint32_t _current_rpm = 0;
+ bool _task_running = false;
+ bool vfd_ok = true;
+
+ static QueueHandle_t vfd_cmd_queue;
+ static TaskHandle_t vfd_cmdTaskHandle;
+ static void vfd_cmd_task(void* pvParameters);
+
+ static uint16_t ModRTU_CRC(uint8_t* buf, int msg_len);
+
+ protected:
+ struct ModbusCommand {
+ bool critical; // TODO SdB: change into `uint8_t critical : 1;`: We want more flags...
+
+ uint8_t tx_length;
+ uint8_t rx_length;
+ uint8_t msg[VFD_RS485_MAX_MSG_SIZE];
+ };
+
+ virtual void default_modbus_settings(uart_config_t& uart);
+
+ // Commands:
+ virtual void direction_command(uint8_t mode, ModbusCommand& data) = 0;
+ virtual void set_speed_command(uint32_t rpm, ModbusCommand& data) = 0;
+
+ // Commands that return the status. Returns nullptr if unavailable by this VFD (default):
+ using response_parser = bool (*)(const uint8_t* response, VFD* spindle);
+
+ virtual response_parser get_max_rpm(ModbusCommand& data) { return nullptr; }
+ virtual response_parser get_current_rpm(ModbusCommand& data) { return nullptr; }
+ virtual response_parser get_current_direction(ModbusCommand& data) { return nullptr; }
+ virtual response_parser get_status_ok(ModbusCommand& data) = 0;
+
+ public:
+ VFD() = default;
+ VFD(const VFD&) = delete;
+ VFD(VFD&&) = delete;
+ VFD& operator=(const VFD&) = delete;
+ VFD& operator=(VFD&&) = delete;
+
+ // TODO FIXME: Have to make these public because of the parsers.
+ // Should hide them and use a member function.
+ volatile uint32_t _min_rpm;
+ volatile uint32_t _max_rpm;
+
+ void init();
+ void config_message();
+ void set_state(uint8_t state, uint32_t rpm);
+ uint8_t get_state();
+ uint32_t set_rpm(uint32_t rpm);
+ void stop();
+
+ virtual ~VFD() {}
+ };
+}
diff --git a/generate_vcxproj.py b/generate_vcxproj.py
index 36b77907..d0662e8c 100644
--- a/generate_vcxproj.py
+++ b/generate_vcxproj.py
@@ -66,7 +66,7 @@ class Vcxproj:
' platformio --force run',
' platformio --force run -t clean',
' WIN32;_DEBUG;$(NMakePreprocessorDefinitions)',
- ' $(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\toolchain-xtensa32\\xtensa-esp32-elf\\include;$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\framework-arduinoespressif32\\cores\\esp32;$(NMakeIncludeSearchPath)',
+ ' $(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\framework-arduinoespressif32\\tools\\sdk\\include\\freertos;$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\toolchain-xtensa32\\xtensa-esp32-elf\\include;$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\framework-arduinoespressif32\\cores\\esp32;$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\framework-arduinoespressif32\\tools\\sdk\\include\\driver;$(NMakeIncludeSearchPath)',
' '
])