从零吃透Modbus通信|第3章:STM32 Modbus RTU主机
2026/9/23 3:32:14 网站建设 项目流程

前言

前两章我们搞定了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、异常帧、地址校验全套容错

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询