STM32 上实现 Modbus-RTU

STM32 上实现 Modbus-RTU ,最稳妥的是"裸机驱动 + 状态机"(资源小、实时性强)。


一、方案选型(STM32 推荐)

方案 适用场景 评价
裸机状态机(推荐) 从站 / 简单主站 稳定、可控
FreeMODBUS 快速成型 但裁剪麻烦
libmodbus Linux / 高端 MCU ❌ 不适合小 RAM
自己写全套 学习 ⚠️ 易踩坑

工业现场:裸机 + 定时器 + UART 中断


二、硬件与串口配置

1、硬件连接(RS485)

复制代码
STM32 USART2
TX  ->  MAX485 DI
RX  ->  MAX485 RO
PB8 ->  MAX485 DE/RE

2、CubeMX 配置

参数 设置
Baudrate 9600 / 19200 / 115200
Word Length 8 Bits
Parity None
Stop Bits 1
Mode Asynchronous
USART Interrupt Enable

三、Modbus-RTU 核心规则

帧结构

复制代码
[地址][功能码][数据...][CRC低][CRC高]

帧结束判定(关键)

RTU 必须用 3.5 个字符静默时间

波特率 3.5字符时间
9600 ~4 ms
19200 ~2 ms
115200 ~0.35 ms

四、CRC16 校验(查表法 )

c 复制代码
static const uint16_t crc_tab[] = {
    0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241,
    // ...(完整表略)
};

uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    while (len--)
        crc = (crc >> 8) ^ crc_tab[(crc ^ *buf++) & 0xFF];
    return crc;
}

五、接收状态机

c 复制代码
typedef enum {
    RX_IDLE,
    RX_RECEIVING,
    RX_DONE
} RxState;

volatile RxState rx_state = RX_IDLE;
volatile uint8_t rx_buf[256];
volatile uint8_t rx_len = 0;
volatile uint16_t idle_cnt = 0;

UART 接收中断

c 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2)
    {
        rx_state = RX_RECEIVING;
        idle_cnt = 0;
        HAL_UART_Receive_IT(&huart2, (uint8_t *)&rx_buf[rx_len++], 1);
    }
}

1ms 定时器中断(帧结束判定)

c 复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (rx_state == RX_RECEIVING)
        {
            if (++idle_cnt >= 4)   // 3.5字符
            {
                rx_state = RX_DONE;
            }
        }
    }
}

六、Modbus 从站处理(功能码 03 / 06)

c 复制代码
uint16_t hold_reg[10] = {0};

void Modbus_Process(void)
{
    if (rx_state != RX_DONE) return;

    uint16_t crc_rx = (rx_buf[rx_len-1]<<8) | rx_buf[rx_len-2];
    if (Modbus_CRC16((uint8_t*)rx_buf, rx_len-2) != crc_rx)
    {
        rx_len = 0;
        rx_state = RX_IDLE;
        return;
    }

    if (rx_buf[0] != 0x01) goto exit;   // 地址不符

    switch (rx_buf[1])
    {
        case 0x03:  // 读保持寄存器
            Modbus_ReadHolding();
            break;
        case 0x06:  // 写单个寄存器
            Modbus_WriteSingle();
            break;
    }

exit:
    rx_len = 0;
    rx_state = RX_IDLE;
}

七、功能码 03 示例(读寄存器)

c 复制代码
void Modbus_ReadHolding(void)
{
    uint16_t addr = (rx_buf[2]<<8) | rx_buf[3];
    uint16_t cnt  = (rx_buf[4]<<8) | rx_buf[5];

    uint8_t tx[256];
    uint8_t i = 0;

    tx[i++] = 0x01;
    tx[i++] = 0x03;
    tx[i++] = cnt * 2;

    for (uint8_t j = 0; j < cnt; j++)
    {
        tx[i++] = hold_reg[addr + j] >> 8;
        tx[i++] = hold_reg[addr + j] & 0xFF;
    }

    uint16_t crc = Modbus_CRC16(tx, i);
    tx[i++] = crc & 0xFF;
    tx[i++] = crc >> 8;

    HAL_UART_Transmit(&huart2, tx, i, 100);
}

参考代码 stm32 实现Modbus-rtu www.youwenfan.com/contentcst/181948.html

八、调试与避坑指南

问题 原因
帧不完整 3.5字符时间不准
CRC 错 字节序反了
只收到一次 未重新开启接收中断
总线冲突 多从站地址重复
数据乱码 波特率不一致

调试工具

  • Modbus Poll
  • USB-RS485 转换器
  • 示波器看 3.5T 静默
相关推荐
qdprobot5 小时前
【无标题】
人工智能·单片机·嵌入式硬件·51单片机·硬件工程·iot·mixly
Hello:CodeWorld5 小时前
μC/OS vs FreeRTOS:嵌入式实时操作系统深度对比
c语言·开发语言·单片机
振南的单片机世界5 小时前
电机反电动势:断电瞬间的“高压反击”,续流二极管挡驾
单片机·嵌入式硬件
平凡灵感码头6 小时前
MCU 组成原理详解—— 从硬件框图透视微控制器的完整架构
单片机·嵌入式硬件·架构
山木嵌入式6 小时前
【STM32进阶】中断体系全解析:从核心原理到实战(含面试高频考点)
stm32·嵌入式硬件·面试·中断·nvic
puamac6 小时前
c#打开cmd然后输入claude
stm32·单片机·c#
搁浅小泽6 小时前
电子行业常用仪器设备介绍
嵌入式硬件·可靠性工程师
zd8451015006 小时前
[嘉立创EDA]导出BOM设置
嵌入式硬件
时空自由民.7 小时前
无刷电机反电动势和过零点介绍
单片机·嵌入式硬件