
简介STM32与迪文屏通信例程是一份面向嵌入式开发者的Modbus RTU通信实战工程。工程以STM32为主机、迪文屏为从机通过RS485串口4实现双向数据交换完整覆盖初始化、请求帧构建、CRC校验、响应解析与异常处理等流程并附带DMA优化串口收发的参考实验便于理解如何提升大数据量通信效率。压缩包共678个文件约24.53MB其中包含324个C源码、87个头文件以及链接脚本.icf/.sct、编译产物.o/.axf、迪文屏工程文件.hmi和变量/触控配置文件.bin从底层驱动到界面配置均可对照查看方便直接移植或二次开发。目前已有3084人学习下载适合正在调试迪文屏、需要快速实现Modbus RTU通信的STM32开发者。 干嵌入式这些年屏幕这块没少踩坑。早几年做个人机交互界面要么上组态屏成本压不住要么自己刷TFT彩屏光移植GUI库就能耗掉半个项目周期最后做出来还不一定顺滑。后来项目里开始用迪文串口屏配合STM32做主控整体开发节奏一下子快了不少硬件上就一根串口线的事界面在迪文的DGUS工具里拖拽配置MCU端只需要按协议收发数据就行。手头正好整理了一套STM32与迪文屏通信例程从工程搭建到协议收发到线上调试全部跑通今天就把这套例程的完整思路和代码细节拆开来聊一聊。这套东西适合什么场景手上有STM32项目需要加一个可配置的显示触摸界面比如设备参数设置、运行状态实时显示、报警信息提示又不打算在MCU端花钱花精力去搞GUI库的迪文屏是很现实的选择。本文对用标准库和HAL库的朋友都适用协议部分是通用的串口驱动代码稍作调整就能移植到自己的工程里。1. 项目整体设计与思路拆解1.1 选型背景为什么是迪文屏配STM32做产品最怕什么怕需求天天变。周一要显示温度周五要加个电压曲线下周一又要改成中文菜单。如果全部在MCU端用GUI库实现每次调整都涉及重新编译固件甚至要改底层代码Bug率直线上升。迪文屏这种串口屏最大的价值就是把界面开发和逻辑开发彻底分开屏幕页面在PC端用DGUS软件配MCU只负责往指定变量地址写数、读数界面怎么改基本不影响固件。STM32在工控、仪器仪表领域的基础太扎实了性能足够、外设丰富、资料多跟迪文屏通信几乎零成本。我见过不少团队用STM32F103C8T6这种小芯片就能撑起一套带触摸屏的中小型设备香得很。另一个关键点是迪文屏抗干扰能力不错在电机、继电器这类电磁环境复杂的设备上RS232接口的迪文屏配合屏蔽线跑115200波特率实测可以稳定通信。1.2 通信架构MCU和屏幕的数据通路整套系统的数据链路分三块人机交互侧触摸屏、通信链路串口、逻辑控制侧STM32。用户手指点一下屏幕迪文屏通过串口把按键动作和数据请求发给MCUMCU解析后执行相应逻辑再把需要显示的数据按协议帧发回去。整个过程可以理解成两个人通过串口对话一个问、一个答或者你说我听。串口参数怎么定以DGUS二代屏为例我习惯用115200bps、8数据位、1停止位、无校验。波特率不是越高越好9600和115200在协议层没区别但画面数据量大的时候高波特率能明显降低屏幕刷新的卡顿感。RS232电平的屏幕要注意STM32的USART是TTL电平两者之间需要加MAX3232之类的电平转换芯片别直接把TX、RX怼在一起会烧引脚。1.3 开发环境与文件分配这套例程基于STM32标准外设库开发环境用的是Keil MDK5。我知道现在不少人都在转HAL库但标准库代码简洁、寄存器操作直观特别适合学习和快速验证通信协议。如果你用的是CubeMX生成的HAL库工程也没关系把串口收发函数替换成HAL_UART_Transmit和HAL_UART_Receive_IT协议帧部分直接搬过去就能用。例程的文件结构不用复杂四个文件足够一个主循环文件放着业务逻辑一个dwin.c/dwin.h专门处理迪文屏协议拼接和解析一个uart.c/dwin.h做串口初始化和中断接收剩下的就是stm32f10x_it.c里的中断服务函数。2. 核心细节解析与实操要点2.1 迪文屏协议帧格式与地址分配逻辑迪文屏协议虽然版本多但核心思路就一条读写变量地址。屏幕上的每个显示控件、触控控件、文本控件背后都绑了一个变量地址MCU往这个地址写入数据控件就显示对应内容反过来用户操作触控控件时屏幕会把数据发到串口上。最常用的帧格式是0xA5 0x5A开头帧头1帧头2数据长度指令地址高字节地址低字节数据0xA50x5A0x050x820x100x000x12 0x34长度字段表示从指令字节开始到帧尾的字节总数。比如上面这帧长度是5代表指令0x82占1字节、地址0x1000占2字节、数据0x1234占2字节。0x82指令是写变量0x83是读变量0x80和0x81用于读写系统寄存器日常用最多的是0x82和0x83。迪文屏的变量地址范围通常是0x00000x6FFF每个地址对应一个16位数据。地址不区分高低位字节按字为单位寻址。不少新人在这栽过跟头在DGUS工具里给控件设置地址比如0x1000然后MCU端也往0x1000写但发现屏幕不显示。这通常是工具里填的是变量地址偏移有的控件会要求填双字还是单字配置时要留意数据类型和字节序。2.2 硬件接线与电平转换的坑接线看起来简单但坑不少。TTL电平的迪文屏RX接STM32的TXTX接STM32的RX两条线交叉这不是废话我见过有人拿着两头都是母头的杜邦线A屏RX接B屏TX结果全串到一块。RS232电平的屏幕则必须在中间加MAX3232如果项目需要长距离走线比如超过1米RS232比TTL可靠得多。还有一点别忘了共地不接GND通信时好时坏逻辑分析仪抓还抓不出来特别阴。电源方面迪文屏背光和触摸部分电流不小5V供电尽量单独走一条线别从STM32的3.3V强行取电。如果屏幕和MCU共用一个电源要注意电源纹波大电流浪涌会导致串口通信偶发乱码。这些细节在开发板上一不明显一上真机就全暴露了。2.3 例程工程结构规划工程规划上我习惯把协议处理单独拎出来做成dwin.c/dwin.h不跟具体业务代码混在一堆。这样改一个页面需求只需要在应用层改数据映射协议层完全不动。dwin.c里主要放几个函数变量写入、变量读取、帧接收解析、校验处理和错误处理。接收部分最好做一个环形缓冲区串口中断只负责收数据解析在定时器或主循环里做避免中断里做太多运算影响实时性。3. 实操过程与核心环节实现3.1 串口初始化标准库配置USART1通信调试第一步先把串口配置对。下面这段是标准库配置USART1的代码波特率1152008-N-1void UART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // TX - PA9 推挽复用输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // RX - PA10 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }用HAL库的朋友注意CubeMX里串口参数配置成同样的参数即可不用纠结代码是否一致底层工作方式没有区别。发送部分我习惯在发送前先清一下TC标志位否则第一帧数据可能发不出去。3.2 变量写入与读取核心发送代码写变量是使用频率最高的操作。假设屏幕上有一个数据显示控件变量地址0x1000现在要把温度值23摄氏度显示上去先转成整型23然后按0x82指令组装帧void DWIN_WriteVariable(uint16_t addr, uint8_t *data, uint16_t len) { uint8_t frame[8 len]; uint8_t i; frame[0] 0xA5; frame[1] 0x5A; frame[2] 0x03 len; // 指令1字节 地址2字节 数据len字节 frame[3] 0x82; // 写变量指令 frame[4] addr 8; frame[5] addr 0xFF; for (i 0; i len; i) { frame[6 i] data[i]; } UART1_SendBytes(frame, 6 len); }调用方式很直接uint8_t tempData[2]; tempData[0] 0x00; tempData[1] 23; // 23摄氏度高位在前 DWIN_WriteVariable(0x1000, tempData, 2);有人会问为什么数据要拆成高字节和低字节两段发送因为22位数据在协议里是大端格式高位在前。如果你把0x23放第一个字节0x00放第二个屏幕显示出来的值就成了0x2300直接变成8960排查半天以为地址配错了。读取变量稍微不一样。读取变量需要先发0x83指令向屏幕请求屏幕收到后会主动返回一帧数据。比如读0x1000地址启动一个字的数据void DWIN_ReadVariable(uint16_t addr) { uint8_t frame[7]; frame[0] 0xA5; frame[1] 0x5A; frame[2] 0x04; // 指令地址数据长度 frame[3] 0x83; // 读变量指令 frame[4] addr 8; frame[5] addr 0xFF; frame[6] 0x01; // 读一个字 UART1_SendBytes(frame, 7); }读取指令里frame[6]是读取的字数这个值也不能写错。如果变量在DGUS工具里配置的是双字32位你需要读2个字就得写0x02否则返回的数据格式对不上。3.3 串口中断接收与帧解析接收迪文屏上传的数据我推荐用中断加状态机不用轮询。串口收到一个字节中断函数存进缓冲解析程序通过状态机判断帧头、长度、数据段。代码如下void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); DWIN_RxProcess(byte); USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }状态机处理逻辑void DWIN_RxProcess(uint8_t byte) { static uint8_t state 0; static uint16_t frameLen 0; static uint16_t index 0; switch (state) { case 0: // 等待帧头1 if (byte 0xA5) state 1; break; case 1: // 等待帧头2 if (byte 0x5A) state 2; else state 0; break; case 2: // 接收长度 frameLen byte - 1; // 除去长度字节本身还有 frameLen 字节 index 0; state 3; break; case 3: // 接收数据 rxBuffer[index] byte; if (index frameLen) { DWIN_ParseFrame(rxBuffer, index); state 0; } break; default: state 0; break; } }这段状态机代码的核心思路是先找0xA5 0x5A帧头然后读取长度字段之后连续接收其余字节。有一点要注意迪文屏返回的帧长度和请求帧不总是相同比如0x83读多字变量时返回长度会包含数据字节数所以解析时要用实际接收到的帧长而不是写死某个值。另外如果中间某段传输异常状态机要及时复位回state 0否则之后的所有帧都会错位。3.4 一个完整的小例子按键上传与数据下发举个综合例子。屏幕上有两个控件一个触摸屏按下按键地址0x2000一个数值显示控件地址0x1000。用户按下按键时屏幕会主动通过串口发来一帧数据内容类似A5 5A 05 82 20 00 00 01这表示地址0x2000被写入0x0001即按键被按下。MCU收到后解析地址和数据判断到是0x2000且值为1就执行相应的逻辑比如把采集到的ADC值写入0x1000显示。对应伪代码void DWIN_ParseFrame(uint8_t *buf, uint16_t len) { uint16_t addr; uint16_t value; if (len 4) return; if (buf[0] ! 0x82) return; // 只处理写变量指令 addr (buf[1] 8) | buf[2]; value (buf[3] 8) | buf[4]; if (addr 0x2000 value 0x0001) { uint16_t adcValue Read_ADC(); uint8_t data[2] { adcValue 8, adcValue 0xFF }; DWIN_WriteVariable(0x1000, data, 2); } }这个模式就是迪文屏开发最常见的事件回调触摸屏产生事件MCU通过变量地址识别事件类型再决定怎么响应。整个开发过程中90%的交互逻辑都能用这种地址-数据搭配来完成。4. 常见问题与排查技巧实录4.1 屏幕完全没反应就像没接线先别急着改代码用万用表量一下RX/TX是否有电平变化。再用串口助手把STM32发出的数据抓下来看看有没有波形。最有效的排查办法是直接用串口助手的自发自收模式测试迪文屏如果屏幕有反应说明屏没问题问题在STM32端如果屏没反应检查波特率和接线。有个细节很多人都忽略STM32刚上电时如果程序还没初始化串口IO口默认可能是浮空状态这时迪文屏偶尔会收到一堆噪声数据导致后续正常帧被错位处理。新手上板测试时如果发现时好时坏不妨在屏幕接收到错误帧后手动复位一下屏或者在MCU初始化时先把TX引脚拉高。4.2 乱码和丢帧乱码大概率是波特率不匹配其次就是电平不共地。115200波特率下如果两边晶振误差稍大高波特率乱码率会明显上升这时可以把波特率降到19200或者9600试试通信稳定性立刻不一样。还有一点如果MCU代码里有其他优先级更高的中断长时间打断串口发送也会导致帧中间间隔过大屏幕那边接收超时判定为帧错误。解决办法是发送帧的过程中暂时关闭不必要的中断或者用DMA发送让数据整帧一口气发出去。4.3 能通信但数据不对地址对不上这种情况最常见也最让人抓狂。现象是屏幕能刷新、能收到数据但显示的内容和预期完全不一样要么是0.00变成三千多要么是值翻倍。大概率是大小端问题或者变量地址多写了偏移量。排查时有几个小技巧先在DGUS工具里给控件随便设一个变量地址比如0x0001然后MCU往这个地址写一个固定值0x0055看屏幕显示是85还是218。如果是218说明高低字节反了把数据的高低字节位置换一下就好。4.4 高速数据刷新时的卡顿与延迟如果业务需要频繁刷新多个变量比如示波器波形显示那种几十毫秒就更新一次的场景建议把多个变量的写操作合并成一帧。迪文屏支持连续写变量也就是一条帧里从某个起始地址开始连续写多个字的数据。这样比一条一帧地发要高效很多屏幕处理起来也轻松。我在做动态曲线显示时把一条曲线的1024个点拆成多条连续写帧每帧写128个字刷新率比逐点发送提升了一个数量级。现象可能原因常用处理办法屏幕完全无反应接线错误、波特率不匹配用串口助手自发自收确认屏幕本体正常乱码波特率不一致、电平不共地降低波特率、严格共地、使用屏蔽线数据值异常大大小端字节序错误交换数据高字节与低字节时好时坏通信错误帧导致状态机卡死状态机超时复位校验帧头刷新卡顿单帧刷新点过多或串口速度不足提高波特率使用连续写变量指令5. 例程复用与扩展建议这套通信例程我后来在至少三个项目里直接复用每次只需要改变量地址表。所以强烈建议你在自己的工程里维护一份变量地址表把屏幕上所有用到的变量地址、类型、含义集中定义成一个头文件// dwin_variables.h #define DWIN_ADDR_TEMP 0x1000 // 温度显示 #define DWIN_ADDR_HUMI 0x1001 // 湿度显示 #define DWIN_ADDR_BTN_START 0x2000 // 启动按钮 #define DWIN_ADDR_BTN_STOP 0x2001 // 停止按钮这样写业务逻辑的时候不是裸地址而是有名字的宏可读性提升很多换屏或者改地址时一个文件搞定。如果项目要迁移到HAL库只需要把uart1_sendbytes和中断接收部分替换成HAL库对应的接口dwin.c里的逻辑完全可以原封不动带过去。另外迪文屏还支持RTC时间同步、曲线控件、历史数据存储这些功能的底层通信方式都是同一个协议。吃透变量读写这一层其余功能就是查迪文协议手册往对应地址写数据的事。我一开始上手时也对着协议手册翻半天后来发现核心就是一个变量读写后面越用越顺手。回想一下这套例程给我最大的感受是嵌入式的人机交互不一定要在MCU这边死磕。把界面的事交给串口屏把逻辑的事留给MCU复杂度一下子就降下来了。如果你正卡在显示方案的选型上或者被串口屏通信折腾得怀疑人生照着本文的步骤把协议层跑通后续的扩展基本就是一马平川了。本文还有配套的精品资源点击获取