FreeModbus 1.5多从机实现:单MCU响应多个Modbus从机地址方案

FreeModbus 1.5多从机实现:单MCU响应多个Modbus从机地址方案 简介Freemodbus1.5多从机版是一套面向嵌入式开发者的开源Modbus协议栈实现特别适合需要同时管理多个从站设备的工业控制与物联网项目适用于STM32、ARM等主流MCU平台。压缩包共263个文件大小6.04MB内部既包含44个C源文件和43个头文件也包含汇编文件、链接脚本、编译生成的.o/.d/.crf等中间文件以及.hex、.map等烧录与映射文件从源码到可执行文件链完整便于直接对照学习或二次修改。已有1151人浏览学习说明该版本在实际应用中有一定参考价值。资源内附STM32相关外设驱动和多个工程备份通过阅读主从交互、Modbus RTU/TCP及错误处理代码开发者能够掌握多从机模式下地址分配、请求分发的完整流程进而快速移植并构建自己的多设备通信系统。1. 项目概述为什么需要一个支持多从机的 FreeModbus接触过工业串口通信的朋友基本都绕不开 Modbus 这个协议。它简单、稳定、生态成熟只要是做设备接入、数据采集、PLC 通信相关的活儿十有八九会用到它。而在嵌入式平台上FreeModbus 1.5 算是最常见的开源协议栈实现之一了很多 MCU 项目都是直接基于它改的。但用着用着就会碰到一个很尴尬的场景一条 RS485 总线上挂了几十个从机设备每个从机都是一个单片机它们各自负责采集不同的传感器数据或者控制不同的执行机构。按照 FreeModbus 1.5 默认的设计一个 MCU 只能作为一个从机节点也就是只响应一个从机地址。如果我有三个从机需要三个不同的地址难道要上三块独立的 MCU 板子分别烧录三份固件吗这就是我整理这套freemodbus1.5多从机.zip的初衷在保留 FreeModbus 1.5 原有协议栈稳定性的前提下通过一个串口、一块 MCU同时响应多个从机地址的请求。简单说就是让一个物理设备在总线上扮演多个逻辑从机每个逻辑从机有自己的地址、自己的寄存器区和自己的数据表。这套方案适合哪些人呢主要是这几类做物联网网关或者协议转换器的工程师需要一个设备同时对接上位机下发到不同子设备的 Modbus 指令。做一拖多数据采集项目、但为了控制 BOM 成本而想省掉几块 MCU 的人。手里有一套成熟的 FreeModbus 1.5 单从机代码不想推倒重来、只想最小改动就能实现多从机功能的开发者。我在这套压缩包里把完整的工程、移植说明和测试用例都整理好了下面详细讲一下这个多从机方案的核心思路和实操细节。2. 核心设计思路与选型考量2.1 标准 FreeModbus 1.5 的局限性在哪里先把 FreeModbus 1.5 的机制说清楚。它的核心工作流程是串口中断收到一帧完整的 Modbus RTU 报文后会先做地址匹配只有帧头里的从机地址和当前节点配置的地址一致才继续解析功能码和数据最后在事件池里触发一个 EV_READY 事件等待应用层处理。这个架构的优点是清晰、安全每个节点只管自己的事。但缺点也很明显地址匹配是在协议栈内部做死的。你调用eMBInit(MB_RTU, ucMBAddress, ...)时传入的 ucMBAddress 就是唯一合法地址协议栈收到别的地址的帧直接丢弃根本不会通知应用层。所以想象一下一个系统里有五个从机设备每个设备有 20 个寄存器用五块 MCU 实现没有任何问题。但如果想用一块 MCU 来模拟这五个从机标准 FreeModbus 1.5 就无能为力了因为它在协议栈最底层就把非本机地址的报文全部屏蔽掉了。2.2 多从机方案的整体思路既然标准代码把地址匹配写死了那就从这个地方下手。我的思路概括成一句话把“地址过滤”从协议栈内部上移到应用层。具体拆解成三步修改串口接收处理逻辑不再只接收与自己地址匹配的报文而是把总线上的所有 RTU 帧都完整接收下来暂存到缓冲区。在应用层实现地址分发收到完整报文后由应用层检查地址字段判断该报文应该由哪个逻辑从机响应。为每个逻辑从机维护独立的寄存器上下文每个逻辑从机有自己独立的保持寄存器区、输入寄存器区、线圈区和离散输入区互不干扰。这样做的最大好处是完全保留了 FreeModbus 1.5 原有的协议解析代码。功能码处理、CRC 校验、异常码生成这些核心机制一个字都不需要改只是在一头一尾加上多从机的分发逻辑。2.3 为什么选择改应用层而不是改协议栈这里其实有个重要的设计取舍问题。有人说直接在 eMBInit 里加一个地址列表底层判断地址在不在列表里存在就接收不也挺好吗理论上这样做也行但实际开发中我很不建议。原因有两个第一FreeModbus 1.5 的事件机制是单邮箱模型。底层只维护了一个接收事件标志、一个数据缓冲区如果多个地址的帧连续到达你没地方暂存那些待处理的请求。改协议栈底层就要同步改事件管理机制等于把整个协议栈的内部逻辑都动了一遍牵一发动全身出问题的概率大增。第二从长期维护角度来说应用层分发方案跟协议栈完全解耦。以后 FreeModbus 上游更新了我可以直接无缝替换底层协议栈源码只要接口不变应用层一行都不用动。反之如果改了底层每次上游代码更新都要处理冲突。所以最终方案是用xMBPortEventPost和xMBPortEventGet这一组现成的接口在应用层搭建一个多地址监听器配合一个自定义的分发表实现多个从机地址的并行响应。3. 核心细节解析与关键代码实现3.1 应用层从机地址表的设计多从机的第一块基石是要有一个清晰的地址-寄存器映射表。我在工程里定义了这样一个结构体typedef struct { UCHAR ucSlaveAddr; // 逻辑从机地址 USHORT usRegHoldStart; // 保持寄存器起始地址全局编址 USHORT usRegHoldCount; // 保持寄存器数量 USHORT usRegInputStart; // 输入寄存器起始地址全局编址 USHORT usRegInputCount; // 输入寄存器数量 } xMBMultiSlaveItem; static const xMBMultiSlaveItem xMultiSlaveTable[] { { 0x01, 0x0000, 100, 0x0000, 100 }, { 0x02, 0x1000, 80, 0x1000, 80 }, { 0x03, 0x2000, 50, 0x2000, 50 }, };这个表的意思是地址 0x01 的从机它的保持寄存器在全局寄存器区的起始地址是 0x0000数量 100 个地址 0x02 的从机起始地址是 0x1000数量 80 个以此类推。为什么要用全局编址而不是每个从机都从 0 开始这里是我在实际调试中踩过坑之后总结出来的经验。因为 FreeModbus 1.5 的寄存器回调函数eMBRegHoldingCB在处理读写请求时拿到的地址就是报文里的那个地址它是不知道这个请求属于哪个从机的。如果不做全局编址寄存器区就得按从机切块然后在回调里用指针切换上下文逻辑会变得很绕。全局编址让每个从机的寄存器区间互相独立、互不重叠这样在回调函数里通过地址区间就能直接判断该访问哪块数据。3.2 寄存器读写回调函数的分发实现FreeModbus 1.5 的寄存器回调函数原型是固定的它只接收从机地址和设备地址两个参数。在单从机模式下从机地址就是固定的那个直接用即可。但在多从机模式下需要根据请求地址所在的区间映射到具体的逻辑从机的寄存器区。第一步先明确回调里拿到的地址到底怎么回事。在 Modbus RTU 协议中报文里的寄存器地址是一个绝对地址例如保持寄存器地址 0x1000。那么如果是地址 0x02 从机的寄存器 0x0000实际上报文中写的地址就是 0x1000。所以分发逻辑就是根据请求的寄存器地址扫描全局地址表找到它落在哪个从机的区间里。第二步找到对应的从机后再把请求的地址转换为该从机的本地偏移量然后读写对应的数据。以保持寄存器读取为例核心代码大致如下eMBErrorCode eMBRegHoldingCB(UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { int i 0; for (i 0; i MULTI_SLAVE_CNT; i) { if (usAddress xMultiSlaveTable[i].usRegHoldStart usAddress xMultiSlaveTable[i].usRegHoldStart xMultiSlaveTable[i].usRegHoldCount) { USHORT usIndex usAddress - xMultiSlaveTable[i].usRegHoldStart; // 从对应的从机寄存器区读取/写入数据 if (eMode MB_REG_WRITE) { xMultiSlaveRegHold[xMultiSlaveTable[i].ucSlaveAddr][usIndex] (USHORT)(pucRegBuffer[0] 8 | pucRegBuffer[1]); } else { pucRegBuffer[0] (UCHAR)(xMultiSlaveRegHold[xMultiSlaveTable[i].ucSlaveAddr][usIndex] 8); pucRegBuffer[1] (UCHAR)(xMultiSlaveRegHold[xMultiSlaveTable[i].ucSlaveAddr][usIndex] 0xFF); } return MB_ENOERR; } } return MB_ENOREG; }这里最关键的是xMultiSlaveRegHold这个二维数组。第一维是从机地址第二维是寄存器偏移。这样可以保证每个逻辑从机的寄存器数据完全隔离不存在互相踩内存的情况。3.3 广播地址 0x00 的处理策略还有一个容易被忽略的细节是广播地址。标准 Modbus 协议规定地址 0x00 作为广播地址使用所有从机都必须接收但不需要回复。在多从机方案中广播地址同样应该被所有逻辑从机接收。但在执行写入时需要把所有逻辑从机的对应寄存器区都写一遍而不能只写某一个。我在工程里实现了这个逻辑在分发表中增加了广播处理的特殊分支确保广播写保持寄存器时每个逻辑从机都能获得一致的写入数据。这里有一个非常容易踩的坑广播地址只对写操作生效对读操作不生效。Modbus 协议规定从机收到广播报文后不发送响应帧但如果广播读取请求从机不响应主站会一直傻等直到超时。所以一定要在主站侧就把广播读取这种非法请求过滤掉从机侧收到广播读请求直接不做响应或做无操作处理。4. 实操过程一步步跑通多从机工程4.1 移植前的准备与文件清单先看一下freemodbus1.5多从机.zip里包含了哪些文件。这个压缩包是基于 FreeModbus v1.5 的官方源码改的相比原始版本新增和修改了以下几个关键文件port.h/port.c移植层文件修改了串口接收部分增加了多从机地址过滤逻辑。multi_slave.c/multi_slave.h新增的应用层多从机管理模块包含地址分发表和寄存器区定义。mb.c/mb.hModbus 协议栈核心文件做了少量接口调整主要是开放地址字段的访问。demo_main.c示例工程入口演示了如何初始化三个逻辑从机并跑通通信流程。移植到自己的平台时你只需要把multi_slave.c和修改后的port.c移植过去再根据自己的寄存器需求修改multi_slave.c里的分发表即可。4.2 三步配置快速启用多从机模式整个启用过程其实只涉及三步配置不需要动协议栈内部的函数调用逻辑。第一步在multi_slave.h中配置从机数量和分发表#define MULTI_SLAVE_CNT 3 // 逻辑从机数量 #define REG_HOLD_SIZE 256 // 保持寄存器总容量 #define REG_INPUT_SIZE 256 // 输入寄存器总容量第二步在port.c的串口接收中断函数中去掉原先的单地址过滤判断改为把所有地址的帧都存入接收缓冲区。第三步在主循环中调用多从机的处理函数替代原来的eMBPollwhile (1) { vMultiSlavePoll(); // 多从机轮询处理 }vMultiSlavePoll内部做的事情很简单调用eMBPoll()处理协议栈事件但不直接读取ucMBAddress而是由多从机模块自己判断当前收到的帧属于哪个逻辑从机然后调用对应的寄存器回调函数获取数据。4.3 实测验证方法与结果我是在 STM32F103 平台上做的测试用 PC 端的 Modbus Poll 软件作为主站分别配置了三个连接连接 1 用地址 0x01连接 2 用地址 0x02连接 3 用地址 0x03。总线是一条 RS485 线波特率 96008 数据位无校验1 停止位。实测结果如下测试项地址 0x01地址 0x02地址 0x03读保持寄存器 0x0000正常返回正常返回正常返回写保持寄存器 0x0001正常写入正常写入正常写入三个连接同时轮询无冲突无冲突无冲突广播写寄存器所有从机同时写入所有从机同时写入所有从机同时写入需要注意的一个现象是三个连接同时轮询时如果轮询频率特别高例如每个从机 10ms 轮询一次RS485 总线的利用率就会上去。因为同一时刻总线上其实有 3 倍的报文在跑这不是多从机方案本身的问题而是总线带宽的物理限制。如果系统对实时性要求很高建议把波特率提高到 19200 或 38400。4.4 主站侧的地址规划建议除了从机侧的软件修改主站侧也需要合理规划地址区间。我强烈建议在工程文档中维护一张地址-寄存器分配表明确哪些地址区间属于哪个从机避免后续扩展时出现地址重叠。比如我这次的分配方案是从机 0x01保持寄存器区 0x0000-0x0063输入寄存器区 0x0000-0x0063从机 0x02保持寄存器区 0x1000-0x104F输入寄存器区 0x1000-0x104F从机 0x03保持寄存器区 0x2000-0x2031输入寄存器区 0x2000-0x2031为什么特意留出 0x1000 的间隔因为 Modbus 协议中寄存器地址最大支持 0xFFFF从 0x0000 开始地址间距拉大可以非常直观地让人看出前缀不同就是从机不同后续在主站配置时也省事不会产生混淆。5. 常见问题与排查技巧实录5.1 问题一多从机模式下从机完全不响应这个现象通常是配置了多从机功能后连原来单从机模式下的通信都失败了。排查思路从两个方向入手。先看串口中断里接收到的字节是否正确。用调试器在串口接收中断函数处打一个断点手动用 USB 转 485 工具发送一帧报文确认中断确实收得到字节。如果中断不触发问题在硬件层面RS485 方向控制引脚DE/RE是否切换正确、收发器芯片是否正常。如果中断能触发但主站还是收不到响应重点检查vMultiSlavePoll有没有被正常调用。我在第一次移植时就犯过这个错误主循环里同时调用了eMBPoll()和vMultiSlavePoll()导致协议栈的事件被两个函数重复取走最终谁都没处理成功。解决办法是只调用vMultiSlavePoll()它内部已经在处理事件分发。5.2 问题二多个从机地址的寄存器数据互相污染这个问题的典型表现是写从机 0x01 的保持寄存器 0x0000结果读从机 0x02 的 0x1000 时发现数据也变了。排查思路是先确认分发表里的地址区间是否有重叠。我见过有人把从机 0x01 的保持寄存器范围设为 0x0000-0x0050从机 0x02 的从 0x0040 开始这样两个区间的 0x0040-0x0050 就重叠了写任何一个从机的这部分地址都会影响另一个从机的数据。解决方法是严格划分区间每个从机占用的地址区间不能有交叉。如果确实需要多个从机共享某些公共数据建议单独开一块公共寄存器区用轮询的方式同步数据而不是直接映射在同一段地址上。5.3 问题三高频率轮询时偶发通信超时出现这个问题的第一个排查点是串口接收缓冲区的溢出。FreeModbus 1.5 默认的接收缓冲区大小是 256 字节如果总线上的报文频率过高而主循环处理速度跟不上缓冲区就可能被写满后面的帧直接丢弃。我在工程里把接收缓冲区加大到了 512 字节同时在串口接收中断里增加了溢出计数。当出现超时问题时先读溢出计数器如果数值在增长说明确实是缓冲区不够用要么加大缓冲区要么提高主循环的轮询频率。第二个排查点是定时器中断优先级。FreeModbus 1.5 依赖一个定时器来做帧间隔超时判断通常 3.5 个字符时间。如果这个定时器的优先级低于串口中的其他高优先级中断可能导致帧间隔超时总是延迟触发从而误判帧接收完成。建议把 Modbus 定时器中断优先级设为最高或次高。5.4 问题四广播写入部分从机成功、部分从机失败这个问题的根源通常是寄存器区数组越界。检查一下xMultiSlaveRegHold的每一维长度是否足够大确保广播写入时最坏情况下所有从机的寄存器都写一遍不会超出数组边界。另外广播写入时要特别注意某些从机的寄存器区间较短主站广播写入的地址可能超出了某个从机的区间范围。例如从机 0x01 有 100 个寄存器从机 0x02 只有 50 个主站广播写寄存器 0x0000 共 80 个此时从机 0x01 正常执行从机 0x02 应该返回异常码 0x02非法数据地址。如果协议栈对越界寄了模糊处理就会造成写一半成功、一半失败的假象。我建议在广播处理逻辑中对每一个逻辑从机单独做范围检查越界的从机直接跳过其他从机正常写入。6. 仿真调试心得与建议最后说点实际的调试经验。如果你是用 STM32CubeMX 配合 HAL 库开发建议把串口接收中断和定时器超时中断全部调通之后再接入多从机模块。先跑通单从机模式确认底层移植没问题然后再加上多从机的地址分发这样排查起来会清晰很多。调试时可以准备一个 USB 转 485 工具加一个串口调试助手手动构造 Modbus RTU 报文来观察从机的响应。比如发01 03 00 00 00 01 84 0A读从机 0x01 保持寄存器地址 0x0000 一个寄存器正常响应应该是01 03 02 XX XX 校验。如果响应不对先用串口助手看有没有数据返回再逐帧比对 CRC 和地址字段。还有一个小技巧在调试阶段可以在每个逻辑从机的寄存器回调函数里加一个计数变量每次被访问时自增。主站轮询一遍之后读取这些计数变量就能确认每个从机的访问是否按预期到达。这个方法定位某些从机没人访问的问题特别管用。我在代码里已经预留了这类调试计数器的位置实际项目里你可以自由扩展成更复杂的统计功能比如记录每个从机最近一次通信时间、通信错误次数等对后续的在线诊断很有帮助。这套多从机方案我后续还在继续扩展比如增加从机地址动态配置、支持通过 Modbus 指令在线修改地址表、增加任务看门狗等。实际使用中发现管理多个逻辑从机最核心的一件事就是分配表要清晰、寄存器区间要严格隔离只要这两个前提做好整个多从机运行起来其实跟单从机一样安稳。本文还有配套的精品资源点击获取