|
/** |
|
* Boseji's Simple Modbus Slave and Bridge over USB-to-UART |
|
* |
|
* ## Introduction |
|
* |
|
* Here is the Program outline: |
|
* |
|
* - The hardware serial (USB-to-UART) is configured |
|
* at 19200 baud as the Modbus RTU slave. |
|
* |
|
* - A SoftwareSerial instance is created |
|
* (on pins 10 and 11) to act as a "bridge" port. |
|
* When an incoming Modbus frame has a slave address |
|
* different from the one defined (here set as 1), |
|
* the complete frame is forwarded to the |
|
* software port and any reply is passed back. |
|
* |
|
* - 16 digital I/O pins |
|
* First the original digital pins |
|
* (2, 3, 4, 5, 6, 7, 8, 9, 12, 13) plus the analog |
|
* pins A0–A5 (which can also be used as digital I/O) |
|
* are mapped as “Modbus coils” (for outputs) |
|
* or “discrete inputs” (if configured as inputs). |
|
* The code uses digitalWrite/digitalRead accordingly. |
|
* |
|
* - Two 16‑bit holding registers are used: |
|
* one (regDirection) to set each pin’s direction |
|
* (bit = 1 means output, 0 means input) |
|
* and a second (regPullup) to specify if the input pin |
|
* should have an internal pull‑up. |
|
* |
|
* - Analog Input Registers: |
|
* The two analog-only pins A6 and A7 are configured |
|
* to be read via the ADC. |
|
* They are mapped as two input registers |
|
* (using standard Modbus function code 0x04). |
|
* When a master requests input registers starting |
|
* at address 0 for a count of 1 or 2, the sketch |
|
* reads A6 (register 0) and A7 (register 1) using |
|
* analogRead and returns the ADC values |
|
* (typically 10‑bit readings). |
|
* |
|
* - Basic support for Modbus function codes is implemented: |
|
* - **0x01:** Read Coils. |
|
* - **0x02:** Read Discrete Inputs. |
|
* - **0x04:** Read Input Registers. |
|
* - **0x05:** Write Single Coil. |
|
* - **0x0F:** Write Multiple Coils. |
|
* - **0x03:** Read Holding Registers. |
|
* - **0x06:** Write Single Holding Register. |
|
* - **0x10:** Write Multiple Holding Registers. |
|
* |
|
* - A CRC16 routine is used to check and append the |
|
* Modbus CRC. |
|
* |
|
* You can adjust the mapping (number of pins, |
|
* SoftwareSerial pins, etc.) as needed. |
|
* |
|
* ## Explanation |
|
* |
|
* 1. **Modbus Slave & Bridging:** |
|
* The sketch listens on hardware Serial at 19200 baud. |
|
* When a complete Modbus RTU frame is received, |
|
* the CRC is checked. |
|
* If the slave address (first byte) does not match |
|
* the device’s address (`MODBUS_SLAVE_ID`), |
|
* the frame is forwarded to the SoftwareSerial port. |
|
* The response received on the software serial is |
|
* then sent back via hardware serial. |
|
* |
|
* 2. **GPIO & Coils:** |
|
* An array (`modbusPins`) defines which Arduino digital |
|
* pins are used. |
|
* The two holding registers (`regDirection` and |
|
* `regPullup`) are used to configure each pin’s mode. |
|
* When a coil is written (via function codes 0x05 or |
|
* 0x0F) and the pin is set as output, |
|
* the code calls `digitalWrite` and updates |
|
* the `coilStates` array. |
|
* For reading coils (0x01) or discrete inputs (0x02), |
|
* the code either returns the stored state (for outputs) |
|
* or reads the pin (for inputs). |
|
* |
|
* 3. **Holding Registers:** |
|
* Reading (0x03) and writing (0x06/0x10) |
|
* holding registers allow external controllers |
|
* to change the pin configuration. |
|
* The `updatePinModes()` function reconfigures |
|
* the pins immediately when these registers change. |
|
* |
|
* 4. **Input Registers:** |
|
* When the master sends a Read Input Registers (0x04) |
|
* request for registers 0 and/or 1, the sketch performs |
|
* an analogRead on A6 (register 0) and A7 (register 1) |
|
* and returns the ADC values. |
|
* |
|
* Feel free to adjust the mapping, timing values, or |
|
* supported function codes to better suit your hardware |
|
* and application needs. |
|
* |
|
* ## MODPOLL Command Reference |
|
* |
|
* Base Command |
|
* |
|
* ```sh |
|
* modpoll -m rtu -b 19200 -a 1 -p none -t 0 -r 9 -c 4 \ |
|
* /dev/ttyUSB0 0 0 0 0 |
|
* ``` |
|
* |
|
* - `-m rtu` is the Mode of the MODBUS protocol as RTU |
|
* - `-b 19200` is the Baud Rate |
|
* - `-a 1` is the Slave ID for the Device |
|
* - `-p none` for not using partity. |
|
* By default even partiy is used. |
|
* - `-t 0` is for Coils |
|
* - `-r 9` is for the 9th Coil |
|
* - `-c 4` means 4 coils will be written |
|
* - `/dev/ttyUSB0` is the UART Port of the Board |
|
* - End are the values written. |
|
* |
|
* IO Configuration |
|
* |
|
* ```sh |
|
* modpoll -m rtu -b 19200 -a 1 -p none -t 4 -r 1 -c 1 \ |
|
* /dev/ttyUSB0 0xFFFF |
|
* ``` |
|
* This would configure all the pins as coils by writing |
|
* to the holding register `regDirection`. |
|
* |
|
* ## License |
|
* |
|
* SPDX: Apache-2.0 |
|
* |
|
* *Boseji's Simple Modbus Slave and Bridge over USB-to-UART* |
|
* |
|
* Copyright (C) 2024 Abhijit Bose (aka. Boseji). All rights reserved. |
|
* |
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
* you may not use this file except in compliance with the License. |
|
* You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, software |
|
* distributed under the License is distributed on an "AS IS" BASIS, |
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
* See the License for the specific language governing permissions and |
|
* limitations under the License. |
|
*/ |
|
#include <SoftwareSerial.h> |
|
|
|
// ----- Configuration ----- |
|
// Modbus slave ID for this device: |
|
#define MODBUS_SLAVE_ID 1 |
|
// Size of our modbus buffers: |
|
#define MODBUS_BUFFER_SIZE 256 |
|
// Enable this to Configure EVEN Parity |
|
//#define MODBUS_RTU_EVEN_PARITY |
|
|
|
// Create a SoftwareSerial port for bridging (RX, TX) |
|
SoftwareSerial softSerial(10, 11); // Ensure these pins are not used for other I/O |
|
|
|
// ----- Pin Mapping ----- |
|
// Expanded list: 10 modbus pins from digital pins + A0-A5 = 16 pins in total. |
|
// A6 and A7 are reserved for ADC input registers. |
|
const int numPins = 16; |
|
const int modbusPins[numPins] = { |
|
2, 3, 4, 5, 6, 7, 8, 9, 12, 13, // Digital pins |
|
A0, A1, A2, A3, A4, A5 // Analog pins used as digital I/O |
|
}; |
|
|
|
// Two holding registers used for configuration: |
|
// - regDirection: bit = 1 means pin is an OUTPUT, 0 = INPUT. |
|
// - regPullup: bit = 1 means if INPUT then enable internal pull-up. |
|
uint16_t regDirection = 0; // default: all pins as input |
|
uint16_t regPullup = 0; // default: no pull-ups |
|
|
|
// Array to store the state of each “coil” (used when a pin is configured as output) |
|
bool coilStates[numPins] = { false }; |
|
|
|
// Buffer variables for Modbus message reception on hardware Serial |
|
byte modbusBuffer[MODBUS_BUFFER_SIZE]; |
|
int bufferIndex = 0; |
|
unsigned long lastByteTime = 0; |
|
const unsigned int interFrameDelay = 5; // milliseconds to wait after last byte |
|
|
|
// ----- Function Prototypes ----- |
|
uint16_t modbusCRC(byte *buf, int len); |
|
void updatePinModes(); |
|
void processModbusFrame(byte* frame, int len); |
|
void sendModbusException(byte slaveId, byte functionCode, byte exceptionCode); |
|
void bridgeModbusFrame(byte* frame, int len); |
|
|
|
void setup() { |
|
#ifndef MODBUS_RTU_EVEN_PARITY |
|
// Initialize hardware Serial (USB-to-UART) for Modbus RTU at 19200 baud. |
|
Serial.begin(19200); |
|
// Initialize software serial at 19200 baud. |
|
softSerial.begin(19200); |
|
#else |
|
Serial.begin(19200, SERIAL_8E1); |
|
softSerial.begin(19200); // Even Parity not available here |
|
#endif |
|
|
|
// Initialize configuration registers: all modbus pins as INPUT without pull-up. |
|
regDirection = 0; |
|
regPullup = 0; |
|
|
|
// Set default coil states (false means LOW) |
|
for (int i = 0; i < numPins; i++) { |
|
coilStates[i] = false; |
|
} |
|
|
|
// Set the pin modes based on current configuration. |
|
updatePinModes(); |
|
} |
|
|
|
void loop() { |
|
// Read available bytes from hardware Serial (Modbus RTU) |
|
while (Serial.available() > 0) { |
|
byte b = Serial.read(); |
|
if (bufferIndex < MODBUS_BUFFER_SIZE) { |
|
modbusBuffer[bufferIndex++] = b; |
|
lastByteTime = millis(); |
|
} |
|
} |
|
|
|
// When a pause (inter-frame delay) has passed, process the received frame. |
|
if (bufferIndex > 0 && (millis() - lastByteTime > interFrameDelay)) { |
|
processModbusFrame(modbusBuffer, bufferIndex); |
|
bufferIndex = 0; // Reset for the next frame. |
|
} |
|
} |
|
|
|
// ----- Utility Functions ----- |
|
|
|
// Calculate Modbus RTU CRC16 for the given buffer. |
|
uint16_t modbusCRC(byte *buf, int len) { |
|
uint16_t crc = 0xFFFF; |
|
for (int pos = 0; pos < len; pos++) { |
|
crc ^= buf[pos]; |
|
for (int i = 0; i < 8; i++) { |
|
if (crc & 0x0001) { |
|
crc >>= 1; |
|
crc ^= 0xA001; |
|
} else { |
|
crc >>= 1; |
|
} |
|
} |
|
} |
|
return crc; |
|
} |
|
|
|
// Update each pin’s mode based on the holding registers. |
|
void updatePinModes() { |
|
for (int i = 0; i < numPins; i++) { |
|
int pin = modbusPins[i]; |
|
// If the corresponding bit in regDirection is 1, set as OUTPUT. |
|
if (regDirection & (1 << i)) { |
|
pinMode(pin, OUTPUT); |
|
digitalWrite(pin, coilStates[i] ? HIGH : LOW); |
|
} else { |
|
// Otherwise, set as INPUT (with or without pull-up based on regPullup) |
|
if (regPullup & (1 << i)) { |
|
pinMode(pin, INPUT_PULLUP); |
|
} else { |
|
pinMode(pin, INPUT); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// ----- Modbus Processing Functions ----- |
|
|
|
// Process an incoming Modbus RTU frame. |
|
void processModbusFrame(byte* frame, int len) { |
|
// Minimum frame length: address, function, CRC low, CRC high. |
|
if (len < 4) return; |
|
|
|
// Verify CRC (last two bytes: low, then high) |
|
uint16_t receivedCRC = frame[len - 2] | (frame[len - 1] << 8); |
|
uint16_t calcCRC = modbusCRC(frame, len - 2); |
|
if (receivedCRC != calcCRC) { |
|
// CRC error – ignore the frame. |
|
return; |
|
} |
|
|
|
byte slaveId = frame[0]; |
|
// If the slave address is not ours, forward (bridge) the frame. |
|
if (slaveId != MODBUS_SLAVE_ID) { |
|
bridgeModbusFrame(frame, len); |
|
return; |
|
} |
|
|
|
byte functionCode = frame[1]; |
|
byte response[MODBUS_BUFFER_SIZE]; // Buffer for our reply. |
|
int responseLen = 0; |
|
|
|
switch (functionCode) { |
|
case 0x01: { // Read Coils |
|
// Request: [slaveId][0x01][startHi][startLo][qtyHi][qtyLo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startAddr = (frame[2] << 8) | frame[3]; |
|
int quantity = (frame[4] << 8) | frame[5]; |
|
if (quantity < 1 || quantity > numPins || (startAddr + quantity) > numPins) { |
|
sendModbusException(slaveId, functionCode, 2); // Illegal data address. |
|
return; |
|
} |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
int byteCount = (quantity + 7) / 8; |
|
response[2] = byteCount; |
|
// Pack coil statuses into response bytes. |
|
for (int i = 0; i < quantity; i++) { |
|
bool state; |
|
// If configured as output, use stored coil state; if input, do a digitalRead. |
|
if (regDirection & (1 << (startAddr + i))) { |
|
state = coilStates[startAddr + i]; |
|
} else { |
|
int pin = modbusPins[startAddr + i]; |
|
state = (digitalRead(pin) == HIGH); |
|
} |
|
int byteIndex = 3 + (i / 8); |
|
int bitIndex = i % 8; |
|
if (state) |
|
response[byteIndex] |= (1 << bitIndex); |
|
else |
|
response[byteIndex] &= ~(1 << bitIndex); |
|
} |
|
responseLen = 3 + byteCount; |
|
break; |
|
} |
|
case 0x02: { // Read Discrete Inputs |
|
// Request: [slaveId][0x02][startHi][startLo][qtyHi][qtyLo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startAddr = (frame[2] << 8) | frame[3]; |
|
int quantity = (frame[4] << 8) | frame[5]; |
|
if (quantity < 1 || quantity > numPins || (startAddr + quantity) > numPins) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
int byteCount = (quantity + 7) / 8; |
|
response[2] = byteCount; |
|
for (int i = 0; i < quantity; i++) { |
|
bool state; |
|
// For discrete inputs, if the pin is input, read its value. |
|
if (!(regDirection & (1 << (startAddr + i)))) { |
|
int pin = modbusPins[startAddr + i]; |
|
state = (digitalRead(pin) == HIGH); |
|
} else { |
|
// If the pin is output, return the stored coil state. |
|
state = coilStates[startAddr + i]; |
|
} |
|
int byteIndex = 3 + (i / 8); |
|
int bitIndex = i % 8; |
|
if (state) |
|
response[byteIndex] |= (1 << bitIndex); |
|
else |
|
response[byteIndex] &= ~(1 << bitIndex); |
|
} |
|
responseLen = 3 + byteCount; |
|
break; |
|
} |
|
case 0x04: { // Read Input Registers (for ADC on A6 and A7) |
|
// Request: [slaveId][0x04][startReg_Hi][startReg_Lo][regCount_Hi][regCount_Lo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startReg = (frame[2] << 8) | frame[3]; |
|
int regCount = (frame[4] << 8) | frame[5]; |
|
// We have two ADC channels: registers 0 (A6) and 1 (A7). |
|
if (startReg < 0 || (startReg + regCount) > 2) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
response[2] = regCount * 2; // Number of data bytes. |
|
for (int i = 0; i < regCount; i++) { |
|
uint16_t value = 0; |
|
if ((startReg + i) == 0) { |
|
value = analogRead(A6); |
|
} else if ((startReg + i) == 1) { |
|
value = analogRead(A7); |
|
} |
|
response[3 + i*2] = highByte(value); |
|
response[4 + i*2] = lowByte(value); |
|
} |
|
responseLen = 3 + regCount * 2; |
|
break; |
|
} |
|
case 0x05: { // Write Single Coil |
|
// Request: [slaveId][0x05][coilAddr_Hi][coilAddr_Lo][value_Hi][value_Lo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int coilAddr = (frame[2] << 8) | frame[3]; |
|
if (coilAddr < 0 || coilAddr >= numPins) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
uint16_t value = (frame[4] << 8) | frame[5]; |
|
bool newState = (value == 0xFF00); |
|
// Only update if the pin is configured as output. |
|
if (regDirection & (1 << coilAddr)) { |
|
coilStates[coilAddr] = newState; |
|
int pin = modbusPins[coilAddr]; |
|
digitalWrite(pin, newState ? HIGH : LOW); |
|
} |
|
// Echo the request as the response. |
|
for (int i = 0; i < 6; i++) { |
|
response[i] = frame[i]; |
|
} |
|
responseLen = 6; |
|
break; |
|
} |
|
case 0x0F: { // Write Multiple Coils |
|
// Request: [slaveId][0x0F][startHi][startLo][qtyHi][qtyLo][byteCount][coil data...][CRC_lo][CRC_hi] |
|
if (len < 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startAddr = (frame[2] << 8) | frame[3]; |
|
int quantity = (frame[4] << 8) | frame[5]; |
|
int byteCount = frame[6]; |
|
if (quantity < 1 || quantity > numPins || (startAddr + quantity) > numPins) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
if (byteCount != (quantity + 7) / 8) { |
|
sendModbusException(slaveId, functionCode, 3); |
|
return; |
|
} |
|
// Update each coil. |
|
for (int i = 0; i < quantity; i++) { |
|
int dataByte = frame[7 + i/8]; |
|
bool state = (dataByte >> (i % 8)) & 0x01; |
|
if (regDirection & (1 << (startAddr + i))) { |
|
coilStates[startAddr + i] = state; |
|
int pin = modbusPins[startAddr + i]; |
|
digitalWrite(pin, state ? HIGH : LOW); |
|
} |
|
} |
|
// Response: echo [slaveId][0x0F][startHi][startLo][qtyHi][qtyLo] |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
response[2] = frame[2]; |
|
response[3] = frame[3]; |
|
response[4] = frame[4]; |
|
response[5] = frame[5]; |
|
responseLen = 6; |
|
break; |
|
} |
|
case 0x03: { // Read Holding Registers (for configuration registers) |
|
// Request: [slaveId][0x03][startReg_Hi][startReg_Lo][regCount_Hi][regCount_Lo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startReg = (frame[2] << 8) | frame[3]; |
|
int regCount = (frame[4] << 8) | frame[5]; |
|
// We have two holding registers: index 0 (regDirection) and index 1 (regPullup). |
|
if (startReg < 0 || (startReg + regCount) > 2) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
response[2] = regCount * 2; // Number of data bytes. |
|
for (int i = 0; i < regCount; i++) { |
|
uint16_t value = 0; |
|
if ((startReg + i) == 0) { |
|
value = regDirection; |
|
} else if ((startReg + i) == 1) { |
|
value = regPullup; |
|
} |
|
response[3 + i*2] = highByte(value); |
|
response[4 + i*2] = lowByte(value); |
|
} |
|
responseLen = 3 + regCount * 2; |
|
break; |
|
} |
|
case 0x06: { // Write Single Holding Register |
|
// Request: [slaveId][0x06][regAddr_Hi][regAddr_Lo][value_Hi][value_Lo][CRC_lo][CRC_hi] |
|
if (len != 8) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int regAddr = (frame[2] << 8) | frame[3]; |
|
uint16_t regValue = (frame[4] << 8) | frame[5]; |
|
if (regAddr < 0 || regAddr > 1) { |
|
sendModbusException(slaveId, functionCode, 2); |
|
return; |
|
} |
|
if (regAddr == 0) { |
|
regDirection = regValue; |
|
} else if (regAddr == 1) { |
|
regPullup = regValue; |
|
} |
|
updatePinModes(); |
|
// Echo the request. |
|
for (int i = 0; i < 6; i++) { |
|
response[i] = frame[i]; |
|
} |
|
responseLen = 6; |
|
break; |
|
} |
|
case 0x10: { // Write Multiple Holding Registers |
|
// Request: [slaveId][0x10][startReg_Hi][startReg_Lo][regCount_Hi][regCount_Lo] |
|
// [byteCount][register data...][CRC_lo][CRC_hi] |
|
if (len < 9) { sendModbusException(slaveId, functionCode, 3); return; } |
|
int startReg = (frame[2] << 8) | frame[3]; |
|
int regCount = (frame[4] << 8) | frame[5]; |
|
int byteCount = frame[6]; |
|
if ((startReg < 0) || (startReg + regCount) > 2 || (byteCount != regCount * 2)) { |
|
sendModbusException(slaveId, functionCode, 3); |
|
return; |
|
} |
|
for (int i = 0; i < regCount; i++) { |
|
uint16_t value = (frame[7 + i*2] << 8) | frame[8 + i*2]; |
|
if ((startReg + i) == 0) { |
|
regDirection = value; |
|
} else if ((startReg + i) == 1) { |
|
regPullup = value; |
|
} |
|
} |
|
updatePinModes(); |
|
// Response echo: [slaveId][0x10][startReg_Hi][startReg_Lo][regCount_Hi][regCount_Lo] |
|
response[0] = slaveId; |
|
response[1] = functionCode; |
|
response[2] = frame[2]; |
|
response[3] = frame[3]; |
|
response[4] = frame[4]; |
|
response[5] = frame[5]; |
|
responseLen = 6; |
|
break; |
|
} |
|
default: |
|
// Unsupported function code – send exception response. |
|
sendModbusException(slaveId, functionCode, 1); |
|
return; |
|
} |
|
|
|
// Append CRC to the response. |
|
uint16_t respCRC = modbusCRC(response, responseLen); |
|
response[responseLen++] = lowByte(respCRC); |
|
response[responseLen++] = highByte(respCRC); |
|
|
|
// Send the complete response back over hardware Serial. |
|
Serial.write(response, responseLen); |
|
} |
|
|
|
// Send a Modbus exception response. |
|
void sendModbusException(byte slaveId, byte functionCode, byte exceptionCode) { |
|
byte response[5]; |
|
response[0] = slaveId; |
|
response[1] = functionCode | 0x80; // Set MSB to indicate exception. |
|
response[2] = exceptionCode; |
|
uint16_t crc = modbusCRC(response, 3); |
|
response[3] = lowByte(crc); |
|
response[4] = highByte(crc); |
|
Serial.write(response, 5); |
|
} |
|
|
|
// Bridge the Modbus frame to the SoftwareSerial port if the slave address does not match. |
|
void bridgeModbusFrame(byte* frame, int len) { |
|
// Forward the request. |
|
softSerial.write(frame, len); |
|
// Wait for a response (timeout ~100 ms). |
|
unsigned long startTime = millis(); |
|
while ((millis() - startTime) < 100) { |
|
if (softSerial.available() > 0) { |
|
int bytesAvailable = softSerial.available(); |
|
byte resp[MODBUS_BUFFER_SIZE]; |
|
int i = 0; |
|
while (softSerial.available() && i < MODBUS_BUFFER_SIZE) { |
|
resp[i++] = softSerial.read(); |
|
} |
|
// Forward the response back to the hardware serial. |
|
Serial.write(resp, i); |
|
break; |
|
} |
|
} |
|
} |