☰
FreeModbus主从同栈STM32H743+FreeRTOS移植调试
2026/11/2 9:25:51 网站建设 项目流程

简介:面向嵌入式开发者与STM32+FreeRTOS学习者的FreeModbus协议栈进阶资料,围绕主站、从站移植调试与协议理解展开。由于FreeModbus原生仅开放从机源码、主机源码收费且优质开源主机栈较少,资料重点说明在STM32H743+FreeRTOS上移植主从协议栈的思路,并保持主机与从机风格及接口一致,支持主从在同一协议栈运行、FreeRTOS环境、阻塞与非阻塞请求及自定义超时,覆盖常用Modbus方法。资源包共1个文件,主要为PDF文档,约120KB,内容涉及FreeModbus V1.6源码说明、STM32+FreeRTOS主从站例程、调试工具、主机与从机流程图、协议文档及工程结构梳理,便于按模块对照学习。已有506人学习,适合需要从入门到掌握主从站开发、移植与排错的读者。

1. 一块板子跑双角色:FreeModbus 主从栈在 STM32H743 上的真实用途

在嵌入式系统里做 Modbus协议 对接,最尴尬的场景是设备既要当从站被上位机轮询,又要当主站去读下游仪表。FreeModbus 的从机部分开源,主机源码往往要另找,STM32H743 上再叠 FreeRTOS,串口和定时器一冲突就容易翻车。这份资源把 FreeModbus 主站和从站合到同一套接口里,带 STM32+FreeRTOS 例程、流程图和调试工具,适合已经能点灯、想直接把主从链路跑通的嵌入式开发者,也适合需要现场排查 3.5T 定时和超时参数的人。它不是只给一份协议栈源码,而是把主站请求模型、从站回调、RTOS 任务划分和调试工具串成一条可复现的路径,拿来就能对照自己的板子改端口层。

2. 拆包先看分层:FreeModbus 源码、移植层与 STM32+FreeRTOS 接口怎么对上

FreeModbus 这类协议栈看着文件多,真正要动的区域并不散。源码包、移植工程、流程图和协议文档放在一起时,先别急着编译,先把“核心协议层—端口层—RTOS 适配层—应用回调层”四层切开。核心协议层负责 PDU 组包、功能码解析、异常码返回;端口层只关心串口字节收发和 3.5 字符定时;RTOS 适配层把事件通知、互斥和任务延时接进来;应用回调层才是寄存器映射和业务数据处理。主站源码与从站源码风格保持一致,意味着主站也沿用同一套端口接口,移植时不需要为两种角色各写一套串口驱动,这一点对 STM32+FreeRTOS 工程很关键。

2.1 资源包里的代码入口与阅读顺序

拿到资源后,我一般先看移植工程里的文件结构图,再对照 FreeModbus v1.6 源码目录。不要从 main.c 一路往下读,那样容易被业务代码带偏。先找portserial.c、porttimer.c、portevent.c这三个文件,它们决定协议栈能不能在目标板上跑起来;再看eMBInit、eMBEnable、eMBPoll的调用位置,确认从站任务入口;主站侧再看eMBMasterInit、eMBMasterEnable、eMBMasterPoll以及请求接口的封装位置。流程图的作用是帮你确认状态迁移,尤其是从站收到广播、地址不匹配、CRC 错误时分别回到哪个状态。

# 典型工程目录阅读顺序,不放具体路径,按自己的工程替换 FreeModbus/ ├── modbus/ # 核心协议层:功能码、PDU、CRC ├── port/ # 端口层:串口、定时器、事件 │ ├── portserial.c # 串口收发与中断 │ ├── porttimer.c # 3.5T 定时 │ └── portevent.c # 事件通知 ├── demo/ # STM32+FreeRTOS 主从例程 │ ├── mbslave.c # 从站任务与回调 │ └── mbmaster.c # 主站任务与请求 └── doc/ # 流程图、协议说明、使用说明

这段目录不是让你照抄路径,而是建立“先端口、后核心、再应用”的阅读顺序。核心协议层通常不需要大改,除非你要裁剪功能码;端口层才是板级差异集中的地方。串口文件里重点看接收中断是否按字节推入缓冲区、发送是否等待 TC 标志;定时器文件里重点看 3.5T 是软件计数还是硬件定时器捕获;事件文件里重点看从站轮询和主站请求完成是如何唤醒任务的。

2.2 主从共用接口与移植层要改哪几个点

主站与从站共用一套端口接口,好处是串口初始化、波特率切换、校验位配置只有一处。代价是主从同时运行时,串口和定时器不能再按“单角色”假设来写。移植层常见的改动点如下:

移植文件主要职责常改参数容易漏的点
portserial.c串口初始化、字节收发、中断使能波特率、数据位、停止位、校验位接收中断里清标志的顺序
porttimer.c3.5 字符静默定时定时器时钟、预分频、比较值波特率切换后重算计数
portevent.c事件标志、任务通知事件组位定义、超时时间主从事件位冲突
port.h端口宏与类型临界区宏、调试宏临界区嵌套
FreeRTOSConfig.h任务栈、堆、优先级堆大小、tick 频率协议任务栈不足

这张表里的“常改参数”不是拍脑袋填的。以 115200、8N1 为例,一个字符约 86.8 微秒,3.5 个字符约 304 微秒;如果定时器 tick 是 1 毫秒,直接拿 tick 做 3.5T 就会粗得没法用。常见做法是用一个 16 位定时器跑 1 微秒计数,或者用串口波特率推导出比较值。主从共用时,主站请求超时和从站帧间隔定时要分开,别把同一个定时器通道既当帧间隔又当请求超时,否则主站重发时会把从站帧边界搅乱。

/* port.h 中常见的临界区与事件位定义,按实际 MCU 头文件替换 */ #define ENTER_CRITICAL_SECTION() taskENTER_CRITICAL() #define EXIT_CRITICAL_SECTION() taskEXIT_CRITICAL() #define EV_READY (1 << 0) /* 协议栈就绪 */ #define EV_FRAME_RECEIVED (1 << 1) /* 从站收到完整帧 */ #define EV_EXECUTE (1 << 2) /* 从站执行 */ #define EV_MASTER_PROCESS (1 << 3) /* 主站处理请求 */ #define EV_MASTER_ERROR (1 << 4) /* 主站请求错误 */ /* 事件组句柄由应用层创建后挂到端口层 */ extern EventGroupHandle_t xModbusEvent;

这段代码的逻辑是把端口层需要的事件位先固定下来,避免主站和从站各用一套事件组导致唤醒丢失。ENTER_CRITICAL_SECTION在 FreeRTOS 下用任务临界区,不要在中断里调用;如果端口层函数可能被中断调用,要换成taskENTER_CRITICAL_FROM_ISR的配套写法。事件位从低位开始分配,主站事件放在从站事件之后,调试时看事件组值就能判断当前是帧到达还是请求完成。参数上,事件组位宽通常 24 位可用,别把业务事件也塞进同一组高位,后面排查会变成黑匣子。

2.3 建立第一个可编译工程:宏、串口参数和任务栈

第一次编译不要直接开主从双角色,先把从站单独跑通。工程里需要确认三个宏:从站地址、串口波特率、寄存器区大小。从站地址范围 1 到 247,0 是广播地址,248 以上保留;调试阶段用 1 就行,别用 247 这种靠近边界的值,有些上位机对边界地址处理不一致。串口参数要和调试工具完全一致,Modbus RTU 常见 9600 8E1 或 115200 8N1,校验位错一位,现象就是所有帧 CRC 都错,容易误判成协议栈问题。

/* 从站初始化参数示例,函数名以工程头文件为准 */ #define MB_SLAVE_ADDR 1 #define MB_UART_BAUDRATE 115200 #define MB_REG_HOLDING_NUM 64 #define MB_REG_INPUT_NUM 32 static void ModbusSlaveTask(void *pvParameters) { eMBErrorCode eStatus; /* 初始化从站:地址、端口、波特率、校验方式 */ eStatus = eMBInit(MB_RTU, MB_SLAVE_ADDR, 0, MB_UART_BAUDRATE, MB_PAR_NONE); configASSERT(eStatus == MB_ENOERR); /* 使能协议栈,内部会打开串口和定时器 */ eStatus = eMBEnable(); configASSERT(eStatus == MB_ENOERR); for (;;) { /* 轮询协议栈,内部处理接收、执行和发送 */ (void)eMBPoll(); /* 没有事件时让出 CPU,时间按波特率调整 */ vTaskDelay(pdMS_TO_TICKS(1)); } }

这段初始化代码的关键在eMBInit的参数顺序和校验方式。MB_RTU表示 RTU 模式,MB_SLAVE_ADDR必须和上位机扫描地址一致,MB_PAR_NONE对应 8N1;如果调试工具设了偶校验,这里要改为MB_PAR_EVEN,否则从站会丢弃所有帧。eMBPoll不是越频繁越好,115200 下 1 毫秒轮询一次足够,太低会丢帧,太高会白占 CPU。任务栈别按默认 128 字就上,协议栈内部有局部缓冲和回调调用,STM32H743 上我一般给 512 字以上,堆不够时先看FreeRTOSConfig.h的configTOTAL_HEAP_SIZE。

3. 从站先落地:寄存器回调、轮询任务与 3.5T 定时怎么配

从站能不能被 Modbus Poll 稳定读到,取决于三件事:寄存器回调返回的字节序对不对、eMBPoll是否及时被调度、3.5T 定时是否把帧边界切准。FreeModbus 的从站状态机在收到字节后等静默时间,静默时间到了才认为一帧结束;如果定时器被高优先级任务打断太久,状态机会把一帧拆成两帧,现象是上位机偶尔超时、偶尔 CRC 错,看上去很玄学。先把从站单独跑稳,再开主站,这是血泪经验。

3.1 寄存器区设计与回调函数的字节序

FreeModbus 的回调接口按功能码分,保持寄存器、输入寄存器、线圈、离散输入各有一个回调。应用层不要直接把业务结构体指针丢进去,建议单独开一块uint16_t数组做协议映射区,业务任务周期性地把数据刷进去。这样做的理由是字节序和寄存器数量都可控,主站读到的 32 位变量不会因为结构体对齐出现高低字颠倒。STM32 是小端,Modbus 寄存器传输也是高字节在前,回调里用usRegHoldingBuf[usAddress]直接返回即可,但如果你在应用层拼过 32 位数据,要确认高低字顺序。

/* 保持寄存器映射区,地址 0 开始,长度 64 */ static uint16_t usRegHoldingBuf[MB_REG_HOLDING_NUM]; eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; USHORT usRegIndex = usAddress - 1; /* 协议地址从 1 开始,数组从 0 开始 */ if ((usRegIndex + usNRegs) > MB_REG_HOLDING_NUM) { return MB_ENOREG; /* 地址越界,返回非法数据地址 */ } if (eMode == MB_REG_READ) { for (i = 0; i < usNRegs; i++) { *pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[usRegIndex + i] >> 8); *pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[usRegIndex + i] & 0xFF); } } else { for (i = 0; i < usNRegs; i++) { usRegHoldingBuf[usRegIndex + i] = (uint16_t)((pucRegBuffer[0] << 8) | pucRegBuffer[1]); pucRegBuffer += 2; } } return MB_ENOERR; }

这段回调的逻辑是读模式把寄存器拆成高字节、低字节依次填进 PDU,写模式把两个字节拼回一个 16 位寄存器。usAddress是协议地址,通常从 1 开始,数组下标要减 1;usNRegs是一次请求的寄存器数量,功能码 03 和 06 的差别就在这里。返回MB_ENOREG会触发异常码 02,调试时如果上位机收到非法数据地址,先查映射区长度和起始地址,不要急着怀疑 CRC。写回调里没有做范围保护之外的业务校验,实际项目要在应用层判断数值是否越界,否则主站写什么从站都收。

3.2 从站任务与 eMBPoll 的调度节奏

从站任务不需要很复杂,一个循环加eMBPoll就能跑,但任务优先级要低于串口中断服务,高于低速业务任务。eMBPoll内部会检查事件标志,收到完整帧后执行回调并组发送帧;如果任务被长时间阻塞,主站等不到响应就会超时重发,重发帧叠在旧帧上,现场看起来像通信不稳定。常见做法是把从站任务优先级设到中等,比如比日志任务高、比控制环低,轮询间隔 1 到 5 毫秒。115200 下 5 毫秒可能丢帧,9600 下 5 毫秒问题不大,别一套参数打天下。

/* 从站任务创建示例,优先级和栈按工程调整 */ #define MB_SLAVE_TASK_PRIO 4 #define MB_SLAVE_TASK_STACK 512 void ModbusSlaveStart(void) { BaseType_t xRet; xRet = xTaskCreate(ModbusSlaveTask, "mb_slave", MB_SLAVE_TASK_STACK, NULL, MB_SLAVE_TASK_PRIO, NULL); configASSERT(xRet == pdPASS); }

这段创建代码只做一件事:把从站任务挂到调度器。MB_SLAVE_TASK_PRIO要结合系统里其他任务定,不能只看协议栈;如果串口中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY,中断里调 FreeRTOS API 会直接翻车。栈大小 512 字是经验值,协议栈里如果有浮点打印或大局部数组,要往上加。创建失败先查堆,xPortGetFreeHeapSize比猜有用。

3.3 3.5T 定时与串口中断的配合

3.5T 是 RTU 帧边界,不是固定 3.5 毫秒。波特率 9600 时一个字符约 1.04 毫秒,3.5T 约 3.65 毫秒;115200 时约 0.3 毫秒。用 FreeRTOS 的 tick 做这个定时,1 毫秒 tick 下 115200 根本切不准,所以端口层要用硬件定时器。常见做法是串口每收到一个字节就重置定时器计数,定时器溢出中断里置EV_FRAME_RECEIVED事件;主站侧请求超时用另一个软件定时或 tick 计数,别和帧间隔共用比较寄存器。

/* 定时器中断里置帧结束事件,伪代码按 HAL 库风格写 */ void TIM3_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_UPDATE); /* 3.5T 到,通知从站任务一帧结束 */ xEventGroupSetBitsFromISR(xModbusEvent, EV_FRAME_RECEIVED, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }

这段中断代码的逻辑是定时器溢出后清标志、置事件位,再从 ISR 触发一次任务切换。xEventGroupSetBitsFromISR不能在中断里直接调用普通版本,必须用 FromISR 版本并传xHigherPriorityTaskWoken。定时器预分频要按 APB 时钟算,STM32H743 的定时器时钟不一定等于系统主频,拿 CubeMX 生成的配置对照最稳。串口接收中断里每收一个字节重置定时器计数,这个重置动作要短,别在中断里做协议解析,否则高波特率下丢字节。

4. 主站再发力:阻塞/非阻塞请求模型与超时参数

主站和从站的最大区别是主动发起请求,所以请求模型直接决定应用层好不好写。资源里的主站支持阻塞和非阻塞两种模式,阻塞模式在调用处等结果,适合初始化阶段读参数;非阻塞模式把请求挂到队列后立刻返回,适合周期轮询多个从站。超时时间不能拍脑袋填 1000 毫秒,按波特率和帧长算:115200 下读 10 个寄存器,请求帧 8 字节、响应帧 25 字节左右,再加从站处理时间,200 毫秒足够;9600 下就要放到 500 毫秒以上。主站超时太短会频繁重发,太长会拖住整个轮询周期。

4.1 主站状态机与请求队列

主站状态机通常分空闲、发送请求、等待响应、处理响应、错误重试几个状态。应用层调用请求接口后,协议栈把请求放进队列,主站任务在eMBMasterPoll里推进状态机。队列深度决定能同时挂多少个请求,但 Modbus RTU 是半双工,同一串口同一时刻只能有一个请求在途,所以队列深度 4 到 8 足够,再多只是缓存,不会提高吞吐。请求结构里要带从站地址、功能码、起始寄存器、数量、超时时间和回调,回调里拿响应数据,别在协议栈内部直接操作业务全局变量。

请求模式调用行为适用场景注意点
阻塞调用后等完成或超时初始化读参数、少量关键寄存器别在低优先级任务里长等
非阻塞入队后立即返回周期轮询多从站回调里别做耗时操作
带重试超时后自动重发现场干扰较大重试次数别超过 3 次
只写不读发完即完成下发设定值仍要检查异常响应

这张表里的“带重试”不是越多越好。Modbus 从站如果已经收到写命令但响应丢了,主站重发写命令可能重复执行,业务上要做幂等。资源里的主站接口开放了自定义超时时间,正好用来区分读请求和写请求:读可以短超时快重试,写要给足从站处理时间,重试次数降到 1 次。

4.2 阻塞与非阻塞调用的封装

应用层不要到处直接调协议栈接口,包一层自己的函数,把从站地址、超时和回调统一起来。下面示例按常见主站接口风格写,实际函数名以工程头文件为准。阻塞封装里用信号量等完成,非阻塞封装里传回调。

/* 主站读保持寄存器:阻塞模式封装 */ eMBErrorCode MasterReadHoldingBlocking(uint8_t slaveAddr, uint16_t regAddr, uint16_t regNum, uint16_t *outBuf, uint32_t timeoutMs) { eMBErrorCode eStatus; SemaphoreHandle_t xDone = xSemaphoreCreateBinary(); if (xDone == NULL) { return MB_ENORES; } /* 发起读请求,回调里释放信号量并拷贝数据 */ eStatus = eMBMasterReqReadHoldingRegister(slaveAddr, regAddr, regNum, timeoutMs); if (eStatus == MB_ENOERR) { if (xSemaphoreTake(xDone, pdMS_TO_TICKS(timeoutMs + 50)) != pdTRUE) { eStatus = MB_ETIMEDOUT; } } vSemaphoreDelete(xDone); return eStatus; }

这段封装的逻辑是把异步请求包成同步调用:创建二值信号量,发请求,等信号量,超时返回。timeoutMs同时传给协议栈和信号量等待,信号量多给 50 毫秒是留给任务调度的余量,别给太多,否则应用层超时和协议栈超时对不上。outBuf的拷贝要在回调里完成,示例省略了回调注册,实际工程里把回调挂到请求结构上。创建信号量失败返回MB_ENORES,这个错误码在不同版本里可能叫法不同,以头文件为准。

/* 非阻塞请求:入队后立即返回,结果走回调 */ eMBErrorCode MasterReadHoldingAsync(uint8_t slaveAddr, uint16_t regAddr, uint16_t regNum, uint32_t timeoutMs, MasterDoneCallback cb) { /* 请求结构由协议栈或应用层池化管理,避免动态分配 */ MasterRequest_t req; req.slaveAddr = slaveAddr; req.regAddr = regAddr; req.regNum = regNum; req.timeoutMs = timeoutMs; req.cb = cb; return eMBMasterReqReadHoldingRegisterAsync(&req); }

非阻塞封装的重点是请求结构生命周期。上面用栈上变量只是示意,真正入队后栈变量就失效了,常见做法是静态请求池或内存池,回调执行完再回收。timeoutMs在非阻塞模式下由协议栈内部计时,应用层不要再用vTaskDelay等结果。回调里只做数据拷贝和事件置位,别在里面调用会阻塞的 Flash 写或串口打印,否则主站状态机被拖住,下一个请求发不出去。

4.3 主从同栈时的串口互斥与地址规划

主站和从站跑在同一块板子上,串口只有一个时,必须做互斥。常见做法是加一把递归互斥锁,从站发送响应前拿锁,主站发送请求前也拿锁;拿不到就等,不能直接丢帧。更稳的做法是主从分时:从站响应优先,主站请求在总线空闲窗口发起。地址规划也要提前做,本机从站地址不能和主站要轮询的下游从站地址重复,否则总线上会出现两个相同地址的设备,现象是偶尔响应、偶尔异常,查起来很费劲。

/* 串口发送前的互斥,伪代码 */ SemaphoreHandle_t xUartMutex; bool ModbusUartLock(uint32_t timeoutMs) { return xSemaphoreTakeRecursive(xUartMutex, pdMS_TO_TICKS(timeoutMs)) == pdTRUE; } void ModbusUartUnlock(void) { xSemaphoreGiveRecursive(xUartMutex); }

这段互斥代码用递归锁,原因是主站发送函数内部可能再调用端口层发送,普通二值信号量会自己把自己锁死。timeoutMs给 20 到 50 毫秒,太长会让主站请求排队,太短会在从站响应时频繁失败。锁的创建要在协议栈初始化之前,别等任务跑起来才创建。主从同栈时还要注意串口接收中断里的帧归属:地址匹配本机就从站处理,不匹配且主站有请求在途就交给主站,这个判断放在端口层比放在应用层干净。

5. 避坑与排查:主从同栈最容易翻车的五处细节

主从同栈的坑,多数不是协议栈核心算法错,而是端口层和任务层没对齐。下面五条是调试时最常见的翻车点,每条按现象、原因、解决写,照着排查能省不少时间。

5.1 串口与定时器层面的坑

现象一:Modbus Poll 扫描时偶尔能读到数据,但连续读十次错三次,CRC 错误和超时交替出现。原因通常是 3.5T 定时被高优先级任务打断,或者串口接收中断里清了错误标志却没重新使能接收。解决方法是把帧间隔定时器中断优先级设到高于协议任务但低于系统关键中断,接收中断里先读数据寄存器再清标志,出错时重新打开接收。115200 下不要用 1 毫秒 tick 做 3.5T,这是最常见的翻车点。

现象二:主站发请求后从站有响应,但主站收到的数据整体偏移一个字节。原因多半是串口发送完成判断用了 TXE 而不是 TC,最后一个字节还没移出移位寄存器就切换了收发方向,或者 RS485 方向控制引脚翻转太早。解决方法是发送等待 TC 标志,方向引脚在 TC 中断后再切回接收;如果没有方向引脚,检查串口是否配置成半双工。这个坑在 9600 下不明显,115200 下必现。

5.2 协议与任务层面的坑

现象三:从站地址 1 能读,地址 2 读不到,但用调试工具单独测地址 2 又正常。原因通常是主站轮询表里地址和请求结构体没刷新,或者从站回调里用全局变量存地址,多个请求并发时被覆盖。解决方法是每个请求结构独立保存从站地址,主站队列里的请求在完成前不要复用;从站回调只依赖传入参数,不读全局当前地址。

现象四:主站非阻塞请求发出去后回调不执行,任务看着还在跑。原因可能是回调里调用了阻塞 API,或者请求结构是栈上变量,入队后已经失效。解决方法是回调只做拷贝和置事件,请求结构用静态池;检查eMBMasterPoll是否被低优先级任务饿死,主站任务优先级不要低于日志和显示任务。

现象五:主从同栈运行时,从站响应偶尔变成异常码 04 或主站超时。原因是串口互斥没覆盖完整帧,或者主站请求和从站响应在总线上撞车。解决方法是发送整帧期间持锁,接收中断按地址分流;主站轮询周期里留出从站响应窗口,别把主站请求排得太密。地址规划上,本机从站地址和下游设备地址必须唯一,这一点在系统联调前就要画表确认。

注意:调试工具里的校验位、停止位、从站地址必须和固件完全一致,任何一项不一致都会表现成协议栈问题。

6. 进阶验证:用寄存器扫描表把链路压到稳定

从站能读、主站能发,不等于链路稳定。我习惯做一张寄存器扫描表,把每个从站地址、功能码、起始地址、数量、预期值列出来,用主站非阻塞接口按表轮询,连续跑 30 分钟看错误计数。扫描表不要只扫 0 到 9 这种小地址,要覆盖边界:起始地址 1、结束地址等于映射区长度、跨区请求、数量为 1 和最大数量。每次错误记录时间戳、从站地址、功能码和异常码,连续错误超过 3 次就停下来查端口层,别让重试把日志刷爆。

扫描项请求示例观察点期望
单寄存器读地址 1,03,寄存器 1,数量 1响应时间、CRC稳定小于 50 毫秒
连续寄存器读地址 1,03,寄存器 1,数量 10字节序、总长度高低字正确
边界地址地址 1,03,寄存器 64,数量 1异常码越界返回 02
写单寄存器地址 1,06,寄存器 10回读值写入后一致
广播写地址 0,06从站是否执行不回复但生效

错误注入也值得做:把校验位临时改错、把从站地址改成不存在的值、把超时调到 50 毫秒,看主站是否按预期重试和报错。下面这个统计结构放在主站任务里,每轮打印一次,比肉眼看日志靠谱。

typedef struct { uint32_t totalReq; uint32_t timeoutCnt; uint32_t crcErrCnt; uint32_t exceptionCnt; } ModbusStat_t; static ModbusStat_t g_stat; void MasterStatRecord(eMBErrorCode eStatus) { g_stat.totalReq++; if (eStatus == MB_ETIMEDOUT) { g_stat.timeoutCnt++; } else if (eStatus == MB_EIO) { g_stat.crcErrCnt++; } else if (eStatus == MB_EXCEPTION) { g_stat.exceptionCnt++; } }

这段统计代码只做计数,不参与协议决策。MB_EIO在不同端口层可能对应 CRC 错或帧错,按实际错误码调整;每 1000 次请求打印一次,观察timeoutCnt是否随从站数量增加而上升。从那以后我每次移植 FreeModbus 都先跑从站扫描表,再开主站轮询,最后做错误注入,顺序反了就会把端口层问题误判成协议栈问题。希望帮到你。

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

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

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

立即咨询