mirror of
https://github.com/bdring/Grbl_Esp32.git
synced 2025-09-02 19:02:35 +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 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
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
};
|
||||
|
||||
|
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
|
||||
|
||||
@@ -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
|
||||
@@ -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 <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 {
|
||||
// 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; };
|
||||
}
|
||||
}
|
||||
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
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"
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@@ -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"); }
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 <driver/dac.h>
|
||||
@@ -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();
|
||||
};
|
||||
|
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>',
|
||||
' <NMakeCleanCommandLine>platformio --force run -t clean</NMakeCleanCommandLine>',
|
||||
' <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>'
|
||||
])
|
||||
|
||||
|
Reference in New Issue
Block a user