资讯动态

FreeModbus主从同栈移植与RS-485丢帧排障

发布时间:2026/10/28 4:27:33 来源:尧图企业网站定制
简介面向嵌入式开发初学者与已有STM32基础的工程师这份PDF资料围绕FreeModbus协议栈的主从机开发展开重点解决开源FreeModbus仅有从机、主机源码收费且优质参考资料零散的问题。文档结合STM32H743与FreeRTOS平台梳理主站、从站移植思路并给出注释较详尽的DEMO说明便于读者理解协议栈接口、任务调度与请求模式。资源共1个文件为PDF格式大小约120KB适合作为案头速查与入门索引。内容涉及FreeModbus V1.6、主机使用说明、主从流程图、协议文档整理及工程结构说明可帮助读者快速建立从协议概念到工程落地的整体认知也便于在真实项目中对照排查通信问题。目前已有505人学习适合希望系统掌握Modbus主从通信的嵌入式从业者。1. 一条 485 总线上三台表为什么只有一台丢帧现场最典型的一幕一条 RS-485 总线上挂三台仪表主站每 200 ms 轮询一轮两台稳得很第三台隔几分钟丢一次。抓波形看帧头帧尾都在CRC 却校验不过。这活儿最后十有八九落到两处——T3.5 帧间隔定时不准或者 485 收发方向切早了。这份资源就是冲这类现场问题准备的以 FreeModbus V1.6 源码打底把原本只开源从机的那套协议栈补出了主站并移植到 STM32H743 FreeRTOS 上跑通主站和从站能在同一套工程里共存附带的还有主从流程图、协议文档和调试工具。手上有一块 H7 或 F4 板子要接仪表、做过从站想摸主站轮询、想看清状态机到底怎么转的都能从里面挖到东西。下面按“架构分层—从机—主站—排障—改造”一路拆下去。2. 架构与移植分层主站代码为什么长得跟从站一个样2.1 补主站的动机与技术选型FreeModbus 本身是个很轻的协议栈从机部分开源代码量小、可裁剪、移植层抽象得干净一个port/目录就能把 MCU 相关的东西全隔离掉。麻烦的是主站官方主站要授权网上能找到的开源主站实现风格五花八门有的把 PDU 和串口收发糊在一个文件里有的干脆用阻塞读串口接进 RTOS 就卡死。这套移植的出发点很直接——主站照着从机的文件划分和函数命名重写一遍让eMBInit和eMBMasterInit、eMBPoll和eMBMasterPoll、eMBRegHoldingCB和eMBMasterRegHoldingCB成对出现应用层迁移成本压到最低。技术选型上没什么花活RTU 模式为主ASCII 和 TCP 的框架保留但不作为重点实时性交给 FreeRTOS事件通知用队列主站等待响应用信号量STM32H743 的串口和定时器资源充足一个串口给 Modbus一个定时器给 T3.5互不打架。选 H7 不是因为协议栈吃性能而是现场板子就是它顺手把 F4 上能跑的移植习惯带过来了。这里有个判断协议栈本身的 CPU 占用极低1 ms 轮询一次都嫌浪费真正吃精度的是帧间隔定时器和中断响应延迟芯片性能反而不是瓶颈。协议栈支持的方法覆盖了常用集合0x01 读线圈、0x02 读离散输入、0x03 读保持寄存器、0x04 读输入寄存器、0x05 写单线圈、0x06 写单寄存器、0x0F 写多线圈、0x10 写多寄存器异常响应 0x80 加原功能码那一套也实现了。主站侧把这些功能码封装成一组eMBMasterReq*函数应用层不需要自己拼 PDU。这点对二次开发很关键——现场最常见的需求是“多轮询几个寄存器”如果每次都要手搓字节序出错概率高得离谱。2.2 分层结构与工程目录协议栈的分层很清楚应用层只认回调函数协议层管 PDU 编解码和状态机端口层管串口、定时器和事件。移植的时候真正需要动的只有端口层那三四个文件其余原样编译。目录大致长这样freemodbus/ ├── modbus/ │ ├── mb.c # 从站协议核心状态机调度入口 │ ├── rtu/ │ │ ├── mbrtu.c # 从站 RTU 收发与 CRC │ │ └── mbrtumaster.c # 新增主站 RTU 收发与从站风格对齐 │ ├── ascii/ # ASCII 模式保留未启用 │ ├── tcp/ # TCP 模式保留未启用 │ ├── functions/ │ │ ├── mbfunccoils.c # 0x01 / 0x05 / 0x0F │ │ ├── mbfuncdisc.c # 0x02 │ │ ├── mbfuncholding.c # 0x03 / 0x06 / 0x10 │ │ ├── mbfuncinput.c # 0x04 │ │ └── mbfuncother.c # 异常响应与不支持功能码 │ └── include/ │ ├── mb.h # 对外 API 与错误码 │ ├── mbport.h # 端口层需要实现的函数声明 │ └── mbconfig.h # 裁剪开关功能码、缓冲区长度、从站数量 ├── port/ │ ├── portserial.c # 串口初始化、收发使能、字节读写 │ ├── porttimer.c # T3.5 定时器 │ └── portevent.c # 事件队列RTOS 或裸机标志位 └── demo/ └── stm32h743_freertos/ # 移植示例工程mbconfig.h是第一个要看的文件。里面几个开关直接决定内存占用MB_FUNC_HANDLERS_MAX控制能挂多少个功能码处理函数MB_PDU_SIZE_MAX定 PDU 缓冲MB_PDU_FUNC_READ_REGCNT_MAX限制一次能读多少个寄存器。默认值偏保守现场如果一次要读 100 个保持寄存器得把这个上限往上调否则从站回异常码 0x03非法数据值抓包看半天会以为是地址错了。端口层要实现的东西一共四组mbport.h里声明得明明白白/* 串口初始化、收发使能、字节级读写、字节到达检测 */ BOOL xMBPortSerialInit(UCHAR ucPort, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity); void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable); BOOL xMBPortSerialGetByte(CHAR *pucByte); BOOL xMBPortSerialPutByte(CHAR ucByte); /* 定时器以 50us 为一个 tick 单位从站用它判 T3.5 */ BOOL xMBPortTimersInit(USHORT usTimeOut50us); void vMBPortTimersEnable(void); void vMBPortTimersDisable(void); /* 事件协议层靠它知道“帧到了”“发送完了”“超时了” */ BOOL xMBPortEventInit(void); BOOL xMBPortEventPost(eMBEventType eEvent); BOOL xMBPortEventGet(eMBEventType *peEvent);这四组函数就是协议栈和硬件之间唯一的契约。理解了这一点移植到别的芯片只是换个外设库的事——串口换成任意 USART定时器换成任意 TIM事件换成队列或者裸机标志位。2.3 主从同栈的任务模型主站和从站要在同一块板子上跑第一个要回答的问题是任务怎么划分。常见做法有两种一种是一个 Modbus 任务里循环调eMBPoll()和eMBMasterPoll()另一种是拆成两条任务从站一条阻塞等事件主站一条按调度周期发起请求。这套移植走的是后者理由是主站轮询的节奏由业务决定有的表 200 ms 一轮有的 2 s 一轮跟从站的被动响应节奏完全不搭塞一个循环里会互相拖。但拆任务就带来共享串口的问题。主站发请求的瞬间如果从站正好也在收总线上就撞了。所以需要一个串口互斥锁主站在发起请求前拿锁收到响应或超时后释放。骨架大致是这样static SemaphoreHandle_t xMutexUart; /* 串口互斥 */ static QueueHandle_t xMbevtQueue; /* 从站事件队列 */ /* 从站任务被动响应阻塞等事件 */ static void vSlaveTask(void *pvParameters) { eMBInit(MB_RTU, 0x01, 0, 115200, MB_PAR_NONE); /* 从站地址 0x01 */ eMBEnable(); for (;;) { if (xSemaphoreTake(xMutexUart, portMAX_DELAY) pdTRUE) { (void)eMBPoll(); /* 内部非阻塞取事件处理 */ xSemaphoreGive(xMutexUart); } vTaskDelay(pdMS_TO_TICKS(1)); /* 1ms 让出避免饿死低优先级任务 */ } } /* 主站任务主动轮询节奏由业务表决定 */ static void vMasterTask(void *pvParameters) { eMBMasterInit(MB_RTU, 0, 115200, MB_PAR_NONE); /* 主站无地址 */ eMBMasterEnable(); for (;;) { prvPollOneSlave(); /* 见第 4 章的调度器 */ vTaskDelay(pdMS_TO_TICKS(200)); /* 一轮间隔 */ } }eMBPoll()和eMBMasterPoll()都不是阻塞函数它们只是查一下事件队列有没有待处理项有就推进状态机没有就立刻返回。真正让任务睡下去的是外层的vTaskDelay。这一点跟很多人对“协议栈轮询”的直觉相反——以为它内部会死等帧其实不会等待的活儿全交给了定时器和串口中断。也正因为如此从站任务里那把互斥锁的持有时间很短不会把主站卡太久。严格来说用互斥锁保护两个任务对同一串口的访问只是“不撞”的最低要求真要主从同栈还得看业务形态如果本机既是某段总线的主站又要作为从站被上一级 PLC 轮询那最好分两个串口各走各的物理链路省掉一堆仲裁逻辑。这套工程里两种接法都留了位置具体用哪种看现场机柜里还剩几根线。3. 从机侧拆解状态机、寄存器回调与 T3.5 定时器3.1 初始化序列与轮询入口从机的启动流程是固定的三步eMBInit配模式、地址、波特率和校验eMBEnable打开收发使能并启动定时器然后任务循环里反复调eMBPoll。这三步顺序不能乱eMBInit之前必须先初始化好串口和定时器的底层外设否则xMBPortSerialInit里访问未初始化的外设会直接飞。eMBErrorCode eStatus; /* 步骤 1初始化RTU 模式从站地址 0x01115200 8N1 */ eStatus eMBInit(MB_RTU, 0x01, 0, 115200, MB_PAR_NONE); if (eStatus ! MB_ENOERR) { /* MB_EINVAL: 参数错 MB_ENORES: 资源不足 */ prvErrHandler(eStatus); } /* 步骤 2使能内部会调用 vMBPortSerialEnable(TRUE, FALSE) 进入接收态 */ eStatus eMBEnable(); if (eStatus ! MB_ENOERR) { prvErrHandler(eStatus); } /* 步骤 3轮询放在任务循环里1ms 一次足够 */ for (;;) { (void)eMBPoll(); vTaskDelay(pdMS_TO_TICKS(1)); }eMBInit的五个参数里eMode选MB_RTU或MB_ASCIIucSlaveAddress就是本机从站地址ucPort在 STM32 移植里一般忽略或者映射到串口号ulBaudRate和eParity决定串口配置。校验位这个参数特别容易翻车MB_PAR_NONE是 8N1MB_PAR_EVEN是 8E1选错了从站根本不会应答但串口上能看到字节抓包工具里显示全是乱码或者校验错。eMBPoll内部维护一个接收状态机状态大致是四个STATE_RX_INIT等第一个 T3.5 超时来确认总线空闲STATE_RX_IDLE等待帧起始STATE_RX_RCV正在收字节STATE_RX_ERROR出错了等总线静默。状态切换全靠字节到达中断和定时器超时中断驱动eMBPoll只是把事件取出来往后推。所以调eMBPoll的频率不用太高1 ms 一次绰绰有余但也不能几百毫秒才调一次否则事件积压响应会明显变慢。3.2 寄存器回调怎么写协议栈把“寄存器在哪”这个问题完全交给应用层靠四个回调函数eMBRegCoilsCB、eMBRegDiscreteCB、eMBRegHoldingCB、eMBRegInputCB。读、写、读写混合三种操作都用同一个回调通过eMBRegisterMode区分。保持寄存器是最常用的写法如下#define REG_HOLDING_NREGS 128 static USHORT usRegHoldingBuf[REG_HOLDING_NREGS]; /* 应用数据协议栈不碰 */ eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; /* 越界检查地址从 0 开始算usAddress usNRegs 不能超上限 */ if ((usAddress usNRegs) REG_HOLDING_NREGS) { return MB_ENOREG; /* 协议栈会回异常码 0x02 */ } if (eMode MB_REG_READ) { for (i 0; i usNRegs; i) { /* Modbus 线上是大端高字节在前 */ pucRegBuffer[i * 2] (UCHAR)(usRegHoldingBuf[usAddress i] 8); pucRegBuffer[i * 2 1] (UCHAR)(usRegHoldingBuf[usAddress i] 0xFF); } } else { /* MB_REG_WRITE */ for (i 0; i usNRegs; i) { usRegHoldingBuf[usAddress i] (USHORT)((pucRegBuffer[i * 2] 8) | pucRegBuffer[i * 2 1]); } } return MB_ENOERR; }三个参数的含义pucRegBuffer是协议栈给的缓冲区读的时候往里填写的时候从里取usAddress是报文里的起始寄存器地址注意这里是 0 基usNRegs是寄存器个数eMode区分读写。有两个细节值得专门说。一是字节序Modbus 线上传输统一大端而 STM32 是小端所以必须手动拆字节直接把USHORT*强转成UCHAR*拷过去读出来的数值在高低位上会整个反掉现象是“读 100 回来 25600”第一次遇到会怀疑人生。二是返回值MB_ENOREG触发异常码 02非法数据地址MB_ENOERR是正常别的地方还有MB_EINVAL。有不少人越界了直接返回 0结果从站回一个正常响应带一堆垃圾数据主站那边看着“通了”实际数值全错这种问题排查起来最费时间。usRegHoldingBuf是应用和协议栈之间的唯一交接面。工程里常见的做法是把它当影子寄存器用定时器中断里把采集到的真实数据刷进来主站读的时候直接读影子避免在回调里做耗时操作。回调是在协议栈任务上下文里跑的里面要是加了 10 ms 的延时或者等信号量整个从站响应都会被拖慢主站那边就该报超时了。3.3 T3.5 定时器与帧边界判定RTU 模式靠时间间隔判断帧边界3.5 个字符时间内没有新字节就认为一帧结束。这个时间跟波特率挂钩按 11 位一个字符1 起始 8 数据 1 校验 1 停止算9600 bps 下约 4.01 ms115200 bps 下约 0.335 ms。协议栈的定时器接口用的是 50 μs 为一个 tick初始化时传进去的是 tick 数/* usTimeOut50us T3.5 换算成 50us 的个数 */ BOOL xMBPortTimersInit(USHORT usTimeOut50us) { /* 定时器计数频率 20kHz - 1 tick 50us */ /* 例如 72MHz / 3600 20kHz预分频写 3599 */ htim3.Init.Prescaler 3599; htim3.Init.Period usTimeOut50us - 1; /* 只开更新中断不开自动重装载每次 Enable 重新计数 */ return (HAL_TIM_Base_Init(htim3) HAL_OK); }xMBPortTimersInit只做一件事把定时器配成 50 μs 分辨率。真正决定超时时长的是调用方传进来的usTimeOut50us协议栈内部会根据波特率算好。vMBPortTimersEnable和vMBPortTimersDisable负责在每收到一个字节时重置计数——收到字节就清零重新数数满 3.5 个字符时间还没新字节就触发更新中断中断里xMBPortEventPost(EV_FRAME_RECEIVED)协议栈据此判定一帧收完。这里是最容易踩坑的地方。用 FreeRTOS 的 1 ms 系统 tick 去做 T3.5115200 下根本分不出来0.335 ms 比 1 ms 还小9600 下勉强能用但会抖现象就是“大部分帧正常偶尔丢一帧”。正确姿势是独立硬件定时器50 μs 分辨率中断里只发事件、不做任何耗时操作。另外定时器中断优先级要设得比串口略低或者同级别让它把串口字节中断给堵了。4. 主站侧拆解阻塞/非阻塞请求与轮询调度4.1 主站请求 API 与返回码主站的对外接口是按功能码封装的风格和从站回调对齐。每个请求函数的参数基本是四件套从站地址、起始寄存器地址、数量、超时时间。这套移植额外加了第五个参数——阻塞标志让应用层自己选调用模型。/* 读保持寄存器从站 0x02起始地址 0x0000读 10 个超时 100ms非阻塞 */ eMBErrorCode eStatus; eStatus eMBMasterReqReadHoldingRegister(0x02, 0x0000, 10, 100, FALSE); switch (eStatus) { case MB_ENOERR: /* 非阻塞模式下表示“请求已入队” */ break; case MB_MASTER_BUSY: /* 上一笔请求还没结束本次被拒 */ break; case MB_EMASTEREOUT: /* 阻塞模式下等响应超时 */ break; default: break; }阻塞和非阻塞的区别值得单独说清楚因为它直接决定应用层怎么写模式调用后行为返回值含义适用场景阻塞bBlock TRUE一直等到响应到达或超时直接返回本次请求结果单从站、低频、逻辑简单的场景非阻塞bBlock FALSE立即返回不等只表示请求是否被接受多从站轮询、有实时性要求的任务阻塞模式写起来最省心一行调用拿到结果但它会把调用任务挂住。多从站轮询场景里一台设备掉线就是 100 ms 卡顿轮询表长一点整个周期就崩了。非阻塞模式把“发请求”和“拿结果”拆开调用返回MB_ENOERR只代表请求发出去了结果要通过回调或者查询状态拿。工程里默认走非阻塞阻塞模式留给初始化阶段或者调试用。超时参数的单位是毫秒实际生效的是“发送完成后等第一个字节”和“字节间超时”两个阶段。设太小慢速设备来不及回设太大掉线设备拖垮整轮。经验值是按从站响应时间的两到三倍给串口波特率 9600 时一般 100200 ms115200 时 50 ms 就够。4.2 非阻塞轮询调度器多从站场景的调度器本身不复杂核心是把“轮询表”和“状态机”分开。表描述要问谁、问什么状态机管什么时候发下一笔。typedef struct { UCHAR ucSlaveAddr; /* 从站地址 */ USHORT usStartAddr; /* 起始寄存器0 基 */ USHORT usRegCount; /* 寄存器个数 */ USHORT usPeriodMs; /* 轮询周期 */ ULONG ulLastTick; /* 上次发起时刻 */ } PollItem_t; static PollItem_t s_pollTable[] { { 0x02, 0x0000, 10, 200, 0 }, /* 电表电压电流 */ { 0x03, 0x0010, 4, 500, 0 }, /* 温控器温度设定 */ { 0x05, 0x0000, 2, 1000, 0 }, /* 变频器状态字 */ }; static void prvPollOneSlave(void) { static UCHAR s_ucIndex 0; ULONG ulNow xTaskGetTickCount(); PollItem_t *pItem s_pollTable[s_ucIndex]; /* 到点、且主站空闲、且上一笔已经收完三者同时满足才发 */ if ((ulNow - pItem-ulLastTick) pdMS_TO_TICKS(pItem-usPeriodMs) !eMBMasterIsBusy()) { if (eMBMasterReqReadHoldingRegister(pItem-ucSlaveAddr, pItem-usStartAddr, pItem-usRegCount, 100, FALSE) MB_ENOERR) { pItem-ulLastTick ulNow; } else { /* 请求被拒记一次失败下一轮再试不要原地重试 */ } s_ucIndex (s_ucIndex 1) % (sizeof(s_pollTable) / sizeof(s_pollTable[0])); } }这段的逻辑是按表项轮转每次只发一笔。三个判断条件缺一不可ulNow - ulLastTick period保证节奏!eMBMasterIsBusy()保证上一笔请求已经结束收到响应或者超时第三点隐含在eMBMasterReqReadHoldingRegister返回成功里。少了IsBusy判断连续两笔请求会把状态机冲掉现象是“偶尔回一个不属于本次请求的数据”相当难查。ulLastTick用的是xTaskGetTickCount()不是绝对时间。注意无符号减法在 tick 回绕时仍然成立这是 FreeRTOS 的惯用法别改成直接比较大小。调度器还有个容易忽略的点发送失败时的处理。上面代码里请求被拒时只记录、不重试下一轮照常。原地重试几乎一定会把事情搞得更糟——总线本来就忙连续重发只会挤占其他从站的时间片还会让日志里堆满相同的错误掩盖真正的问题源。真要重试放在业务层做并且带退避。4.3 主站回调、超时与异常码回传主站收到响应后数据往哪放跟从站对称通过回调交给应用层/* 主站读保持寄存器的响应回调usAddress 是响应里的起始地址 */ eMBErrorCode eMBMasterRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; for (i 0; i usNRegs; i) { /* 同样是线上大端转成本机 USHORT */ g_usRs485Cache[i] (USHORT)((pucRegBuffer[i * 2] 8) | pucRegBuffer[i * 2 1]); } g_usRs485Count usNRegs; g_ucRs485SrcAddr g_ucMasterCurrentSlave; /* 记住是哪个从站回的 */ g_xRs485DataReady pdTRUE; /* 通知业务任务 */ return MB_ENOERR; }回调里只做字节搬运和标志置位别做业务处理。g_usRs485Cache这类全局缓存是主站模式的常见做法——同一时刻只有一笔请求在飞用一块全局缓存加一个“当前从站地址”就够了不需要为每个从站分配独立缓冲省 RAM 也省心。超时和异常响应走另一条路径。从站回异常响应功能码最高位置 1时协议栈解析出异常码通过主站错误回调上报常见的是 01 非法功能、02 非法数据地址、03 非法数据值、04 从站设备故障。应用层要能区分“超时”和“从站回了异常”——超时通常意味着物理层或者地址配错了异常响应说明从站是活的只是请求内容有问题。这两种情况在现场的处理方式完全不同日志里必须分开记。重试策略上这套移植没有在协议栈里内置自动重试原因上面提过协议栈层重试会把时序搅乱。工程里的做法是在业务层用状态机做两连击——第一次超时记一次下一轮再失败就判定设备离线把它的表项周期临时拉长等设备恢复了再降回来。这套降级逻辑写起来不到三十行但能让一条总线在一台设备挂掉时继续正常工作。5. 常见问题排查五个把帧吃掉的现场坑5.1 现象偶发丢帧CRC 校验不过重发就好原因T3.5 定时器精度不够。用 FreeRTOS 的 1 ms tick 或者 SysTick 去数帧间隔115200 波特率下 3.5 个字符只有 0.335 ms根本分辨不出来帧尾和下一帧帧头会被粘成一帧9600 下虽然勉强够用但中断延迟一抖就判错。解决换独立硬件定时器配成 50 μs 分辨率收到字节就重置计数。另外注意xMBPortTimersInit里传进来的usTimeOut50us不能小于 1波特率极高时要确认这个值算下来不是 0否则定时器永远不会触发。改完拿示波器量一下最后一个字节停止位到定时器中断的间隔对得上才算过。5.2 现象一收数据就进 HardFault或者卡死在中断里原因串口中断优先级数值小于configMAX_SYSCALL_INTERRUPT_PRIORITY即逻辑优先级高于 FreeRTOS 允许的范围却在中断服务函数里调用了xQueueSend之类的 FreeRTOS API。Cortex-M 上这类违规不一定立刻报错表现可能是随机死机、某个任务再也不调度排查起来像玄学。解决把所有会调用 FreeRTOS API 的中断优先级数值设成大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY并且中断里一律用FromISR结尾的版本退出前调用portYIELD_FROM_ISR。Modbus 的串口和定时器中断都在这个范围内的话优先级就得往下压一点实测对帧间隔判定没有影响。5.3 现象从站偶尔回一帧但主站收到的是截断的最后几个字节原因RS-485 方向引脚切换太早。发送完成中断TXE只表示数据寄存器空了移位寄存器里还有字节没发完这时候拉低 DE 会让最后一个字节被截断对方收到的帧长度不够CRC 自然错。解决改用发送完成TC中断里拉低 DE或者 TXE 之后再等一个字符时间。还有一种更稳的做法是把 DE 控制交给串口外设的硬件流控或者 RS-485 模式部分 STM32 系列支持 DE 自动管理寄存器一配就不用管时序了。切完方向再观察重点看从站回帧的最后一个字节是否完整。5.4 现象读回来的数值整体错位一个寄存器或者读到的全是 0原因寄存器地址 0 基和 1 基搞混了。Modbus 协议报文里的地址是 0 基而设备手册上写的往往是 40001、40002 这种 1 基编号直接把手册上的数字填进eMBMasterReqReadHoldingRegister的usRegAddr整体就偏了一位。解决写映射表的时候统一减 1并且把这个转换收敛到一个宏里别在每个调用点手写。从站侧的回调同理usAddress进来就是 0 基usRegHoldingBuf[usAddress]直接索引即可不要又减一次。这类错误最典型的特征是“读到的值看起来像隔壁寄存器的”一旦发现错位规律基本就能锁定。5.5 现象主站和从站同时工作时总线上出现两帧重叠或者主站请求永远超时原因主从同栈抢同一个串口。主站刚把请求发出去从站任务那边因为收到上一帧的残留数据进了接收态两边同时操作串口外设发送和接收状态互相覆盖。解决加串口互斥锁是最直接的办法主站发起请求前拿锁收到响应或超时后释放从站处理一帧期间也持锁。更彻底的做法是分两个物理串口主站一路、从站一路从根上消除仲裁。另外注意vMBPortSerialEnable的调用时机主站发送前要关接收、发完再开这个切换必须在锁内完成否则中间窗口里从站中断一进来就乱套。6. 二次开发加一个私有功能码并把它跑通现场设备经常有自己的私有功能码比如某型号变频器用 0x41 读序列号。扩展开关在mbconfig.h里eMBRegisterCB把功能码和处理函数绑在一起实现只要照着mbfuncholding.c的骨架改。/* 私有功能码 0x41请求 PDU [0x41][偏移H][偏移L]响应 [0x41][6 字节序列号] */ static eMBException prvReadSnFunction(UCHAR *pucFrame, USHORT *pusLength) { USHORT usOffset; UCHAR i; /* pucFrame[0] 是功能码数据从 pucFrame[1] 开始 */ usOffset (USHORT)((pucFrame[1] 8) | pucFrame[2]); if (usOffset ! 0 || *pusLength ! 3) { return MB_EX_ILLEGAL_DATA_VALUE; /* 异常码 0x03 */ } pucFrame[1] 0x41; /* 响应功能码 */ for (i 0; i 6; i) { pucFrame[2 i] g_ucDeviceSn[i]; /* 6 字节序列号 */ } *pusLength 8; /* 功能码 6 字节数据 无附加 */ return MB_EX_NONE; } /* 注册放在 eMBEnable 之前 */ if (eMBRegisterCB(0x41, prvReadSnFunction) ! MB_ENOERR) { prvErrHandler(MB_EINVAL); }几个参数要盯住pucFrame指向整个 PDU 的起始第 0 字节就是功能码所以数据从下标 1 开始*pusLength进函数时是请求长度出函数前必须改成响应长度写错了主站会收到长度不符的帧返回值是eMBException类型MB_EX_NONE表示正常其他值协议栈会自动组异常响应。私有功能码还会带来一个坑MB_FUNC_HANDLERS_MAX默认值可能不够加之前先确认mbconfig.h里留了余量注册失败返回MB_EINVAL多半就是这个原因。主站侧要对称地加一个eMBMasterReqReadSn和对应的响应解析风格参考eMBMasterReqReadHoldingRegister同样是组 PDU、发请求、等响应、校验功能码。别图省事直接在应用层用串口裸发——那样就绕过了状态机和总线仲裁前面辛苦搭起来的调度器全废。多从站轮询这块把第 4 章的s_pollTable改成从 Flash 读取就行。参数地址、起始寄存器、数量、周期做成结构体存 Flash上电加载配合一个串口命令或者按键进入配置模式修改。这样现场换设备不用重新烧固件省掉很多来回。配置写入记得带 CRC 和版本号掉电写坏了一整张表就全乱了。验证手法上手边有主站模拟工具和从站模拟工具各一个是最省事的从站模拟工具挂在本机虚拟串口上冒充仪表回应主站代码不用改就能验证 PDU 编解码和超时逻辑反过来用主站模拟工具轮询自己写的从站能快速验证回调里的地址映射。再往下就是示波器看波形重点量三个点帧起始到 T3.5 定时器启动的延迟、最后一个字节停止位到方向引脚翻转的间隔、两帧之间的空闲时间。这三个数对上了基本上就没有玄学问题了。工程里附的主从流程图在这时候特别有用尤其调状态机的时候——对着图看当前停在哪个状态比在代码里翻switch快得多。协议文档用来核对异常码和 PDU 长度有些细节比如写多寄存器的字节数字段光看代码容易漏。最后说个我自己的习惯。早几年调一条 485 总线三台设备来回丢帧我上来就怀疑从站固件有问题改了两天对方的代码最后发现是自己 T3.5 定时器用了 SysTick 分频115200 下压根分不出来。从那以后我每次移植 Modbus第一件事就是拿示波器量帧间隔量完再动别的代码宁可多花二十分钟也不在这上面栽第二次。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑