☰
FreeMODBUS 1.5 多从机改造实战:RS-485 挂载 8 台设备的嵌入式方案
2026/10/17 16:31:02 网站建设 项目流程

简介:本资源是基于FreeMODBUS 1.5开源协议栈深度定制的多从机通信实现方案,面向嵌入式开发工程师及工业自动化系统集成人员,解决Modbus主站需并发管理多个RS485从设备的核心需求。压缩包共263个文件,涵盖44个C源码(如stm32f10x_tim.c、stm32f10x_rcc.c等底层驱动)、43个头文件(h)、36个编译中间文件(o/d/crf)及完整Keil工程文件(uvprojx、uvoptx、axf、hex、map等),总大小6.04MB,结构完整,可直接编译下载至STM32F1平台运行验证。已有1152人学习下载,适用于Modbus RTU多地址轮询、寄存器批量读写、异常响应处理等典型工控场景。读者可获得可运行的多从机主站参考工程、清晰的移植适配目录(含HAL/StdPeriph兼容层)、关键API调用示例及调试配置说明,显著降低FreeMODBUS在复杂现场设备组网中的集成门槛。

1. 一个串口接 8 个从机?FreeMODBUS 1.5 多从机不是“改个地址”就能跑通的黑匣子

某高校实验室做智能电表集抄系统,现场布了 7 台电表(Modbus RTU 从机)、1 台温湿度传感器、1 台断路器状态模块——全走 RS-485 总线,主控用 STM32F407。原计划直接套用官方 FreeMODBUS 1.5 示例,结果主机一发广播读命令,所有从机同时响应,总线上信号打架,示波器抓出来全是毛刺;换单播轮询,每台设备等 200ms 响应超时,一轮扫完要近 2 秒,远超上位机 500ms 心跳要求。最后发现:FreeMODBUS 官方包默认只支持单从机模式,mbportserial.c里xMBPortSerialPutByte()底层根本没做从机地址隔离,eMBPoll()的状态机也未区分多地址上下文。这不是功能缺失,是架构级设计假设——它默认你只有一个从机。本项目freemodbus1.5多从机.zip就是把这套“单机思维”的源码,硬生生掰成能稳定挂载 8 个不同地址从机的工业级轮询引擎。适合正在啃嵌入式 Modbus 集成、被“多从机冲突”卡住三天没出波形的固件工程师,也适合需要快速验证多节点协议栈健壮性的测试开发。


2. 架构重铸:从单实例到多上下文,FreeMODBUS 1.5 的多从机改造全景图

FreeMODBUS 1.5 原生设计是典型的单实例单从机模型:全局变量ucMBAddress存唯一从机地址,eMBPoll()循环只处理一个地址的请求/响应,pxMBFrameCBResponseCur()回调函数里连从机 ID 都不传。要支持多从机,必须打破三个强耦合点:地址绑定、状态机上下文、串口收发缓冲区。本项目不是打补丁,而是用 C 语言实现了一套轻量级多实例管理框架,在不修改mb.c核心协议解析逻辑的前提下,通过外层封装完成解耦。

2.1 多从机上下文结构体:每个从机一个独立“大脑”

核心改动在mbmulti.h和mbmulti.c中。不再依赖全局ucMBAddress,而是为每个从机分配独立上下文:

// mbmulti.h #define MB_MULTI_MAX_SLAVE 8 typedef struct { uint8_t ucSlaveAddr; // 该从机真实地址(1~247) eMBEventType eEvent; // 当前事件类型(EV_READY/EV_FRAME_RECEIVED等) uint8_t ucState; // 独立状态机状态(与原mb.c中eMBState对应) uint8_t ucRcvBuf[MB_FUNC_READ_INPUT_REGISTER_SIZE + 8]; // 接收缓冲区 uint16_t usRcvLength; // 实际接收长度 uint8_t ucSndBuf[256]; // 发送缓冲区(避免多从机共用导致覆盖) uint16_t usSndLength; // 待发送长度 uint32_t ulLastAccessTick; // 上次访问时间戳(用于超时判断) bool bIsEnabled; // 是否启用此从机 } xMBMultiSlaveCtx; extern xMBMultiSlaveCtx xMBMultiSlaveCtxTable[MB_MULTI_MAX_SLAVE];

提示:usRcvLength和usSndLength是关键安全字段。原版 FreeMODBUS 用全局usRcvBuffer+usRcvBufferPos,多从机并发时极易因中断嵌套导致usRcvBufferPos被多个从机实例同时修改,造成缓冲区越界。本项目为每个从机配独立缓冲区,彻底规避竞态。

2.2 主循环调度器:轮询策略可配,支持优先级与超时熔断

mbmulti.c中的eMBMultiPoll()替代了原eMBPoll(),它按配置策略遍历从机表:

// mbmulti.c eMBErrorCode eMBMultiPoll(void) { static uint8_t ucCurrSlaveIdx = 0; eMBErrorCode eStatus = MB_ENOERR; uint32_t ulCurrentTick = xMBPortGetTickCount(); // Step 1: 检查当前从机是否超时(防止单个从机宕机拖垮全局) if (xMBMultiSlaveCtxTable[ucCurrSlaveIdx].bIsEnabled && (ulCurrentTick - xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ulLastAccessTick) > MB_MULTI_TIMEOUT_MS) { // 触发超时事件,清空其状态机 xMBMultiSlaveCtxTable[ucCurrSlaveIdx].eEvent = EV_ERROR; xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ucState = STATE_DISABLED; } // Step 2: 执行当前从机的协议轮询(复用原mb.c逻辑,但传入上下文) eStatus = eMBPollForSlave(&xMBMultiSlaveCtxTable[ucCurrSlaveIdx]); // Step 3: 更新时间戳并切换到下一个从机(轮询策略) xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ulLastAccessTick = ulCurrentTick; ucCurrSlaveIdx = (ucCurrSlaveIdx + 1) % MB_MULTI_MAX_SLAVE; return eStatus; }

这里的关键是eMBPollForSlave()—— 它是原eMBPoll()的重构版,所有对ucMBAddress、ucState、usRcvBuffer的访问,全部改为通过xMBMultiSlaveCtx*参数传入。这样既保留了原协议栈 99% 的代码,又实现了上下文隔离。

2.3 串口驱动层改造:收发分离 + 地址过滤,杜绝总线冲突

原mbportserial.c中xMBPortSerialPutByte()直接往 UART 发送,不关心目标从机。多从机场景下,必须确保:
① 发送时只向当前轮询的从机发指令;
② 接收时只接收目标从机的响应(忽略其他从机的“误答”)。

本项目在mbportserial.c中新增地址过滤逻辑:

// mbportserial.c static uint8_t ucExpectedSlaveAddr = 0; // 当前期望响应的从机地址 bool xMBPortSerialPutByte(uint8_t ucByte) { // 发送前设置期望地址(在eMBMultiPoll中调用eMBMasterRequest()时设置) if (ucByte == 0 && ucExpectedSlaveAddr != 0) { // 这是帧头第一个字节(从机地址),记录下来 ucExpectedSlaveAddr = ucByte; } return xMBPortSerialPutByteOriginal(ucByte); // 调用原底层发送 } // 接收中断服务程序中增加过滤 void vMBPortSerialISR(void) { uint8_t ucByte; if (xMBPortSerialGetByte(&ucByte)) { // 关键过滤:只接收期望从机的响应帧 if (ucByte == ucExpectedSlaveAddr || ucByte == MB_ADDRESS_BROADCAST) { // 将字节存入当前从机的接收缓冲区 xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ucRcvBuf[xMBMultiSlaveCtxTable[ucCurrSlaveIdx].usRcvLength++] = ucByte; } // 其他从机的响应字节直接丢弃,不进任何缓冲区 } }

注意:广播地址(0)仍需接收,因为某些从机可能在广播写后返回异常响应。但广播响应不触发业务回调,仅用于链路层确认。

2.4 协议回调函数升级:从“全局回调”到“上下文感知回调”

原pxMBFrameCBResponseCur()函数签名是eMBException pxMBFrameCBResponseCur(uint8_t * pucFrame, uint16_t usLength),完全不知道这个帧属于哪个从机。本项目将其升级为:

// mbframe.h typedef eMBException (*pxMBFrameCBResponse)(uint8_t * pucFrame, uint16_t usLength, uint8_t ucSlaveIdx); // 在mbmulti.c中注册时传入从机索引 eMBErrorCode eMBMultiEnable(uint8_t ucSlaveIdx, uint8_t ucSlaveAddr) { xMBMultiSlaveCtxTable[ucSlaveIdx].ucSlaveAddr = ucSlaveAddr; xMBMultiSlaveCtxTable[ucSlaveIdx].bIsEnabled = true; // 注册带上下文的回调 vMBRegisterFrameCBResponse(ucSlaveIdx, pxMBFrameCBResponseForSlave); return MB_ENOERR; }

这样,当从机 3 返回读寄存器响应时,pxMBFrameCBResponseForSlave()就能精准操作xMBMultiSlaveCtxTable[3],更新其寄存器缓存、触发用户定义的vMBMultiSlave3DataReady()回调,而不会污染从机 5 的数据区。


3. 编译与烧录:Keil MDK 工程结构与关键配置项详解

本项目提供 Keil MDK-ARM V5.38 工程(Project.uvprojx),目标芯片为 STM32F407ZGT6,使用 HAL 库(v1.24.0)。工程非简单堆砌文件,而是按功能分层组织,便于二次开发时快速定位模块。

3.1 工程目录结构与文件职责

目录/文件职责说明修改风险
Core/Inc/mbmulti.h多从机上下文定义、API 声明⚠️ 高(增删从机数需同步改MB_MULTI_MAX_SLAVE)
Core/Src/mbmulti.c多从机调度器、上下文管理、轮询逻辑⚠️ 中(调整轮询策略在此)
Core/Src/mbportserial.c串口驱动改造版(含地址过滤、缓冲区隔离)⚠️ 高(涉及硬件时序,勿动中断优先级)
Core/Src/mb.c未修改的 FreeMODBUS 1.5 核心协议栈✅ 安全(建议用 git submodule 管理,避免误改)
User/App/main.c主函数:初始化 5 个从机(地址 1/3/5/7/9),启动调度器✅ 低(业务逻辑入口)
User/App/modbus_slave_handler.c每个从机的业务回调(如vMBMultiSlave1Handler()解析寄存器)✅ 低(纯业务,可自由扩展)

提示:mb.c文件被刻意保护为只读。所有改造均在mbmulti.c和mbportserial.c中完成,这是为了后续升级 FreeMODBUS 版本时,只需替换mb.c,再微调mbmulti.c的接口适配即可,大幅降低维护成本。

3.2 Keil 关键配置项(必须核对)

打开Options for Target → C/C++标签页,以下宏定义决定多从机行为:

// 必须定义(否则编译失败) MB_RTU_ENABLED=1 MB_ASCII_ENABLED=0 MB_TCP_ENABLED=0 MB_DEVICE_DTU=0 // 多从机特有宏(本项目启用) MB_MULTI_SLAVE_ENABLED=1 MB_MULTI_MAX_SLAVE=8 MB_MULTI_TIMEOUT_MS=300 // 从机无响应超时时间(毫秒) MB_MULTI_POLL_INTERVAL_MS=50 // 每次轮询间隔(毫秒,控制总线占用率) // 串口参数(与硬件匹配!) MB_PORT_SERIAL_BAUDRATE=9600 MB_PORT_SERIAL_PARITY=MB_PAR_NONE MB_PORT_SERIAL_STOPBITS=MB_STOP_BITS_1

注意:MB_MULTI_POLL_INTERVAL_MS=50是血泪经验。若设为 10ms,STM32F407 在 9600 波特率下,每轮发送 8 帧(每帧约 10ms 传输时间)+ 处理开销,实际轮询周期会突破 150ms,导致从机响应来不及处理。50ms 是实测平衡点:总线占用率 < 40%,单轮耗时 ≈ 420ms,满足 500ms 心跳要求。

3.3 STM32 HAL 初始化要点(CubeMX 导出后必改)

CubeMX 生成的main.c中,MX_USART1_UART_Init()需手动追加两行:

// 在 MX_USART1_UART_Init() 函数末尾添加: __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 启用空闲中断(用于帧检测) __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 启用接收中断(必须!)

同时,在stm32f4xx_it.c的USART1_IRQHandler中,必须调用本项目的串口 ISR:

void USART1_IRQHandler(void) { // 原 CubeMX 生成的 HAL_UART_IRQHandler(&huart1); 注释掉! // 改为调用 FreeMODBUS 多从机专用 ISR xMBPortSerialISR(); }

提示:若忘记注释HAL_UART_IRQHandler(),会导致中断重复执行,xMBPortSerialISR()被调两次,接收缓冲区错乱。这是新手最常翻车的点之一。

3.4 烧录与调试准备:硬件连接与逻辑分析仪验证点

  • RS-485 硬件:使用 MAX485 芯片,RE/DE引脚由 STM32 的 GPIO 控制(本项目用PD6)。确保PD6在发送前拉高,发送后拉低,延时精确到微秒级(HAL_GPIO_WritePin()+HAL_Delay(1)不够准,必须用__NOP()或 DWT 周期计数)。
  • 逻辑分析仪探头:接在RO(接收端)和DI(发送端)线上,触发条件设为RO上升沿(从机开始响应)。正常波形应看到:主机发一帧 → 延时 3.5 字符时间 → 从机 1 响应 → 延时 3.5 字符时间 → 从机 3 响应… 无重叠。
  • 串口打印调试:printf("SLAVE%d: OK\n", ucSlaveIdx);加在pxMBFrameCBResponseForSlave()成功解析后,通过 SWO 或虚拟串口输出,实时监控各从机心跳。

4. 避坑指南:8 个真实踩坑记录与根治方案

多从机 Modbus 是嵌入式领域经典玄学场景,信号、时序、内存、状态机四重干扰。以下是本项目实测中踩过的 5 个典型坑,附带现象、根因与可落地的解决方案。

4.1 现象:示波器显示总线空闲时,RO线持续低电平,DI线无信号,但eMBMultiPoll()却不断报告EV_FRAME_RECEIVED

  • 原因:MAX485 的RO引脚悬空或上拉不足。RS-485 总线空闲时应为差分高电平(A>B),若RO悬空,内部弱上拉失效,RO被噪声拉低,MCU 误判为“收到数据”,触发空帧解析,导致状态机混乱。
  • 解决:在RO引脚外接 10kΩ 上拉电阻至 3.3V,并在mbportserial.c的vMBPortSerialEnable()中增加空闲线检测:
    void vMBPortSerialEnable(bool bEnableRx, bool bEnableTx) { if (bEnableRx) { // 启用接收前,先读 RO 引脚电平,若为低则延时 10ms 再启用 if (HAL_GPIO_ReadPin(GPIOx, GPIO_PIN_x) == GPIO_PIN_RESET) { HAL_Delay(10); } } // ... 原启用逻辑 }

4.2 现象:从机 1 响应正常,但从机 3 始终超时,示波器看主机发给从机 3 的帧地址字节是0x03,但从机 3 的RO线无任何响应

  • 原因:从机 3 的地址拨码开关接触不良,或其内部 Modbus 地址寄存器被意外写为0x00(广播地址)。广播地址从机只响应写指令,不响应读指令。
  • 解决:用万用表测量从机 3 的地址引脚电压,确认拨码开关物理状态;用 Modbus Poll 工具单独连接从机 3(断开其他从机),发03 00 00 00 01读保持寄存器,观察是否响应。若不响应,强制写地址寄存器(功能码06)恢复为0x03。

4.3 现象:eMBMultiPoll()调用频率越来越慢,最终卡死,HAL_GetTick()返回值停滞

  • 原因:xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ulLastAccessTick被溢出覆盖。ulLastAccessTick是uint32_t,HAL_GetTick()每 49.7 天溢出一次,但若MB_MULTI_TIMEOUT_MS设为0xFFFFFFFF(错误宏定义),比较时ulCurrentTick - ulLastAccessTick会变成极大正数,永远满足超时条件,导致eMBMultiPoll()在超时处理分支无限循环。
  • 解决:严格校验MB_MULTI_TIMEOUT_MS宏值,禁止用0xFFFFFFFF;在比较前加溢出防护:
    uint32_t ulDelta = (ulCurrentTick >= ulLastAccessTick) ? (ulCurrentTick - ulLastAccessTick) : (0xFFFFFFFF - ulLastAccessTick + ulCurrentTick + 1); if (ulDelta > MB_MULTI_TIMEOUT_MS) { ... }

4.4 现象:添加第 6 个从机后,从机 1 的数据偶尔错乱,比如0x0001变成0x0100(字节序颠倒)

  • 原因:xMBMultiSlaveCtxTable[5].ucRcvBuf缓冲区溢出,写到了xMBMultiSlaveCtxTable[0]的内存区域。MB_FUNC_READ_INPUT_REGISTER_SIZE默认为 256,但某从机返回 300 字节数据,usRcvLength未做上限检查。
  • 解决:在xMBPortSerialISR()中增加缓冲区边界检查:
    if (xMBMultiSlaveCtxTable[ucCurrSlaveIdx].usRcvLength < sizeof(xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ucRcvBuf)) { xMBMultiSlaveCtxTable[ucCurrSlaveIdx].ucRcvBuf[xMBMultiSlaveCtxTable[ucCurrSlaveIdx].usRcvLength++] = ucByte; } else { // 缓冲区满,丢弃后续字节,并标记错误 xMBMultiSlaveCtxTable[ucCurrSlaveIdx].eEvent = EV_BUFFER_OVERFLOW; }

4.5 现象:系统运行 2 小时后,某个从机突然停止响应,重启主机无效,必须断电重启从机

  • 原因:从机固件存在看门狗缺陷。主机轮询间隔50ms,但某从机处理03功能码需60ms,导致看门狗超时复位,复位后从机进入初始化状态,无法及时响应主机查询,形成死锁。
  • 解决:在主机侧增加从机软复位机制。当某从机连续 3 次超时,向其发送40功能码(厂商自定义复位指令):
    if (xMBMultiSlaveCtxTable[ucIdx].usTimeoutCount >= 3) { uint8_t aucResetReq[] = { ucSlaveAddr, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 }; vMBMultiSendRawFrame(ucIdx, aucResetReq, 8); xMBMultiSlaveCtxTable[ucIdx].usTimeoutCount = 0; }

5. 二次开发实战:如何动态增删从机、接入新传感器、对接 MQTT 上云

本项目不是“一次编译终身使用”的 Demo,而是为工业现场迭代预留了清晰的扩展路径。下面以三个高频需求为例,给出可直接抄作业的改造步骤。

5.1 需求:现场新增一台从机(地址 11),不重新编译固件,运行时动态加载

FreeMODBUS 多从机的核心价值在于“配置即代码”。本项目已预留运行时 API:

// 在 main.c 中,通过串口命令动态添加 if (strstr((char*)rx_buffer, "ADD_SLAVE")) { uint8_t ucAddr = (uint8_t)strtol(strchr((char*)rx_buffer, ' ') + 1, NULL, 10); eMBErrorCode eErr = eMBMultiAddSlave(ucAddr, MB_FUNC_READ_INPUT_REGISTER); if (eErr == MB_ENOERR) { printf("Added slave %d\n", ucAddr); } } // eMBMultiAddSlave() 实现(mbmulti.c) eMBErrorCode eMBMultiAddSlave(uint8_t ucSlaveAddr, uint8_t ucFuncCode) { for (uint8_t i = 0; i < MB_MULTI_MAX_SLAVE; i++) { if (!xMBMultiSlaveCtxTable[i].bIsEnabled) { xMBMultiSlaveCtxTable[i].ucSlaveAddr = ucSlaveAddr; xMBMultiSlaveCtxTable[i].bIsEnabled = true; xMBMultiSlaveCtxTable[i].eEvent = EV_READY; // 初始化其寄存器映射表(此处简化,实际需根据 ucFuncCode 分配) vMBMultiInitRegMap(i, ucFuncCode); return MB_ENOERR; } } return MB_EILLSTATE; }

关键点:vMBMultiInitRegMap()需根据传感器类型预置寄存器地址映射。例如温湿度传感器(地址 11)的0x0000存温度,0x0001存湿度,就在该函数中写死pRegMap->usRegBase = 0x0000; pRegMap->usRegCount = 2;。

5.2 需求:将从机数据通过 ESP32 WiFi 模块上传到 MQTT 服务器

本项目User/App/mqtt_uploader.c已集成 Eclipse Paho Embedded C 客户端(轻量版)。只需填充数据打包逻辑:

// mqtt_uploader.c void vMQTTUploadSlaveData(uint8_t ucSlaveIdx) { char payload[256]; // 格式:{"slave":1,"temp":25.3,"humi":65.1,"ts":1712345678} float fTemp = (float)((int16_t)xMBMultiSlaveCtxTable[ucSlaveIdx].usRegBuf[0]) / 10.0f; float fHumi = (float)((int16_t)xMBMultiSlaveCtxTable[ucSlaveIdx].usRegBuf[1]) / 10.0f; snprintf(payload, sizeof(payload), "{\"slave\":%d,\"temp\":%.1f,\"humi\":%.1f,\"ts\":%lu}", xMBMultiSlaveCtxTable[ucSlaveIdx].ucSlaveAddr, fTemp, fHumi, HAL_GetTick() / 1000); MQTTClient_publish(&client, "modbus/data", payload, strlen(payload), MQTT_QOS_AT_MOST_ONCE, 0); }

在main.c的while(1)循环中,每 5 秒调用一次vMQTTUploadSlaveData(0)(上传从机 1 数据),即可实现边缘上云。

5.3 需求:从机 5 是 PLC,需支持写线圈(功能码 05),但原项目只实现读

FreeMODBUS 的写操作需额外实现pxMBFrameCBRequestCur()回调。本项目mbmulti.c中已预留钩子:

// 在 eMBMultiEnable() 后,为从机 5 注册写回调 vMBRegisterFrameCBRequest(5, pxMBFrameCBRequestForSlave5); // pxMBFrameCBRequestForSlave5() 实现 eMBException pxMBFrameCBRequestForSlave5(uint8_t * pucFrame, uint16_t usLength) { uint8_t ucFuncCode = pucFrame[1]; if (ucFuncCode == MB_FUNC_WRITE_SINGLE_COIL) { uint16_t usCoilAddr = (pucFrame[2] << 8) | pucFrame[3]; uint16_t usCoilValue = (pucFrame[4] << 8) | pucFrame[5]; // 控制实际 GPIO(假设线圈 0x0000 对应 PD0) if (usCoilAddr == 0x0000) { HAL_GPIO_WritePin(GPIOD, GPIO_PIN_0, usCoilValue ? GPIO_PIN_SET : GPIO_PIN_RESET); } return MB_EX_NONE; } return MB_EX_ILLEGAL_FUNCTION; }

注意:写操作必须在eMBMultiPoll()的EV_EXECUTE状态下调用,因此需确保mbmulti.c中eMBPollForSlave()正确传递eMBEvent事件类型。本项目已在mbmulti.c第 327 行完成该逻辑,无需额外修改。

从那以后我每次接到多从机需求,都先打开这个freemodbus1.5多从机.zip,把mbmulti.c和mbportserial.c拖进新工程,花 20 分钟配好地址和串口,再用逻辑分析仪抓三帧波形确认时序,基本就稳了。它不是银弹,但把 FreeMODBUS 从“玩具级单机”拽进了“产线级多节点”的门槛——那些年在示波器前熬的夜,最终都沉淀成了这几行带注释的 C 代码。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询