十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零吃透Modbus通信|第4章:手写STM32 Modbus RTU从机

从零吃透Modbus通信|第4章:手写STM32 Modbus RTU从机 前言上一章我们写完了Modbus RTU 完整主机。主机负责主动发指令、轮询设备、读写数据。但一个完整的 Modbus 工程必须有从机传感器、下位机、电机控制器、采集板、IO板全部都是从机。很多人只会用别人写好的从机库但是不会自定义寄存器不会拦截读写指令不会报错、不会返回异常码一遇到地址越界、非法参数就直接死机本章带你从零手写一套完整 Modbus 从机框架。无库、极简、可量产、可任意扩展寄存器。读完本章你将彻底掌握Modbus 从机完整接收、校验、解析、应答流程标准寄存器分区架构只读/可写/状态/故障03读、06单写、16多写完整实现自动返回标准异常码01/02/03/04指令拦截可以禁止修改关键参数、限制参数范围一主多从组网稳定运行1. 从机核心逻辑和主机完全相反1.1 从机唯一工作模式从机永远被动绝不主动发包。从机一辈子只做三件事监听总线等待主机指令校验帧、解析功能码、判断地址合法性合法则应答数据非法则返回异常帧1.2 从机必须具备的 4 层保护地址校验不是本机地址直接忽略CRC校验干扰错帧直接丢弃地址越界保护读超出寄存器范围 → 02异常数据合法性保护写入非法值 → 03异常这四层保护就是工业设备不死机、不乱跑参数的核心。2. 工程架构设计2.1 寄存器分区思想非常重要所有Modbus设备的寄存器一定要分区不能一堆数组乱写。我们分为 4 大区域只读数据区0~9温度、电压、电流、转速、状态可写参数区10~19转速指令、PID参数、阈值、地址、波特率故障状态区20~24故障码、报警标志系统保留区禁止读写越界直接报错2.2 全局寄存器定义// Modbus 从机寄存器总量 #define REG_NUM_MAX 30 // 分区定义 #define REG_RD_ONLY_BEGIN 0 #define REG_RD_ONLY_END 9 #define REG_WR_BEGIN 10 #define REG_WR_END 19 #define REG_FAULT_BEGIN 20 #define REG_FAULT_END 24 // 全局寄存器数组 uint16_t slave_reg[REG_NUM_MAX] {0};后续所有读写、权限拦截、越界判断全部基于这套分区。3. 从机底层全局配置#define SLAVE_ADDR 0x01 // 本机从机地址 #define RX_BUF_MAX 32 #define TX_BUF_MAX 32 uint8_t rx_buf[RX_BUF_MAX]; uint8_t tx_buf[TX_BUF_MAX]; uint8_t rx_len 0;沿用前面标准CRC16、CRC追加函数不再重复粘贴。4. 串口接收中断从机唯一入口从机所有数据全部来自串口中断接收。收到一帧完整数据后立刻进入解析处理函数。void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_FLAG_RXNE) ! RESET) { if(rx_len RX_BUF_MAX) { rx_buf[rx_len] USART_ReceiveData(USART1); } // 帧接收完成RTU空闲中断 / 超时判定 // 工程最简稳定方式10ms无新数据判定一帧结束 Modbus_Slave_Process(); } }5. 核心从机帧处理总函数超级重点这是整个从机的大脑。完成地址校验 → CRC校验 → 功能码分发 → 越界判断 → 应答/异常返回。void Modbus_Slave_Process(void) { uint16_t crc_rx, crc_calc; uint8_t res_flag 0; // 1. 帧长度过滤 if(rx_len 4) { rx_len 0; return; } // 2. 本机地址匹配 if(rx_buf[0] ! SLAVE_ADDR) { rx_len 0; return; } // 3. CRC校验 crc_calc Modbus_CRC16(rx_buf, rx_len - 2); crc_rx (uint16_t)rx_buf[rx_len-2] | ((uint16_t)rx_buf[rx_len-1] 8); if(crc_calc ! crc_rx) { rx_len 0; return; } // 4. 根据功能码分发处理 switch(rx_buf[1]) { case 0x03: // 读保持寄存器 res_flag Slave_Read_Reg(); break; case 0x06: // 写单个寄存器 res_flag Slave_Write_Single_Reg(); break; case 0x10: // 写多个寄存器 res_flag Slave_Write_Multi_Reg(); break; default: // 不支持的功能码返回01异常 Slave_Return_Exception(0x01); break; } // 发送应答帧 if(res_flag) { USART_Send_Data(tx_buf, res_flag); } rx_len 0; }6. 三大功能码从机实现03 / 06 / 166.1 03功能码读寄存器最常用自动判断地址越界越界直接返回02异常码。uint8_t Slave_Read_Reg(void) { uint16_t start_addr; uint16_t reg_cnt; uint8_t tx_len 0; uint16_t i; // 解析起始地址、读取数量 start_addr (rx_buf[2] 8) | rx_buf[3]; reg_cnt (rx_buf[4] 8) | rx_buf[5]; // 地址越界判断 if(start_addr reg_cnt REG_NUM_MAX) { Slave_Return_Exception(0x02); return 5; } // 组装应答帧头 tx_buf[tx_len] SLAVE_ADDR; tx_buf[tx_len] 0x03; tx_buf[tx_len] reg_cnt * 2; // 填充寄存器数据大端 for(i 0; i reg_cnt; i) { tx_buf[tx_len] (slave_reg[start_addr i] 8) 0xFF; tx_buf[tx_len] slave_reg[start_addr i] 0xFF; } // 追加CRC Modbus_Append_CRC(tx_buf, tx_len); return tx_len; }6.2 06功能码写单个寄存器增加权限拦截只读区禁止写入写入非法值返回03异常。uint8_t Slave_Write_Single_Reg(void) { uint16_t addr (rx_buf[2] 8) | rx_buf[3]; uint16_t val (rx_buf[4] 8) | rx_buf[5]; uint8_t tx_len 0; // 1. 地址越界 if(addr REG_NUM_MAX) { Slave_Return_Exception(0x02); return 5; } // 2. 只读区域禁止写入 if(addr REG_RD_ONLY_END) { Slave_Return_Exception(0x03); return 5; } // 3. 自定义数值范围拦截举例转速0~3000 if(addr 10 val 3000) { Slave_Return_Exception(0x03); return 5; } // 写入成功 slave_reg[addr] val; // 回显应答帧 tx_buf[tx_len] SLAVE_ADDR; tx_buf[tx_len] 0x06; tx_buf[tx_len] rx_buf[2]; tx_buf[tx_len] rx_buf[3]; tx_buf[tx_len] rx_buf[4]; tx_buf[tx_len] rx_buf[5]; Modbus_Append_CRC(tx_buf, tx_len); return tx_len; }6.3 16功能码批量写寄存器uint8_t Slave_Write_Multi_Reg(void) { uint16_t start_addr (rx_buf[2] 8) | rx_buf[3]; uint16_t reg_cnt (rx_buf[4] 8) | rx_buf[5]; uint8_t data_len rx_buf[6]; uint8_t tx_len 0; uint16_t i; // 地址越界 if(start_addr reg_cnt REG_NUM_MAX) { Slave_Return_Exception(0x02); return 5; } // 禁止写入只读区 if(start_addr REG_RD_ONLY_END) { Slave_Return_Exception(0x03); return 5; } // 批量写入 for(i 0; i reg_cnt; i) { slave_reg[start_addr i] (rx_buf[7 i*2] 8) | rx_buf[8 i*2]; } // 应答帧 tx_buf[tx_len] SLAVE_ADDR; tx_buf[tx_len] 0x10; tx_buf[tx_len] rx_buf[2]; tx_buf[tx_len] rx_buf[3]; tx_buf[tx_len] rx_buf[4]; tx_buf[tx_len] rx_buf[5]; Modbus_Append_CRC(tx_buf, tx_len); return tx_len; }7. 标准异常帧生成函数工程必备很多开源库不会自动报错导致调试极其困难。我们直接封装标准异常生成函数void Slave_Return_Exception(uint8_t err_code) { uint8_t tx_len 0; tx_buf[tx_len] SLAVE_ADDR; tx_buf[tx_len] rx_buf[1] | 0x80; // 异常功能码最高位置1 tx_buf[tx_len] err_code; Modbus_Append_CRC(tx_buf, tx_len); USART_Send_Data(tx_buf, tx_len); }持标准报错0x01非法功能码0x02非法地址/越界0x03非法数据值0x04设备故障8. 业务数据绑定让寄存器真正跑数据在主循环中把传感器、电压、转速、故障数据刷新到寄存器void Modbus_Data_Update(void) { // 模拟温度 25.5℃ slave_reg[0] 255; // 模拟电压 12.0V slave_reg[1] 120; // 模拟转速 slave_reg[2] motor_speed; // 故障码 slave_reg[20] fault_code; }从此以后上位机、主机读寄存器直接读到真实设备状态。9. 整机运行流程一主一从、一主多从9.1 完整通信链路主机 → 485总线 → 从机地址匹配 → CRC校验 → 功能码解析 → 权限判断 → 应答/报错9.2 多从机组网方法只需要修改SLAVE_ADDR设备10x01设备20x02设备30x03主机轮询不同地址即可实现一主多从工业组网。10. 本章总结你学到了什么本章彻底完成了Modbus RTU 从机完整架构掌握从机被动监听、帧解析、应答机制搭建了分区式寄存器架构量产标准实现 03/06/16 三大核心功能码实现地址越界、只读保护、数值拦截、标准异常报错可直接实现 485 多设备组网通信
返回列表