mirror of
https://github.com/bdring/Grbl_Esp32.git
synced 2025-09-02 10:53:01 +02:00
Changed workings of VFD code, added H2A VFD (#544)
* Renamed Huanyang to VFD for H2A work * Fixed Huanyang spindle implementation again. Fixed includes in vcxproj generator * Changed the VFD implementation. Implemented H2A along the way. UNTESTED! * Fixed retry loop in VFD. Added SettingsDefinition. Fixed name conflict within GRBL (`init()`). * Added VFD_DEBUG_MODE. * Fixed usability of VFD_DEBUG_MODE. Added a TODO in the H2ASpindle code. Removed it from the test machine config * Fixed bug in VFD spindle: the uart should be set up first, before running the rs485 task. * Fixed bug in VFD_DEBUG_MODE output. Fixed bug in RX length of set_speed command for Huanyang VFD. * Fixed a bug in the spindle code with states. Also, VFD didn't update state correctly. Updated TODO/FIXME * Added some more functionality to the Null spindle, to aid testing purposes. Fixed report compatibility with vanilla grbl. Some values were reported in a slightly different format. * Fixed commands.h * Fixed review by Mitch Co-authored-by: Stefan de Bruijn <stefan@nubilosoft.com>
This commit is contained in:
71
Grbl_Esp32/src/Machines/3axis_rs485.h
Normal file
71
Grbl_Esp32/src/Machines/3axis_rs485.h
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
@@ -51,9 +51,9 @@
|
|||||||
|
|
||||||
|
|
||||||
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
|
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
|
||||||
#define HUANYANG_TXD_PIN GPIO_NUM_17
|
#define VFD_RS485_TXD_PIN GPIO_NUM_17
|
||||||
#define HUANYANG_RXD_PIN GPIO_NUM_4
|
#define VFD_RS485_RXD_PIN GPIO_NUM_4
|
||||||
#define HUANYANG_RTS_PIN GPIO_NUM_16
|
#define VFD_RS485_RTS_PIN GPIO_NUM_16
|
||||||
|
|
||||||
#define X_LIMIT_PIN GPIO_NUM_34
|
#define X_LIMIT_PIN GPIO_NUM_34
|
||||||
#define Y_LIMIT_PIN GPIO_NUM_35
|
#define Y_LIMIT_PIN GPIO_NUM_35
|
||||||
|
@@ -123,9 +123,9 @@
|
|||||||
|
|
||||||
// RS485 In socket #3
|
// RS485 In socket #3
|
||||||
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
|
#define SPINDLE_TYPE SPINDLE_TYPE_HUANYANG // only one spindle at a time
|
||||||
#define HUANYANG_TXD_PIN GPIO_NUM_26
|
#define VFD_RS485_TXD_PIN GPIO_NUM_26
|
||||||
#define HUANYANG_RTS_PIN GPIO_NUM_4
|
#define VFD_RS485_RTS_PIN GPIO_NUM_4
|
||||||
#define HUANYANG_RXD_PIN GPIO_NUM_16
|
#define VFD_RS485_RXD_PIN GPIO_NUM_16
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -386,7 +386,7 @@ void report_gcode_modes(uint8_t client) {
|
|||||||
strcat(modes_rpt, temp);
|
strcat(modes_rpt, temp);
|
||||||
sprintf(temp, report_inches->get() ? " F%.1f" : " F%.0f", gc_state.feed_rate);
|
sprintf(temp, report_inches->get() ? " F%.1f" : " F%.0f", gc_state.feed_rate);
|
||||||
strcat(modes_rpt, temp);
|
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, temp);
|
||||||
strcat(modes_rpt, "]\r\n");
|
strcat(modes_rpt, "]\r\n");
|
||||||
grbl_send(client, modes_rpt);
|
grbl_send(client, modes_rpt);
|
||||||
|
@@ -62,6 +62,7 @@ enum_opt_t spindleTypes = {
|
|||||||
{ "HUANYANG", SPINDLE_TYPE_HUANYANG },
|
{ "HUANYANG", SPINDLE_TYPE_HUANYANG },
|
||||||
{ "BESC", SPINDLE_TYPE_BESC },
|
{ "BESC", SPINDLE_TYPE_BESC },
|
||||||
{ "10V", SPINDLE_TYPE_10V },
|
{ "10V", SPINDLE_TYPE_10V },
|
||||||
|
{ "H2A", SPINDLE_TYPE_H2A },
|
||||||
// clang-format on
|
// clang-format on
|
||||||
};
|
};
|
||||||
|
|
||||||
|
148
Grbl_Esp32/src/Spindles/H2ASpindle.cpp
Normal file
148
Grbl_Esp32/src/Spindles/H2ASpindle.cpp
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <driver/uart.h>
|
||||||
|
|
||||||
|
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; };
|
||||||
|
}
|
||||||
|
}
|
37
Grbl_Esp32/src/Spindles/H2ASpindle.h
Normal file
37
Grbl_Esp32/src/Spindles/H2ASpindle.h
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
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; }
|
||||||
|
};
|
||||||
|
}
|
148
Grbl_Esp32/src/Spindles/H2ASpindle.md
Normal file
148
Grbl_Esp32/src/Spindles/H2ASpindle.md
Normal file
@@ -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.
|
@@ -1,3 +1,5 @@
|
|||||||
|
#include "HuanyangSpindle.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HuanyangSpindle.cpp
|
HuanyangSpindle.cpp
|
||||||
|
|
||||||
@@ -6,7 +8,8 @@
|
|||||||
VFD was a PITA. I am just trying to help the next person.
|
VFD was a PITA. I am just trying to help the next person.
|
||||||
|
|
||||||
Part of Grbl_ESP32
|
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
|
Grbl is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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
|
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
|
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
|
a timeout. If the Grbl is in a critical state, an alarm will be generated and
|
||||||
the machine stopped.
|
the machine stopped.
|
||||||
|
|
||||||
If there are no commands to execute, various status items will be polled. If there
|
If there are no commands to execute, various status items will be polled. If there
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
=========================================================================
|
=========================================================================
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
ADDR CMD LEN DATA CRC
|
ADDR CMD LEN DATA CRC
|
||||||
0x01 0x03 0x01 0x01 0x31 0x88 Start spindle clockwise
|
0x01 0x03 0x01 0x01 0x31 0x88 Start spindle clockwise
|
||||||
0x01 0x03 0x01 0x08 0xF1 0x8E Stop spindle
|
0x01 0x03 0x01 0x08 0xF1 0x8E Stop spindle
|
||||||
0x01 0x03 0x01 0x11 0x30 0x44 Start spindle counter-clockwise
|
0x01 0x03 0x01 0x11 0x30 0x44 Start spindle counter-clockwise
|
||||||
@@ -84,7 +87,7 @@
|
|||||||
|
|
||||||
==========================================================================
|
==========================================================================
|
||||||
|
|
||||||
Setting RPM
|
Setting RPM
|
||||||
ADDR CMD LEN DATA CRC
|
ADDR CMD LEN DATA CRC
|
||||||
0x01 0x05 0x02 0x09 0xC4 0xBF 0x0F Write Frequency (0x9C4 = 2500 = 25.00HZ)
|
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
|
0x01 0x04 0x03 0x07 0x00 0x00 CRC CRC // VFD Temp
|
||||||
Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above)
|
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 <driver/uart.h>
|
#include <driver/uart.h>
|
||||||
|
|
||||||
#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 {
|
namespace Spindles {
|
||||||
// communication task and queue stuff
|
void Huanyang::default_modbus_settings(uart_config_t& uart) {
|
||||||
typedef struct {
|
// sets the uart to 9600 8N1
|
||||||
uint8_t tx_length;
|
VFD::default_modbus_settings(uart);
|
||||||
uint8_t rx_length;
|
|
||||||
bool critical;
|
|
||||||
char msg[HUANYANG_MAX_MSG_SIZE];
|
|
||||||
} hy_command_t;
|
|
||||||
|
|
||||||
typedef enum : uint8_t {
|
// uart.baud_rate = 9600;
|
||||||
READ_SET_FREQ = 0, // The set frequency
|
// Baud rate is set in the PD164 setting.
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
switch (mode) {
|
||||||
|
case SPINDLE_ENABLE_CW: data.msg[3] = 0x01; break;
|
||||||
// The communications task
|
case SPINDLE_ENABLE_CCW: data.msg[3] = 0x11; break;
|
||||||
void vfd_cmd_task(void* pvParameters) {
|
default: // SPINDLE_DISABLE
|
||||||
static bool unresponsive = false; // to pop off a message once each time it becomes unresponsive
|
data.msg[3] = 0x08;
|
||||||
uint8_t reg_item = 0x00;
|
break;
|
||||||
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?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== 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() {
|
// data.msg[0] is omitted (modbus address is filled in later)
|
||||||
hy_ok = true; // initialize
|
data.msg[1] = 0x05;
|
||||||
|
data.msg[2] = 0x02;
|
||||||
|
|
||||||
// fail if required items are not defined
|
uint16_t value = (uint16_t)(rpm * 100 / 60); // send Hz * 10 (Ex:1500 RPM = 25Hz .... Send 2500)
|
||||||
if (!get_pins_and_settings()) {
|
|
||||||
grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Huanyang spindle errors");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_task_running) { // init can happen many times, we only want to start one task
|
data.msg[3] = (value >> 8) & 0xFF;
|
||||||
hy_cmd_queue = xQueueCreate(HUANYANG_QUEUE_SIZE, sizeof(hy_command_t));
|
data.msg[4] = (value & 0xFF);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks for all the required pin definitions
|
Huanyang::response_parser Huanyang::get_status_ok(ModbusCommand& data) {
|
||||||
// It returns a message for each missing pin
|
// NOTE: data length is excluding the CRC16 checksum.
|
||||||
// Returns true if all pins are defined.
|
data.tx_length = 6;
|
||||||
bool Huanyang::get_pins_and_settings() {
|
data.rx_length = 6;
|
||||||
#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
|
|
||||||
|
|
||||||
#ifdef HUANYANG_RXD_PIN
|
// data.msg[0] is omitted (modbus address is filled in later)
|
||||||
_rxd_pin = HUANYANG_RXD_PIN;
|
data.msg[1] = 0x04;
|
||||||
#else
|
data.msg[2] = 0x03;
|
||||||
grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "Undefined HUANYANG_RXD_PIN");
|
data.msg[3] = reg;
|
||||||
hy_ok = false;
|
data.msg[4] = 0x00;
|
||||||
#endif
|
data.msg[5] = 0x00;
|
||||||
|
|
||||||
#ifdef HUANYANG_RTS_PIN
|
if (reg < 0x03) {
|
||||||
_rts_pin = HUANYANG_RTS_PIN;
|
reg++;
|
||||||
#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());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (_current_rpm != rpm)
|
reg = 0x00;
|
||||||
set_rpm(rpm);
|
|
||||||
}
|
}
|
||||||
|
return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; };
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,62 +1,38 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "VFDSpindle.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HuanyangSpindle.h
|
HuanyangSpindle.h
|
||||||
|
|
||||||
Part of Grbl_ESP32
|
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
|
Grbl is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
Grbl is distributed in the hope that it will be useful,
|
Grbl is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Grbl. If not, see <http://www.gnu.org/licenses/>.
|
along with Grbl. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
#include "Spindle.h"
|
|
||||||
|
|
||||||
namespace Spindles {
|
namespace Spindles {
|
||||||
class Huanyang : public Spindle {
|
class Huanyang : public VFD {
|
||||||
private:
|
private:
|
||||||
uint16_t ModRTU_CRC(char* buf, int len);
|
int reg;
|
||||||
|
|
||||||
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() {}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t _min_rpm;
|
void default_modbus_settings(uart_config_t& uart) override;
|
||||||
uint32_t _max_rpm;
|
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -30,9 +30,15 @@ namespace Spindles {
|
|||||||
use_delays = false;
|
use_delays = false;
|
||||||
config_message();
|
config_message();
|
||||||
}
|
}
|
||||||
uint32_t Null::set_rpm(uint32_t rpm) { return rpm; }
|
uint32_t Null::set_rpm(uint32_t rpm) {
|
||||||
void Null::set_state(uint8_t state, uint32_t rpm) {}
|
sys.spindle_speed = rpm;
|
||||||
uint8_t Null::get_state() { return (SPINDLE_STATE_DISABLE); }
|
return rpm;
|
||||||
void Null::stop() {}
|
}
|
||||||
void Null::config_message() { grbl_msg_sendf(CLIENT_SERIAL, MSG_LEVEL_INFO, "No spindle"); }
|
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"); }
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
Part of Grbl_ESP32
|
Part of Grbl_ESP32
|
||||||
|
|
||||||
2020 - Bart Dring
|
2020 - Bart Dring
|
||||||
|
|
||||||
Grbl is free software: you can redistribute it and/or modify
|
Grbl is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "Laser.h"
|
#include "Laser.h"
|
||||||
#include "DacSpindle.h"
|
#include "DacSpindle.h"
|
||||||
#include "HuanyangSpindle.h"
|
#include "HuanyangSpindle.h"
|
||||||
|
#include "H2ASpindle.h"
|
||||||
#include "BESCSpindle.h"
|
#include "BESCSpindle.h"
|
||||||
#include "10vSpindle.h"
|
#include "10vSpindle.h"
|
||||||
|
|
||||||
@@ -44,9 +45,10 @@ namespace Spindles {
|
|||||||
Null null;
|
Null null;
|
||||||
PWM pwm;
|
PWM pwm;
|
||||||
Relay relay;
|
Relay relay;
|
||||||
Laser laser;
|
Laser laser;
|
||||||
Dac dac;
|
Dac dac;
|
||||||
Huanyang huanyang;
|
Huanyang huanyang;
|
||||||
|
H2A h2a;
|
||||||
BESC besc;
|
BESC besc;
|
||||||
_10v _10v;
|
_10v _10v;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ namespace Spindles {
|
|||||||
case SPINDLE_TYPE_HUANYANG: spindle = &huanyang; break;
|
case SPINDLE_TYPE_HUANYANG: spindle = &huanyang; break;
|
||||||
case SPINDLE_TYPE_BESC: spindle = &besc; break;
|
case SPINDLE_TYPE_BESC: spindle = &besc; break;
|
||||||
case SPINDLE_TYPE_10V: spindle = &_10v; break;
|
case SPINDLE_TYPE_10V: spindle = &_10v; break;
|
||||||
|
case SPINDLE_TYPE_H2A: spindle = &h2a; break;
|
||||||
case SPINDLE_TYPE_NONE:
|
case SPINDLE_TYPE_NONE:
|
||||||
default: spindle = &null; break;
|
default: spindle = &null; break;
|
||||||
}
|
}
|
||||||
|
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define SPINDLE_STATE_DISABLE 0 // Must be zero.
|
#define SPINDLE_STATE_DISABLE 0 // Must be zero.
|
||||||
#define SPINDLE_STATE_CW bit(0)
|
#define SPINDLE_STATE_CW bit(4) // matches PL_COND_FLAG_SPINDLE_CW
|
||||||
#define SPINDLE_STATE_CCW bit(1)
|
#define SPINDLE_STATE_CCW bit(5) // matches PL_COND_FLAG_SPINDLE_CCW
|
||||||
|
|
||||||
#define SPINDLE_TYPE_NONE 0
|
#define SPINDLE_TYPE_NONE 0
|
||||||
#define SPINDLE_TYPE_PWM 1
|
#define SPINDLE_TYPE_PWM 1
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
#define SPINDLE_TYPE_HUANYANG 5
|
#define SPINDLE_TYPE_HUANYANG 5
|
||||||
#define SPINDLE_TYPE_BESC 6
|
#define SPINDLE_TYPE_BESC 6
|
||||||
#define SPINDLE_TYPE_10V 7
|
#define SPINDLE_TYPE_10V 7
|
||||||
|
#define SPINDLE_TYPE_H2A 8
|
||||||
|
|
||||||
#include "../Grbl.h"
|
#include "../Grbl.h"
|
||||||
#include <driver/dac.h>
|
#include <driver/dac.h>
|
||||||
@@ -68,9 +69,9 @@ namespace Spindles {
|
|||||||
|
|
||||||
virtual ~Spindle() {}
|
virtual ~Spindle() {}
|
||||||
|
|
||||||
bool is_reversable;
|
bool is_reversable;
|
||||||
bool use_delays; // will SpinUp and SpinDown delays be used.
|
bool use_delays; // will SpinUp and SpinDown delays be used.
|
||||||
uint8_t _current_state;
|
volatile uint8_t _current_state = SPINDLE_DISABLE;
|
||||||
|
|
||||||
static void select();
|
static void select();
|
||||||
};
|
};
|
||||||
|
466
Grbl_Esp32/src/Spindles/VFDSpindle.cpp
Normal file
466
Grbl_Esp32/src/Spindles/VFDSpindle.cpp
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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<VFD*>(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<const char*>(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;
|
||||||
|
}
|
||||||
|
}
|
93
Grbl_Esp32/src/Spindles/VFDSpindle.h
Normal file
93
Grbl_Esp32/src/Spindles/VFDSpindle.h
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "Spindle.h"
|
||||||
|
|
||||||
|
#include <driver/uart.h>
|
||||||
|
|
||||||
|
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() {}
|
||||||
|
};
|
||||||
|
}
|
@@ -66,7 +66,7 @@ class Vcxproj:
|
|||||||
' <NMakeBuildCommandLine>platformio --force run</NMakeBuildCommandLine>',
|
' <NMakeBuildCommandLine>platformio --force run</NMakeBuildCommandLine>',
|
||||||
' <NMakeCleanCommandLine>platformio --force run -t clean</NMakeCleanCommandLine>',
|
' <NMakeCleanCommandLine>platformio --force run -t clean</NMakeCleanCommandLine>',
|
||||||
' <NMakePreprocessorDefinitions>WIN32;_DEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>',
|
' <NMakePreprocessorDefinitions>WIN32;_DEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>',
|
||||||
' <NMakeIncludeSearchPath>$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\toolchain-xtensa32\\xtensa-esp32-elf\\include;$(HOMEDRIVE)$(HOMEPATH)\\.platformio\\packages\\framework-arduinoespressif32\\cores\\esp32;$(NMakeIncludeSearchPath)</NMakeIncludeSearchPath>',
|
' <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)</NMakeIncludeSearchPath>',
|
||||||
' </PropertyGroup>'
|
' </PropertyGroup>'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user