FreeRTOS下FreeModbus移植:STM32 RS485从站实现
发布时间:2026/9/3 20:02:52 作者:尧图编辑部 阅读量:1,286

简介这是一份面向嵌入式开发者的FreeModbus移植实战资料核心解决在FreeRTOS实时操作系统上集成FreeModbus协议栈、并实现与西门子组态屏通信的问题。资源包含50个文件其中35个C源码和15个头文件涵盖主库与从库的Modbus协议实现、串口移植、定时器移植、事件管理以及用户寄存器接口定义等模块压缩包仅110KB轻量紧凑便于直接阅读和二次开发。已有2326人学习下载。资料中完整呈现了FreeRTOS下FreeModbus移植的关键文件结构如portserial.c、porttimer.c、portevent.c以及modbus_user.c等并配套用户回调与寄存器映射示例能帮助读者快速理解RTOS任务、队列、信号量如何与Modbus协议栈衔接。对于正在学习嵌入式实时系统或需要将工业通信协议集成到项目中的开发者这份资源能提供清晰的移植路径和可参考的代码框架。 前段时间把一个带RS485接口的温控模块从裸机程序迁到FreeRTOS上顺手把原来的自写Modbus协议栈换成了FreeModbus。这次基于FreeRTOS的FreeModbus移植前后大概折腾了两个晚上踩了几个不深不浅的坑把思路和代码细节整理一下。如果你也是在STM32上跑FreeRTOS同时又需要给设备加一个Modbus RTU从站功能这篇东西应该能帮你省下不少排查时间。1. 移植前先想清楚FreeRTOS与FreeModbus怎么分工1.1 项目需求与选型思路我做的是一个小型温控设备MCU用的是STM32F103C8T6板子上带一个RS485接口需要把温度、湿度、加热器状态这些寄存器值上报给上位机组态软件。原来裸机时代写过一个简化版Modbus功能单一只能读保持寄存器CRC校验和超时处理都很粗糙。这次因为要接入多个传感器采集任务、LCD显示任务和按键任务决定上FreeRTOS协议栈干脆换成了FreeModbus。FreeModbus这个开源协议栈在工控圈子里用得很多最大优势是协议解析、异常码生成、CRC校验这些脏活累活都做好了留给我们的只有底层串口收发和定时器时基。它支持Modbus RTU、ASCII和TCP我这次只用RTU从机模式。选择它而不是自己继续造轮子理由是协议栈的可靠性直接关系到上位机通信是否顺畅自写协议又要处理帧边界又要处理异常帧验证成本太高。1.2 FreeRTOS和FreeModbus的分工逻辑很多第一次接触这两个东西组合的人会有一个疑问FreeModbus不是本来就带了自己的事件循环吗为什么还要塞进FreeRTOS里其实FreeModbus的eMBPoll函数确实是一个不断轮询的状态机裸机上可以在主循环里调用但放在FreeRTOS里之后这个轮询就被封装成一个独立任务。我的理解是FreeRTOS负责提供多任务调度、任务间通信和中断延迟处理而FreeModbus主要负责串口字节流的状态机解析。两者之间通过事件来交互。串口收到一帧完整数据后底层ISR通知Modbus任务去调用eMBPoll协议栈内部会去读取缓冲区里的数据、校验CRC、解析功能码、回调用户寄存器读写函数最后组织响应帧再由串口发送出去。这种结构的最大好处是Modbus通信不会阻塞其他任务温度采集、显示刷新照常运行。另一个好处是Modbus从机地址、波特率、校验方式这些参数可以在运行时通过eMBInit重新配置非常灵活。2. 源码结构拆解真正要改的其实只有port层2.1 官方包里需要拿哪些文件FreeModbus源码目录看起来有点多但真正需要加入工程的文件其实很少。以官方1.6版本为例我整理了一份文件清单照着加到MDK或者STM32CubeIDE工程里就行文件目录需要加入的文件作用demo/…/portport.h、portevent.c、portserial.c、porttimer.c底层移植文件需要自己改写includemb.h、mbconfig.h、mbproto.h等协议栈对外接口和配置rtumbrtu.c、mbrtu.hRTU模式封装functionsmbfunccoils.c、mbfuncdisc.c、mbfuncholding.c、mbfuncinput.c功能码处理tcp不需要RTU模式不需要—如果你是直接从官方demo工程里复制需要注意demo工程里有些stm32示例代码是配合老标准外设库写的和现在的HAL库不兼容。我建议只保留协议栈核心目录port层四个文件根据自己工程重写别直接搬demo里的实现。2.2 port层每个文件的职责port.h主要定义数据类型的重映射比如BOOLEAN、UCHAR、USHORT这些FreeModbus内部不使用标准C的类型而是通过port.h映射到具体平台。STM32上直接typedef成unsigned char、unsigned short、unsigned long即可。这里有个容易忽略的点USHORT必须定义为16位无符号整型因为Modbus寄存器就是16位宽度定义成int会出大问题。portserial.c是串口收发底层需要实现串口初始化、使能接收/发送、接收中断处理、发送完成中断处理这几个函数。我这次用HAL库的UART DMA功能重写了这个文件。porttimer.c是实现Modbus RTU帧超时判断的关键协议栈会根据这个时基判断一帧数据是否结束。典型做法是用一个硬件定时器产生50us的Tick中断。portevent.c是事件机制裸机版本通常用标志位实现在FreeRTOS里可以直接用任务通知或者事件标志组实现。我偷懒用了最简单的方式在中断里置一个全局变量Modbus任务轮询时判断这个变量。虽然不够优雅但实际运行很稳。2.3 应用层需要关心的三个接口真正需要应用层调用的接口不多eMBInit负责初始化协议栈参数eMBEnable启动从机eMBPoll是轮询入口。再加上几个寄存器读写回调函数比如eMBRegHoldingCB对应03/06/16功能码的保持寄存器操作。不过eMBInit内部会在portserial和porttimer里注册中断所以底层实现的函数名前缀必须和协议栈预期一致否则编译后会链接不上。3. STM32F103上的完整移植实操3.1 CubeMX配置工程我这次用STM32CubeMX生成基础工程勾选了FreeRTOS串口1设置为异步模式使能USART1全局中断。RS485方向控制引脚选了一个普通GPIOPA8。定时器方面我选TIM3作为FreeModbus时基配置为内部时钟定时周期50us。CubeMX里有两个设置直接影响移植成败。一个是在NVIC设置中把USART1全局中断优先级设置为5TIM3中断优先级设置为6两个都必须低于FreeRTOS可管理中断上限。另一个是FreeRTOS的堆大小我设了8KB默认的4KB在设备和串口任务多的时候容易不够。CubeMX生成的代码会自动创建FreeRTOS的默认start任务我后面会把Modbus任务挂到同一个线程里或单独再建一个任务。注意CubeMX生成的HAL时间基准默认使用SysTick而FreeRTOS也需要SysTick两者会冲突。建议在CubeMX的SYS设置里把Timebase Source改成TIM6或者TIM7否则系统一跑起来就进HardFault。3.2 串口DMA收发实现FreeModbus的RTU模式本质上是一字节一字节喂给协议栈的但底层接收可以用DMA来降低CPU占用。我的做法是串口DMA接收空闲中断判断帧结束这样一帧Modbus报文到达后会一次性被搬到缓冲区再由空闲中断通知Modbus任务处理。初始化代码如下static void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 9600; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; HAL_UART_Init(huart1); HAL_UART_Receive_DMA(huart1, ModbusRxBuf, MODBUS_RX_BUF_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); }关键是最后一行的空闲中断使能。DMA工作在循环模式或正常模式都可以我推荐用正常模式。每次收到一帧数据后在空闲中断里读取DMA剩余计数算出实际接收长度然后停止DMA等待处理完再重新开启。串口中断处理函数这样写void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_DMAStop(huart1); uint16_t rxLen MODBUS_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); ModbusRxLen rxLen; ModbusRxFlag 1; vTaskNotifyGiveFromISR(ModbusTaskHandle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } HAL_UART_IRQHandler(huart1); }这里有几个细节。第一Modbus RTU的帧之间是连续发送的空闲中断能很精准地识别出一帧结束。第二DMA停止后必须清理状态否则下一次启动接收时会卡在busy状态。第三发送RS485方向切换不能在中断里反复拉高拉低我是在任务里根据发送完成标志来控制的。3.3 Modbus定时器时基的实现porttimer.c是帧超时判断的核心。FreeModbus内部用T35_50US这个宏决定定时器时基是否按50us计算但真正重要的是定时器中断周期配置。在9600波特率下3.5个字符时间约4.01ms协议栈靠定时器计数来判定一帧是否结束。时基越短帧边界判断越准但中断开销也越大。我的定时器初始化代码void vMBPortTimerInit(void) { htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz / 72 1MHz htim3.Init.Period 50 - 1; // 1MHz / 50 20kHz即50us周期 htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim3); HAL_TIM_Base_Start_IT(htim3); }定时器中断里只需要做一件事调用协议栈的超时处理函数让状态机推进。void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(htim3); }协议栈在定时器中断中会检查是否已经超过3.5字符时间如果超时且收到了非空数据就触发接收完成事件。这里的优先级很讲究定时器中断必须能打断串口中断吗不一定。但如果两者优先级配置不当可能导致帧结束事件丢失。我实际测试中是让串口中断优先级高于定时器中断这样接收缓慢时不丢字节。3.4 Modbus任务的创建和回调注册在FreeRTOS任务中创建Modbus任务void ModbusTask(void *argument) { eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE); eMBEnable(); for (;;) { eMBPoll(); if (ModbusRxFlag) { ModbusRxFlag 0; HAL_UART_Receive_DMA(huart1, ModbusRxBuf, MODBUS_RX_BUF_SIZE); } vTaskDelay(pdMS_TO_TICKS(1)); } }任务栈大小建议分配256 words以上因为eMBPoll内部会调用一些回调函数如果回调里处理浮点数栈需求会更大干脆给512 words运行时刻用栈高水位做监测。寄存器回调函数比较关键以保持寄存器为例eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { uint16_t regIndex usAddress - 1; if (eMode MB_REG_WRITE) { for (int i 0; i usNRegs; i) { holdingRegs[regIndex i] (USHORT)((pucRegBuffer[i * 2] 8) | pucRegBuffer[i * 2 1]); } } else { for (int i 0; i usNRegs; i) { pucRegBuffer[i * 2] (UCHAR)(holdingRegs[regIndex i] 8); pucRegBuffer[i * 2 1] (UCHAR)(holdingRegs[regIndex i] 0xFF); } } return MB_ENOERR; }注意寄存器的地址偏移。Modbus协议地址从1开始而C数组从0开始不减去1的话写一次寄存器就会越界。另外Modbus数据是大端序所以高低字节换算不能搞反。4. 移植调试中那些容易翻车的细节4.1 串口收到数据但eMBPoll没反应这个问题我一开始遇到过现象是上位机发送请求后设备完全无响应。排查发现底层DMA确实收到了数据但协议栈没有解析出完整帧。问题出在porttimer中断没有正常启动导致协议栈永远等不到帧结束事件。检查链路不复杂先看串口DMA有没有把数据搬进缓冲区再看定时器中断有没有进入最后在eMBPoll入口打一个断点确认Modbus任务有被调度。如果是用任务通知唤醒的方式还要确认通知值有没有被清除。还有一种低级错误是串口波特率配置错了。9600和19200肉眼难辨用逻辑分析仪看波形最直观。如果手头没有分析仪可以在串口中断里做一个字节计数器打印出来。4.2 FreeRTOS中断优先级导致HardFault这个坑基本每个人都会踩。FreeRTOS对中断安全管理很严格如果串口中断优先级比configMAX_SYSCALL_INTERRUPT_PRIORITY对应的优先级更高在中断里调用任务通知相关API就会触发断言或HardFault。我刚开始把串口中断优先级设成0也就是最高优先级结果一收到数据就死机。解决方法是打开FreeRTOSConfig.h把configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设为5然后让串口和定时器中断优先级都大于等于5。对于STM32F103来说数值越大优先级越低所以设成5或6是安全的。另外如果CubeMX生成的HAL时间基准和FreeRTOS共用SysTick也会出现系统卡死。我当时把HAL的Timebase Source改到TIM6后问题才消失。4.3 RS485方向切换时机不对RS485是半双工总线方向控制引脚必须在上位机请求到来前处于接收状态在设备准备响应前切换为发送状态。很多人直接在主库里把RS485方向控制放在发送函数前拉高发送完成后立刻拉低结果响应帧被自己截断或者对方收不到。我的做法是在任务里发送前拉高方向引脚调用HAL_UART_Transmit_DMA后等待DMA传输完成中断在完成中断里拉低方向引脚。这样方向切换和DMA完成时机是严格同步的。如果板子上的RS485芯片有自动方向切换功能会省很多事但大部分国产模块还是需要手动控制。4.4 调试工具与验证顺序协议栈移植完成后不要直接拿组态软件联调先用Modbus Poll或者Modscan这类主站模拟工具做最小验证。验证顺序按照功能码来先读保持寄存器03再读输入寄存器04最后测试写单个寄存器06和写多个寄存器16。每通过一个功能码再进入下一个能大幅缩小问题范围。Modbus Poll里能看到详细的收发日志包括报文和响应耗时对定位CRC错误、地址不匹配、寄存器地址偏移都有帮助。如果上位机返回超时先检查设备侧有没有收到请求帧再检查响应帧有没有发出去分两步排查不要上来就改协议栈配置。最后再说一个我个人的小习惯每次移植完FreeModbus我都会在eMBRegHoldingCB里故意写一个固定的测试寄存器比如地址0x10默认值0x55AA。先用Modbus Poll写入一个值再读回来确认整个收发链路都是通的。这样做的好处是能迅速区分是通信链路问题还是业务逻辑问题。还有一个建议刚上电时最好通过串口打印一行FreeModbus版本和从机地址方便后面现场联调。别小看这行日志很多次现场调试都是在没有仿真器的情况下靠它确认程序是否跑到了正确位置。本文还有配套的精品资源点击获取