1. Today’s topic
Today we examine industrial serial transport:
UART
→ RS-485 transceiver
→ two-wire half-duplex bus
→ DE/~RE control
→ frame boundaries
→ CRC
→ Modbus RTU or a custom protocolThe central idea: RS-485 defines the electrical interface, not frames, addresses, or commands. A protocol such as Modbus RTU must separately determine who may transmit and when, where a frame ends, and what to do after an error.
2. Why this matters in your projects
RS-485 can connect remote IO modules, relay units, illumination controllers, a HIL bench, an industrial panel, or a PLC. For a traffic-light phase controller, a remote module may return:
register 0: current phase
register 1: raw input mask
register 2: conflict flags
register 3: glitch counter
register 4-5: supply voltage
register 6-7: sequenceDistinguish:
the device did not respond;
CRC is incorrect;
the frame is truncated;
the line is busy;
the response came from another address;
UART lost bytes;
DE switched off too early.3. Theory
3.3.1 3.1. UART and RS-485 are different layers
UART forms start/data/parity/stop bits. The RS-485 transceiver converts MCU logic levels to the differential A/B pair.
MCU UART_TX/RX/DE
→ RS-485 transceiver
→ A/B twisted pairDE and ~RE are often connected together:
0: transmitter disabled, receiver enabled
1: transmitter enabled, receiver disabledBut with this arrangement the node cannot hear its own transmission and detects collisions less effectively.
3.3.2 3.2. Half-duplex requires a bus owner
On a two-wire line, all devices share one pair for TX and RX. Only one node should transmit at a time. Typical model:
client/master creates a request
server/slave responds only to its own address3.3.3 3.3. DMA complete and UART Transmission Complete are different
DMA complete means DMA transferred the last byte into the UART data register/FIFO. UART may still be transmitting the final data byte, parity bit, and stop bit. Correct TX sequence:
1. DE = 1.
2. Wait for the transceiver to enable.
3. Start UART TX DMA.
4. Wait for DMA complete.
5. Wait for UART TC - Transmission Complete.
6. DE = 0.
7. Enter RX.3.3.4 3.4. Transmitter state machine
typedef enum {
RS485_STATE_RX_IDLE = 0,
RS485_STATE_WAIT_BUS_IDLE,
RS485_STATE_DE_ASSERT,
RS485_STATE_TX_DMA,
RS485_STATE_WAIT_UART_TC,
RS485_STATE_DE_RELEASE,
RS485_STATE_TURNAROUND,
RS485_STATE_WAIT_RESPONSE,
RS485_STATE_ERROR,
} rs485_state_t;Do not allow several tasks to call UART TX directly. One rs485_task or modbus_task should own UART, DE, RX framing, the TX queue, request timeout, and the current transaction. 3.3.5 3.5. Character time
For the 8E1 format:
1 start + 8 data + 1 parity + 1 stop = 11 bitsThe duration of one character:
Tchar = character_bits / baudrateExample at 9600 baud, 11 bits:
Tchar = 11 / 9600 ≈ 1.146 ms
t1.5 ≈ 1.719 ms
t3.5 ≈ 4.010 msHelper:
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
bool parity_enabled;
uint8_t stop_bits;
} serial_format_t;
static uint32_t serial_char_time_us(const serial_format_t *fmt)
{
uint32_t bits = 1U + fmt->data_bits +
(fmt->parity_enabled ? 1U : 0U) +
fmt->stop_bits;
return (uint32_t)(((uint64_t)bits * 1000000ULL +
fmt->baudrate - 1U) /
fmt->baudrate);
}3.3.6 3.6. Modbus RTU framing
Frame:
Address | Function | Data | CRC16Silence defines the boundaries:
before the frame: at least 3.5 character times of silence
inside the frame: a gap no longer than 1.5 character times
after the frame: at least 3.5 character times of silenceIf an in-frame gap exceeds t1.5, the frame is considered incomplete and must be discarded. Above 19200 bit/s, fixed values are often used:
t1.5 = 750 us
t3.5 = 1750 us3.3.7 3.7. Why UART IDLE alone is insufficient
UART IDLE often occurs after approximately one character time. Modbus requires t3.5. Therefore IDLE can be used only as an early signal:
UART IDLE
→ record DMA position
→ start one-shot timer t3.5
→ if no new bytes arrive before timeout
→ frame complete3.3.8 3.8. Modbus CRC16
uint16_t modbus_crc16(const uint8_t *data, size_t length)
{
uint16_t crc = 0xFFFFU;
for (size_t i = 0; i < length; i++) {
crc ^= data[i];
for (unsigned bit = 0; bit < 8; bit++) {
if ((crc & 1U) != 0U) {
crc = (uint16_t)((crc >> 1U) ^ 0xA001U);
} else {
crc >>= 1U;
}
}
}
return crc;
}Check CRC before changing application state.
3.3.9 3.9. Broadcast
A broadcast request must not produce a response. Otherwise every server responds at once and creates a collision.
3.3.10 3.10. ESP32 RS-485 mode
ESP-IDF can configure UART in half-duplex RS-485 mode, where the driver controls RTS/DE:
uart_set_pin(UART_NUM_1, TX_GPIO, RX_GPIO, RTS_GPIO, UART_PIN_NO_CHANGE);
uart_set_mode(UART_NUM_1, UART_MODE_RS485_HALF_DUPLEX);After this, other modules must not manually toggle the same DE GPIO.
4. Circuit-design constraints
RS-485 requires correct topology:
termination termination
│ │
────┴───────────┬──────────┬─────────────┴──── trunk
│ │
short stub short stubTermination should be placed only at both ends of the trunk, not at every module. Biasing/polarization should also be at one controlled point.
5. Common mistakes
1. Disabling DE at DMA complete.
2. Using vTaskDelay() to control DE.
3. Treating UART IDLE as the end of a Modbus frame.
4. Sharing UART between several tasks.
5. Not checking CRC before executing a command.
6. Responding to broadcast.
7. Terminating every node.
8. Placing bias on every board.
9. Swapping A/B.
10. Not accounting for Common.
11. Continuing the parser after RX overrun.
12. Using a low-accuracy software timeout.6. Practical assignment for 30-60 minutes
Create RS485_MODBUS_POLICY.md:
# RS-485 / Modbus policy
1. UART and DE have one owner task.
2. DE is released only after UART Transmission Complete.
3. DMA Complete is not treated as end of wire transmission.
4. RX framing follows t1.5 and t3.5 rules.
5. CRC is checked before application processing.
6. Broadcast requests never produce a reply.
7. RX loss moves parser to DESYNC.
8. Termination exists only at both ends of the trunk.
9. Biasing exists at no more than one designated location.
10. Every request has timeout, retry limit and diagnostics.Add a CRC test for this request:
01 03 00 00 00 0AThe complete frame should be:
01 03 00 00 00 0A C5 CDAdd diagnostics:
typedef struct {
uint32_t tx_frames;
uint32_t rx_frames;
uint32_t timeout;
uint32_t crc_error;
uint32_t interchar_error;
uint32_t rx_overflow;
uint32_t collision;
uint32_t wrong_address;
uint32_t wrong_function;
uint32_t retries;
uint32_t devices_offline;
uint32_t max_response_us;
uint32_t max_turnaround_us;
} modbus_diag_t;7. Further reading or experiments
- ESP-IDF UART/RS-485 documentation.
- STM32 HAL/LL UART DMA, UART TC, IDLE detection, and hardware DE.
- MODBUS over Serial Line Specification and Implementation Guide V1.02.
Brief recap
application request
→ modbus service queue
→ RTU encoder + CRC
→ RS-485 owner task
→ DE assert
→ UART DMA
→ UART Transmission Complete
→ DE release
→ RX DMA/ring
→ t1.5/t3.5 framer
→ CRC/address/function validation
→ immutable response eventExercise
DMA complete fires while the final stop bit is still leaving UART. Explain the failure if DE is released immediately and define the correct state transition.
Self-check criteria: Distinguish DMA and UART completion and specify a single DE owner; do not use an arbitrary task delay as proof.
Show the supplied answer
Releasing DE may truncate the transmitted frame. DMA completion means memory-to-UART transfer is finished, not wire transmission. Keep DE asserted until UART Transmission Complete, then release it and enter RX according to the transceiver timing contract.
Exercise
A UART IDLE event occurs before the Modbus t3.5 boundary. Define framing behavior if another byte arrives and if the silence reaches the required boundary.
Self-check criteria: Define both timing cases and the invalid-frame path; do not equate IDLE with protocol completion.
Show the supplied answer
Use IDLE as an early observation, track byte arrival and the framing timer, and declare a complete frame only after the required silence. Reject an incomplete frame when its internal timing violates the t1.5 rule; validate CRC/address/function before changing application state.