尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32与威纶通屏Modbus RTU通讯实战:从协议解析到工业HMI应用

STM32与威纶通屏Modbus RTU通讯实战:从协议解析到工业HMI应用 1. 项目缘起当嵌入式大脑遇上工业触手最近在做一个工业现场的小型控制项目核心是一块STM32F103做主控需要实时显示十几个传感器的数据并且能通过屏幕下发几个简单的控制指令。选型的时候我直接跳过了传统的按键数码管方案也没用那种小尺寸的TFT屏自己折腾GUI而是选择了一款威纶通Weinview的MT8071iE工业触控屏。原因很简单在粉尘、震动、电磁干扰常见的车间环境里工业屏的可靠性、防护等级和成熟的组态软件能省去后期无数的维护麻烦。这个决定很正确但随之而来的核心问题就是如何让STM32这块“嵌入式大脑”和威纶通这块“工业触手”顺畅地对话这其实就是典型的嵌入式HMI人机界面应用场景。STM32负责底层逻辑控制、数据采集和运算而威纶通屏则负责友好的人机交互和数据可视化。两者之间的桥梁就是通讯协议。翻看威纶通的手册和STM32的例程你会发现选项不少Modbus RTU over RS485、Modbus TCP over Ethernet、甚至自由口协议。对于大多数中小型、对实时性要求不是极端苛刻的项目RS485总线Modbus RTU协议的组合几乎是性价比和可靠性的黄金标准。它硬件简单两根线就能组网传输距离远理论上可达1200米抗干扰能力强正好契合工业现场的需求。我这次的项目也不例外选择了这条最经典、最稳妥的路径。接下来的内容我会完整复盘从硬件接线、协议理解、STM32程序实现到威纶通画面组态的全过程。这不是一个简单的“点灯”教程而是会深入到帧结构解析、数据地址映射、异常处理以及那些手册里不会写的调试坑点。无论你是刚开始接触工控的嵌入式开发者还是想为STM32项目添加一个专业级交互界面的爱好者相信这些从实际项目中踩出来的经验都能让你少走弯路。2. 通讯基石深入理解Modbus RTU协议栈在动手接线和写代码之前我们必须对Modbus RTU协议有一个清晰的认识。很多人觉得它就是一串十六进制数照着格式发就行但真正理解其设计哲学才能在调试时快速定位问题。Modbus本质上是一种主从Master-Slave架构的问答协议。在我们的系统里威纶通触摸屏通常作为主站MasterSTM32作为从站Slave。屏负责发起所有的数据请求STM32则被动响应。这很重要它决定了程序的流程STM32的串口中断服务程序中核心任务是解析主站发来的命令帧并组织相应的应答帧发回去而不是主动上报数据除非用特殊功能码或自定义协议变体。一个完整的Modbus RTU帧由以下几部分组成从站地址1字节用于标识网络上的哪个设备应答。地址范围1-2470是广播地址从站不应答。你的STM32设备需要设置一个唯一的地址。功能码1字节告诉从站要做什么。最常用的几个0x03读保持寄存器Holding Register。这是最核心的功能威纶通屏读取STM32的数据如传感器值、状态主要用它。0x06写单个寄存器。屏向STM32写入一个控制命令或设定值。0x10写多个寄存器。批量写入数据时效率更高。数据区N字节根据功能码不同内容不同。对于读请求0x03这里包含起始寄存器地址和要读的寄存器数量对于写响应这里包含被写入的寄存器地址和值。CRC校验2字节循环冗余校验用于检查帧在传输过程中是否出错。这是Modbus RTU可靠性的关键必须由软件计算硬件CRC通常不适用。寄存器地址映射是关键中的关键也是最容易混乱的地方。Modbus协议中的“寄存器”是一个16位2字节的数据单元。威纶通软件如EasyBuilder Pro里配置的“设备地址”需要和你STM32程序中定义的寄存器地址空间一一对应。这里有一个巨大的坑点协议地址 vs. 数据模型地址。Modbus协议帧里携带的地址是“基于0”的地址。例如你想访问你STM32程序中定义的第一个保持寄存器我们假设你把它命名为holdingReg[0]在发送的Modbus帧中这个寄存器的地址是0x0000。但是在很多主站软件包括威纶通的部分老版本或某些驱动以及文档描述中习惯使用“基于1”的地址即他们所说的“40001”地址对应协议地址0x0000。威纶通的新版驱动通常可以直接配置偏移量你需要非常清楚你使用的驱动约定是什么。在我的项目里我统一在STM32端使用“基于0”的地址逻辑在威纶通软件中则选择对应的驱动并设置正确的偏移量通常是0。注意务必仔细核对威纶通设备属性页中的“站号”从机地址、“地址类型”如4x代表保持寄存器以及“实际地址”的偏移设置。一个数字对不上通讯就会失败。3. 硬件连接与STM32底层驱动配置硬件连接是通讯物理层的基础务必扎实。3.1 RS485电路连接STM32芯片本身只有UART通用异步收发器是TTL电平。要连接RS485网络需要一个电平转换芯片最常用的是SP3485或MAX3485。接线要点如下STM32_UART_TX-SP3485_DI(数据输入)STM32_UART_RX-SP3485_RO(数据输出)STM32_GPIO-SP3485_DE和RE(通常这两个引脚短接用同一个GPIO控制收发方向)SP3485_A和B线 - 连接到威纶通屏的RS485端口A对AB对B。在RS485总线两端通常是威纶通屏端和最后一个STM32设备端需要各接一个120欧姆的终端电阻以消除信号反射保证长距离传输的波形质量。如果设备数量少、距离近有时可以不接但规范做法是接上。控制收发方向的GPIO配置至关重要。RS485是半双工同一时刻只能有一个设备发送。STM32默认应处于接收状态。只有在UART发送数据前才将GPIO拉高使能发送器发送完成后立即拉低切换回接收状态。这个切换时机要精准特别是在发送完最后一字节数据后需要等待该字节完全从硬件移位寄存器发出可通过查询TC传输完成标志或使用DMATC中断再延时一小段时间如几十微秒后才能切换方向。切换太快最后一个字节可能发送不完整切换太慢会影响对方回应的接收。3.2 STM32 UART与DMA配置为了提高效率并减少CPU中断负担我强烈推荐使用DMA直接存储器访问来收发UART数据。接收RX配置UART为IDLE空闲中断 DMA循环接收模式。将DMA指向一个较大的环形缓冲区如256字节。当总线空闲超过一帧字符时间时会触发UART空闲中断。在中断服务程序里我们可以根据DMA的当前指针位置计算出本次接收到的数据长度然后将这一帧数据交给协议解析函数处理。这种方式可以自动处理任意长度的帧且不丢数据。发送TX配置为DMA单次发送模式。当需要发送应答帧时将组织好的数据放入发送缓冲区启动DMA传输。在DMA传输完成中断中去切换RS485方向控制引脚为接收状态。以下是基于STM32 HAL库的核心配置代码逻辑摘要// 1. UART 初始化 huart1.Instance USART1; huart1.Init.BaudRate 9600; // 与威纶通屏设置一致 huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; // Modbus RTU 通常无校验 huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; HAL_UART_Init(huart1); // 2. 使能UART空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 3. DMA接收配置循环模式 hdma_usart1_rx.Instance DMA1_Channel5; hdma_usart1_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.Mode DMA_CIRCULAR; // 循环模式 hdma_usart1_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; HAL_DMA_Init(hdma_usart1_rx); __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); HAL_UART_Receive_DMA(huart1, uart_rx_buf, RX_BUF_SIZE); // 4. 空闲中断处理函数在stm32f1xx_it.c中 void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除空闲中断标志 // 计算本次接收到的数据长度 uint16_t rx_len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); if(rx_len 0) { // 将 uart_rx_buf 中前 rx_len 个字节复制到解析缓冲区 // 调用协议解析函数 process_modbus_frame() } } HAL_UART_IRQHandler(huart1); }4. STM32从站协议解析与实现有了稳定的数据流接下来就是核心的协议解析。我们需要在STM32中实现一个Modbus从站。4.1 数据结构定义首先在内存中开辟几块区域模拟Modbus的设备数据模型。虽然我们只用了保持寄存器但规范定义有助于理解。#define HOLDING_REG_NUM 100 // 保持寄存器数量根据需求定义 #define COIL_REG_NUM 16 // 线圈开关量数量 // Modbus 从站数据模型 typedef struct { uint8_t device_addr; // 本设备地址 uint16_t holdingReg[HOLDING_REG_NUM]; // 保持寄存器数组存放温度、压力等数据 uint16_t inputReg[50]; // 输入寄存器只读 uint8_t coils[COIL_REG_NUM]; // 线圈可读可写位 uint8_t discreteInputs[16]; // 离散输入只读位 } ModbusSlave_t; ModbusSlave_t mb_slave { .device_addr 0x01, // 假设设备地址为1 };4.2 帧解析与分发在空闲中断中获取到一帧数据后process_modbus_frame函数需要做以下工作长度校验帧长度至少为4字节地址功能码CRC低CRC高否则丢弃。CRC校验计算接收帧除最后两个CRC字节外的CRC与帧中自带的CRC对比。不一致则丢弃绝不响应。这是保证数据可靠性的第一道关卡。地址匹配检查帧中地址是否与本设备地址或广播地址匹配。功能码分发根据功能码调用相应的处理函数。void process_modbus_frame(uint8_t *frame, uint16_t len) { // 1. 基本长度检查 if(len 4) return; // 2. CRC校验 uint16_t crc_received (frame[len-1] 8) | frame[len-2]; uint16_t crc_calculated modbus_crc16(frame, len-2); if(crc_received ! crc_calculated) return; // CRC错误静默丢弃 // 3. 地址检查 (广播地址0x00通常不处理) uint8_t addr frame[0]; if(addr ! mb_slave.device_addr addr ! 0x00) return; // 4. 功能码分发 uint8_t func_code frame[1]; switch(func_code) { case 0x03: // 读保持寄存器 handle_read_holding_registers(frame, len); break; case 0x06: // 写单个寄存器 handle_write_single_register(frame, len); break; case 0x10: // 写多个寄存器 handle_write_multiple_registers(frame, len); break; default: // 不支持的函数码可构造异常响应 send_exception_response(addr, func_code, 0x01); // 非法功能码 break; } }4.3 处理“读保持寄存器”0x03请求这是威纶通屏读取数据的主要方式。请求帧格式[地址][0x03][起始地址高][起始地址低][寄存器数量高][寄存器数量低][CRC低][CRC高]。例如请求读取设备1的保持寄存器从地址0开始读2个寄存器。帧为01 03 00 00 00 02 CRC。STM32的响应帧格式[地址][0x03][字节数][数据1高][数据1低][数据2高][数据2低]...[CRC]。void handle_read_holding_registers(uint8_t *req, uint16_t req_len) { // 解析请求帧中的起始地址和寄存器数量 uint16_t start_addr (req[2] 8) | req[3]; // 基于0的地址 uint16_t reg_count (req[4] 8) | req[5]; // 合法性检查地址是否越界数量是否超限协议规定一次最多读125个寄存器 if((start_addr reg_count) HOLDING_REG_NUM || reg_count 125) { send_exception_response(req[0], 0x03, 0x02); // 非法数据地址 return; } // 组织响应帧 uint8_t resp_frame[256]; uint8_t resp_len 0; resp_frame[resp_len] req[0]; // 地址 resp_frame[resp_len] 0x03; // 功能码 resp_frame[resp_len] reg_count * 2; // 字节数 寄存器数 * 2 // 填充寄存器数据 for(int i 0; i reg_count; i) { uint16_t reg_value mb_slave.holdingReg[start_addr i]; resp_frame[resp_len] (reg_value 8) 0xFF; // 高字节在前 resp_frame[resp_len] reg_value 0xFF; // 低字节在后 } // 计算CRC并添加到帧尾 uint16_t crc modbus_crc16(resp_frame, resp_len); resp_frame[resp_len] crc 0xFF; resp_frame[resp_len] (crc 8) 0xFF; // 通过UART发送响应帧注意先切换RS485方向为发送 rs485_set_tx(); HAL_UART_Transmit_DMA(huart1, resp_frame, resp_len); // 发送完成后在DMA发送完成中断中切换回接收方向 rs485_set_rx(); }4.4 处理“写单个寄存器”0x06请求这是屏控制STM32的常用方式比如点击按钮发送一个启动命令。请求帧格式[地址][0x06][寄存器地址高][寄存器地址低][寄存器值高][寄存器值低][CRC]。STM32需要验证地址合法性然后将值写入对应的holdingReg[]最后原样返回整个请求帧作为应答。这是Modbus协议的规定主站以此确认写入成功。void handle_write_single_register(uint8_t *req, uint16_t req_len) { uint16_t reg_addr (req[2] 8) | req[3]; uint16_t reg_value (req[4] 8) | req[5]; if(reg_addr HOLDING_REG_NUM) { send_exception_response(req[0], 0x06, 0x02); return; } // 写入数据 mb_slave.holdingReg[reg_addr] reg_value; // 关键点写寄存器后可能需要触发实际动作。 // 例如reg_addr0 代表“电机启停命令”这里需要调用电机控制函数。 if(reg_addr 0) { control_motor(reg_value); } // 原样返回请求帧作为应答 rs485_set_tx(); HAL_UART_Transmit_DMA(huart1, req, req_len); // req就是完整的请求帧 // 发送完成后切换方向 }5. 威纶通EasyBuilder Pro组态配置详解STM32端准备就绪后需要在威纶通的上位机软件EasyBuilder Pro中进行配置建立连接并设计画面。5.1 设备连接配置新建工程选择与你硬件型号完全一致的触摸屏型号如MT8071iE。进入“系统参数设置”。在“设备列表”中新增设备。设备类型选择Modbus RTU。注意威纶通可能有多个Modbus驱动如“Modbus RTU (Legacy)”和“Modbus RTU”通常选后者。接口类型选择RS485 2W两线制。COM端口选择触摸屏背面标识的RS485端口号如COM2。站号填写STM32程序中设置的device_addr如1。通讯参数波特率、数据位、停止位、校验位必须与STM32的UART配置完全一致例如9600, 8, 1, None。5.2 变量与地址映射这是连接逻辑与画面的桥梁。在“设备”下的“本机设备”中找到你刚添加的Modbus RTU设备右键“新增设备”。设备类型保持为刚才的Modbus RTU设备。设备属性这里需要极其小心。站号同上。地址类型对于我们在STM32中定义的holdingReg[]数组应选择4x Holding Register。实际地址这是最容易出错的地方。假设STM32中holdingReg[0]的协议地址是0。在威纶通中如果你想访问它通常需要填写1。因为威纶通以及很多主流HMI软件的“4x”地址默认是“基于1”的即40001对应协议地址0。所以“实际地址”填1软件内部会将其处理为访问协议地址0。务必查阅当前版本EasyBuilder Pro的驱动手册或通过测试验证。一个安全的测试方法是在STM32端将holdingReg[0]固定为一个特殊值如0x55AA在威纶通中分别用地址1和0建立两个数值显示元件看哪个能读到正确值。5.3 画面元件关联与调试放置元件从元件库拖拽“数值显示”、“数值输入”、“位状态设定开关”、“仪表”等到画面。关联变量双击元件在“属性”-“地址”中选择之前定义的设备并填写偏移地址。例如关联holdingReg[5]协议地址5如果威纶通“基于1”则此处填6。下载与在线模拟配置好画面后可以先使用EasyBuilder Pro的“离线模拟”或“在线模拟”功能进行测试无需连接实际硬件。在线模拟时软件会模拟主站发送请求但需要STM32实际响应所以通常还是直接下载到屏里测试更真实。下载到触摸屏通过网线或USB线将工程下载到威纶通触摸屏。6. 联调实战问题排查与性能优化将程序烧录进STM32工程下载到威纶通屏连接好RS485线上电。如果一切顺利屏幕上应该能正确显示STM32发送的数据并且操作开关能控制STM32。但现实往往是骨感的下面是我遇到过的典型问题及排查手段。6.1 通讯完全失败无任何数据检查硬件万用表测量RS485的A、B线之间是否有电压差静止时应有稳定电平通讯时有跳变。确认A-A、B-B连接正确。检查STM32的RS485收发控制引脚波形发送时是否拉高。检查波特率等参数确保STM32和威纶通屏的波特率、数据位、停止位、校验位一个比特都不差。常用9600-8-N-1。监听数据使用USB转RS485适配器连接到总线借助串口助手如SecureCRT、Modbus Poll/Slave监听数据流。这是最强大的调试手段。如果看不到威纶通屏发出的请求帧检查屏的通讯参数和设备地址设置。如果能看到请求帧但无STM32应答检查STM32程序是否收到数据RX灯或调试打印CRC计算是否正确地址是否匹配应答帧发送函数是否被调用RS485方向切换时机是否正确如果能看到STM32的应答帧但屏上显示“通讯超时”或“无效数据”检查威纶通的设备地址和变量地址映射特别是地址偏移。6.2 数据能读但显示不对或写操作不生效字节序问题Modbus协议规定大端序高字节在前。STM32是ARM Cortex-M内核内存存储是小端序。当你把一个uint16_t类型的变量如0x1234直接存入holdingReg[]时在内存中是0x34 0x12。如果你直接按字节发送就变成了0x34, 0x12这与Modbus要求的大端序0x12, 0x34相反。因此在组织响应帧时必须手动进行字节交换如前文代码所示(reg_value 8) 0xFF先发高字节。同样收到写命令时也要将收到的高低字节组合成正确的uint16_t值。地址映射错误再次强调反复确认威纶通中变量地址与STM32寄存器数组下标的对应关系。建立一个测试变量在STM32中定期变化如每秒加1在威纶通上观察是否正确显示。写操作后STM32无动作检查handle_write_single_register函数中在更新了holdingReg[]数组后是否调用了相应的控制函数。写寄存器只是更新了内存需要你手动关联到具体的IO操作或函数调用。6.3 通讯不稳定偶尔断线或数据错误终端电阻长距离或多设备时务必在总线两端安装120Ω终端电阻。总线拓扑与接地尽量使用总线型拓扑避免星型连接。确保所有设备的RS485地线如果有良好共地但避免与强电地形成环流。可以使用屏蔽双绞线屏蔽层单点接地。软件超时与重发威纶通屏作为主站有默认的通讯超时时间和重试次数。如果STM32响应太慢可能导致屏报错。可以在STM32端优化代码确保在接收到有效帧后尽快响应通常在几十毫秒内。也可以适当增加屏的超时时间。中断优先级与阻塞确保UART空闲中断和DMA中断的优先级设置合理避免被其他高优先级中断如SysTick长时间阻塞导致数据接收不完整。避免在中断服务函数中进行复杂运算或调用可能阻塞的函数如HAL_Delay。6.4 性能与资源优化使用DMA和空闲中断如前所述这是稳定高效接收的不二法门。合理规划寄存器地址将需要频繁读取的数据如实时温度、速度放在连续的寄存器地址块这样威纶通屏可以用一次0x03请求读取多个数据减少通讯频率。心跳或状态监测可以在威纶通屏上做一个“通讯状态”指示灯关联一个STM32的特定寄存器。STM32在主循环中定期更新该寄存器如每秒取反屏通过定时读取该寄存器来判断通讯是否正常。批量操作对于初始化时需要写入大量参数的情况优先使用0x10写多个寄存器功能码而不是多次调用0x06可以极大提高效率。整个调试过程串口监听工具是你的眼睛。通过它你能清晰地看到每一帧请求和应答的原始十六进制数据结合Modbus协议手册任何问题都无处遁形。从硬件链路到软件逻辑从字节序到地址映射一步步排查最终让STM32与威纶通屏稳定、可靠地对话看到屏幕上实时跳动起来自单片机世界的数据时那种成就感正是嵌入式开发的乐趣所在。
返回列表