尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32移植FreeModbus教程:Modbus RTU从机实现与调试经验

STM32移植FreeModbus教程:Modbus RTU从机实现与调试经验 简介面向STM32F407嵌入式开发者的FreeModbus从机移植参考工程基于HAL库与MDK5环境解决工业Modbus RTU通信中从机协议栈落地问题适用于工业自动化、设备数据采集等场景。压缩包共687个文件其中C源码384个、H头文件155个覆盖协议栈核心、驱动与用户应用层同时包含启动汇编文件.s、IAR/Keil工程配置.ewp/.uvprojx、链接脚本.icf及可直接烧录的HEX固件整体6.36MB工程结构清晰便于检索。已有1932人学习使用。资料提供了完整的HAL库串口初始化、中断回调处理、寄存器映射与报文解析框架并保留MDK工程目录和编译脚本可直接对照移植或裁剪复用同时附带工程说明与启动脚本方便按需调整外设配置适合正在搭建Modbus从机节点的工程师快速上手。 搞嵌入式开发的朋友只要接触过工业控制基本都绕不开Modbus。就算你没直接写过程序只要和PLC、触摸屏、传感器、工控机打过交道大概率也听说过这个协议。我这次在正点原子探索者STM32F407开发板上用HAL库完整把freemodbus协议栈的从机程序跑通了上位机通过串口调试助手模拟Modbus主站读写寄存器、控制IO都验证了一遍。整个过程从下载协议栈源码、Cubemx建工程、底层驱动对接到调试排错前后折腾了两天。这篇文章就把我的移植路径、踩过的坑和最终能直接抄作业的代码结构完整记录下来希望给需要在STM32上快速实现Modbus RTU从机的朋友一条更顺的路。这个项目解决的问题很简单你想让MCU作为Modbus从机响应上位机或触摸屏的请求读写参数、控制IO、上报数据。与其自己从零搓一整套状态机去处理RTU帧解析、CRC校验、功能码分发、异常应答不如直接把freemodbus移植过来。它把这些脏活累活全干完了你只需要把串口、定时器这两个底层接好然后实现寄存器的读写回调就行。适合有一定STM32基础、想在半天内把Modbus从机功能落地的开发者。零基础的朋友跟着做也行但建议先熟悉一下Cubemx创建工程和HAL库串口中断的基本用法不然上手会有点吃力。1. 先从整体设计说起为什么选F407HAL库freemodbus1.1 硬件平台选择的理由主控选STM32F407有两个很现实的原因。第一手头这块探索者开发板性能足够168MHz主频跑个Modbus从机纯属大材小用但后续你还可能要加模拟量采集、PWM输出、屏幕显示、多路串口F407的余量对这种以后还要加功能的项目来说非常舒服。第二F407的外设资源充足USART多随便拿一个出来做RS485都方便定时器也多单独分一个做Modbus帧间隔判断一点都不心疼。HAL库是我这次特意选的。网上很多freemodbus移植教程还停留在标准外设库代码直接操作寄存器看起来很高深但可维护性真的差。HAL库虽然代码冗余大一些运行效率也不如寄存器操作那么极致但开发速度快配合STM32CubeMX可以图形化配置时钟、串口、GPIO出错的概率低很多。Modbus从机这种业务本身对实时性的要求没那么夸张HAL库那点性能损耗完全在可接受范围内。我的观点是项目周期紧、功能杂、后续要迭代就用HAL库如果你在做一个对RAM、Flash和时序极度苛刻的小批量产品再考虑标准库或直接寄存器操作。1.2 freemodbus协议栈选型分析freemodbus其实就是一套开源的Modbus协议栈从机、主机都支持我这次用的是从机版本。网上能搜到的freemodbus版本不少最经典的是老外写的那份后面国内开发者维护了一份在gitee上有镜像功能差不多使用起来差别也不大。选它不选自己写协议处理是因为它内部已经把Modbus RTU帧的收解、CRC16校验、1/2/3/4/5/6/15/16等功能码的分发、异常应答、广播帧处理这些都做好了状态机也相当成熟我在项目里直接用它省掉的开发量不是一点半点。协议栈的基本运行模型是串口中断里一个字节一个字节收数据定时器负责判断帧与帧之间的时间间隔主循环里不断轮询eMBPoll()去处理收到的完整帧并生成应答。也就是说主循环不能死等或者长时间阻塞否则应答会超时。理解了这套模型后面写底层驱动就清楚多了一个字节收进来该干什么、定时器溢出该干什么、主循环该干什么。1.3 移植前需要理清的整体架构移植前一定要先把整个程序分层想明白不然代码会越改越乱。我的分层是这样的最底层是STM32的HAL库外设驱动负责串口收发字节、定时器中断、GPIO控制再往上是freemodbus的移植层主要是port文件包括串口收发函数portserial.c、定时器函数porttimer.c、配置头文件port.h最上层是用户应用包括寄存器数据区的定义以及freemodbus回调函数eMBRegHoldingCB这些。这样分层有个好处协议栈内部实现基本不用动你只需要把port层的接口函数都和STM32的HAL库驱动对接好。以后换平台了比如换成STM32F103或者GD32只需要重写port层那三四个文件上层应用和协议栈逻辑原封不动移植成本非常低。这也是freemodbus最值得用的地方。2. Cubemx工程配底层时钟、串口、定时器一步到位2.1 工程创建与时钟配置我的工程是用STM32CubeMX生成的MCU型号选STM32F407VGT6开发板上的晶振是8MHz所以RCC的HSE选Crystal/Ceramic Resonator。时钟树的配置这里要特别讲清楚因为后面串口波特率、定时器周期都依赖这个时钟一旦配错通信必然出问题。开发板的V2/V3版本硬件有差异但时钟电路基本一样系统主频我配的是168MHz这也是F407比较典型的主频。在CubeMX的Clock Configuration里把HCLK填168让工具自动分配各个总线的时钟APB1外设时钟默认是42MHz但APB1定时器时钟是84MHzAPB2外设时钟是84MHzAPB2定时器时钟是168MHz。这个细节其实很多人会忽略但后面的定时器周期计算全靠它记住定时器时钟不等于总线外设时钟Cubemx会帮你把定时器时钟倍频上去。调试接口选SWD只用到PA13和PA14两个引脚省下JTAG那几个引脚干别的用。生成工程的时候记得在Project Manager里勾选“Copy only the necessary library files”不然HAL库全量拷贝会把工程撑得很大。还有一个容易踩的坑Cubemx生成的工程默认会开启HAL_Init和SystemClock_Config这些函数但如果你改了外部晶振或者时钟频率一定要重新生成后再检查SystemClock_Config里的RCC配置是否和你的板子匹配我遇到过一次用外部25MHz晶振的板子按8MHz配置最后串口波特率完全不对排查了很久。2.2 串口参数配置与RS485方向控制串口我选择USART2通过板上自带的RS485芯片转成差分信号默认波特率最稳的是9600后面调试通过后也可以往上调115200。参数选8位数据、无校验、1位停止位也就是常见的8N1这符合Modbus RTU的默认习惯。需要注意的是Modbus RTU是半双工通信RS485收发切换必须通过方向引脚控制探索者开发板上RS485方向控制引脚是PB12发送时候拉高接收时候拉低。在CubeMX里把PB12配为GPIO_Output初始电平设低让它默认处于接收状态。串口中断也要在CubeMX里开好USART2的全局中断NVIC置能中断优先级我一般设到串口比定时器高后面再说为什么。这里强调一点HAL库的串口接收中断不要用HAL_UART_Receive_IT一次性接收一整帧因为Modbus帧长不定你不知道什么时候该收完。推荐的做法是启动单字节接收中断收进来一个字节交给协议栈再用定时器去判断帧是否结束。这是freemodbus移植里非常关键的一个设计思路千万别用类似DMA空闲中断去整个接收后面会在调试部分详细解释为什么我最后放弃了这套听起来更高级的方案。2.3 定时器配置3.5字符时间是怎么算出来的Modbus RTU协议规定两个字节之间间隔超过3.5个字符时间就认为一帧结束。3.5个字符时间怎么算一个字符在串口中实际传输的位数大概包括1位起始位、8位数据位、1位停止位也就是至少10位一般按11位估算。9600波特率下1位的时长是1/9600秒约104.17us一个字符约1.146ms3.5个字符就是约4.01ms。所以如果你的帧间间隔超过4ms从机就认为当前帧结束了如果帧中间某个字节间隔超过这个时间主机就会把一帧拆成两帧从机解析自然出错。freemodbus的porttimer.c里机制是配置一个定时器中断作为时间基准在中断里递减一个计数器计数器减到0就触发帧结束事件。定时器中断周期的选择我的经验是让中断周期不大于3.5字符时间的一半。9600波特率下T35约4ms用1ms中断就够但如果你用115200T35只有约0.33ms1ms中断就太粗了需要把定时器中断周期调到0.1ms左右。所以在项目里我把波特率和定时器参数的关系做了一个计算而不是直接用默认值。我这里TIM2挂在APB1上定时器时钟84MHz配置预分频器839得到100kHz计数频率再设置自动重载值99就是1ms中断一次T35_TICKS设4。这是比较标准的一套配法。3. 移植核心port文件、串口驱动、定时器驱动、寄存器回调3.1 源码工程接入与port文件说明从网上下载freemodbus源码后里面有一个demo目录里面是现成的移植示例但这个示例是基于标准外设库的不能直接用只能参考。需要加入工程的源文件包括mb.c、mbcrc.c、mbfunc.c、mbrtu.c、mbproto.c这些都在freemodbus的根目录下另外还有portevent.c、portserial.c、porttimer.c这几个port层文件通常我们自己重写。头文件路径要把freemodbus根目录、port目录都加进去。这里我想特别说一下很多人移植不成功就是因为他把demo里的port文件原封不动拿来用结果里面的寄存器操作和HAL库冲突或者引脚不对。我的做法是port文件自己写只保留函数接口函数内部全部用HAL库实现。freemodbus需要的接口其实不多串口部分就是初始化、接收一个字节、发送一个字节、查询是否发送中定时器部分就是初始化、启动T35定时、关闭T35定时、定时中断处理。port.h里主要定义寄存器数量、定时器tick数、波特率等宏。把这些接口接好协议栈就跑起来了没那么多神神秘秘的东西。3.2 串口底层用HAL库怎么接在portserial.c里xMBPortSerialInit是串口初始化函数本质上就是调用一次HAL_UART_Receive_IT(huart2, ucRcvChar, 1)启动单字节接收。这里注意HAL库的HAL_UART_Receive_IT是“收一个字节进中断”的函数但它在回调里需要重新启动才能继续收下一个字节所以你的HAL_UART_RxCpltCallback回调里要这么写先把收到的字节通过xMBPortSerialRxByte交给协议栈然后立即再次调用HAL_UART_Receive_IT接收下一个字节。这样才能保证Modbus帧里的每个字节都能被顺序收进来。发送方向我一开始很傻地在发送函数里用HAL_UART_Transmit阻塞发送结果整个程序在发送期间卡死了接收也停了后来改成HAL_UART_Transmit_IT中断发送。freemodbus在准备回帧后会调用xMBPortSerialPutByte逐字节发送这个函数在HAL库下可以用一个简单的环形缓冲配合中断方式实现也可以更直接一点由于freemodbus发送时是先把整帧放到发送缓冲区里所以我直接把xMBPortSerialPutByte实现成把字节写入一个fifo然后在串口的发送完成中断里不断从fifo取下一个字节发出去直到fifo空。发送过程中要记得把RS485方向引脚拉高等最后一字节发送完成中断里再把引脚拉低回到接收状态。另外HAL_UART_Transmit_IT发送完成会调用HAL_UART_TxCpltCallback在这个回调里调用prvvUARTTxReadyISR()告知协议栈“当前字节发送完成可以发下一个”。这套机制对HAL库来说完全够用实际跑起来很稳。3.3 定时器底层这个tick别拍脑袋乱设porttimer.c的移植相对简单但要理解它的工作方式。xMBPortTimersInit是初始化定时器我这里是初始化TIM2并配置好中断但注意初始化后不要把定时器一直开着freemodbus只会在收到第一个字节时才调用vMBPortTimersEnable开启T35定时器收到下一个字节时会再次把它重置并开启当帧间隔超过T35时定时器溢出在中断里调用prvvTIMERExpiredISR()通知协议栈帧接收结束。我实际遇到的一个问题是T35_TICKS这个值如果设得太小比如9600波特率下你设成1即1ms判断帧结束那么主机在发送一个连续帧时哪怕字节间隔只有0.5ms从机也会误判帧已经结束把这帧拆断开。设得太大则相反两帧之间明明已经收了完一帧从机还在傻等后续字节应答延迟大主站容易超时报错。所以这个值一定要根据波特率算准而不是网上教程里随便抄一个几。我的建议是先按上面说的公式算出T35再根据你定时器中断周期两者相除取整。9600配1ms中断就是4115200配0.1ms中断就是4正好一致这个巧合很多人没注意到容易抄错。3.4 寄存器回调函数的实现与地址映射freemodbus暴露给用户的接口核心是几个回调函数分别是eMBRegHoldingCB、eMBRegInputCB、eMBRegCoilsCB和eMBRegDiscreteCB。对应Modbus的保持寄存器、输入寄存器、线圈和离散输入。一般项目最常用的是保持寄存器我直接在程序里定义一个数组usHoldingReg[64]然后在回调里根据地址读写这个数组。这里要注意Modbus协议里寄存器地址是从0开始的而用户看到的地址是40001开头中间差1。也就是说主站发03功能码读地址0x00对应的是usHoldingReg[0]对应Modbus地址40001别搞混。还有个大坑是字节序。Modbus RTU传输寄存器数据时高字节在前低字节在后。你在eMBRegHoldingCB里从pucRegBuffer取数据、往pucRegBuffer存数据时必须自己处理高低字节的拼装。freemodbus不会帮你做这个它只是把原始字节给你。我最初就是没处理好这个上位机读回来的寄存器数据全是大小端反的调试了快一个小时才发现。正确写法是写入寄存器时usHoldingReg[usRegIndex] (uint16_t)(pucRegBuffer[0] 8) | pucRegBuffer[1]; 读出去时pucRegBuffer[0] usHoldingReg[usRegIndex] 8; pucRegBuffer[1] usHoldingReg[usRegIndex] 0xFF。这两个小操作基本决定了你寄存器数据能不能被上位机正确解析。4. 调试实录常见问题与排查技巧4.1 主站发一帧从机完全没回复先查这三层这个问题是新手最容易卡住的也是最值得逐层排查的。我的排查顺序是先用逻辑分析仪或者串口助手直接看RS485芯片的A/B差分信号确认主站发过来的帧有没有经过RS485转换然后用示波器看TXD引脚确认从机有没有发出任何数据最后再回到程序里检查有没有进入接收中断。排查下来绝大多数情况是三种原因一是RS485方向引脚接反或没控制导致芯片一直处于发送状态数据发不进来二是串口接收中断没有正确启动HAL_UART_Receive_IT只启动了一次收到一个字节后回调里忘记再次启动后面所有字节都丢光了三是波特率、校验位、停止位和主站不一致收到的全是乱码或者干脆进不了中断。我这次实际遇到的情况是RS485方向引脚用了PB12但CubeMX生成代码时默认把PB12配置成了输出高电平导致RS485芯片一直处于发送状态从机根本收不到数据。把PB12初始化改为输出低电平后通信立刻正常了。这个坑说起来很基础但真到排查的时候很多人第一反应是代码逻辑不对会绕很大弯子。所以我的建议是先把硬件方向控制逻辑装在心里想清楚当前应该是收还是发再去看代码。4.2 回复了但主站提示CRC错误或者收到的数据完全乱码能收到回复说明通路已经通了问题一般集中在字节序、波特率精度或定时器断帧时间上。CRC错误优先检查波特率误差如果外部晶振不是8MHz或者时钟树配置和实际晶振不一致会导致实际波特率和标称值偏差过大。在9600波特率下一点点偏差还能忍但如果偏差超过2%接收端就会频繁出错。我建议先用逻辑分析仪实测一帧的波形时长只要11位时间的实际值和理论值偏差在1%以内基本没问题。另一种CRC错误是很隐蔽的主站发完帧后从机回帧前有300到500微秒左右的延时这个延时其实是正常的但如果你在串口助手里看到的回帧前多了一个0x00或者0xFF多半是RS485方向切换的时候芯片收发切换瞬间产生了杂散信号被主站当成数据了。解决方法是发送完成后方向引脚拉低之前加一个短暂延时让最后一个停止位完整发送完毕再切换方向。另外也可以调用HAL库的HAL_UART_GetState确认发送空闲后再拉低方向引脚。回帧数据乱码的话重点检查T35_TICKS是不是太小导致从机把合法的一帧拆成两帧处理协议栈认为帧异常回的东西自然不对。4.3 寄存器读回来是0或者数据总是对不上这个我从一开始就说了十有八九是地址偏移和字节序搞错了。我遇到过这样一个场景上位机发03功能码读地址0x00长度0x0002也就是读保持寄存器40001和40002。如果我在eMBRegHoldingCB里把usRegAddress直接用成数组下标那么读到的是usHoldingReg[0]和usHoldingReg[1]看起来没错但如果上位机软件是从40001开始计数而你的地址偏移处理写成了usHoldingReg[usRegAddress - 1]那结果就差一位全乱套。所以首先要明确协议帧里的地址字段是0基的对应数组下标0。千万别再减1。数据对不上还有一个原因是寄存器数量配置。port.h里通常有一个定义比如REG_HOLDING_NREGS它决定了从机声明支持的保持寄存器数量。如果上位机一次读的长度超过了这个数量协议栈会返回异常响应界面上显示的可能就是读取失败或者数值为空。所以如果主站读多个寄存器读不出来先看看是不是你声明的寄存器数量不够。最后一次读长度超过125个寄存器是不允许的这是Modbus协议的硬限制设计寄存器地址规划的时候就要考虑到。4.4 常见问题速查表先存着再动手我把这段时间遇到的典型问题整理成一张表调试的时候可以直接对号入座故障现象可能原因排查/解决思路从机完全没响应RS485方向引脚电平不对确认初始化后默认处于接收状态发送完成后再切回接收从机完全没响应串口接收中断未重复启动HAL_UART_RxCpltCallback里收到字节后立刻再次HAL_UART_Receive_IT从机完全没响应波特率不一致用逻辑分析仪实测一帧位时长误差控制在1%以内能收到回复但CRC报错字节序处理错误检查寄存器回调里高低字节是否正确排列能收到回复但CRC报错从机把一帧拆成多帧增大T35_TICKS或减小定时器中断周期能收到回复但CRC报错RS485切换瞬间杂散信号最后一字节发送完成后延时再切换方向寄存器数据对不上地址偏移多减了1协议地址为0基对应数组下标0寄存器读不出来声明寄存器数量不足修改port.h里对应的NREGS宏115200波特率下通信不稳定时器1ms中断精度不够改用0.1ms中断或相应缩短定时器周期这张表我后来贴在了项目文档的首页每次调试新板子都会先过一遍省了不少事。还有一个心得串口调试助手一定要选支持Modbus RTU CRC校验计算的那种不光发帧方便自动解析应答帧也能直观看到协议栈返回的数据和异常码。我自己习惯用Modbus Poll这个软件来做主站模拟它的功能码、寄存器地址、长度、轮询周期都可以设置比用通用串口助手发十六进制数组方便太多。设置好轮询周期后可以看到数据实时刷新对验证寄存器读写特别直观。移植freemodbus这件事情说难吧协议栈已经帮你做完了大部分工作说简单吧底层驱动对接和调试过程中的细节确实能磨掉你不少耐心。我个人最大的体会是底层串口收发和定时器这两个环节一定要理解协议栈的运行机制再去写代码而不是照着别人工程的函数名去抄。你把数据流的逻辑理清了——字节进来触发中断、中断交给协议栈、定时器判断帧边界、主循环解析并回帧——整个移植过程就会变得非常顺。这个项目做完之后你也可以继续往里面加功能比如把保持寄存器换成EEPROM存储实现掉电保存或者加上多从机地址切换、把DMA接收和空闲中断用起来都是很好的扩展方向。到时候你会发现当初花在移植上的这些时间值了。本文还有配套的精品资源点击获取
返回列表