简介:这份资源面向使用STM32进行嵌入式硬件开发的工程师与学生,聚焦迪文串口屏与主控芯片之间的串口通信实现,帮助解决显示模块驱动、指令收发与触控反馈处理等常见问题。压缩包共165个文件,以47个h头文件与46个c源文件为核心,配合18个o、18个d、17个crf等编译中间文件,以及uvprojx工程、sct分散加载、hex固件、map映射等工程配置,整体约3.61MB,覆盖STM32外设驱动与LCD相关模块。资源围绕UART初始化、波特率与数据格式配置、命令与数据帧发送、反向通信接收及错误处理等环节展开,可让读者直接对照示例代码理解迪文屏指令集与通信流程,快速搭建可运行的串口屏交互工程。目前已有2277人学习下载,适合需要缩短显示界面开发周期、降低系统复杂度的嵌入式开发者参考借鉴。
1. 迪文屏串口通信:一块工业屏的协议黑匣子怎么撬开
车间里一台老设备,屏幕上显示正常,但 PLC 就是读不到它的数据。拆开一看,是迪文屏,型号 DGUS 系列,背面只留了 4 根线:VCC、GND、TXD、RXD。没有文档,没有源码,只有一个迪文屏串口通信.rar的压缩包名字在脑子里打转。这种场景在工控圈太常见了——迪文串口屏便宜、稳定、能跑组态,但它的串口协议不像 Modbus 那样人尽皆知,很多人卡在“线接对了,数据出不来”这一步。
迪文屏串口通信的核心,是 DGUS 协议。它不像标准 Modbus RTU 那样有公开的寄存器映射表,而是围绕“变量地址”和“触控控件”组织数据。你往某个变量地址写值,屏幕上的文本或图标就变;你按一下屏幕上的按钮,它往串口吐一串十六进制。这套机制让迪文屏在低成本 HMI 场景里非常能打,但也意味着:不搞清楚帧结构和变量地址表,你连一个“Hello World”都传不上去。
这篇文章面向的是手里有迪文屏、需要跟 STM32、PLC、LabVIEW 或 Unity 做串口通信的工程师。我会从协议帧拆解开始,讲到最小可复现的收发代码、变量地址配置、常见翻车点,最后给一个用 Python 做协议验证的技巧。不依赖任何不存在的官方文档,只讲我实际调通过的那条路。
2. 迪文屏 DGUS 协议拆解:帧结构、变量地址与配置工具
2.1 一条串口帧里到底有什么
迪文屏的串口通信默认是 8 数据位、1 停止位、无校验,波特率常见 115200,但老屏可能是 9600。帧格式分两种:读指令和写指令。写指令的典型结构是:
5A A5 05 82 10 00 00 01逐字节拆开看:
| 字节位置 | 值 | 含义 |
|---|---|---|
| 0 | 5A | 帧头高字节 |
| 1 | A5 | 帧头低字节 |
| 2 | 05 | 数据长度(从指令字节到数据末尾) |
| 3 | 82 | 写变量存储器指令 |
| 4 | 10 | 变量地址高字节 |
| 5 | 00 | 变量地址低字节 |
| 6 | 00 | 数据高字节 |
| 7 | 01 | 数据低字节 |
这条指令的意思是:往变量地址0x1000写入值0x0001。屏幕那边如果把这个地址绑定了一个文本控件,就会显示“1”;如果绑定的是图标控件,就会切换到第 1 号图标。
读指令稍微不同,指令码是83,长度通常是04,后面跟变量地址和读取字长。屏幕返回的帧里会带5A A5帧头和实际数据。
注意:迪文屏的变量地址是 16 位,但实际可用的范围取决于屏幕型号和 DGUS 版本。T5L 系列通常从
0x1000开始分配用户变量,老款 T5 可能从0x0000开始。不要凭感觉写地址,后面会讲怎么查。
2.2 变量地址表从哪来:DGUS 配置工具的实际用法
迪文屏不像 Modbus 从站那样有一张固定寄存器表。它的变量地址是你自己在 DGUS 配置工具里分配的。常见做法是:
- 打开 DGUS Tool(或 T5L 版本的 DGUS II),新建工程,选对分辨率。
- 在“变量配置”里添加变量,每个变量有一个地址,比如
0x1000对应“温度值”,0x1001对应“设定值”。 - 在“控件配置”里把文本控件或图标控件绑定到这些变量地址。
- 生成配置文件,通过 SD 卡或串口下载到屏幕。
这一步做完,你手里就有一张“变量地址-含义”对照表。没有这张表,串口发什么都是盲发。我一般会把这张表直接写在代码的注释里,后面调试时省得来回翻工程文件。
如果你拿到的是一块已经烧好程序的屏,没有工程文件,那就只能靠“扫地址”的方式试探。后面避坑章节会讲具体怎么扫。
2.3 串口参数怎么设:波特率、校验与流控
迪文屏出厂默认波特率通常是 115200,但有些行业定制屏会改成 9600 或 19200。校验位默认无,数据位 8,停止位 1。流控一般不用,除非你的主控和屏幕之间距离超过 2 米且干扰大,才考虑加 120 欧姆终端电阻。
在 STM32 或 PLC 侧配置时,重点确认三件事:
- 波特率必须和屏幕一致,误差超过 2% 就可能丢帧。
- 不要开硬件流控,迪文屏的串口通常只接了 TX、RX、GND。
- 如果屏幕和主控共地不好,会出现“能收不能发”的玄学现象,先查地线。
在 LabVIEW 里用 VISA 配置串口时,注意“终止符”要设为无,因为迪文协议是二进制帧,不是 ASCII 行。Unity 串口通信也类似,SerialPort类的ReadTimeout设 100ms 左右,避免阻塞主线程。
3. 最小可复现的迪文屏串口收发:从 Python 验证到 STM32 落地
3.1 用 Python 在 PC 上先跑通一条写指令
在把代码烧进 STM32 之前,我习惯先用 PC 的串口助手或 Python 脚本验证屏幕能不能响应。这样能把“屏幕问题”和“主控代码问题”分开。
import serial import struct import time # 打开串口,参数要和屏幕一致 ser = serial.Serial( port='COM3', # Windows 下是 COMx,Linux 下是 /dev/ttyUSB0 baudrate=115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=0.1 ) def build_write_frame(addr, value): """构造迪文写变量帧""" frame = bytearray() frame.append(0x5A) frame.append(0xA5) frame.append(0x05) # 数据长度 frame.append(0x82) # 写指令 frame.append((addr >> 8) & 0xFF) # 地址高字节 frame.append(addr & 0xFF) # 地址低字节 frame.append((value >> 8) & 0xFF) # 数据高字节 frame.append(value & 0xFF) # 数据低字节 return bytes(frame) # 往 0x1000 写 1 cmd = build_write_frame(0x1000, 1) ser.write(cmd) print("发送:", cmd.hex()) time.sleep(0.05) # 读回显,迪文屏通常不回写指令的应答,但读指令会返回 response = ser.read(16) if response: print("收到:", response.hex()) else: print("无应答,检查接线和地址") ser.close()这段代码的逻辑很直接:拼帧、发送、等回显。参数说明:addr是变量地址,value是 16 位无符号整数。如果你要写 32 位数据,需要拆成两条写指令,或者用0x82指令连续写两个地址。
提示:迪文屏对写指令通常不返回应答,所以
ser.read可能为空。不要以为没回复就是失败,直接看屏幕上的控件有没有变化。
3.2 STM32 串口发送:HAL 库下的帧封装与中断接收
在 STM32 上用 HAL 库发迪文帧,核心是把上面的 Python 逻辑翻译成 C。我一般会写一个dwin_write_var函数,放在main.c或单独的dwin.c里。
#include "main.h" #include <string.h> extern UART_HandleTypeDef huart1; void dwin_write_var(uint16_t addr, uint16_t value) { uint8_t frame[8]; frame[0] = 0x5A; frame[1] = 0xA5; frame[2] = 0x05; frame[3] = 0x82; frame[4] = (addr >> 8) & 0xFF; frame[5] = addr & 0xFF; frame[6] = (value >> 8) & 0xFF; frame[7] = value & 0xFF; HAL_UART_Transmit(&huart1, frame, 8, 100); }调用时直接dwin_write_var(0x1000, 1);就行。注意HAL_UART_Transmit的超时参数设 100ms,太短可能在低波特率下丢数据。
接收方向稍微麻烦一点。迪文屏主动上报的帧(比如触摸按钮)也是5A A5开头,但长度不固定。我一般用中断接收 + 环形缓冲区,在回调里判断帧头,然后按长度字段取完整帧。
#define RX_BUF_SIZE 64 uint8_t rx_byte; uint8_t rx_buf[RX_BUF_SIZE]; uint8_t rx_len = 0; uint8_t frame_ready = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rx_buf[rx_len++] = rx_byte; // 至少收到帧头+长度+指令 if (rx_len >= 3 && rx_buf[0] == 0x5A && rx_buf[1] == 0xA5) { uint8_t data_len = rx_buf[2]; if (rx_len >= data_len + 3) { frame_ready = 1; // 一帧完整 rx_len = 0; } } else if (rx_len >= RX_BUF_SIZE) { rx_len = 0; // 防溢出 } HAL_UART_Receive_IT(&huart1, &rx_byte, 1); } }这段代码的关键是:data_len是帧里第 3 个字节,表示从指令码到数据末尾的字节数。完整帧长 =data_len + 3(帧头 2 字节 + 长度字节本身)。收到完整帧后置frame_ready,主循环里再解析。
3.3 和 PLC、LabVIEW 对接时的参数差异
PLC 侧如果用 RS232 串口通信模块,注意迪文屏是 3.3V TTL 电平还是 RS232 电平。老款迪文屏有的直接出 RS232 电平,有的只出 TTL,接 PLC 的 RS232 口需要加电平转换。Easy320 PLC 的串口通信编程里,发送缓冲区要按字节填,不要用字符串函数,因为0x5A在 ASCII 里是Z,容易被转义。
LabVIEW 用 VISA Write 发字节数组,注意把“显示模式”切成十六进制,否则你看到的是乱码。Unity 串口通信则要注意SerialPort.Read的线程阻塞问题,建议开独立线程读,主线程只更新 UI。
4. 迪文屏串口通信避坑:5 个让我熬夜的翻车现场
4.1 现象:屏幕收到数据但控件不更新
原因:变量地址写对了,但控件绑定的不是这个地址,或者控件类型不匹配。比如你往文本控件绑定的地址写0x0001,它显示的是 ASCII 字符而不是数字“1”。
解决:在 DGUS 工具里确认控件的“变量地址”和“数据类型”。文本控件要显示数字,需要勾选“数字显示”或使用“数据变量”控件。图标控件则要求写入的值在图标范围内。
4.2 现象:STM32 发指令后屏幕无反应,但 PC 串口助手发同样的指令正常
原因:STM32 的串口 TX 和 RX 接反了,或者共地没接。还有一种可能是 STM32 的串口引脚配置成了开漏输出,驱动能力不够。
解决:先用示波器看 TX 引脚有没有波形。如果没有,检查 GPIO 初始化和复用功能配置。如果有波形但屏幕不响应,把 STM32 的 TX 直接短接到 PC 串口的 RX,用串口助手看发出的数据对不对。
4.3 现象:读指令返回的数据长度不对
原因:迪文屏的读指令返回帧里,长度字段是“数据长度”,不是“总帧长”。比如你读 2 个字长,返回的帧可能是5A A5 06 83 10 00 02 00 01,其中06是从指令码到数据末尾的字节数。
解决:解析时先取rx_buf[2]作为data_len,然后等rx_len >= data_len + 3再处理。不要用固定长度读,否则会截断或粘连。
4.4 现象:波特率 115200 下偶尔丢帧
原因:STM32 的串口中断优先级太低,被其他中断打断;或者 HAL 库的HAL_UART_Receive_IT在高速下来不及重新武装。
解决:把串口中断优先级设到最高(抢占优先级 0),或者改用 DMA 接收。DMA 方式下,配置HAL_UART_Receive_DMA,在空闲中断里取数据,能彻底解决丢帧。
4.5 现象:屏幕主动上报的触摸数据解析错乱
原因:迪文屏的触摸上报帧可能和写指令的应答混在一起,如果缓冲区没做好帧同步,会把两帧粘成一帧。
解决:在解析函数里加一个状态机,只有收到5A A5才开始计数,收到完整长度后立即处理并重置缓冲区。不要用memcpy一次性搬整个缓冲区,按字节喂状态机最稳。
5. 进阶:用 Python 脚本自动扫描变量地址与协议验证
当你手里只有一块屏、没有工程文件时,最头疼的是不知道变量地址分配。我一般会写一个扫描脚本,从0x1000到0x10FF逐个写值,同时观察屏幕哪个控件在变。虽然笨,但有效。
import serial import time ser = serial.Serial('COM3', 115200, timeout=0.05) def write_var(addr, value): frame = bytes([0x5A, 0xA5, 0x05, 0x82, (addr >> 8) & 0xFF, addr & 0xFF, (value >> 8) & 0xFF, value & 0xFF]) ser.write(frame) # 扫描 0x1000 到 0x1010,每个地址写 1 再写 0 for addr in range(0x1000, 0x1011): write_var(addr, 1) time.sleep(0.2) write_var(addr, 0) time.sleep(0.2) print(f"已测试地址: 0x{addr:04X}") ser.close()跑这个脚本时,眼睛盯着屏幕。哪个控件闪了一下,那个地址就是它的。记下来,填进你的地址表。这个方法对文本控件、图标控件都管用,但对曲线控件可能不明显,因为曲线需要连续写多个值。
另一个技巧是用读指令验证地址是否存在。迪文屏对不存在的地址读操作可能返回全0xFF或全0x00,而对存在的地址会返回实际值。你可以先写一个已知值,再读回来对比。
def read_var(addr, word_count=1): frame = bytes([0x5A, 0xA5, 0x04, 0x83, (addr >> 8) & 0xFF, addr & 0xFF, word_count]) ser.write(frame) time.sleep(0.05) resp = ser.read(32) return resp.hex() # 先写再读 write_var(0x1000, 0x1234) time.sleep(0.1) print(read_var(0x1000))如果返回的帧里包含12 34,说明地址有效且可读写。如果返回FF FF或空,要么地址不存在,要么屏幕没配置这个变量。
最后说一个我自己的习惯:每次调通一个迪文屏项目,我都会把变量地址表、波特率、帧格式示例写成一个README.txt放在工程根目录。下次再遇到同型号屏,直接翻记录,省得重新扫地址。这个习惯帮我省了至少三个通宵。希望帮到你。
本文还有配套的精品资源,点击获取