☰
STM32F103裸机FreeModbus移植实战:从解压到稳定通信
2026/10/28 16:43:06 网站建设 项目流程

简介:这份资源面向嵌入式开发初学者与需要快速落地Modbus通信的工程师,提供FreeModbus协议栈在STM32F103裸机环境下的完整移植工程。内容涵盖Modbus RTU通信原理、STM32F103的UART外设配置、FreeModbus源码裁剪与中断适配,并附一份Word移植说明文档,讲解硬件连接、CubeMX配置、代码结构及读写寄存器示例,帮助读者理解协议栈与硬件如何对接。压缩包共257个文件,约6.97MB,以c源码、h头文件、o目标文件、d依赖文件及crf交叉引用文件为主,另含uvprojx工程、sct分散加载、hex固件与map映射文件,可直接用Keil打开编译调试。目前已有1243人学习下载,适合希望掌握FreeModbus裸机移植流程、排查串口通信问题并快速验证读写功能的开发者参考。

1. 从一份 STM32F103 裸机 FreeModbus 移植包说起:它能解决什么

如果你手上有 STM32F103 最小系统板,想让它作为 Modbus RTU 从站跟 PLC、触摸屏或上位机通信,又不想上 RTOS,那这份 FreeModbus 裸机移植包就是冲着你来的。它把 FreeModbus 协议栈塞进 STM32F103 的裸机环境,省掉了操作系统调度层,直接靠 SysTick 或定时器中断驱动协议状态机。适合谁?做工业采集模块、仪表从站、串口服务器原型的嵌入式工程师,尤其是那些用 Keil MDK 加 ST-Link 调试、板子跑 72MHz、串口接 485 芯片的典型场景。你拿到的是一个 .rar 包,里面是完整的工程目录,不是零散文件,解压就能用 Keil 打开编译。但别急着烧录,先搞清楚它怎么把 FreeModbus 的 RTU 模式跑在裸机上,以及哪些参数必须按你的硬件改。

2. FreeModbus 裸机移植的底层逻辑:为什么不用 RTOS 也能跑

2.1 协议栈与裸机定时器的配合方式

FreeModbus 本身设计成可移植的,它把硬件相关的部分抽象成 port 层,核心协议逻辑放在 mb.c、mbrtu.c 这些文件里。裸机移植的关键在于:RTU 模式需要 3.5 个字符时间的帧间隔来判断一帧结束,还需要 1.5 个字符时间的帧内超时。在 RTOS 下通常用任务延时或软件定时器,裸机就得靠一个硬件定时器,比如 TIM2 或 TIM3,产生固定 tick,然后在中断里递减协议栈内部的超时计数器。常见做法是配置定时器为 50us 中断一次,对应 20kHz,这样 9600 波特率下 1.5 个字符时间约 1.56ms,3.5 个字符时间约 3.65ms,都能被 50us 整除,误差可控。移植包里一般已经写好了 porttimer.c,你只需要确认定时器编号和中断优先级。

2.2 串口中断与收发状态机的衔接

裸机下没有阻塞式发送,必须用中断或 DMA。FreeModbus 的 portserial.c 里通常实现成发送空中断触发下一个字节,接收中断把字节塞进协议栈的缓冲区。STM32F103 的 USART1 挂在 APB2 上,72MHz 时钟,配置波特率时要算准 BRR 寄存器。移植包里一般用 USART1 或 USART2,你拿到后第一件事是看 portserial.c 里用的是哪个 USART,以及对应的 GPIO 和 485 方向控制引脚。如果板子上是 MAX485 芯片,还需要一个 GPIO 控制 DE/RE,发送时拉高,接收时拉低。这个方向切换的时机很关键,切早了会丢掉最后一个字节,切晚了会干扰总线。

2.3 移植包里的文件结构与关键文件

解压后你会看到类似这样的目录:

FreeModbus_STM32F103/ ├── FreeModbus/ │ ├── modbus/ │ │ ├── mb.c │ │ ├── mbrtu.c │ │ ├── mbascii.c │ │ └── ... │ └── port/ │ ├── port.c │ ├── portserial.c │ ├── porttimer.c │ └── portevent.c ├── STM32F10x_FWLib/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c │ └── ... └── Project.uvprojx

核心是 port 目录下的四个文件,它们把 FreeModbus 和 STM32 的硬件绑在一起。port.c 里放的是临界区进入退出、事件通知这些,裸机下通常用开关中断实现。portevent.c 负责把协议栈的事件投递到主循环。main.c 里会调用 eMBInit 和 eMBEnable,然后在 while(1) 里轮询 eMBPoll。你不需要改 modbus 目录下的核心文件,除非要裁剪功能码。

2.4 从站地址、波特率与校验方式的配置入口

这些参数在 main.c 里通过 eMBInit 传入。典型调用:

eMBInit( MB_RTU, 0x01, 0, 9600, MB_PAR_EVEN ); eMBEnable( ); while( 1 ) { eMBPoll( ); // 其他任务 }

第一个参数是模式,MB_RTU 表示 RTU;第二个是从站地址,0x01 到 0xF7 之间;第三个是端口号,裸机下通常忽略,填 0;第四个是波特率;第五个是校验,MB_PAR_EVEN 偶校验、MB_PAR_ODD 奇校验、MB_PAR_NONE 无校验。注意:如果你用无校验,RTU 模式下仍然需要两个停止位,STM32 的 USART 配置里要对应改。这些参数改完要重新编译,不能运行时改,除非你自己加个命令处理。

3. 动手移植:从解压到跑通第一个 Modbus 请求

3.1 环境准备与工程打开

你需要 Keil MDK,版本 5 以上,装好 STM32F1 的器件包。解压 .rar 后,双击 Project.uvprojx,Keil 会加载工程。先别急着编译,检查 Options for Target 里的器件型号是不是 STM32F103C8 或你板子对应的型号。如果是 C8T6,Flash 是 64KB,RAM 20KB,FreeModbus 裸机移植后代码大概占 20KB 左右,RAM 占用主要看缓冲区大小,默认 256 字节收发缓冲,够用。然后看 Debug 选项卡,选 ST-Link Debugger,Settings 里确认能识别到芯片。常见问题是 Keil 里没装 ST-Link 驱动,或者 SWD 引脚被复用,导致连不上。

3.2 串口与定时器硬件初始化检查

打开 portserial.c,找到 vMBPortSerialEnable 和 prvvUARTTxReadyISR 这些函数。确认它用的是哪个 USART,以及 GPIO 配置。以 USART1 为例,TX 是 PA9,RX 是 PA10,APB2 时钟要开。如果板子上用了 485 芯片,还会有一个 GPIO 比如 PA8 控制方向。检查 porttimer.c,看用的是 TIM2 还是 TIM3,中断优先级不能太高也不能太低,一般设成 1 或 2。如果定时器中断和串口中断优先级冲突,会导致帧间隔判断出错,表现为通信时好时坏。

3.3 编译下载与串口助手验证

编译整个工程,应该 0 Error 0 Warning。然后用 ST-Link 下载到板子。接线:USB 转 485 模块的 A 接板子 485 的 A,B 接 B,GND 对接。打开串口助手,选对 COM 口,波特率 9600,偶校验,8 数据位,1 停止位。发送一帧读保持寄存器的请求:

01 03 00 00 00 01 84 0A

这是从站地址 01,功能码 03,起始地址 0,读 1 个寄存器。如果移植成功,你会收到类似:

01 03 02 00 00 B8 44

返回的数据里 00 00 是寄存器值,具体值取决于你的寄存器映射。如果没反应,先看板子上的 LED 有没有闪,确认程序在跑。然后检查 485 方向控制引脚有没有在发送时拉高。用示波器看 USART TX 引脚有没有波形,是最直接的排查手段。

3.4 寄存器映射与回调函数的修改

FreeModbus 用回调函数处理读写请求。在 main.c 或单独的文件里,你会找到 eMBRegHoldingCB、eMBRegCoilsCB 这些函数。默认可能只返回错误码,你需要根据实际需求填充。比如:

eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode ) { if( eMode == MB_REG_READ ) { for( USHORT i = 0; i < usNRegs; i++ ) { USHORT usReg = usAddress + i; if( usReg < REG_HOLDING_NREGS ) { pucRegBuffer[2*i] = (UCHAR)(usHoldingRegs[usReg] >> 8); pucRegBuffer[2*i+1] = (UCHAR)(usHoldingRegs[usReg] & 0xFF); } else { return MB_ENOREG; } } } else if( eMode == MB_REG_WRITE ) { // 类似处理写入 } return MB_ENOERR; }

注意字节序:Modbus 是大端,高字节在前。usHoldingRegs 是你自己定义的数组,大小根据需求定。如果地址超出范围,返回 MB_ENOREG,协议栈会自动回异常码 02。这个回调函数在主循环里被 eMBPoll 调用,所以不要在里面做耗时操作,否则会丢帧。

4. 避坑与排查:裸机 FreeModbus 最容易翻车的五个点

4.1 现象:通信偶尔成功,大部分时间超时

原因:定时器中断优先级低于串口中断,导致帧间隔计时被串口接收中断打断,累计误差超过阈值。或者定时器中断里做了太多事,比如调用了 printf。解决:把定时器中断优先级设成比串口高,或者至少同组。中断服务函数里只做计数器递减和标志置位,其他事放到主循环。检查 porttimer.c 里的 TIM_IT_Config 和 NVIC 配置。

4.2 现象:发送完一帧后 485 总线一直被拉低,收不到回应

原因:485 方向控制引脚在发送完成后没有及时切回接收。常见是发送空中断里切了方向,但最后一个字节还没完全移出移位寄存器。解决:在 USART 的 TC 中断里切方向,而不是 TXE 中断。TC 表示发送完成,TXE 只表示数据寄存器空。STM32 的 USART 有 TC 标志,使能 TC 中断,在中断里拉低 DE。或者用 DMA 发送完成中断来切。

4.3 现象:编译报错 undefined symbol eMBRegHoldingCB

原因:回调函数没有实现,或者实现的名字和协议栈里声明的不一致。FreeModbus 默认在 mb.c 里用弱符号定义,但有些移植包去掉了弱符号,需要你自己实现。解决:在工程里搜索 eMBRegHoldingCB,看它在哪个文件里被引用,然后确保你实现的函数签名完全一致,包括参数类型和返回类型。注意 UCHAR 和 USHORT 是 FreeModbus 自己 typedef 的,不要改成 uint8_t。

4.4 现象:从站地址设为 0x00 后完全不响应

原因:Modbus 规范里 0x00 是广播地址,从站收到广播帧不回应。如果你想让从站回应,地址必须设在 1 到 247 之间。有些移植包的 eMBInit 里对地址范围没做检查,你传 0 进去它也不报错,但协议栈内部会当成广播处理。解决:把地址改成 0x01 或你实际需要的值。如果确实需要广播,那就不指望回应,只执行写操作。

4.5 现象:读多个寄存器时返回的数据错位

原因:回调函数里处理 usAddress 和 usNRegs 时,没有正确计算数组偏移,或者字节序搞反了。比如你定义的是 uint16_t 数组,但协议栈传进来的 pucRegBuffer 是 UCHAR 指针,每个寄存器占两个字节。解决:写个简单的测试,只读一个寄存器,确认高低字节顺序。然后再读连续多个,检查循环里的索引。注意 usAddress 是协议栈传进来的起始地址,不是数组下标,如果你的寄存器映射不是从 0 开始,要做偏移转换。

5. 进阶:把 FreeModbus 裸机移植包用出生产级稳定性

5.1 用 DMA 加空闲中断替代字节中断

字节中断在 115200 波特率下每 87us 进一次中断,CPU 开销不小。更稳的做法是串口接收用 DMA 加空闲中断,一次收完一帧再通知协议栈。但 FreeModbus 的 RTU 模式依赖 3.5 字符间隔判断帧结束,如果你用空闲中断,需要把协议栈的帧间隔检测关掉,改成由空闲中断触发帧完成事件。具体做法是修改 portserial.c,在空闲中断里调用 pxMBFrameCBByteReceived 的替代逻辑,或者直接调用协议栈内部的帧完成函数。这个改动需要看 mbrtu.c 里的状态机,确保不会重复触发。

5.2 用硬件 CRC 加速校验

STM32F103 没有硬件 CRC 外设,但可以用查表法。FreeModbus 默认的 CRC 是逐位计算,在 115200 下每帧多花几十微秒。如果你对实时性要求高,把 mbrtu.c 里的 usMBCRC16 换成查表实现,表可以放在 Flash 里,256 个 uint16_t 占 512 字节。查表法比逐位法快 5 到 8 倍。注意表要按多项式 0xA001 生成,低字节在前。

5.3 寄存器映射的工程化组织

不要把所有寄存器都塞在一个大数组里。按功能分结构体,然后用联合体或者宏做映射。比如:

typedef struct { uint16_t voltage; uint16_t current; uint16_t status; } SensorRegs; SensorRegs sensorRegs; #define REG_HOLDING_VOLTAGE 0x0000 #define REG_HOLDING_CURRENT 0x0001 #define REG_HOLDING_STATUS 0x0002

然后在回调函数里用 switch 或者地址判断来读写。这样代码可读性好,也方便后期加寄存器。注意结构体成员的地址要连续,否则 Modbus 的连续读会出错。可以用 #pragma pack(1) 或者手动保证对齐。

5.4 用条件编译裁剪功能码

FreeModbus 支持的功能码有 01、02、03、04、05、06、15、16。如果你的从站只需要读保持寄存器,可以把其他功能码的代码裁掉,减小 Flash 占用。在 mbconfig.h 里定义 MB_FUNC_READ_HOLDING_ENABLED 为 1,其他为 0。裁剪后编译,Flash 能省几 KB。但注意:如果上位机发了不支持的功能码,协议栈会回异常码 01,这是符合规范的,不用额外处理。

5.5 验证方法:用 Modbus Poll 做压力测试

自己写串口助手发帧只能验证基本功能。要验证稳定性,用 Modbus Poll 这个工具,设置好从站地址、功能码、起始地址和数量,然后开连续轮询,间隔设 10ms。跑半小时,看有没有超时或错误响应。同时用示波器看 485 总线的波形,确认帧间隔和电平正常。如果出现偶发错误,把轮询间隔加大到 50ms 再试,判断是协议栈问题还是总线冲突。我一般还会在从站里加一个错误计数器,每收到一帧错误就加一,通过某个寄存器暴露出来,方便远程诊断。

从那以后我每次拿到新的 Modbus 移植包,都先跑一遍这个压力测试,确认底层没问题再动应用层。希望帮到你。

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

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

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

立即咨询