1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-03-15 10:33:07 +01:00
This commit is contained in:
Travis Farmer 2024-12-02 19:15:25 +00:00 committed by GitHub
commit 97f9e0870f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 1060 additions and 0 deletions

648
IO_Modbus.cpp Normal file
View File

@ -0,0 +1,648 @@
/*
* © 2024, Travis Farmer. All rights reserved.
* © 2024, Chris Bulliner. All rights reserved. https://github.com/CMB27
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IO_Modbus.h"
#include "defines.h"
void ModbusADU::setTransactionId(uint16_t transactionId) {
_setRegister(tcp, 0, transactionId);
}
void ModbusADU::setProtocolId(uint16_t protocolId) {
_setRegister(tcp, 2, protocolId);
}
void ModbusADU::setLength(uint16_t length) {
if (length < 3 || length > 254) _setRegister(tcp, 4, 0);
else _setRegister(tcp, 4, length);
}
void ModbusADU::setUnitId(uint8_t unitId) {
tcp[6] = unitId;
}
void ModbusADU::setFunctionCode(uint8_t functionCode) {
pdu[0] = functionCode;
}
void ModbusADU::setDataRegister(uint8_t index, uint16_t value) {
_setRegister(data, index, value);
}
void ModbusADU::setRtuLen(uint16_t rtuLen) {
setLength(rtuLen - 2);
}
void ModbusADU::setTcpLen(uint16_t tcpLen) {
setLength(tcpLen - 6);
}
void ModbusADU::setPduLen(uint16_t pduLen) {
setLength(pduLen + 1);
}
void ModbusADU::setDataLen(uint16_t dataLen) {
setLength(dataLen + 2);
}
uint16_t ModbusADU::getTransactionId() {
return _getRegister(tcp, 0);
}
uint16_t ModbusADU::getProtocolId() {
return _getRegister(tcp, 2);
}
uint16_t ModbusADU::getLength() {
uint16_t length = _getRegister(tcp, 4);
if (length < 3 || length > 254) return 0;
else return length;
}
uint8_t ModbusADU::getUnitId() {
return tcp[6];
}
uint8_t ModbusADU::getFunctionCode() {
return pdu[0];
}
uint16_t ModbusADU::getDataRegister(uint8_t index) {
return _getRegister(data, index);
}
uint16_t ModbusADU::getRtuLen() {
uint16_t len = getLength();
if (len == 0) return 0;
else return len + 2;
}
uint16_t ModbusADU::getTcpLen() {
uint16_t len = getLength();
if (len == 0) return 0;
else return len + 6;
}
uint16_t ModbusADU::getPduLen() {
uint16_t len = getLength();
if (len == 0) return 0;
else return len - 1;
}
uint16_t ModbusADU::getDataLen() {
uint16_t len = getLength();
if (len == 0) return 0;
else return len - 2;
}
void ModbusADU::updateCrc() {
uint16_t len = getLength();
uint16_t crc = _calculateCrc(len);
rtu[len] = lowByte(crc);
rtu[len + 1] = highByte(crc);
}
bool ModbusADU::crcGood() {
uint16_t len = getLength();
uint16_t aduCrc = rtu[len] | (rtu[len + 1] << 8);
uint16_t calculatedCrc = _calculateCrc(len);
if (aduCrc == calculatedCrc) return true;
else return false;
}
void ModbusADU::prepareExceptionResponse(uint8_t exceptionCode) {
pdu[0] |= 0x80;
pdu[1] = exceptionCode;
setPduLen(2);
}
void ModbusADU::_setRegister(uint8_t *buf, uint16_t index, uint16_t value) {
buf[index] = highByte(value);
buf[index + 1] = lowByte(value);
}
uint16_t ModbusADU::_getRegister(uint8_t *buf, uint16_t index) {
return (buf[index] << 8) | buf[index + 1];
}
uint16_t ModbusADU::_calculateCrc(uint16_t len) {
uint16_t value = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
value ^= (uint16_t)rtu[i];
for (uint8_t j = 0; j < 8; j++) {
bool lsb = value & 1;
value >>= 1;
if (lsb == true) value ^= 0xA001;
}
}
return value;
}
uint16_t div8RndUp(uint16_t value) {
return (value + 7) >> 3;
}
ModbusRTUComm::ModbusRTUComm(Stream& serial, VPIN dePin, VPIN rePin) : _serial(serial) {
_dePin = dePin;
_rePin = rePin;
}
void ModbusRTUComm::begin(unsigned long baud, uint32_t config) {
unsigned long bitsPerChar;
switch (config) {
case SERIAL_8E2:
case SERIAL_8O2:
bitsPerChar = 12;
break;
case SERIAL_8N2:
case SERIAL_8E1:
case SERIAL_8O1:
bitsPerChar = 11;
break;
case SERIAL_8N1:
default:
bitsPerChar = 10;
break;
}
if (baud <= 19200) {
_charTimeout = (bitsPerChar * 2500000) / baud;
_frameTimeout = (bitsPerChar * 4500000) / baud;
}
else {
_charTimeout = (bitsPerChar * 1000000) / baud + 750;
_frameTimeout = (bitsPerChar * 1000000) / baud + 1750;
}
#if defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_GIGA) || (defined(ARDUINO_NANO_RP2040_CONNECT) && defined(ARDUINO_ARCH_MBED))
_postDelay = ((bitsPerChar * 1000000) / baud) + 2;
#endif
if (_dePin != VPIN_NONE) {
pinMode(_dePin, OUTPUT);
ArduinoPins::fastWriteDigital(_dePin, LOW);
}
if (_rePin != VPIN_NONE) {
pinMode(_rePin, OUTPUT);
ArduinoPins::fastWriteDigital(_rePin, LOW);
}
clearRxBuffer();
}
void ModbusRTUComm::setTimeout(unsigned long timeout) {
_readTimeout = timeout;
}
ModbusRTUCommError ModbusRTUComm::readAdu(ModbusADU& adu) {
adu.setRtuLen(0);
unsigned long startMillis = millis();
if (_startTimeout == 0UL) _startTimeout = startMillis;
if (!_serial.available()) {
//if (millis() - startMillis >= _readTimeout) return MODBUS_RTU_COMM_TIMEOUT;
_waiting_for_read = true;
if (millis() - _startTimeout >= _readTimeout) {
//_serial.flush();
return MODBUS_RTU_COMM_TIMEOUT;
} else {
return MODBUS_RTU_COMM_WAITING;
}
}
_waiting_for_read = false;
_startTimeout = 0UL;
uint16_t len = 0;
unsigned long startMicros = micros();
do {
if (_serial.available()) {
startMicros = micros();
adu.rtu[len] = _serial.read();
len++;
}
} while (micros() - startMicros <= _charTimeout && len < 256);
adu.setRtuLen(len);
while (micros() - startMicros < _frameTimeout);
if (_serial.available()) {
adu.setRtuLen(0);
return MODBUS_RTU_COMM_FRAME_ERROR;
}
if (!adu.crcGood()) {
adu.setRtuLen(0);
return MODBUS_RTU_COMM_CRC_ERROR;
}
return MODBUS_RTU_COMM_SUCCESS;
}
void ModbusRTUComm::writeAdu(ModbusADU& adu) {
adu.updateCrc();
if (_dePin != VPIN_NONE) ArduinoPins::fastWriteDigital(_dePin, HIGH);
if (_rePin != VPIN_NONE) ArduinoPins::fastWriteDigital(_rePin, HIGH);
_serial.write(adu.rtu, adu.getRtuLen());
_serial.flush();
///delayMicroseconds(_postDelay); // TJF: Commented out as Mbed platforms are not supported
if (_dePin != VPIN_NONE) ArduinoPins::fastWriteDigital(_dePin, LOW);
if (_rePin != VPIN_NONE) ArduinoPins::fastWriteDigital(_rePin, LOW);
}
void ModbusRTUComm::clearRxBuffer() {
unsigned long startMicros = micros();
do {
if (_serial.available() > 0) {
startMicros = micros();
_serial.read();
}
} while (micros() - startMicros < _frameTimeout);
}
void Modbus::setTimeout(unsigned long timeout) {
_rtuComm.setTimeout(timeout);
}
ModbusRTUMasterError Modbus::readCoils(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity) {
return _readValues(id, 1, startAddress, buf, quantity);
}
ModbusRTUMasterError Modbus::readDiscreteInputs(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity) {
return _readValues(id, 2, startAddress, buf, quantity);
}
ModbusRTUMasterError Modbus::readHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) {
return _readValues(id, 3, startAddress, buf, quantity);
}
ModbusRTUMasterError Modbus::readInputRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) {
return _readValues(id, 4, startAddress, buf, quantity);
}
ModbusRTUMasterError Modbus::writeSingleCoil(uint8_t id, uint16_t address, int value) {
return _writeSingleValue(id, 5, address, ((value) ? 0xFF00 : 0x0000));
}
ModbusRTUMasterError Modbus::writeSingleHoldingRegister(uint8_t id, uint16_t address, uint16_t value) {
return _writeSingleValue(id, 6, address, value);
}
ModbusRTUMasterError Modbus::writeMultipleCoils(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity) {
const uint8_t functionCode = 15;
if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID;
if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER;
if (quantity == 0 || quantity > 1968) return MODBUS_RTU_MASTER_INVALID_QUANTITY;
ModbusADU adu;
uint16_t byteCount = div8RndUp(quantity);
adu.setUnitId(id);
adu.setFunctionCode(functionCode);
adu.setDataRegister(0, startAddress);
adu.setDataRegister(2, quantity);
adu.data[4] = byteCount;
for (uint16_t i = 0; i < quantity; i++) {
bitWrite(adu.data[5 + (i >> 3)], i & 7, buf[i]);
}
for (uint16_t i = quantity; i < (byteCount * 8); i++) {
bitClear(adu.data[5 + (i >> 3)], i & 7);
}
adu.setDataLen(5 + byteCount);
if (_rtuComm._waiting_for_read == false) _rtuComm.writeAdu(adu);
if (id == 0) return MODBUS_RTU_MASTER_SUCCESS;
ModbusRTUCommError commError = _rtuComm.readAdu(adu);
if (commError) return _translateCommError(commError);
if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID;
if (adu.getFunctionCode() == (functionCode + 0x80)) {
_exceptionResponse = adu.data[0];
return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE;
}
if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE;
if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH;
if (adu.getDataRegister(0) != startAddress) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS;
if (adu.getDataRegister(2) != quantity) return MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY;
return MODBUS_RTU_MASTER_SUCCESS;
}
ModbusRTUMasterError Modbus::writeMultipleHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) {
uint8_t functionCode = 16;
if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID;
if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER;
if (quantity == 0 || quantity > 123) return MODBUS_RTU_MASTER_INVALID_QUANTITY;
uint16_t byteCount = quantity * 2;
ModbusADU adu;
adu.setUnitId(id);
adu.setFunctionCode(functionCode);
adu.setDataRegister(0, startAddress);
adu.setDataRegister(2, quantity);
adu.data[4] = byteCount;
for (uint16_t i = 0; i < quantity; i++) {
adu.setDataRegister(5 + (i * 2), buf[i]);
}
adu.setDataLen(5 + byteCount);
if (_rtuComm._waiting_for_read == false) _rtuComm.writeAdu(adu);
if (id == 0) return MODBUS_RTU_MASTER_SUCCESS;
ModbusRTUCommError commError = _rtuComm.readAdu(adu);
if (commError) return _translateCommError(commError);
if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID;
if (adu.getFunctionCode() == (functionCode + 0x80)) {
_exceptionResponse = adu.data[0];
return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE;
}
if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE;
if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH;
if (adu.getDataRegister(0) != startAddress) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS;
if (adu.getDataRegister(2) != quantity) return MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY;
return MODBUS_RTU_MASTER_SUCCESS;
}
uint8_t Modbus::getExceptionResponse() {
return _exceptionResponse;
}
ModbusRTUMasterError Modbus::_readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, int buf[], uint16_t quantity) {
if (id < 1 || id > 247) return MODBUS_RTU_MASTER_INVALID_ID;
if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER;
if (quantity == 0 || quantity > 2000) return MODBUS_RTU_MASTER_INVALID_QUANTITY;
ModbusADU adu;
adu.setUnitId(id);
adu.setFunctionCode(functionCode);
adu.setDataRegister(0, startAddress);
adu.setDataRegister(2, quantity);
adu.setDataLen(4);
if (_rtuComm._waiting_for_read == false) _rtuComm.writeAdu(adu);
ModbusRTUCommError commError = _rtuComm.readAdu(adu);
if (commError) return _translateCommError(commError);
if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID;
if (adu.getFunctionCode() == (functionCode + 0x80)) {
_exceptionResponse = adu.data[0];
return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE;
}
if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE;
uint16_t byteCount = div8RndUp(quantity);
if (adu.getDataLen() != (1 + byteCount)) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH;
if (adu.data[0] != byteCount) return MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT;
for (uint16_t i = 0; i < quantity; i++) {
buf[i] = (int) bitRead(adu.data[1 + (i >> 3)], i & 7)? 1:0;
}
return MODBUS_RTU_MASTER_SUCCESS;
}
ModbusRTUMasterError Modbus::_readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, uint16_t buf[], uint16_t quantity) {
if (id < 1 || id > 247) return MODBUS_RTU_MASTER_INVALID_ID;
if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER;
if (quantity == 0 || quantity > 125) return MODBUS_RTU_MASTER_INVALID_QUANTITY;
ModbusADU adu;
adu.setUnitId(id);
adu.setFunctionCode(functionCode);
adu.setDataRegister(0, startAddress);
adu.setDataRegister(2, quantity);
adu.setDataLen(4);
if (_rtuComm._waiting_for_read == false) _rtuComm.writeAdu(adu);
ModbusRTUCommError commError = _rtuComm.readAdu(adu);
if (commError) return _translateCommError(commError);
if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID;
if (adu.getFunctionCode() == (functionCode + 0x80)) {
_exceptionResponse = adu.data[0];
return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE;
}
if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE;
uint16_t byteCount = quantity * 2;
if (adu.getDataLen() != (1 + byteCount)) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH;
if (adu.data[0] != byteCount) return MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT;
for (uint16_t i = 0; i < quantity; i++) {
buf[i] = adu.getDataRegister(1 + (i * 2));
}
return MODBUS_RTU_MASTER_SUCCESS;
}
ModbusRTUMasterError Modbus::_writeSingleValue(uint8_t id, uint8_t functionCode, uint16_t address, uint16_t value) {
if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID;
ModbusADU adu;
adu.setUnitId(id);
adu.setFunctionCode(functionCode);
adu.setDataRegister(0, address);
adu.setDataRegister(2, value);
adu.setDataLen(4);
if (_rtuComm._waiting_for_read == false) _rtuComm.writeAdu(adu);
if (id == 0) return MODBUS_RTU_MASTER_SUCCESS;
ModbusRTUCommError commError = _rtuComm.readAdu(adu);
if (commError) return _translateCommError(commError);
if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID;
if (adu.getFunctionCode() == (functionCode + 0x80)) {
_exceptionResponse = adu.data[0];
return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE;
}
if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE;
if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH;
if (adu.getDataRegister(0) != address) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS;
if (adu.getDataRegister(2) != value) return MODBUS_RTU_MASTER_UNEXPECTED_VALUE;
return MODBUS_RTU_MASTER_SUCCESS;
}
ModbusRTUMasterError Modbus::_translateCommError(ModbusRTUCommError commError) {
switch (commError) {
case MODBUS_RTU_COMM_SUCCESS:
return MODBUS_RTU_MASTER_SUCCESS;
case MODBUS_RTU_COMM_TIMEOUT:
return MODBUS_RTU_MASTER_RESPONSE_TIMEOUT;
case MODBUS_RTU_COMM_FRAME_ERROR:
return MODBUS_RTU_MASTER_FRAME_ERROR;
case MODBUS_RTU_COMM_CRC_ERROR:
return MODBUS_RTU_MASTER_CRC_ERROR;
case MODBUS_RTU_COMM_WAITING:
return MODBUS_RTU_MASTER_WAITING;
default:
return MODBUS_RTU_MASTER_UNKNOWN_COMM_ERROR;
}
}
/************************************************************
* Modbus implementation
************************************************************/
// Constructor for Modbus
Modbus::Modbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, int8_t txPin, int waitA, int waitB) : _rtuComm(serial, txPin) {
_baud = baud;
_serialD = &serial;
_txPin = txPin;
_rtuComm.setTimeout(500);
_busNo = busNo;
_cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds.
_waitA = waitA;
_waitB = waitB;
if (_waitA < 3) _waitA = 3;
if (_waitB < 2) _waitB = 2;
// Add device to HAL device chain
IODevice::addDevice(this);
// Add bus to Modbus chain.
_nextBus = _busList;
_busList = this;
}
// Main loop function for Modbus.
// Work through list of nodes. For each node, in separate loop entries
// When the slot time has finished, move on to the next device.
void Modbus::_loop(unsigned long currentMicros) {
_currentMicros = currentMicros;
if (_currentNode == NULL) {
_currentNode = _nodeListStart;
}
if (_currentMicros - _cycleStartTime < _cycleTime) return;
_cycleStartTime = _currentMicros;
if (_currentNode == NULL) return;
const char* errorStrings[16] = { "success", "invalid id", "invalid buffer", "invalid quantity", "response timeout", "frame error", "crc error", "unknown comm error", "unexpected id", "exception response", "unexpected function code", "unexpected response length", "unexpected byte count", "unexpected address", "unexpected value", "unexpected quantity" };
bool flagOK = true;
#if defined(MODBUS_STM_COMM)
ArduinoPins::fastWriteDigital(MODBUS_STM_COMM,HIGH);
#endif
uint8_t error;
// send reads and writes, DIAG on errors other than 0 (Success), or 3 (Invalid Quantity)
switch (_operationCount) {
case 0:
error = writeMultipleHoldingRegisters(_currentNode->getNodeID(), 0, (uint16_t*) _currentNode->holdingRegisters, _currentNode->getNumHoldingRegisters());
if (error != MODBUS_RTU_MASTER_SUCCESS && error != MODBUS_RTU_MASTER_WAITING) DIAG(F("ModbusHR: T%d F%d N%d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumHoldingRegisters(), errorStrings[error]);
if (error != MODBUS_RTU_MASTER_SUCCESS && (error != MODBUS_RTU_MASTER_WAITING || _waitCounter > 2)) flagOK = false;
break;
case 1:
error = writeMultipleCoils(_currentNode->getNodeID(), 0, (int*) _currentNode->coils, _currentNode->getNumCoils());
if (error != MODBUS_RTU_MASTER_SUCCESS && error != MODBUS_RTU_MASTER_WAITING) DIAG(F("ModbusMC: T%d F%d N%d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumCoils(), errorStrings[error]);
if (error != MODBUS_RTU_MASTER_SUCCESS && (error != MODBUS_RTU_MASTER_WAITING || _waitCounter > 2)) flagOK = false;
break;
case 2:
error = readDiscreteInputs(_currentNode->getNodeID(), 0, (int*) _currentNode->discreteInputs, _currentNode->getNumDiscreteInputs());
if (error != MODBUS_RTU_MASTER_SUCCESS && error != MODBUS_RTU_MASTER_WAITING) DIAG(F("ModbusDI: T%d F%d N%d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumDiscreteInputs(), errorStrings[error]);
if (error != MODBUS_RTU_MASTER_SUCCESS && (error != MODBUS_RTU_MASTER_WAITING || _waitCounter > 2)) flagOK = false;
break;
case 3:
error = readInputRegisters(_currentNode->getNodeID(), 0, (uint16_t*) _currentNode->inputRegisters, _currentNode->getNumInputRegisters());
if (error != MODBUS_RTU_MASTER_SUCCESS && error != MODBUS_RTU_MASTER_WAITING) DIAG(F("ModbusIR: T%d F%d N%d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumInputRegisters(), errorStrings[error]);
if (error != MODBUS_RTU_MASTER_SUCCESS && (error != MODBUS_RTU_MASTER_WAITING || _waitCounter > 2)) flagOK = false;
break;
}
if (error == MODBUS_RTU_MASTER_WAITING) {
if (_waitCounter > _waitA) { // retry after 10 cycles of waiting, or user setting waitA.
_resetWaiting(); // reset Waiting flag so it retries.
_waitCounter = 0;
_waitCounterB++;
} else {
_waitCounter++;
}
if (_waitCounterB > _waitB) { // move on to next node if fails 10 times, or user setting waitB.
_waitCounter = 0;
_waitCounterB = 0;
_operationCount = 0;
_currentNode = _currentNode->getNext();
}
} else {
_waitCounter = 0;
_waitCounterB = 0;
}
if (error == MODBUS_RTU_MASTER_SUCCESS) { // should have the effect of retrying same opperation until success
if (_operationCount < 3) { // unless it fails waitB and moves on to next node. may even
_operationCount++; // improve error recovery...
} else {
_operationCount = 0;
_currentNode = _currentNode->getNext();
}
}
#if defined(MODBUS_STM_OK)
if (flagOK == true) {
ArduinoPins::fastWriteDigital(MODBUS_STM_OK,HIGH);
} else {
ArduinoPins::fastWriteDigital(MODBUS_STM_OK,LOW);
}
#endif
#if defined(MODBUS_STM_FAIL)
if (flagOK == false) {
ArduinoPins::fastWriteDigital(MODBUS_STM_FAIL,HIGH);
} else {
ArduinoPins::fastWriteDigital(MODBUS_STM_FAIL,LOW);
}
#endif
#if defined(MODBUS_STM_COMM)
ArduinoPins::fastWriteDigital(MODBUS_STM_COMM,LOW);
#endif
}
// Link to chain of Modbus instances
Modbus *Modbus::_busList = NULL;
/************************************************************
* Modbusnode implementation
************************************************************/
// Constructor for Modbusnode object
Modbusnode::Modbusnode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils, uint8_t numDiscreteInputs, uint8_t numHoldingRegisters, uint8_t numInputRegisters) {
_firstVpin = firstVpin;
_nPins = nPins;
_busNo = busNo;
_nodeID = nodeID;
_numCoils = numCoils;
_numDiscreteInputs = numDiscreteInputs;
_numHoldingRegisters = numHoldingRegisters;
_numInputRegisters = numInputRegisters;
if ((unsigned int)_nPins < numDiscreteInputs + numCoils)
DIAG(F("Modbusnode: bus:%d nodeID:%d WARNING number of Vpins does not cover all inputs and outputs"), _busNo, _nodeID);
if (!discreteInputs || !coils) {
DIAG(F("Modbusnode: ERROR insufficient memory"));
return;
}
// Add this device to HAL device list
IODevice::addDevice(this);
_display();
// Add Modbusnode to Modbus object.
Modbus *bus = Modbus::findBus(_busNo);
if (bus != NULL) {
bus->addNode(this);
return;
}
}

412
IO_Modbus.h Normal file
View File

@ -0,0 +1,412 @@
/*
* © 2024, Travis Farmer. All rights reserved.
* © 2024, Chris Bulliner. All rights reserved. https://github.com/CMB27
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Modbus
* =======
* To define a Modbus, example syntax:
* Modbus::create(bus, serial, baud[, cycletime[, pin]]);
*
* bus = 0-255
* serial = serial port to be used (e.g. Serial3)
* baud = baud rate (9600, 19200, 28800, 57600 or 115200)
* cycletime = minimum time between successive updates/reads of a node in millisecs (default 500ms)
* pin = pin number connected to RS485 module's DE and !RE terminals for half-duplex operation (default VPIN_NONE)
*
* Each bus must use a different serial port.
*
* ModbusNode
* ========
* To define a CMRI node and associate it with a CMRI bus,
* CMRInode::create(firstVPIN, numVPINs, bus, nodeID, type [, inputs, outputs]);
*
* firstVPIN = first vpin in block allocated to this device
* numVPINs = number of vpins (e.g. 72 for an SMINI node)
* bus = 0-255
* nodeID = 0-127
* numDiscreteInputs = number of discrete inputs
* numCoils = number of coils
*
* Reference: "LCS-9.10.1
* Layout Control Specification: CMRInet Protocol
* Version 1.1 December 2014."
*/
#ifndef IO_MODBUS_H
#define IO_MODBUS_H
#include "IODevice.h"
class ModbusADU {
public:
uint8_t *rtu = _adu + 6;
uint8_t *tcp = _adu;
uint8_t *pdu = _adu + 7;
uint8_t *data = _adu + 8;
void setTransactionId(uint16_t transactionId);
void setProtocolId(uint16_t protocolId);
void setLength(uint16_t length);
void setUnitId(uint8_t unitId);
void setFunctionCode(uint8_t functionCode);
void setDataRegister(uint8_t index, uint16_t value);
void setRtuLen(uint16_t rtuLen);
void setTcpLen(uint16_t tcpLen);
void setPduLen(uint16_t pduLen);
void setDataLen(uint16_t dataLen);
uint16_t getTransactionId();
uint16_t getProtocolId();
uint16_t getLength();
uint8_t getUnitId();
uint8_t getFunctionCode();
uint16_t getDataRegister(uint8_t index);
uint16_t getRtuLen();
uint16_t getTcpLen();
uint16_t getPduLen();
uint16_t getDataLen();
void updateCrc();
bool crcGood();
void prepareExceptionResponse(uint8_t exceptionCode);
private:
uint8_t _adu[262];
void _setRegister(uint8_t *buf, uint16_t index, uint16_t value);
uint16_t _getRegister(uint8_t *buf, uint16_t index);
uint16_t _calculateCrc(uint16_t len);
};
uint16_t div8RndUp(uint16_t value);
enum ModbusRTUCommError : uint8_t {
MODBUS_RTU_COMM_SUCCESS = 0,
MODBUS_RTU_COMM_TIMEOUT = 1,
MODBUS_RTU_COMM_FRAME_ERROR = 2,
MODBUS_RTU_COMM_CRC_ERROR = 3,
MODBUS_RTU_COMM_WAITING = 4
};
class ModbusRTUComm {
public:
ModbusRTUComm(Stream& serial, VPIN dePin = VPIN_NONE, VPIN rePin = VPIN_NONE);
void begin(unsigned long baud, uint32_t config = SERIAL_8N1);
void setTimeout(unsigned long timeout);
ModbusRTUCommError readAdu(ModbusADU& adu);
bool _waiting_for_read = false;
void writeAdu(ModbusADU& adu);
void clearRxBuffer();
Stream& _serial;
VPIN _dePin;
VPIN _rePin;
private:
unsigned long _charTimeout;
unsigned long _frameTimeout;
unsigned long _postDelay = 0UL;
unsigned long _readTimeout = 0UL;
unsigned long _startTimeout = 0UL;
};
enum ModbusRTUMasterError : uint8_t {
MODBUS_RTU_MASTER_SUCCESS = 0,
MODBUS_RTU_MASTER_INVALID_ID = 1,
MODBUS_RTU_MASTER_INVALID_BUFFER = 2,
MODBUS_RTU_MASTER_INVALID_QUANTITY = 3,
MODBUS_RTU_MASTER_RESPONSE_TIMEOUT = 4,
MODBUS_RTU_MASTER_FRAME_ERROR = 5,
MODBUS_RTU_MASTER_CRC_ERROR = 6,
MODBUS_RTU_MASTER_UNKNOWN_COMM_ERROR = 7,
MODBUS_RTU_MASTER_UNEXPECTED_ID = 8,
MODBUS_RTU_MASTER_EXCEPTION_RESPONSE = 9,
MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE = 10,
MODBUS_RTU_MASTER_UNEXPECTED_LENGTH = 11,
MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT = 12,
MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS = 13,
MODBUS_RTU_MASTER_UNEXPECTED_VALUE = 14,
MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY = 15,
MODBUS_RTU_MASTER_WAITING = 16
};
/**********************************************************************
* Modbusnode class
*
* This encapsulates the state associated with a single Modbus node,
* which includes the nodeID, number of discrete inputs and coils, and
* the states of the discrete inputs and coils.
**********************************************************************/
class Modbusnode : public IODevice {
private:
uint8_t _busNo;
uint8_t _nodeID;
char _type;
Modbusnode *_next = NULL;
bool _initialised = false;
uint8_t _numCoils;
uint8_t _numDiscreteInputs;
uint8_t _numHoldingRegisters;
uint8_t _numInputRegisters;
public:
static void create(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils=0, uint8_t numDiscreteInputs=0, uint8_t numHoldingRegisters=0, uint8_t numInputRegisters=0) {
if (checkNoOverlap(firstVpin, nPins)) new Modbusnode(firstVpin, nPins, busNo, nodeID, numCoils, numDiscreteInputs, numHoldingRegisters, numInputRegisters);
}
Modbusnode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils, uint8_t numDiscreteInputs, uint8_t numHoldingRegisters, uint8_t numInputRegisters);
int *coils[100];
int *discreteInputs[100];
uint16_t *holdingRegisters[100];
uint16_t *inputRegisters[100];
uint8_t getNodeID() {
return _nodeID;
}
uint8_t getNumCoils() {
return _numCoils;
}
uint8_t getNumDiscreteInputs() {
return _numDiscreteInputs;
}
uint8_t getNumHoldingRegisters() {
return _numHoldingRegisters;
}
uint8_t getNumInputRegisters() {
return _numInputRegisters;
}
Modbusnode *getNext() {
return _next;
}
void setNext(Modbusnode *node) {
_next = node;
}
bool isInitialised() {
return _initialised;
}
void setInitialised() {
_initialised = true;
}
void _begin() override {
_initialised = false;
}
int _read(VPIN vpin) override {
// Return current state from this device
uint16_t pin = vpin - _firstVpin;
return (int) discreteInputs[pin];
}
void _write(VPIN vpin, int value) override {
// Update current state for this device, in preparation the bus transmission
uint16_t pin = vpin - _firstVpin - _numDiscreteInputs;
if (value == 1) coils[pin] = (int*) 0x1;
if (value == 0) coils[pin] = (int*) 0x0;
}
int _readAnalogue(VPIN vpin) {
// Return acquired data value, e.g.
int pin = vpin - _firstVpin - _numDiscreteInputs - _numCoils;
return (int) inputRegisters[pin];
}
void _writeAnalogue(VPIN vpin, int value) {
uint16_t pin = vpin - _firstVpin - _numDiscreteInputs - _numCoils - _numInputRegisters;
holdingRegisters[pin] = (uint16_t*) value;
}
uint8_t getBusNumber() {
return _busNo;
}
uint8_t getNumBinaryInputsVPINsMin() {
if (_numDiscreteInputs > 0) return _firstVpin;
else return 0;
}
uint8_t getNumBinaryInputsVPINsMax() {
if (_numDiscreteInputs > 0) return _firstVpin+_numDiscreteInputs-1;
else return 0;
}
uint8_t getNumBinaryOutputsVPINsMin() {
if (_numCoils > 0) return _firstVpin+_numDiscreteInputs;
else return 0;
}
uint8_t getNumBinaryOutputsVPINsMax() {
if (_numCoils > 0) return _firstVpin+_numDiscreteInputs+_numCoils-1;
else return 0;
}
uint8_t getNumAnalogInputsVPINsMin() {
if (_numInputRegisters > 0) return _firstVpin+_numDiscreteInputs+_numCoils;
else return 0;
}
uint8_t getNumAnalogInputsVPINsMax() {
if (_numInputRegisters > 0) return _firstVpin+_numDiscreteInputs+_numCoils+_numInputRegisters-1;
else return 0;
}
uint8_t getNumAnalogOutputsVPINsMin() {
if (_numHoldingRegisters > 0) return _firstVpin+_numDiscreteInputs+_numCoils+_numInputRegisters;
else return 0;
}
uint8_t getNumAnalogOutputsVPINsMax() {
if (_numHoldingRegisters > 0) return _firstVpin+_numDiscreteInputs+_numCoils+_numInputRegisters+_numHoldingRegisters-1;
else return 0;
}
void _display() override {
DIAG(F("Modbusnode configured on bus:%d nodeID:%d VPINs:%u-%u (B In) %u-%u (B Out) %u-%u (A In) %u-%u (A Out)"),
_busNo, _nodeID, getNumBinaryInputsVPINsMin(), getNumBinaryInputsVPINsMax(),
getNumBinaryOutputsVPINsMin(), getNumBinaryOutputsVPINsMax(),
getNumAnalogInputsVPINsMin(), getNumAnalogInputsVPINsMax(),
getNumAnalogOutputsVPINsMin(), getNumAnalogOutputsVPINsMax());
}
};
/**********************************************************************
* Modbus class
*
* This encapsulates the properties state of the bus and the
* transmission and reception of data across that bus. Each Modbus
* object owns a set of Modbusnode objects which represent the nodes
* attached to that bus.
**********************************************************************/
class Modbus : public IODevice {
private:
// Here we define the device-specific variables.
uint8_t _busNo;
unsigned long _baud;
int8_t _txPin;
Modbusnode *_nodeListStart = NULL, *_nodeListEnd = NULL;
Modbusnode *_currentNode = NULL;
uint8_t _exceptionResponse = 0;
uint8_t getExceptionResponse();
uint16_t _receiveDataIndex = 0; // Index of next data byte to be received.
Modbus *_nextBus = NULL; // Pointer to next bus instance in list.
void setTimeout(unsigned long timeout);
unsigned long _cycleStartTime = 0;
unsigned long _timeoutStart = 0;
unsigned long _cycleTime; // target time between successive read/write cycles, microseconds
unsigned long _timeoutPeriod; // timeout on read responses, in microseconds.
unsigned long _currentMicros; // last value of micros() from _loop function.
unsigned long _postDelay; // delay time after transmission before switching off transmitter (in us)
unsigned long _byteTransmitTime; // time in us for transmission of one byte
int _operationCount = 0;
static Modbus *_busList; // linked list of defined bus instances
ModbusRTUMasterError _readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, int buf[], uint16_t quantity);
ModbusRTUMasterError _readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, uint16_t buf[], uint16_t quantity);
ModbusRTUMasterError _writeSingleValue(uint8_t id, uint8_t functionCode, uint16_t address, uint16_t value);
int _waitCounter = 0;
int _waitCounterB = 0;
int _waitA;
int _waitB;
void _resetWaiting() {
_rtuComm._waiting_for_read = false;
}
public:
static void create(uint8_t busNo, HardwareSerial& serial, unsigned long baud, uint16_t cycleTimeMS=500, int8_t txPin=-1, int waitA=10, int waitB=10) {
new Modbus(busNo, serial, baud, cycleTimeMS, txPin, waitA, waitB);
}
HardwareSerial *_serialD;
ModbusRTUComm _rtuComm;
// Device-specific initialisation
void _begin() override {
_serialD->begin(_baud, SERIAL_8N1);
_rtuComm.begin(_baud, SERIAL_8N1);
#if defined(MODBUS_STM_OK)
pinMode(MODBUS_STM_OK, OUTPUT);
ArduinoPins::fastWriteDigital(MODBUS_STM_OK,LOW);
#endif
#if defined(MODBUS_STM_FAIL)
pinMode(MODBUS_STM_FAIL, OUTPUT);
ArduinoPins::fastWriteDigital(MODBUS_STM_FAIL,LOW);
#endif
#if defined(MODBUS_STM_COMM)
pinMode(MODBUS_STM_COMM, OUTPUT);
ArduinoPins::fastWriteDigital(MODBUS_STM_COMM,LOW);
#endif
#if defined(DIAG_IO)
_display();
#endif
}
ModbusRTUMasterError _translateCommError(ModbusRTUCommError commError);
ModbusRTUMasterError readCoils(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity);
ModbusRTUMasterError readDiscreteInputs(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity);
ModbusRTUMasterError readHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity);
ModbusRTUMasterError readInputRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity);
ModbusRTUMasterError writeSingleCoil(uint8_t id, uint16_t address, int value);
ModbusRTUMasterError writeSingleHoldingRegister(uint8_t id, uint16_t address, uint16_t value);
ModbusRTUMasterError writeMultipleCoils(uint8_t id, uint16_t startAddress, int buf[], uint16_t quantity);
ModbusRTUMasterError writeMultipleHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity);
// Loop function (overriding IODevice::_loop(unsigned long))
void _loop(unsigned long currentMicros) override;
// Display information about the device
void _display() override {
DIAG(F("Modbus Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("OK"));
}
// Locate Modbusnode object with specified nodeID.
Modbusnode *findNode(uint8_t nodeID) {
for (Modbusnode *node = _nodeListStart; node != NULL; node = node->getNext()) {
if (node->getNodeID() == nodeID)
return node;
}
return NULL;
}
// Add new Modbusnode to the list of nodes for this bus.
void addNode(Modbusnode *newNode) {
if (!_nodeListStart)
_nodeListStart = newNode;
if (!_nodeListEnd)
_nodeListEnd = newNode;
else
_nodeListEnd->setNext(newNode);
//DIAG(F("Modbus: 260h nodeID:%d _nodeListStart:%d _nodeListEnd:%d"), newNode, _nodeListStart, _nodeListEnd);
}
protected:
Modbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, int8_t txPin, int waitA, int waitB);
public:
uint8_t getBusNumber() {
return _busNo;
}
static Modbus *findBus(uint8_t busNo) {
for (Modbus *bus=_busList; bus!=NULL; bus=bus->_nextBus) {
if (bus->_busNo == busNo) return bus;
}
return NULL;
}
};
#endif // IO_MODBUS_H