Collecting Sensor Data over RS‑485 Modbus with STM32
1. Hardware Overview
- Sensor: 12 VDC industrial sensor with RS‑485 interface, Modbus RTU protocol
- Interface design:
- RS‑485 is half‑duplex; the transceiver must switch between transmit and recieve
- Direction control can be automated with a hardware auto‑TX enable circuit triggered by the UART TXD start bit
- Jumpers W1/W2 select RS‑485 mode or plain UART mode
- Transceiver enable pins (RE/DE) must be driven correctly; in auto mode, harwdare asserts DE during a start bit and returns to RX after the last stop bit
- Sensor wiring: A→A, B→B
- Power switching: add a MOSFET/transistor switch for the sensor’s 12 V supply to reduce system power when idle
2. RS‑485 Link Bring‑Up
2.1 UART initialization (STM32 HAL)
// Globals
UART_HandleTypeDef huart3;
static volatile uint8_t usart3_rx_byte;
void USART3_Init_9600(void)
{
huart3.Instance = USART3;
huart3.Init.BaudRate = 9600;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
huart3.Init.Mode = UART_MODE_TX_RX;
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart3.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart3) != HAL_OK) {
Error_Handler();
}
// Prime the first byte reception in interrupt mode
HAL_UART_Receive_IT(&huart3, (uint8_t *)&usart3_rx_byte, 1);
}
2.2 Byte buffering in the RX interrupt
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3) {
rb_put(&rx_fifo, usart3_rx_byte); // store received byte
HAL_UART_Receive_IT(&huart3, (uint8_t *)&usart3_rx_byte, 1);
uart3_rx_flag = 1;
}
}
2.3 Minimal ring buffer
#define RX_BUF_CAPACITY 64
typedef struct {
uint16_t head; // write index
uint16_t tail; // read index
uint16_t size; // capacity
uint8_t data[RX_BUF_CAPACITY];
} rb_t;
static rb_t rx_fifo = {0, 0, RX_BUF_CAPACITY, {0}};
static inline uint16_t rb_count(const rb_t *b)
{
return (b->head - b->tail) % b->size;
}
static inline uint8_t rb_put(rb_t *b, uint8_t v)
{
uint16_t next = (b->head + 1) % b->size;
if (next == b->tail) return 0; // full
b->data[b->head] = v;
b->head = next;
return 1;
}
static inline uint8_t rb_get(rb_t *b, uint8_t *out)
{
if (b->tail == b->head) return 0; // empty
*out = b->data[b->tail];
b->tail = (b->tail + 1) % b->size;
return 1;
}
static inline void rb_clear(rb_t *b)
{
b->head = b->tail = 0;
}
2.4 Simple link test
static void send_link_test(void)
{
uint8_t pattern[10];
for (uint8_t i = 0; i < sizeof(pattern); ++i) pattern[i] = (uint8_t)(0x30 + i);
HAL_UART_Transmit(&huart3, pattern, sizeof(pattern), 0xFFFF);
HAL_Delay(100);
}
2.5 Common bring‑up issues
- Verify logic activity on MCU TXD and on the RS‑485 bus lines (A/B)
- If nothing is observed on A/B, confirm the transceiver’s DE/RE control; a stuck‑enabled driver will block reception on the bus
- If the automatic direction circuit holds the transceiver enabled continuously, add or fix the inverter/edge‑detection so DE only asserts during transmission
3. Modbus RTU Integration
3.1 Key points
- RTU framing uses 8N1 bytes; device addressing dsitinguishes nodes on a shared bus
- Frame boudnaries are inferred by idle time ≥ 3.5 character times; at 9600 bps this is ≈4 ms
- All frames include a CRC‑16 (polynomial 0xA001, little‑endian in the frame)
- Reading sensor data requires sending a request frame per the sensor’s register map and scaling rules
3.2 Power control for the sensor
void Sensor_PowerOn(void)
{
// Example: drive the sensor 12 V switch enable pin high
// HAL_GPIO_WritePin(SENSOR_EN_GPIO_Port, SENSOR_EN_Pin, GPIO_PIN_SET);
}
3.3 CRC‑16 (Modbus) helper
uint16_t modbus_crc16(const uint8_t *buf, uint16_t len)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; ++i) {
crc ^= buf[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc; // LSB is transmitted first
}
3.4 Build and send a Read Holding Registers request
static uint16_t build_read_holding(uint8_t addr, uint16_t start_reg, uint16_t qty, uint8_t *out)
{
out[0] = addr; // slave address
out[1] = 0x03; // function: Read Holding Registers
out[2] = (uint8_t)(start_reg >> 8);
out[3] = (uint8_t)(start_reg & 0xFF);
out[4] = (uint8_t)(qty >> 8);
out[5] = (uint8_t)(qty & 0xFF);
uint16_t crc = modbus_crc16(out, 6);
out[6] = (uint8_t)(crc & 0xFF);
out[7] = (uint8_t)(crc >> 8);
return 8;
}
void Modbus_SendReadRequest(uint8_t addr, uint16_t reg, uint16_t qty)
{
uint8_t frame[8];
uint16_t n = build_read_holding(addr, reg, qty, frame);
HAL_UART_Transmit(&huart3, frame, n, 0xFFFF);
}
Example: read one register at 0x0000 from device address 0x01.
// After UART init and sensor power on
Modbus_SendReadRequest(0x01, 0x0000, 1);
3.5 Frame boundary detection via inter‑char timeout
Configure a hardware timer (e.g., TIM5) for a one‑shot period of ≈4 ms. Restart it after every received byte. When it expires, treat the current buffer as one Modbus frame.
extern TIM_HandleTypeDef htim5; // configured ~4 ms period at 9600 bps
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3) {
rb_put(&rx_fifo, usart3_rx_byte);
HAL_UART_Receive_IT(&huart3, (uint8_t *)&usart3_rx_byte, 1);
HAL_TIM_Base_Stop_IT(&htim5);
__HAL_TIM_SET_COUNTER(&htim5, 0);
HAL_TIM_Base_Start_IT(&htim5); // restart 3.5 char timer
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim5) {
HAL_TIM_Base_Stop_IT(&htim5);
uint8_t frame[RX_BUF_CAPACITY];
uint16_t len = 0;
while (rb_get(&rx_fifo, &frame[len]) && len < sizeof(frame)) len++;
if (len > 0) {
Modbus_HandleFrame(frame, len);
}
rb_clear(&rx_fifo);
}
}
3.6 Parsing a typical sensor resposne
Assuming the sensor returns one 16‑bit register (big‑endian) representing depth in millimeters, scaled to meters by dividing by 1000.
static int Modbus_HandleFrame(const uint8_t *frame, uint16_t len)
{
if (len < 5) return 0; // minimal RTU frame length
// CRC check
uint16_t crc_calc = modbus_crc16(frame, len - 2);
uint16_t crc_recv = (uint16_t)frame[len - 2] | ((uint16_t)frame[len - 1] << 8);
if (crc_calc != crc_recv) return 0;
uint8_t addr = frame[0];
uint8_t func = frame[1];
if (func == 0x03) { // Read Holding Registers response
uint8_t byte_count = frame[2];
uint16_t expected = 3 + byte_count + 2; // addr, func, count, data..., crc
if (len != expected) return 0;
// Example: parse first register as depth
if (byte_count >= 2) {
uint16_t raw = ((uint16_t)frame[3] << 8) | frame[4];
double depth_m = (double)raw / 1000.0; // mm to m
(void)addr; // use as needed
// Application: store or forward depth_m
}
return 1;
}
// Handle other function codes as needed
return 0;
}
4. Notes
- RS‑485 wiring is polarity‑sensitive: A→A and B→B
- For long cables, use shielded twisted pair and proper termination/biasing (e.g., 120 Ω termination at the bus ends)