Skip to content

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

uint16_t modbus_crc16(const void *data, size_t len);

Compute CRC-16 with Modbus polynomial 0xA001 (bit-reversed 0x8005). Initial value: 0xFFFF.

modbus_crc16_verify

bool modbus_crc16_verify(const uint8_t *frame, size_t frame_len);

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

size_t modbus_rtu_build_write_reg(uint8_t *buf, uint8_t unit_id,
                                  uint16_t addr, uint16_t value);

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

# Compile
gcc -Wall -Wextra -Werror -O3 -std=c11 -o modbus_demo modbus.c

# Run demo
./modbus_demo

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