ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 Modbus主从站实战:从帧结构到状态机设计

STM32 Modbus主从站实战:从帧结构到状态机设计 简介面向STM32嵌入式开发者的Modbus主从站实现资源基于ARM Cortex-M平台覆盖RTU与TCP两种模式适合需要掌握工业通信协议栈开发与移植的工程师。zip压缩包仅12KB共十一个文件以C源码为主包含七个C文件、三个头文件以及一个文本配置文件是轻量级、便于阅读的参考工程。包内头文件与源文件分目录存放主站和从站分别提供RTU与TCP通信实例。源码涵盖串口收发、报文封装与解析、循环冗余校验、寄存器映射以及主从应答流程同时保留中断处理与异常处理逻辑可直接对照学习或迁移到实际项目对于正在调试传感器、可编程逻辑控制器等设备通信的开发者这份代码能帮助快速理解主站如何发起读写请求、从站如何响应查询并在此基础上进行功能定制。目前已有两千二百八十一人学习下载适合嵌入式入门者及需要快速集成Modbus协议的工程师参考。1. 从一次“仪表读数”需求说起为什么是Modbus我一向觉得做嵌入式这行最踏实的通信方式不是那些看着花哨的无线协议而是Modbus。前阵子一个设备改造项目需求很简单用STM32采集现场三个温湿度传感器的数据再汇总到触摸屏上显示。传感器和触摸屏都支持Modbus RTU主控板上就是一颗STM32F103串口资源刚好够用。方案定了之后我在手机上搜了一圈发现不少朋友都在问“STM32怎么做Modbus主站”“从站怎么移植FreeModbus”——这类问题我也折腾过好几轮今天就顺着这个项目把主站和从站的做法一次讲透。我知道不少刚接触的朋友第一个疑问是Modbus到底是个啥为什么工业上遍地都是它往简单了说Modbus是一个应用层报文协议它定义了一套“问-答”式的通讯规则。物理层你可以跑RS485、RS232也可以跑TCP/IP网络常见的RTU模式是把报文按照二进制格式压缩成紧凑的一帧直接塞进串口里发出去。它最大的优点就是极其简单、极其开放主流PLC、仪表、变频器、触摸屏基本都内置了Modbus从站功能这让它成了控制器之间沟通的“普通话”。这篇文章面向的读者是那些拿着STM32、正准备给板子接入Modbus通讯的朋友无论你是要做主站去采集设备数据还是要做从站去响应上位机指令看完都应该能理清整个套路。我会先讲清楚Modbus最核心的帧结构和寄存器模型再分别从主站和从站两个角度把我实际调试通过的工程思路、状态机设计、临界资源处理都摆出来最后把大家容易踩的坑集中盘点一遍。这一套逻辑不绑定具体库你可以拿着去改FreeModbus也可以自己手撸精简版甚至对照着写PLC端的代码思路都一样。2. Modbus协议最核心的机制先把这些吃透2.1 一帧Moodbus RTU报文长什么样Modbus RTU的帧结构并不复杂但每段都有讲究。标准格式是从站地址(1字节)、功能码(1字节)、数据区(N字节)、CRC16校验(2字节)。先说地址字段。它标识的是“这条消息是发给谁的”。地址范围1到247是有效从站地址0则被保留为广播地址。主站发广播时从站只接收不回复。正常情况下主站发一帧只有对应地址的从站才会应答如果帧里的CRC校验错误从站直接丢弃不回任何东西。我项目里的三个温湿度传感器地址分别设成了0x01、0x02、0x03这在从站固件里通过拨码开关或EEPROM配置即可。功能码是整个帧的灵魂。0x03表示读保持寄存器寄存器里的值可以由主站修改0x04表示读输入寄存器这类寄存器只读从站固件采集到的传感器数据一般就放这里0x06是写单个寄存器0x10是写多个寄存器。这个项目里主站只需要读传感器所以我只用到了0x04。如果你要控制变频器启停、设置仪表量程那就离不开0x06或0x10了。数据区则是实际读写的内容比如读寄存器的报文数据区就是“起始寄存器地址(2字节) 寄存器数量(2字节)”。CRC16校验算法是Modbus专用的多项式0xA001低字节在前发送。这块后面在代码里细说先记住这个字段专门用来保证“发送方没写错接收方没读错”。2.2 寄存器模型这个“地图”必须先建立搞Modbus最大的坎不是协议本身而是理解寄存器地址的映射关系。Modbus把设备的资源和状态分成四张“表格”线圈(Coil)位输出可读可写对应功能码0x01、0x05、0x0F离散输入(Discrete Input)位输入只读对应功能码0x02输入寄存器(Input Register)16位只读值对应功能码0x04保持寄存器(Holding Register)16位可读写值对应功能码0x03、0x06、0x10实际项目中从站设备的传感器值通常暴露在输入寄存器参数配置项暴露在保持寄存器。主站要读数据就去读3x或4x区要修改配置就去写4x区。调试的时候先用Modbus Poll、Modbus Slave这类调试工具扫一遍寄存器确认设备哪些地址有数据、哪些能写别一上来就盲写代码省得后面为了一个地址错位折腾半天。2.3 线缆、电平、波特率这些物理层细节Modbus RTU在STM32项目里绝大多数跑的是RS485物理层。RS485是差分信号抗干扰能力强最大传输距离能到1200米在工业现场远非TTL串口可比。硬件上需要一颗485收发芯片比如SP3485、MAX485单片机的UART_TX、UART_RX接到芯片的DI、RO还有一个DE/RE控制脚用来切换收发方向。波特率常见的是9600和19200传感器设备通常默认96008位数据位、无校验、1位停止位也就是8N1。有人为了追求速度把波特率拉到115200但线一长或者现场干扰大很容易出现CRC错误。工业现场稳定压倒一切我一般选9600除非设备明确支持更高的速率并且现场布线条件好。串口中断收到一帧后怎么判断“这一帧结束”Modbus规定两个字符之间的间隔不能超过3.5个字符时间超了就认为帧结束。以9600波特率计算一个字符大约是1.04ms3.5个字符就是3.64ms左右。所以从站收到一个字节开启个定时器约4ms后还没有新字节到来就判定接收完成开始解析。这个时间窗口后续在代码里会用到建议直接用定时器外设来计算别用软件延时否则中断一嵌套就乱套。3. 从站怎么落地手撸精简RTU从站比想象中简单3.1 从站设计的核心是“状态机 双缓冲”做从站本质上就是处理主站发来的请求然后回一帧正确应答。整个过程可以用状态机管理空闲(IDLE) → 接收中(RECEIVING) → 接收完成(COMPLETE) → 正在处理(PROCESSING) → 发送应答(SENDING)。串口每收到一个字节就进入接收中状态把字节存进接收缓冲区同时重置3.5字符定时器。定时器超时后进入接收完成状态此时解析缓冲区里的帧。校验CRC、检查从站地址、解析功能码、读写指定寄存器最后组应答帧放发送缓冲区打开发送使能串口发送完成后再关闭DE引脚回到空闲状态继续等待。这里有个嵌入式开发里绕不开的坑串口中断和主循环对同一块缓冲区的同时访问。中断往缓冲区写主循环从缓冲区读如果不同步很可能读到半截数据。我的习惯是定义两个缓冲区一个接收一个处理接收完一帧后把完整帧拷贝到处理区或者干脆用环形队列。简单做法是中断里只负责收和置标志位主循环看到标志位再复制和解析复制期间关闭串口接收中断。虽然会损失一点点实时性但在9600波特率下完全够用。3.2 寄存器区定义的三种常见姿势很多从站demo直接把寄存器定义成全局数组比如uint16_t holdingRegs[100]、uint16_t inputRegs[100]。读寄存器时就返回数组对应下标的值写寄存器时就直接修改数组。简单直接适合小项目。第二种姿势是用联合体把uint8_t bytes[2]和uint16_t word共用一块内存方便按字节和按字访问。这在处理高低字节顺序时特别好用。第三种是直接把寄存器映射到真实变量上比如转速、温度传感器原始值。读写函数里做变量和寄存器编号的switch-case映射。这种方式最灵活也最能看明白每个地址代表什么。我做的从站里就是这种方式地址0x0000对应温度传感器原始值0x0001对应湿度原始值0x0002对应设备状态字。这样写的代码可靠性高不容易出现“数组越界写飞了”的危险行为。3.3 CRC16计算与帧解析的代码骨架CRC计算在从站和主站通用可以直接复用。下面这版查表法效率很高放在中断或者主循环里都无压力#include stdint.h static const uint8_t auchCRCHi[] { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, // ... 完整查表需要256项 }; static const uint8_t auchCRCLo[] { 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, // ... 完整查表需要256项 }; uint16_t Modbus_CRC16(const uint8_t *pucFrame, uint16_t usLen) { uint8_t ucCRCHi 0xFF; uint8_t ucCRCLo 0xFF; uint8_t ucIndex; while (usLen--) { ucIndex ucCRCHi ^ (*pucFrame); ucCRCHi ucCRCLo ^ auchCRCHi[ucIndex]; ucCRCLo auchCRCLo[ucIndex]; } return (uint16_t)((uint16_t)ucCRCHi 8 | ucCRCLo); }帧解析的核心逻辑不复杂先确认收到的长度至少4字节地址功能码CRC2字节然后校验CRC不对就直接丢地址比对不上也丢地址对上了才按功能码分派处理。功能码的处理无非是读寄存器、写寄存器然后按照Modbus协议组织应答。碰到不支持的异常情况返回异常码0x01就是“非法功能码”0x02是“非法数据地址”0x03是“非法数据值”这几类异常码在调试工具里看到要能立刻反应过来。3.4 用Modbus Slave工具做从站自测代码写完强烈建议先用Modbus Slave软件配合一个USB转485模块自测。电脑通过串口助手连接STM32从站然后在Modbus Slave里配置从站地址、功能码手动发一帧请求看回复是否正确。如果你手头有那两个传感器也完全可以先用Modbus Poll让电脑仿真主站去测它们确认设备原始报文是什么样再回头写STM32的主站代码这样心里更有底。我每次写从站都这么干因为直接从站代码配合真实主站测出了问题很难定位是自己解析错了还是主站组帧错了。拆开来逐段验证是最高效的调试方式。之前帮我一个朋友排查从站不回复的问题结果查了半天是他USB转485的A、B线接反了这种低级错误用调试工具看波形一眼就能发现。4. 主站怎么构建轮询调度比从站更需要设计感4.1 主站的核心任务不是“发数据”是“管事务”到了主站这边逻辑的复杂度反而不在组帧上而在如何有序地处理多个从站、多个寄存器的读写。一个工业设备上往往挂着好几个Modbus从站每个从站又有好几个功能码请求要看比如温度、湿度、状态、报警。主站的任务就是按照预先规划好的节奏一帧一帧地把请求发出去然后处理每个从站的应答或者超时重试。土办法是每台设备开一组全局变量存数据主循环里挨个函数调用。缺点也很明显某个从站掉线主站苦苦等待超时后面的从站全部跟着被卡住。好的做法是设计一个简单的轮询调度表表里每一项表示一个请求任务目标从站地址、功能码、起始地址、寄存器数量、数据存储指针、超时时间。主循环只要不断扫描这个表挨个执行即可。4.2 非阻塞状态机发请求、等应答、判超时主站的收发模型比从站多一个“超时重试”的状态。我习惯的结构是这样的空闲(IDLE)时从调度表取下一个请求组帧、发送、切换到等待应答状态启动超时定时器等待应答(WAIT_RESP)时如果串口收到了完整一帧并且CRC正确进入解析状态如果超时定时器触发记录本次请求失败根据重试次数决定继续重试还是跳过这一站解析完成更新对应变量的值调度表索引前进回到空闲状态超时时间的设定有讲究。Modbus协议文档推荐主站等待应答的时间为1秒但实际项目里要看从站的处理速度。温湿度传感器响应很快100到200毫秒就够但一些老变频器可能要好几百毫秒。常规做法是设500ms连续重试3次仍无应答就认定该从站离线把对应数据区的“有效标志位”清零然后继续轮询下一条任务。只有这样才能保证一个设备掉线不会拖垮整个总线。状态机这部分用switch-case或者函数指针表都行。我之前的项目中用函数指针表把“发送请求”“处理应答”“处理超时”作为函数指针存起来遇到不同功能码调用不同处理函数这样加一个新的读操作非常快不用改主状态机的代码。4.3 调度表就是一个可配置的数据驱动核心下面给出一个我在项目中实际使用过的简易轮询调度结构思路可以参考typedef struct { uint8_t slaveAddr; uint8_t funcCode; uint16_t startAddr; uint16_t regCount; uint16_t *pDataBuf; uint16_t timeoutMs; uint8_t retryCount; } Modbus_PollItem_t; Modbus_PollItem_t pollTable[] { {0x01, 0x04, 0x0000, 2, (uint16_t *)sensor1_data, 200, 3}, {0x02, 0x04, 0x0000, 2, (uint16_t *)sensor2_data, 200, 3}, {0x03, 0x04, 0x0000, 2, (uint16_t *)sensor3_data, 200, 3}, };把这个表和主循环的状态机一结合主站框架就出来了。每条任务对应一台传感器的两个寄存器一次轮询下来三台设备的状态和数值就都更新到全局结构体里了。触摸屏通过另一个串口来读取这些全局结构体完全不会占用Modbus总线的节奏。这套思路扩展到工作场景里也成立设备数量变多就往表里加几行要支持写寄存器就再加一条功能码为0x06的任务项。4.4 主站调试工具推荐Modbus Poll是好帮手之前热词里反复出现“modbus poll(主站工具)使用简介”可见很多人都卡在这个工具的使用上。Modbus Poll本身是个强大的Modbus主站模拟软件它可以按你配置的功能码、地址、长度周期性地发送请求并显示从站返回的数据。你可以用它对单个传感器或者STM32从站做单点测试也可以让鼠标点击“报文日志”看每一帧收发细节。把Modbus Poll和串口监控工具如AccessPort、VSPD虚拟串口配合起来用是定位协议问题最舒服的姿势一边发帧一边看串口上的原始hex数据哪个字节不对一目了然。而且Modbus Poll还支持把接收到的原始报文直接复制成C数组我调试时经常把它粘贴到代码注释里当测试用例方便后面自动化测试。5. 实操过程中的坑和排查技巧实录5.1 从站不回复先查这三层这类问题的排查顺序我从上到下理一遍第一层硬件接线。USB转485的A接A、B接B检查485芯片DE/RE引脚有没有正确控制。很多人用MAX3485这类芯片DE和RE是共用一个引脚还是分开的芯片手册要看仔细。还有共地问题RS485虽然差分传输但不同节点之间还是建议拉一根地线否则共模电压过高会烧芯片或者直接通信异常。第二层参数一致性。从站的波特率、数据位、校验位、停止位必须和主站完全一致甚至一个字符超时时间的计算都要匹配。很多时候“设备不回包”不是地址错了而是两边校验位一个偶校验一个无校验数据一直错从站CRC不对就丢弃了。第三层逻辑和时序。确认从站有没有进入接收状态定时器是不是正确超时了。调试时在串口中断和状态机切换处打几个调试断点或者加个串口打印。看从站的接收缓冲区是不是收到了完整帧帧头帧尾对不对CRC是否匹配。如果缓冲区里连帧都没收全说明是物理层或方向控制的问题如果收完整了但CRC错那就是波特率或者校验位不对。5.2 发送方向切换DE引脚到底是开还是关RS485是半双工同一时刻只能A→B或B→A一个方向。STM32要发送数据时先把DE拉高让485芯片处于发送模式数据发送完毕再把DE拉低恢复接收模式。这里面有一个很经典的坑串口发送完毕不一定代表数据已经在总线上发送完尤其当波特率较低时。单片机UART寄存器位的“TXE”只是说数据进了移位寄存器不代表物理层已经发完。要做“发送完毕再拉低DE”的操作正确姿势是等“TCTransmission Complete”标志位置位或者在发送完成中断里再关DE。我就见过一个同学DE引脚用延时来控制关闭结果发完一帧紧接着主站又发下一帧因为DE还没完全切回接收导致自己把自己发出去的数据又读回来了整个通信一片混乱。我的建议发送完一帧后延时约1个字符时间再切换到接收态或者直接在USART的发送完成中断里关DE后者更稳。5.3 3.5字符超时到底用多少合适3.5T是Modbus协议里推荐的一个参考基准但在大多数单片机上直接按字节数来判定“接收完毕”并不可靠。因为串口有FIFO、DMA等机制中断延时会抖动直接“收够预计长度就结束”非常容易出错。更稳妥的做法是用一个16位的硬件定时器作为“帧间隔定时器”每次串口收到字节就重置计数值定时器溢出就认为是帧结束。9600波特率下定时器设置到5ms比3.5T宽裕一点既不会把下一帧的第一个字节算进来又能从容判定一帧结束。如果跑115200波特率则把超时控制在1ms左右。这个参数要跟主站侧保持一致否则主站发得快一点从站还卡在“上一帧没超时”的状态就一直处理不了新帧。5.4 通配若干问题速查表我在实际项目里整理过一个速查表基本覆盖了新手和中阶用户最常踩的坑现象可能原因排查方向从站完全不回复A/B接反、DE方向没控制、地址不匹配检查硬件接线示波器或逻辑分析仪看波形收到乱码或CRC错误波特率不一致、校验位不一致、干扰核对参数检查终端电阻和屏蔽层接地主站卡死等待应答超时重试逻辑没做一个失败拖死全部采用非阻塞状态机设置合理的超时时间和重试次数寄存器数值总是0xFFFF寄存器地址映射错误、器件未上电完成先用调试工具读取真实地址再修改映射485总线上数据冲突多个主站、DE控制逻辑有误保证同一时间只有一个发送方检查发送完毕标志偶尔丢帧但对端正常波特率偏高、抗干扰差、帧间隔太短降低波特率延长帧间隔定时器检查终端电阻5.5 一个实际项目的“坑”复盘最后说个我自己的真实案例。有一个项目STM32做主站通过RS485控制三台伺服驱动器。前面调试都很顺结果一上产线就出现偶发抖动频率不高但很烦。查了一圈发现伺服驱动器本身没异常用Modbus Poll单独点读也正常。后来用示波器挂在485总线上发现设备多、布线复杂之后总线上的反射信号很明显导致某些帧的CRC在特定时刻被噪声干扰破坏。解决的方案有三步一是在总线两端分别加上120欧终端电阻抑制反射二是把波特率从38400降回19200降低对时序的敏感度三是主站的帧间隔适当拉长给驱动器留足处理时间。改完之后跑了一整天一根CRC错误都没有。这类问题不遇到一次光看书是体会不到“现场”和“实验室”差别的。6. 这套思路能用到哪从毕业设计到工业设备很多同学问我“我把STM32的Modbus主站和从站都调通了下一步还能做什么”可能性其实非常多。如果从站配合传感器、执行器做一套完整的数据采集板就可以挂在触摸屏或者组态软件上变成一套小型SCADA系统。如果主站用来采集多台设备数据通过以太网上抛给上位机这就已经是工业物联网里的边缘采集网关雏形了。还有人拿FreeModbus直接移植到STM32上做从站再把寄存器区接到WiFi模块上做成一个“数据传输节点”也是可以的。关键是别停留在“把协议跑通”这个层面。当你真正理解了Modbus主站和从站的设计思路你会发现自己对“怎么管理多任务、怎么处理异步事件、怎么保证数据一致性”这些东西的理解都会上一个台阶。协议本身只是外壳里面的状态机设计、超时重试、错误处理、资源互斥才是嵌入式开发真正值钱的内功。我在实际项目中体会最深的是Modbus这种“老”协议之所以至今还统治着工业现场不是因为它多厉害而是因为它简单、可靠、透明。你的底层代码只要能经受住现场电磁干扰和长时间运行的压力这个项目的调通就只是个开始。本文还有配套的精品资源点击获取
返回列表