Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Collecting Sensor Data over RS‑485 Modbus with STM32

Tech 2

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)

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.