Maker Pro
STM32 Nucleo

Building a Modbus RTU Client with STM32 and RS-485

October 15, 2026 by PCB Cool
Share
banner

This project demonstrates how to build a Modbus RTU client using an STM32 microcontroller and an RS-485 transceiver. The STM32 communicates with a Modbus server over a two-wire, half-duplex RS-485 bus to read two holding registers using Function Code 0x03.

Modbus RTU is widely used in industrial electronics to exchange data with temperature controllers, motor drives, energy meters, programmable logic controllers (PLCs), and other devices. Although sending bytes through a UART is relatively straightforward, building a reliable Modbus client requires careful handling of communication timing, RS-485 direction control, error checking, and register data.

An STM32 microcontroller can act as a Modbus RTU client, traditionally called a master, and request data from a remote Modbus server, traditionally called a slave. In this tutorial, we explore the hardware setup and communication principles required to read holding registers over a two-wire, half-duplex RS-485 connection.

The focus is on the design and implementation considerations rather than firmware code. Understanding these details helps when developing a reliable industrial communication interface.

Understanding Modbus RTU and RS-485

Modbus RTU and RS-485 perform different roles in the communication system.

  • Modbus defines the request-response protocol, including device addresses, function codes, register data, and error responses.
  • RTU defines the binary serial format used to transmit Modbus messages.
  • RS-485 provides the electrical interface for transmitting data over a differential signal pair.

A common arrangement uses an STM32 UART connected to an external RS-485 transceiver. The transceiver converts the UART's logic-level signals into differential signals suitable for the bus.

The client initiates a transaction, and the addressed server responds. All devices on the bus must use compatible serial settings, including baud rate, parity, and stop bits. On a two-wire half-duplex connection, only one device should drive the bus at a time.

For the example discussed here, the system uses an STM32, a 3.3 V RS-485 transceiver, and a Modbus server with address 0x11.

Hardware Required

The example setup uses:

  • An STM32 microcontroller with USART2
  • A 3.3 V half-duplex RS-485 transceiver
  • A Modbus RTU server or a USB-to-RS-485 adapter for testing
  • A twisted-pair communication cable
  • Suitable termination and biasing components, if required by the bus
  • A common reference conductor where required by the transceiver application and system design

A different STM32 board can be used, but the UART peripheral and GPIO assignments must be adapted to the selected hardware.

Using a 3.3 V transceiver helps keep the logic levels compatible with the STM32. Avoid using a 5 V-only transceiver if its receiver output can exceed the STM32 input pin's voltage rating.

Connecting the STM32 to the RS-485 Transceiver

The UART and transceiver connections form the core of the hardware interface.

For the example using USART2, make these connections:

  • STM32 USART2 TX to the transceiver DI pin
  • STM32 USART2 RX to the transceiver RO pin
  • One STM32 GPIO output to the transceiver DE and active-low /RE pins, with DE and /RE tied together
  • Transceiver A and B to the corresponding RS-485 bus connections

With DE and /RE tied together, a HIGH GPIO signal enables transmission, while a LOW signal places the transceiver in receive mode.

Initialize the direction-control GPIO LOW so the device starts in receive mode. Also provide a pull-up on the STM32 RX pin when needed. Some transceivers place RO in a high-impedance state during transmission; without a defined RX level, a floating input can cause framing errors or spurious bytes.

Check the transceiver datasheet and remote-device documentation before finalizing the connections. A/B labels are not consistent across all manufacturers, so verify the actual signal polarity rather than relying on the letters alone.

RS-485 bus layout

Several physical-layer details influence communication reliability:

  • Install 120 Ω termination resistors at the two physical ends of the cable, not at every node.
  • Use one fail-safe bias network per bus when required. Check whether the selected transceivers already provide receiver fail-safe behavior.
  • Avoid star wiring and long unterminated stubs.
  • Provide a common reference conductor when required by the transceiver application, or use appropriate isolation when ground potentials differ.

These considerations become particularly important as cable length, network size, or electrical noise increases.

Configure the UART for Modbus RTU

The example configuration uses:

  • Baud rate: 9600 bit/s
  • Serial format: 8E1 — eight data bits, even parity, one stop bit
  • UART peripheral: USART2
  • Remote server address: 0x11
  • Starting holding-register address: 0x0010
  • Number of registers requested: two

A critical STM32 HAL detail is that the UART word-length setting includes the parity bit. For eight data bits with even parity, configure a nine-bit word length with even parity enabled. Selecting an eight-bit word length while enabling parity can result in only seven data bits being transmitted and received.

All devices on the bus must agree on the serial settings. If the remote equipment uses a different configuration, the request may not be understood.

Understand the Modbus RTU Frame

A Modbus RTU frame contains four main fields:

  • Server address: one byte
  • Function code: one byte
  • Data: zero to 252 bytes
  • CRC-16: two bytes

The maximum RTU frame length is 256 bytes. The CRC is transmitted low byte first.

Individual server addresses range from 1 to 247. Address 0 is reserved for broadcast requests, which do not receive responses and therefore are not suitable for reading registers.

Reading holding registers

Function code 0x03 is used to read holding registers.

For example, to request two registers starting at protocol address 0x0010 from server 0x11, the request frame is:

11 03 00 10 00 02 C7 5E

Here, 0x03 identifies the Read Holding Registers function, 0x0010 is the starting address, and 0x0002 specifies the number of registers. The final two bytes contain the CRC, low byte first.

A successful response contains the server address, function code, byte count, register data, and CRC. Because each register is 16 bits, two registers produce four data bytes.

The bytes within each 16-bit register are transmitted most significant byte first. However, the word order used for 32-bit values and floating-point numbers spanning multiple registers depends on the device. Always consult the equipment's register map for byte order, word order, scaling, and signedness.

Register notation also requires attention. A register identified as 40017 in a device manual often corresponds to zero-based protocol address 16, or 0x0010, but conventions vary between manufacturers. Confirm the actual protocol address before sending requests.

Why RTU Timing Matters

Unlike protocols with a dedicated start-of-frame marker, Modbus RTU uses silent intervals to separate frames.

Two timing requirements are important:

  • t1.5: A gap longer than 1.5 character times within a frame makes that frame invalid.
  • t3.5: A silent interval of at least 3.5 character times separates consecutive frames.

At baud rates above 19,200 bit/s, the serial-line guide recommends fixed timing values of 750 µs for t1.5 and 1.75 ms for t3.5.

At 9600 bit/s using 8E1, a character contains 11 bits. Its transmission time is approximately 1.146 ms, giving a t1.5 interval of about 1.719 ms and a t3.5 interval of about 4.010 ms.

A fixed 1 ms delay or an arbitrary UART timeout is therefore not a reliable way to identify the end of every RTU frame.

A more suitable implementation restarts a hardware timer whenever a byte is received. Alternatively, some STM32 USART peripherals support a receiver-timeout feature through the RTOR register. On supported families, including G4, L4, and H7, this can be configured to detect an interval of approximately 39 bit times for an 11-bit character format.

The selected timing method should be verified against the STM32 reference manual, the HAL version, and the required Modbus serial-line behavior.

Controlling RS-485 Transmission

In a half-duplex system, the STM32 must switch the transceiver between transmit and receive modes at the correct time.

When sending a request, the controller sets DE HIGH, waits for any required driver-enable propagation delay, and transmits the UART data. After the complete frame has left the UART, it sets DE LOW to allow the remote server to respond.

The important distinction is between the UART transmit-data-register-empty flag and the transmission-complete flag.

The transmit-data-register-empty flag indicates that the UART can accept more data. It does not mean the final byte has finished transmitting. The transmission-complete flag indicates that the final stop bit has left the UART.

With manual direction control, DE must remain asserted until transmission is complete. Releasing it too early can truncate the frame or corrupt the CRC. Releasing it too late can interfere with the server's response.

Some STM32 USART peripherals support automatic RS-485 driver-enable control. Hardware support varies by family; several F0, F3, F7, G0, G4, L4, and H7 devices provide this capability, while the F1 and F4 USARTs do not. Check the specific microcontroller's documentation before deciding whether to use automatic or GPIO-based direction control.

Receiving and Validating a Response

After transmitting a request, the client must collect the response and confirm that it is valid before using the register values.

A robust receiver should:

  1. Collect incoming bytes using interrupt- or DMA-based reception.
  2. Detect the end of the frame using the t3.5 timing requirement.
  3. Reject receive-buffer overflow and handle UART errors.
  4. Validate the CRC before accepting the message.
  5. Check the server address and expected function code.
  6. Verify the frame length and byte-count field.
  7. Decode the register data only after all checks succeed.

The CRC-16/MODBUS algorithm uses the reflected polynomial 0xA001, an initial value of 0xFFFF, and no final XOR. It covers the address, function, and data bytes.

For verification, the standard CRC check value for the ASCII string 123456789 is 0x4B37. The example two-register request should produce CRC value 0x5EC7, transmitted as C7 5E.

A normal response to a function-03 request should contain exactly five overhead bytes plus two bytes for every requested register. For two registers, the total response length is nine bytes.

The parser must also recognize exception responses. A function-03 exception uses function code 0x83 and includes an exception code. The complete exception response is five bytes long.

Common exception codes include:

  • 0x01: Illegal function
  • 0x02: Illegal data address
  • 0x03: Illegal data value
  • 0x04: Server device failure

An exception response is a valid Modbus message reporting a device-level problem, not necessarily a communication failure. For example, retrying a request with an invalid register address will not fix the underlying issue.

Transaction Management and Timeouts

A Modbus client should manage each request as a transaction. A typical sequence is:

Idle → Wait for bus gap → Send request → Wait for reply → Validate response → Complete or retry

The client must ensure the bus has been idle for at least t3.5 before transmitting. After the request is sent, it starts a response timeout that accounts for server processing time and the time needed to transmit the response.

Three timing mechanisms should be treated separately:

  • Frame-gap timeout: Determines when a received RTU frame has ended.
  • Response timeout: Limits how long the client waits for the addressed server.
  • Retry policy: Limits the number of attempts when communication fails.

Do not overlap transactions on a two-wire half-duplex bus. If a response times out, wait for the bus to become idle before attempting another request.

Write operations require additional care. If a write request times out, the client may not know whether the server applied the command before the response was lost. Repeating a non-idempotent operation without understanding its effects can cause unintended behavior.

Conclusion

Building a reliable Modbus RTU client on STM32 involves more than configuring a UART and sending a request. RS-485 direction control, frame timing, CRC validation, response parsing, transaction management, and error recovery all affect the reliability of the communication link.

The hardware and protocol configuration described here provides a foundation for reading holding registers from a Modbus server using function code 0x03. A field-ready implementation also needs a complete receive path, bounded timeouts and retries, diagnostic counters, and testing on the exact STM32 and transceiver being used.

For additional implementation details and the complete firmware examples, refer to the PCBCool tutorial: Building a Modbus RTU Client on STM32 over RS-485.

Related Content

Comments


You May Also Like