From 8e36c50349c06f001b120b2658cec9aeec154015 Mon Sep 17 00:00:00 2001 From: Stefan de Bruijn Date: Sun, 27 Oct 2024 17:42:36 +0100 Subject: [PATCH] VFD rework (#1324) * Fixed Generate_VCXProj * Moved VFD spindles to their own namespace and inheritance structure. TODO: Configuration. * Changed the factory a bit to ensure configurations still work. * This compiles. TODO: Loader script. * Changed names and made them consistent. This also removes them from IRAM. * Split out the VFDProtocol code into a separate file. * Added comment --------- Co-authored-by: Stefan de Bruijn --- FluidNC/src/Configuration/GenericFactory.h | 23 +- FluidNC/src/Platform.h | 1 + FluidNC/src/Spindles/DanfossSpindle.cpp | 157 ------- FluidNC/src/Spindles/DanfossSpindle.h | 83 ---- FluidNC/src/Spindles/H100Spindle.cpp | 177 -------- FluidNC/src/Spindles/H100Spindle.h | 31 -- FluidNC/src/Spindles/H2ASpindle.cpp | 133 ------ FluidNC/src/Spindles/H2ASpindle.h | 27 -- FluidNC/src/Spindles/HuanyangSpindle.cpp | 395 ----------------- FluidNC/src/Spindles/HuanyangSpindle.h | 32 -- FluidNC/src/Spindles/NowForeverSpindle.cpp | 263 ------------ FluidNC/src/Spindles/NowForeverSpindle.h | 27 -- FluidNC/src/Spindles/SiemensV20Spindle.h | 32 -- FluidNC/src/Spindles/Spindle.h | 7 +- .../Spindles/VFD/DanfossVLT2800Protocol.cpp | 167 ++++++++ .../src/Spindles/VFD/DanfossVLT2800Protocol.h | 83 ++++ FluidNC/src/Spindles/VFD/H100Protocol.cpp | 179 ++++++++ FluidNC/src/Spindles/VFD/H100Protocol.h | 30 ++ .../{H100Spindle.md => VFD/H100Protocol.md} | 0 FluidNC/src/Spindles/VFD/H2AProtocol.cpp | 136 ++++++ FluidNC/src/Spindles/VFD/H2AProtocol.h | 26 ++ .../{H2ASpindle.md => VFD/H2AProtocol.md} | 0 FluidNC/src/Spindles/VFD/HuanyangProtocol.cpp | 399 ++++++++++++++++++ FluidNC/src/Spindles/VFD/HuanyangProtocol.h | 31 ++ .../src/Spindles/VFD/NowForeverProtocol.cpp | 267 ++++++++++++ FluidNC/src/Spindles/VFD/NowForeverProtocol.h | 26 ++ .../SiemensV20Protocol.cpp} | 206 +++++---- FluidNC/src/Spindles/VFD/SiemensV20Protocol.h | 32 ++ FluidNC/src/Spindles/VFD/VFDProtocol.cpp | 291 +++++++++++++ FluidNC/src/Spindles/VFD/VFDProtocol.h | 81 ++++ FluidNC/src/Spindles/VFD/YL620Protocol.cpp | 231 ++++++++++ FluidNC/src/Spindles/VFD/YL620Protocol.h | 26 ++ FluidNC/src/Spindles/VFDSpindle.cpp | 367 +++------------- FluidNC/src/Spindles/VFDSpindle.h | 71 +--- FluidNC/src/Spindles/YL620Spindle.cpp | 227 ---------- FluidNC/src/Spindles/YL620Spindle.h | 27 -- generate_vcxproj.py | 8 +- 37 files changed, 2214 insertions(+), 2085 deletions(-) delete mode 100644 FluidNC/src/Spindles/DanfossSpindle.cpp delete mode 100644 FluidNC/src/Spindles/DanfossSpindle.h delete mode 100644 FluidNC/src/Spindles/H100Spindle.cpp delete mode 100644 FluidNC/src/Spindles/H100Spindle.h delete mode 100644 FluidNC/src/Spindles/H2ASpindle.cpp delete mode 100644 FluidNC/src/Spindles/H2ASpindle.h delete mode 100644 FluidNC/src/Spindles/HuanyangSpindle.cpp delete mode 100644 FluidNC/src/Spindles/HuanyangSpindle.h delete mode 100644 FluidNC/src/Spindles/NowForeverSpindle.cpp delete mode 100644 FluidNC/src/Spindles/NowForeverSpindle.h delete mode 100644 FluidNC/src/Spindles/SiemensV20Spindle.h create mode 100644 FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.h create mode 100644 FluidNC/src/Spindles/VFD/H100Protocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/H100Protocol.h rename FluidNC/src/Spindles/{H100Spindle.md => VFD/H100Protocol.md} (100%) create mode 100644 FluidNC/src/Spindles/VFD/H2AProtocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/H2AProtocol.h rename FluidNC/src/Spindles/{H2ASpindle.md => VFD/H2AProtocol.md} (100%) create mode 100644 FluidNC/src/Spindles/VFD/HuanyangProtocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/HuanyangProtocol.h create mode 100644 FluidNC/src/Spindles/VFD/NowForeverProtocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/NowForeverProtocol.h rename FluidNC/src/Spindles/{SiemensV20Spindle.cpp => VFD/SiemensV20Protocol.cpp} (58%) create mode 100644 FluidNC/src/Spindles/VFD/SiemensV20Protocol.h create mode 100644 FluidNC/src/Spindles/VFD/VFDProtocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/VFDProtocol.h create mode 100644 FluidNC/src/Spindles/VFD/YL620Protocol.cpp create mode 100644 FluidNC/src/Spindles/VFD/YL620Protocol.h delete mode 100644 FluidNC/src/Spindles/YL620Spindle.cpp delete mode 100644 FluidNC/src/Spindles/YL620Spindle.h diff --git a/FluidNC/src/Configuration/GenericFactory.h b/FluidNC/src/Configuration/GenericFactory.h index 0fb559a68..4e0c539d1 100644 --- a/FluidNC/src/Configuration/GenericFactory.h +++ b/FluidNC/src/Configuration/GenericFactory.h @@ -19,7 +19,7 @@ namespace Configuration { GenericFactory() = default; - GenericFactory(const GenericFactory&) = delete; + GenericFactory(const GenericFactory&) = delete; GenericFactory& operator=(const GenericFactory&) = delete; class BuilderBase { @@ -28,7 +28,7 @@ namespace Configuration { public: BuilderBase(const char* name) : name_(name) {} - BuilderBase(const BuilderBase& o) = delete; + BuilderBase(const BuilderBase& o) = delete; BuilderBase& operator=(const BuilderBase& o) = delete; virtual BaseType* create(const char* name) const = 0; @@ -59,6 +59,24 @@ namespace Configuration { BaseType* create(const char* name) const override { return new DerivedType(name); } }; + template + class DependentInstanceBuilder : public BuilderBase { + public: + explicit DependentInstanceBuilder(const char* name, bool autocreate = false) : BuilderBase(name) { + instance().registerBuilder(this); + if (autocreate) { + auto& objects = instance().objects_; + auto object = create(name); + objects.push_back(object); + } + } + + DerivedType* create(const char* name) const override { + auto dependency = new DependencyType(); + return new DerivedType(name, dependency); + } + }; + // This factory() method is used when there can be only one instance of the type, // as with a kinematics system. The variable that points to the instance must // be created externally and passed as an argument. @@ -75,6 +93,7 @@ namespace Configuration { handler.enterSection(inst->name(), inst); } } + // This factory() method is used when there can be multiple instances, // as with spindles and modules. A vector in the GenericFactory // singleton holds the derived type instances, so there is no need to diff --git a/FluidNC/src/Platform.h b/FluidNC/src/Platform.h index ca19f0852..6beec0fa9 100644 --- a/FluidNC/src/Platform.h +++ b/FluidNC/src/Platform.h @@ -9,5 +9,6 @@ #else # define WEAK_LINK +# define IRAM_ATTR #endif diff --git a/FluidNC/src/Spindles/DanfossSpindle.cpp b/FluidNC/src/Spindles/DanfossSpindle.cpp deleted file mode 100644 index 0ec931f96..000000000 --- a/FluidNC/src/Spindles/DanfossSpindle.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) 2024 - Jan Speckamp, whosmatt -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -/* - This is for a Danfoss VLT 2800 VFD based spindle to be controlled via RS485 Modbus RTU. - FluidNC imposes limitations (methods dont have access to full spindle state), while the Danfoss VFD expects the full state to be set with every command. - As an interim solution, the state of the spindle is cached in cachedSpindleState. - - Modbus setup of the VFD is covered in https://files.danfoss.com/download/Drives/doc_A_1_mg10s122.pdf - General setup of the VFD is covered in https://files.danfoss.com/download/Drives/doc_B_1_MG28E902.pdf -*/ - -#include "DanfossSpindle.h" - -#include - -#define READ_COIL 0x01 -#define READ_HR 0x03 - -#define WRITE_SINGLE_COIL 0x05 -#define WRITE_MULTIPLE_COIL 0x0F - -namespace Spindles { - void DanfossVLT2800::init() { - VFD::init(); - setupSpeeds(_maxFrequency); - } - - void IRAM_ATTR DanfossVLT2800::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { - // Cache received speed - cachedSpindleState.speed = dev_speed; - - // Write speed and direction from cache to VFD - writeVFDState(cachedSpindleState, data); - } - - void DanfossVLT2800::direction_command(SpindleState mode, ModbusCommand& data) { - // Cache received direction - cachedSpindleState.state = mode; - - // Write speed and direction from cache to VFD - writeVFDState(cachedSpindleState, data); - } - - VFD::response_parser DanfossVLT2800::get_current_speed(ModbusCommand& data) { - data.tx_length = 6; // including automatically set client_id, excluding crc - data.rx_length = 3 + 2; // excluding crc - - // We write a full control word instead of setting individual coils - data.msg[1] = READ_HR; - data.msg[2] = 0x14; - data.msg[3] = 0x3b; // start register - data.msg[4] = 0x00; - data.msg[5] = 0x01; // no of points - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - // const uint8_t slave_addr = response[0] - // const uint8_t function = response[1] - // const uint8_t response_byte_count = response[2] - - uint16_t freq = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - vfd->_sync_dev_speed = freq; - return true; - }; - } - - VFD::response_parser DanfossVLT2800::get_status_ok(ModbusCommand& data) { - data.tx_length = 6; // including automatically set client_id, excluding crc - data.rx_length = 5; // excluding crc - - // Read out current state - data.msg[1] = READ_COIL; - data.msg[2] = 0x00; - data.msg[3] = 0x20; // Coil index 32 - data.msg[4] = 0x00; - data.msg[5] = 0x10; // Read 16 Bits - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - SpindleStatus status; - status.statusWord = int16_t(response[3]) | uint16_t(response[4] << 8); // See DanfossSpindle.h for structure - -#ifdef DEBUG_VFD - log_debug("Control ready:" << status.flags.control_ready); - log_debug("Drive ready:" << status.flags.drive_ready); - log_debug("Coasting stop:" << status.flags.warning); - log_debug("Trip status:" << status.flags.trip); - log_debug("Trip lock:" << status.flags.trip_lock); - log_debug("No warning/warning:" << status.flags.warning); - log_debug("Speed == ref:" << status.flags.speed_status); - log_debug("Local operation/serial communication control:" << status.flags.local_control); - log_debug("Outside frequency range:" << status.flags.freq_range_err); - log_debug("Motor running:" << status.flags.motor_running); - log_debug("Not used:" << status.flags.voltage_warn); - log_debug("Voltage warn:" << status.flags.voltage_warn); - log_debug("Current limit:" << status.flags.current_limit); - log_debug("Thermal warn:" << status.flags.thermal_warn); -#endif - log_error("TODO: actually check status bits and output potential errors"); - return true; - }; - } - - // The VLT2800 expects speed, direction and enable to be sent together at all times. - // This function uses a combined cached spindle state that includes the speed and sends it to the VFD. - void DanfossVLT2800::writeVFDState(combinedSpindleState spindle, ModbusCommand& data) { - SpindleControl cword; - cword.flags.coasting_stop = 1; - cword.flags.dc_braking_stop = 1; - cword.flags.quick_stop = 1; - cword.flags.freeze_freq = 1; - cword.flags.jog = 0; - cword.flags.reference_preset = 0; - cword.flags.setup_preset = 0; - cword.flags.output_46 = 0; - cword.flags.relay_01 = 0; - cword.flags.data_valid = 1; - cword.flags.reset = 0; - cword.flags.reverse = 0; - - switch (spindle.state) { - case SpindleState::Cw: - cword.flags.reverse = 0; - cword.flags.start_stop = 1; - break; - case SpindleState::Ccw: - cword.flags.reverse = 1; - cword.flags.start_stop = 1; - break; - case SpindleState::Disable: - cword.flags.start_stop = 0; - break; - default: - break; - } - - // Assemble packet: - data.tx_length = 11; - data.rx_length = 6; - - // We write a full control word instead of setting individual coils - data.msg[1] = WRITE_MULTIPLE_COIL; - data.msg[2] = 0x00; - data.msg[3] = 0x00; // start coil address - data.msg[4] = 0x00; - data.msg[5] = 0x20; // write length - data.msg[6] = 0x04; // payload byte count - - data.msg[7] = cword.controlWord & 0xFF; // MSB - data.msg[8] = cword.controlWord >> 8; // LSB - - data.msg[9] = spindle.speed & 0xFF; - data.msg[10] = spindle.speed >> 8; - } - namespace { - SpindleFactory::InstanceBuilder registration("DanfossVLT2800"); - } -} diff --git a/FluidNC/src/Spindles/DanfossSpindle.h b/FluidNC/src/Spindles/DanfossSpindle.h deleted file mode 100644 index d898451e7..000000000 --- a/FluidNC/src/Spindles/DanfossSpindle.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2024 - Jan Speckamp, whosmatt -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class DanfossVLT2800 : public VFD { - union SpindleStatus { - struct { - bool control_ready : 1; // bit 00 = 0: "" | Bit = 1: "Control ready" - bool drive_ready : 1; // bit 01 = 0: "" | Bit = 1: "Drive ready" - bool coasting_stop : 1; // bit 02 = 0: "Coasting stop" | Bit = 1: "" - bool trip : 1; // bit 03 = 0: "No Trip" | Bit = 1: "Trip" - bool unused1 : 1; // bit 04 Not used - bool unused2 : 1; // bit 05 Not used - bool trip_lock : 1; // bit 06 = 0: "" | Bit = 1: "Trip lock" - bool warning : 1; // bit 07 = 0: "No warning" | Bit = 1: "Warning" - bool speed_status : 1; // bit 08 = 0: "Speed != ref." | Bit = 1: "Speed = ref." - bool local_control : 1; // bit 09 = 0: "Local control" | Bit = 1: "Ser. communi." - bool freq_range_err : 1; // bit 10 = 0: "Outside frequency range" | Bit = 1: "Frequency limit OK" - bool motor_running : 1; // bit 11 = 0: "" | Bit = 1: "Motor running" - bool unused3 : 1; // bit 12 Not used - bool voltage_warn : 1; // bit 13 = 0: "" | Bit = 1: "Voltage warn." - bool current_limit : 1; // bit 14 = 0: "" | Bit = 1: "Current limit" - bool thermal_warn : 1; // bit 15 = 0: "" | Bit = 1: "Thermal wan." - } flags; - uint16_t statusWord; - }; - - union SpindleControl { - struct { - uint8_t reference_preset : 2; // bit 00 = lsb of 2 bit value for preset reference selection - // bit 01 = msb - bool dc_braking_stop : 1; // bit 02 = 0 causes stop with dc brake - bool coasting_stop : 1; // bit 03 = 0 causes coasting stop - bool quick_stop : 1; // bit 04 = 0 causes quick stop - bool freeze_freq : 1; // bit 05 = 0 causes output frequency to be locked from inputs, stops still apply - bool start_stop : 1; // bit 06 = 1 causes motor start, 0 causes motor stop, standard ramp applies - bool reset : 1; // bit 07 = resets trip condition on change from 0 to 1 - bool jog : 1; // bit 08 = 1 switches to jogging (par. 213) - bool ramp_select : 1; // bit 09 = ramp selection: 0 = ramp 1 (par. 207-208), 1 = ramp 2 (par. 209-210) - bool data_valid : 1; // bit 10 = 0 causes entire control word to be ignored - bool relay_01 : 1; // bit 11 = 1 activates relay 01 - bool output_46 : 1; // bit 12 = 1 activates digital output on terminal 46 - uint8_t setup_preset : 2; // bit 13 = lsb of 2 bit value for setup selection when par. 004 multi setup is enabled - // bit 14 = msb - bool reverse : 1; // bit 15 = 1 causes reversing - } flags; - uint16_t controlWord; - }; - - protected: - // Unlike other VFDs, the VLT seems to take speed as int16_t, mapped to 0-200% of the configured maximum reference. - uint16_t _minFrequency = 0x0; // motor off (0% speed) - uint16_t _maxFrequency = 0x4000; // max speed the VFD will allow. 0x4000 = 100% for VLT2800 - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t rpm, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) { return nullptr; }; - response_parser get_current_speed(ModbusCommand& data) override; - response_parser get_current_direction(ModbusCommand& data) override { return nullptr; }; - response_parser get_status_ok(ModbusCommand& data) override; - - bool safety_polling() const override { return true; } - - public: - DanfossVLT2800(const char* name) : VFD(name) {} - void init(); - - private: - struct combinedSpindleState { - SpindleState state; - uint32_t speed; - } cachedSpindleState; - - void writeVFDState(combinedSpindleState spindle, ModbusCommand& data); - - void parse_spindle_status(uint16_t statusword, SpindleStatus& status); - }; -} diff --git a/FluidNC/src/Spindles/H100Spindle.cpp b/FluidNC/src/Spindles/H100Spindle.cpp deleted file mode 100644 index a7c8557bb..000000000 --- a/FluidNC/src/Spindles/H100Spindle.cpp +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -/* - H100Spindle.cpp - - This is for a H100 VFD based spindle via RS485 Modbus. - - WARNING!!!! - VFDs are very dangerous. They have high voltages and are very powerful - Remove power before changing bits. -*/ - -#include "H100Spindle.h" - -#include // std::max -#include // IRAM_ATTR - -namespace Spindles { - void H100Spindle::direction_command(SpindleState mode, ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 6; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x05; - data.msg[2] = 0x00; - - switch (mode) { - case SpindleState::Cw: //[01] [05] [00 49] [ff 00] -- forward run - data.msg[3] = 0x49; - data.msg[4] = 0xFF; - data.msg[5] = 0x00; - break; - case SpindleState::Ccw: //[01] [05] [00 4A] [ff 00] -- reverse run - data.msg[3] = 0x4A; - data.msg[4] = 0xFF; - data.msg[5] = 0x00; - break; - default: // SpindleState::Disable [01] [05] [00 4B] [ff 00] -- stop - data.msg[3] = 0x4B; - data.msg[4] = 0xFF; - data.msg[5] = 0x00; - break; - } - } - - void IRAM_ATTR H100Spindle::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 6; - - if (dev_speed != 0 && (dev_speed < _minFrequency || dev_speed > _maxFrequency)) { - log_warn(name() << " requested freq " << (dev_speed) << " is outside of range (" << _minFrequency << "," << _maxFrequency << ")"); - } - -#ifdef DEBUG_VFD - log_debug("Setting VFD dev_speed to " << dev_speed); -#endif - - //[01] [06] [0201] [07D0] Set frequency to [07D0] = 200.0 Hz. (2000 is written!) - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x06; // Set register command - data.msg[2] = 0x02; - data.msg[3] = 0x01; - //data.msg[4] = dev_speed >> 8; - BC 11/24/21 - data.msg[4] = dev_speed >> 8; - data.msg[5] = dev_speed & 0xFF; - } - - // This gets data from the VFD. It does not set any values - VFD::response_parser H100Spindle::initialization_sequence(int index, ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 5; - // Read F011 (min frequency) and F005 (max frequency): - // - // [03] [000B] [0001] gives [03] [02] [xxxx] (with 02 the result byte count). - // [03] [0005] [0001] gives [03] [02] [xxxx] (with 02 the result byte count). - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x03; // Read setting - data.msg[2] = 0x00; - // [3] = set below... - data.msg[4] = 0x00; // length - data.msg[5] = 0x01; - - if (index == -1) { - // Max frequency - data.msg[3] = 0x05; // PD005: max frequency the VFD will allow. Normally 400. - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[3] << 8) | response[4]; - -#ifdef DEBUG_VFD - log_debug("VFD: Max frequency = " << value / 10 << "Hz " << value / 10 * 60 << "RPM"); -#endif - log_info("VFD: Max speed:" << (value / 10 * 60) << "rpm"); - - // Set current RPM value? Somewhere? - auto h100 = static_cast(vfd); - h100->_maxFrequency = value; - - return true; - }; - - } else if (index == -2) { - // Min Frequency - data.msg[3] = 0x0B; // PD011: frequency lower limit. Normally 0. - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[3] << 8) | response[4]; - -#ifdef DEBUG_VFD - log_debug("VFD: Min frequency = " << value / 10 << "Hz " << value / 10 * 60 << "RPM"); -#endif - log_info("VFD: Min speed:" << (value / 10 * 60) << "rpm"); - - // Set current RPM value? Somewhere? - auto h100 = static_cast(vfd); - h100->_minFrequency = value; - - h100->updateRPM(); - - return true; - }; - } - - // Done. - return nullptr; - } - - void H100Spindle::updateRPM() { - if (_minFrequency > _maxFrequency) { - _minFrequency = _maxFrequency; - } - - if (_speeds.size() == 0) { - SpindleSpeed minRPM = _minFrequency * 60 / 10; - SpindleSpeed maxRPM = _maxFrequency * 60 / 10; - - shelfSpeeds(minRPM, maxRPM); - } - setupSpeeds(_maxFrequency); - _slop = std::max(_maxFrequency / 40, 1); - - log_info("VFD: VFD settings read: Freq range(" << _minFrequency << " , " << _maxFrequency << ")]"); - } - - VFD::response_parser H100Spindle::get_current_speed(ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - // [01] [04] [0000] [0002] -- output frequency - data.tx_length = 6; - data.rx_length = 7; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x04; - data.msg[2] = 0x00; - data.msg[3] = 0x00; // Output frequency - data.msg[4] = 0x00; - data.msg[5] = 0x02; - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - // 01 04 04 [freq 16] [set freq 16] [crc16] - uint16_t frequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - // Store speed for synchronization - vfd->_sync_dev_speed = frequency; - return true; - }; - } - - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("H100"); - } -} diff --git a/FluidNC/src/Spindles/H100Spindle.h b/FluidNC/src/Spindles/H100Spindle.h deleted file mode 100644 index 7f93e4461..000000000 --- a/FluidNC/src/Spindles/H100Spindle.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class H100Spindle : public VFD { - private: - int reg; - - protected: - uint16_t _minFrequency = 0; - uint16_t _maxFrequency = 4000; // H100 works with frequencies scaled by 10. - - void updateRPM(); - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t rpm, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } - response_parser get_current_speed(ModbusCommand& data) override; - - bool use_delay_settings() const override { return false; } - - public: - H100Spindle(const char* name) : VFD(name) {} - }; -} diff --git a/FluidNC/src/Spindles/H2ASpindle.cpp b/FluidNC/src/Spindles/H2ASpindle.cpp deleted file mode 100644 index 460bf4713..000000000 --- a/FluidNC/src/Spindles/H2ASpindle.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -/* - H2ASpindle.cpp - - This is for the new H2A H2A VFD based spindle via RS485 Modbus. - - 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 "H2ASpindle.h" - -namespace Spindles { - void H2A::direction_command(SpindleState mode, ModbusCommand& data) { - 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 == SpindleState::Ccw) ? 0x02 : (mode == SpindleState::Cw ? 0x01 : 0x06); - } - - void H2A::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { - // NOTE: H2A inverters are a-symmetrical. You set the speed in 1/100 - // percentages, and you get the speed in RPM. So, we need to convert - // the RPM using maxRPM to a percentage. See MD document for details. - // - // For the H2A VFD, the speed is read directly units of RPM, unlike many - // other VFDs where it is given in Hz times some scale factor. - data.tx_length = 6; - data.rx_length = 6; - - uint16_t speed = (uint32_t(dev_speed) * 10000L) / uint32_t(_maxRPM); - 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] = speed >> 8; - data.msg[5] = speed & 0xFF; - } - - VFD::response_parser H2A::initialization_sequence(int index, ModbusCommand& data) { - if (index == -1) { - 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 maxRPM = (uint16_t(response[4]) << 8) | uint16_t(response[5]); - - if (vfd->_speeds.size() == 0) { - vfd->shelfSpeeds(maxRPM / 4, maxRPM); - } - - vfd->setupSpeeds(maxRPM); // The speed is given directly in RPM - vfd->_slop = 300; // 300 RPM - - static_cast(vfd)->_maxRPM = uint32_t(maxRPM); - - log_info("H2A spindle initialized at " << maxRPM << " RPM"); - - return true; - }; - } else { - return nullptr; - } - } - - VFD::response_parser H2A::get_current_speed(ModbusCommand& data) { - 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 speed - data.msg[3] = 0x0C; - data.msg[4] = 0x00; // Read 2 values - data.msg[5] = 0x02; - - // Recv: 01 03 0004 095D 0000 - // ---- = 2397 (val #1) - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - vfd->_sync_dev_speed = (uint16_t(response[4]) << 8) | uint16_t(response[5]); - return true; - }; - } - - VFD::response_parser H2A::get_current_direction(ModbusCommand& data) { - 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 vfd state? - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; }; - } - - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("H2A"); - } -} diff --git a/FluidNC/src/Spindles/H2ASpindle.h b/FluidNC/src/Spindles/H2ASpindle.h deleted file mode 100644 index 4b2d65f16..000000000 --- a/FluidNC/src/Spindles/H2ASpindle.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class H2A : public VFD { - protected: - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t dev_speed, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_current_speed(ModbusCommand& data) override; - response_parser get_current_direction(ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } - - bool use_delay_settings() const override { return false; } - bool safety_polling() const override { return false; } - - uint32_t _maxRPM; - - public: - H2A(const char* name) : VFD(name) {} - }; -} diff --git a/FluidNC/src/Spindles/HuanyangSpindle.cpp b/FluidNC/src/Spindles/HuanyangSpindle.cpp deleted file mode 100644 index c2b1bd789..000000000 --- a/FluidNC/src/Spindles/HuanyangSpindle.cpp +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (c) 2020 - Bart Dring -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -/* - HuanyangSpindle.cpp - - This is for a Huanyang VFD based spindle via RS485 Modbus. - Sorry for the lengthy comments, but finding the details on this - VFD was a PITA. I am just trying to help the next person. - - WARNING!!!! - VFDs are very dangerous. They have high voltages and are very powerful - Remove power before changing bits. - - ============================================================================== - - If a user changes state or RPM level, the command to do that is sent. If - the command is not responded to a message is sent to serial that there was - a timeout. If the system is in a critical state, an alarm will be generated and - the machine stopped. - - If there are no commands to execute, various status items will be polled. If there - is no response, it will behave as described above. It will stop any running jobs with - an alarm. - - =============================================================================== - - Protocol Details - - A lot of good information about the details of all these parameters and how they should - be setup can be found on this page: - https://community.carbide3d.com/t/vfd-parameters-huanyang-model/15459/7 . - - Before using spindle, VFD must be setup for RS485 and match your spindle: - - PD004 400 Base frequency as rated on my spindle (default was 50) - PD005 400 Maximum frequency Hz (Typical for spindles) - PD011 120 Min Speed (Recommend Aircooled=120 Water=100) - PD014 10 Acceleration time (Test to optimize) - PD015 10 Deceleration time (Test to optimize) - PD023 1 Reverse run enabled - PD141 220 Spindle max rated voltage - PD142 3.7 Max current Amps (0.8kw=3.7 1.5kw=7.0, 2.2kw=??) - PD143 2 Poles most are 2 (I think this is only used for RPM calc from Hz) - PD144 3000 Max rated motor revolution at 50 Hz => 24000@400Hz = 3000@50HZ - - PD001 2 RS485 Control of run commands - PD002 2 RS485 Control of operating frequency - PD163 1 RS485 Address: 1 (Typical. OK to change...see below) - PD164 1 RS485 Baud rate: 9600 (Typical. OK to change...see below) - PD165 3 RS485 Mode: RTU, 8N1 - - The official documentation of the RS485 is horrible. I had to piece it together from - a lot of different sources: - - Manuals: https://github.com/RobertOlechowski/Huanyang_VFD/tree/master/Documentations/pdf - Reference: https://github.com/Smoothieware/Smoothieware/blob/edge/src/modules/tools/spindle/HuanyangSpindleControl.cpp - Refernece: https://gist.github.com/Bouni/803492ed0aab3f944066 - VFD settings: https://www.hobbytronics.co.za/Content/external/1159/Spindle_Settings.pdf - Spindle Talker 2 https://github.com/GilchristT/SpindleTalker2/releases - Python https://github.com/RobertOlechowski/Huanyang_VFD - - ========================================================================= - - Commands - ADDR CMD LEN DATA CRC - 0x01 0x03 0x01 0x01 0x31 0x88 Start spindle clockwise - 0x01 0x03 0x01 0x08 0xF1 0x8E Stop spindle - 0x01 0x03 0x01 0x11 0x30 0x44 Start spindle counter-clockwise - - Return values are - 0 = run - 1 = jog - 2 = r/f - 3 = running - 4 = jogging - 5 = r/f - 6 = Braking - 7 = Track start - - ========================================================================== - - Setting RPM - ADDR CMD LEN DATA CRC - 0x01 0x05 0x02 0x09 0xC4 0xBF 0x0F Write Frequency (0x9C4 = 2500 = 25.00HZ) - - Response is same as data sent - - ========================================================================== - - Setting registers - Addr Read Len Reg DataH DataL CRC CRC - 0x01 0x01 0x03 5 0x00 0x00 CRC CRC // PD005 - 0x01 0x01 0x03 11 0x00 0x00 CRC CRC // PD011 - 0x01 0x01 0x03 143 0x00 0x00 CRC CRC // PD143 - 0x01 0x01 0x03 144 0x00 0x00 CRC CRC // PD144 - - Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above) - - ========================================================================== - - Status registers - Addr Read Len Reg DataH DataL CRC CRC - 0x01 0x04 0x03 0x00 0x00 0x00 CRC CRC // Set Frequency * 100 (25Hz = 2500) - 0x01 0x04 0x03 0x01 0x00 0x00 CRC CRC // Ouput Frequency * 100 - 0x01 0x04 0x03 0x02 0x00 0x00 CRC CRC // Ouput Amps * 10 - 0x01 0x04 0x03 0x03 0x00 0x00 0xF0 0x4E // Read RPM (example CRC shown) - 0x01 0x04 0x03 0x0 0x00 0x00 CRC CRC // DC voltage - 0x01 0x04 0x03 0x05 0x00 0x00 CRC CRC // AC voltage - 0x01 0x04 0x03 0x06 0x00 0x00 CRC CRC // Cont - 0x01 0x04 0x03 0x07 0x00 0x00 CRC CRC // VFD Temp - - Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above) - - ========================================================================== - - The math: - - PD005 400 Maximum frequency Hz (Typical for spindles) - PD011 120 Min Speed (Recommend Aircooled=120 Water=100) - PD143 2 Poles most are 2 (I think this is only used for RPM calc from Hz) - PD144 3000 Max rated motor revolution at 50 Hz => 24000@400Hz = 3000@50HZ - - During initialization these 4 are pulled from the VFD registers. It then sets min and max RPM - of the spindle. So: - - MinRPM = PD011 * PD144 / 50 = 120 * 3000 / 50 = 7200 RPM min - MaxRPM = PD005 * PD144 / 50 = 400 * 3000 / 50 = 24000 RPM max - - If you then set 12000 RPM, it calculates the frequency: - - int targetFrequency = targetRPM * PD005 / MaxRPM = targetRPM * PD005 / (PD005 * PD144 / 50) = - targetRPM * 50 / PD144 = 12000 * 50 / 3000 = 200 - - If the frequency is -say- 25 Hz, Huanyang wants us to send 2500 (eg. 25.00 Hz). -*/ - -#include "HuanyangSpindle.h" - -#include // std::max - -namespace Spindles { - // Baud rate is set in the PD164 setting. If it is not 9600, add, for example, - // _baudrate = 19200; - - void Huanyang::direction_command(SpindleState mode, ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 4; - data.rx_length = 4; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x03; - data.msg[2] = 0x01; - - switch (mode) { - case SpindleState::Cw: - data.msg[3] = 0x01; - break; - case SpindleState::Ccw: - data.msg[3] = 0x11; - break; - default: // SpindleState::Disable - data.msg[3] = 0x08; - break; - } - } - - void IRAM_ATTR Huanyang::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { - // The units for setting Huanyang speed are Hz * 100. For a 2-pole motor, - // RPM is Hz * 60 sec/min. The maximum possible speed is 400 Hz so - // 400 * 60 = 24000 RPM. - - if (dev_speed != 0 && (dev_speed < _minFrequency || dev_speed > _maxFrequency)) { - log_warn(name() << " requested freq " << (dev_speed) << " is outside of range (" << _minFrequency << "," << _maxFrequency << ")"); - } - - // There is a configuration register PD144 that scales the display to show the - // RPMs. It is nominally set to 3000 (= 50Hz * 60) for a 2-pole motor or - // 1500 for a 4-pole motor, but it can be set slightly lower to account for - // slip - the torque-limited difference between actual spindle speed and the - // the frequency delivered to the motor windings. - - // Frequency comes from a conversion of revolutions per second to revolutions per minute - // (factor of 60) and a factor of 2 from counting the number of poles. E.g. rpm * 120 / 100. - - // int targetFrequency = targetRPM * PD005 / MaxRPM - // = targetRPM * PD005 / (PD005 * PD144 / 50) - // = targetRPM * 50 / PD144 - // - // Huanyang wants a factor 100 bigger numbers. So, 1500 rpm -> 25 HZ. Send 2500. - - // The conversion from RPM as requested by the GCode program and the speed number - // (nominally Hz * 100) is done in mapping code that is shared across all spindles. - // The dev_speed argument is precomputed by that code. - - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 5; - data.rx_length = 5; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x05; // Set register command - data.msg[2] = 0x02; // Register PD002 - main frequency in units of 0.01 Hz - data.msg[3] = dev_speed >> 8; - data.msg[4] = dev_speed & 0xFF; - } - - // This gets data from the VFS. It does not set any values - VFD::response_parser Huanyang::initialization_sequence(int index, ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 6; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x01; // Read setting - data.msg[2] = 0x03; // Len - // [3] = set below... - data.msg[4] = 0x00; - data.msg[5] = 0x00; - - switch (index) { - case -1: - data.msg[3] = 5; // PD005: max frequency the VFD will allow. Normally 400. - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[4] << 8) | response[5]; - - // Set current RPM value? Somewhere? - auto huanyang = static_cast(vfd); - huanyang->_maxFrequency = value; - return true; - }; - break; - case -2: - data.msg[3] = 11; // PD011: frequency lower limit. Normally 0. - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[4] << 8) | response[5]; - - // Set current RPM value? Somewhere? - auto huanyang = static_cast(vfd); - huanyang->_minFrequency = value; - - log_info(huanyang->name() << " PD0011, PD005 Freq range (" << (huanyang->_minFrequency / 100) << "," - << (huanyang->_maxFrequency / 100) << ") Hz" - << " (" << (huanyang->_minFrequency / 100 * 60) << "," << (huanyang->_maxFrequency / 100 * 60) - << ") RPM"); - - return true; - }; - break; - case -3: - data.msg[3] = 144; // PD144: max rated motor revolution at 50Hz => 24000@400Hz = 3000@50HZ - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[4] << 8) | response[5]; - - // Set current RPM value? Somewhere? - auto huanyang = static_cast(vfd); - huanyang->_maxRpmAt50Hz = value; - - log_info(huanyang->name() << " PD144 Rated RPM @ 50Hz:" << huanyang->_maxRpmAt50Hz); - - // Regarding PD144, the 2 versions of the manuals both say "This is set according to the - // actual revolution of the motor. The displayed value is the same as this set value. It - // can be used as a monitoring parameter, which is convenient to the user. This set value - // corresponds to the revolution at 50Hz". - - // Calculate the VFD settings: - huanyang->updateRPM(); - - return true; - }; - break; - case -4: - data.rx_length = 5; - data.msg[3] = 143; // PD143: 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint8_t value = response[4]; // Single byte response. - auto huanyang = static_cast(vfd); - // Sanity check. We expect something like 2 or 4 poles. - if (value <= 4 && value >= 2) { - // Set current RPM value? Somewhere? - - huanyang->_numberPoles = value; - - log_info(huanyang->name() << " PD143 Poles:" << huanyang->_numberPoles); - - huanyang->updateRPM(); - - return true; - } else { - log_error(huanyang->name() << " PD143 Poles: expected 2-4, got:" << value); - return false; - } - }; - break; - case -5: - data.msg[3] = 14; // Accel value displayed is X.X - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[4] << 8) | response[5]; - - auto huanyang = static_cast(vfd); - log_info(huanyang->name() << " PD014 Accel:" << float(value) / 10.0); - return true; - }; - break; - case -6: - data.msg[3] = 15; // Decel alue displayed is X.X - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t value = (response[4] << 8) | response[5]; - - auto huanyang = static_cast(vfd); - log_info(huanyang->name() << " PD015 Decel:" << float(value) / 10.0); - return true; - }; - break; - default: - break; - } - - // Done. - return nullptr; - } - - void Huanyang::updateRPM() { - /* - PD005 = 400.00 ; max frequency the VFD will allow - MaxRPM = PD005 * 60; but see PD176 - - Frequencies are expressed in centiHz. - */ - - if (_minFrequency > _maxFrequency) { - _minFrequency = _maxFrequency; - } - if (_speeds.size() == 0) { - // Convert from Frequency in centiHz (the divisor of 100) to RPM (the factor of 60) - SpindleSpeed minRPM = _minFrequency * 60 / 100; - SpindleSpeed maxRPM = _maxFrequency * 60 / 100; - shelfSpeeds(minRPM, maxRPM); - } - setupSpeeds(_maxFrequency); - _slop = std::max(_maxFrequency / 40, 1); - } - - VFD::response_parser Huanyang::get_status_ok(ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 6; - - // 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; - - if (reg < 0x03) { - reg++; - } else { - reg = 0x00; - } - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; }; - } - - VFD::response_parser Huanyang::get_current_speed(ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 6; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x04; - data.msg[2] = 0x03; - data.msg[3] = 0x01; // Output frequency - data.msg[4] = 0x00; - data.msg[5] = 0x00; - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t frequency = (response[4] << 8) | response[5]; - - // Store speed for synchronization - vfd->_sync_dev_speed = frequency; - return true; - }; - } - - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("Huanyang"); - } -} diff --git a/FluidNC/src/Spindles/HuanyangSpindle.h b/FluidNC/src/Spindles/HuanyangSpindle.h deleted file mode 100644 index 57881219a..000000000 --- a/FluidNC/src/Spindles/HuanyangSpindle.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2020 - Bart Dring -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class Huanyang : public VFD { - private: - int reg; - - protected: - uint16_t _minFrequency = 0; // PD011: frequency lower limit. Normally 0. - uint16_t _maxFrequency = 400; // PD005: max frequency the VFD will allow. Normally 400. - uint16_t _maxRpmAt50Hz = 100; // PD144: rated motor revolution at 50Hz => 24000@400Hz = 3000@50HZ - uint16_t _numberPoles = 2; // PD143: 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles - - void updateRPM(); - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t rpm, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override; - response_parser get_current_speed(ModbusCommand& data) override; - - public: - Huanyang(const char* name) : VFD(name) {} - }; -} diff --git a/FluidNC/src/Spindles/NowForeverSpindle.cpp b/FluidNC/src/Spindles/NowForeverSpindle.cpp deleted file mode 100644 index 3cb6c487e..000000000 --- a/FluidNC/src/Spindles/NowForeverSpindle.cpp +++ /dev/null @@ -1,263 +0,0 @@ -#include "NowForeverSpindle.h" - -namespace Spindles { - void NowForever::direction_command(SpindleState mode, ModbusCommand& data) { - data.tx_length = 9; - data.rx_length = 6; - - data.msg[1] = 0x10; // WRITE - data.msg[2] = 0x09; // Register address, high byte (spindle status) - data.msg[3] = 0x00; // Register address, low byte (spindle status) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x01; // Number of elements, low byte (1 element) - data.msg[6] = 0x02; // Length of first element in bytes (1 regsiter with 2 bytes length) - data.msg[7] = 0x00; // Data, high byte - - /* - Contents of register 0x0900 - Bit 0: run, 1=run, 0=stop - Bit 1: direction, 1=ccw, 0=cw - Bit 2: jog, 1=jog, 0=stop - Bit 3: reset, 1=reset, 0=dont reset - Bit 4-15: reserved - */ - - switch (mode) { - case SpindleState::Cw: - data.msg[8] = 0b00000001; // Data, low byte (run, forward) - log_debug("VFD: Set direction CW"); - break; - - case SpindleState::Ccw: - data.msg[8] = 0b00000011; // Data, low byte (run, reverse) - log_debug("VFD: Set direction CCW"); - break; - - case SpindleState::Disable: - data.msg[8] = 0b00000000; // Data, low byte (run, reverse) - log_debug("VFD: Disabled spindle"); - break; - - default: - log_debug("VFD: Unknown spindle state"); - break; - } - } - - void NowForever::set_speed_command(uint32_t hz, ModbusCommand& data) { - data.tx_length = 9; - data.rx_length = 6; - - data.msg[1] = 0x10; // WRITE - data.msg[2] = 0x09; // Register address, high byte (speed in hz) - data.msg[3] = 0x01; // Register address, low byte (speed in hz) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x01; // Number of elements, low byte (1 element) - data.msg[6] = 0x02; // Length of first element in bytes (1 regsiter with 2 bytes length) - - /* - Contents of register 0x0901 - Bit 0-15: speed in hz - */ - - data.msg[7] = hz >> 8; // Data, high byte - data.msg[8] = hz & 0xFF; // Data, low byte - - log_debug("VFD: Set speed: " << hz / 100 << "hz or" << (hz * 60 / 100) << "rpm"); - } - - VFD::response_parser NowForever::initialization_sequence(int index, ModbusCommand& data) { - if (index == -1) { - data.tx_length = 6; - data.rx_length = 7; - - data.msg[1] = 0x03; // READ - data.msg[2] = 0x00; // Register address, high byte (speed in hz) - data.msg[3] = 0x07; // Register address, low byte (speed in hz) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x02; // Number of elements, low byte (2 elements) - - /* - Contents of register 0x0007 - Bit 0-15: max speed in hz * 100 - - Contents of register 0x0008 - Bit 0-15: min speed in hz * 100 - */ - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - if (response[1] != 0x03) { - return false; - } - - // We expect a result length of 4 bytes - if (response[2] != 4) { - return false; - } - - auto nowForever = static_cast(vfd); - - nowForever->_minFrequency = (uint16_t(response[5]) << 8) | uint16_t(response[6]); - nowForever->_maxFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - log_debug("VFD: Min frequency: " << nowForever->_minFrequency << "hz Min speed:" << (nowForever->_minFrequency * 60 / 100) - << "rpm"); - log_debug("VFD: Max frequency: " << nowForever->_maxFrequency << "hz Max speed:" << (nowForever->_maxFrequency * 60 / 100) - << "rpm"); - - nowForever->updateRPM(); - - return true; - }; - } - - return nullptr; - } - - void NowForever::updateRPM() { - if (_minFrequency > _maxFrequency) { - uint16_t tmp = _minFrequency; - _minFrequency = _maxFrequency; - _maxFrequency = _minFrequency; - } - - if (_speeds.size() == 0) { - SpindleSpeed minRPM = _minFrequency * 60 / 100; - SpindleSpeed maxRPM = _maxFrequency * 60 / 100; - - shelfSpeeds(minRPM, maxRPM); - } - setupSpeeds(_maxFrequency); - _slop = std::max(_maxFrequency / 400, 1); - } - - VFD::response_parser NowForever::get_current_speed(ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 5; - - data.msg[1] = 0x03; // READ - data.msg[2] = 0x05; // Register address, high byte (current output frequency in hz) - data.msg[3] = 0x02; // Register address, low byte (current output frequency in hz) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x01; // Number of elements, low byte (1 element) - - /* - Contents of register 0x0502 - Bit 0-15: current output frequency in hz * 100 - */ - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t currentHz = 0; - - if (response[1] != 0x03) { - return false; - } - - // We expect a result length of 2 bytes - if (response[2] != 2) { - return false; - } - - // Conversion from hz to rpm not required ? - vfd->_sync_dev_speed = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - log_debug("VFD: Current speed: " << vfd->_sync_dev_speed / 100 << "hz or " << (vfd->_sync_dev_speed * 60 / 100) << "rpm"); - - return true; - }; - } - - VFD::response_parser NowForever::get_current_direction(ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 5; - - data.msg[1] = 0x03; // READ - data.msg[2] = 0x05; // Register address, high byte (inverter running state) - data.msg[3] = 0x00; // Register address, low byte (inverter running state) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x01; // Number of elements, low byte (1 element) - - /* - Contents of register 0x0500 - Bit 0: run, 1=run, 0=stop - Bit 1: direction, 1=ccw, 0=cw - Bit 2: control, 1=local, 0=remote - Bit 3: sight fault, 1=fault, 0=no fault - Bit 4: fault, 1=fault, 0=no fault - Bit 5-15: reserved - */ - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - bool running = false; - bool direction = false; // false = cw, true = ccw - - if (response[1] != 0x03) { - return false; - } - - // We expect a result length of 2 bytes - if (response[2] != 2) { - return false; - } - - running = response[4] & 0b00000001; - direction = (response[4] & 0b00000001) >> 1; - - //TODO: Check what to do with the inform ation we have now. - if (running) { - if (direction) { - log_debug("VFD: Got direction CW"); - } else { - log_debug("VFD: Got direction CCW"); - } - } else { - log_debug("VFD: Got spindle not running"); - } - - return true; - }; - } - - VFD::response_parser NowForever::get_status_ok(ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 5; - - data.msg[1] = 0x03; // READ - data.msg[2] = 0x03; // Register address, high byte (current fault number) - data.msg[3] = 0x00; // Register address, low byte (current fault number) - data.msg[4] = 0x00; // Number of elements, high byte - data.msg[5] = 0x01; // Number of elements, low byte (1 element) - - /* - Contents of register 0x0300 - Bit 0-15: current fault number, 0 = no fault, 1~18 = fault number - */ - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t currentFaultNumber = 0; - - if (response[1] != 0x03) { - return false; - } - - // We expect a result length of 2 bytes - if (response[2] != 2) { - return false; - } - - currentFaultNumber = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - if (currentFaultNumber != 0) { - log_debug("VFD: Got fault number: " << currentFaultNumber); - return false; - } - - return true; - }; - } - - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("NowForever"); - } -} diff --git a/FluidNC/src/Spindles/NowForeverSpindle.h b/FluidNC/src/Spindles/NowForeverSpindle.h deleted file mode 100644 index 0e3e03f88..000000000 --- a/FluidNC/src/Spindles/NowForeverSpindle.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class NowForever : public VFD { - protected: - uint16_t _minFrequency = 0; - uint16_t _maxFrequency = 0; - - void updateRPM(); - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t hz, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_current_speed(ModbusCommand& data) override; - response_parser get_current_direction(ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override; - bool safety_polling() const { return true; } - - bool use_delay_settings() const override { return false; } - - public: - NowForever(const char* name) : VFD(name) {} - }; -} diff --git a/FluidNC/src/Spindles/SiemensV20Spindle.h b/FluidNC/src/Spindles/SiemensV20Spindle.h deleted file mode 100644 index 07b62bb74..000000000 --- a/FluidNC/src/Spindles/SiemensV20Spindle.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2020 - Bart Dring -// Copyright (c) 2020 - Stefan de Bruijn -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class SiemensV20 : public VFD { - protected: - uint16_t _minFrequency = 0; // PD011: frequency lower limit. Normally 0. - uint16_t _maxFrequency = 400; // PD005: max frequency the VFD will allow. Normally 400. - uint16_t _numberPoles = 2; // 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles - uint16_t _NumberPhases = 3; // Typically 3 Phases for standard VFDs - float _FreqScaler = - float(-16384.0) / - _maxFrequency; // SPEED_SCALING Internally it is standardized to 16384. (16384 / 400) With this scaling HSW and HIW are transferred via the Modbus register. - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t rpm, ModbusCommand& data) override; - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_current_speed(ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } - - bool use_delay_settings() const override { return false; } - bool safety_polling() const override { return false; } - - public: - SiemensV20(const char* name) : VFD(name) {} - }; -} diff --git a/FluidNC/src/Spindles/Spindle.h b/FluidNC/src/Spindles/Spindle.h index 4fa35b523..fd906cd9e 100644 --- a/FluidNC/src/Spindles/Spindle.h +++ b/FluidNC/src/Spindles/Spindle.h @@ -34,10 +34,10 @@ namespace Spindles { public: Spindle(const char* name) : _name(name) {} - Spindle(const Spindle&) = delete; - Spindle(Spindle&&) = delete; + Spindle(const Spindle&) = delete; + Spindle(Spindle&&) = delete; Spindle& operator=(const Spindle&) = delete; - Spindle& operator=(Spindle&&) = delete; + Spindle& operator=(Spindle&&) = delete; bool _defaultedSpeeds; uint32_t offSpeed() { return _speeds[0].offset; } @@ -111,6 +111,7 @@ namespace Spindles { protected: uint8_t _current_tool = 0; }; + using SpindleFactory = Configuration::GenericFactory; } extern Spindles::Spindle* spindle; diff --git a/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.cpp b/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.cpp new file mode 100644 index 000000000..376c4969d --- /dev/null +++ b/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.cpp @@ -0,0 +1,167 @@ +// Copyright (c) 2024 - Jan Speckamp, whosmatt +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +/* + This is for a Danfoss VLT 2800 VFD based spindle to be controlled via RS485 Modbus RTU. + FluidNC imposes limitations (methods dont have access to full spindle state), while the Danfoss VFD expects the full state to be set with every command. + As an interim solution, the state of the spindle is cached in cachedSpindleState. + + Modbus setup of the VFD is covered in https://files.danfoss.com/download/Drives/doc_A_1_mg10s122.pdf + General setup of the VFD is covered in https://files.danfoss.com/download/Drives/doc_B_1_MG28E902.pdf +*/ + +#include "DanfossVLT2800Protocol.h" +#include "../../Platform.h" +#include "../VFDSpindle.h" + +#include + +#define READ_COIL 0x01 +#define READ_HR 0x03 + +#define WRITE_SINGLE_COIL 0x05 +#define WRITE_MULTIPLE_COIL 0x0F + +namespace Spindles { + namespace VFD { + void DanfossVLT2800Protocol::set_speed_command(uint32_t dev_speed, VFDProtocol::ModbusCommand& data) { + // Cache received speed + cachedSpindleState.speed = dev_speed; + + // Write speed and direction from cache to VFD + writeVFDState(cachedSpindleState, data); + } + + void DanfossVLT2800Protocol::direction_command(SpindleState mode, VFDProtocol::ModbusCommand& data) { + // Cache received direction + cachedSpindleState.state = mode; + + // Write speed and direction from cache to VFD + writeVFDState(cachedSpindleState, data); + } + + VFDProtocol::response_parser DanfossVLT2800Protocol::get_current_speed(VFDProtocol::ModbusCommand& data) { + data.tx_length = 6; // including automatically set client_id, excluding crc + data.rx_length = 3 + 2; // excluding crc + + // We write a full control word instead of setting individual coils + data.msg[1] = READ_HR; + data.msg[2] = 0x14; + data.msg[3] = 0x3b; // start register + data.msg[4] = 0x00; + data.msg[5] = 0x01; // no of points + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + // const uint8_t slave_addr = response[0] + // const uint8_t function = response[1] + // const uint8_t response_byte_count = response[2] + + uint16_t freq = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + vfd->_sync_dev_speed = freq; + return true; + }; + } + + VFDProtocol::response_parser DanfossVLT2800Protocol::get_status_ok_and_init(VFDProtocol::ModbusCommand& data, bool init) { + data.tx_length = 6; // including automatically set client_id, excluding crc + data.rx_length = 5; // excluding crc + + // Read out current state + data.msg[1] = READ_COIL; + data.msg[2] = 0x00; + data.msg[3] = 0x20; // Coil index 32 + data.msg[4] = 0x00; + data.msg[5] = 0x10; // Read 16 Bits + + if (init) { + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + // The VFD is responsive. Initialize speeds. + + auto df = static_cast(detail); + vfd->setupSpeeds(df->_maxFrequency); + + return true; + }; + } else { + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + SpindleStatus status; + status.statusWord = int16_t(response[3]) | uint16_t(response[4] << 8); // See DanfossSpindle.h for structure + +#ifdef DEBUG_VFD + log_debug("Control ready:" << status.flags.control_ready); + log_debug("Drive ready:" << status.flags.drive_ready); + log_debug("Coasting stop:" << status.flags.warning); + log_debug("Trip status:" << status.flags.trip); + log_debug("Trip lock:" << status.flags.trip_lock); + log_debug("No warning/warning:" << status.flags.warning); + log_debug("Speed == ref:" << status.flags.speed_status); + log_debug("Local operation/serial communication control:" << status.flags.local_control); + log_debug("Outside frequency range:" << status.flags.freq_range_err); + log_debug("Motor running:" << status.flags.motor_running); + log_debug("Not used:" << status.flags.voltage_warn); + log_debug("Voltage warn:" << status.flags.voltage_warn); + log_debug("Current limit:" << status.flags.current_limit); + log_debug("Thermal warn:" << status.flags.thermal_warn); +#endif + log_error("TODO: actually check status bits and output potential errors"); + return true; + }; + } + } + + // The VLT2800 expects speed, direction and enable to be sent together at all times. + // This function uses a combined cached spindle state that includes the speed and sends it to the VFD. + void DanfossVLT2800Protocol::writeVFDState(combinedSpindleState spindle, ModbusCommand& data) { + SpindleControl cword; + cword.flags.coasting_stop = 1; + cword.flags.dc_braking_stop = 1; + cword.flags.quick_stop = 1; + cword.flags.freeze_freq = 1; + cword.flags.jog = 0; + cword.flags.reference_preset = 0; + cword.flags.setup_preset = 0; + cword.flags.output_46 = 0; + cword.flags.relay_01 = 0; + cword.flags.data_valid = 1; + cword.flags.reset = 0; + cword.flags.reverse = 0; + + switch (spindle.state) { + case SpindleState::Cw: + cword.flags.reverse = 0; + cword.flags.start_stop = 1; + break; + case SpindleState::Ccw: + cword.flags.reverse = 1; + cword.flags.start_stop = 1; + break; + case SpindleState::Disable: + cword.flags.start_stop = 0; + break; + default: + break; + } + + // Assemble packet: + data.tx_length = 11; + data.rx_length = 6; + + // We write a full control word instead of setting individual coils + data.msg[1] = WRITE_MULTIPLE_COIL; + data.msg[2] = 0x00; + data.msg[3] = 0x00; // start coil address + data.msg[4] = 0x00; + data.msg[5] = 0x20; // write length + data.msg[6] = 0x04; // payload byte count + + data.msg[7] = cword.controlWord & 0xFF; // MSB + data.msg[8] = cword.controlWord >> 8; // LSB + + data.msg[9] = spindle.speed & 0xFF; + data.msg[10] = spindle.speed >> 8; + } + namespace { + SpindleFactory::DependentInstanceBuilder registration("DanfossVLT2800"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.h b/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.h new file mode 100644 index 000000000..cd85d1471 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/DanfossVLT2800Protocol.h @@ -0,0 +1,83 @@ +// Copyright (c) 2024 - Jan Speckamp, whosmatt +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class DanfossVLT2800Protocol : public VFDProtocol { + union SpindleStatus { + struct { + bool control_ready : 1; // bit 00 = 0: "" | Bit = 1: "Control ready" + bool drive_ready : 1; // bit 01 = 0: "" | Bit = 1: "Drive ready" + bool coasting_stop : 1; // bit 02 = 0: "Coasting stop" | Bit = 1: "" + bool trip : 1; // bit 03 = 0: "No Trip" | Bit = 1: "Trip" + bool unused1 : 1; // bit 04 Not used + bool unused2 : 1; // bit 05 Not used + bool trip_lock : 1; // bit 06 = 0: "" | Bit = 1: "Trip lock" + bool warning : 1; // bit 07 = 0: "No warning" | Bit = 1: "Warning" + bool speed_status : 1; // bit 08 = 0: "Speed != ref." | Bit = 1: "Speed = ref." + bool local_control : 1; // bit 09 = 0: "Local control" | Bit = 1: "Ser. communi." + bool freq_range_err : 1; // bit 10 = 0: "Outside frequency range" | Bit = 1: "Frequency limit OK" + bool motor_running : 1; // bit 11 = 0: "" | Bit = 1: "Motor running" + bool unused3 : 1; // bit 12 Not used + bool voltage_warn : 1; // bit 13 = 0: "" | Bit = 1: "Voltage warn." + bool current_limit : 1; // bit 14 = 0: "" | Bit = 1: "Current limit" + bool thermal_warn : 1; // bit 15 = 0: "" | Bit = 1: "Thermal wan." + } flags; + uint16_t statusWord; + }; + + union SpindleControl { + struct { + uint8_t reference_preset : 2; // bit 00 = lsb of 2 bit value for preset reference selection + // bit 01 = msb + bool dc_braking_stop : 1; // bit 02 = 0 causes stop with dc brake + bool coasting_stop : 1; // bit 03 = 0 causes coasting stop + bool quick_stop : 1; // bit 04 = 0 causes quick stop + bool freeze_freq : 1; // bit 05 = 0 causes output frequency to be locked from inputs, stops still apply + bool start_stop : 1; // bit 06 = 1 causes motor start, 0 causes motor stop, standard ramp applies + bool reset : 1; // bit 07 = resets trip condition on change from 0 to 1 + bool jog : 1; // bit 08 = 1 switches to jogging (par. 213) + bool ramp_select : 1; // bit 09 = ramp selection: 0 = ramp 1 (par. 207-208), 1 = ramp 2 (par. 209-210) + bool data_valid : 1; // bit 10 = 0 causes entire control word to be ignored + bool relay_01 : 1; // bit 11 = 1 activates relay 01 + bool output_46 : 1; // bit 12 = 1 activates digital output on terminal 46 + uint8_t setup_preset : 2; // bit 13 = lsb of 2 bit value for setup selection when par. 004 multi setup is enabled + // bit 14 = msb + bool reverse : 1; // bit 15 = 1 causes reversing + } flags; + uint16_t controlWord; + }; + + protected: + // Unlike other VFDs, the VLT seems to take speed as int16_t, mapped to 0-200% of the configured maximum reference. + uint16_t _minFrequency = 0x0; // motor off (0% speed) + uint16_t _maxFrequency = 0x4000; // max speed the VFD will allow. 0x4000 = 100% for VLT2800 + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t rpm, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) { return get_status_ok_and_init(data, true); } + response_parser get_current_speed(ModbusCommand& data) override; + response_parser get_current_direction(ModbusCommand& data) override { return nullptr; }; + response_parser get_status_ok(ModbusCommand& data) override { return get_status_ok_and_init(data, false); } + + response_parser get_status_ok_and_init(ModbusCommand& data, bool init); + + bool safety_polling() const override { return true; } + + private: + struct combinedSpindleState { + SpindleState state; + uint32_t speed; + } cachedSpindleState; + + void writeVFDState(combinedSpindleState spindle, ModbusCommand& data); + + void parse_spindle_status(uint16_t statusword, SpindleStatus& status); + }; + } +} diff --git a/FluidNC/src/Spindles/VFD/H100Protocol.cpp b/FluidNC/src/Spindles/VFD/H100Protocol.cpp new file mode 100644 index 000000000..4bd2389bb --- /dev/null +++ b/FluidNC/src/Spindles/VFD/H100Protocol.cpp @@ -0,0 +1,179 @@ +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +/* + H100Spindle.cpp + + This is for a H100 VFD based spindle via RS485 Modbus. + + WARNING!!!! + VFDs are very dangerous. They have high voltages and are very powerful + Remove power before changing bits. +*/ + +#include "H100Protocol.h" +#include "../VFDSpindle.h" + +#include // std::max + +namespace Spindles { + namespace VFD { + void H100Protocol::direction_command(SpindleState mode, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 6; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x05; + data.msg[2] = 0x00; + + switch (mode) { + case SpindleState::Cw: //[01] [05] [00 49] [ff 00] -- forward run + data.msg[3] = 0x49; + data.msg[4] = 0xFF; + data.msg[5] = 0x00; + break; + case SpindleState::Ccw: //[01] [05] [00 4A] [ff 00] -- reverse run + data.msg[3] = 0x4A; + data.msg[4] = 0xFF; + data.msg[5] = 0x00; + break; + default: // SpindleState::Disable [01] [05] [00 4B] [ff 00] -- stop + data.msg[3] = 0x4B; + data.msg[4] = 0xFF; + data.msg[5] = 0x00; + break; + } + } + + void H100Protocol::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 6; + + if (dev_speed != 0 && (dev_speed < _minFrequency || dev_speed > _maxFrequency)) { + log_warn("H100 requested freq " << (dev_speed) << " is outside of range (" << _minFrequency << "," << _maxFrequency << ")"); + } + +#ifdef DEBUG_VFD + log_debug("Setting VFD dev_speed to " << dev_speed); +#endif + + //[01] [06] [0201] [07D0] Set frequency to [07D0] = 200.0 Hz. (2000 is written!) + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x06; // Set register command + data.msg[2] = 0x02; + data.msg[3] = 0x01; + //data.msg[4] = dev_speed >> 8; - BC 11/24/21 + data.msg[4] = dev_speed >> 8; + data.msg[5] = dev_speed & 0xFF; + } + + // This gets data from the VFD. It does not set any values + VFDProtocol::response_parser H100Protocol::initialization_sequence(int index, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 5; + // Read F011 (min frequency) and F005 (max frequency): + // + // [03] [000B] [0001] gives [03] [02] [xxxx] (with 02 the result byte count). + // [03] [0005] [0001] gives [03] [02] [xxxx] (with 02 the result byte count). + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x03; // Read setting + data.msg[2] = 0x00; + // [3] = set below... + data.msg[4] = 0x00; // length + data.msg[5] = 0x01; + + if (index == -1) { + // Max frequency + data.msg[3] = 0x05; // PD005: max frequency the VFD will allow. Normally 400. + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[3] << 8) | response[4]; + +#ifdef DEBUG_VFD + log_debug("VFD: Max frequency = " << value / 10 << "Hz " << value / 10 * 60 << "RPM"); +#endif + log_info("VFD: Max speed:" << (value / 10 * 60) << "rpm"); + + // Set current RPM value? Somewhere? + auto h100 = static_cast(detail); + h100->_maxFrequency = value; + + return true; + }; + + } else if (index == -2) { + // Min Frequency + data.msg[3] = 0x0B; // PD011: frequency lower limit. Normally 0. + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[3] << 8) | response[4]; + +#ifdef DEBUG_VFD + log_debug("VFD: Min frequency = " << value / 10 << "Hz " << value / 10 * 60 << "RPM"); +#endif + log_info("VFD: Min speed:" << (value / 10 * 60) << "rpm"); + + // Set current RPM value? Somewhere? + auto h100 = static_cast(detail); + h100->_minFrequency = value; + + h100->updateRPM(vfd); + + return true; + }; + } + + // Done. + return nullptr; + } + + void H100Protocol::updateRPM(VFDSpindle* vfd) { + if (_minFrequency > _maxFrequency) { + _minFrequency = _maxFrequency; + } + + if (vfd->_speeds.size() == 0) { + SpindleSpeed minRPM = _minFrequency * 60 / 10; + SpindleSpeed maxRPM = _maxFrequency * 60 / 10; + + vfd->shelfSpeeds(minRPM, maxRPM); + } + vfd->setupSpeeds(_maxFrequency); + vfd->_slop = std::max(_maxFrequency / 40, 1); + + log_info("VFD: VFD settings read: Freq range(" << _minFrequency << " , " << _maxFrequency << ")]"); + } + + VFDProtocol::response_parser H100Protocol::get_current_speed(ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + // [01] [04] [0000] [0002] -- output frequency + data.tx_length = 6; + data.rx_length = 7; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x04; + data.msg[2] = 0x00; + data.msg[3] = 0x00; // Output frequency + data.msg[4] = 0x00; + data.msg[5] = 0x02; + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + // 01 04 04 [freq 16] [set freq 16] [crc16] + uint16_t frequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + // Store speed for synchronization + vfd->_sync_dev_speed = frequency; + return true; + }; + } + + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("H100"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/H100Protocol.h b/FluidNC/src/Spindles/VFD/H100Protocol.h new file mode 100644 index 000000000..d6a339d17 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/H100Protocol.h @@ -0,0 +1,30 @@ +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class H100Protocol : public VFDProtocol { + private: + int reg = 0; + + protected: + uint16_t _minFrequency = 0; + uint16_t _maxFrequency = 4000; // H100 works with frequencies scaled by 10. + + void updateRPM(VFDSpindle* vfd); + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t rpm, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } + response_parser get_current_speed(ModbusCommand& data) override; + + bool use_delay_settings() const override { return false; } + }; + } +} diff --git a/FluidNC/src/Spindles/H100Spindle.md b/FluidNC/src/Spindles/VFD/H100Protocol.md similarity index 100% rename from FluidNC/src/Spindles/H100Spindle.md rename to FluidNC/src/Spindles/VFD/H100Protocol.md diff --git a/FluidNC/src/Spindles/VFD/H2AProtocol.cpp b/FluidNC/src/Spindles/VFD/H2AProtocol.cpp new file mode 100644 index 000000000..3345a957b --- /dev/null +++ b/FluidNC/src/Spindles/VFD/H2AProtocol.cpp @@ -0,0 +1,136 @@ +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +/* + H2ASpindle.cpp + + This is for the new H2A H2A VFD based spindle via RS485 Modbus. + + 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 "H2AProtocol.h" +#include "../VFDSpindle.h" + +namespace Spindles { + namespace VFD { + void H2AProtocol::direction_command(SpindleState mode, ModbusCommand& data) { + 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 == SpindleState::Ccw) ? 0x02 : (mode == SpindleState::Cw ? 0x01 : 0x06); + } + + void H2AProtocol::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { + // NOTE: H2A inverters are a-symmetrical. You set the speed in 1/100 + // percentages, and you get the speed in RPM. So, we need to convert + // the RPM using maxRPM to a percentage. See MD document for details. + // + // For the H2A VFD, the speed is read directly units of RPM, unlike many + // other VFDs where it is given in Hz times some scale factor. + data.tx_length = 6; + data.rx_length = 6; + + uint16_t speed = (uint32_t(dev_speed) * 10000L) / uint32_t(_maxRPM); + 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] = speed >> 8; + data.msg[5] = speed & 0xFF; + } + + VFDProtocol::response_parser H2AProtocol::initialization_sequence(int index, ModbusCommand& data) { + if (index == -1) { + 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, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t maxRPM = (uint16_t(response[4]) << 8) | uint16_t(response[5]); + + if (vfd->_speeds.size() == 0) { + vfd->shelfSpeeds(maxRPM / 4, maxRPM); + } + + vfd->setupSpeeds(maxRPM); // The speed is given directly in RPM + vfd->_slop = 300; // 300 RPM + + static_cast(detail)->_maxRPM = uint32_t(maxRPM); + + log_info("H2A spindle initialized at " << maxRPM << " RPM"); + + return true; + }; + } else { + return nullptr; + } + } + + VFDProtocol::response_parser H2AProtocol::get_current_speed(ModbusCommand& data) { + 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 speed + data.msg[3] = 0x0C; + data.msg[4] = 0x00; // Read 2 values + data.msg[5] = 0x02; + + // Recv: 01 03 0004 095D 0000 + // ---- = 2397 (val #1) + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + vfd->_sync_dev_speed = (uint16_t(response[4]) << 8) | uint16_t(response[5]); + return true; + }; + } + + VFDProtocol::response_parser H2AProtocol::get_current_direction(ModbusCommand& data) { + 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 vfd state? + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { return true; }; + } + + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("H2A"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/H2AProtocol.h b/FluidNC/src/Spindles/VFD/H2AProtocol.h new file mode 100644 index 000000000..921daf3ac --- /dev/null +++ b/FluidNC/src/Spindles/VFD/H2AProtocol.h @@ -0,0 +1,26 @@ +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class H2AProtocol : public VFDProtocol { + protected: + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t dev_speed, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_current_speed(ModbusCommand& data) override; + response_parser get_current_direction(ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } + + bool use_delay_settings() const override { return false; } + bool safety_polling() const override { return false; } + + uint32_t _maxRPM; + }; + } +} diff --git a/FluidNC/src/Spindles/H2ASpindle.md b/FluidNC/src/Spindles/VFD/H2AProtocol.md similarity index 100% rename from FluidNC/src/Spindles/H2ASpindle.md rename to FluidNC/src/Spindles/VFD/H2AProtocol.md diff --git a/FluidNC/src/Spindles/VFD/HuanyangProtocol.cpp b/FluidNC/src/Spindles/VFD/HuanyangProtocol.cpp new file mode 100644 index 000000000..c8e3469a7 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/HuanyangProtocol.cpp @@ -0,0 +1,399 @@ +// Copyright (c) 2020 - Bart Dring +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +/* + HuanyangSpindle.cpp + + This is for a Huanyang VFD based spindle via RS485 Modbus. + Sorry for the lengthy comments, but finding the details on this + VFD was a PITA. I am just trying to help the next person. + + WARNING!!!! + VFDs are very dangerous. They have high voltages and are very powerful + Remove power before changing bits. + + ============================================================================== + + If a user changes state or RPM level, the command to do that is sent. If + the command is not responded to a message is sent to serial that there was + a timeout. If the system is in a critical state, an alarm will be generated and + the machine stopped. + + If there are no commands to execute, various status items will be polled. If there + is no response, it will behave as described above. It will stop any running jobs with + an alarm. + + =============================================================================== + + Protocol Details + + A lot of good information about the details of all these parameters and how they should + be setup can be found on this page: + https://community.carbide3d.com/t/vfd-parameters-huanyang-model/15459/7 . + + Before using spindle, VFD must be setup for RS485 and match your spindle: + + PD004 400 Base frequency as rated on my spindle (default was 50) + PD005 400 Maximum frequency Hz (Typical for spindles) + PD011 120 Min Speed (Recommend Aircooled=120 Water=100) + PD014 10 Acceleration time (Test to optimize) + PD015 10 Deceleration time (Test to optimize) + PD023 1 Reverse run enabled + PD141 220 Spindle max rated voltage + PD142 3.7 Max current Amps (0.8kw=3.7 1.5kw=7.0, 2.2kw=??) + PD143 2 Poles most are 2 (I think this is only used for RPM calc from Hz) + PD144 3000 Max rated motor revolution at 50 Hz => 24000@400Hz = 3000@50HZ + + PD001 2 RS485 Control of run commands + PD002 2 RS485 Control of operating frequency + PD163 1 RS485 Address: 1 (Typical. OK to change...see below) + PD164 1 RS485 Baud rate: 9600 (Typical. OK to change...see below) + PD165 3 RS485 Mode: RTU, 8N1 + + The official documentation of the RS485 is horrible. I had to piece it together from + a lot of different sources: + + Manuals: https://github.com/RobertOlechowski/Huanyang_VFD/tree/master/Documentations/pdf + Reference: https://github.com/Smoothieware/Smoothieware/blob/edge/src/modules/tools/spindle/HuanyangSpindleControl.cpp + Refernece: https://gist.github.com/Bouni/803492ed0aab3f944066 + VFD settings: https://www.hobbytronics.co.za/Content/external/1159/Spindle_Settings.pdf + Spindle Talker 2 https://github.com/GilchristT/SpindleTalker2/releases + Python https://github.com/RobertOlechowski/Huanyang_VFD + + ========================================================================= + + Commands + ADDR CMD LEN DATA CRC + 0x01 0x03 0x01 0x01 0x31 0x88 Start spindle clockwise + 0x01 0x03 0x01 0x08 0xF1 0x8E Stop spindle + 0x01 0x03 0x01 0x11 0x30 0x44 Start spindle counter-clockwise + + Return values are + 0 = run + 1 = jog + 2 = r/f + 3 = running + 4 = jogging + 5 = r/f + 6 = Braking + 7 = Track start + + ========================================================================== + + Setting RPM + ADDR CMD LEN DATA CRC + 0x01 0x05 0x02 0x09 0xC4 0xBF 0x0F Write Frequency (0x9C4 = 2500 = 25.00HZ) + + Response is same as data sent + + ========================================================================== + + Setting registers + Addr Read Len Reg DataH DataL CRC CRC + 0x01 0x01 0x03 5 0x00 0x00 CRC CRC // PD005 + 0x01 0x01 0x03 11 0x00 0x00 CRC CRC // PD011 + 0x01 0x01 0x03 143 0x00 0x00 CRC CRC // PD143 + 0x01 0x01 0x03 144 0x00 0x00 CRC CRC // PD144 + + Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above) + + ========================================================================== + + Status registers + Addr Read Len Reg DataH DataL CRC CRC + 0x01 0x04 0x03 0x00 0x00 0x00 CRC CRC // Set Frequency * 100 (25Hz = 2500) + 0x01 0x04 0x03 0x01 0x00 0x00 CRC CRC // Ouput Frequency * 100 + 0x01 0x04 0x03 0x02 0x00 0x00 CRC CRC // Ouput Amps * 10 + 0x01 0x04 0x03 0x03 0x00 0x00 0xF0 0x4E // Read RPM (example CRC shown) + 0x01 0x04 0x03 0x0 0x00 0x00 CRC CRC // DC voltage + 0x01 0x04 0x03 0x05 0x00 0x00 CRC CRC // AC voltage + 0x01 0x04 0x03 0x06 0x00 0x00 CRC CRC // Cont + 0x01 0x04 0x03 0x07 0x00 0x00 CRC CRC // VFD Temp + + Message is returned with requested value = (DataH * 16) + DataL (see decimal offset above) + + ========================================================================== + + The math: + + PD005 400 Maximum frequency Hz (Typical for spindles) + PD011 120 Min Speed (Recommend Aircooled=120 Water=100) + PD143 2 Poles most are 2 (I think this is only used for RPM calc from Hz) + PD144 3000 Max rated motor revolution at 50 Hz => 24000@400Hz = 3000@50HZ + + During initialization these 4 are pulled from the VFD registers. It then sets min and max RPM + of the spindle. So: + + MinRPM = PD011 * PD144 / 50 = 120 * 3000 / 50 = 7200 RPM min + MaxRPM = PD005 * PD144 / 50 = 400 * 3000 / 50 = 24000 RPM max + + If you then set 12000 RPM, it calculates the frequency: + + int targetFrequency = targetRPM * PD005 / MaxRPM = targetRPM * PD005 / (PD005 * PD144 / 50) = + targetRPM * 50 / PD144 = 12000 * 50 / 3000 = 200 + + If the frequency is -say- 25 Hz, Huanyang wants us to send 2500 (eg. 25.00 Hz). +*/ + +#include "HuanyangProtocol.h" + +#include "../VFDSpindle.h" + +#include // std::max + +namespace Spindles { + namespace VFD { + // Baud rate is set in the PD164 setting. If it is not 9600, add, for example, + // _baudrate = 19200; + + void HuanyangProtocol::direction_command(SpindleState mode, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 4; + data.rx_length = 4; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x03; + data.msg[2] = 0x01; + + switch (mode) { + case SpindleState::Cw: + data.msg[3] = 0x01; + break; + case SpindleState::Ccw: + data.msg[3] = 0x11; + break; + default: // SpindleState::Disable + data.msg[3] = 0x08; + break; + } + } + + void HuanyangProtocol::set_speed_command(uint32_t dev_speed, ModbusCommand& data) { + // The units for setting Huanyang speed are Hz * 100. For a 2-pole motor, + // RPM is Hz * 60 sec/min. The maximum possible speed is 400 Hz so + // 400 * 60 = 24000 RPM. + + if (dev_speed != 0 && (dev_speed < _minFrequency || dev_speed > _maxFrequency)) { + log_warn("Huanyang requested freq " << (dev_speed) << " is outside of range (" << _minFrequency << "," << _maxFrequency + << ")"); + } + + // There is a configuration register PD144 that scales the display to show the + // RPMs. It is nominally set to 3000 (= 50Hz * 60) for a 2-pole motor or + // 1500 for a 4-pole motor, but it can be set slightly lower to account for + // slip - the torque-limited difference between actual spindle speed and the + // the frequency delivered to the motor windings. + + // Frequency comes from a conversion of revolutions per second to revolutions per minute + // (factor of 60) and a factor of 2 from counting the number of poles. E.g. rpm * 120 / 100. + + // int targetFrequency = targetRPM * PD005 / MaxRPM + // = targetRPM * PD005 / (PD005 * PD144 / 50) + // = targetRPM * 50 / PD144 + // + // Huanyang wants a factor 100 bigger numbers. So, 1500 rpm -> 25 HZ. Send 2500. + + // The conversion from RPM as requested by the GCode program and the speed number + // (nominally Hz * 100) is done in mapping code that is shared across all spindles. + // The dev_speed argument is precomputed by that code. + + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 5; + data.rx_length = 5; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x05; // Set register command + data.msg[2] = 0x02; // Register PD002 - main frequency in units of 0.01 Hz + data.msg[3] = dev_speed >> 8; + data.msg[4] = dev_speed & 0xFF; + } + + // This gets data from the VFS. It does not set any values + VFDProtocol::response_parser HuanyangProtocol::initialization_sequence(int index, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 6; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x01; // Read setting + data.msg[2] = 0x03; // Len + // [3] = set below... + data.msg[4] = 0x00; + data.msg[5] = 0x00; + + switch (index) { + case -1: + data.msg[3] = 5; // PD005: max frequency the VFD will allow. Normally 400. + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[4] << 8) | response[5]; + + // Set current RPM value? Somewhere? + auto huanyang = static_cast(detail); + huanyang->_maxFrequency = value; + return true; + }; + break; + case -2: + data.msg[3] = 11; // PD011: frequency lower limit. Normally 0. + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[4] << 8) | response[5]; + + // Set current RPM value? Somewhere? + auto huanyang = static_cast(detail); + huanyang->_minFrequency = value; + + log_info("Huanyang PD0011, PD005 Freq range (" + << (huanyang->_minFrequency / 100) << "," << (huanyang->_maxFrequency / 100) << ") Hz" + << " (" << (huanyang->_minFrequency / 100 * 60) << "," << (huanyang->_maxFrequency / 100 * 60) << ") RPM"); + + return true; + }; + break; + case -3: + data.msg[3] = 144; // PD144: max rated motor revolution at 50Hz => 24000@400Hz = 3000@50HZ + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[4] << 8) | response[5]; + + // Set current RPM value? Somewhere? + auto huanyang = static_cast(detail); + huanyang->_maxRpmAt50Hz = value; + + log_info("Huanyang PD144 Rated RPM @ 50Hz:" << huanyang->_maxRpmAt50Hz); + + // Regarding PD144, the 2 versions of the manuals both say "This is set according to the + // actual revolution of the motor. The displayed value is the same as this set value. It + // can be used as a monitoring parameter, which is convenient to the user. This set value + // corresponds to the revolution at 50Hz". + + // Calculate the VFD settings: + huanyang->updateRPM(vfd); + + return true; + }; + break; + case -4: + data.rx_length = 5; + data.msg[3] = 143; // PD143: 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint8_t value = response[4]; // Single byte response. + auto huanyang = static_cast(detail); + // Sanity check. We expect something like 2 or 4 poles. + if (value <= 4 && value >= 2) { + // Set current RPM value? Somewhere? + + huanyang->_numberPoles = value; + + log_info("Huanyang PD143 Poles:" << huanyang->_numberPoles); + + huanyang->updateRPM(vfd); + + return true; + } else { + log_error("Huanyang PD143 Poles: expected 2-4, got:" << value); + return false; + } + }; + break; + case -5: + data.msg[3] = 14; // Accel value displayed is X.X + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[4] << 8) | response[5]; + + auto huanyang = static_cast(detail); + log_info("Huanyang PD014 Accel:" << float(value) / 10.0); + return true; + }; + break; + case -6: + data.msg[3] = 15; // Decel alue displayed is X.X + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t value = (response[4] << 8) | response[5]; + + auto huanyang = static_cast(detail); + log_info("Huanyang PD015 Decel:" << float(value) / 10.0); + return true; + }; + break; + default: + break; + } + + // Done. + return nullptr; + } + + void HuanyangProtocol::updateRPM(VFDSpindle* vfd) { + /* + PD005 = 400.00 ; max frequency the VFD will allow + MaxRPM = PD005 * 60; but see PD176 + + Frequencies are expressed in centiHz. + */ + + if (_minFrequency > _maxFrequency) { + _minFrequency = _maxFrequency; + } + if (vfd->_speeds.size() == 0) { + // Convert from Frequency in centiHz (the divisor of 100) to RPM (the factor of 60) + SpindleSpeed minRPM = _minFrequency * 60 / 100; + SpindleSpeed maxRPM = _maxFrequency * 60 / 100; + vfd->shelfSpeeds(minRPM, maxRPM); + } + vfd->setupSpeeds(_maxFrequency); + vfd->_slop = std::max(_maxFrequency / 40, 1); + } + + VFDProtocol::response_parser HuanyangProtocol::get_status_ok(ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 6; + + // 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; + + if (reg < 0x03) { + reg++; + } else { + reg = 0x00; + } + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { return true; }; + } + + VFDProtocol::response_parser HuanyangProtocol::get_current_speed(ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 6; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x04; + data.msg[2] = 0x03; + data.msg[3] = 0x01; // Output frequency + data.msg[4] = 0x00; + data.msg[5] = 0x00; + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t frequency = (response[4] << 8) | response[5]; + + // Store speed for synchronization + vfd->_sync_dev_speed = frequency; + return true; + }; + } + + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("Huanyang"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/HuanyangProtocol.h b/FluidNC/src/Spindles/VFD/HuanyangProtocol.h new file mode 100644 index 000000000..e11696d7c --- /dev/null +++ b/FluidNC/src/Spindles/VFD/HuanyangProtocol.h @@ -0,0 +1,31 @@ +// Copyright (c) 2020 - Bart Dring +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class HuanyangProtocol : public VFDProtocol { + private: + int reg = 0; + + protected: + uint16_t _minFrequency = 0; // PD011: frequency lower limit. Normally 0. + uint16_t _maxFrequency = 400; // PD005: max frequency the VFD will allow. Normally 400. + uint16_t _maxRpmAt50Hz = 100; // PD144: rated motor revolution at 50Hz => 24000@400Hz = 3000@50HZ + uint16_t _numberPoles = 2; // PD143: 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles + + void updateRPM(VFDSpindle* vfd); + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t rpm, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override; + response_parser get_current_speed(ModbusCommand& data) override; + }; + } +} diff --git a/FluidNC/src/Spindles/VFD/NowForeverProtocol.cpp b/FluidNC/src/Spindles/VFD/NowForeverProtocol.cpp new file mode 100644 index 000000000..b02732dcd --- /dev/null +++ b/FluidNC/src/Spindles/VFD/NowForeverProtocol.cpp @@ -0,0 +1,267 @@ +#include "NowForeverProtocol.h" + +#include "../VFDSpindle.h" + +namespace Spindles { + namespace VFD { + void NowForeverProtocol::direction_command(SpindleState mode, ModbusCommand& data) { + data.tx_length = 9; + data.rx_length = 6; + + data.msg[1] = 0x10; // WRITE + data.msg[2] = 0x09; // Register address, high byte (spindle status) + data.msg[3] = 0x00; // Register address, low byte (spindle status) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x01; // Number of elements, low byte (1 element) + data.msg[6] = 0x02; // Length of first element in bytes (1 regsiter with 2 bytes length) + data.msg[7] = 0x00; // Data, high byte + + /* + Contents of register 0x0900 + Bit 0: run, 1=run, 0=stop + Bit 1: direction, 1=ccw, 0=cw + Bit 2: jog, 1=jog, 0=stop + Bit 3: reset, 1=reset, 0=dont reset + Bit 4-15: reserved + */ + + switch (mode) { + case SpindleState::Cw: + data.msg[8] = 0b00000001; // Data, low byte (run, forward) + log_debug("VFD: Set direction CW"); + break; + + case SpindleState::Ccw: + data.msg[8] = 0b00000011; // Data, low byte (run, reverse) + log_debug("VFD: Set direction CCW"); + break; + + case SpindleState::Disable: + data.msg[8] = 0b00000000; // Data, low byte (run, reverse) + log_debug("VFD: Disabled spindle"); + break; + + default: + log_debug("VFD: Unknown spindle state"); + break; + } + } + + void NowForeverProtocol::set_speed_command(uint32_t hz, ModbusCommand& data) { + data.tx_length = 9; + data.rx_length = 6; + + data.msg[1] = 0x10; // WRITE + data.msg[2] = 0x09; // Register address, high byte (speed in hz) + data.msg[3] = 0x01; // Register address, low byte (speed in hz) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x01; // Number of elements, low byte (1 element) + data.msg[6] = 0x02; // Length of first element in bytes (1 regsiter with 2 bytes length) + + /* + Contents of register 0x0901 + Bit 0-15: speed in hz + */ + + data.msg[7] = hz >> 8; // Data, high byte + data.msg[8] = hz & 0xFF; // Data, low byte + + log_debug("VFD: Set speed: " << hz / 100 << "hz or" << (hz * 60 / 100) << "rpm"); + } + + VFDProtocol::response_parser NowForeverProtocol::initialization_sequence(int index, ModbusCommand& data) { + if (index == -1) { + data.tx_length = 6; + data.rx_length = 7; + + data.msg[1] = 0x03; // READ + data.msg[2] = 0x00; // Register address, high byte (speed in hz) + data.msg[3] = 0x07; // Register address, low byte (speed in hz) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x02; // Number of elements, low byte (2 elements) + + /* + Contents of register 0x0007 + Bit 0-15: max speed in hz * 100 + + Contents of register 0x0008 + Bit 0-15: min speed in hz * 100 + */ + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + if (response[1] != 0x03) { + return false; + } + + // We expect a result length of 4 bytes + if (response[2] != 4) { + return false; + } + + auto nowForever = static_cast(detail); + + nowForever->_minFrequency = (uint16_t(response[5]) << 8) | uint16_t(response[6]); + nowForever->_maxFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + log_debug("VFD: Min frequency: " << nowForever->_minFrequency + << "hz Min speed:" << (nowForever->_minFrequency * 60 / 100) << "rpm"); + log_debug("VFD: Max frequency: " << nowForever->_maxFrequency + << "hz Max speed:" << (nowForever->_maxFrequency * 60 / 100) << "rpm"); + + nowForever->updateRPM(vfd); + + return true; + }; + } + + return nullptr; + } + + void NowForeverProtocol::updateRPM(VFDSpindle* spindle) { + if (_minFrequency > _maxFrequency) { + uint16_t tmp = _minFrequency; + _minFrequency = _maxFrequency; + _maxFrequency = _minFrequency; + } + + if (spindle->_speeds.size() == 0) { + SpindleSpeed minRPM = _minFrequency * 60 / 100; + SpindleSpeed maxRPM = _maxFrequency * 60 / 100; + + spindle->shelfSpeeds(minRPM, maxRPM); + } + spindle->setupSpeeds(_maxFrequency); + spindle->_slop = std::max(_maxFrequency / 400, 1); + } + + VFDProtocol::response_parser NowForeverProtocol::get_current_speed(ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 5; + + data.msg[1] = 0x03; // READ + data.msg[2] = 0x05; // Register address, high byte (current output frequency in hz) + data.msg[3] = 0x02; // Register address, low byte (current output frequency in hz) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x01; // Number of elements, low byte (1 element) + + /* + Contents of register 0x0502 + Bit 0-15: current output frequency in hz * 100 + */ + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t currentHz = 0; + + if (response[1] != 0x03) { + return false; + } + + // We expect a result length of 2 bytes + if (response[2] != 2) { + return false; + } + + // Conversion from hz to rpm not required ? + vfd->_sync_dev_speed = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + log_debug("VFD: Current speed: " << vfd->_sync_dev_speed / 100 << "hz or " << (vfd->_sync_dev_speed * 60 / 100) << "rpm"); + + return true; + }; + } + + VFDProtocol::response_parser NowForeverProtocol::get_current_direction(ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 5; + + data.msg[1] = 0x03; // READ + data.msg[2] = 0x05; // Register address, high byte (inverter running state) + data.msg[3] = 0x00; // Register address, low byte (inverter running state) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x01; // Number of elements, low byte (1 element) + + /* + Contents of register 0x0500 + Bit 0: run, 1=run, 0=stop + Bit 1: direction, 1=ccw, 0=cw + Bit 2: control, 1=local, 0=remote + Bit 3: sight fault, 1=fault, 0=no fault + Bit 4: fault, 1=fault, 0=no fault + Bit 5-15: reserved + */ + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + bool running = false; + bool direction = false; // false = cw, true = ccw + + if (response[1] != 0x03) { + return false; + } + + // We expect a result length of 2 bytes + if (response[2] != 2) { + return false; + } + + running = response[4] & 0b00000001; + direction = (response[4] & 0b00000001) >> 1; + + //TODO: Check what to do with the inform ation we have now. + if (running) { + if (direction) { + log_debug("VFD: Got direction CW"); + } else { + log_debug("VFD: Got direction CCW"); + } + } else { + log_debug("VFD: Got spindle not running"); + } + + return true; + }; + } + + VFDProtocol::response_parser NowForeverProtocol::get_status_ok(ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 5; + + data.msg[1] = 0x03; // READ + data.msg[2] = 0x03; // Register address, high byte (current fault number) + data.msg[3] = 0x00; // Register address, low byte (current fault number) + data.msg[4] = 0x00; // Number of elements, high byte + data.msg[5] = 0x01; // Number of elements, low byte (1 element) + + /* + Contents of register 0x0300 + Bit 0-15: current fault number, 0 = no fault, 1~18 = fault number + */ + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t currentFaultNumber = 0; + + if (response[1] != 0x03) { + return false; + } + + // We expect a result length of 2 bytes + if (response[2] != 2) { + return false; + } + + currentFaultNumber = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + if (currentFaultNumber != 0) { + log_debug("VFD: Got fault number: " << currentFaultNumber); + return false; + } + + return true; + }; + } + + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("NowForever"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/NowForeverProtocol.h b/FluidNC/src/Spindles/VFD/NowForeverProtocol.h new file mode 100644 index 000000000..fcb5763c8 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/NowForeverProtocol.h @@ -0,0 +1,26 @@ +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class NowForeverProtocol : public VFDProtocol { + protected: + uint16_t _minFrequency = 0; + uint16_t _maxFrequency = 0; + + void updateRPM(VFDSpindle* spindle); + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t hz, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_current_speed(ModbusCommand& data) override; + response_parser get_current_direction(ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override; + bool safety_polling() const { return true; } + + bool use_delay_settings() const override { return false; } + }; + } +} diff --git a/FluidNC/src/Spindles/SiemensV20Spindle.cpp b/FluidNC/src/Spindles/VFD/SiemensV20Protocol.cpp similarity index 58% rename from FluidNC/src/Spindles/SiemensV20Spindle.cpp rename to FluidNC/src/Spindles/VFD/SiemensV20Protocol.cpp index 3c701078a..2152b4341 100644 --- a/FluidNC/src/Spindles/SiemensV20Spindle.cpp +++ b/FluidNC/src/Spindles/VFD/SiemensV20Protocol.cpp @@ -102,114 +102,134 @@ Take note that the serial interface use EVEN parity! */ -#include "SiemensV20Spindle.h" +#include "SiemensV20Protocol.h" + +#include "../VFDSpindle.h" #include // std::max namespace Spindles { - // Baud rate is set in the PD164 setting. If it is not 9600, add, for example, - // _baudrate = 19200; - - void SiemensV20::direction_command(SpindleState mode, ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 6; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x06; - data.msg[2] = 0x00; - data.msg[3] = 0x63; - - switch (mode) { - case SpindleState::Cw: - data.msg[4] = 0x0C; - data.msg[5] = 0x7F; - break; - case SpindleState::Ccw: - data.msg[4] = 0x04; - data.msg[5] = 0x7F; - break; - default: // SpindleState::Disable - data.msg[4] = 0x0C; - data.msg[5] = 0x7E; - break; + namespace VFD { + // Baud rate is set in the PD164 setting. If it is not 9600, add, for example, + // _baudrate = 19200; + + void SiemensV20Protocol::direction_command(SpindleState mode, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 6; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x06; + data.msg[2] = 0x00; + data.msg[3] = 0x63; + + switch (mode) { + case SpindleState::Cw: + data.msg[4] = 0x0C; + data.msg[5] = 0x7F; + break; + case SpindleState::Ccw: + data.msg[4] = 0x04; + data.msg[5] = 0x7F; + break; + default: // SpindleState::Disable + data.msg[4] = 0x0C; + data.msg[5] = 0x7E; + break; + } } - } - void IRAM_ATTR SiemensV20::set_speed_command(uint32_t speed, ModbusCommand& data) { - // The units for setting SiemensV20 speed are Hz * 100. For a 2-pole motor, - // RPM is Hz * 60 sec/min. The maximum possible speed is 400 Hz so - // 400 * 60 = 24000 RPM. + void SiemensV20Protocol::set_speed_command(uint32_t speed, ModbusCommand& data) { + // The units for setting SiemensV20 speed are Hz * 100. For a 2-pole motor, + // RPM is Hz * 60 sec/min. The maximum possible speed is 400 Hz so + // 400 * 60 = 24000 RPM. - log_debug("Setting VFD speed to " << uint32_t(speed)); + log_debug("Setting VFD speed to " << uint32_t(speed)); - if (speed != 0 && (speed < _minFrequency || speed > _maxFrequency)) { - log_warn(name() << " requested freq " << uint32_t(speed) << " is outside of range (" << _minFrequency << "," << _maxFrequency - << ")"); - } - /* + if (speed != 0 && (speed < _minFrequency || speed > _maxFrequency)) { + log_warn("Siemens V20 requested freq " << uint32_t(speed) << " is outside of range (" << _minFrequency << "," + << _maxFrequency << ")"); + } + /* V20 has a scalled input and is standardized to 16384 please note Signed numbers work IE -16384 to 16384 but for this implementation only posivite number are allowed */ - int16_t ScaledFreq = speed * _FreqScaler; - log_debug("Setting VFD Scaled Value " << int16_t(ScaledFreq) << " Byte 1 " << uint8_t(ScaledFreq >> 8) << " Byte 2 " - << uint8_t(ScaledFreq & 0xFF)); - - data.tx_length = 6; - data.rx_length = 6; - - data.msg[1] = 0x06; - data.msg[2] = 0x00; - data.msg[3] = 0x64; - data.msg[4] = ScaledFreq >> 8; - data.msg[5] = ScaledFreq & 0xFF; - } - VFD::response_parser SiemensV20::initialization_sequence(int index, ModbusCommand& data) { - /* - The VFD does not have any noticeable registers to set this information up programmatically - For now - it is user set in the software but is a typical setup - */ - if (_minFrequency > _maxFrequency) { - _minFrequency = _maxFrequency; + int16_t ScaledFreq = speed * _FreqScaler; + log_debug("Setting VFD Scaled Value " << int16_t(ScaledFreq) << " Byte 1 " << uint8_t(ScaledFreq >> 8) << " Byte 2 " + << uint8_t(ScaledFreq & 0xFF)); + + data.tx_length = 6; + data.rx_length = 6; + + data.msg[1] = 0x06; + data.msg[2] = 0x00; + data.msg[3] = 0x64; + data.msg[4] = ScaledFreq >> 8; + data.msg[5] = ScaledFreq & 0xFF; } - if (_speeds.size() == 0) { - //RPM = (Frequency * (360/ Num_Phases))/Num_Poles - SpindleSpeed minRPM = (_minFrequency * (360 / _NumberPhases)) / _numberPoles; - SpindleSpeed maxRPM = (_maxFrequency * (360 / _NumberPhases)) / _numberPoles; - shelfSpeeds(minRPM, maxRPM); + + VFDProtocol::response_parser SiemensV20Protocol::initialization_sequence(int index, ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 5; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x03; + data.msg[2] = 0x00; + data.msg[3] = 0x6E; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + /* + The VFD does not have any noticeable registers to set this information up programmatically + For now - it is user set in the software but is a typical setup + */ + auto siemens = static_cast(detail); + + if (siemens->_minFrequency > siemens->_maxFrequency) { + siemens->_minFrequency = siemens->_maxFrequency; + } + if (vfd->_speeds.size() == 0) { + //RPM = (Frequency * (360/ Num_Phases))/Num_Poles + SpindleSpeed minRPM = (siemens->_minFrequency * (360 / siemens->_NumberPhases)) / siemens->_numberPoles; + SpindleSpeed maxRPM = (siemens->_maxFrequency * (360 / siemens->_NumberPhases)) / siemens->_numberPoles; + vfd->shelfSpeeds(minRPM, maxRPM); + } + vfd->setupSpeeds(siemens->_maxFrequency); + vfd->_slop = std::max(siemens->_maxFrequency / 40, 1); + return true; + }; } - setupSpeeds(_maxFrequency); - _slop = std::max(_maxFrequency / 40, 1); - return nullptr; - } - VFD::response_parser SiemensV20::get_current_speed(ModbusCommand& data) { - // NOTE: data length is excluding the CRC16 checksum. - data.tx_length = 6; - data.rx_length = 5; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x03; - data.msg[2] = 0x00; - data.msg[3] = 0x6E; - data.msg[4] = 0x00; - data.msg[5] = 0x01; - - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - auto siemensV20 = static_cast(vfd); - int16_t Scaledfrequency = ((response[3] << 8) | response[4]); - int16_t frequency = float(Scaledfrequency) / (-1 * (siemensV20->_FreqScaler)); - log_debug("VFD Measured Value " << int16_t(Scaledfrequency) << " Freq " << int16_t(frequency)); - - // Store speed for synchronization - vfd->_sync_dev_speed = uint16_t(frequency); - return true; - }; - } + VFDProtocol::response_parser SiemensV20Protocol::get_current_speed(ModbusCommand& data) { + // NOTE: data length is excluding the CRC16 checksum. + data.tx_length = 6; + data.rx_length = 5; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x03; + data.msg[2] = 0x00; + data.msg[3] = 0x6E; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + auto siemensV20 = static_cast(detail); + int16_t Scaledfrequency = ((response[3] << 8) | response[4]); + int16_t frequency = float(Scaledfrequency) / (-1 * (siemensV20->_FreqScaler)); + log_debug("VFD Measured Value " << int16_t(Scaledfrequency) << " Freq " << int16_t(frequency)); + + // Store speed for synchronization + vfd->_sync_dev_speed = uint16_t(frequency); + return true; + }; + } - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("SiemensV20"); + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("SiemensV20"); + } } } diff --git a/FluidNC/src/Spindles/VFD/SiemensV20Protocol.h b/FluidNC/src/Spindles/VFD/SiemensV20Protocol.h new file mode 100644 index 000000000..b7eabd39f --- /dev/null +++ b/FluidNC/src/Spindles/VFD/SiemensV20Protocol.h @@ -0,0 +1,32 @@ +// Copyright (c) 2020 - Bart Dring +// Copyright (c) 2020 - Stefan de Bruijn +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class SiemensV20Protocol : public VFDProtocol { + protected: + uint16_t _minFrequency = 0; // PD011: frequency lower limit. Normally 0. + uint16_t _maxFrequency = 400; // PD005: max frequency the VFD will allow. Normally 400. + uint16_t _numberPoles = 2; // 4 or 2 poles in motor. Default is 4. A spindle being 24000RPM@400Hz implies 2 poles + uint16_t _NumberPhases = 3; // Typically 3 Phases for standard VFDs + + float _FreqScaler = + float(-16384.0) / + _maxFrequency; // SPEED_SCALING Internally it is standardized to 16384. (16384 / 400) With this scaling HSW and HIW are transferred via the Modbus register. + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t rpm, ModbusCommand& data) override; + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_current_speed(ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } + + bool use_delay_settings() const override { return false; } + bool safety_polling() const override { return false; } + }; + } +} diff --git a/FluidNC/src/Spindles/VFD/VFDProtocol.cpp b/FluidNC/src/Spindles/VFD/VFDProtocol.cpp new file mode 100644 index 000000000..84458bb4b --- /dev/null +++ b/FluidNC/src/Spindles/VFD/VFDProtocol.cpp @@ -0,0 +1,291 @@ +#include "VFDProtocol.h" + +#include "../VFDSpindle.h" +#include "../../MotionControl.h" // mc_critical + +#include +#include +#include + +namespace Spindles +{ + namespace VFD + { + const int VFD_RS485_BUF_SIZE = 127; + const int RESPONSE_WAIT_MS = 1000; // how long to wait for a response + const int VFD_RS485_POLL_RATE = 250; // in milliseconds between commands + const TickType_t response_ticks = RESPONSE_WAIT_MS / portTICK_PERIOD_MS; // in milliseconds between commands + + QueueHandle_t VFDProtocol::vfd_cmd_queue = nullptr; + TaskHandle_t VFDProtocol::vfd_cmdTaskHandle = nullptr; + + void VFDProtocol::reportParsingErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length) { + #ifdef DEBUG_VFD + hex_msg(cmd.msg, "RS485 Tx: ", cmd.tx_length); + hex_msg(rx_message, "RS485 Rx: ", read_length); + #endif + } + void VFDProtocol::reportCmdErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length, uint8_t id) { + #ifdef DEBUG_VFD + hex_msg(cmd.msg, "RS485 Tx: ", cmd.tx_length); + hex_msg(rx_message, "RS485 Rx: ", read_length); + + if (read_length != 0) { + if (rx_message[0] != id) { + log_info("RS485 received message from other modbus device"); + } else if (read_length != cmd.rx_length) { + log_info("RS485 received message of unexpected length; expected:" << int(cmd.rx_length) << " got:" << int(read_length)); + } else { + log_info("RS485 CRC check failed"); + } + } else { + log_info("RS485 No response"); + } + #endif + } + + // The communications task + void VFDProtocol::vfd_cmd_task(void* pvParameters) { + static bool unresponsive = false; // to pop off a message once each time it becomes unresponsive + static int pollidx = -1; + + VFDSpindle* instance = static_cast(pvParameters); + auto impl = instance->detail_; + auto& uart = *instance->_uart; + ModbusCommand next_cmd; + uint8_t rx_message[VFD_RS485_MAX_MSG_SIZE]; + bool safetyPollingEnabled = impl->safety_polling(); + + for (; true; delay_ms(VFD_RS485_POLL_RATE)) { + std::atomic_thread_fence(std::memory_order::memory_order_seq_cst); // read fence for settings + response_parser parser = nullptr; + + // First check if we should ask the VFD for the speed parameters as part of the initialization. + if (pollidx < 0 && (parser = impl->initialization_sequence(pollidx, next_cmd)) != nullptr) { + } else { + pollidx = 1; // Done with initialization. Main sequence. + } + next_cmd.critical = false; + + VFDaction action; + if (parser == nullptr) { + // If we don't have a parser, the queue goes first. + if (xQueueReceive(vfd_cmd_queue, &action, 0)) { + switch (action.action) { + case actionSetSpeed: + if (!impl->prepareSetSpeedCommand(action.arg, next_cmd, instance)) { + // prepareSetSpeedCommand() can return false if the speed + // change is unnecessary - already at that speed. + // In that case we just discard the command. + continue; // main loop + } + next_cmd.critical = action.critical; + break; + case actionSetMode: + log_debug("vfd_cmd_task mode:" << action.action); + if (!impl->prepareSetModeCommand(SpindleState(action.arg), next_cmd, instance)) { + continue; // main loop + } + next_cmd.critical = action.critical; + break; + } + } else { + // We do not have a parser and there is nothing in the queue, so we cycle + // through the set of periodic queries. + + // 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. + if (instance->_syncing) { + parser = impl->get_current_speed(next_cmd); + } else if (safetyPollingEnabled) { + switch (pollidx) { + case 1: + parser = impl->get_current_speed(next_cmd); + if (parser) { + pollidx = 2; + break; + } + // fall through if get_current_speed did not return a parser + case 2: + parser = impl->get_current_direction(next_cmd); + if (parser) { + pollidx = 3; + break; + } + // fall through if get_current_direction did not return a parser + case 3: + default: + parser = impl->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) { + continue; // main loop + } + } + } + + // At this point next_cmd has been filled with a command block + { + // Fill in the fields that are the same for all protocol variants + next_cmd.msg[0] = instance->_modbus_id; + + // Grabbed the command. Add the CRC16 checksum: + auto crc16 = ModRTU_CRC(next_cmd.msg, next_cmd.tx_length); + next_cmd.msg[next_cmd.tx_length++] = (crc16 & 0xFF); + next_cmd.msg[next_cmd.tx_length++] = (crc16 & 0xFF00) >> 8; + next_cmd.rx_length += 2; + + #ifdef DEBUG_VFD_ALL + if (parser == nullptr) { + 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(); + uart.write(next_cmd.msg, next_cmd.tx_length); + uart.flushTxTimed(response_ticks); + + // Read the response + size_t read_length = 0; + size_t current_read = uart.timedReadBytes(rx_message, next_cmd.rx_length, response_ticks); + read_length += current_read; + + // Apparently some Huanyang report modbus errors in the correct way, and the rest not. Sigh. + // Let's just check for the condition, and truncate the first byte. + if (read_length > 0 && instance->_modbus_id != 0 && rx_message[0] == 0) { + memmove(rx_message + 1, rx_message, read_length - 1); + } + + while (read_length < next_cmd.rx_length && current_read > 0) { + // Try to read more; we're not there yet... + current_read = uart.timedReadBytes(rx_message + read_length, next_cmd.rx_length - read_length, response_ticks); + read_length += current_read; + } + + // 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] == instance->_modbus_id && // 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) { + if (parser(rx_message, instance, impl)) { + // If we're initializing, move to the next initialization command: + if (pollidx < 0) { + --pollidx; + } + } else { + // Parsing failed + reportParsingErrors(next_cmd, rx_message, read_length); + + // If we were initializing, move back to where we started. + unresponsive = true; + pollidx = -1; // Re-initializing the VFD seems like a plan + log_info("Spindle RS485 did not give a satisfying response"); + } + } + } else { + reportCmdErrors(next_cmd, rx_message, read_length, instance->_modbus_id); + + // Wait a bit before we retry. Set the delay to poll-rate. Not sure + // if we should use a different value... + delay_ms(VFD_RS485_POLL_RATE); + + #ifdef DEBUG_TASK_STACK + static UBaseType_t uxHighWaterMark = 0; + reportTaskStackSize(uxHighWaterMark); + #endif + } + } + + if (retry_count == MAX_RETRIES) { + if (!unresponsive) { + log_info("VFD RS485 Unresponsive"); + unresponsive = true; + pollidx = -1; + } + if (next_cmd.critical) { + mc_critical(ExecAlarm::SpindleControl); + log_error("Critical VFD RS485 Unresponsive"); + } + } + } + } + + bool VFDProtocol::prepareSetModeCommand(SpindleState mode, ModbusCommand& data, VFDSpindle* spindle) { + // Do variant-specific command preparation + direction_command(mode, data); + + if (mode == SpindleState::Disable) { + if (!xQueueReset(vfd_cmd_queue)) { + log_info(spindle->name() << " spindle off, queue could not be reset"); + } + } + + spindle->_current_state = mode; + return true; + } + + bool VFDProtocol::prepareSetSpeedCommand(uint32_t speed, ModbusCommand& data, VFDSpindle* spindle) { + log_debug("prep speed " << speed << " curr " << spindle->_current_dev_speed); + if (speed == spindle->_current_dev_speed) { // prevent setting same speed twice + return false; + } + spindle->_current_dev_speed = speed; + + #ifdef DEBUG_VFD_ALL + log_debug("Setting spindle speed to:" << int(speed)); + #endif + // Do variant-specific command preparation + set_speed_command(speed, data); + + // Sometimes sync_dev_speed is retained between different set_speed_command's. We don't want that - we want + // spindle sync to kick in after we set the speed. This forces that. + spindle->_sync_dev_speed = UINT32_MAX; + + return true; + } + + // 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 VFDProtocol::ModRTU_CRC(uint8_t* buf, int msg_len) { + uint16_t crc = 0xFFFF; + for (int pos = 0; pos < msg_len; pos++) { + crc ^= uint16_t(buf[pos]); // XOR byte into least sig. byte of crc. + + for (int i = 8; i != 0; i--) { // Loop over each bit + if ((crc & 0x0001) != 0) { // If the LSB is set + crc >>= 1; // Shift right and XOR 0xA001 + crc ^= 0xA001; + } else { // Else LSB is not set + crc >>= 1; // Just shift right + } + } + } + + return crc; + } + } +} diff --git a/FluidNC/src/Spindles/VFD/VFDProtocol.h b/FluidNC/src/Spindles/VFD/VFDProtocol.h new file mode 100644 index 000000000..70a14c434 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/VFDProtocol.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include "../Spindle.h" + +namespace Spindles { + class VFDSpindle; + + namespace VFD { + // VFDProtocol resides in a separate class because it doesn't need to be in IRAM. This contains all the + // VFD specific code, which is called from a separate task. + class VFDProtocol { + public: + using response_parser = bool (*)(const uint8_t* response, VFDSpindle* spindle, VFDProtocol* detail); + + static const int VFD_RS485_MAX_MSG_SIZE = 16; // more than enough for a modbus message + static const int MAX_RETRIES = 5; // otherwise the spindle is marked 'unresponsive' + + 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]; + }; + + protected: + // Enable spindown / spinup settings: + virtual bool use_delay_settings() const { return true; } + + // Commands: + virtual void direction_command(SpindleState 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): + + virtual response_parser initialization_sequence(int index, ModbusCommand& data) { return nullptr; } + virtual response_parser get_current_speed(ModbusCommand& data) { return nullptr; } + virtual response_parser get_current_direction(ModbusCommand& data) { return nullptr; } + virtual response_parser get_status_ok(ModbusCommand& data) = 0; + virtual bool safety_polling() const { return true; } + + private: + friend class Spindles::VFDSpindle; // For ISR related things. + + enum VFDactionType : uint8_t { actionSetSpeed, actionSetMode }; + + struct VFDaction { + VFDactionType action; + bool critical; + uint32_t arg; + }; + + // Careful observers will notice that these *shouldn't* be static, but they are. The reason is + // hard to track down. In the spindle class, you can find: + // + // 'virtual void init() = 0; // not in constructor because this also gets called when $$ settings change' + // + // With init being called multiple times, static suddenly makes more sense - especially since there is + // no de-init. Oh well... + + 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); + bool prepareSetModeCommand(SpindleState mode, ModbusCommand& data, VFDSpindle* spindle); + bool prepareSetSpeedCommand(uint32_t speed, ModbusCommand& data, VFDSpindle* spindle); + + static void reportParsingErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length); + static void reportCmdErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length, uint8_t id); + + public: + VFDProtocol() {} + VFDProtocol(const VFDProtocol&) = delete; + VFDProtocol(VFDProtocol&&) = delete; + VFDProtocol& operator=(const VFDProtocol&) = delete; + VFDProtocol& operator=(VFDProtocol&&) = delete; + }; + } +} diff --git a/FluidNC/src/Spindles/VFD/YL620Protocol.cpp b/FluidNC/src/Spindles/VFD/YL620Protocol.cpp new file mode 100644 index 000000000..a4643cf5d --- /dev/null +++ b/FluidNC/src/Spindles/VFD/YL620Protocol.cpp @@ -0,0 +1,231 @@ +// Copyright (c) 2021 - Marco Wagner +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +/* + This is for a Yalang YL620/YL620-A VFD based spindle to be controlled via RS485 Modbus RTU. + + WARNING!!!! + VFDs are very dangerous. They have high voltages and are very powerful + Remove power before changing bits. + + ============================================================================================================= + A Chinese manual for Modbus communication to YL620 can be found at + https://docs.google.com/document/d/1TkERAvHZby4uad_i9kSk19HlDhbM7_xd/edit + You can use Google Translate to translate it. + + Only Modbus RTU mode is supported, not Modbus ASCII mode. + + Manual Configuration required for the YL620 + + Parameter number Description Value + ------------------------------------------------------------------------------- + P00.00 Main frequency 400.00Hz (match to your spindle) + P00.01 Command source 3 + + P03.00 RS485 Baud rate 3 (9600) + P03.01 RS485 address 1 + P03.02 RS485 protocol 2 + P03.08 Frequency given lower limit 100.0Hz (match to your spindle cooling-type) + + =============================================================================================================== + + RS485 communication is standard Modbus RTU + + Therefore, the following operation codes are relevant: + 0x03: read single holding register + 0x06: write single holding register + + Given a parameter Pnn.mm, the high byte of the register address is nn, + the low is mm. The numbers nn and mm in the manual are given in decimal, + so P13.16 would be register address 0x0d10 when represented in hex. + + Holding register address Description + --------------------------------------------------------------------------- + 0x0000 main frequency + 0x0308 frequency given lower limit + + 0x2000 command register (further information below) + 0x2001 Modbus485 frequency command (x0.1Hz => 2500 = 250.0Hz) + + 0x200A Target frequency + 0x200B Output frequency + 0x200C Output current + + + Command register at holding address 0x2000 + -------------------------------------------------------------------------- + bit 1:0 b00: No function + b01: shutdown command + b10: start command + b11: Jog command + bit 3:2 reserved + bit 5:4 b00: No function + b01: Forward command + b10: Reverse command + b11: change direction + bit 7:6 b00: No function + b01: reset an error flag + b10: reset all error flags + b11: reserved +*/ + +#include "YL620Protocol.h" + +#include "../VFDSpindle.h" + +#include + +namespace Spindles { + namespace VFD { + void YL620Protocol::direction_command(SpindleState mode, ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 6; + + // data.msg[0] is omitted (modbus address is filled in later) + data.msg[1] = 0x06; // 06: write output register + data.msg[2] = 0x20; // 0x2000: command register address + data.msg[3] = 0x00; + + data.msg[4] = 0x00; // High-Byte of command always 0x00 + switch (mode) { + case SpindleState::Cw: + data.msg[5] = 0x12; // Start in forward direction + break; + case SpindleState::Ccw: + data.msg[5] = 0x22; // Start in reverse direction + break; + default: // SpindleState::Disable + data.msg[5] = 0x01; // Disable spindle + break; + } + } + + void YL620Protocol::set_speed_command(uint32_t speed, ModbusCommand& data) { +#ifdef DEBUG_VFD + log_debug("Setting VFD speed to " << speed); +#endif + + data.tx_length = 6; + data.rx_length = 6; + + data.msg[1] = 0x06; + data.msg[2] = 0x20; + data.msg[3] = 0x01; + data.msg[4] = speed >> 8; + data.msg[5] = speed & 0xFF; + } + + VFDProtocol::response_parser YL620Protocol::initialization_sequence(int index, ModbusCommand& data) { + if (index == -1) { + data.tx_length = 6; + data.rx_length = 5; + + data.msg[1] = 0x03; + data.msg[2] = 0x03; + data.msg[3] = 0x08; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + // Recv: 01 03 02 03 E8 xx xx + // -- -- = 1000 + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + auto yl620 = static_cast(detail); + yl620->_minFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + +#ifdef DEBUG_VFD + log_debug("YL620 allows minimum frequency of:" << yl620->_minFrequency << " Hz"); +#endif + + return true; + }; + } else if (index == -2) { + data.tx_length = 6; + data.rx_length = 5; + + data.msg[1] = 0x03; + data.msg[2] = 0x00; + data.msg[3] = 0x00; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + // Recv: 01 03 02 0F A0 xx xx + // -- -- = 4000 + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + auto yl620 = static_cast(detail); + yl620->_maxFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + // frequency is in Hz * 10, so RPM is frequency * 60 / 10 = frequency * 6 + // E.g. for 400 Hz, we have frequency = 4000, so 4000 * 6 = 24000 RPM + + if (vfd->_speeds.size() == 0) { + // Convert from frequency in deciHz to RPM (*60/10) + SpindleSpeed maxRPM = yl620->_maxFrequency * 6; + SpindleSpeed minRPM = yl620->_minFrequency * 6; + + vfd->shelfSpeeds(minRPM, maxRPM); + } + + vfd->setupSpeeds(yl620->_maxFrequency); + vfd->_slop = std::max(int(yl620->_maxFrequency) / 40, 1); + + // vfd->_min_rpm = uint32_t(vfd->_max_rpm) * uint32_t(yl620->_minFrequency) / + // uint32_t(yl620->_maxFrequency); // 1000 * 24000 / 4000 = 6000 RPM. + +#ifdef DEBUG_VFD + log_debug("YL620 allows maximum frequency " << yl620->_maxFrequency << " Hz"); +#endif + + return true; + }; + } else { + return nullptr; + } + } + + VFDProtocol::response_parser YL620Protocol::get_current_speed(ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 5; + + // Send: 01 03 200B 0001 + data.msg[1] = 0x03; + data.msg[2] = 0x20; + data.msg[3] = 0x0B; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + // Recv: 01 03 02 05 DC xx xx + // ---- = 1500 + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { + uint16_t freq = (uint16_t(response[3]) << 8) | uint16_t(response[4]); + + auto yl620 = static_cast(detail); + + vfd->_sync_dev_speed = freq; + return true; + }; + } + + VFDProtocol::response_parser YL620Protocol::get_current_direction(ModbusCommand& data) { + data.tx_length = 6; + data.rx_length = 5; + + // Send: 01 03 20 00 00 01 + data.msg[1] = 0x03; + data.msg[2] = 0x20; + data.msg[3] = 0x00; + data.msg[4] = 0x00; + data.msg[5] = 0x01; + + // Receive: 01 03 02 00 0A xx xx + // ----- status is in 00 0A bit 5:4 + + // TODO: What are we going to do with this? Update vfd state? + return [](const uint8_t* response, VFDSpindle* vfd, VFDProtocol* detail) -> bool { return true; }; + } + + // Configuration registration + namespace { + SpindleFactory::DependentInstanceBuilder registration("YL620"); + } + } +} diff --git a/FluidNC/src/Spindles/VFD/YL620Protocol.h b/FluidNC/src/Spindles/VFD/YL620Protocol.h new file mode 100644 index 000000000..ac4686fd8 --- /dev/null +++ b/FluidNC/src/Spindles/VFD/YL620Protocol.h @@ -0,0 +1,26 @@ +// Copyright (c) 2021 - Marco Wagner +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "VFDProtocol.h" + +namespace Spindles { + namespace VFD { + class YL620Protocol : public VFDProtocol { + protected: + uint16_t _minFrequency = 0; // frequency lower limit. Factor 10 of actual frequency + uint16_t _maxFrequency = 4000; // max frequency the VFD will allow. Normally 400.0. Factor 10 of actual frequency + + void direction_command(SpindleState mode, ModbusCommand& data) override; + void set_speed_command(uint32_t rpm, ModbusCommand& data) override; + + response_parser initialization_sequence(int index, ModbusCommand& data) override; + response_parser get_current_speed(ModbusCommand& data) override; + response_parser get_current_direction(ModbusCommand& data) override; + response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } + + bool safety_polling() const override { return false; } + }; + } +} diff --git a/FluidNC/src/Spindles/VFDSpindle.cpp b/FluidNC/src/Spindles/VFDSpindle.cpp index 21381dd01..31315e225 100644 --- a/FluidNC/src/Spindles/VFDSpindle.cpp +++ b/FluidNC/src/Spindles/VFDSpindle.cpp @@ -19,242 +19,26 @@ */ #include "VFDSpindle.h" +#include "VFD/VFDProtocol.h" -#include "src/Machine/MachineConfig.h" -#include "src/MotionControl.h" // mc_critical -#include "src/Protocol.h" // rtAlarm -#include "src/Report.h" // hex message -#include "src/Configuration/HandlerType.h" +#include "../Machine/MachineConfig.h" +#include "../Protocol.h" // rtAlarm +#include "../Report.h" // hex message +#include "../Configuration/HandlerType.h" +#include "../Platform.h" #include #include #include -const int VFD_RS485_BUF_SIZE = 127; -const int VFD_RS485_QUEUE_SIZE = 10; // number of commands that can be queued up. -const int RESPONSE_WAIT_MS = 1000; // how long to wait for a response -const int VFD_RS485_POLL_RATE = 250; // in milliseconds between commands -const TickType_t response_ticks = RESPONSE_WAIT_MS / portTICK_PERIOD_MS; // in milliseconds between commands - -namespace Spindles { - QueueHandle_t VFD::vfd_cmd_queue = nullptr; - TaskHandle_t VFD::vfd_cmdTaskHandle = nullptr; - - void VFD::reportParsingErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length) { -#ifdef DEBUG_VFD - hex_msg(cmd.msg, "RS485 Tx: ", cmd.tx_length); - hex_msg(rx_message, "RS485 Rx: ", read_length); -#endif - } - void VFD::reportCmdErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length, uint8_t id) { -#ifdef DEBUG_VFD - hex_msg(cmd.msg, "RS485 Tx: ", cmd.tx_length); - hex_msg(rx_message, "RS485 Rx: ", read_length); - - if (read_length != 0) { - if (rx_message[0] != id) { - log_info("RS485 received message from other modbus device"); - } else if (read_length != cmd.rx_length) { - log_info("RS485 received message of unexpected length; expected:" << int(cmd.rx_length) << " got:" << int(read_length)); - } else { - log_info("RS485 CRC check failed"); - } - } else { - log_info("RS485 No response"); - } -#endif - } - - // 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 = -1; - - VFD* instance = static_cast(pvParameters); - auto& uart = *instance->_uart; - ModbusCommand next_cmd; - uint8_t rx_message[VFD_RS485_MAX_MSG_SIZE]; - bool safetyPollingEnabled = instance->safety_polling(); - - for (; true; delay_ms(VFD_RS485_POLL_RATE)) { - std::atomic_thread_fence(std::memory_order::memory_order_seq_cst); // read fence for settings - response_parser parser = nullptr; - - // First check if we should ask the VFD for the speed parameters as part of the initialization. - if (pollidx < 0 && (parser = instance->initialization_sequence(pollidx, next_cmd)) != nullptr) { - } else { - pollidx = 1; // Done with initialization. Main sequence. - } - next_cmd.critical = false; - - VFDaction action; - if (parser == nullptr) { - // If we don't have a parser, the queue goes first. - if (xQueueReceive(vfd_cmd_queue, &action, 0)) { - switch (action.action) { - case actionSetSpeed: - if (!instance->prepareSetSpeedCommand(action.arg, next_cmd)) { - // prepareSetSpeedCommand() can return false if the speed - // change is unnecessary - already at that speed. - // In that case we just discard the command. - continue; // main loop - } - next_cmd.critical = action.critical; - break; - case actionSetMode: - log_debug("vfd_cmd_task mode:" << action.action); - if (!instance->prepareSetModeCommand(SpindleState(action.arg), next_cmd)) { - continue; // main loop - } - next_cmd.critical = action.critical; - break; - } - } else { - // We do not have a parser and there is nothing in the queue, so we cycle - // through the set of periodic queries. - - // 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. - if (instance->_syncing) { - parser = instance->get_current_speed(next_cmd); - } else if (safetyPollingEnabled) { - switch (pollidx) { - case 1: - parser = instance->get_current_speed(next_cmd); - if (parser) { - pollidx = 2; - break; - } - // fall through if get_current_speed did not return a parser - case 2: - parser = instance->get_current_direction(next_cmd); - if (parser) { - pollidx = 3; - break; - } - // fall through if get_current_direction did not return a parser - case 3: - default: - 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) { - continue; // main loop - } - } - } - - // At this point next_cmd has been filled with a command block - { - // Fill in the fields that are the same for all protocol variants - next_cmd.msg[0] = instance->_modbus_id; - - // Grabbed the command. Add the CRC16 checksum: - auto crc16 = ModRTU_CRC(next_cmd.msg, next_cmd.tx_length); - next_cmd.msg[next_cmd.tx_length++] = (crc16 & 0xFF); - next_cmd.msg[next_cmd.tx_length++] = (crc16 & 0xFF00) >> 8; - next_cmd.rx_length += 2; - -#ifdef DEBUG_VFD_ALL - if (parser == nullptr) { - 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(); - uart.write(next_cmd.msg, next_cmd.tx_length); - uart.flushTxTimed(response_ticks); - - // Read the response - size_t read_length = 0; - size_t current_read = uart.timedReadBytes(rx_message, next_cmd.rx_length, response_ticks); - read_length += current_read; - - // Apparently some Huanyang report modbus errors in the correct way, and the rest not. Sigh. - // Let's just check for the condition, and truncate the first byte. - if (read_length > 0 && instance->_modbus_id != 0 && rx_message[0] == 0) { - memmove(rx_message + 1, rx_message, read_length - 1); - } - - while (read_length < next_cmd.rx_length && current_read > 0) { - // Try to read more; we're not there yet... - current_read = uart.timedReadBytes(rx_message + read_length, next_cmd.rx_length - read_length, response_ticks); - read_length += current_read; - } - - // 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] == instance->_modbus_id && // 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) { - if (parser(rx_message, instance)) { - // If we're initializing, move to the next initialization command: - if (pollidx < 0) { - --pollidx; - } - } else { - // Parsing failed - reportParsingErrors(next_cmd, rx_message, read_length); - - // If we were initializing, move back to where we started. - unresponsive = true; - pollidx = -1; // Re-initializing the VFD seems like a plan - log_info("Spindle RS485 did not give a satisfying response"); - } - } - } else { - reportCmdErrors(next_cmd, rx_message, read_length, instance->_modbus_id); - - // Wait a bit before we retry. Set the delay to poll-rate. Not sure - // if we should use a different value... - delay_ms(VFD_RS485_POLL_RATE); - -#ifdef DEBUG_TASK_STACK - static UBaseType_t uxHighWaterMark = 0; - reportTaskStackSize(uxHighWaterMark); -#endif - } - } - - if (retry_count == MAX_RETRIES) { - if (!unresponsive) { - log_info("VFD RS485 Unresponsive"); - unresponsive = true; - pollidx = -1; - } - if (next_cmd.critical) { - mc_critical(ExecAlarm::SpindleControl); - log_error("Critical VFD RS485 Unresponsive"); - } - } - } - } - +namespace Spindles +{ + // number of commands that can be queued up. + const int VFD_RS485_QUEUE_SIZE = 10; + // ================== Class methods ================================== - void VFD::init() { + void VFDSpindle::init() { _sync_dev_speed = 0; _syncing = false; @@ -283,14 +67,14 @@ namespace Spindles { _current_state = SpindleState::Disable; // Initialization is complete, so now it's okay to run the queue task: - if (!vfd_cmd_queue) { // init can happen many times, we only want to start one task - vfd_cmd_queue = xQueueCreate(VFD_RS485_QUEUE_SIZE, sizeof(VFDaction)); - xTaskCreatePinnedToCore(vfd_cmd_task, // task - "vfd_cmdTaskHandle", // name for task - 2048, // size of task stack - this, // parameters - 1, // priority - &vfd_cmdTaskHandle, + if (!VFD::VFDProtocol::vfd_cmd_queue) { // init can happen many times, we only want to start one task + VFD::VFDProtocol::vfd_cmd_queue = xQueueCreate(VFD_RS485_QUEUE_SIZE, sizeof(VFD::VFDProtocol::VFDaction)); + xTaskCreatePinnedToCore(VFD::VFDProtocol::vfd_cmd_task, // task + "vfd_cmdTaskHandle", // name for task + 2048, // size of task stack + this, // parameters + 1, // priority + &VFD::VFDProtocol::vfd_cmdTaskHandle, SUPPORT_TASK_CORE // core ); } @@ -301,11 +85,22 @@ namespace Spindles { set_mode(SpindleState::Disable, true); } - void VFD::config_message() { - _uart->config_message(name(), " Spindle "); + void VFDSpindle::config_message() { _uart->config_message(name(), " Spindle "); } + + void VFDSpindle::set_mode(SpindleState mode, bool critical) { + _last_override_value = sys.spindle_speed_ovr; // sync these on mode changes + if (VFD::VFDProtocol::vfd_cmd_queue) { + VFD::VFDProtocol::VFDaction action; + action.action = VFD::VFDProtocol::actionSetMode; + action.arg = uint32_t(mode); + action.critical = critical; + if (xQueueSend(VFD::VFDProtocol::vfd_cmd_queue, &action, 0) != pdTRUE) { + log_info("VFD Queue Full"); + } + } } - void VFD::setState(SpindleState state, SpindleSpeed speed) { + void VFDSpindle::setState(SpindleState state, SpindleSpeed speed) { log_debug("VFD setState:" << uint8_t(state) << " SpindleSpeed:" << speed); if (sys.abort) { return; // Block during abort. @@ -328,7 +123,7 @@ namespace Spindles { setSpeed(dev_speed); } } - if (use_delay_settings()) { + if (detail_->use_delay_settings()) { spindleDelay(state, speed); } else { // _sync_dev_speed is set by a callback that handles @@ -380,110 +175,42 @@ namespace Spindles { // } } - bool VFD::prepareSetModeCommand(SpindleState mode, ModbusCommand& data) { - // Do variant-specific command preparation - direction_command(mode, data); - - if (mode == SpindleState::Disable) { - if (!xQueueReset(vfd_cmd_queue)) { - log_info(name() << " spindle off, queue could not be reset"); - } - } - - _current_state = mode; - return true; - } - - void VFD::set_mode(SpindleState mode, bool critical) { - _last_override_value = sys.spindle_speed_ovr; // sync these on mode changes - if (vfd_cmd_queue) { - VFDaction action; - action.action = actionSetMode; - action.arg = uint32_t(mode); - action.critical = critical; - if (xQueueSend(vfd_cmd_queue, &action, 0) != pdTRUE) { - log_info("VFD Queue Full"); - } - } - } - - void IRAM_ATTR VFD::setSpeedfromISR(uint32_t dev_speed) { + void IRAM_ATTR VFDSpindle::setSpeedfromISR(uint32_t dev_speed) { if (_current_dev_speed == dev_speed || _last_speed == dev_speed) { return; } _last_speed = dev_speed; - if (vfd_cmd_queue) { - VFDaction action; - action.action = actionSetSpeed; + if (VFD::VFDProtocol::vfd_cmd_queue) { + VFD::VFDProtocol::VFDaction action; + action.action = VFD::VFDProtocol::actionSetSpeed; action.arg = dev_speed; action.critical = (dev_speed == 0); // Ignore errors because reporting is not safe from an ISR. // Perhaps set a flag instead? - xQueueSendFromISR(vfd_cmd_queue, &action, 0); + xQueueSendFromISR(VFD::VFDProtocol::vfd_cmd_queue, &action, 0); } } - void VFD::setSpeed(uint32_t dev_speed) { - if (vfd_cmd_queue) { - VFDaction action; - action.action = actionSetSpeed; + void VFDSpindle::setSpeed(uint32_t dev_speed) { + if (VFD::VFDProtocol::vfd_cmd_queue) { + VFD::VFDProtocol::VFDaction action; + action.action = VFD::VFDProtocol::actionSetSpeed; action.arg = dev_speed; action.critical = dev_speed == 0; - if (xQueueSend(vfd_cmd_queue, &action, 0) != pdTRUE) { + if (xQueueSend(VFD::VFDProtocol::vfd_cmd_queue, &action, 0) != pdTRUE) { log_info("VFD Queue Full"); } } } - bool VFD::prepareSetSpeedCommand(uint32_t speed, ModbusCommand& data) { - log_debug("prep speed " << speed << " curr " << _current_dev_speed); - if (speed == _current_dev_speed) { // prevent setting same speed twice - return false; - } - _current_dev_speed = speed; - -#ifdef DEBUG_VFD_ALL - log_debug("Setting spindle speed to:" << int(speed)); -#endif - // Do variant-specific command preparation - set_speed_command(speed, data); - - // Sometimes sync_dev_speed is retained between different set_speed_command's. We don't want that - we want - // spindle sync to kick in after we set the speed. This forces that. - _sync_dev_speed = UINT32_MAX; - - return true; - } - - // 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; - } - void VFD::validate() { + void VFDSpindle::validate() { Spindle::validate(); Assert(_uart != nullptr || _uart_num != -1, "VFD: missing UART configuration"); } - void VFD::group(Configuration::HandlerBase& handler) { + void VFDSpindle::group(Configuration::HandlerBase& handler) { if (handler.handlerType() == Configuration::HandlerType::Generator) { if (_uart_num == -1) { handler.section("uart", _uart, 1); diff --git a/FluidNC/src/Spindles/VFDSpindle.h b/FluidNC/src/Spindles/VFDSpindle.h index 52a794858..5ca45cd55 100644 --- a/FluidNC/src/Spindles/VFDSpindle.h +++ b/FluidNC/src/Spindles/VFDSpindle.h @@ -14,61 +14,28 @@ namespace Spindles { extern Uart _uart; - - class VFD : public Spindle { + + + namespace VFD { + // VFDProtocol resides in a separate class because it doesn't need to be in IRAM. This contains all the + // VFD specific code, which is called from a separate task. + class VFDProtocol; + } + + // VFD base class. Called by the stepper engine. Normally you don't want to touch this. + class VFDSpindle : public Spindle { private: - static const int VFD_RS485_MAX_MSG_SIZE = 16; // more than enough for a modbus message - static const int MAX_RETRIES = 5; // otherwise the spindle is marked 'unresponsive' + friend class Spindles::VFD::VFDProtocol; - void set_mode(SpindleState mode, bool critical); + VFD::VFDProtocol* detail_ = nullptr; int32_t _current_dev_speed = -1; uint32_t _last_speed = 0; Percent _last_override_value = 100; // no override is 100 percent - 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); - enum VFDactionType : uint8_t { actionSetSpeed, actionSetMode }; - struct VFDaction { - VFDactionType action; - bool critical; - uint32_t arg; - }; - - 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]; - }; - - private: - bool prepareSetModeCommand(SpindleState mode, ModbusCommand& data); - bool prepareSetSpeedCommand(uint32_t speed, ModbusCommand& data); - - static void reportParsingErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length); - static void reportCmdErrors(ModbusCommand cmd, uint8_t* rx_message, size_t read_length, uint8_t id); + void set_mode(SpindleState mode, bool critical); protected: - // Commands: - virtual void direction_command(SpindleState 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 initialization_sequence(int index, ModbusCommand& data) { return nullptr; } - virtual response_parser get_current_speed(ModbusCommand& data) { return nullptr; } - virtual response_parser get_current_direction(ModbusCommand& data) { return nullptr; } - virtual response_parser get_status_ok(ModbusCommand& data) = 0; - virtual bool safety_polling() const { return true; } - bool use_delay_settings() const override { return true; } - // The constructor sets these int _uart_num = -1; Uart* _uart = nullptr; @@ -79,11 +46,11 @@ namespace Spindles { volatile bool _syncing; public: - VFD(const char* name) : Spindle(name) {} - VFD(const VFD&) = delete; - VFD(VFD&&) = delete; - VFD& operator=(const VFD&) = delete; - VFD& operator=(VFD&&) = delete; + VFDSpindle(const char* name, VFD::VFDProtocol* detail) : Spindle(name), detail_(detail) {} + VFDSpindle(const VFDSpindle&) = delete; + VFDSpindle(VFDSpindle&&) = delete; + VFDSpindle& operator=(const VFDSpindle&) = delete; + VFDSpindle& operator=(VFDSpindle&&) = delete; void init(); void config_message(); @@ -97,6 +64,6 @@ namespace Spindles { void validate() override; void group(Configuration::HandlerBase& handler) override; - virtual ~VFD() {} + virtual ~VFDSpindle() {} }; } diff --git a/FluidNC/src/Spindles/YL620Spindle.cpp b/FluidNC/src/Spindles/YL620Spindle.cpp deleted file mode 100644 index 59adc6588..000000000 --- a/FluidNC/src/Spindles/YL620Spindle.cpp +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) 2021 - Marco Wagner -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -/* - This is for a Yalang YL620/YL620-A VFD based spindle to be controlled via RS485 Modbus RTU. - - WARNING!!!! - VFDs are very dangerous. They have high voltages and are very powerful - Remove power before changing bits. - - ============================================================================================================= - A Chinese manual for Modbus communication to YL620 can be found at - https://docs.google.com/document/d/1TkERAvHZby4uad_i9kSk19HlDhbM7_xd/edit - You can use Google Translate to translate it. - - Only Modbus RTU mode is supported, not Modbus ASCII mode. - - Manual Configuration required for the YL620 - - Parameter number Description Value - ------------------------------------------------------------------------------- - P00.00 Main frequency 400.00Hz (match to your spindle) - P00.01 Command source 3 - - P03.00 RS485 Baud rate 3 (9600) - P03.01 RS485 address 1 - P03.02 RS485 protocol 2 - P03.08 Frequency given lower limit 100.0Hz (match to your spindle cooling-type) - - =============================================================================================================== - - RS485 communication is standard Modbus RTU - - Therefore, the following operation codes are relevant: - 0x03: read single holding register - 0x06: write single holding register - - Given a parameter Pnn.mm, the high byte of the register address is nn, - the low is mm. The numbers nn and mm in the manual are given in decimal, - so P13.16 would be register address 0x0d10 when represented in hex. - - Holding register address Description - --------------------------------------------------------------------------- - 0x0000 main frequency - 0x0308 frequency given lower limit - - 0x2000 command register (further information below) - 0x2001 Modbus485 frequency command (x0.1Hz => 2500 = 250.0Hz) - - 0x200A Target frequency - 0x200B Output frequency - 0x200C Output current - - - Command register at holding address 0x2000 - -------------------------------------------------------------------------- - bit 1:0 b00: No function - b01: shutdown command - b10: start command - b11: Jog command - bit 3:2 reserved - bit 5:4 b00: No function - b01: Forward command - b10: Reverse command - b11: change direction - bit 7:6 b00: No function - b01: reset an error flag - b10: reset all error flags - b11: reserved -*/ - -#include "YL620Spindle.h" - -#include - -namespace Spindles { - void YL620::direction_command(SpindleState mode, ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 6; - - // data.msg[0] is omitted (modbus address is filled in later) - data.msg[1] = 0x06; // 06: write output register - data.msg[2] = 0x20; // 0x2000: command register address - data.msg[3] = 0x00; - - data.msg[4] = 0x00; // High-Byte of command always 0x00 - switch (mode) { - case SpindleState::Cw: - data.msg[5] = 0x12; // Start in forward direction - break; - case SpindleState::Ccw: - data.msg[5] = 0x22; // Start in reverse direction - break; - default: // SpindleState::Disable - data.msg[5] = 0x01; // Disable spindle - break; - } - } - - void IRAM_ATTR YL620::set_speed_command(uint32_t speed, ModbusCommand& data) { -#ifdef DEBUG_VFD - log_debug("Setting VFD speed to " << speed); -#endif - - data.tx_length = 6; - data.rx_length = 6; - - data.msg[1] = 0x06; - data.msg[2] = 0x20; - data.msg[3] = 0x01; - data.msg[4] = speed >> 8; - data.msg[5] = speed & 0xFF; - } - - VFD::response_parser YL620::initialization_sequence(int index, ModbusCommand& data) { - if (index == -1) { - data.tx_length = 6; - data.rx_length = 5; - - data.msg[1] = 0x03; - data.msg[2] = 0x03; - data.msg[3] = 0x08; - data.msg[4] = 0x00; - data.msg[5] = 0x01; - - // Recv: 01 03 02 03 E8 xx xx - // -- -- = 1000 - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - auto yl620 = static_cast(vfd); - yl620->_minFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - -#ifdef DEBUG_VFD - log_debug("YL620 allows minimum frequency of:" << yl620->_minFrequency << " Hz"); -#endif - - return true; - }; - } else if (index == -2) { - data.tx_length = 6; - data.rx_length = 5; - - data.msg[1] = 0x03; - data.msg[2] = 0x00; - data.msg[3] = 0x00; - data.msg[4] = 0x00; - data.msg[5] = 0x01; - - // Recv: 01 03 02 0F A0 xx xx - // -- -- = 4000 - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - auto yl620 = static_cast(vfd); - yl620->_maxFrequency = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - // frequency is in Hz * 10, so RPM is frequency * 60 / 10 = frequency * 6 - // E.g. for 400 Hz, we have frequency = 4000, so 4000 * 6 = 24000 RPM - - if (vfd->_speeds.size() == 0) { - // Convert from frequency in deciHz to RPM (*60/10) - SpindleSpeed maxRPM = yl620->_maxFrequency * 6; - SpindleSpeed minRPM = yl620->_minFrequency * 6; - - vfd->shelfSpeeds(minRPM, maxRPM); - } - - vfd->setupSpeeds(yl620->_maxFrequency); - vfd->_slop = std::max(yl620->_maxFrequency / 40, 1); - - // vfd->_min_rpm = uint32_t(vfd->_max_rpm) * uint32_t(yl620->_minFrequency) / - // uint32_t(yl620->_maxFrequency); // 1000 * 24000 / 4000 = 6000 RPM. - -#ifdef DEBUG_VFD - log_debug("YL620 allows maximum frequency " << yl620->_maxFrequency << " Hz"); -#endif - - return true; - }; - } else { - return nullptr; - } - } - - VFD::response_parser YL620::get_current_speed(ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 5; - - // Send: 01 03 200B 0001 - data.msg[1] = 0x03; - data.msg[2] = 0x20; - data.msg[3] = 0x0B; - data.msg[4] = 0x00; - data.msg[5] = 0x01; - - // Recv: 01 03 02 05 DC xx xx - // ---- = 1500 - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { - uint16_t freq = (uint16_t(response[3]) << 8) | uint16_t(response[4]); - - auto yl620 = static_cast(vfd); - - vfd->_sync_dev_speed = freq; - return true; - }; - } - - VFD::response_parser YL620::get_current_direction(ModbusCommand& data) { - data.tx_length = 6; - data.rx_length = 5; - - // Send: 01 03 20 00 00 01 - data.msg[1] = 0x03; - data.msg[2] = 0x20; - data.msg[3] = 0x00; - data.msg[4] = 0x00; - data.msg[5] = 0x01; - - // Receive: 01 03 02 00 0A xx xx - // ----- status is in 00 0A bit 5:4 - - // TODO: What are we going to do with this? Update vfd state? - return [](const uint8_t* response, Spindles::VFD* vfd) -> bool { return true; }; - } - - // Configuration registration - namespace { - SpindleFactory::InstanceBuilder registration("YL620"); - } -} diff --git a/FluidNC/src/Spindles/YL620Spindle.h b/FluidNC/src/Spindles/YL620Spindle.h deleted file mode 100644 index 49acc239b..000000000 --- a/FluidNC/src/Spindles/YL620Spindle.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2021 - Marco Wagner -// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. - -#pragma once - -#include "VFDSpindle.h" - -namespace Spindles { - class YL620 : public VFD { - protected: - uint16_t _minFrequency = 0; // frequency lower limit. Factor 10 of actual frequency - uint16_t _maxFrequency = 4000; // max frequency the VFD will allow. Normally 400.0. Factor 10 of actual frequency - - void direction_command(SpindleState mode, ModbusCommand& data) override; - void set_speed_command(uint32_t rpm, ModbusCommand& data) override; - - response_parser initialization_sequence(int index, ModbusCommand& data) override; - response_parser get_current_speed(ModbusCommand& data) override; - response_parser get_current_direction(ModbusCommand& data) override; - response_parser get_status_ok(ModbusCommand& data) override { return nullptr; } - - bool safety_polling() const override { return false; } - - public: - YL620(const char* name) : VFD(name) {} - }; -} diff --git a/generate_vcxproj.py b/generate_vcxproj.py index 64ece9536..884baa3fc 100644 --- a/generate_vcxproj.py +++ b/generate_vcxproj.py @@ -47,7 +47,7 @@ class Vcxproj: # configuration, platform ImportGroupFmt = '\n'.join([ ' ', - ' ', + ' ', ' ' ]) @@ -192,12 +192,12 @@ def CreateProject(self): project.append(' Win32Proj') project.append('') - project.append('') + project.append('') for p in self.Platforms: for c in self.Configurations: project.append(Vcxproj.ConfigTypePropertyGroup(c, p)) - project.append('') + project.append('') project.append('') project.append('') project.append(' ') @@ -214,7 +214,7 @@ def CreateProject(self): project.append('') project.append('') - project.append('') + project.append('') project.append(' ') project.append('') project.append('')