
前言前两章我们搞定了Modbus基础原理、RTU完整帧结构、CRC16算法、正常/异常报文解析。很多同学学完理论还是不会写代码核心问题只有一个只懂协议不懂工程落地逻辑。本章我们从零手写纯裸机Modbus RTU主机不依赖任何第三方库、不移植源码、全程逐行编写、逐行讲解。读完本章你将掌握Modbus主机核心调度逻辑轮询机制、一问一答03/06/16三大核心功能码完整实现工业级超时判断、丢包重发、异常帧过滤CRC校验、帧合法性校验全套逻辑STM32裸机主机工程模板1. Modbus主机核心逻辑工程底层核心1.1 主机唯一规则绝对被动轮询再次强调Modbus最核心的底层规则所有代码逻辑都围绕这一点展开总线永远只有一台主机所有通信必须主机先发指令从机被动应答从机绝不主动发包。主机工作流程固定死没有任何例外组装Modbus请求帧地址功能码寄存器地址数量CRC串口发送完整报文开启超时计时等待从机应答收到数据后校验帧合法性、CRC、异常码合法数据解析存储异常/超时直接丢弃、标记故障间隔固定时间发起下一轮轮询1.2 新手最大误区90%人踩坑误区1连续快速发指令不等待应答 → 总线拥堵、数据错乱误区2没有超时机制 → 从机掉线后程序永久卡死误区3不校验CRC、不判断异常帧 → 干扰环境下数据乱跳误区4帧长度写死 → 多寄存器读取解析失败本章代码将彻底规避以上所有问题完全对标工业设备标准。2. 工程底层配置统一标准2.1 硬件基础MCUSTM32F103/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、异常帧、地址校验全套容错