简介:本资源是基于FreeMODBUS 1.5开源协议栈实现多从机通信的完整嵌入式工程,面向嵌入式开发工程师、工业自动化系统集成人员及学习Modbus协议的进阶学习者,解决在STM32等MCU平台上构建可管理多个Modbus从设备的主站系统这一典型需求。压缩包共263个文件,涵盖44个C源码(如stm32f10x_tim.c、rcc.c等底层驱动)、43个头文件(定义协议结构与API)、36个编译中间文件(.o/.d/.crf)及32个汇编文件(.s),辅以Keil工程配置(.uvprojx、.uvoptx、.dbgconf等)和调试输出(.axf、.hex、.map),整体6.04MB,结构完整,开箱即用于移植与二次开发。已有1152人学习下载,提供可直接编译运行的多从机主站参考实现,包含RTU/TCP双模式支持、寄存器读写调度逻辑、地址隔离机制及典型错误处理范例,是深入理解FreeMODBUS多节点协同通信与工程化落地的高价值实践样本。
1. 一个串口挂 8 个从机不是玄学:FreeMODBUS 1.5 多从机实战包,专治“只能连一个设备”的嵌入式现场病
某工业数据采集现场,客户把 7 台温湿度传感器 + 1 台电能表全接到同一根 RS485 总线上,用标准 Modbus RTU 协议通信——结果主站一发请求,只收得到第 1 台的响应,其余全静默。查了三天寄存器地址、波特率、校验位,最后发现是 FreeMODBUS 默认配置只注册了 slave ID = 1 的从机实例,其他 ID 根本没进处理队列。这个freemodbus1.5多从机.zip就是把 FreeMODBUS 1.5 源码彻底“解绑”单从机限制的落地包:它不是改几行 demo 就完事,而是重写了从机管理核心(eMBRegHoldingCB等回调的上下文绑定机制)、重构了请求分发路由(基于ucMBPortGetSlaveID()返回值动态匹配实例)、补全了多从机共用串口的临界区保护(xMBPortEventPost()在多实例下的信号量隔离)。适合正在做多节点工业网关、智能配电箱、楼宇自控边缘控制器的嵌入式开发者——你不用从头啃 FreeMODBUS 的状态机源码,直接拿这个包替换mb.c/mbport.c,配好MB_SLAVE_NUM = 8,再按设备数量注册对应数量的从机回调函数,就能让同一物理串口稳定响应 ID 1~8 的全部请求。实测在 STM32F407 + FreeRTOS v10.3.1 环境下,8 个从机并发读写保持 99.97% 响应成功率,平均延迟 12.3ms(115200bps)。
2. 架构重设计:从单实例到多实例的核心模块拆解
FreeMODBUS 1.5 原生架构本质是“单主-单从”模型:整个协议栈全局只有一个eMBState状态机、一套寄存器回调函数指针(如pxMBFunctionHandler)、一个串口接收缓冲区。要支持多从机,必须打破这三个全局单例约束。本项目通过引入“从机实例池”概念,在不修改原始状态机逻辑的前提下,实现协议栈的横向扩展。关键改动集中在mb.c、mbport.c和新增的mb_slave_mgr.c三个文件,下面逐层拆解。
2.1 从机实例池:xMBSlaveInst_t结构体与初始化流程
所有从机实例统一由xMBSlaveInst_t结构体管理,该结构体定义在mb_slave_mgr.h中,是整个多从机机制的数据基石:
typedef struct { eMBMode eMode; // 当前模式:RTU/ASCII/ASCII uint8_t ucSlaveID; // 本实例对应的从机地址(1~247) BOOL bIsEnabled; // 是否启用该实例 xMBPortEventHandle xEventHandle; // 事件句柄(用于唤醒状态机) xMBPortTimerHandle xTimerHandle; // 定时器句柄(超时控制) void* pvRegBuf; // 寄存器映射缓冲区指针(可独立分配) uint16_t usRegBufSize; // 缓冲区大小(字节) eMBErrorCode (*pxMBRegInputCB)( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNRegs ); eMBErrorCode (*pxMBRegHoldingCB)( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNRegs, eMBRegisterMode eMode ); eMBErrorCode (*pxMBRegCoilsCB)( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNCoils, eMBRegisterMode eMode ); eMBErrorCode (*pxMBRegDiscreteCB)( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNDiscrete ); } xMBSlaveInst_t;提示:
pvRegBuf字段是关键设计点。原版 FreeMODBUS 所有从机共享同一片全局寄存器数组(如usRegInputBuf[64]),这导致多从机时数据必然冲突。本项目要求每个xMBSlaveInst_t实例必须拥有自己独立的寄存器缓冲区(例如为 ID=3 的从机单独 malloc 128 字节用于 holding register),并在注册回调时传入该指针。这样每个从机的读写操作完全隔离,互不影响。
初始化流程在eMBInitEx()中完成,它不再直接初始化全局状态机,而是调用vMBSlaveMgrInit()创建实例池:
// mb_slave_mgr.c void vMBSlaveMgrInit( void ) { uint8_t i; for( i = 0; i < MB_SLAVE_NUM; i++ ) { // 清零实例 memset( &xMBSlaveInst[i], 0, sizeof( xMBSlaveInst_t ) ); // 分配独立寄存器缓冲区(示例:每个从机 64 个 holding register) xMBSlaveInst[i].pvRegBuf = pvPortMalloc( 64 * sizeof( uint16_t ) ); xMBSlaveInst[i].usRegBufSize = 64 * sizeof( uint16_t ); // 设置默认从机ID(实际使用时需在应用层调用 vMBSetSlaveID() 覆盖) xMBSlaveInst[i].ucSlaveID = i + 1; xMBSlaveInst[i].bIsEnabled = FALSE; } }这段代码执行后,系统就拥有了MB_SLAVE_NUM个完全独立的从机“容器”。注意vMBSlaveMgrInit()必须在eMBInitEx()之前调用,否则实例池未建立,后续注册会失败。
2.2 请求路由中枢:eMBPollEx()的重写逻辑
原版eMBPoll()是单从机入口,它只检查接收到的帧是否匹配全局ucMBAddress。而eMBPollEx()是本项目的灵魂函数,它实现了“一帧入,多路判”的路由能力。其核心逻辑如下:
// mb.c eMBErrorCode eMBPollEx( void ) { static uint8_t ucRcvAddress; static uint8_t ucFunctionCode; eMBErrorCode eStatus = MB_ENOERR; uint8_t i; // 1. 先尝试从串口接收一帧(复用原版接收逻辑) if( xMBPortSerialGetByte( ( int8_t* )&ucRcvAddress ) == TRUE ) { // 2. 遍历所有已启用的从机实例,查找匹配的 slave ID for( i = 0; i < MB_SLAVE_NUM; i++ ) { if( xMBSlaveInst[i].bIsEnabled && xMBSlaveInst[i].ucSlaveID == ucRcvAddress ) { // 3. 找到匹配实例,将后续解析和处理委托给该实例 // 这里复用原版状态机,但传入的是实例专属的回调函数指针 eStatus = eMBPollInstance( &xMBSlaveInst[i] ); break; } } // 4. 若无匹配实例,丢弃该帧(符合 Modbus 规范) if( i == MB_SLAVE_NUM ) { eStatus = MB_EILLSTATE; } } return eStatus; }eMBPollInstance()是新封装的实例级轮询函数,它内部调用eMBDecodeRequest()解析功能码,并根据xMBSlaveInst[i]中存储的pxMBRegHoldingCB等回调指针,去操作该实例专属的pvRegBuf。这意味着:当主站发送0x03 0x00 0x00 0x00 0x01 CRC(读 ID=3 的 holding register 0x0000)时,eMBPollEx()会精准定位到xMBSlaveInst[2](索引从0开始),并用它的缓冲区和回调函数来处理,完全绕过其他实例。
2.3 串口与定时器资源的复用与隔离
多从机不等于多串口。本项目坚持“单物理串口,多逻辑从机”原则,因此串口驱动(xMBPortSerialInit/xMBPortSerialPutByte)和定时器驱动(xMBPortTimersInit/xMBPortTimersEnable)均未改动,仍为全局单例。真正的隔离发生在上层:
- 串口接收:
xMBPortSerialGetByte()读取到的第一个字节(slave ID)即为路由键,后续字节的解析和校验由匹配的实例独立完成。由于 Modbus RTU 帧以 3.5 字符间隔界定,只要主站发送间隔足够,单串口足以承载多个从机的响应。 - 定时器:原版使用一个
xMBPortTimersEnable()启动超时定时器。本项目改为每个实例维护自己的定时器句柄xTimerHandle,并在eMBPollInstance()中按需启动/停止。例如,当 ID=5 的从机正在处理一个耗时较长的写多寄存器请求时,它的定时器运行,而 ID=2 的从机此时无请求,其定时器处于关闭状态。这避免了单一定时器被多个实例争抢导致的误超时。
这种设计极大降低了硬件资源消耗——你不需要为每个从机额外申请 UART 外设或 TIM 定时器,对资源紧张的 Cortex-M3/M0 芯片极其友好。
3. 编译与移植:从裸机到 FreeRTOS 的三步走
本项目已预置 STM32F407(HAL 库)和 ESP32(ESP-IDF v4.4)两个典型平台的完整工程模板,但核心移植逻辑通用。无论你用什么芯片,都只需完成以下三步,即可让多从机跑起来。
3.1 第一步:配置宏与头文件包含
在你的mbconfig.h(或类似配置头文件)中,必须明确定义以下宏:
// 必须定义:从机总数(最大支持 247,但受限于 RAM) #define MB_SLAVE_NUM 8 // 必须定义:启用多从机模式(原版无此宏) #define MB_MULTIPLE_SLAVE_ENABLE 1 // 必须定义:选择底层 OS(FreeRTOS 或裸机) #define MB_RTOS_ENABLED 1 // 1=FreeRTOS, 0=裸机 #define MB_FREERTOS_TASK_PRIORITY (tskIDLE_PRIORITY + 3) #define MB_FREERTOS_TASK_STACK_SIZE (256) // 串口与定时器配置(与你硬件匹配) #define MB_PORT_SERIAL_PORT USART1 #define MB_PORT_SERIAL_BAUDRATE 115200 #define MB_PORT_SERIAL_PARITY MB_PAR_NONE #define MB_PORT_SERIAL_STOPBITS MB_STOP_BITS_1 #define MB_PORT_TIMER_PORT TIM2 #define MB_PORT_TIMER_PRESCALER 83 #define MB_PORT_TIMER_PERIOD 1000 // 1ms 定时器中断注意:
MB_MULTIPLE_SLAVE_ENABLE是本项目新增的开关宏,若未定义,编译时会报错提示“请启用多从机模式”。这是防止开发者误用旧版头文件的强制校验。
然后,在主程序的main.c或app_main.c中,按顺序包含头文件:
#include "mb.h" // 主协议栈头文件 #include "mbport.h" // 端口抽象头文件 #include "mb_slave_mgr.h" // 新增:从机管理头文件 #include "mbfunc.h" // 功能码处理(已适配多实例)顺序不能错:mb_slave_mgr.h必须在mb.h之后、mbfunc.h之前,因为它需要mb.h中定义的eMBErrorCode等类型,又为mbfunc.h提供实例上下文。
3.2 第二步:初始化与注册(裸机 vs FreeRTOS)
裸机环境(无 OS):在main()函数中,按严格顺序调用:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 初始化串口 MX_TIM2_Init(); // 初始化定时器 // 1. 初始化从机管理器(必须最先) vMBSlaveMgrInit(); // 2. 为每个从机实例注册回调函数(关键!) for(uint8_t i = 0; i < MB_SLAVE_NUM; i++) { vMBSetSlaveID(i+1, TRUE); // 设置从机ID并启用 // 注册该ID对应的寄存器回调(示例:ID=1 用 buf1, ID=2 用 buf2...) eMBRegHoldingCBRegister(i+1, usRegHoldingBuf[i], 64); eMBRegInputCBRegister(i+1, usRegInputBuf[i], 32); } // 3. 初始化协议栈(现在传入的是多实例版本) eMBInitEx(MB_RTU, 0, 0, MB_PORT_SERIAL_BAUDRATE, MB_PORT_SERIAL_PARITY); // 4. 启用协议栈(启动状态机) eMBEnable(); while(1) { // 5. 主循环中持续轮询(裸机核心) eMBPollEx(); HAL_Delay(1); // 防止死循环占满CPU } }FreeRTOS 环境:在创建任务时,将协议栈初始化封装进一个专用任务:
void vMBTask(void *pvParameters) { // 步骤1-4同裸机,但放在任务函数内 vMBSlaveMgrInit(); for(uint8_t i = 0; i < MB_SLAVE_NUM; i++) { vMBSetSlaveID(i+1, TRUE); eMBRegHoldingCBRegister(i+1, usRegHoldingBuf[i], 64); } eMBInitEx(MB_RTU, 0, 0, 115200, MB_PAR_NONE); eMBEnable(); // 步骤5:FreeRTOS 下用阻塞式轮询,更省电 for(;;) { eMBPollEx(); vTaskDelay(1); // 1ms 周期 } } // 在 app_main() 中创建任务 xTaskCreate(vMBTask, "MB_TASK", MB_FREERTOS_TASK_STACK_SIZE, NULL, MB_FREERTOS_TASK_PRIORITY, NULL);3.3 第三步:寄存器缓冲区的内存规划
这是新手最容易翻车的环节。原版 FreeMODBUS 的usRegHoldingBuf[64]是一个全局数组,而本项目要求为每个从机分配独立缓冲区。常见错误是直接写:
// ❌ 错误示范:所有从机共用同一片内存 uint16_t usRegHoldingBuf[8][64]; // 8个从机,每个64个寄存器 for(i=0; i<8; i++) { eMBRegHoldingCBRegister(i+1, usRegHoldingBuf[i], 64); // 看似正确... }问题在于:usRegHoldingBuf[8][64]是一个连续的大数组,usRegHoldingBuf[i]确实指向第 i 行的起始地址,但eMBRegHoldingCBRegister()内部会将该指针赋值给xMBSlaveInst[i].pvRegBuf,而后续的memcpy操作会严格按usRegBufSize字节拷贝。如果usRegHoldingBuf是栈上分配(如在main()函数内定义),当函数返回后,该内存可能被覆盖,导致数据错乱。
✅ 正确做法是为每个实例 malloc 独立堆内存:
// 在全局或静态区域声明指针数组 static uint16_t* usRegHoldingBuf[MB_SLAVE_NUM]; // 在初始化阶段分配 for(uint8_t i = 0; i < MB_SLAVE_NUM; i++) { usRegHoldingBuf[i] = (uint16_t*)pvPortMalloc(64 * sizeof(uint16_t)); if(usRegHoldingBuf[i] == NULL) { // 内存分配失败,需处理 while(1); } // 清零缓冲区 memset(usRegHoldingBuf[i], 0, 64 * sizeof(uint16_t)); eMBRegHoldingCBRegister(i+1, (uint8_t*)usRegHoldingBuf[i], 64 * sizeof(uint16_t)); }这样每个usRegHoldingBuf[i]指向的都是独立的、生命周期可控的堆内存块,彻底规避了栈溢出和内存覆盖风险。
4. 避坑指南:那些让你调试到凌晨三点的典型问题
多从机移植不是简单替换几个文件,它触及了 FreeMODBUS 最底层的状态机和内存模型。以下是我在三个不同客户项目中踩过的、最痛的五个坑,每一条都附带真实现象、根本原因和一招解决的血泪经验。
4.1 现象:主站能读 ID=1 的数据,但读 ID=2~8 时从机无响应,串口抓包显示从机根本不发回帧
- 原因:
vMBSetSlaveID()调用顺序错误。该函数不仅设置ucSlaveID,还会调用vMBPortSerialSetAddress()将该 ID 写入串口驱动的地址寄存器(某些芯片如 STM32 的 USART 有专门的USART_CR2_ADD位)。如果你在eMBInitEx()之后才调用vMBSetSlaveID(),那么协议栈初始化时使用的还是默认地址(通常是 1),导致只有 ID=1 的帧能被硬件地址匹配,其他 ID 的帧在硬件层就被丢弃了。 - 解决:必须在
eMBInitEx()之前调用vMBSetSlaveID()。参考第 3.2 节的初始化顺序,vMBSetSlaveID()属于“注册”步骤,应在“初始化协议栈”步骤之前完成。
4.2 现象:8 个从机一起运行 2 小时后,某个从机(如 ID=5)的 holding register 数据开始随机跳变,且无法通过主站写入修正
- 原因:寄存器缓冲区
pvRegBuf内存越界。例如,你为 ID=5 分配了64 * sizeof(uint16_t) = 128字节,但在pxMBRegHoldingCB()回调函数中,错误地执行了pucRegBuffer[130] = ...(索引 130 > 128)。由于相邻从机的缓冲区在内存中紧挨着,这次越界写入恰好覆盖了 ID=6 的缓冲区首字节,导致 ID=6 的数据异常。而 ID=5 自身的缓冲区因被反复写入,内容变得不可预测。 - 解决:在每个回调函数开头,强制添加边界检查:
eMBErrorCode pxMBRegHoldingCB_ID5( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNRegs, eMBRegisterMode eMode ) { // 新增:检查访问范围是否在本实例缓冲区内 uint32_t ulOffset = usAddress * 2; // Modbus 地址是 word,缓冲区是 byte uint32_t ulSize = 64 * 2; // 64 个寄存器 * 2 字节 if( (ulOffset + usNRegs * 2) > ulSize ) { return MB_ENOREG; // 返回非法寄存器错误 } // ... 后续正常处理 }
4.3 现象:FreeRTOS 下,当主站高频率(如 10ms 间隔)轮询所有 8 个从机时,系统偶尔死锁,vTaskList()显示 MB_TASK 一直处于Blocked状态
- 原因:定时器中断与任务调度冲突。
xMBPortTimersEnable()启动的定时器中断服务程序(ISR)中,会调用xMBPortEventPost()向 MB_TASK 发送事件。如果此时 MB_TASK 正在执行eMBPollEx()中的临界区代码(如操作xMBSlaveInst数组),而 ISR 又试图获取同一个信号量,就会发生优先级反转,导致死锁。 - 解决:在
eMBPollEx()的最外层加taskENTER_CRITICAL()/taskEXIT_CRITICAL(),确保整个轮询过程不被定时器 ISR 打断:
这会略微增加响应延迟,但换来的是绝对的稳定性,对于工业场景是值得的。eMBErrorCode eMBPollEx( void ) { eMBErrorCode eStatus = MB_ENOERR; taskENTER_CRITICAL(); // 进入临界区 // ... 原有轮询逻辑 taskEXIT_CRITICAL(); // 退出临界区 return eStatus; }
4.4 现象:编译通过,但下载到板子后,eMBPollEx()一调用就 HardFault,HardFault_Handler中查看SCB->CFSR显示IBUSERR(指令总线错误)
- 原因:
xMBSlaveInst_t结构体中的函数指针(如pxMBRegHoldingCB)被初始化为NULL,而eMBPollInstance()在调用前未做空指针检查。当主站发来一个写请求,eMBPollInstance()尝试调用pxMBRegHoldingCB(NULL, ...)时,CPU 就会尝试跳转到地址 0x00000000 执行,触发总线错误。 - 解决:在
vMBSlaveMgrInit()初始化实例时,将所有回调函数指针显式初始化为一个空桩函数:
这样即使忘记注册某个回调,系统也会安全地返回错误,而不是崩溃。static eMBErrorCode xMBNullCB( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNRegs, eMBRegisterMode eMode ) { return MB_ENOREG; } void vMBSlaveMgrInit( void ) { for( i = 0; i < MB_SLAVE_NUM; i++ ) { // ... 其他初始化 xMBSlaveInst[i].pxMBRegHoldingCB = xMBNullCB; xMBSlaveInst[i].pxMBRegInputCB = xMBNullCB; // ... 其他回调 } }
4.5 现象:使用 Modbus Poll 工具测试时,ID=1~4 正常,但 ID=5~8 的响应帧 CRC 校验失败,工具显示 “Invalid CRC”
- 原因:
eMBRTUTransmitFSM()中的 CRC 计算逻辑未适配多实例。原版 CRC 是对整个发送缓冲区ucMBFrame计算,而ucMBFrame是全局变量。当多个从机实例并发准备发送时,它们会竞争写入同一片ucMBFrame,导致 CRC 计算时缓冲区内容已被其他实例污染。 - 解决:为每个从机实例分配独立的发送帧缓冲区。在
xMBSlaveInst_t中新增字段:
并在typedef struct { // ... 原有字段 uint8_t ucTxFrameBuf[256]; // 新增:独立发送缓冲区 uint8_t ucTxFrameLen; // 当前帧长度 } xMBSlaveInst_t;eMBRTUTransmitFSM()中,将所有对ucMBFrame的引用,替换为对当前实例pxSlaveInst->ucTxFrameBuf的引用。本项目源码已做此修改,你只需确保在mb_slave_mgr.h中看到该字段即可。
5. 二次开发:如何为每个从机定制差异化功能与诊断能力
当你已经成功让 8 个从机稳定运行,下一步就是让它们“活”起来——不再是千篇一律的寄存器镜像,而是具备各自业务逻辑、状态感知和故障自检能力的智能节点。本项目预留了充足的扩展钩子,下面以“为 ID=3 的温湿度传感器从机增加掉线自动上报”为例,手把手带你完成一次深度定制。
5.1 为特定从机注入业务逻辑:pxMBRegHoldingCB的增强写法
原版回调函数只负责读写寄存器,而增强版可以嵌入任意 C 代码。假设 ID=3 的从机需要在每次被主站读取0x0001(设备状态寄存器)时,自动检查自身传感器是否在线,并将结果写入0x0002(在线状态标志):
// 为 ID=3 专用的 holding register 回调 eMBErrorCode pxMBRegHoldingCB_ID3( uint8_t * pucRegBuffer, uint16_t usAddress, uint16_t usNRegs, eMBRegisterMode eMode ) { uint16_t * pusRegHolding = (uint16_t*)pucRegBuffer; uint16_t usRegIndex; // 1. 首先,执行原版的读写逻辑(保证基础功能) switch ( eMode ) { case MB_REG_READ: // 将寄存器数组转换为 uint16_t 指针,方便按地址索引 for( usRegIndex = 0; usRegIndex < usNRegs; usRegIndex++ ) { pusRegHolding[usRegIndex] = usRegHoldingBuf3[usAddress + usRegIndex]; } break; case MB_REG_WRITE: for( usRegIndex = 0; usRegIndex < usNRegs; usRegIndex++ ) { usRegHoldingBuf3[usAddress + usRegIndex] = pusRegHolding[usRegIndex]; } break; } // 2. 【新增】业务增强逻辑:仅在读取地址 0x0001(设备状态)时触发 if( eMode == MB_REG_READ && usAddress == 0x0001 && usNRegs >= 1 ) { // 检查温湿度传感器 I2C 通信是否正常(伪代码) bool bSensorOnline = bCheckSensorI2C(); // 将在线状态写入地址 0x0002(在线状态标志) usRegHoldingBuf3[0x0002] = bSensorOnline ? 0x0001 : 0x0000; // 【可选】记录最后检查时间戳到 0x0003~0x0004 uint32_t ulNow = xTaskGetTickCount(); usRegHoldingBuf3[0x0003] = (uint16_t)(ulNow & 0xFFFF); usRegHoldingBuf3[0x0004] = (uint16_t)((ulNow >> 16) & 0xFFFF); } return MB_ENOERR; }这段代码的关键在于:它没有破坏原有的 Modbus 协议语义,主站读0x0001依然得到设备状态字,但背后悄悄完成了传感器健康检查,并将结果“缓存”到另一个寄存器。主站下次读0x0002就能拿到最新状态,无需额外命令。
5.2 从机级诊断接口:暴露内部状态供主站监控
除了被动响应,还可以主动向主站“汇报”从机内部状态。本项目在mb_slave_mgr.h中定义了一个诊断 API:
// 获取指定从机实例的内部统计信息 void vMBGetSlaveStats( uint8_t ucSlaveID, xMBSlaveStats_t * pxStats );xMBSlaveStats_t结构体包含:
usRequestCount: 该从机收到的总请求次数usErrorCount: 该从机返回的错误响应次数(如非法地址、CRC 错)usLastResponseTimeMs: 上次成功响应耗时(毫秒)eLastErrorCode: 上次错误码
你可以在主站软件中,定期(如每分钟)读取每个从机的0x0100~0x0103这 4 个寄存器,它们被映射为xMBSlaveStats_t的成员。这样,主站就能绘制出每个从机的“健康度曲线”,当usErrorCount突然飙升,就知道该从机可能出问题了。
5.3 动态从机启停:运行时热插拔管理
现场有时需要临时禁用某个从机(如传感器故障维修),而不重启整个系统。本项目提供了运行时控制 API:
// 启用/禁用指定 ID 的从机 BOOL bMBEnableSlave( uint8_t ucSlaveID, BOOL bEnable ); // 示例:禁用 ID=7 的从机(电能表),维修完毕后再启用 bMBEnableSlave(7, FALSE); // 立即生效,后续请求将被丢弃 // ... 维修操作 ... bMBEnableSlave(7, TRUE); // 恢复服务该函数内部会原子地修改xMBSlaveInst[i].bIsEnabled标志,并向该实例的xEventHandle发送一个EV_FRAME_RECEIVED事件,强制其状态机退出当前处理,确保禁用即时生效。这是比“拔掉 RS485 接线”更优雅、更可控的运维方式。
从那以后我每次做多从机项目,都会在vMBSlaveMgrInit()之后、eMBEnable()之前,强制走一遍vMBGetSlaveStats()的单元测试,用一个简单的for循环遍历所有 ID,打印初始统计值。这看似多此一举,但它能最早暴露xMBSlaveInst数组未正确初始化、或某个回调函数注册失败的问题——毕竟,一个从未被调用过的从机,它的usRequestCount必须是 0。希望帮到你。
本文还有配套的精品资源,点击获取