ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32与维控屏MODBUS RTU通信实战:从协议到代码

STM32与维控屏MODBUS RTU通信实战:从协议到代码 简介本资源是一套完整的STM32嵌入式项目实战代码包面向工业自动化领域初/中级开发者及高校电子类专业学生聚焦STM32通过MODBUS RTU协议与维控HMI屏实现双向通信的核心需求解决LED控制、实时变量显示与IO状态监控等典型工业人机交互问题。压缩包共307个文件含58个头文件h定义寄存器映射与协议结构、55个C源文件c实现FreeModbus移植、USART驱动、GPIO控制逻辑及中断响应机制辅以编译中间文件o/d/crf、Keil工程配置uvproj/uVopt/sct、位图资源bmp及可执行镜像axf/hex整体4.87MB结构完整、即拿即用。已有3941人学习下载提供从硬件接线逻辑、MODBUS帧解析、功能码0x05写线圈控制LED、0x03读保持寄存器上传变量、到CRC校验与超时重传等全流程实现代码注释详尽便于理解协议底层机制并快速适配同类HMI设备。 做嵌入式这么多年经常有人问起“STM32怎么跟触摸屏通信”。这类问题看着简单真做起来坑不少尤其是用MODBUS协议跟维控屏对接时不少人卡在协议帧格式、寄存器地址映射、CRC校验这几个坎上。今天我就把一套完整的STM32作为MODBUS从机、与维控屏通过串口通信的方案拆开来聊从协议细节到每一行关键代码都讲清楚。这个项目解决的是用一块STM32采集或控制现场数据通过MODBUS RTU协议挂在串行总线上维控屏作为主机主动读写STM32内部寄存器从而在屏幕上显示数据、下发参数。适合正在做工业控制、数据采集类项目或者刚接触MODBUS协议想找一套可复现代码的朋友参考。1. 整体设计思路为什么选MODBUS RTU与维控屏的组合1.1 协议选型的底层逻辑工业触摸屏HMI常见的通信协议有MODBUS、西门子PPI、三菱FX系列编程口、台达专用协议等。维控屏本身对MODBUS的支持非常成熟而MODBUS又分为RTU、ASCII、TCP三种模式。RS485串口场景下绝大多数工程选的都是RTU模式原因是RTU在相同波特率下传输效率比ASCII高一倍数据帧紧凑信息密度高。选用MODBUS RTU还有一个原因在于它的开放性协议帧格式公开任何MCU只要有一个UART外设加定时器就能实现不依赖特定厂商的协议栈。这在国产MCU、ST、GD、NXP等平台之间移植非常方便。正因为如此MODBUS成了工业设备之间通信的“通用语言”而维控屏天然支持这个语言两者搭配不需要中间转换层。如果你是要远距离传输比如几百米或者强干扰环境一般用RS485物理层A/B差分信号能有效抑制共模干扰。室内短距离设备间调试直接用TTL串口也行但项目最终落地到现场建议还是走RS485抗干扰能力和传输距离都更有保障。1.2 主从架构与数据流划分MODBUS协议规定总线上只能有一个主机Master其余都是从机Slave。维控屏的角色是主机它按照组态软件里设定的周期主动轮询从机STM32是从机不上主动发数据只能响应主机的请求。这个主从模型决定了代码里不需要写“主动上报”的逻辑只需要写好“收到请求后如何应答”。对很多习惯写串口主动收发的人来说这需要转换一下思维。举个例子你要在屏幕上显示一个温度值维控屏每隔100ms发一条“读保持寄存器”的请求帧STM32收到后把温度数据回给屏幕仅此而已。如果屏幕上的数字不刷新排查方向就是“请求有没有到”“响应有没有回”“数据值对不对”这三件事。实际数据流如下维控屏组态软件设置好寄存器地址运行时会周期发送请求帧STM32的串口接收中断把字节收进缓冲区然后用状态机解析出完整帧解析通过后根据功能码访问对应的寄存器映射区准备响应数据CRC校验后把响应帧发回给维控屏。2. MODBUS RTU协议核心细节功能码、数据帧结构与CRC16校验2.1 从数据帧说起地址、功能码与数据区怎么排MODBUS RTU的请求帧和响应帧格式非常紧凑一个标准请求帧包含从站地址1字节、功能码1字节、数据区N字节、CRC校验2字节。发送时CRC低字节在前高字节在后这是新手最容易搞反的地方。地址范围是1到2470是广播地址所有从机都要接收但不回复。项目里我一般给STM32设地址1维控屏里对应填1。功能码决定这条报文是干什么的03读保持寄存器、04读输入寄存器、06写单个保持寄存器、160x10写多个保持寄存器这4个在屏与MCU通信中用到的最频繁。像01/02/05/15这类线圈相关功能码因为维控屏开关量控件也可以用但实际项目中我用得少主要原因是线圈在MCU端需要额外维护位寻址表不如直接映射到寄存器数组里方便。举个例子维控屏发出“读保持寄存器”请求数据区是起始地址2字节高字节在前 寄存器数量2字节高字节在前。比如读地址0x0000开始的2个寄存器数据区就是00 00 00 02。STM32收到后响应帧数据区为字节数1字节寄存器数×2 寄存器值每个2字节高字节在前。2个寄存器就是4个字节数据区里第一个字节写0x04后面跟上寄存器值。2.2 CRC16-MODBUS校验按位计算与查表法两种实现CRC校验是MODBUS RTU里最核心的防错机制没有它现场干扰会导致数据错乱。MODBUS RTU使用的CRC16算法参数是多项式0x8005初值0xFFFF结果不异或。每条报文从站地址到数据区最后一个字节都要做CRC校验值低字节先发。网上一堆现成的CRC生成器但工程上必须在MCU里自己实现。按位计算适合不占内存的场合uint16_t ModbusCRC16(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }实测中我用的是查表法把256个CRC值提前算好存成const数组运行时每个字节只需查一次表速度快很多适合主频不高的STM32F103系列。代码很多网上随便都能搜到完整表这里不贴数组了核心就是uint16_t _crc16_update(uint16_t crc, uint8_t a) { return (crc 8) ^ crc_table[(crc ^ a) 0xFF]; }注意很多人在串口助手调试时发现帧末尾CRC不对大概率是字节顺序搞反了。发送时先发低字节再发高字节这一点务必确认。3. STM32端代码实现从串口接收状态机到响应帧处理3.1 串口参数与不定长数据接收方案STM32端串口配置成8数据位、1停止位、无校验波特率与维控屏保持一致常用9600或115200。我在项目里用的是HAL库主要是它能快速把外设初始化跑通状态机逻辑自己写不依赖CubeMX生成的成套代码。不定长接收是MODBUS从机的关键。HAL库提供了HAL_UARTEx_ReceiveToIdle_IT接口可以在串口空闲中断时触发回调一次收完一帧数据。但纯靠硬件空闲中断有个局限字节间隔如果不稳定可能把一个完整帧拆成两段。工业场景下更稳的方案是额外起一个定时器按3.5个字符时间判断帧结束。MODBUS RTU标准要求帧结束之后等待3.5个字符时间才算完整帧。9600波特率下一个字符约1ms3.5个字符就是约3.5ms。工程上我取整为5ms控制在合理范围。做法是每收到一个字节就把定时器计数清零重新开始计时如果定时器计到5ms没再收到新字节就判定一帧接收完成进入解析函数。接收缓冲区我用的是环形缓冲加帧完成标志uint8_t rx_buf[256]; volatile uint16_t rx_len 0; volatile uint8_t rx_complete 0; void UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buf[rx_len] recv_byte; timer_cnt 0; HAL_UART_Receive_IT(huart1, recv_byte, 1); } }每收一个字节重新使能接收中断同时刷新定时器。定时器中断里判断是否超过帧间隔则置rx_complete标记。3.2 从机核心状态机一条请求从接收到响应的完整流程收到完整帧后进入解析。从机逻辑其实就是一个有限状态机先校验从站地址再校验CRC再按功能码分发处理。很多新手一上来就判断功能码结果忽略了地址不匹配导致协议栈错乱。第一步对比帧首字节和站地址。如果地址对不上直接丢弃因为这条报文不是发给当前从机的。第二步全帧CRC校验。我一次性把整个帧含CRC喂进CRC16函数返回值应为0如果非0说明数据在传输过程中被干扰了。第三步根据功能码进入不同分支。伪代码如下void Modbus_ProcessFrame(void) { if (rx_buf[0] ! SLAVE_ADDR) { rx_len 0; return; } uint16_t crc_recv rx_buf[rx_len-1] 8 | rx_buf[rx_len-2]; uint16_t crc_calc ModbusCRC16(rx_buf, rx_len-2); if (crc_recv ! crc_calc) return; uint8_t func rx_buf[1]; switch (func) { case 0x03: Modbus_ReadHoldingRegisters(); break; case 0x06: Modbus_WriteSingleRegister(); break; case 0x10: Modbus_WriteMultiRegisters(); break; default: Modbus_SendException(0x01); } }每个功能码处理完后把响应帧写入发送缓冲区最后统一调用HAL_UART_Transmit发送。发送期间不建议被其他帧打断所以发送前后记得关全局中断或者用发送互斥锁。3.3 寄存器映射让数据区与项目变量对应起来从机最基础的数据结构就是一个寄存器数组。我一般定义一个大小为100的uint16_t数组regs[100]把它当作“数据映射区”每个元素对应一个寄存器的值。温度、湿度、状态标志、参数设定值等变量统一映射到数组下标维控屏那边再用“4x0001”这种地址访问。比如regs[0]放当前温度regs[1]放当前湿度regs[2]放设备运行状态regs[3]放启动/停止命令。读保持寄存器功能码请求地址0x0000数量2那响应就是regs[0]和regs[1]的值。写寄存器功能码则直接修改regs数组对应位置。字节序问题要特别留意。MODBUS协议规定16位寄存器值高字节在前大端序STM32内存里uint16_t是小端序存储所以在组装响应帧时要手动拆开先把高字节发出去再发低字节。我见过不少人在屏幕上读出来数字完全不对就是因为没有做字节交换。tx_buf[0] SLAVE_ADDR; tx_buf[1] 0x03; tx_buf[2] num_regs * 2; for (int i 0; i num_regs; i) { tx_buf[3 i*2] (regs[start_addr i] 8) 0xFF; tx_buf[4 i*2] regs[start_addr i] 0xFF; }4. 维控屏组态配置从新建工程到地址映射4.1 维控屏工程与串口参数设置维控屏的组态软件叫维控HMI组态编辑器新建工程时先选机型和屏幕背面铭牌的型号一致。然后双击工程树里的“串口设置”节点把通信参数改到和STM32一致比如波特率9600、数据位8、停止位1、无校验。协议选择MODBUS RTU。这里有个容易忽略的设定从站地址。屏幕作为主机去访问从机时要填目标从机地址。如果你把维控屏的站号设成了1而STM32代码里从机地址也是1看起来都填对了但在MODBUS里站号是总线上区分不同设备的身份二者必须分开。实际上维控屏作为主机是不需要从站地址的它只需要填写要访问的“远程从站地址”那个才是STM32的地址。4.2 画面控件绑定寄存器40001到底是什么维控屏的画面编辑里添加“数值显示”或“数据显示”控件后要绑定一个寄存器地址。MODBUS寄存器地址有不同前缀0x对应线圈1x对应离散输入3x对应输入寄存器4x对应保持寄存器。这里我们STM32实现的数组对应的是4x保持寄存器。屏幕显示上一般会写“40001”或“4x0001”这指的都是MODBUS 4x区第一个寄存器也就是偏移地址0x0000。所以你在维控屏的数值显示控件里填“40001”对应的就是STM32代码里功能码03读出来的第一个寄存器regs[0]。填“40002”就是regs[1]以此类推。数据格式在控件属性里也要设置。默认是16位无符号整数如果你要显示负温度值就要设置为16位有符号整数。如果数据是32位浮点比如温度带一位小数那要连续占用两个寄存器请务必把控件数据类型改为32位浮点并注意寄存器顺序是高字在前还是低字在前。维控屏一般可以在属性里选择“高位在前”或“低位在前”STM32端组装数据时按高字在前传屏幕那边也选高字在前两边保持一致。写参数下发时用“数值输入”控件地址同样填40001这一类维控屏运行时弹出的输入框会生成06或16写命令直接写进STM32的regs数组。5. 联调过程与常见问题排查实录5.1 联调失败先按这个顺序查坐标实测中90%的MODBUS通信故障都出在物理层和参数不一致上。我总结的排查顺序是先查线再查参数最后查协议。查线看的是RS485的A/B是否接反。不同厂家设备的A/B定义没有统一标准维控屏和你的RS485转接板接线端A/B很可能定义相反。最稳的办法是用万用表测出RS485芯片的A脚和B脚电压A对地电压通常比B高因为偏置电阻让A默认高电平。如果设备A/B反接通常表现是通信完全没有任何反应。参数排查用串口助手监听。把RS485转TTL模块接到电脑监看“维控屏发出的请求帧”和“STM32返回的响应帧”。串口助手里能直接看到01 03 00 00 00 02 C4 0B这样的HEX数据。如果维控屏在发请求但STM32没响应先核对地址、CRC、串口参数如果STM32有响应但维控屏不显示数据那多半是响应帧格式或地址映射有问题。5.2 常见问题速查表现象可能原因解决方法屏幕一直显示0或----地址映射错误核对4x地址偏移与实际寄存器下标是否对应通信完全无反应RS485 A/B接反交换A/B接线用万用表确认电压数据偶尔跳变电阻不匹配长线末端并联120欧终端电阻屏幕数值乱码字节序错误确认高字节在前还是低字节在前一写寄存器就重置帧被拆成两段处理使用字节间隔定时器合并帧上报值不对但寄存器值对控件数据类型错误修改屏幕控件为16位/32位/浮点对应类型5.3 几个让我折腾半夜的坑第一坑是串口调试助手收不到响应原因是STM32发送响应帧的时候还没等发送完成就进入了下一轮接收中断。RS485半双工模式必须切换方向建议你在发送前拉高RE/DE方向控制引脚发送完再拉低并且留出至少一个字符的转换时间。第二坑是维控屏请求周期太快STM32处理不过来。屏幕默认轮询周期是100ms但有时候写多个寄存器的命令发过来刚好碰上主循环里有耗时的阻塞任务响应就超时了。维控屏超时重试几次之后会报通信错误。解决办法是把MODBUS解析和响应放在中断里完成或者提高优先级也可以在屏幕端把轮询间隔调到200ms以上。第三坑是CRC计算表被编译器优化出错。查表法需要把static const数组初始化正确如果定义成局部变量每次函数调用都会重新初始化数组耗时又占栈空间。务必定义成全局const数组。第四坑是寄存器地址偏移量大时容易数错。当数据量超过100个寄存器时屏幕端地址是40001到40100但访问地址超过0x0063之后协议里起始地址和数量的组合就复杂了。建议设备寄存器不超过64个超过的话规划两张表分开管理否则排错维护成本极高。还有个细节STM32的USART发送函数HAL_UART_Transmit有超时机制默认1000ms。如果连续多次发送失败这个超时时间会被累积导致主循环卡顿。建议调用前先判断发送状态或者把超时改短到50ms。调试阶段强烈建议在串口助手里安装MODBUS调试器功能比如使用MODBUS Poll作为主机模拟维控屏或者使用MODBUS Slave模拟从机这样能先把两端分别测通再联调能省掉一半的排错时间。等主从两端各自都能跟PC软件通信正常了再接在一起问题定位会快很多。最后说一下代码的扩展方向。这套从机逻辑改一改就能变成TCP版本把串口收发换成socket收发包即可。维控屏本身也支持MODBUS TCP如果后期需要上位机或远程监控可以基于这套寄存器映射表做个网关把串口数据转换成TCP上报逻辑不用大改只替换传输层就行。记住寄存器映射表是核心只要表设计得清晰协议栈再怎么换都不慌。本文还有配套的精品资源点击获取
返回列表