Protocol Overview
Modbus RTU is a widely adopted industrial serial communication standard that utilizes binary encoding for efficient data exchange. When paired with the RS485 physical layer, it creates a robust system ideal for noisy industrial environments, sensor networks, and PLC integrations.
RS485 employs differential signaling for noise immunity, operating in half-duplex mode where transmission and reception cannot occur simultaneously. It supports cable lengths up to 1200 meters and can accommodate up to 32 standard nodes (expandable to 256 with lower-load transceivers).
This guide details the implementation of a Modbus RTU node on an STM32 microcontroller (such as the F103 or F407 series), capable of operating as either a master or a slave. It covers reading/writing holding registers, reading input registers, and coil manipulation using the STM32 HAL library.
System Architecture
- Master Node: The STM32 initiates communications by sending queries (e.g., polling sensor data, sending actuator commands) and processes the slave replies.
- Slave Node: Peripheral devices that listen for queries addressed to them, executing the requested action and returning the appropriate response.
- RS485 Transceiver: ICs like the MAX485 or SP3485 bridge the STM32's TTL logic levels to the RS485 differential bus. The DE and RE pins manage the direction of data flow on the bus.
Hardware Design
Component Selection
| Component | Specification | Purpose |
|---|---|---|
| Microcontroller | STM32F103C8T6 | ARM Cortex-M3 core running at 72MHz |
| Bus Transceiver | SP3485 / MAX485 | 3.3V/5V compatible half-duplex RS485 driver |
| Isolation (Optional) | ADuM1201 | Galvanic isolation to prevent ground loops |
| Termination | 120Ω Resistor | Impedance matching to prevent signal reflection |
| Power Supply | 3.3V / 5V LDO | Powering the MCU and transceiver |
Wiring Diagram: STM32 to RS485 Transceiver
| Transceiver Pin | STM32 Pin | Description |
|---|---|---|
| RO | PA10 (USART1_RX) | Receiver Output (TTL level) |
| DI | PA9 (USART1_TX) | Driver Input (TTL level) |
| DE / RE | PB0 (GPIO) | Direction Control (High=TX, Low=RX) |
| VCC | 3.3V / 5V | Power Supply |
| GND | GND | Common Ground |
| A | Bus Line A | Differential signal A (connects to slave A) |
| B | Bus Line B | Differential signal B (connects to slave B) |
Bus Connection Note: All 'A' pins must be wired together, and all 'B' pins wired together. A 120Ω termination resistor should be placed at both extreme ends of the bus. Reversing A and B lines will result in communication failure. For long-distance runs, shielded twisted pair cabling is highly recommended.
Software Implementation
Development Setup
- IDE: STM32CubeIDE
- Framework: STM32 HAL Library
- Protocol: Modbus RTU (Function Codes: 0x03 Read Holding Registers, 0x06 Write Single Register, 0x10 Write Multiple Registers)
Core Concepts
Modbus RTU Frame Format
[Device Address (1B)][Function Code (1B)][Payload (NB)][CRC16 Checksum (2B)]
- Device Address: Target slave identifier (1-247, 0 is reserved for broadcast).
- Function Code: Defines the requested operation (e.g., 0x03 for reading).
- Payload: Contains parameters like register offset and quantity, or the data payload itself.
- CRC16: Cyclic Redundancy Check for error detection (Little-endian: LSB first, MSB second).
RS485 Direction Logic
- Transmit State: Set DE/RE pin HIGH. The UART shifts data out, and the transceiver converts TTL to a differential RS485 signal.
- Receive State: Set DE/RE pin LOW. The transceiver converts incoming differential signals to TTL, feeding the STM32's UART RX line.
Source Code
UART & RS485 Initialization
#include "stm32f1xx_hal.h"
extern UART_HandleTypeDef huart1;
UART_HandleTypeDef *mb_port = &huart1;
uint8_t mb_rx_fifo[256];
uint8_t mb_tx_fifo[256];
void MB_GPIO_Init(void) {
GPIO_InitTypeDef cfg = {0};
cfg.Pin = GPIO_PIN_0;
cfg.Mode = GPIO_MODE_OUTPUT_PP;
cfg.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &cfg);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // Default to Receive
}
void MB_UART_Init(uint32_t baud) {
huart1.Instance = USART1;
huart1.Init.BaudRate = baud;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
HAL_UART_Receive_IT(mb_port, mb_rx_fifo, 1);
}
CRC16 Calculation
Modbus RTU utilizes the CRC16-IBM algorithm using the polynomial 0xA001:
uint16_t Compute_CRC16(const uint8_t *pMsg, uint16_t size) {
uint16_t checksum = 0xFFFF;
uint16_t byte_idx;
uint8_t bit_idx;
for (byte_idx = 0; byte_idx < size; byte_idx++) {
checksum ^= (uint16_t)pMsg[byte_idx];
for (bit_idx = 0; bit_idx < 8; bit_idx++) {
if (checksum & 1) {
checksum = (checksum >> 1) ^ 0xA001;
} else {
checksum >>= 1;
}
}
}
return checksum;
}
Master Node Logic
The master actively queries slaves by building and transmitting frames:
void MB_Master_Transmit(uint8_t target_id, uint8_t op_code, uint16_t start_reg, uint16_t quantity, const uint16_t *payload) {
uint16_t idx = 0;
mb_tx_fifo[idx++] = target_id;
mb_tx_fifo[idx++] = op_code;
if (op_code == 0x03) { // Read Holding Registers
mb_tx_fifo[idx++] = (start_reg >> 8) & 0xFF;
mb_tx_fifo[idx++] = start_reg & 0xFF;
mb_tx_fifo[idx++] = (quantity >> 8) & 0xFF;
mb_tx_fifo[idx++] = quantity & 0xFF;
} else if (op_code == 0x06) { // Write Single Register
mb_tx_fifo[idx++] = (start_reg >> 8) & 0xFF;
mb_tx_fifo[idx++] = start_reg & 0xFF;
mb_tx_fifo[idx++] = (payload[0] >> 8) & 0xFF;
mb_tx_fifo[idx++] = payload[0] & 0xFF;
}
uint16_t crc = Compute_CRC16(mb_tx_fifo, idx);
mb_tx_fifo[idx++] = crc & 0xFF; // CRC Low
mb_tx_fifo[idx++] = (crc >> 8) & 0xFF; // CRC High
// Switch to TX mode
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_UART_Transmit(mb_port, mb_tx_fifo, idx, 100);
// Wait for shift register to empty before switching to RX
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
}
uint8_t MB_Master_ParseResponse(uint8_t expected_id, uint16_t *out_data) {
// Blocking receive for demonstration
HAL_UART_Receive(mb_port, mb_rx_fifo, 256, 100);
if (mb_rx_fifo[0] != expected_id || mb_rx_fifo[1] != 0x03) return 0;
uint16_t frame_len = mb_rx_fifo[2] + 5; // Addr + FC + ByteCount + Data + CRC
uint16_t recv_crc = (mb_rx_fifo[frame_len-1] << 8) | mb_rx_fifo[frame_len-2];
if (recv_crc != Compute_CRC16(mb_rx_fifo, frame_len-2)) return 0;
uint8_t num_bytes = mb_rx_fifo[2];
for (uint8_t i = 0; i < num_bytes / 2; i++) {
out_data[i] = (mb_rx_fifo[3 + 2*i] << 8) | mb_rx_fifo[4 + 2*i];
}
return num_bytes / 2;
}
Slave Node Logic
The slave passively processes incoming requests and formulates replies:
uint16_t slave_register_map[10] = {0}; // Emulated holding registers
void MB_Slave_HandleFrame(uint8_t *incoming_data, uint16_t frame_size) {
uint8_t src_id = incoming_data[0];
uint8_t op_code = incoming_data[1];
uint16_t recv_crc = (incoming_data[frame_size-1] << 8) | incoming_data[frame_size-2];
if (recv_crc != Compute_CRC16(incoming_data, frame_size-2)) return;
uint16_t reply_idx = 0;
if (op_code == 0x03) { // Read Holding
uint16_t target_reg = (incoming_data[2] << 8) | incoming_data[3];
uint16_t reg_qty = (incoming_data[4] << 8) | incoming_data[5];
mb_tx_fifo[reply_idx++] = src_id;
mb_tx_fifo[reply_idx++] = op_code;
mb_tx_fifo[reply_idx++] = reg_qty * 2;
for (uint16_t i = 0; i < reg_qty; i++) {
mb_tx_fifo[reply_idx++] = (slave_register_map[target_reg + i] >> 8) & 0xFF;
mb_tx_fifo[reply_idx++] = slave_register_map[target_reg + i] & 0xFF;
}
} else if (op_code == 0x06) { // Write Single
uint16_t target_reg = (incoming_data[2] << 8) | incoming_data[3];
uint16_t val = (incoming_data[4] << 8) | incoming_data[5];
slave_register_map[target_reg] = val;
// Echo the request frame as the response
mb_tx_fifo[reply_idx++] = src_id;
mb_tx_fifo[reply_idx++] = op_code;
mb_tx_fifo[reply_idx++] = (target_reg >> 8) & 0xFF;
mb_tx_fifo[reply_idx++] = target_reg & 0xFF;
mb_tx_fifo[reply_idx++] = (val >> 8) & 0xFF;
mb_tx_fifo[reply_idx++] = val & 0xFF;
} else { // Exception: Illegal Function
mb_tx_fifo[reply_idx++] = src_id;
mb_tx_fifo[reply_idx++] = op_code | 0x80;
mb_tx_fifo[reply_idx++] = 0x01;
}
uint16_t crc = Compute_CRC16(mb_tx_fifo, reply_idx);
mb_tx_fifo[reply_idx++] = crc & 0xFF;
mb_tx_fifo[reply_idx++] = (crc >> 8) & 0xFF;
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_UART_Transmit(mb_port, mb_tx_fifo, reply_idx, 100);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
}
Main Application Loop
#include "main.h"
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MB_GPIO_Init();
MB_UART_Init(9600);
uint16_t sensor_vals[2];
while (1) {
// Master Operation: Read 2 registers from slave ID 1
MB_Master_Transmit(0x01, 0x03, 0x0000, 0x0002, NULL);
if (MB_Master_ParseResponse(0x01, sensor_vals) == 2) {
// Process acquired sensor values
}
HAL_Delay(1000);
}
}
Optimization and Validation
Configuration Parameters
- Baud Rate: Standard rates include 9600, 19200, and 38400. Both master and slave must match. Ensure the MCU clock configuration yields a low UART error percentage (ideally <2%).
- Data Format: 8-N-1 (8 data bits, No parity, 1 stop bit) is standard. For parity checks, 8-E-1 is also common.
- Node Addressing: Assign a unique ID (1-247) to each slave. The master addresses them individually or uses 0 for broadcast.
Reliability Enhancements
- Impedance Matching: Always install 120Ω termination resistors at the furthest endpoints of the trunk line to mitigate signal reflections.
- Isolation: In noisy environments, integrate digital isolators (e.g., ADuM1201) between the STM32 and the transceiver to eliminate ground loop currents.
- Timeout & Retry: Implement inter-frame delays (3.5 character times) and retry counters in the master logic to handle unresponsive slaves.
Testing Methodology
- Software Simulation: Validate the STM32 slave stack using PC utilities like Modbus Poll. For testing STM32 master logic, configure Modbus Slave on a PC.
- Hardware Verification: Attach an oscilloscope or logic analyzer to the A/B terminals to verify differential voltage levels (should be >200mV differential during active transmission) and confirm clean square waves free of ringing.