从零吃透Modbus通信|第3章:STM32 Modbus RTU主机

前言

前两章我们搞定了Modbus基础原理、RTU完整帧结构、CRC16算法、正常/异常报文解析。很多同学学完理论还是不会写代码,核心问题只有一个:只懂协议,不懂工程落地逻辑

本章我们从零手写纯裸机Modbus RTU主机,不依赖任何第三方库、不移植源码、全程逐行编写、逐行讲解。

读完本章,你将掌握:

  • Modbus主机核心调度逻辑(轮询机制、一问一答)

  • 03/06/16三大核心功能码完整实现

  • 工业级超时判断、丢包重发、异常帧过滤

  • CRC校验、帧合法性校验全套逻辑

  • STM32裸机主机工程模板

1. Modbus主机核心逻辑(工程底层核心)

1.1 主机唯一规则:绝对被动轮询

再次强调Modbus最核心的底层规则,所有代码逻辑都围绕这一点展开:

总线永远只有一台主机,所有通信必须主机先发指令,从机被动应答,从机绝不主动发包。

主机工作流程固定死,没有任何例外:

  1. 组装Modbus请求帧(地址+功能码+寄存器地址+数量+CRC)

  2. 串口发送完整报文

  3. 开启超时计时,等待从机应答

  4. 收到数据后,校验帧合法性、CRC、异常码

  5. 合法数据解析存储,异常/超时直接丢弃、标记故障

  6. 间隔固定时间,发起下一轮轮询

1.2 新手最大误区(90%人踩坑)

  • 误区1:连续快速发指令,不等待应答 → 总线拥堵、数据错乱

  • 误区2:没有超时机制 → 从机掉线后程序永久卡死

  • 误区3:不校验CRC、不判断异常帧 → 干扰环境下数据乱跳

  • 误区4:帧长度写死 → 多寄存器读取解析失败

本章代码将彻底规避以上所有问题,完全对标工业设备标准。


2. 工程底层配置(统一标准)

2.1 硬件基础

  • MCU:STM32F103/F407 通用

  • 通信方式:USART串口 + MAX485收发模块

  • 波特率:9600/115200(工程最常用)

  • 数据位8、停止位1、无校验(Modbus RTU标准)

2.2 全局宏定义(统一参数,方便改配)

单独封装配置宏,后续适配不同设备只需改这几行:

复制代码
// Modbus主机全局配置
#define MODBUS_BAUDRATE    115200    // 波特率
#define MODBUS_TIMEOUT     100       // 超时时间ms
#define MODBUS_MAX_TX_LEN  20        // 最大发送帧长
#define MODBUS_MAX_RX_LEN  30        // 最大接收帧长
#define MODBUS_SLAVE_ADDR  0x01      // 默认从机地址

2.3 全局变量定义

复制代码
// 收发缓冲区
uint8_t modbus_tx_buf[MODBUS_MAX_TX_LEN];
uint8_t modbus_rx_buf[MODBUS_MAX_RX_LEN];

// 帧长度、超时标记、接收计数
uint8_t tx_len = 0;
uint8_t rx_len = 0;
uint8_t modbus_timeout_flag = 0;
uint32_t modbus_tick = 0;

// 解析出的寄存器数据(可存储16组寄存器)
uint16_t modbus_reg_data[16] = {0};

3. 前置基础函数(CRC+串口底层)

3.1 标准Modbus CRC16校验函数(复用前章)

固定多项式0xA001、初始值0xFFFF,完全适配RTU协议:

复制代码
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length)
{
    uint16_t crc = 0xFFFF;
    uint16_t i, j;
    for(i = 0; i < length; i++)
    {
        crc ^= data[i];
        for(j = 0; j < 8; j++)
        {
            if(crc & 0x0001)
            {
                crc >>= 1;
                crc ^= 0xA001;
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    return crc;
}

3.2 CRC追加函数(自动补全帧尾校验码)

自动计算CRC并按低字节在前、高字节在后的Modbus标准追加:

复制代码
void Modbus_Append_CRC(uint8_t *buf, uint8_t *len)
{
    uint16_t crc = Modbus_CRC16(buf, *len);
    buf[*len] = crc & 0xFF;        // CRC低字节
    buf[*len + 1] = (crc >> 8) & 0xFF;// CRC高字节
    *len += 2;
}

3.3 串口基础收发函数

基于STM32标准串口,极简封装,适配所有型号:

复制代码
// 串口发送指定长度数据
void USART_Send_Data(uint8_t *buf, uint8_t len)
{
    uint8_t i = 0;
    for(i = 0; i < len; i++)
    {
        USART_SendData(USART1, buf[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    }
    // 等待发送完成
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

// 串口中断接收(清空接收缓冲区、重置超时)
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_FLAG_RXNE) != RESET)
    {
        modbus_rx_buf[rx_len++] = USART_ReceiveData(USART1);
        modbus_tick = HAL_GetTick(); // 刷新计时,防止超时
        USART_ClearITPendingBit(USART1, USART_FLAG_RXNE);
    }
}

4. 三大核心功能码 主机发送函数(可直接商用)

我们实现工程最常用的三个功能码:03读寄存器、06写单寄存器、16写多寄存器,参数封装统一,调用极简。

4.1 03功能码:读取保持寄存器

参数:从机地址、起始寄存器地址、读取寄存器数量

复制代码
void Modbus_Master_Read_Reg(uint8_t slave_addr, uint16_t start_reg, uint16_t reg_num)
{
    tx_len = 0;
    // 1. 组装请求帧
    modbus_tx_buf[tx_len++] = slave_addr;                // 从机地址
    modbus_tx_buf[tx_len++] = 0x03;                     // 03功能码
    modbus_tx_buf[tx_len++] = (start_reg >> 8) & 0xFF;   // 起始地址高字节
    modbus_tx_buf[tx_len++] = start_reg & 0xFF;          // 起始地址低字节
    modbus_tx_buf[tx_len++] = (reg_num >> 8) & 0xFF;     // 数量高字节
    modbus_tx_buf[tx_len++] = reg_num & 0xFF;            // 数量低字节

    // 2. 追加CRC校验
    Modbus_Append_CRC(modbus_tx_buf, &tx_len);

    // 3. 清空接收缓冲区、重置超时
    rx_len = 0;
    modbus_timeout_flag = 0;
    modbus_tick = HAL_GetTick();

    // 4. 发送报文
    USART_Send_Data(modbus_tx_buf, tx_len);
}

4.2 06功能码:写入单个保持寄存器

复制代码
void Modbus_Master_Write_Single_Reg(uint8_t slave_addr, uint16_t reg_addr, uint16_t value)
{
    tx_len = 0;
    // 1. 组装请求帧
    modbus_tx_buf[tx_len++] = slave_addr;
    modbus_tx_buf[tx_len++] = 0x06;                     // 06功能码
    modbus_tx_buf[tx_len++] = (reg_addr >> 8) & 0xFF;
    modbus_tx_buf[tx_len++] = reg_addr & 0xFF;
    modbus_tx_buf[tx_len++] = (value >> 8) & 0xFF;      // 写入数据高字节
    modbus_tx_buf[tx_len++] = value & 0xFF;             // 写入数据低字节

    // 2. 追加CRC
    Modbus_Append_CRC(modbus_tx_buf, &tx_len);

    // 3. 重置接收状态
    rx_len = 0;
    modbus_timeout_flag = 0;
    modbus_tick = HAL_GetTick();

    // 4. 发送
    USART_Send_Data(modbus_tx_buf, tx_len);
}

4.3 16功能码:批量写入多个寄存器

复制代码
void Modbus_Master_Write_Multi_Reg(uint8_t slave_addr, uint16_t start_reg, uint16_t reg_num, uint16_t *data_buf)
{
    tx_len = 0;
    // 基础帧头
    modbus_tx_buf[tx_len++] = slave_addr;
    modbus_tx_buf[tx_len++] = 0x10;                     // 16功能码
    modbus_tx_buf[tx_len++] = (start_reg >> 8) & 0xFF;
    modbus_tx_buf[tx_len++] = start_reg & 0xFF;
    modbus_tx_buf[tx_len++] = (reg_num >> 8) & 0xFF;
    modbus_tx_buf[tx_len++] = reg_num & 0xFF;
    modbus_tx_buf[tx_len++] = reg_num * 2;              // 数据总字节数

    // 填充多组写入数据
    for(uint16_t i = 0; i < reg_num; i++)
    {
        modbus_tx_buf[tx_len++] = (data_buf[i] >> 8) & 0xFF;
        modbus_tx_buf[tx_len++] = data_buf[i] & 0xFF;
    }

    // 追加CRC、发送
    Modbus_Append_CRC(modbus_tx_buf, &tx_len);
    rx_len = 0;
    modbus_timeout_flag = 0;
    modbus_tick = HAL_GetTick();
    USART_Send_Data(modbus_tx_buf, tx_len);
}

5. 工业级帧解析+容错处理(核心精华)

这是区分玩具代码量产代码的关键!包含:超时判断、CRC校验、异常帧过滤、地址校验、数据解析全套逻辑。

复制代码
// 返回值:0=失败 1=成功 2=从机异常
uint8_t Modbus_Master_Parse_Frame(uint16_t reg_num)
{
    uint16_t crc_calc, crc_rx;
    uint16_t i;

    // 1. 超时判断(无应答直接返回失败)
    if(HAL_GetTick() - modbus_tick > MODBUS_TIMEOUT)
    {
        modbus_timeout_flag = 1;
        rx_len = 0;
        return 0;
    }

    // 2. 帧长度合法性校验(最小帧长5字节)
    if(rx_len < 5) return 0;

    // 3. 从机地址校验
    if(modbus_rx_buf[0] != MODBUS_SLAVE_ADDR) return 0;

    // 4. 判断是否为异常响应帧
    if(modbus_rx_buf[1] & 0x80)
    {
        rx_len = 0;
        return 2; // 返回从机异常码
    }

    // 5. 仅处理03功能码应答
    if(modbus_rx_buf[1] != 0x03) return 0;

    // 6. 数据字节数校验
    if(modbus_rx_buf[2] != reg_num * 2) return 0;

    // 7. CRC校验(核心防干扰)
    crc_calc = Modbus_CRC16(modbus_rx_buf, rx_len - 2);
    crc_rx = (uint16_t)modbus_rx_buf[rx_len - 2] | ((uint16_t)modbus_rx_buf[rx_len - 1] << 8);
    if(crc_calc != crc_rx) return 0;

    // 8. 解析寄存器数据(大端模式转换)
    for(i = 0; i < reg_num; i++)
    {
        modbus_reg_data[i] = ((uint16_t)modbus_rx_buf[3 + i*2] << 8) | modbus_rx_buf[4 + i*2];
    }

    // 清空接收缓存,准备下一轮轮询
    rx_len = 0;
    return 1;
}

6. 主循环轮询调度(完整运行逻辑)

采用100ms定时轮询,不阻塞程序、稳定不卡死,适配所有工控场景:

复制代码
int main(void)
{
    // 底层初始化
    HAL_Init();
    SystemClock_Config();
    USART1_Init();  // 串口初始化
    LED_Init();     // 状态灯初始化

    uint32_t poll_tick = 0;
    uint8_t res = 0;

    while(1)
    {
        // 100ms轮询一次Modbus
        if(HAL_GetTick() - poll_tick >= 100)
        {
            poll_tick = HAL_GetTick();

            // 读取从机0、1号寄存器(温度、电压)
            Modbus_Master_Read_Reg(MODBUS_SLAVE_ADDR, 0, 2);

            // 解析应答数据
            res = Modbus_Master_Parse_Frame(2);
            if(res == 1)
            {
                // 解析成功,数据有效
                printf("温度:%.2f ℃\r\n", modbus_reg_data[0] / 10.0f);
                printf("电压:%.2f V\r\n", modbus_reg_data[1] / 10.0f);
                LED_ON();
            }
            else if(res == 2)
            {
                printf("从机设备异常!\r\n");
                LED_BLINK();
            }
            else
            {
                printf("通信超时/数据错误!\r\n");
                LED_OFF();
            }
        }
    }
}

7. 工程级避坑指南(专治所有通信故障)

7.1 通信抖动、偶尔丢包

原因:轮询间隔过短、485总线干扰、无超时保护 解决方案:固定100ms轮询、开启超时过滤、严格CRC校验

7.2 从机返回异常码02

原因:寄存器地址越界、起始地址+读取数量超出设备寄存器范围 解决方案:对照设备手册核对寄存器地址(注意手册1起始/代码0起始偏移)

7.3 CRC校验一直失败

99%概率:CRC高低字节顺序写反!必须严格遵循低字节在前、高字节在后

7.4 程序偶尔卡死

原因:无超时机制,从机掉线后主机无限等待 解决方案:本章100ms超时判断,彻底杜绝卡死


8. 本章核心总结

本章我们从零手写了一套工业级Modbus RTU主机,彻底摆脱开源库依赖:

  • 掌握了主机核心轮询、一问一答调度逻辑

  • 实现03/06/16三大刚需功能码

  • 补齐超时、CRC、异常帧、地址校验全套容错

相关推荐
斑马13926 分钟前
Linux软件编程学习笔记(四)——进程
linux·运维·服务器
云上工程笔记42 分钟前
海外 GPU 云主机有哪些选择?2026 年区域节点、卡型、网络和数据合规对比清单
网络
one3121 小时前
高精度MEMS电容式倾角传感器:原理、结构与Python三维可视化
开发语言·python
Brilliantwxx1 小时前
【算法从零到千】【59-65】链表专题
数据结构·链表
xixingzhe21 小时前
nginx无网络升级
服务器·nginx
程序员小八7771 小时前
Go 语言特性
开发语言·后端·golang
恒锐丰小瑞1 小时前
率能SS6285L 18V/5A/单通道直流电机驱动IC,超低导通电阻(58mΩ)与多重保护,用于电子锁/玩具/机器人
单片机·嵌入式硬件·机器人
学linux的QQ蛋2 小时前
Linux 文件 IO vs 标准 IO:缓冲区、目录操作、常用函数汇总
linux·运维·服务器
比兔代理2 小时前
静态住宅IP在品牌保护中的角色:稳定出口与长期监测
服务器·网络·ip