☰
迪文串口屏通用驱动:三层架构与状态机协议解析
2026/10/21 22:35:45 网站建设 项目流程

简介:面向AVR平台的迪文DWIN串口TFT屏通用驱动程序,以标准C语言实现,具备良好的可读性与可移植性,配套两个文件:dwin-display.c负责屏幕初始化、点线矩形圆绘制及文本显示等核心功能,dwin-display.h则定义屏幕尺寸、颜色深度等配置结构并开放简洁API,开发者只需调用dwin_init、dwin_draw_pixel、dwin_print_string等接口即可完成显示控制,无需纠结底层时序。压缩包采用rar格式,共2个文件(1个h、1个c),整体仅3KB,体量极小、便于直接嵌入AVR工程。该驱动兼容多种迪文屏型号与分辨率,能适配工业控制、智能家居、物联网等场景;同时通过阅读源码还可掌握迪文屏SPI/UART串口通信的参数配置及GPIO连接方法,为后续性能优化和低功耗设计提供参考。目前已有831人学习下载,适合嵌入式开发者快速上手。

1. 迪文串口TFT屏通用驱动:从改三次串口解析到一套代码通吃

前两年做一个产线设备仪表项目,主控从某51单片机换到STM32,又因为芯片供应问题换到国产GD32,每次换主控,显示部分的串口解析都要从头改一遍。屏幕始终没换,都是迪文(DWIN)的串口TFT,但每版代码里拼帧、拆帧、等返回的逻辑都和业务混在一起,越改越不敢动。后来我把这块屏的通信单独抽成一套通用驱动:传输层只收发字节,协议层只解析帧,应用层只写业务。换主控时只改两个端口函数,页面和变量地址一行不用动。如果你正在做工业HMI、仪器仪表、设备面板,被迪文屏的DGUS协议、变量字节序和返回帧解析折磨过,这套驱动和它的拆分思路可以直接抄走。

2. 架构与目录设计:把驱动拆成传输、协议、应用三层

2.1 为什么必须分三层

迪文屏本质上是带DGUS操作系统的显示终端。屏幕内部的串口接收、帧校验、字库和FLASH存储全部封装在屏里,主控能控制的只有串口上的一帧一帧字节。所以驱动能不能复用,取决于“发字节”“帧长什么样”“收到帧之后干什么”这三件事有没有被拆开。

我见过比较多反例是直接把屏幕通信写在业务里:测温函数里拼一个数组塞给串口,按键回调里再写一段解析。页面一多,全局数组和临时标志位缠在一起,排查一个乱码要打好几个断点。分层之后,驱动和MCU外设解耦,也和应用业务解耦,这是整个驱动设计的底线。

顺带说一句,回调不能放到串口中断里直接执行。因为回调里一旦出现耗时操作,比如写日志、翻转继电器,中断响应时间就会被拖长,后续字节跟着丢。所以整个驱动的原则是:中断只入FIFO,业务回调全部放到 dwin_poll 里执行。这么做还有个衍生好处,将来接RTOS时可以把 dwin_poll 单独放一个任务,用户不用关心临界区。

2.2 目录结构与模块定位

驱动源码内部是这样组织的:

dwin_drv/ ├── dwin_config.h // 帧格式、波特率、校验开关、超时参数 ├── dwin_port.h // 移植层接口声明 ├── dwin_port.c // 串口收发、毫秒时间戳(换平台只改这里) ├── dwin_fifo.c // 环形缓冲区,串口中断到解析器的中转 ├── dwin_proto.c // 帧组装、帧解析状态机、校验 ├── dwin_core.c // 对外API、回调表、事件分发 └── demo/ ├── demo_stm32_uart.c └── demo_rtos_thread.c

dwin_config.h 是整个驱动的参数总闸。帧模式(旧版AA帧还是新版5A A5帧)、是否启用校验、波特率、接收空闲超时时间都放在这里,调屏幕型号时第一件事就是改这个头文件。dwin_fifo.c 是典型的环形缓冲区:串口中断只做一步操作,把字节压进FIFO;主循环里的 dwin_poll() 再从FIFO取字节喂给状态机。这个设计保证中断里没有耗时逻辑,也不丢字节。

dwin_proto.c 是这个驱动最核心的文件。它负责把“写变量、读变量、写文本、跳页”请求拼成符合屏幕协议的一帧,同时把屏幕返回的原始字节流还原成带指令号、地址、数据的数据包。dwin_core.c 站在更上层,对外暴露简洁的API,比如 dwin_write_uint16(0x1000, &val, 1),内部先查配置,再调协议层拼帧,最后经端口层发出去。这样换屏幕型号时,应用层调用的接口可以完全不变。

2.3 对外接口设计:6个函数覆盖90%场景

dwin_core 对外提供的核心API如下表:

函数作用关键参数说明
dwin_init初始化协议栈、清空回调表和FIFO无
dwin_poll主循环周期调用,处理FIFO中的字节无
dwin_write_uint16向变量地址写1到N个字起始地址、数据指针、字数
dwin_read_uint16向屏幕发出读变量请求起始地址、字数
dwin_set_text给文本控件写入字符串地址、字符串、长度
dwin_register_callback注册返回帧与按键上报回调事件类型、回调函数指针

dwin_write_uint16 是使用频率最高的函数。温度、速度、计数值这类数据,在DGUS工程里绑定到一个整数数据变量显示控件,驱动往对应的变量地址写一个字,屏幕自动刷新。dwin_set_text 适合给标签控件写单位或状态文字,它内部会把字符串放到帧数据区。这里要多留一个心眼:中文字符串必须按屏幕烧录时的编码(一般是GB2312)发送,否则显示乱码。

dwin_read_uint16 是异步的,发出请求之后不能立刻拿到结果,数据要等屏幕返回帧到了、回调被触发后才能拿到。这个异步语义很多第一次接触的人会踩坑,所以驱动里专门做了回调注册机制。

2.4 数据流向:一次dwin_write_uint16调用经历了什么

以写一个变量为例,完整链路是:应用层拿到数值 val=125,调用 dwin_write_uint16(0x1000, &val, 1);dwin_core 把 125 拆成两个字节,高位 0x00 在前、低位 0x7D 在后;dwin_proto 的帧组装函数把这个数据连同指令码 0x82、地址 0x1000 拼成一帧;最后 dwin_port_send 把整帧发给屏幕。由于串口是流式的,所以高位先发。

接收方向是异步的。屏幕收到读变量请求后,返回一帧包含变量值的数据;串口中断把字节塞进FIFO,主循环调用 dwin_poll 时逐字节解析,完整帧解析出来之后按指令号匹配回调。所以 dwin_poll 必须被周期性调用,这一点恰恰是移植时最容易被忽略的,只在初始化时调一次,接收永远不会触发。

在RTOS环境下,dwin_poll 适合放在一个独立线程里循环调用,用户回调也在该线程执行,不会阻塞中断,也不会和业务线程抢串口。

3. 核心模块拆解:帧组装、状态机解析与回调分发

3.1 帧格式:新旧两套协议并存

迪文屏协议在不同固件版本间差异不小,驱动里遇到最多的是两种:旧版以 0xAA 开头的帧,和新版以 5A A5 开头的帧。两者都是“帧头+长度+指令+变量地址+数据+校验”的结构,但长度字段包含的范围不同,校验方式也可能不同。

驱动通过编译期宏选择帧模式:

/* dwin_config.h */ #define DWIN_FRAME_MODE_NEW 1 /* 1: 5A A5帧; 0: 旧版AA帧 */ #define DWIN_FRAME_LEN_INCLUDE_CMD 1 /* 长度字段是否包含指令本身 */ #define DWIN_FRAME_TAIL_SUM 1 /* 帧尾是否附带求和校验 */

先看 5A A5 模式的帧结构,一个完整的写变量帧是这样:

字段字节数说明
0x5A 0xA52帧头
长度2从指令字节到数据末尾的字节数,小端
指令10x82写变量、0x83读变量、0x84写文本
地址2DGUS变量地址,高字节在前
数据N实际数据
校验1对指令、地址、数据求和取低字节

需要提醒的是,具体型号的固件对长度字段的含法、校验形式可能有差异。拿到屏之后要先看它随附的DGUS协议手册,再决定 DWIN_FRAME_LEN_INCLUDE_CMD 置1还是置0。这个配置对不上时,通常就是手册和代码没统一。

3.2 发送端:可变长帧组装函数

帧组装函数是发送侧的核心。它把指令、地址、数据三个部分拼成完整帧,并自动处理长度字段和校验。

uint16_t dwin_build_frame(uint8_t *buf, uint8_t cmd, uint16_t addr, const uint8_t *data, uint16_t data_len) { uint16_t i = 0; #if DWIN_FRAME_MODE_NEW buf[i++] = 0x5A; /* 帧头 */ buf[i++] = 0xA5; #else buf[i++] = 0xAA; #endif uint16_t len = 0; len += 1; /* 指令占1字节 */ len += 2; /* 地址占2字节 */ len += data_len; /* 数据区 */ if (DWIN_FRAME_LEN_INCLUDE_CMD) { /* 长度从指令开始算,上面len已经包含指令 */ } else { len -= 1; /* 长度不包含指令 */ } len += DWIN_FRAME_TAIL_SUM ? 1 : 0; /* 校验字节计入长度 */ #if DWIN_FRAME_MODE_NEW buf[i++] = len & 0xFF; /* 长度低字节在前 */ buf[i++] = (len >> 8) & 0xFF; #else buf[i++] = len & 0xFF; /* 旧版按单字节长度处理 */ #endif buf[i++] = cmd; /* 指令 */ buf[i++] = addr >> 8; /* 地址高字节 */ buf[i++] = addr & 0xFF; /* 地址低字节 */ uint8_t sum = cmd + (uint8_t)(addr >> 8) + (uint8_t)(addr & 0xFF); for (uint16_t j = 0; j < data_len; j++) { sum += data[j]; buf[i++] = data[j]; } if (DWIN_FRAME_TAIL_SUM) { buf[i++] = sum; /* 求和校验 */ } return i; /* 返回整帧长度 */ }

这段代码最需要注意的地方是长度字段的计算。它把指令、地址、数据三部分相加,再根据宏决定是否去掉指令字节、是否加上校验字节。如果你在实测中发现屏幕对长度字段理解不一致,问题几乎都出在这三个宏的组合上。数据区按顺序拷贝后,校验字节使用最朴素的求和取低字节,如果屏幕手册要求CRC,只需把这一段替换成CRC函数。

3.3 接收端:状态机拆帧,而不是等完整中断

接收侧我习惯用一个轻量状态机,而不是在串口中断里攒数组。状态机的优势是随时可以从噪声中重新同步到帧头,不需要额外空闲定时器去判断“一帧结束了”。

typedef enum { ST_WAIT_5A = 0, ST_WAIT_A5, ST_WAIT_LEN_L, ST_WAIT_LEN_H, ST_WAIT_CMD, ST_WAIT_DATA } dwin_parse_state_t; typedef struct { dwin_parse_state_t state; uint16_t len; /* 帧体总长度(含指令与数据) */ uint16_t idx; /* 已接收字节数 */ uint8_t buf[64]; /* 帧体缓冲 */ uint8_t cmd; uint32_t last_byte_tick; /* 上次收到字节的时间戳 */ } dwin_parser_t;

解析函数逐字节输入,每收到一个字节驱动状态机往前步进:

uint8_t dwin_parse_fsm(dwin_parser_t *p, uint8_t ch) { switch (p->state) { case ST_WAIT_5A: if (ch == 0x5A) p->state = ST_WAIT_A5; break; case ST_WAIT_A5: p->state = (ch == 0xA5) ? ST_WAIT_LEN_L : ST_WAIT_5A; break; case ST_WAIT_LEN_L: p->len = ch; p->state = ST_WAIT_LEN_H; break; case ST_WAIT_LEN_H: p->len |= (uint16_t)ch << 8; p->idx = 0; p->state = ST_WAIT_CMD; break; case ST_WAIT_CMD: p->cmd = ch; p->buf[p->idx++] = ch; p->state = ST_WAIT_DATA; break; case ST_WAIT_DATA: p->buf[p->idx++] = ch; if (p->idx >= p->len) { /* 收满整个帧体 */ p->state = ST_WAIT_5A; return 1; } break; default: p->state = ST_WAIT_5A; break; } return 0; }

这里需要特别说明 ST_WAIT_CMD 和 ST_WAIT_DATA 的区别。ST_WAIT_CMD 只收一个字节,也就是指令码;ST_WAIT_DATA 则继续收剩余的帧体字节,包括地址和数据。当 idx 累加到等于 len 时,说明一帧完整到达,此时返回1,dwin_poll 会把帧体交给回调分发函数。如果中途插入噪声字节,状态机会回到 ST_WAIT_5A 重新找帧头,不会把错位数据当成有效帧。

注意:帧体缓冲 buf 默认64字节,如果屏幕有超过60字节数据的返回帧,记得把数组加大,否则会越界覆盖。

3.4 超时复位:给状态机一条后悔药

状态机虽然能自动找回帧头,但有一种情况很让人抓狂:屏幕回帧还没收完,线缆抖了一下,从此后续所有字节都错位。所以我加了一个接收超时机制。dwin_poll 每次被调用时,会顺便检查当前帧是否太久没有新字节到达,超时就强制复位。

void dwin_poll(void) { dwin_parser_t *p = &g_parser; uint32_t now = dwin_port_get_tick_ms(); if (now - p->last_byte_tick > DWIN_RX_TIMEOUT_MS) { p->state = ST_WAIT_5A; /* 超时,状态机回到等帧头 */ p->idx = 0; } while (!dwin_fifo_is_empty(&g_fifo)) { uint8_t ch = dwin_fifo_pop(&g_fifo); p->last_byte_tick = dwin_port_get_tick_ms(); if (dwin_parse_fsm(p, ch)) { dwin_dispatch(p); } } }

超时时间我一般设5ms,因为屏幕在115200波特率下发送几十字节的返回帧耗时都在1ms以内,5ms足够宽裕,又不会让噪声长时间影响状态机。移植到不同平台时,dwin_port_get_tick_ms 是唯一需要新增的时间函数,很多MCU的SysTick就能提供。这个“超时复位”解决的是串口屏项目里非常典型的玄学问题:线缆质量不好时,明明引脚没接错,程序却间歇性收不到回调。

3.5 回调分发:把帧体翻译成业务语义

完整帧进入 dispatch 后,还需要从协议层视角翻译成业务事件。驱动维护一张回调表,调用方可以针对不同事件注册处理函数:

typedef struct { uint8_t cmd; uint16_t addr; uint8_t *data; uint16_t len; } dwin_pkt_t; typedef void (*dwin_callback_t)(const dwin_pkt_t *pkt); static dwin_callback_t s_cb_table[8]; void dwin_register_callback(dwin_evt_id_t evt, dwin_callback_t cb) { if (evt < 8) s_cb_table[evt] = cb; } static void dwin_dispatch(dwin_parser_t *p) { dwin_pkt_t pkt; pkt.cmd = p->buf[0]; pkt.addr = (uint16_t)((p->buf[1] << 8) | p->buf[2]); pkt.data = &p->buf[3]; pkt.len = p->len - 3; /* 从总长度中去掉指令和地址 */ if (pkt.cmd == DWIN_CMD_READ_ANSWER && s_cb_table[DWIN_EVT_READ]) { s_cb_table[DWIN_EVT_READ](&pkt); } }

调用方使用时,只需要关心“某个事件到了我该做什么”,而不用关心帧是怎么收上来的。比如屏幕上的按键控件在DGUS工程里配置好上报功能后,按下时屏幕会主动回一帧,驱动解析出地址和值,触发 DWIN_EVT_KEY 回调,应用层在回调里判断地址属于哪个按键,再执行对应逻辑。

4. 运行指南:从移植到点亮第一块屏

4.1 移植层:只改两个半函数

驱动把和硬件相关的部分收拢在 dwin_port.c 里,换平台时只需要实现四个简单函数:初始化、发送字节数组、接收一个字节、获取tick。

/* dwin_port.h */ void dwin_port_init(uint32_t baud); void dwin_port_send(uint8_t *buf, uint16_t len); uint8_t dwin_port_get_char(void); /* 从FIFO取一个字节,无数据返回0 */ uint32_t dwin_port_get_tick_ms(void); /* 毫秒时间戳 */

以STM32为例,串口初始化用USART1,115200波特率,8位数据、无校验、一位停止位。发送函数就是循环写发送寄存器:

void dwin_port_send(uint8_t *buf, uint16_t len) { for (uint16_t i = 0; i < len; i++) { while (!LL_USART_IsActiveFlag_TXE(USART1)); LL_USART_TransmitData8(USART1, buf[i]); } }

接收方向不是轮询,要走中断。串口中断里只做一件事,把收到的字节压进FIFO:

void USART1_IRQHandler(void) { if (LL_USART_IsActiveFlag_RXNE(USART1)) { uint8_t ch = LL_USART_ReceiveData8(USART1); dwin_port_on_rx_byte(ch); /* 内部就是 fifo_push */ } }

dwin_port_get_char 则从同一个FIFO取字节。连接屏幕时三根线必须共地,屏幕TXD接MCU的RX,RXD接MCU的TX;如果屏幕是5V逻辑而MCU是3.3V,需要电平转换,否则长时间运行可能出现随机错误帧。这个细节在项目现场踩了不止一次。

注意:屏幕供电别和主控共用带强干扰的电源,电机启停时屏幕容易重启,串口也会跟着丢数据。

4.2 最小跑通:让屏幕显示一个每秒变化的数

驱动移植完成后,最小可运行工程非常短。前提是你在DGUS软件里建好工程,把某个数据变量显示控件绑定到变量地址 0x1000,并把工程下载到屏幕。然后主循环里这样写:

#include "dwin_drv.h" int main(void) { dwin_port_init(115200); dwin_init(); uint16_t counter = 0; while (1) { counter++; dwin_write_uint16(0x1000, &counter, 1); dwin_poll(); hal_delay_ms(1000); } }

这段代码干的活就是:每秒把 counter 的值通过串口写到0x1000地址,屏幕上的数据变量显示控件读到新值后刷新显示。如果0x1000被当成字节地址而不是字地址,显示就会错乱,因为DGUS的变量地址是字地址,一个地址对应两个字节。

跑通这一步,证明通信链路没问题。接下来再试读变量、写文本、处理按键上报就水到渠成。注意主循环里不能没有 dwin_poll(),否则接收方向的按键上报和读变量返回永远不处理。

4.3 先离线验证,再上驱动代码

每次拿到一块新屏,我建议先别直接烧驱动,先用USB转TTL加串口助手,在PC上把屏幕的协议摸清楚。操作步骤是:USB转TTL的TXD接屏幕RXD,RXD接屏幕TXD,共地;串口助手选HEX发送,波特率先设115200;手动发一条读版本号指令;观察返回帧内容。

读版本号指令一般是 0x80 或 0x81,不同固件定义不同。完全可以在串口助手先输入:

5A A5 04 80 00 00 00 01

如果这条指令格式正确,屏幕会回一帧包含固件版本的报文。这里的帧长度和校验要和屏幕手册逐字节核对,核对通过后再把这套格式用到驱动配置里。很多人跳过了这个验证步骤,直接拿驱动代码烧进去,然后花两小时怀疑是波特率还是电平问题,其实问题只是长度字段差了一个字节。

4.4 运行配置参数一览

配置项推荐值说明
波特率115200或屏幕实际设置屏幕上的OS参数也要一致
DWIN_FRAME_MODE_NEW按屏固件选新版固件用1
DWIN_RX_TIMEOUT_MS5空闲超时,状态机复位用
FIFO大小128字节大于最长返回帧
校验方式求和或按手册改不匹配时帧会被解析成噪声

还有一个常见的误解是波特率越高越好。其实屏幕DGUS在处理某些长数据时,发送间隔比波特率更影响稳定性。波特率拉到921600看上去很酷,但批量写变量时丢帧概率明显上升,因为屏幕端解析速度跟不上。工控场景我一般稳定在115200,帧间隔留够,比硬上高频更可靠。

5. 避坑指南:串口屏调试中的高频踩坑记录

5.1 屏幕上电白屏,串口收发毫无反应

现象:程序烧进去,屏幕白屏或停在开机页,串口助手发任何指令都不回显。

原因:最常见的是波特率不一致。屏幕内部OS和DGUS配置里的波特率与主控串口初始化不一致,有的固件默认9600,有的默认115200;还有一部分屏在上电时必须先收到正确的帧头才开放通信。

解决:先用串口助手,从9600到921600逐个波特率发送读版本指令,看哪个波特率能收到返回帧。确认后再改 dwin_config.h 里的波特率配置,并把屏幕端配置一并改到一致。如果是新屏,可以在DGUS软件里检查工程和OS的波特率设置。

5.2 显示正常,但批量写变量时偶尔丢数

现象:单独写一个变量没问题,一次性写十几个变量,总有一两个变量没有被刷新。

原因:连续的帧间隔太短,屏幕DGUS解析来不及处理。串口发送虽然是流式的,但屏幕端需要时间把收到的帧解析、写变量、刷新显示,尤其图库较大的页面,处理耗时更长。

解决:驱动里在每帧发送之间加2到5ms延时,并且把相邻地址的写入合并成一次写多个字。迪文屏的变量地址是连续的,0x1000到0x1007这8个地址可以一次帧写8个16位字,网络开销小很多,也比分帧发送稳定。我在驱动里封装了按地址段批量写的接口,遇到需要整块刷新的仪表盘数据时切到批量写。

5.3 读回来的变量值,和屏上看到的不一致

现象:屏幕上显示125,驱动读回来的数值却是32000,或者错开2个字节。

原因:DGUS变量地址是16位字地址,读一个变量返回一个字(两字节),高字节在前。如果驱动按字节去解析,比如把返回的两个字节当成两个8位数据,就会错位。另一个常见原因是地址计算错误:0x1000到0x1002差两个地址,实际上是两个字,中间隔4个字节。

解决:读变量时明确按字来申请,数据解析统一用 uint16_t 数组,高位在前。我在驱动里读变量回调用的是 uint16_t 指针,不提供按字节解析的接口,从源头避免这个错误。另外DGUS工程里变量地址通常从0x1000往上分配,建议在项目文档里建一张变量地址分配表,不然过两周就忘了0x1000是哪一路温度。

5.4 状态机被干扰帧卡住,回调忽然不再触发

现象:系统运行几小时后,按键回调彻底不响应,重新上电又好了。

原因:串口线或电源质量差,导致某个噪声字节破坏了正在解析的半帧。状态机没有超时复位,就一直卡在等数据的错误状态,后续任何帧都跟不上。

解决:驱动里增加接收超时复位,就是前面 dwin_poll 里那段逻辑,超过5ms没有新字节,强制回到等帧头状态。这个功能一定要在量产版本里保留,否则现场电源波动一次,机器就得断电重启,这是工业环境里最典型的玄学故障之一。

5.5 用DMA接收数据错乱,中断逐字节模式却正常

现象:把串口接收改成DMA加空闲中断后,返回帧偶尔顺序错乱,甚至出现帧头被误判成数据。

原因:最常见的是调试时自发自收。USB转TTL的TXD和RXD接反,或者把主控的TX接到了调试串口上,导致主控自己发送的字节被DMA收进来。DMA方式和中断方式的区别在于,中断模式会过滤掉一部分自发字节,而DMA会把所有字节都收进缓冲区,问题暴露得更明显。

解决:先确认物理接线,三线共地;DMA调试阶段在接收路径上加一个环回抑制,发送期间关闭接收方向,发送完成后恢复。如果屏幕和主控电平不匹配,也会出现类似现象,这时需要加电平转换电路,而不是调大波特率。

6. 二次开发:从通用驱动长出自己的业务专属驱动

6.1 扩展指令映射表

驱动把指令号和业务语义解耦之后,新增屏幕功能只需要扩展一张指令映射表和一个分发分支。我习惯把屏幕所有可用指令集中定义在一张表里:

typedef struct { uint8_t cmd; const char *name; } dwin_cmd_map_t; static const dwin_cmd_map_t s_cmd_map[] = { { 0x82, "write_var" }, { 0x83, "read_var" }, { 0x84, "write_text" }, { 0x90, "jump_page" }, { 0x00, NULL } };

增加新功能时,比如要支持屏幕固件里的曲线数据上传,只需要在表里加一条{ 0x91, "upload_curve" },然后在 dispatch 里加一个路由分支。好处是新人接手时扫一眼这张表就能知道驱动支持哪些操作,不用翻代码。

6.2 把驱动搬进RTOS线程

如果项目里用FreeRTOS,dwin_poll 可以放进一个独立线程,接收中断仍然只负责字节进FIFO:

void dwin_task(void *arg) { for (;;) { dwin_poll(); osDelay(1); } }

发送路径需要保护。驱动内部组装帧用的是静态缓冲,跨线程调用时两个线程可能同时往缓冲区里写,导致帧头和数据错开。我的做法是对 dwin_send 加一个互斥锁,把“组装加发送”作为一个临界区。接收侧不受影响,因为FIFO是单生产者单消费者模型,中断是唯一生产者,dwin_poll 是唯一消费者。

6.3 验证方法:先回环后联调

二次开发每次改完协议层,我都先做回环测试,不接屏幕,把主控的TXD直接接到自己的RXD,自发自收一帧,验证组装和解析能对上。这一步能分辨问题是出在驱动还是屏幕上。回环通过后再接屏幕,跑一遍变量读写和全部回调。从那以后,我每换一个屏型号,第一件事永远是串口助手手动发读版本指令,确认帧格式再动驱动代码,这套流程帮我省掉的调试时间远比想象中多。希望这些拆分和调试习惯能帮到你。

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

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

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

立即咨询