
简介一份针对STM32F103VET6的Modbus RTU从机实现工程面向嵌入式开发者与工业自动化学习者解决在无专用485收发芯片条件下通过软件模拟收发使能完成RS485通信的常见问题同时兼顾CRC校验、寄存器映射与串口中断处理等核心环节。工程基于HAL库编写包含完整的寄存器管理、错误处理与多任务调度示例可直接导入Keil MDK编译调试有助于理解Modbus协议栈在STM32平台上的落地方式。资源共226个文件以c/h源码、o/axf编译输出、uvproj工程配置及map/lst链接映射文件为主压缩包仅5.49MB结构紧凑、目录分层清楚。已有3664人学习下载适合需要快速移植Modbus从机功能、排查RS485通信问题或参考外设驱动写法的开发者。1. 项目概述与方案选型1.1 你要解决的真实需求做嵌入式开发的朋友应该都有这种经历设备端采集了一堆传感器数据想送给上位机或者PLC显示、存储结果两边各说各话协议对不上联调的时候头皮发麻。Stm32实现Modbus协议从机程序解决的就是这个设备端作为数据提供方主动接受主机轮询的经典需求。我在实际项目中接手过好几台设备比如环境监控节点、小型PLC扩展模块、电机驱动器上位机清一色用Modbus RTU轮询。你只写业务逻辑和寄存器映射表就行。要不要用FreeModbus我的个人经验是能用现成开源库就不要自己造轮子尤其是CRC校验、异常码处理这种细碎但容易出错的环节。你从零撸一遍完整支持功能码的从机栈调试周期至少多一周而FreeModbus在稳定性上比你三天写的裸奔代码强太多。2. 开发环境与源码准备2.1 我的开发环境参考硬件平台我用的STM32F103C8T6就是最便宜的那块蓝板子主频72MHz串口1接RS485收发器PA9/PA10做收发引脚。开发环境是标准库V3.5配合Keil MDK 5。工程里没有用RTOS因为FreeModbus移植到裸机上非常顺畅不需要额外引入并发机制。MCU: STM32F103C8T6主频72MHz编译环境: Keil MDK 5 STM32标准库V3.5协议栈: FreeModbus v1.6上位机调试工具: Modbus Poll、Modbus Slave这里有个细节FreeModbus v1.6有个modbus.h里的宏定义需要改MB_ASCII_ENABLED默认是打开的我们用RTU的话直接置0。ASCII模式虽然和RTU并存了但增加了很多解析分支裸机上没必要开关掉能省不少flash空间。2.2 移植前必须理清的源码结构FreeModbus的源码目录大家都有个初印象但真正移植时就只关心三个文件夹demo\STM32\ - 官方给的STM32裸机移植示例 port\ - 串口、定时器、底层IO的移植接口 mb\ - 协议栈内部实现mb文件夹里的核心文件大概有这么几个mbfunc.c功能码处理的分发入口mbrtu.cRTU模式收发状态机mb.c协议栈初始化和主循环调用入口mbutils.cCRC校验和地址校验你是不是想说那我拿到手应该从哪个文件看起我觉得不要先钻代码细节先把跑通的流程搭起来port文件夹三个接口文件搞定demo里的示例工程只要硬件改对串口号基本就能跑起来。协议栈内部的细节后续再慢慢看。3. 移植关键步骤与核心代码3.1 三个必改的接口文件FreeModbus做得最爽的一点是它把硬件相关的部分全部隔离在port文件夹里你只需要处理三个文件portserial.c串口收发字节的底层驱动porttimer.c3.5个字符时间间隔定时器portevent.c事件传递、或者配合操作系统的信号量我们裸机环境下portevent.c其实不用大改关键是前两个。portserial.c的重写思路协议栈要求发送一帧数据时底层串口要能一气呵成把数据发完接收的时候每来一个字节都要立刻转交给协议栈解析。我直接操作标准库的USART1中断在中断里调用xMBPortSerialPutByte或xMBPortSerialGetByte。这里踩过一个坑就是串口终端接收处理要在中断里尽量快不能在中断里做耗时运算否则接收超时计时器会错乱。一个关键点RS485收发方向切换。发送一帧之前把DE拉高最后一个字节发完后拉低DE重新回到接收状态。方向切换的时机一定要准早了会把发送尾部字节切掉晚了会吞掉主机下一帧的开头。我的做法是在串口发送完成中断TC中断里拉低DE比用延时稳妥得多。void USART1_IRQHandler(void) { // 接收中断 if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); xMBPortSerialPutByte((CHAR)byte); // 喂给协议栈 } // 发送完成中断此处做485方向切换 if (USART_GetITStatus(USART1, USART_IT_TC) ! RESET) { RS485_DE_LOW(); // 拉低发送使能回到接收态 USART_ClearITPendingBit(USART1, USART_IT_TC); } }3.2 定时器参数怎么算Modbus RTU规定了一帧的起始和结束是靠静默时间来判断的这个静默时间是3.5个字符时间。FreeModbus的porttimer.c里需要设置一个周期中断用这个中断来累加计时。波特率9600时1个字符是11位起始位1 数据8 停止位1 可能是校验位那么1个字符时间是 11 / 9600 ≈ 1.146ms3.5个字符时间就是4.01ms。你在定时器中断里做计数到达这个值就认为一帧接收结束了。具体做法用TIM4做1ms的基础定时中断在中断里调用一次vMBPortTimerPoll()。timer的装载值配置为72MHz / 1000 72000就是1ms一个周期。这样协议栈内部就能自己判断帧间隔了。有一个关键细节波特率越高3.5字符时间越短定时器精度要求也越高。比如115200波特率下3.5个字符时间只有0.334ms如果你的定时器是1ms节拍那误差就太大了。所以FreeModbus在porttimer.c里是支持微秒级精度的。我实测下来9600和19200波特率用1ms节拍没问题但上了57600以上最好把定时器周期改到0.1ms级别。3.3 主循环里必须轮询的两个函数裸机环境下FreeModbus不是全中断驱动的。主循环里需要周期调用vMBPoll()函数来处理协议栈的状态机。int main(void) { // ... 硬件初始化 ... eMBInit(MB_RTU, 0x01, 1, 9600, MB_PAR_NONE); eMBEnable(); while (1) { eMBPoll(); // 你自己的业务代码放这里 } }有人问我那串口收发的实时性怎么保证答案是中断负责把字节一个一个收进来协议栈的帧解析和拼包是由vMBPoll()来驱动的。在主循环里高频调用它解析及时性不受影响。实测在主循环里加点别的任务也没事只要别阻塞太久就行。4. 核心功能码与寄存器映射实现4.1 Modbus寄存器模型速记Modbus从机里面特别容易弄混的就是四种数据对象。我用个浅显的记忆方式线圈就是开关量寄存器就是数值量0x开头支持读写3x/4x一个只读一个读写。具体对应关系如下数据对象功能码(读)功能码(写)PLC地址操作类型线圈Coil0x010x05 / 0x0F00001-09999位读写离散输入Discrete Input0x02无10001-19999只读位输入寄存器Input Register0x04无30001-39999只读字保持寄存器Holding Register0x030x06 / 0x1040001-49999读写字FreeModbus里最常用的是保持寄存器。你把传感器数据放到保持寄存器里上位机用0x03读用0x06或0x10写配置参数一套下来很顺手。线圈和离散输入在IO控制场景下用得多一些。4.2 注册回调函数FreeModbus定义了一堆回调函数你必须在mbconfig.h里把对应的宏打开然后在C文件里实现。核心回调就下面这几个eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT regIndex usAddress - 40001; USHORT i; if (eMode MB_REG_READ) { for (i 0; i usNRegs; i) { *pucRegBuffer (UCHAR)(holdingRegs[regIndex i] 8); *pucRegBuffer (UCHAR)(holdingRegs[regIndex i] 0xFF); } } else { for (i 0; i usNRegs; i) { holdingRegs[regIndex i] (*pucRegBuffer 8) | *(pucRegBuffer 1); pucRegBuffer 2; } } return MB_ENOERR; }这里最关键的是地址映射别写错。上位机如果读40001那usAddress传入的就是40001你要减去40001得到数组下标0。曾见过有人直接把usAddress当数组下标用结果一读就数组越界。4.3 把传感器数据挂到寄存器上简单说下我的实际做法。我的设备有3路模拟量采集1个设备状态1个运行模式配置。那我就在main.c里定义这样一个数组uint16_t holdingRegs[20] {0}; uint16_t inputRegs[20] {0};在主循环里周期性把ADC采集结果写到inputRegs[0]、inputRegs[1]、inputRegs[2]设备状态和运行模式放在holdingRegs[0]和holdingRegs[1]。这样上位机读的方式就很直白了功能码0x04地址30001读到的是第一路ADC功能码0x03地址40001读到的是设备状态这个映射逻辑非常像你给每个数据起了一个门牌号。上位机不用关心MCU内部怎么实现的只要按照门牌号去取数据就行了。5. 常见问题与排查技巧实录5.1 通讯不上怎么办我刚开始用FreeModbus时也卡在这儿上电后Modbus Poll一连接全部超时。排查步骤我整理了一个固定套路照着走基本能定位确认参数一致性从机地址、波特率、校验位必须和主机完全一致。Modbus Poll的Setup菜单里改一下如果设备地址设成0x01Poll里也一定要填1。确认RS485方向控制这是最容易被忽略的地方。用示波器抓一下DE引脚电平变化如果发送期间DE一直是低电平那数据根本没发出去问题就在方向切换。确认电源共地RS485的A/B线之间必须有良好的共地否则干扰会很大。尤其两块板子用USB线各供电时共地问题特别突出。确认是收不到还是发不出把硬件改成自发自收测试串口调试助手发一帧看MCU能不能回。回说明串口通路正常不回就查协议栈初始化。我还试过一个更快的验证方法先用PC的USB转RS485直接连MCU用Modbus Slave模拟一个从机通过回环测试找出问题在主机侧还是从机侧。5.2 CRC错误频繁CRC报错分为两类一类是每一帧都报CRC错误另一类是偶发报错。前者是代码层面的问题后者大概率是硬件干扰。如果是每一帧都错先查usMBCRC16函数实现是否被正确移植再看串口波形。我喜欢直接看串口调试助手的HEX收发把收到的完整帧对着CRC计算器验算。一个常见的低级错误是串口停止位配置不一致主机设了2个停止位从机设了1个高波特率下就会偶发字节错位最后CRC几乎必挂。如果是偶发CRC错误先从硬件排查RS485总线的终端电阻120欧有没接对位置A/B线是否接反布线距离超过几十米时屏蔽层是否单端接地5.3 数制顺序和高低字节顺序我遇到过的另一个高频问题上位机读到的数值和MCU里看到的不一致比如预期0x1234结果读出0x3412。Modbus RTU规定寄存器是大端模式高位字节在前。在eMBRegHoldingCB回调里向缓冲区写入时先写高字节再写低字节如果写反了数值就会被字节交换。上位的Modbus Poll里也有Word Order这个选项可以选择High Word First或Low Word First但那是针对32位数据跨两个寄存器时才用它。内部寄存器数据是16位的高低字节顺序问题一定出现在从机的回调实现里。5.4 功能码不支持返回异常码FreeModbus在遇到不支持的功能码时会自动返回异常码0x01非法功能。这时候先查mbconfig.h里的宏有没有打开对应的功能码使能位#define MB_FUNC_HANDLERS_SUPPORTED // 总开关 #define MB_FUNC_READ_HOLDING_ENABLED // 0x03 #define MB_FUNC_WRITE_HOLDING_ENABLED // 0x06 #define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED // 0x10 #define MB_FUNC_READ_INPUT_ENABLED // 0x04 #define MB_FUNC_READ_COILS_ENABLED // 0x01刚开始移植时我图省事只开了0x03和0x04上位机要写参数时结果返回异常码。后来把需要的宏全部打开问题立刻解决。这个错误很隐蔽因为编译不报错就是把功能码静默地禁用了。6. 上位机联调实战与经验总结6.1 用Modbus Poll做从机测试的完整流程调试时我习惯用Modbus Poll当主机它的界面直观还能自动统计错误帧。联调流程大致下面这样在Connection里选择串口设备设置波特率9600、无校验、1个停止位。从机地址填1功能码选03 Holding Register。点击OK后如果从机程序正常界面里就能看到寄存器数值在刷新。如果想测试写功能双击某个寄存器填入数值用0x06功能码下发然后在MCU的断点里检查对应数组值有没有变化。第一次跑通时看到界面上的数据跟着ADC采集结果一起刷新那感觉真的非常爽。这款工具免费版就够用网上找安装包的时候认准官方渠道就行别随手装到捆绑软件。6.2 现在已经用得非常顺手STM32 FreeModbus这套组合我在三个项目里都验证过了从简单的温湿度采集节点到带多路IO控制的设备稳定性和开发效率都满意。如果你现在拿到的需求是给设备加Modbus从机通讯直接啃这个方案的可行性很高。它的这套回调机制加上自动CRC校验太适合我要快速实现通讯但不精通协议细节的场景了。等你跑通第一版有时间再深入读读协议栈的源码把RTU状态机的收包逻辑搞清楚对理解Modbus协议本身非常有帮助。最后再分享一个调试时让我少掉很多头发的小技巧在eMBPoll()返回后加一个空闲计数标志位总线上没有主机轮询时LED每隔一秒翻转一次。有了这个可视信号排查设备到底有没有进入正常工作状态时一眼就能判断不用每次怀疑人生。本文还有配套的精品资源点击获取