Modbus (RTU / TCP)¶
Overview¶
Modbus is a serial communication protocol for industrial devices, introduced by Modicon in 1979. This implementation supports both RTU (serial, binary framing with CRC-16) and TCP (Ethernet, MBAP header encapsulation) variants, with server-side PDU processing for building slave devices.
Status: Implemented
Full implementation with RTU frame build/parse, TCP frame build/parse, CRC-16 verification, coil/register access, and server-side PDU processing.
Protocol Specification¶
Modbus RTU Frame¶
┌──────────┬───────────────┬──────────────────────────┬──────────┐
│ Address │ Function Code │ Data │ CRC-16 │
│ (1 byte) │ (1 byte) │ (N bytes, varies) │ (2 bytes)│
└──────────┴───────────────┴──────────────────────────┴──────────┘
0x01-0xF7 0x01-0x7F Variable LSB first
Inter-frame gap: ≥ 3.5 character times (silent interval)
Max frame size: 256 bytes (address + PDU + CRC)
Modbus TCP Frame (MBAP Header + PDU)¶
┌─────────────────┬───────────────┬──────────┬──────────┬──────────────────┐
│ Transaction ID │ Protocol ID │ Length │ Unit ID │ PDU │
│ (2 bytes) │ (2 bytes) │ (2 bytes)│ (1 byte) │ (FC + Data) │
└─────────────────┴───────────────┴──────────┴──────────┴──────────────────┘
Echoed back Always 0x0000 N+1 1-247 Function + data
No CRC in TCP reliability handled by TCP layer
Transaction ID allows matching requests to responses
Function Codes¶
| Code | Name | Access | Description |
|---|---|---|---|
| 0x01 | Read Coils | Read | Digital outputs (1-bit, 1–2000) |
| 0x02 | Read Discrete Inputs | Read | Digital inputs (1-bit, 1–2000) |
| 0x03 | Read Holding Registers | Read | Analog outputs (16-bit, 1–125) |
| 0x04 | Read Input Registers | Read | Analog inputs (16-bit, 1–125) |
| 0x05 | Write Single Coil | Write | Set one coil ON/OFF |
| 0x06 | Write Single Register | Write | Set one holding register |
| 0x0F | Write Multiple Coils | Write | Set N coils (1–1968) |
| 0x10 | Write Multiple Registers | Write | Set N registers (1–123) |
Exception Response¶
Normal response: [Address] [FC] [Data...] [CRC]
Exception response: [Address] [FC|0x80] [Exception Code] [CRC]
| Exception Code | Name | Meaning |
|---|---|---|
| 0x01 | Illegal Function | Function code not supported |
| 0x02 | Illegal Data Address | Address out of range |
| 0x03 | Illegal Data Value | Value out of range |
| 0x04 | Server Device Failure | Internal device error |
| 0x05 | Acknowledge | Long operation in progress |
| 0x06 | Server Device Busy | Retry later |
API Reference¶
Data Structures¶
/* MBAP header for TCP */
typedef struct __attribute__((packed)) {
uint16_t transaction_id;
uint16_t protocol_id; /* Always 0x0000 */
uint16_t length; /* Remaining bytes */
uint8_t unit_id; /* Slave address */
} modbus_mbap_t;
/* Server register map */
typedef struct {
uint8_t coils[MODBUS_NUM_COILS / 8]; /* Bit-packed coils */
uint8_t discrete_inputs[MODBUS_NUM_INPUTS / 8]; /* Bit-packed inputs */
uint16_t holding_registers[MODBUS_NUM_HOLD_REGS];
uint16_t input_registers[MODBUS_NUM_INPUT_REGS];
uint8_t unit_id;
} modbus_register_map_t;
Constants¶
#define MODBUS_MAX_PDU 253 /* Max PDU size */
#define MODBUS_RTU_MAX_FRAME 256 /* Max RTU frame */
#define MODBUS_TCP_HEADER_SIZE 7 /* MBAP header */
#define MODBUS_MAX_READ_REGS 125 /* Max registers per read */
#define MODBUS_MAX_WRITE_REGS 123 /* Max registers per write */
#define MODBUS_NUM_COILS 1000
#define MODBUS_NUM_HOLD_REGS 1000
CRC-16 (Modbus RTU)¶
modbus_crc16¶
Compute CRC-16 with Modbus polynomial 0xA001 (bit-reversed 0x8005). Initial value: 0xFFFF.
modbus_crc16_verify¶
Verify CRC-16 on a received RTU frame. Compares computed CRC against the last 2 bytes.
RTU Frame Operations¶
modbus_rtu_build_read_regs¶
size_t modbus_rtu_build_read_regs(uint8_t *buf, uint8_t unit_id,
uint16_t start_addr, uint16_t quantity);
Build a Read Holding Registers request (FC 0x03). Returns 8-byte frame.
modbus_rtu_build_write_reg¶
Build a Write Single Register request (FC 0x06).
modbus_rtu_parse_read_regs¶
int modbus_rtu_parse_read_regs(const uint8_t *frame, size_t frame_len,
uint16_t *regs, int max_regs);
Parse a Read Holding Registers response. Validates CRC and extracts register values (host byte order).
Returns: Number of registers decoded, or -1 on error.
TCP Frame Operations¶
modbus_tcp_build_read_regs¶
size_t modbus_tcp_build_read_regs(uint8_t *buf, uint16_t transaction_id,
uint8_t unit_id, uint16_t start_addr,
uint16_t quantity);
Build a Modbus TCP Read Holding Registers request. Returns 12-byte frame (MBAP + PDU).
modbus_tcp_build_write_reg¶
size_t modbus_tcp_build_write_reg(uint8_t *buf, uint16_t transaction_id,
uint8_t unit_id, uint16_t addr, uint16_t value);
modbus_tcp_parse_read_regs¶
int modbus_tcp_parse_read_regs(const uint8_t *frame, size_t frame_len,
uint16_t *transaction_id, uint16_t *regs, int max_regs);
Coil/Register Helpers¶
bool modbus_get_coil(const modbus_register_map_t *map, uint16_t addr);
void modbus_set_coil(modbus_register_map_t *map, uint16_t addr, bool value);
bool modbus_get_input(const modbus_register_map_t *map, uint16_t addr);
Server-Side PDU Processing¶
modbus_process_pdu¶
int modbus_process_pdu(modbus_register_map_t *map,
const uint8_t *pdu_in, size_t pdu_in_len,
uint8_t *pdu_out, size_t *pdu_out_len);
Process a received Modbus PDU and generate a response. Handles FC 0x01, 0x03, 0x05, 0x06.
| Parameter | Type | Description |
|---|---|---|
map | modbus_register_map_t * | Server register map |
pdu_in | const uint8_t * | Received PDU (FC + data, no addr/CRC) |
pdu_out | uint8_t * | Response PDU buffer |
pdu_out_len | size_t * | Output: response length |
Returns: 0 on success, exception code on error (for building exception response).
Usage Examples¶
RTU Master: Read Registers¶
#include "modbus.h"
#include "uart.h"
#include <stdio.h>
int main(void)
{
uart_t serial;
uart_config_t cfg = uart_default_config();
cfg.baud_rate = 9600;
cfg.parity = UART_PARITY_NONE;
uart_open(&serial, "/dev/ttyUSB0", &cfg);
/* Build Read Holding Registers request (slave 1, addr 0, qty 10) */
uint8_t request[8];
size_t req_len = modbus_rtu_build_read_regs(request, 1, 0, 10);
/* Send request */
uart_write_all(&serial, request, req_len, 1000);
/* Wait for response */
uint8_t response[256];
ssize_t n = uart_read_timeout(&serial, response, sizeof(response), 500);
if (n <= 0) {
fprintf(stderr, "Timeout waiting for response\n");
uart_close(&serial);
return 1;
}
/* Parse response */
uint16_t regs[125];
int count = modbus_rtu_parse_read_regs(response, (size_t)n, regs, 125);
if (count < 0) {
fprintf(stderr, "Parse error (CRC or exception)\n");
} else {
printf("Read %d registers:\n", count);
for (int i = 0; i < count; i++) {
printf(" Reg[%d] = %u (0x%04x)\n", i, regs[i], regs[i]);
}
}
uart_close(&serial);
return 0;
}
TCP Client: Write Register¶
#include "modbus.h"
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(502),
};
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
/* Write value 1234 to register 0 on unit 1 */
uint8_t frame[12];
size_t len = modbus_tcp_build_write_reg(frame, 0x0001, 1, 0, 1234);
send(sock, frame, len, 0);
/* Read response */
uint8_t resp[256];
ssize_t n = recv(sock, resp, sizeof(resp), 0);
printf("Response: %zd bytes\n", n);
close(sock);
return 0;
}
Server/Slave: PDU Processing¶
#include "modbus.h"
#include <stdio.h>
int main(void)
{
/* Initialize register map */
modbus_register_map_t map;
memset(&map, 0, sizeof(map));
map.unit_id = 1;
/* Pre-fill some registers */
map.holding_registers[0] = 100;
map.holding_registers[1] = 200;
map.holding_registers[2] = 300;
modbus_set_coil(&map, 0, true);
modbus_set_coil(&map, 1, false);
/* Simulate received PDU: Read Holding Registers, addr=0, qty=3 */
uint8_t pdu_in[] = {0x03, 0x00, 0x00, 0x00, 0x03};
uint8_t pdu_out[256];
size_t pdu_out_len;
int rc = modbus_process_pdu(&map, pdu_in, sizeof(pdu_in),
pdu_out, &pdu_out_len);
if (rc == 0) {
printf("Response PDU (%zu bytes): ", pdu_out_len);
for (size_t i = 0; i < pdu_out_len; i++)
printf("%02x ", pdu_out[i]);
printf("\n");
/* Expected: 03 06 00 64 00 C8 01 2C */
/* FC BC R0=100 R1=200 R2=300 */
} else {
printf("Exception: 0x%02x\n", rc);
}
return 0;
}
CRC-16 Algorithm¶
Polynomial: 0xA001 (bit-reversed CRC-CCITT 0x8005)
Initial value: 0xFFFF
Input reflection: Yes
Result reflection: Yes
Algorithm (bit-by-bit):
1. crc = 0xFFFF
2. For each byte b:
a. crc ^= b
b. For 8 bits:
if (crc & 1): crc = (crc >> 1) ^ 0xA001
else: crc = crc >> 1
3. Append crc as [low byte][high byte] (little-endian)
Build & Run¶
Modbus Register Model¶
┌─────────────────────────────────────────────────────────────────┐
│ Register Type │ Address Range │ Access │ Function Codes │
├──────────────────────┼───────────────┼────────┼──────────────────┤
│ Coils (1-bit) │ 0x0000–0x03E7 │ R/W │ FC 01, 05, 15 │
│ Discrete Inputs │ 0x0000–0x03E7 │ R │ FC 02 │
│ Holding Registers │ 0x0000–0x03E7 │ R/W │ FC 03, 06, 16 │
│ Input Registers │ 0x0000–0x03E7 │ R │ FC 04 │
└──────────────────────┴───────────────┴────────┴──────────────────┘
Test Output¶
$ ./build/modbus_demo
[modbus] CRC-16 test: computed=0xB5C2, expected=0xB5C2 ✓
[modbus] RTU build: Read Regs (unit=1, addr=0, qty=10) → 8 bytes
[modbus] RTU parse: 10 registers decoded, CRC valid ✓
[modbus] TCP build: Write Reg (txn=1, unit=1, addr=0, val=1234) → 12 bytes
[modbus] PDU process: FC=0x03, response=8 bytes
[modbus] PDU process: FC=0x06, echo response ✓
[modbus] PDU process: FC=0x01, coil bitmap ✓
[modbus] Exception test: FC=0xFF → Illegal Function (0x01) ✓
[PASS] All modbus tests passed