资讯动态

STM32F103上FreeModbus双协议栈RTU/TCP共用寄存器实战

发布时间:2026/10/15 22:33:13 来源:尧图企业网站定制
简介本资源是面向嵌入式开发工程师与STM32进阶学习者的FreeModbus协议栈移植工程聚焦于在STM32F103平台同时实现Modbus RTU串行与Modbus TCP以太网双模通信解决工业现场设备需兼容本地总线与远程网络接入的典型需求。压缩包共601个文件含110个C源文件如stm32f10x_tim.c、w5500_http.c、112个头文件h、110个编译中间文件o及调试/配置相关文件axf、uvprojx、bat等完整覆盖驱动层、协议栈适配、W5500以太网模块支持与Keil工程构建全流程包体大小为12.37MB。已有4956人学习下载说明其经过真实项目验证具备较高参考价值。用户可直接基于“主控板APP V007”工程运行双协议并发实例快速掌握FreeModbus在Cortex-M3平台的移植要点、RTU串口帧处理机制、TCP网络层对接逻辑以及STM32LwIPW5500的典型Modbus网关实现方案。1. STM32F103上跑通FreeModbus双协议栈不是“加个TCP就完事”而是RTU与TCP共用同一套寄存器映射、同一套回调逻辑、同一套状态机你手头那块STM32F103C8T6最小系统板UART接RS485模块能和PLC正常收发ModbusRTU指令但一加网口芯片ENC28J60或W5500TCP连上就报“非法功能码”或“响应超时”——问题往往不出在PHY驱动而在于FreeModbus的协议栈初始化顺序、从站地址绑定方式、以及最关键的RTU和TCP不能简单地“并行启动两套mb_t”。这个资源包不是把两个独立例程拼在一起的缝合怪它实现了单核MCU上FreeModbus的真正双模复用同一份eMBRegHoldingCB()回调函数同时服务串口请求和以太网请求同一组保持寄存器数组如usRegHoldingBuf[128]被RTU读写和TCP读写原子访问甚至同一套定时器中断如SysTick统一驱动两种协议的超时检测与帧间隔管理。适合正在做工业边缘网关、多协议数据采集终端、或需要现场串口调试远程以太网监控的嵌入式工程师。如果你的项目要求“现场用485查寄存器远程用网页改参数”这份移植就是绕不开的实操基线。2. FreeModbus双协议架构设计为什么必须重写portevent.c与portserial.c而不是直接套用官方demoFreeModbus官方源码默认只支持单一传输层RTU或ASCII或TCP三选一其核心限制在于eMBInit()函数内部硬编码了协议类型与端口初始化逻辑的强耦合。要让STM32F103同时跑RTU和TCP必须打破这种“单协议单实例”范式。关键改造点有三处事件分发中枢重构、串口驱动解耦、TCP连接状态同步。下面逐层拆解。2.1 事件模型重构从单事件队列到双通道事件分发器原始FreeModbus使用全局xMBPortEventPost()向一个xMBPortEventQueue发送事件如EV_READY、EV_FRAME_RECEIVED。双协议下RTU帧到达和TCP新连接建立必须被区分处理。我们改为定义两个独立事件句柄// mbport.h 新增 extern xQueueHandle xMBRTUEventQueue; extern xQueueHandle xMBTCPEventQueue; // mbportevent.c 实现 xQueueHandle xMBRTUEventQueue NULL; xQueueHandle xMBTCPEventQueue NULL; void vMBPortEventInit( void ) { // 创建两个独立队列长度均为5足够应对突发帧 xMBRTUEventQueue xQueueCreate( 5, sizeof( eMBEventType ) ); xMBTCPEventQueue xQueueCreate( 5, sizeof( eMBEventType ) ); }提示队列长度设为5是血泪经验——RTU在485总线噪声大时可能连续触发3次EV_FRAME_RECEIVEDTCP在并发连接时可能瞬间涌入2个EV_CONNECTION_ESTABLISHED5是平衡RAM占用与丢帧风险的临界值。2.2 串口驱动解耦UART外设复用与中断优先级隔离STM32F103通常只有一路USART1可用PA9/PA10但RTU需RS485方向控制DE引脚TCP需以太网芯片SPI通信。若共用同一USART中断TCP任务中调用xMBPortSerialPutByte()可能被RTU接收中断打断导致发送字节错乱。解决方案是物理分离软件同步硬件层面将USART1_TXPA9接RS485芯片DIUSART1_RXPA10接RODE引脚如PB12由GPIO控制软件层面RTU发送前置位DE发送完成后清零TCP任务绝不操作DE引脚仅通过SPI与W5500交互。// mbportserial.c 中 RTU 发送函数 BOOL xMBPortSerialPutByte( CHAR ucByte ) { // 关闭所有中断确保DE控制与UART发送原子性 __disable_irq(); GPIO_SetBits(GPIOB, GPIO_Pin_12); // DE1, 驱动总线 USART_SendData(USART1, ucByte); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); GPIO_ResetBits(GPIOB, GPIO_Pin_12); // DE0, 释放总线 __enable_irq(); return TRUE; }参数说明GPIO_Pin_12对应PB12需在RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB, ENABLE)后初始化为推挽输出USART_FLAG_TC是发送完成标志比TXE更可靠避免最后一字节未移出移位寄存器就被关闭DE。2.3 TCP连接状态同步共享从站地址与寄存器缓冲区FreeModbus TCP协议栈要求每个连接维护独立的xMBTCPServerHandle但寄存器数据必须全局一致。我们放弃官方eMBTCPInit()的独立实例模式改为单实例TCP服务器 多连接会话池// mbtcp.c 修改关键结构 #define MB_TCP_MAX_CONNECTIONS 4 typedef struct { uint8_t ucState; // CONN_STATE_IDLE / CONN_STATE_ESTABLISHED uint16_t usClientPort; uint32_t ulClientIP; } xMBTCPConnection_t; xMBTCPConnection_t xMBTCPConnPool[MB_TCP_MAX_CONNECTIONS]; uint8_t ucMBTCPActiveConnCount 0; // 在 xMBTCPStart() 中注册统一回调 eMBErrorCode eMBTCPStart( uint16_t usTCPPort ) { // 启动TCP监听后所有连接共享同一套寄存器回调 pvMBFunctionHandlerCur eMBFuncReadHoldingRegisterTCP; return MB_ENOERR; }逻辑说明pvMBFunctionHandlerCur指向TCP专用处理函数该函数内部调用eMBRegHoldingCB()——与RTU的eMBFuncReadHoldingRegisterRTU()调用的是同一个回调。这样无论请求来自串口还是网口对地址0x0000~0x007F的读写都操作同一片usRegHoldingBuf[]内存实现数据强一致性。3. STM32F103硬件适配关键点时钟配置、内存分配与中断抢占优先级实战配置STM32F103资源紧张64KB Flash / 20KB RAMFreeModbus双协议栈对时序和内存极其敏感。很多移植失败并非代码逻辑错误而是硬件抽象层HAL配置失当。以下三点必须手敲验证不能依赖CubeMX自动生成。3.1 SysTick时基精度必须锁定为1ms且禁止被其他中断延迟FreeModbus的超时检测如RTU字符间超时T3.5、TCP连接空闲超时全部依赖xMBPortTimersEnable()启动的SysTick。若SysTick被高优先级中断如以太网DMA中断阻塞超过1msRTU帧就会被误判为超时丢弃。// system_stm32f10x.c 中修改 SysTick 初始化 void SysTick_Configuration(void) { // 关键使用内核时钟HCLK/8 72MHz/8 9MHz而非APB时钟 // 这样即使APB1总线被低功耗模式拉低SysTick仍稳定 if (SysTick_Config(SystemCoreClock / 1000)) // 1ms中断 { while (1); // 配置失败死循环 } // 设置SysTick中断为最低优先级NVIC优先级组4下值为15 NVIC_SetPriority(SysTick_IRQn, 0xF0); }参数说明0xF0在优先级组44位抢占0位子优先级下表示抢占优先级15最低确保任何外设中断都能打断SysTick但SysTick绝不会打断外设中断——这是避免超时误判的铁律。3.2 RAM分配为FreeModbus预留连续内存块禁用malloc动态分配FreeModbus在mb.c中声明大量静态数组如ucMBFrame[256]若链接脚本未为其分配连续空间可能导致帧缓冲区跨页导致DMA异常。必须在STM32F103C8_FLASH.ld中显式划分/* 自定义内存段FreeModbus专用RAM */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K MB_RAM (xrw) : ORIGIN 0x20004000, LENGTH 4K /* 从0x20004000起留4KB */ } SECTIONS { .mb_ram (NOLOAD) : { *(.mb_ram) } MB_RAM }// mbport.h 中声明缓冲区 #define MB_BUFFER_SIZE 256 __attribute__((section(.mb_ram))) uint8_t ucMBFrame[MB_BUFFER_SIZE]; __attribute__((section(.mb_ram))) uint16_t usRegHoldingBuf[128];逻辑说明.mb_ram段强制所有FreeModbus全局缓冲区落在0x20004000~0x20004FFF连续地址避开标准堆栈区域0x20000000~0x20003FFF防止malloc()碎片化破坏帧缓冲区完整性。3.3 中断抢占优先级矩阵RTU UART vs TCP SPI vs SysTick 的生死排序中断源抢占优先级数值越小越高原因说明SysTick15最低仅用于超时计时绝不允许打断实时通信USART12RTU接收必须最高优先确保每个字节进缓冲区不丢SPI1W55003TCP数据收发需及时响应但可容忍微秒级延迟EXTI0ENC28J60中断1若用ENC28J60其INT引脚需最高优先级捕获数据到达事件// 中断初始化代码 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 4位抢占0位子优先 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel SPI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 3; NVIC_Init(NVIC_InitStructure);避坑提示若将SPI1优先级设为1高于USART1W5500接收中断可能打断RTU接收过程导致ucMBFrame数组被SPI DMA覆盖出现随机乱码。这是最隐蔽的“玄学”问题示波器都难抓。4. 双协议协同工作避坑指南5条真实翻车记录与后悔药FreeModbus双协议移植中80%的问题源于对协议栈状态机理解偏差或硬件时序误判。以下是我在某工业网关项目中踩过的5个深坑每一条都附带现象、根因与可立即执行的修复方案。4.1 现象RTU能正常读写TCP连接成功但所有请求返回0x04Slave Device Failure原因TCP协议栈未正确初始化xMBTCPFrame缓冲区。FreeModbus TCP要求xMBTCPFrame必须是256字节对齐的内存且首地址需为偶数。若malloc()分配的地址为奇数如0x20004001TCP解析时指针运算溢出。解决强制指定对齐属性__attribute__((aligned(256))) uint8_t xMBTCPFrame[256];4.2 现象TCP客户端连续发送10次读寄存器请求第7次开始返回0x02Illegal Data Address原因usRegHoldingBuf[]数组越界访问。FreeModbus TCP默认最大支持125个寄存器0x007D但RTU配置中usMBCurrentAddress被设为0x0080导致TCP请求地址0x0080时索引计算为负数。解决统一校验范围在eMBRegHoldingCB()开头添加if ( usAddress 128 || usNRegs 128 || (usAddress usNRegs) 128 ) { return MB_ENOREG; }4.3 现象RTU通信正常TCP偶尔收到重复帧同一请求被处理两次原因W5500的Sn_IR中断标志未及时清除。TCP接收中断服务程序中读取Sn_RX_RSR后未写1清Sn_IR[RECV]位导致中断持续触发。解决在SPI读取完RX缓冲区后强制写1清标志IINCHIP_WRITE(Sn_IR(0), Sn_IR_RECV); // Sn_IR_RECV 0x014.4 现象系统运行2小时后RTU通信卡死串口无数据输出原因xMBPortSerialPutByte()中while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET)死循环。当USART1时钟被意外关闭如低功耗模式进入TC标志永不置位。解决增加超时保护uint32_t ulTimeout 0xFFFF; while ((USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET) ulTimeout--) ; if (ulTimeout 0) { USART_Cmd(USART1, DISABLE); // 强制复位USART USART_Cmd(USART1, ENABLE); }4.5 现象TCP客户端断开连接后再次连接失败日志显示“no free connection slot”原因xMBTCPConnPool[]中连接状态未重置。客户端FIN包到达后W5500硬件自动关闭连接但FreeModbus未收到EV_CONNECTION_CLOSED事件ucState仍为CONN_STATE_ESTABLISHED。解决在TCP主循环中轮询连接状态for (uint8_t i 0; i MB_TCP_MAX_CONNECTIONS; i) { if (xMBTCPConnPool[i].ucState CONN_STATE_ESTABLISHED) { if (getSn_SR(0) SOCK_CLOSED) { // 检查W5500 socket状态 xMBTCPConnPool[i].ucState CONN_STATE_IDLE; ucMBTCPActiveConnCount--; } } }5. 调试与验证用Modbus Poll 逻辑分析仪交叉验证双协议时序一致性验证FreeModbus双协议是否真正“数据同源、时序可信”不能只靠上位机点灯。必须用硬件工具捕捉物理层信号并与应用层日志对齐。以下是我在某跨平台数据采集终端项目中固化下来的四步验证法。5.1 步骤一RTU物理层抓包 —— 确认帧结构与T3.5间隔使用Saleae Logic Pro 8逻辑分析仪探头接RS485 A/B线差分转单端需加电平转换芯片采样率设为1MS/s信号线探头位置关键观测点ARS485-A查看起始位下降沿时间戳BRS485-B与A反相确认差分信号完整性DEPB12方向控制观测DE高电平持续时间是否 ≥ T3.53.5字符时间// 示例9600bps下T3.5 3.5 * 10 * 1000 / 9600 ≈ 3.65ms // 逻辑分析仪截图应显示 // [DE1] ──────────────── (3.7ms) ──────────────── [DE0] // [UART TX] 0x01 0x03 0x00 0x00 0x00 0x01 0x84 0x0A技巧在xMBPortSerialPutByte()中GPIO_SetBits(GPIOB, GPIO_Pin_12)前插入GPIO_SetBits(GPIOA, GPIO_Pin_0)PA0接逻辑分析仪通道即可精确定位DE使能时刻。5.2 步骤二TCP网络层抓包 —— Wireshark过滤与Modbus解码在PC端运行Wireshark过滤条件设为tcp.port 502 ip.addr 192.168.1.100假设STM32 IP为192.168.1.100字段名正常值示例异常含义Transaction ID0x0001 ~ 0x00FF若重复出现相同ID说明TCP连接未正确释放Protocol ID0x0000非零值表明协议栈未识别为Modbus TCPLength0x0006表示后续6字节为Modbus PDU功能码数据Unit Identifier0x01必须与RTU从站地址一致否则上位机认为不同设备注意Wireshark需启用Modbus TCP协议解析Edit → Preferences → Protocols → Modbus TCP否则只显示原始TCP流。5.3 步骤三寄存器数据一致性验证 —— 双通道写入冲突测试编写Python脚本同时发起RTU和TCP写请求验证usRegHoldingBuf[0]是否被原子更新# modbus_test_consistency.py from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient import threading import time def rtu_write(): client ModbusSerialClient(methodrtu, port/dev/ttyUSB0, baudrate9600) for i in range(100): client.write_register(0, 0xAA55 i) time.sleep(0.01) def tcp_write(): client ModbusTcpClient(192.168.1.100) for i in range(100): client.write_register(0, 0x55AA i) time.sleep(0.01) # 并发执行 t1 threading.Thread(targetrtu_write) t2 threading.Thread(targettcp_write) t1.start(); t2.start() t1.join(); t2.join() # 最终读取验证 final_val client.read_holding_registers(0, 1).registers[0] print(fFinal register 0 value: 0x{final_val:04X}) # 应为0x55AA或0xAA55的最后一次写入值逻辑说明若最终值既非0x55AA系列也非0xAA55系列如0x0000说明usRegHoldingBuf[0]被两个线程非原子写入需在eMBRegHoldingCB()中加临界区保护__disable_irq(); usRegHoldingBuf[usAddress] usValue; __enable_irq();5.4 步骤四压力测试 —— 72小时双协议混合负载运行部署真实工业场景负载RTU侧每200ms读取10个寄存器0x0000~0x0009TCP侧每500ms写入1个寄存器0x000A每2s读取100个寄存器0x0000~0x0063监控项xMBRunRes返回值必须恒为TRUEucMBTCPActiveConnCount峰值≤4usRegHoldingBuf[0]与usRegHoldingBuf[1]的差值应恒为固定偏移血泪经验某次测试中第38小时xMBRunRes突变为FALSE排查发现是xMBTCPConnPool[]数组未初始化为0导致某个连接槽位ucState初始值为随机数被误判为活跃连接。从此我养成了所有全局结构体必须显式初始化的习惯——哪怕编译器说“默认0”也要亲手写xMBTCPConnPool[4] {0}。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑