☰
STM32与迪文DGUS串口屏通信实战:从USART初始化到帧解析
2026/11/4 14:51:56 网站建设 项目流程

简介:这份例程面向希望在STM32项目中集成迪文DGUS串口屏的嵌入式开发者,提供从串口初始化到协议收发的完整参考实现。内容围绕HAL库的UART配置、波特率与校验参数设置、DGUS专有数据包格式的构建与解析展开,并涉及屏幕显示对象、触控事件等基础操作,适合已具备一定单片机基础、需要打通主控与智能屏通信链路的读者。压缩包共998个文件,以647个C源文件与173个头文件为主体,辅以86个汇编文件、56个链接脚本及工程配置、界面图片与固件文件,整体约16.59MB,工程结构完整,可直接对照阅读与移植。目前已有3521人学习下载。通过源码与工程配置,读者能够理清串口收发函数的组织方式、协议帧的拼装逻辑以及调试阶段的数据监控思路,为后续扩展复杂人机界面打下可复用的代码基础。

1. 串口屏联调现场:STM32 与迪文 DGUS 屏到底怎么对上话

手上拿到一块迪文 DGUS 串口屏,再配一块 STM32 最小系统板,很多人第一反应是“接上 TX/RX 不就能显示了”。真接上电才发现屏幕黑着、变量不动、按键没反应,串口助手抓包全是乱码。这个例程包解决的就是这段最容易翻车的链路:STM32 侧怎么初始化 USART、怎么按 DGUS 的帧格式打包下发、怎么解析屏幕回传的触摸与变量数据,以及屏幕工程里变量地址和控件怎么配。它适合刚接触串口屏、想快速把“单片机数据搬到屏上”跑通的嵌入式开发者,也适合已经会点灯但没做过 HMI 交互的人。整套代码围绕 DGUS 协议展开,核心不是多高深的算法,而是帧结构、地址映射和收发时序这三件事对齐。

2. 工程结构与通信链路:从 USART 初始化到 DGUS 帧收发

2.1 目录与文件分工

拿到压缩包解压后,工程一般按 STM32 标准外设库或 HAL 库的组织方式铺开,和串口屏相关的代码集中在少数几个文件里,其余是芯片启动和时钟配置。常见结构如下:

文件/目录作用
Core/Src/main.c时钟、GPIO、USART 初始化,主循环里调用屏通信逻辑
Core/Src/usart.c串口参数配置,波特率、字长、停止位、中断使能
Core/Src/dgus.cDGUS 协议封装:写变量、读变量、解析触摸帧
Core/Inc/dgus.h变量地址宏、帧长度宏、函数声明
Core/Src/stm32f1xx_it.c串口中断服务函数,把收到的字节塞进环形缓冲
Screen/迪文屏工程文件(DGUS 工具生成的配置与图片资源)

这个分工的关键在于把“协议”和“业务”拆开。dgus.c只负责把地址和数据变成字节流,main.c只负责决定什么时候发什么。二次开发时改业务逻辑不用碰协议层,换屏幕型号时改协议层不用动业务,这是这套例程最值得沿用的设计习惯。

2.2 USART 初始化:参数必须和屏幕工程一致

DGUS 屏出厂或工程配置里有一个串口波特率,常见是 115200,也有 9600 的。STM32 这边初始化必须完全对齐,否则收到的就是一堆错位字节。以 HAL 库为例:

/* usart.c:USART1 初始化,PA9=TX,PA10=RX */ void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; /* 必须与 DGUS 工程配置一致 */ huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; /* DGUS 默认无校验 */ huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }

参数里最容易出错的是校验位和停止位。DGUS 协议帧是 8N1,一旦在 STM32 侧开了偶校验,屏幕收到的每个字节最高位都被改写,表现就是“能收到但数据全错”。波特率误差也要注意,用内部 RC 振荡器做时钟源时误差偏大,长帧容易丢字节,我一般强制用外部晶振。初始化完成后先别急着发业务数据,用串口助手以同样参数发一帧读版本指令,能收到回包才说明物理链路通了。

2.3 DGUS 帧格式:一条写变量指令的完整拆解

DGUS 的通信帧结构是固定的五段式:帧头 + 长度 + 指令 + 数据 + 结束符。以“向变量地址 0x1000 写一个 16 位数据 0x0064”为例:

/* dgus.c:向指定变量地址写一个 16 位数据 */ void DGUS_WriteVar16(uint16_t addr, uint16_t value) { uint8_t frame[8]; frame[0] = 0x5A; /* 帧头固定 0x5A */ frame[1] = 0x05; /* 长度:指令1 + 数据2 + 结束2 */ frame[2] = 0x82; /* 0x82 = 写变量指令 */ frame[3] = (uint8_t)(addr >> 8); /* 地址高字节 */ frame[4] = (uint8_t)(addr & 0xFF); /* 地址低字节 */ frame[5] = (uint8_t)(value >> 8); /* 数据高字节 */ frame[6] = (uint8_t)(value & 0xFF); /* 数据低字节 */ frame[7] = 0x00; /* 结束符低字节,部分固件要求 */ HAL_UART_Transmit(&huart1, frame, 8, 100); }

逐段说明:0x5A是 DGUS 帧头,所有下行指令都以它开头;长度字节表示“指令字 + 数据字 + 结束字”的总字数,写 16 位变量时是 1+2+2=5;0x82是写变量指令码,读变量是0x83;地址和数据都是高字节在前,这点和很多单片机的习惯相反,写反了屏幕会显示成另一个值。结束符在不同固件版本里要求不一,有的必须带0x00 0x00,有的可以省略,例程里统一带上最稳。发送用阻塞式HAL_UART_Transmit在低频刷新场景够用,如果要做 20ms 级的高频刷新,建议换成 DMA 发送,避免阻塞主循环。

2.4 接收解析:触摸帧与变量回传怎么拆

屏幕主动上报的数据主要有两类:触摸事件和读变量回包。触摸帧以0x5A开头,指令码0x83表示触摸坐标上报,后面跟控件地址和触摸状态。接收侧不能假设一次中断收完整帧,必须用状态机或环形缓冲攒够长度再解析:

/* dgus.c:串口接收状态机,逐字节喂入 */ void DGUS_RxByte(uint8_t byte) { static uint8_t buf[16]; static uint8_t idx = 0; if (idx == 0 && byte != 0x5A) return; /* 等帧头 */ buf[idx++] = byte; if (idx >= 2 && idx == buf[1] + 3) /* 帧头+长度+数据+结束 */ { if (buf[2] == 0x83) /* 触摸上报 */ { uint16_t vp = (buf[3] << 8) | buf[4]; uint8_t state = buf[5]; DGUS_OnTouch(vp, state); /* 交给业务层处理 */ } idx = 0; /* 复位,等下一帧 */ } if (idx >= sizeof(buf)) idx = 0; /* 防溢出 */ }

这段状态机的核心是“长度字段决定帧边界”。buf[1] + 3是因为长度字节本身不计入长度值,加上帧头、长度字节和结束符共 3 字节偏移。触摸帧里的vp是控件绑定的变量地址,业务层拿这个地址判断是哪个按钮被按下。中断服务函数里只做“取字节、调状态机”,解析和业务处理放到主循环,避免在中断里做耗时操作导致丢字节。

3. 跑通第一个界面:屏幕工程配置与联调步骤

3.1 屏幕侧工程:变量地址是双方的契约

STM32 和屏幕之间唯一的“接口文档”就是变量地址表。在 DGUS 工具里给每个控件分配 VP 地址,比如文本显示控件绑0x1000、按钮控件绑0x2000,这个地址必须和dgus.h里的宏一一对应:

/* dgus.h:变量地址宏,与屏幕工程严格对应 */ #define VP_TEMP_VALUE 0x1000 /* 温度数值显示 */ #define VP_BTN_START 0x2000 /* 启动按钮 */ #define VP_STATUS_TEXT 0x1010 /* 状态文本 */

我见过最常见的翻车是屏幕工程改过地址、代码里没同步,现象是“数据发出去了但屏幕不动”。所以地址宏一定要集中管理,改屏幕工程时先改这个头文件。文本显示控件还要注意编码,DGUS 默认 GBK,STM32 侧如果直接发 UTF-8 字符串会显示乱码,要么在屏幕工程里切成 UTF-8,要么在代码里做转码。

3.2 上电联调顺序:先通链路再通业务

联调不要一上来就跑完整业务,按下面顺序逐层验证,每步都有明确的成功判据:

  1. 只接 TX/RX/GND 三根线,共地是前提,不共地必然乱码。
  2. 用串口助手以 115200 8N1 发读版本指令5A 03 81 00 00,收到回包说明物理层通。
  3. 烧录例程,主循环里只发一条写变量指令,观察屏幕对应控件是否变化。
  4. 在屏幕上点按钮,串口助手观察是否有5A开头的触摸帧上报。
  5. 打通后再接入传感器数据,做周期性刷新。

第 2 步的读版本指令是排障利器,它不依赖任何业务代码,能快速区分“硬件接线问题”和“协议打包问题”。如果这步就不通,别往下查代码,先查线序和共地。

3.3 主循环里的刷新节奏

业务数据刷新不要放在中断里,也不要用HAL_Delay硬等。常见做法是用一个软件定时器或HAL_GetTick()做非阻塞节拍:

/* main.c:200ms 周期刷新温度显示 */ uint32_t last_tick = 0; while (1) { if (HAL_GetTick() - last_tick >= 200) { last_tick = HAL_GetTick(); uint16_t temp = Read_Temperature(); /* 业务取值 */ DGUS_WriteVar16(VP_TEMP_VALUE, temp); } DGUS_Poll(); /* 处理接收缓冲 */ }

200ms 是文本类显示的舒适区间,再快人眼也看不出差别,反而增加串口负载。如果是波形或进度条控件,可以压到 50ms,但要配合 DMA 发送。DGUS_Poll()在主循环里消费环形缓冲,保证接收不被业务阻塞。

4. 避坑与排查:串口屏联调最常见的五类问题

4.1 现象:屏幕完全无反应,串口助手也收不到回包

原因通常是接线或共地问题。TX/RX 要交叉接,STM32 的 TX 接屏幕的 RX;两块板子如果各自独立供电,GND 必须连在一起,否则电平参考不一致,收发的都是垃圾数据。解决方法是先用万用表量通断,再单独给屏幕供电、只连三根信号线复测。还有一种隐蔽情况是屏幕工程里串口被配置成了别的模式,需要在 DGUS 工具里确认通信参数。

4.2 现象:能收到数据但全是乱码

原因集中在波特率和校验位。屏幕工程里设的是 9600,代码里写 115200,收到的字节边界全错。或者代码里开了校验位,DGUS 帧的字节被改写。解决办法是把两边参数逐项对齐:波特率、8 数据位、无校验、1 停止位。用内部 RC 时钟的板子还要检查波特率误差,误差超过 3% 长帧必丢,换外部晶振最省事。

4.3 现象:数据发出去了,屏幕控件纹丝不动

先查地址是否一致,屏幕工程里控件的 VP 地址和代码宏对不上是最常见原因。再查指令码,写变量是0x82,有人误用成0x80(那是写曲线)。还要确认控件类型匹配:文本控件收 16 位数值会按配置的格式显示,如果控件配的是“ASCII 字符串”模式,发数值过去就是乱码。排查时用串口助手手动发一帧已知正确的指令,能显示就说明是代码打包问题,不能显示就是屏幕工程配置问题。

4.4 现象:触摸按钮偶尔失灵,连点几次才响应

原因是接收侧没有做帧同步,中断里收到半帧就开始解析,或者缓冲区被覆盖。表现是“有时候灵有时候不灵”。解决方法是严格用状态机按长度字段攒帧,并且在中断里只入队、不在中断里解析。另外触摸帧有“按下”和“抬起”两种状态,业务层如果只判断按下、不判断抬起,会出现一次点击触发多次的问题,需要按状态位过滤。

4.5 现象:刷新频率一高就丢数据或屏幕卡顿

阻塞式发送在高频刷新时会拖慢主循环,接收中断也可能因为主循环来不及消费而溢出。解决办法是发送改 DMA、接收用环形缓冲加空闲中断,把“攒帧”和“解析”彻底分开。另外不要每条数据都单独发一帧,多个连续变量地址可以用一条0x82指令批量写,减少帧数和总线占用。

5. 二次开发改造:把例程变成自己的 HMI 框架

例程跑通只是起点,真正落地时要把它改造成能复用的结构。我一般做三件事:第一,把dgus.c里的地址硬编码全部抽成一张地址表,用结构体数组管理“控件名—地址—数据类型”,新增界面只改表不改逻辑;第二,把发送层抽象成DGUS_Send(frame, len)一个出口,底层可以在阻塞发送和 DMA 发送之间切换,业务层无感知;第三,接收侧引入回调注册机制,按 VP 地址分发到不同处理函数,避免switch越写越长。

验证改造是否成功,有个简单方法:新增一个文本控件和一个按钮,从屏幕工程配地址到代码里加回调,全程只改地址表和注册一行回调,不碰协议层代码。如果做到了,说明分层是干净的。下面是一个地址表加回调注册的骨架:

/* dgus_port.h:控件描述表,新增控件只加一行 */ typedef struct { uint16_t vp; uint8_t type; /* 0=数值 1=文本 2=按钮 */ void (*on_touch)(uint8_t state); } DGUS_Item; static DGUS_Item items[] = { { VP_TEMP_VALUE, 0, NULL }, { VP_BTN_START, 2, OnStartBtn }, }; /* 按 VP 查找并分发触摸事件 */ void DGUS_OnTouch(uint16_t vp, uint8_t state) { for (int i = 0; i < sizeof(items)/sizeof(items[0]); i++) { if (items[i].vp == vp && items[i].on_touch) { items[i].on_touch(state); return; } } }

这样改造后,换一块屏幕、换一套地址,工作量集中在表里,业务回调原样复用。还有一个容易忽略的点:屏幕工程文件要纳入版本管理,图片资源和配置文件跟代码一起提交,否则过两个月没人记得某个控件当初为什么绑那个地址。我吃过一次亏,屏幕工程只存在本地,重装系统后全丢了,只能对着现象反推地址。从那以后我每次改完屏幕工程,都把工程目录和代码一起提交,并在提交说明里写清这次新增了哪些 VP。希望这些经验帮到你,少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询