From 14757313bc1e3272206e326cf79acd096eeb1104 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Thu, 22 Mar 2018 11:03:25 -0700 Subject: [PATCH] Continuing work on huanyang/modbus split --- CHANGELOG.md | 5 +- src/avr/src/config.h | 19 +- src/avr/src/huanyang.c | 513 ++++++++--------------------- src/avr/src/huanyang.h | 5 +- src/avr/src/modbus.c | 330 +++++++++++++++++++ src/avr/src/modbus.h | 109 ++++++ src/avr/src/pwm_spindle.h | 2 - src/avr/src/rtc.c | 4 +- src/avr/src/usart.c | 38 ++- src/avr/src/usart.h | 3 + src/avr/src/vars.def | 37 ++- src/jade/templates/tool-view.jade | 14 +- src/js/tool-view.js | 42 +-- src/resources/config-template.json | 12 +- 14 files changed, 689 insertions(+), 444 deletions(-) create mode 100644 src/avr/src/modbus.c create mode 100644 src/avr/src/modbus.h diff --git a/CHANGELOG.md b/CHANGELOG.md index ac11208..7e15bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -Buildbotics CNC Controller Firmware Change Log +Buildbotics CNC Controller Firmware Changelog ============================================== ## v0.3.21 - Implemented M70-M73 modal state save/restore. + - Added support for modbus VFDs. ## v0.3.20 - Eliminated drift caused by miscounting half microsteps. @@ -166,4 +167,4 @@ Buildbotics CNC Controller Firmware Change Log - Start Web server eariler in case of Python coding errors -Change log not maintained in previous versions. See git commit log. +Changelog not maintained in previous versions. See git commit log. diff --git a/src/avr/src/config.h b/src/avr/src/config.h index a8b64de..648e499 100644 --- a/src/avr/src/config.h +++ b/src/avr/src/config.h @@ -172,14 +172,17 @@ enum { DRV8711_CTRL_EXSTALL_bm) -// Huanyang settings -#define HUANYANG_PORT USARTD1 -#define HUANYANG_DRE_vect USARTD1_DRE_vect -#define HUANYANG_TXC_vect USARTD1_TXC_vect -#define HUANYANG_RXC_vect USARTD1_RXC_vect -#define HUANYANG_TIMEOUT 50 // ms. response timeout -#define HUANYANG_RETRIES 4 // Number of retries before failure -#define HUANYANG_ID 1 // Default ID +// RS485 settings +#define RS485_PORT USARTD1 +#define RS485_DRE_vect USARTD1_DRE_vect +#define RS485_TXC_vect USARTD1_TXC_vect +#define RS485_RXC_vect USARTD1_RXC_vect + + +// Modbus settings +#define MODBUS_TIMEOUT 50 // ms. response timeout +#define MODBUS_RETRIES 4 // Number of retries before failure +#define MODBUS_BUF_SIZE 64 // Max bytes in rx/tx buffers // Serial settings diff --git a/src/avr/src/huanyang.c b/src/avr/src/huanyang.c index 547332a..87d47a6 100644 --- a/src/avr/src/huanyang.c +++ b/src/avr/src/huanyang.c @@ -27,29 +27,24 @@ #include "huanyang.h" #include "config.h" -#include "rtc.h" +#include "modbus.h" #include "report.h" -#include "pgmspace.h" -#include -#include -#include - -#include #include #include + /* - Huanyang supposedly is not quite Modbus compliant. + Huanyang is not quite Modbus compliant. Message format is: - [id][cmd][length][data][checksum] + [id][func][length][data][checksum] Where: id - 1-byte Peer ID - cmd - 1-byte One of hy_cmd_t + func - 1-byte One of hy_func_t length - 1-byte Length data in bytes data - length bytes - Command arguments checksum - 16-bit CRC: x^16 + x^15 + x^2 + 1 (0xa001) initial: 0xffff @@ -58,7 +53,7 @@ // See VFD manual pg56 3.1.3 typedef enum { - HUANYANG_FUNC_READ = 1, // Use hy_func_addr_t + HUANYANG_FUNC_READ = 1, // Use hy_addr_t HUANYANG_FUNC_WRITE, // ? HUANYANG_CTRL_WRITE, // Use hy_ctrl_state_t HUANYANG_CTRL_READ, // Use hy_ctrl_addr_t @@ -66,7 +61,7 @@ typedef enum { HUANYANG_RESERVED_1, HUANYANG_RESERVED_2, HUANYANG_LOOP_TEST, -} hy_cmd_t; +} hy_func_t; // See VFD manual pg57 3.1.3.d @@ -106,25 +101,16 @@ typedef enum { } hy_ctrl_status_t; -typedef bool (*next_command_cb_t)(int index); +static struct { + uint8_t id; + uint8_t state; + hy_func_t func; + uint8_t data[4]; + uint8_t bytes; + uint8_t response; -typedef struct { - uint8_t id; - bool debug; - - next_command_cb_t next_command_cb; - uint8_t command_index; - uint8_t current_offset; - uint8_t command[8]; - uint8_t command_length; - uint8_t response_length; - uint8_t response[10]; - uint32_t last; - uint8_t retry; - - uint8_t shutdown; - bool connected; + bool shutdown; bool changed; float speed; @@ -138,424 +124,213 @@ typedef struct { uint16_t rated_rpm; uint8_t status; -} hy_t; - - -static hy_t ha = {0}; - - -#define CTRL_STATUS_RESPONSE(R) ((uint16_t)R[4] << 8 | R[5]) -#define FUNC_RESPONSE(R) (R[2] == 2 ? R[4] : ((uint16_t)R[4] << 8 | R[5])) - - -static uint16_t _crc16(const uint8_t *buffer, unsigned length) { - uint16_t crc = 0xffff; - - for (unsigned i = 0; i < length; i++) - crc = _crc16_update(crc, buffer[i]); - - return crc; -} - - -static void _set_baud(uint16_t bsel, uint8_t bscale) { - HUANYANG_PORT.BAUDCTRLB = (uint8_t)((bscale << 4) | (bsel >> 8)); - HUANYANG_PORT.BAUDCTRLA = bsel; -} - - -static void _set_write(bool x) {SET_PIN(RS485_RW_PIN, x);} - - -static void _set_dre_interrupt(bool enable) { - if (enable) HUANYANG_PORT.CTRLA |= USART_DREINTLVL_MED_gc; - else HUANYANG_PORT.CTRLA &= ~USART_DREINTLVL_MED_gc; -} +} hy = {1}; // Default ID -static void _set_txc_interrupt(bool enable) { - if (enable) HUANYANG_PORT.CTRLA |= USART_TXCINTLVL_MED_gc; - else HUANYANG_PORT.CTRLA &= ~USART_TXCINTLVL_MED_gc; +static void _ctrl_write(hy_ctrl_state_t state) { + hy.func = HUANYANG_CTRL_WRITE; + hy.bytes = 2; + hy.response = 2; + hy.data[0] = 1; + hy.data[1] = state; } -static void _set_rxc_interrupt(bool enable) { - if (enable) HUANYANG_PORT.CTRLA |= USART_RXCINTLVL_MED_gc; - else HUANYANG_PORT.CTRLA &= ~USART_RXCINTLVL_MED_gc; +static void _ctrl_read(hy_ctrl_addr_t addr) { + hy.func = HUANYANG_CTRL_READ; + hy.bytes = 2; + hy.response = 4; + hy.data[0] = 1; + hy.data[1] = addr; } -static void _set_command1(int code, uint8_t arg0) { - ha.command_length = 4; - ha.command[1] = code; - ha.command[2] = 1; - ha.command[3] = arg0; +static void _freq_write(uint16_t freq) { + hy.func = HUANYANG_FREQ_WRITE; + hy.bytes = 3; + hy.response = 2; + hy.data[0] = 2; + hy.data[1] = freq >> 8; + hy.data[2] = freq; } -static void _set_command2(int code, uint8_t arg0, uint8_t arg1) { - ha.command_length = 5; - ha.command[1] = code; - ha.command[2] = 2; - ha.command[3] = arg0; - ha.command[4] = arg1; +static void _func_read(uint16_t addr) { + hy.func = HUANYANG_FUNC_READ; + hy.bytes = 4; + hy.response = 4; + hy.data[0] = 3; + hy.data[1] = addr; + hy.data[2] = addr >> 8; + hy.data[3] = 0; } -static void _set_command3(int code, uint8_t arg0, uint8_t arg1, uint8_t arg2) { - ha.command_length = 6; - ha.command[1] = code; - ha.command[2] = 3; - ha.command[3] = arg0; - ha.command[4] = arg1; - ha.command[5] = arg2; -} - - -static int _response_length(int code) { - switch (code) { - case HUANYANG_FUNC_READ: return 8; - case HUANYANG_FUNC_WRITE: return 8; - case HUANYANG_CTRL_WRITE: return 6; - case HUANYANG_CTRL_READ: return 8; - case HUANYANG_FREQ_WRITE: return 7; - default: return -1; +static void _func_read_response(hy_addr_t addr, uint16_t value) { + switch (addr) { + case HY_PD005_MAX_FREQUENCY: hy.max_freq = value * 0.01; break; + case HY_PD011_FREQUENCY_LOWER_LIMIT: hy.min_freq = value * 0.01; break; + case HY_PD144_RATED_MOTOR_RPM: hy.rated_rpm = value; break; + default: break; } } -static bool _update(int index) { - // Read response - switch (index) { - case 1: ha.status = ha.response[5]; break; - case 2: ha.max_freq = FUNC_RESPONSE(ha.response) * 0.01; break; - case 3: ha.min_freq = FUNC_RESPONSE(ha.response) * 0.01; break; - case 4: ha.rated_rpm = FUNC_RESPONSE(ha.response); break; +static void _ctrl_read_response(hy_ctrl_addr_t addr, uint16_t value) { + switch (addr) { + case HUANYANG_ACTUAL_FREQ: hy.actual_freq = value * 0.01; break; + case HUANYANG_ACTUAL_CURRENT: hy.actual_current = value * 0.01; break; + case HUANYANG_ACTUAL_RPM: hy.actual_rpm = value; break; + case HUANYANG_TEMPERATURE: hy.temperature = value; break; default: break; } +} - // Setup next command - uint8_t var; - switch (index) { - case 0: { // Update direction - uint8_t state = HUANYANG_STOP; - if (!ha.shutdown) { - if (0 < ha.speed) state = HUANYANG_RUN; - else if (ha.speed < 0) state = HUANYANG_RUN | HUANYANG_REV_FWD; - } - - _set_command1(HUANYANG_CTRL_WRITE, state); - - return true; - } - - case 1: var = HY_PD005_MAX_FREQUENCY; break; - case 2: var = HY_PD011_FREQUENCY_LOWER_LIMIT; break; - case 3: var = HY_PD144_RATED_MOTOR_RPM; break; - - case 4: { // Update freqency - // Compute frequency in Hz - float freq = fabs(ha.speed * 50 / ha.rated_rpm); - - // Clamp frequency - if (ha.max_freq < freq) freq = ha.max_freq; - if (freq < ha.min_freq) freq = ha.min_freq; - if (ha.shutdown) freq = 0; - - // Frequency write command - uint16_t f = freq * 100; - _set_command2(HUANYANG_FREQ_WRITE, f >> 8, f); - if (1 < ha.shutdown) ha.shutdown--; +static uint16_t _read_word(const uint8_t *data) { + return (uint16_t)data[0] << 8 | data[1]; +} - return true; - } - default: - report_request(); - return false; +static void _handle_response(hy_func_t func, const uint8_t *data) { + switch (func) { + case HUANYANG_FUNC_READ: { + _func_read_response((hy_addr_t)_read_word(data), _read_word(data + 2)); + break; } - _set_command3(HUANYANG_FUNC_READ, var, 0, 0); - - return true; -} - + case HUANYANG_FUNC_WRITE: break; + case HUANYANG_CTRL_WRITE: hy.status = _read_word(data); break; -static bool _query_status(int index) { - // Read response - switch (index) { - case 1: ha.actual_freq = CTRL_STATUS_RESPONSE(ha.response) * 0.01; break; - case 2: ha.actual_current = CTRL_STATUS_RESPONSE(ha.response) * 0.1; break; - case 3: ha.actual_rpm = CTRL_STATUS_RESPONSE(ha.response); break; - case 4: ha.temperature = CTRL_STATUS_RESPONSE(ha.response); break; - default: break; + case HUANYANG_CTRL_READ: { + _ctrl_read_response((hy_ctrl_addr_t)_read_word(data), _read_word(data + 2)); + break; } - // Setup next command - uint8_t var; - switch (index) { - case 0: var = HUANYANG_ACTUAL_FREQ; break; - case 1: var = HUANYANG_ACTUAL_CURRENT; break; - case 2: var = HUANYANG_ACTUAL_RPM; break; - case 3: var = HUANYANG_TEMPERATURE; break; - default: - report_request(); - return false; + case HUANYANG_FREQ_WRITE: break; + default: break; } - - _set_command1(HUANYANG_CTRL_READ, var); - - return true; } static void _next_command(); -static void _next_state() { - if (ha.changed || ha.shutdown) { - ha.next_command_cb = _update; - ha.changed = false; - - } else ha.next_command_cb = _query_status; - - _next_command(); -} - - -static bool _check_response() { - // Check CRC - uint16_t computed = _crc16(ha.response, ha.response_length - 2); - uint16_t expected = (ha.response[ha.response_length - 1] << 8) | - ha.response[ha.response_length - 2]; - - if (computed != expected) { - STATUS_WARNING(STAT_OK, "huanyang: invalid CRC, expected=0x%04u got=0x%04u", - expected, computed); - return false; - } - - // Check if response code matches the code we sent - if (ha.command[1] != ha.response[1]) { - STATUS_WARNING(STAT_OK, - "huanyang: invalid function code, expected=%u got=%u", - ha.command[2], ha.response[2]); - return false; - } - - return true; -} - - static void _reset(bool halt) { - _set_dre_interrupt(false); - _set_txc_interrupt(false); - _set_rxc_interrupt(false); - _set_write(true); // RS485 write mode - - // Flush USART - uint8_t x = HUANYANG_PORT.DATA; - x = HUANYANG_PORT.STATUS; - x = x; - // Save settings - uint8_t id = ha.id; - float speed = ha.speed; - bool debug = ha.debug; + uint8_t id = hy.id; + float speed = hy.speed; // Clear state - memset(&ha, 0, sizeof(ha)); + memset(&hy, 0, sizeof(hy)); // Restore settings - ha.id = id; - ha.speed = speed; - ha.debug = debug; - ha.changed = true; + hy.id = id; + hy.speed = speed; + hy.changed = true; - if (!halt) _next_state(); + if (!halt) _next_command(); } -static void _next_command() { - if (ha.shutdown == 1) { - _reset(true); - - // Disable USART - HUANYANG_PORT.CTRLA = 0; - HUANYANG_PORT.CTRLC = 0; - HUANYANG_PORT.CTRLB = 0; - - // Float write pins - DIRCLR_PIN(RS485_DI_PIN); - DIRCLR_PIN(RS485_RW_PIN); - - return; - } - - if (ha.next_command_cb && ha.next_command_cb(ha.command_index++)) { - ha.response_length = _response_length(ha.command[1]); - - ha.command[0] = ha.id; +static void _modbus_cb(uint8_t slave, uint8_t func, uint8_t bytes, + const uint8_t *data) { + if (data && bytes == *data + 1) { + _handle_response((hy_func_t)func, data + 1); - uint16_t crc = _crc16(ha.command, ha.command_length); - ha.command[ha.command_length++] = crc; - ha.command[ha.command_length++] = crc >> 8; + if (func == HUANYANG_CTRL_WRITE && hy.shutdown) { + _reset(true); + modbus_deinit(); + return; + } - _set_dre_interrupt(true); + // Next command + if (++hy.state == 9) { + hy.state = 5; + report_request(); + } - return; + // Update freq + if (hy.shutdown || (hy.changed && 4 < hy.state)) hy.state = 0; } - ha.command_index = 0; - _next_state(); -} - - -static void _retry_command() { - ha.last = 0; - ha.current_offset = 0; - ha.retry++; - - _set_write(true); // RS485 write mode - - _set_txc_interrupt(false); - _set_rxc_interrupt(false); - _set_dre_interrupt(true); - - if (ha.debug) STATUS_DEBUG("huanyang: retry %d", ha.retry); + _next_command(); } -// Data register empty interrupt -ISR(HUANYANG_DRE_vect) { - HUANYANG_PORT.DATA = ha.command[ha.current_offset++]; +static void _next_command() { + switch (hy.state) { + case 0: { // Update direction + hy_ctrl_state_t state = HUANYANG_STOP; + if (!hy.shutdown) { + if (0 < hy.speed) state = HUANYANG_RUN; + else if (hy.speed < 0) + state = (hy_ctrl_state_t)(HUANYANG_RUN | HUANYANG_REV_FWD); + } - if (ha.current_offset == ha.command_length) { - _set_dre_interrupt(false); - _set_txc_interrupt(true); - ha.current_offset = 0; + _ctrl_write(state); + break; } -} + case 1: _func_read(HY_PD005_MAX_FREQUENCY); break; + case 2: _func_read(HY_PD011_FREQUENCY_LOWER_LIMIT); break; + case 3: _func_read(HY_PD144_RATED_MOTOR_RPM); break; -/// Transmit complete interrupt -ISR(HUANYANG_TXC_vect) { - _set_txc_interrupt(false); - _set_rxc_interrupt(true); - _set_write(false); // RS485 read mode - ha.last = rtc_get_time(); // Set timeout timer -} - - -// Data received interrupt -ISR(HUANYANG_RXC_vect) { - ha.response[ha.current_offset++] = HUANYANG_PORT.DATA; + case 4: { // Update freqency + // Compute frequency in Hz + float freq = fabs(hy.speed * 50 / hy.rated_rpm); - if (ha.current_offset == ha.response_length) { - _set_rxc_interrupt(false); - ha.current_offset = 0; - _set_write(true); // RS485 write mode + // Clamp frequency + if (hy.max_freq < freq) freq = hy.max_freq; + if (freq < hy.min_freq) freq = hy.min_freq; - if (_check_response()) { - ha.last = 0; // Clear timeout timer - ha.retry = 0; // Reset retry counter - ha.connected = true; + // Frequency write command + _freq_write(freq * 100); + hy.changed = false; + break; + } - _next_command(); - } + case 5: _ctrl_read(HUANYANG_ACTUAL_FREQ); break; + case 6: _ctrl_read(HUANYANG_ACTUAL_CURRENT); break; + case 7: _ctrl_read(HUANYANG_ACTUAL_RPM); break; + case 8: _ctrl_read(HUANYANG_TEMPERATURE); break; } + + // Send command + modbus_func(hy.id, hy.func, hy.bytes, hy.data, hy.response, _modbus_cb); } void hy_init() { - PR.PRPD &= ~PR_USART1_bm; // Disable power reduction - - DIRCLR_PIN(RS485_RO_PIN); // Input - OUTSET_PIN(RS485_DI_PIN); // High - DIRSET_PIN(RS485_DI_PIN); // Output - OUTSET_PIN(RS485_RW_PIN); // High - DIRSET_PIN(RS485_RW_PIN); // Output - - _set_baud(3325, 0b1101); // 9600 @ 32MHz with 2x USART - - // No parity, 8 data bits, 1 stop bit - HUANYANG_PORT.CTRLC = USART_CMODE_ASYNCHRONOUS_gc | USART_PMODE_DISABLED_gc | - USART_CHSIZE_8BIT_gc; - - // Configure receiver and transmitter - HUANYANG_PORT.CTRLB = USART_RXEN_bm | USART_TXEN_bm | USART_CLK2X_bm; - - ha.id = HUANYANG_ID; - hy_reset(); + modbus_init(); + _reset(false); } -void hy_deinit() {ha.shutdown = 5;} +void hy_deinit() {hy.shutdown = true;} void hy_set(float speed) { - if (ha.speed != speed) { - if (ha.debug) STATUS_DEBUG("huanyang: speed=%0.2f", speed); - ha.speed = speed; - ha.changed = true; - } -} - - -void hy_reset() {_reset(false);} - - -void hy_rtc_callback() { - if (ha.last && rtc_expired(ha.last + HUANYANG_TIMEOUT)) { - if (ha.retry < HUANYANG_RETRIES) _retry_command(); - else { - if (ha.debug) STATUS_DEBUG("huanyang: timedout"); - - if (ha.debug && ha.current_offset) { - const uint8_t buf_len = 8 * 2 + 1; - char sent[buf_len]; - char received[buf_len]; - - uint8_t i; - for (i = 0; i < ha.command_length; i++) - sprintf(sent + i * 2, "%02x", ha.command[i]); - sent[i * 2] = 0; - - for (i = 0; i < ha.current_offset; i++) - sprintf(received + i * 2, "%02x", ha.response[i]); - received[i * 2] = 0; - - STATUS_DEBUG("huanyang: sent 0x%s received 0x%s expected %u bytes", - sent, received, ha.response_length); - } - - // Try changing pin polarity - PINCTRL_PIN(RS485_RO_PIN) ^= PORT_INVEN_bm; - PINCTRL_PIN(RS485_DI_PIN) ^= PORT_INVEN_bm; - - hy_reset(); - } + if (hy.speed != speed) { + hy.speed = speed; + hy.changed = true; } } void hy_stop() { hy_set(0); - hy_reset(); + _reset(false); } -uint8_t get_hy_id() {return ha.id;} -void set_hy_id(uint8_t value) {ha.id = value;} -bool get_hy_debug() {return ha.debug;} -void set_hy_debug(bool value) {ha.debug = value;} -bool get_hy_connected() {return ha.connected;} -float get_hy_freq() {return ha.actual_freq;} -float get_hy_current() {return ha.actual_current;} -uint16_t get_hy_rpm() {return ha.actual_rpm;} -uint16_t get_hy_temp() {return ha.temperature;} -float get_hy_max_freq() {return ha.max_freq;} -float get_hy_min_freq() {return ha.min_freq;} -uint16_t get_hy_rated_rpm() {return ha.rated_rpm;} -float get_hy_status() {return ha.status;} +uint8_t get_hy_id() {return hy.id;} +void set_hy_id(uint8_t value) {hy.id = value;} +float get_hy_freq() {return hy.actual_freq;} +float get_hy_current() {return hy.actual_current;} +uint16_t get_hy_rpm() {return hy.actual_rpm;} +uint16_t get_hy_temp() {return hy.temperature;} +float get_hy_max_freq() {return hy.max_freq;} +float get_hy_min_freq() {return hy.min_freq;} +uint16_t get_hy_rated_rpm() {return hy.rated_rpm;} +float get_hy_status() {return hy.status;} diff --git a/src/avr/src/huanyang.h b/src/avr/src/huanyang.h index ae0cfa7..739199d 100644 --- a/src/avr/src/huanyang.h +++ b/src/avr/src/huanyang.h @@ -33,10 +33,9 @@ void hy_init(); void hy_deinit(); void hy_set(float speed); -void hy_reset(); -void hy_rtc_callback(); void hy_stop(); + /// See Huanyang VFD user manual typedef enum { HY_PD000_PARAMETER_LOCK, @@ -223,4 +222,4 @@ typedef enum { HY_PD181_SOFTWARE_VERSION, HY_PD182_MANUFACTURE_DATE, HY_PD183_SERIAL_NO, -} hy_func_addr_t; +} hy_addr_t; diff --git a/src/avr/src/modbus.c b/src/avr/src/modbus.c new file mode 100644 index 0000000..6e3bee1 --- /dev/null +++ b/src/avr/src/modbus.c @@ -0,0 +1,330 @@ +/******************************************************************************\ + + This file is part of the Buildbotics firmware. + + Copyright (c) 2015 - 2018, Buildbotics LLC + All rights reserved. + + This file ("the software") is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License, + version 2 as published by the Free Software Foundation. You should + have received a copy of the GNU General Public License, version 2 + along with the software. If not, see . + + The software is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the software. If not, see + . + + For information regarding this software email: + "Joseph Coffland" + +\******************************************************************************/ + +#include "modbus.h" +#include "usart.h" +#include "status.h" +#include "rtc.h" +#include "config.h" + +#include +#include +#include + +#include +#include + + +static struct { + bool debug; + uint8_t baud; + + uint8_t bytes; + uint8_t command[MODBUS_BUF_SIZE]; + uint8_t command_length; + uint8_t response[MODBUS_BUF_SIZE]; + uint8_t response_length; + + modbus_cb_t receive_cb; + + uint32_t last; + uint8_t retry; + bool connected; + bool busy; +} mb = {true, USART_BAUD_9600}; + + +static uint16_t _crc16(const uint8_t *buffer, unsigned length) { + uint16_t crc = 0xffff; + + for (unsigned i = 0; i < length; i++) + crc = _crc16_update(crc, buffer[i]); + + return crc; +} + + +static void _set_write(bool x) {SET_PIN(RS485_RW_PIN, x);} + + +#define INTLVL_SET(REG, NAME, LEVEL) \ + REG = ((REG) & ~NAME##INTLVL_gm) | NAME##INTLVL_##LEVEL##_gc + + +#define INTLVL_ENABLE(REG, NAME, LEVEL, ENABLE) do { \ + if (ENABLE) INTLVL_SET(REG, NAME, LEVEL); \ + else INTLVL_SET(REG, NAME, OFF); \ + } while (0) + + +static void _set_dre_interrupt(bool enable) { + INTLVL_ENABLE(RS485_PORT.CTRLA, USART_DRE, MED, enable); +} + + +static void _set_txc_interrupt(bool enable) { + INTLVL_ENABLE(RS485_PORT.CTRLA, USART_TXC, MED, enable); +} + + +static void _set_rxc_interrupt(bool enable) { + INTLVL_ENABLE(RS485_PORT.CTRLA, USART_RXC, MED, enable); +} + + +static bool _check_response() { + // Check CRC + uint16_t computed = _crc16(mb.response, mb.response_length - 2); + uint16_t expected = (mb.response[mb.response_length - 1] << 8) | + mb.response[mb.response_length - 2]; + + if (computed != expected) { + STATUS_WARNING(STAT_OK, "modbus: invalid CRC, expected=0x%04u got=0x%04u", + expected, computed); + return false; + } + + // Check that slave id matches + if (mb.command[0] != mb.response[0]) { + STATUS_WARNING(STAT_OK, "modbus: unexpected slave id, expected=%u got=%u", + mb.command[0], mb.response[0]); + return false; + } + + // Check that function code matches + if (mb.command[1] != mb.response[1]) { + STATUS_WARNING(STAT_OK, "modbus: invalid function code, expected=%u got=%u", + mb.command[1], mb.response[1]); + return false; + } + + return true; +} + + +static void _handle_response() { + if (!_check_response()) return; + + mb.last = 0; // Clear timeout timer + mb.retry = 0; // Reset retry counter + + if (mb.receive_cb) + mb.receive_cb(mb.response[0], mb.response[1], mb.response_length - 4, + mb.response + 2); + + mb.connected = true; + mb.busy = false; +} + + +/// Data register empty interrupt +ISR(RS485_DRE_vect) { + RS485_PORT.DATA = mb.command[mb.bytes++]; + + if (mb.bytes == mb.command_length) { + _set_dre_interrupt(false); + _set_txc_interrupt(true); + mb.bytes = 0; + } +} + + +/// Transmit complete interrupt +ISR(RS485_TXC_vect) { + _set_txc_interrupt(false); + _set_rxc_interrupt(true); + _set_write(false); // Switch to read mode + mb.last = rtc_get_time(); // Set timeout timer +} + + +/// Data received interrupt +ISR(RS485_RXC_vect) { + mb.response[mb.bytes++] = RS485_PORT.DATA; + + if (mb.bytes == mb.response_length) { + _set_rxc_interrupt(false); + _set_write(true); // Back to write mode + mb.bytes = 0; + _handle_response(); + } +} + + +static void _reset() { + _set_dre_interrupt(false); + _set_txc_interrupt(false); + _set_rxc_interrupt(false); + _set_write(true); // RS485 write mode + + // Flush USART + uint8_t x = RS485_PORT.DATA; + x = RS485_PORT.STATUS; + x = x; + + // Save settings + bool debug = mb.debug; + uint8_t baud = mb.baud; + + // Clear state + memset(&mb, 0, sizeof(mb)); + + // Restore settings + mb.debug = debug; + mb.baud = baud; +} + + +static void _retry() { + mb.last = 0; + mb.bytes = 0; + mb.retry++; + + _set_write(true); // RS485 write mode + + _set_txc_interrupt(false); + _set_rxc_interrupt(false); + _set_dre_interrupt(true); + + if (mb.debug) STATUS_DEBUG("modbus: retry %d", mb.retry); +} + + +static void _timeout() { + if (mb.debug) STATUS_DEBUG("modbus: timedout"); + + // Try changing pin polarity + PINCTRL_PIN(RS485_RO_PIN) ^= PORT_INVEN_bm; + PINCTRL_PIN(RS485_DI_PIN) ^= PORT_INVEN_bm; + + mb.retry = -1; + _retry(); +} + + +void modbus_init() { + PR.PRPD &= ~PR_USART1_bm; // Disable power reduction + + DIRCLR_PIN(RS485_RO_PIN); // Input + OUTSET_PIN(RS485_DI_PIN); // High + DIRSET_PIN(RS485_DI_PIN); // Output + OUTSET_PIN(RS485_RW_PIN); // High + DIRSET_PIN(RS485_RW_PIN); // Output + + usart_set_port_baud(&RS485_PORT, mb.baud); + + // No parity, 8 data bits, 1 stop bit + RS485_PORT.CTRLC = USART_CMODE_ASYNCHRONOUS_gc | USART_PMODE_DISABLED_gc | + USART_CHSIZE_8BIT_gc; + + // Enable receiver and transmitter + RS485_PORT.CTRLB |= USART_RXEN_bm | USART_TXEN_bm; + + // TODO remove this + PINCTRL_PIN(RS485_RO_PIN) ^= PORT_INVEN_bm; + PINCTRL_PIN(RS485_DI_PIN) ^= PORT_INVEN_bm; + + _reset(); +} + + +void modbus_deinit() { + _reset(); + + // Disable USART + RS485_PORT.CTRLB &= ~(USART_RXEN_bm | USART_TXEN_bm); + + // Float write pins + DIRCLR_PIN(RS485_DI_PIN); + DIRCLR_PIN(RS485_RW_PIN); +} + + +bool modbus_busy() {return mb.busy;} + + +void modbus_func(uint8_t slave, uint8_t func, uint8_t send, const uint8_t *data, + uint8_t receive, modbus_cb_t receive_cb) { + ASSERT(send + 4 <= MODBUS_BUF_SIZE); + ASSERT(receive + 4 <= MODBUS_BUF_SIZE); + + _reset(); + + mb.command[0] = slave; + mb.command[1] = func; + memcpy(mb.command + 2, data, send); + uint16_t crc = _crc16(mb.command, send + 2); + mb.command[send + 2] = crc; + mb.command[send + 3] = crc >> 8; + + mb.command_length = send + 4; + mb.response_length = receive + 4; + mb.receive_cb = receive_cb; + + _set_dre_interrupt(true); +} + + +void modbus_rtc_callback() { + if (mb.last && rtc_expired(mb.last + MODBUS_TIMEOUT)) { + if (mb.debug && mb.bytes) { + const uint8_t buf_len = 8 * 2 + 1; + char sent[buf_len]; + char received[buf_len]; + + uint8_t i; + for (i = 0; i < mb.command_length; i++) + sprintf(sent + i * 2, "%02x", mb.command[i]); + sent[i * 2] = 0; + + for (i = 0; i < mb.bytes; i++) + sprintf(received + i * 2, "%02x", mb.response[i]); + received[i * 2] = 0; + + STATUS_DEBUG("modbus: sent 0x%s received 0x%s expected %u bytes", + sent, received, mb.response_length); + } + + if (mb.retry < MODBUS_RETRIES) _retry(); + else _timeout(); + } +} + + +// Variable callbacks +bool get_modbus_debug() {return mb.debug;} +void set_modbus_debug(bool value) {mb.debug = value;} +uint8_t get_modbus_baud() {return mb.baud;} + + +void set_modbus_baud(uint8_t baud) { + mb.baud = baud; + usart_set_port_baud(&RS485_PORT, baud); +} + + +bool get_modbus_connected() {return mb.connected;} diff --git a/src/avr/src/modbus.h b/src/avr/src/modbus.h new file mode 100644 index 0000000..802791e --- /dev/null +++ b/src/avr/src/modbus.h @@ -0,0 +1,109 @@ +/******************************************************************************\ + + This file is part of the Buildbotics firmware. + + Copyright (c) 2015 - 2018, Buildbotics LLC + All rights reserved. + + This file ("the software") is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License, + version 2 as published by the Free Software Foundation. You should + have received a copy of the GNU General Public License, version 2 + along with the software. If not, see . + + The software is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the software. If not, see + . + + For information regarding this software email: + "Joseph Coffland" + +\******************************************************************************/ + +/******************************************************************************\ + + Modbus RTU command format is as follows: + + Function 01: Read Coil Status + Function 02: Read Input Status + + Send: [id][func][addr][count][crc] + Receive: [id][func][bytes][flags][crc] + + Function 03: Read Holding Registers + Function 04: Read Input Registers + + Send: [id][func][addr][count][crc] + Receive: [id][func][bytes][regs][crc] + + Function 05: Force Single Coil + Function 06: Preset Single Register + + Send: [id][func][addr][value][crc] + Receive: [id][func][addr][value][crc] + + Function 15: Force Multiple Coils + + Send: [id][func][addr][count][bytes][flags][crc] + Receive: [id][func][addr][count][crc] + + Function 16: Preset Multiple Registers + + Send: [id][func][addr][count][bytes][regs][crc] + Receive: [id][func][addr][count][crc] + + Where: + + id: 1-byte Slave ID + func: 1-byte Function code + addr: 2-byte Data address + count: 2-byte Address count + bytes: 1-byte Number of bytes to follow + value: 2-byte Value read or written + bits: n-bytes Flags indicating on/off + regs: n-bytes register values to write + checksum: 16-bit CRC: x^16 + x^15 + x^2 + 1 (0x8005) initial: 0xffff + +\******************************************************************************/ + +#pragma once + + +#include +#include + + +typedef enum { + MODBUS_READ_COILS = 1, + MODBUS_READ_CONTACTS = 2, + MODBUS_READ_OUTPUT_REG = 3, + MODBUS_READ_INPUT_REG = 4, + MODBUS_WRITE_COIL = 5, + MODBUS_WRITE_OUTPUT_REG = 6, + MODBUS_WRITE_COILS = 15, + MODBUS_WRITE_OUTPUT_REGS = 16, +} modbus_function_code_t; + + +typedef enum { + MODBUS_COIL_BASE = 0, + MODBUS_CONTACT_BASE = 10000, + MODBUS_INPUT_REG_BASE = 30000, + MODBUS_OUTPUT_REG_BASE = 40000, +} modbus_base_addrs_t; + + +typedef void (*modbus_cb_t)(uint8_t slave, uint8_t func, uint8_t bytes, + const uint8_t *data); + +void modbus_init(); +void modbus_deinit(); +bool modbus_busy(); +void modbus_func(uint8_t slave, uint8_t func, uint8_t send, const uint8_t *data, + uint8_t receive, modbus_cb_t receive_cb); +void modbus_rtc_callback(); diff --git a/src/avr/src/pwm_spindle.h b/src/avr/src/pwm_spindle.h index 301694e..fe29f28 100644 --- a/src/avr/src/pwm_spindle.h +++ b/src/avr/src/pwm_spindle.h @@ -27,8 +27,6 @@ #pragma once -#include "spindle.h" - void pwm_spindle_init(); void pwm_spindle_deinit(); diff --git a/src/avr/src/rtc.c b/src/avr/src/rtc.c index 702355a..1c7a779 100644 --- a/src/avr/src/rtc.c +++ b/src/avr/src/rtc.c @@ -30,7 +30,7 @@ #include "switch.h" #include "analog.h" #include "io.h" -#include "huanyang.h" +#include "modbus.h" #include "motor.h" #include "lcd.h" @@ -51,7 +51,7 @@ ISR(RTC_OVF_vect) { switch_rtc_callback(); analog_rtc_callback(); io_rtc_callback(); - hy_rtc_callback(); + modbus_rtc_callback(); if (!(ticks & 255)) motor_rtc_callback(); wdt_reset(); } diff --git a/src/avr/src/usart.c b/src/avr/src/usart.c index 2901703..55876a6 100644 --- a/src/avr/src/usart.c +++ b/src/avr/src/usart.c @@ -117,7 +117,7 @@ void usart_init(void) { USART_CHSIZE_8BIT_gc; // Configure receiver and transmitter - SERIAL_PORT.CTRLB = USART_RXEN_bm | USART_TXEN_bm | USART_CLK2X_bm; + SERIAL_PORT.CTRLB |= USART_RXEN_bm | USART_TXEN_bm; PMIC.CTRL |= PMIC_HILVLEN_bm; // Interrupt level on @@ -135,32 +135,38 @@ void usart_init(void) { } -static void _set_baud(uint16_t bsel, uint8_t bscale) { - SERIAL_PORT.BAUDCTRLB = (uint8_t)((bscale << 4) | (bsel >> 8)); - SERIAL_PORT.BAUDCTRLA = bsel; +static void _set_baud(USART_t *port, uint16_t bsel, uint8_t bscale) { + port->BAUDCTRLB = (uint8_t)((bscale << 4) | (bsel >> 8)); + port->BAUDCTRLA = bsel; + port->CTRLB |= USART_CLK2X_bm; } -void usart_set_baud(int baud) { +void usart_set_port_baud(USART_t *port, int baud) { // The BSEL / BSCALE values provided below assume a 32 Mhz clock - // Assumes CTRLB CLK2X bit (0x04) is set + // With CTRLB CLK2X is set // See http://www.avrcalc.elektronik-projekt.de/xmega/baud_rate_calculator switch (baud) { - case USART_BAUD_9600: _set_baud(3325, 0b1101); break; - case USART_BAUD_19200: _set_baud(3317, 0b1100); break; - case USART_BAUD_38400: _set_baud(3301, 0b1011); break; - case USART_BAUD_57600: _set_baud(1095, 0b1100); break; - case USART_BAUD_115200: _set_baud(1079, 0b1011); break; - case USART_BAUD_230400: _set_baud(1047, 0b1010); break; - case USART_BAUD_460800: _set_baud(983, 0b1001); break; - case USART_BAUD_921600: _set_baud(107, 0b1011); break; - case USART_BAUD_500000: _set_baud(1, 0b0010); break; - case USART_BAUD_1000000: _set_baud(1, 0b0001); break; + case USART_BAUD_9600: _set_baud(port, 3325, 0b1101); break; + case USART_BAUD_19200: _set_baud(port, 3317, 0b1100); break; + case USART_BAUD_38400: _set_baud(port, 3301, 0b1011); break; + case USART_BAUD_57600: _set_baud(port, 1095, 0b1100); break; + case USART_BAUD_115200: _set_baud(port, 1079, 0b1011); break; + case USART_BAUD_230400: _set_baud(port, 1047, 0b1010); break; + case USART_BAUD_460800: _set_baud(port, 983, 0b1001); break; + case USART_BAUD_921600: _set_baud(port, 107, 0b1011); break; + case USART_BAUD_500000: _set_baud(port, 1, 0b0010); break; + case USART_BAUD_1000000: _set_baud(port, 1, 0b0001); break; } } +void usart_set_baud(int baud) { + usart_set_port_baud(&SERIAL_PORT, baud); +} + + void usart_set(int flag, bool enable) { if (enable) usart_flags |= flag; else usart_flags &= ~flag; diff --git a/src/avr/src/usart.h b/src/avr/src/usart.h index 9d5a272..77a6349 100644 --- a/src/avr/src/usart.h +++ b/src/avr/src/usart.h @@ -27,10 +27,12 @@ #pragma once +#include #include #include + #define USART_TX_RING_BUF_SIZE 256 #define USART_RX_RING_BUF_SIZE 256 @@ -56,6 +58,7 @@ enum { }; void usart_init(); +void usart_set_port_baud(USART_t *port, int baud); void usart_set_baud(int baud); void usart_set(int flag, bool enable); bool usart_is_set(int flags); diff --git a/src/avr/src/vars.def b/src/avr/src/vars.def index c96a0cb..c2d3246 100644 --- a/src/avr/src/vars.def +++ b/src/avr/src/vars.def @@ -94,32 +94,35 @@ VAR(pwm_freq, sf, u16, 0, 1, 1, "Spindle PWM frequency in Hz") // PWM spindle VAR(pwm_invert, pi, b8, 0, 1, 1, "Inverted spindle PWM") +// Modbus spindle +VAR(modbus_debug, hb, b8, 0, 1, 1, "Modbus debugging") +VAR(modbus_baud, mb, u8, 0, 1, 1, "Modbus BAUD rate") +VAR(modbus_connected, he, b8, 0, 0, 1, "Modbus connected") + // Huanyang spindle -VAR(hy_id, hi, u8, 0, 1, 1, "Huanyang ID") -VAR(hy_freq, hz, f32, 0, 0, 1, "Huanyang actual freq") -VAR(hy_current, hc, f32, 0, 0, 1, "Huanyang actual current") -VAR(hy_rpm, hr, u16, 0, 0, 1, "Huanyang actual RPM") -VAR(hy_temp, ht, u16, 0, 0, 1, "Huanyang temperature") -VAR(hy_max_freq, hx, f32, 0, 0, 1, "Huanyang max freq") -VAR(hy_min_freq, hm, f32, 0, 0, 1, "Huanyang min freq") -VAR(hy_rated_rpm, hq, u16, 0, 0, 1, "Huanyang rated RPM") -VAR(hy_status, hs, u8, 0, 0, 1, "Huanyang status flags") -VAR(hy_debug, hb, b8, 0, 1, 1, "Huanyang debugging") -VAR(hy_connected, he, b8, 0, 0, 1, "Huanyang connected") +VAR(hy_id, hi, u8, 0, 1, 1, "Modbus ID") +VAR(hy_freq, hz, f32, 0, 0, 1, "Modbus actual freq") +VAR(hy_current, hc, f32, 0, 0, 1, "Modbus actual current") +VAR(hy_rpm, hr, u16, 0, 0, 1, "Modbus actual RPM") +VAR(hy_temp, ht, u16, 0, 0, 1, "Modbus temperature") +VAR(hy_max_freq, hx, f32, 0, 0, 1, "Modbus max freq") +VAR(hy_min_freq, hm, f32, 0, 0, 1, "Modbus min freq") +VAR(hy_rated_rpm, hq, u16, 0, 0, 1, "Modbus rated RPM") +VAR(hy_status, hs, u8, 0, 0, 1, "Modbus status flags") // Machine state VAR(id, id, u32, 0, 1, 1, "Last executed command ID") -VAR(speed, s, f32, 0, 1, 1, "Current spindle speed") +VAR(speed, s, f32, 0, 1, 1, "Current spindle speed") VAR(feed_override, fo, f32, 0, 1, 1, "Feed rate override") VAR(speed_override, so, f32, 0, 1, 1, "Spindle speed override") VAR(optional_pause, op, b8, 0, 1, 1, "Optional pause state") // System -VAR(velocity, v, f32, 0, 0, 1, "Current velocity") -VAR(acceleration, ax, f32, 0, 0, 1, "Current acceleration") -VAR(jerk, j, f32, 0, 0, 1, "Current jerk") -VAR(peak_vel, pv, f32, 0, 1, 1, "Peak velocity, set to clear") -VAR(peak_accel, pa, f32, 0, 1, 1, "Peak acceleration, set to clear") +VAR(velocity, v, f32, 0, 0, 1, "Current velocity") +VAR(acceleration, ax, f32, 0, 0, 1, "Current acceleration") +VAR(jerk, j, f32, 0, 0, 1, "Current jerk") +VAR(peak_vel, pv, f32, 0, 1, 1, "Peak velocity, set to clear") +VAR(peak_accel, pa, f32, 0, 1, 1, "Peak acceleration, set to clear") VAR(hw_id, hid, str, 0, 0, 1, "Hardware ID") VAR(estop, es, b8, 0, 1, 1, "Emergency stop") VAR(estop_reason, er, pstr, 0, 0, 1, "Emergency stop reason") diff --git a/src/jade/templates/tool-view.jade b/src/jade/templates/tool-view.jade index c28debb..d22e5d3 100644 --- a/src/jade/templates/tool-view.jade +++ b/src/jade/templates/tool-view.jade @@ -34,17 +34,25 @@ script#tool-view-template(type="text/x-template") templated-input(v-for="templ in template.tool", :name="$key", :model.sync="tool[$key]", :template="templ") - div(v-if="tool['spindle-type'] == 'PWM'") + div(v-if="get_type() == 'PWM'") h2 PWM Spindle form.pure-form.pure-form-aligned fieldset templated-input(v-for="templ in template['pwm-spindle']", :name="$key", :model.sync="pwmSpindle[$key]", :template="templ") - div(v-if="tool['spindle-type'] == 'HUANYANG'") - h2 Huanyang VFD Spindle + div(v-if="get_type() == 'HUANYANG'") + h2 Huanyang VFD form.pure-form.pure-form-aligned fieldset templated-input(v-for="templ in template['huanyang-spindle']", :name="$key", :model.sync="huanyangSpindle[$key]", :template="templ") + + div(v-if="get_type() == 'MODBUS'") + h2 Modbus Compatiable VFD + form.pure-form.pure-form-aligned + fieldset + templated-input(v-for="templ in template['modbus-spindle']", + :name="$key", :model.sync="modbusSpindle[$key]", + :template="templ") diff --git a/src/js/tool-view.js b/src/js/tool-view.js index 122babd..731942b 100644 --- a/src/js/tool-view.js +++ b/src/js/tool-view.js @@ -37,7 +37,8 @@ module.exports = { return { tool: {}, pwmSpindle: {}, - huanyangSpindle: {} + huanyangSpindle: {}, + modbusSpindle: {} } }, @@ -54,9 +55,24 @@ module.exports = { methods: { + get_type: function () { + return (this.tool['spindle-type'] || 'Disabled').toUpperCase(); + }, + + + update_tool: function (type) { + if (this.config.hasOwnProperty(type + '-spindle')) + this[type + 'Spindle'] = this.config[type + '-spindle']; + + var template = this.template[type + '-spindle']; + for (var key in template) + if (!this[type + 'Spindle'].hasOwnProperty(key)) + this.$set(type + 'Spindle["' + key + '"]', template[key].default); + }, + + update: function () { Vue.nextTick(function () { - // Tool if (this.config.hasOwnProperty('tool')) this.tool = this.config.tool; @@ -65,24 +81,10 @@ module.exports = { if (!this.tool.hasOwnProperty(key)) this.$set('tool["' + key + '"]', template[key].default); - // PWM - if (this.config.hasOwnProperty('pwm-spindle')) - this.pwmSpindle = this.config['pwm-spindle']; - - template = this.template['pwm-spindle']; - for (key in template) - if (!this.pwmSpindle.hasOwnProperty(key)) - this.$set('pwmSpindle["' + key + '"]', template[key].default); - - // Huanyang - if (this.config.hasOwnProperty('huanyang-spindle')) - this.huanyangSpindle = this.config['huanyang-spindle']; - - template = this.template['huanyang-spindle']; - for (key in template) - if (!this.huanyangSpindle.hasOwnProperty(key)) - this.$set('huanyangSpindle["' + key + '"]', template[key].default); - }.bind(this)); + this.update_tool('pwm'); + this.update_tool('huanyang'); + this.update_tool('modbus'); + }.bind(this)); } } } diff --git a/src/resources/config-template.json b/src/resources/config-template.json index 15623b4..c1bf508 100644 --- a/src/resources/config-template.json +++ b/src/resources/config-template.json @@ -153,8 +153,8 @@ "tool": { "spindle-type": { "type": "enum", - "values": ["DISABLED", "PWM", "HUANYANG"], - "default": "DISABLED", + "values": ["Disabled", "PWM", "Huanyang", "Modbus"], + "default": "Disabled", "code": "st" }, "spin-reversed": { @@ -186,6 +186,14 @@ } }, + "modbus-spindle": { + "modbus-id": { + "type": "int", + "default": "1", + "code": "bi" + } + }, + "pwm-spindle": { "max-spin": { "type": "float", -- 2.27.0