Skip to content

Instantly share code, notes, and snippets.

@boseji
Last active March 2, 2025 09:25
Show Gist options
  • Save boseji/3b698e79d7740be5952ddb3e23f31b98 to your computer and use it in GitHub Desktop.
Save boseji/3b698e79d7740be5952ddb3e23f31b98 to your computer and use it in GitHub Desktop.
Boseji's Simple Modbus RTU Slave and Bridge over USB-to-UART

Boseji's Simple Modbus Slave and Bridge over USB-to-UART

This project implements a Modbus RTU slave on an Arduino Nano board. The program communicates over the USB-to-UART connection at 19200 baud and supports bridging via a SoftwareSerial port. It allows digital I/O control for 16 pins (using digital pins and analog pins A0–A5 as digital I/O) and reads analog values from two dedicated ADC channels (A6 and A7) as input registers.

Table of Contents

Overview

This Arduino sketch emulates a Modbus RTU slave device. It listens for Modbus frames via the hardware serial (USB-to-UART) port, processes digital I/O commands, and reads ADC values. If an incoming frame is addressed to a different slave ID, it bridges the message to a secondary port (SoftwareSerial).

Features

  • Modbus RTU Slave Emulation:
    Implements a Modbus RTU slave that supports standard function codes.

  • Digital I/O Control:

    • Supports 16 I/O channels (digital pins 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, and analog pins A0–A5 used as digital I/O).
    • Uses two holding registers to configure each pin's mode: one for setting input/output and another for enabling internal pull-up resistors.
  • Analog Input Readings:

    • Configures A6 and A7 as dedicated ADC channels.
    • Reads ADC values via Modbus function code 0x04 (Read Input Registers).
  • Bridging Functionality:

    • If an incoming Modbus frame is not addressed to this device, it is forwarded via a SoftwareSerial port (pins 10 and 11).
  • CRC Error Checking:
    Implements a Modbus CRC16 routine for error detection.

Hardware Requirements

  • Arduino Nano (or a compatible board)
  • USB-to-UART connection for programming and serial communication
  • Required wiring/breadboard as needed
  • (Optional) External device/software for testing (e.g., modpoll CLI)

Software Requirements

  • Arduino IDE
  • SoftwareSerial library (included in the Arduino IDE)

Pin Mapping and Configuration

  • Digital I/O (Coils/Discrete Inputs):

    • Digital Pins: 2, 3, 4, 5, 6, 7, 8, 9, 12, 13
    • Analog Pins as Digital I/O: A0, A1, A2, A3, A4, A5
    • Coil/Input Numbering:
      Coil  1 = D2
      Coil  2 = D3
      Coil  3 = D4
      Coil  4 = D5
      Coil  5 = D6
      Coil  6 = D7
      Coil  7 = D8
      Coil  8 = D9
      Coil  9 = D12
      Coil 10 = D13 (LED also on the Arduino Nano Board)
      Coil 11 = A0
      Coil 12 = A1
      Coil 13 = A2
      Coil 14 = A3
      Coil 15 = A4
      Coil 16 = A5
    

    These 16 pins are used for coil/discrete input operations, with their behavior controlled by two holding registers:

    • Holding Register 0 (regDirection): Bit value
      1 configures the corresponding pin as an OUTPUT;
      0 configures it as an INPUT.
    • Holding Register 1 (regPullup): For pins configured as INPUT,
      a bit value of 1 enables the internal pull-up resistor.
  • Analog Input Registers:

    • ADC Channels:
      • A6 is used as Input Register 0
      • A7 is used as Input Register 1
  • SoftwareSerial Bridging:

    • RX: Pin D10
    • TX: Pin D11

Modbus Communication Details

Serial Settings

  • Baud Rate: 19200
  • Default Frame Format: 8 data bits, no parity, 1 stop bit (8N1)

    Note: Many Modbus master tools (like modpoll) default to even parity (8E1). In that case, either modify the Arduino's serial initialization to Serial.begin(19200, SERIAL_8E1) or instruct the master to use no parity.

Supported Function Codes

  • 0x01 – Read Coils:
    Reads the status of digital outputs (coils).

  • 0x02 – Read Discrete Inputs:
    Reads the status of digital inputs.

  • 0x04 – Read Input Registers:
    Reads ADC values from A6 and A7.

  • 0x03 – Read Holding Registers:
    Reads the configuration registers for pin direction and pull-up settings.

  • 0x05 – Write Single Coil:
    Writes a single digital output value.

  • 0x06 – Write Single Holding Register:
    Writes one of the configuration registers.

  • 0x0F – Write Multiple Coils:
    Writes multiple digital output values.

  • 0x10 – Write Multiple Holding Registers:
    Writes multiple configuration registers.

Code Overview and Functionality

The sketch is divided into several main sections:

CRC Calculation

  • Function: modbusCRC(byte *buf, int len)
  • Purpose: Computes the 16-bit CRC for incoming/outgoing Modbus frames, ensuring data integrity.

Pin Mode Update

  • Function: updatePinModes()
  • Purpose: Reads the holding registers (regDirection and regPullup) and configures each of the 16 digital I/O pins accordingly (either as INPUT, INPUT_PULLUP, or OUTPUT).

Modbus Frame Processing

  • Function: processModbusFrame(byte* frame, int len)
  • Purpose:
    • Validates the frame length and CRC.
    • Determines if the frame is intended for this device (based on slave address).
    • Processes supported function codes to read/write coils, discrete inputs, holding registers, or ADC values.
    • If the address does not match, the frame is forwarded (bridged) to the SoftwareSerial port.

Exception Handling

  • Function: sendModbusException(byte slaveId, byte functionCode, byte exceptionCode)
  • Purpose: Constructs and sends an exception response when an error occurs (e.g., illegal function, invalid address, or data error).

Bridging Logic

  • Function: bridgeModbusFrame(byte* frame, int len)
  • Purpose:
    • Forwards frames with mismatched slave addresses to the SoftwareSerial port.
    • Waits for and relays the response back to the hardware serial interface.

Testing and Troubleshooting

Testing with modpoll

A sample command to test reading a holding register:

modpoll -m rtu -b 19200 -a 1 -t 0 -r 1 -c 1 /dev/ttyUSB0 0

If you encounter a checksum error, verify that the parity settings on the master and the Arduino match. Adjust either the modpoll command (e.g., using -p none for no parity)
or change the Arduino initialization:

Serial.begin(19200, SERIAL_8E1);

Inter-Frame Delay

The sketch uses an inter-frame delay of 5 ms. Adjust this value if necessary to ensure complete frame reception.

Conclusion

This project demonstrates a flexible and modular approach to emulating a Modbus RTU slave device on an Arduino Nano. The code supports both digital I/O operations and analog input readings while also allowing messages to be bridged to a secondary port for further integration. Its modular design makes it easy to customize and extend for various applications.

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.

/**
* 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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment