1
0
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:
Stefan de Bruijn
2020-08-16 22:20:35 +02:00
committed by GitHub
parent 4c0edf9e93
commit a436ddfece
16 changed files with 1070 additions and 410 deletions

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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
}; };

View 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; };
}
}

View 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; }
};
}

View 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.

View File

@@ -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);
} }
} }

View File

@@ -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;
}; };
} }

View File

@@ -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"); }
} }

View File

@@ -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;
} }

View File

@@ -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();
}; };

View 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;
}
}

View 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() {}
};
}

View File

@@ -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>'
]) ])