嵌入式MCU与迪文屏通信:DMA+环形FIFO+变长队列+状态机完整手册
"十年前,我还是个只会用阻塞Delay写代码的年轻小子,现在嘛...我学会了用DMA+状态机让CPU去喝茶。"
"本文带你从零构建一套工业级的串口屏数据接收框架,学会之后,你也可以像老油条一样优雅地摸鱼了。"
📌 写在前面
在嵌入式项目中,串口屏是人机交互的主力担当。而迪文屏(DWIN)凭借其稳定的DGUS系统,在国内工控领域混得风生水起。
本文基于 STM32F429 + HAL库 实战的工程项目,讲解一套四层缓冲架构的完整实现:
DMA接收缓冲 → 环形FIFO → 变长队列 → 协议状态机解析
读完本文你能学到:
- 🎯 DMA+空闲中断的串口接收精髓
- 🎯 环形FIFO的Stream模式实战
- 🎯 变长帧队列的设计与实现
- 🎯 迪文DGUS协议帧解析状态机
- 🎯 触摸/按键数据的业务层处理
⚠️ 阅读姿势建议 :左手拿代码,右手拿咖啡,眼睛盯着波形debug。如果你还在用
HAL_UART_Receive_IT阻塞式接收,那赶紧扶稳坐好------老司机要发车了。
一、整体架构全景图
在掏出代码之前,先让我们居高临下看一眼这个系统的全貌:
┌──────────────────────────────────────────────────────────────────────────────┐
│ 🌟 迪文屏 (T5L DGUS屏) │
│ 触摸/按键 → 串口TX → 0x5A 0xA5 Length Cmd Data... │
└─────────────────────────────────────┬────────────────────────────────────────┘
│ RS232/RS485 物理层
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📡 STM32 UART3 + DMA │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ gu8A_rxbuf_uart3[4096] (DMA环形缓冲) │ │
│ │ 0 tail_ptr→ head_ptr→ │ │
│ │ ├─────────┬───────────────────────────────────────────┤ │ │
│ │ │已接收数据│ 空余区域 │ │ │
│ │ └─────────┴───────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┴────────────────┐ │
│ │ HAL_UART3_RxIdleCallback() │ ← UART空闲中断 │
│ │ (帧接收完成触发) │ │
│ └────────────────┬────────────────┘ │
└───────────────────────────────────────┼──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ 🔄 环形FIFO (Ring FIFO) - Stream模式 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ head → tail → │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │ │
│ │ │5A│A5│07│83│00│14│01│5A│A5│07│82│...│...│...│ │ │ │ │ │ │ │
│ │ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ uart3_read() → gu8A_rxbuf_uart3_temp[] → Dat_enUnFixQueueNode() │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📦 变长队列 (UnFixQueue) - 支持不定长帧 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ PosBuf[节点位置信息数组] DataBuf[数据存储区] │ │
│ │ ┌─────────┐ ┌─────────────────────────────┐ │ │
│ │ │Node 0 │ ──────────────→ │ [帧1数据....][帧2数据...] │ │ │
│ │ │Node 1 │ └─────────────────────────────┘ │ │
│ │ │Node 2 │ │ │
│ │ └─────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────┬──────────────────────────────────────┘
|
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ ⚙️ 协议解析层 - Dwin_Rv_Handler() 状态机 │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ State 0: 等待0x5A帧头 │ │
│ │ State 1: 等待0xA5帧头 │ │
│ │ State 2: 读取Length │ │
│ │ State 3: 读取Cmd(命令字) │ │
│ │ State 4+: 读取Data数据 │ │
│ │ ↓ │ │
│ │ 帧完成 → DwinRvDatF=1 → DwinRvFIFO[DwinFrame] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ 🎮 业务逻辑层 - ScreenScan() / App_GuiSchedule() │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ switch(TouchKey) { │ │
│ │ case KEY_0...KEY_9: // 数字键处理 │ │
│ │ case KEY_UP/DOWN: // 上下键处理 │ │
│ │ case KEY_OK/ESC: // 确认/取消 │ │
│ │ } │ │
│ │ Obj_LcdUart页面调度 → SwitchPage() → GUI界面更新 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
二、第一层:DMA硬件接收
2.1 DMA+串口初始化
c
/* uart1_rt.c */
#define UART3_DMA_RX_BUFLEN 4096 // DMA环形缓冲区大小
uint8_t gu8A_rxbuf_uart3[UART3_DMA_RX_BUFLEN]; // DMA接收缓冲
uint8_t gu8A_rxbuf_uart3_temp[UART3_DMA_RX_BUFLEN]; // 临时缓冲
void MyUartInit(void)
{
// ① 开启UART3空闲中断 (Idle Line Detection)
__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
// ② 开启DMA发送完成中断
__HAL_DMA_ENABLE_IT(&hdma_usart3_tx, DMA_IT_TC);
// ③ 清除空闲标志位 (重要!防止初始化时误触发)
__HAL_UART_CLEAR_IDLEFLAG(&huart3);
// ④ 启动DMA接收 ⭐ 关键: 让DMA提前准备好接收
HAL_UART_Receive_DMA(&huart3, gu8A_rxbuf_uart3, sizeof(gu8A_rxbuf_uart3));
}
💡 知识点扩展:
为什么要开启**空闲中断(IDLE)**而不是只在DMA半满/满中断里处理?
中断类型 触发时机 适用场景 RxHalfCpltCallbackDMA传输到一半 大数据缓冲,防止FIFO溢出 RxCpltCallbackDMA传输完成 固定长度帧 UART_IT_IDLE串口空闲线检测 ✅ 变长帧/一帧一处理 空闲中断的精髓在于:当RX线在超过1帧时间没有数据时触发,这意味着我们可以准确知道"一帧数据接收完毕"这个时间点。
2.2 DMA缓冲区管理原理
DMA缓冲区是一个环形结构,tail_ptr指向DMA当前接收位置,head_ptr是我们已经读取过的位置:
c
/* uart1_rt.c - 空闲中断回调 */
void HAL_UART3_RxIdleCallback(UART_HandleTypeDef *huart)
{
uint32_t tail_ptr;
uint32_t copy, offset;
// ① 获取DMA当前接收位置 (总长度 - 剩余未接收数)
tail_ptr = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// ② 计算有效数据范围
offset = uart3_rt.head_ptr % huart->RxXferSize;
copy = tail_ptr - offset;
// ③ 写入环形FIFO
uart3_write_rx_fifo(huart->pRxBuffPtr + offset, copy);
// ④ 更新已读位置
uart3_rt.head_ptr += copy;
// ⑤ ⭐ 关键: 将数据转入变长队列
gu32_Len = uart3_fifo_cnt();
uart3_read(&gu8A_rxbuf_uart3_temp, gu32_Len);
if((0 < gu32_Len) && (gu32_Len <= COM3_FRAM_MAX_SIZE))
{
Dat_enUnFixQueueNode(qHandleCom3Rx, gu8A_rxbuf_uart3_temp, gu32_Len);
}
}
图示理解DMA缓冲区读写:
初始状态:
┌─────────────────────────────────────────────────────────────┐
│ head=0 │
│ │ │
│ ▼ │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ← 空
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
│ ▲ │
│ │ │
│ tail=0 │
收到数据后 (假设收到 5A A5 07 83 00 14 01):
┌─────────────────────────────────────────────────────────────┐
│ │
│ head=0 │
│ │ │
│ ▼ │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ │5A│A5│07│83│00│14│01│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
│ ▲ ▲
│ │ │
│ tail=7 head=0
读取后 (copy=7, head更新为7, 环形buffer从头覆盖):
┌─────────────────────────────────────────────────────────────┐
│ │
│ │
│ head=7 │
│ │ │
│ ▼ │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
│ ▲ ▲
│ │ │
│ tail=7 head=7
三、第二层:环形FIFO
3.1 为什么需要环形FIFO?
你以为DMA缓冲区直接给业务层用就完事了?Too young!
问题场景:
┌────────────────────────────────────────┐
│ DMA缓冲区: [帧1...][帧2...............] │
│ ↑ │
│ DMA正在写入 │
│ │
│ 如果这时候业务层直接读... │
│ → 数据不一致! (帧2可能被DMA覆盖) │
└────────────────────────────────────────┘
解决方案:
DMA缓冲区 ──→ Ring FIFO (解耦生产/消费) ──→ 业务层
↑
用两把锁(或无锁单生产者)
3.2 环形FIFO核心实现
c
/* ring_fifo.c */
struct ring_fifo_t {
volatile uint32_t head; /* 消费者指针 (读) */
volatile uint32_t tail; /* 生产者指针 (写) */
uint32_t size; /* 缓冲区大小 */
uint32_t mask; /* 大小掩码 (size-1,用于位运算替代模运算) */
void *buf; /* 实际存储区 */
enum ring_fifo_type type; /* FIFO类型: FRAME(定长帧)/STREAM(流式) */
};
本项目使用的是 STREAM模式(流式FIFO),关键写入函数:
c
uint32_t ring_fifo_write(struct ring_fifo_t *ring, const void *buf, uint32_t len)
{
uint32_t wlen;
uint32_t unused;
uint32_t off, l;
/* 计算剩余空间 */
unused = ring->size - (ring->tail - ring->head);
/* STREAM模式不需要帧长度前缀 */
wlen = min(len, unused);
if(0 == wlen) { return 0; }
/* 计算写入位置 (tail + frame_off) % size */
off = (ring->tail) & ring->mask; /* 位与掩码替代模运算,性能更高 */
l = min(wlen, ring->size - off);
/* 两段式写入: 解决缓冲区边界跨越问题 */
memcpy((uint8_t *)ring->buf + off, buf, l);
memcpy(ring->buf, (uint8_t *)buf + l, wlen - l);
ring->tail += wlen;
return wlen;
}
🔧 技术细节:
为什么用
ring->mask(掩码)而不是%取模?
c// 假设 size = 256 (2^8), mask = 255 (0xFF) // 位运算 off = (ring->tail) & ring->mask; // 速度: 1 cycle // 取模运算 off = (ring->tail) % ring->size; // 速度: 数十 cycle (除法)在高频中断中,位运算比取模快几十倍。这就是嵌入式优化的艺术------细节是魔鬼。
3.3 环形FIFO的"分身术"
STREAM模式与FRAME模式的区别:
| 模式 | 特点 | 应用场景 |
|---|---|---|
RF_TYPE_STREAM |
无长度前缀,纯数据流 | ✅ 串口接收(本文用这个) |
RF_TYPE_FRAME |
每帧前有4字节长度 | 定长协议包 |
四、第三层:变长队列
4.1 变长队列的设计哲学
环形FIFO解决了生产/消费解耦,但还有一个问题没解决:业务层需要按帧处理,但数据在FIFO里是流式的。
比如FIFO里有这样一串数据:
[5A A5 07 83 00 14][5A A5 04 83 00 10 01][5A A5 05 82 00 50 01 02]
帧1 帧2 帧3
业务层需要完整取出每一帧,怎么办?
"这就需要变长队列登场了------它像个任性的大厨,你告诉它'我要6个饺子',它就给你包6个,不会多也不会少。"
4.2 变长队列数据结构
c
/* UnFixQueue.h */
typedef struct {
uint16_t Start; /* 数据起始位置 */
uint16_t len; /* 数据长度 */
} Pos_Def; /* 节点位置信息 */
typedef struct {
uint16_t MaxFramSize; /* 最大帧尺寸 */
uint16_t PosMaxLen; /* 位置数组长度 (最大节点数) */
uint16_t front; /* 队首指针 */
uint16_t tail; /* 队尾指针 */
uint16_t rear; /* 写入位置 */
uint16_t PoolMaxLen; /* 数据区总长度 */
uint16_t PoolCurCnt; /* 当前已用长度 */
uint8_t *pDateBuf; /* 数据存储区指针 */
Pos_Def *pPosBuf; /* 节点位置信息数组 */
} UnFixQueueDef;
内存布局:
┌─────────────────────────────────────────────────────────────────────┐
│ 变长队列内存模型 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ DataBuf[POOL_SIZE] PosBuf[PosMaxLen] │
│ ┌───────────────┐ ┌─────────┬─────────┬─────────┐ │
│ │[帧1] [帧2] │ │Pos[0] │Pos[1] │Pos[2] │ │
│ │ Start=0 │──────────→│Start=0 │Start=6 │Start=12 │ │
│ │ len=6 │ │len=6 │len=6 │len=7 │ │
│ │ │ │ │ │ │ │
│ │ │ │ front=0 │ rear=3 │ │ │
│ └───────────────┘ └─────────┴─────────┴─────────┘ │
│ ↑ │
│ │ │
│ front → rear 环形 │
└─────────────────────────────────────────────────────────────────────┘
4.3 变长队列核心操作
入队:存储一帧数据
c
/* UnFixQueue.c */
int16_t Dat_enUnFixQueueNode(UnFixQueueDef* pQu, uint8_t *pData, uint16_t Datalen)
{
uint16_t i;
uint16_t StarIndex;
Pos_Def QueMsg;
/* 边界检查 */
if (pQu == NULL) return ERR_POINTER_NULL;
if (Datalen > (pQu->PoolMaxLen - pQu->PoolCurCnt)) return E_QUEUE_FULL;
if (Datalen > pQu->MaxFramSize) return E_QUEUE_FULL;
__set_PRIMASK(1); /* 进入临界区,禁止中断 */
/* 获取最新节点的位置信息 */
QueMsg = Que_GetLatestPosInf(pQu);
/* 计算新帧存储起始位置 (环形覆盖) */
StarIndex = (QueMsg.Start + QueMsg.len) % pQu->PoolMaxLen;
QueMsg.Start = StarIndex;
QueMsg.len = Datalen;
/* 将节点信息加入位置数组 */
Que_InsPosInf(pQu, QueMsg);
/* 将数据写入数据区 */
for(i = 0; i < Datalen; i++) {
pQu->pDateBuf[StarIndex] = pData[i];
pQu->PoolCurCnt++;
StarIndex = (StarIndex + 1) % pQu->PoolMaxLen;
}
__set_PRIMASK(0); /* 退出临界区 */
return E_QUEUE_NORMAL;
}
出队:取出一帧数据
c
/* UnFixQueue.c */
int16_t Dat_deUnFixQueueNode(UnFixQueueDef* pQu, uint8_t *pData, uint16_t *DateLen)
{
uint16_t i;
uint16_t StarIndex;
Pos_Def QueMsg;
if (pQu == NULL) return ERR_POINTER_NULL;
if (Q_NULL == Dat_GetUnFixQueueFlag(pQu)) {
*DateLen = 0;
return E_QUEUE_NULL;
}
__set_PRIMASK(1); /* 进入临界区 */
/* 获取最老节点 */
QueMsg = Que_GetOldestPosInf(pQu);
StarIndex = QueMsg.Start;
if(0 < QueMsg.len) {
/* 从数据区读取完整帧 */
for(i = 0; i < QueMsg.len; i++) {
pData[i] = pQu->pDateBuf[StarIndex];
pQu->PoolCurCnt--;
StarIndex = (StarIndex + 1) % pQu->PoolMaxLen;
}
*DateLen = QueMsg.len;
}
/* 删除最老节点 */
Que_DelPosInf(pQu);
__set_PRIMASK(0); /* 退出临界区 */
return E_QUEUE_NORMAL;
}
4.4 队列创建
c
/* uart1_rt.c - MyUartInit() */
#define COM3_FRAM_MAX_SIZE 256 /* 最大帧尺寸 */
#define COM3_RECV_POOL_SIZE (LCD_UART_FRAM_MAX_SIZE * COM3_RECV_QUEUE_LEN)
#define COM3_RECV_POS_LEN (2 * COM3_RECV_QUEUE_LEN)
#define COM3_RECV_QUEUE_LEN 30
/* 创建变长队列 */
qHandleCom3Rx = Dat_UnFixQueueCreate(
COM3_RECV_POOL_SIZE, /* 数据缓冲池大小 = 256 * 30 = 7680字节 */
COM3_RECV_POS_LEN, /* 位置信息数组长度 = 60 */
COM3_FRAM_MAX_SIZE /* 单帧最大长度 = 256 */
);
五、第四层:迪文协议解析
5.1 迪文DGUS协议帧格式
迪文屏使用的是自定义二进制协议,帧格式如下:
┌─────────┬─────────┬────────┬────────┬─────────────┬──────────────────┐
│ 0x5A │ 0xA5 │ Length │ Cmd │ Data... │ (CRC可选) │
├─────────┼─────────┼────────┼────────┼─────────────┼──────────────────┤
│ 帧头1 │ 帧头2 │ 数据长度│ 命令字 │ 数据负载 │ │
│ 1字节 │ 1字节 │ 1字节 │ 1字节 │ N字节 │ │
└─────────┴─────────┴────────┴────────┴─────────────┴──────────────────┘
注意:Length是 Cmd + Data 的总长度,不包含3字节帧头。
常用命令字:
| 命令字 | 含义 | 方向 | 说明 |
|---|---|---|---|
0x81 |
读寄存器 | 屏→MCU | 用于读取RTC等寄存器数据 |
0x82 |
写寄存器 | MCU→屏 | 写数据到指定地址 |
0x83 |
读RAM | 屏→MCU | 触摸/按键数据用这个! |
0x84 |
写RAM | MCU→屏 | 写数据到指定地址 |
触摸/按键帧示例 (0x83):
收到: 5A A5 06 83 00 00 00 00 XX YY 0D 0A
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ └── 0D 0A (结束符)
│ │ │ │ │ │ │ │ └── XX YY (按键值, 实际值偏移)
│ │ │ │ │ │ │ └── XX YY (触控坐标Y)
│ │ │ │ │ │ └── XX YY (触控坐标X)
│ │ │ │ │ └── 00 00 (变量地址)
│ │ │ │ └── 83 (读RAM命令)
│ │ │ └── 06 (Length = 6字节)
└── 5A A5 (帧头)
5.2 状态机协议解析器
这是整个解析层的核心------一个五状态的状态机:
c
/* diwen.c */
uint8_t DwinRvState = 0; /* 解析状态 */
uint8_t DwinRvBuf = 0; /* 当前接收字节 */
uint8_t DwinRvCRC = 0; /* CRC校验(未使用) */
uint8_t DwinRvTime = 0; /* 超时计数 */
uint8_t DwinRvDatF = 0; /* 数据就绪标志 ⭐ */
struct DwinFrame DwinRvFrme; /* 正在组帧的结构体 */
struct DwinFrame DwinRvFIFO; /* 已完成帧的FIFO */
/* DWIN帧结构体 */
struct DwinFrame {
uint8_t Length; /* 数据长度 */
uint8_t CmdStyle; /* 命令字 */
uint8_t Datas[256]; /* 数据区 */
};
void Dwin_Rv_Handler(void)
{
switch(DwinRvState) {
case 0: /* 等待帧头0x5A */
if(DwinRvBuf == 0X5A)
DwinRvState = 1;
break;
case 1: /* 等待帧头0xA5 */
if(DwinRvBuf == 0XA5) {
DwinRvCRC = 0;
DwinRvState = 2;
DwinRvTime = 1; /* 启动超时检测 */
} else {
DwinRvState = 0; /* 失败则重来 */
}
break;
case 2: /* 读取长度字节 */
DwinRvFrme.Length = DwinRvBuf;
DwinRvState = 3;
break;
case 3: /* 读取命令字 */
DwinRvFrme.CmdStyle = DwinRvBuf;
DwinRvState = 4;
break;
default: /* 读取数据 (State 4, 5, 6...) */
DwinRvFrme.Datas[DwinRvState - 4] = DwinRvBuf;
/* 判断帧是否接收完成 */
if(DwinRvFrme.Length == DwinRvState - 2) {
DwinRvState = 0;
DwinRvDatF = 1; /* ⭐ 置位数据就绪标志 */
DwinRvFIFO = DwinRvFrme; /* 复制到FIFO */
DwinRvTime = 0; /* 清零超时计数 */
} else {
DwinRvState++; /* 继续读下一个数据字节 */
}
break;
}
}
状态机流程图:
┌─────────────────────────────────────────┐
│ │
▼ │
┌──────────┐ │ ┌──────────┐ │ ┌──────────┐ │
│ State 0 │────┼──→ │ State 1 │────┼──→ │ State 2 │
│ 等待0x5A │ │ │ 等待0xA5 │ │ │ 读Length │
└──────────┘ │ └──────────┘ │ └──────────┘ │
│ │ │ │ │ │
│ 不匹配 │ │ 不匹配 │ │ │
▼ │ ▼ │ ▼ │
┌──────────┐ │ ┌──────────┐ │ ┌──────────┐ │
│ Reset │←───┴─── │ Reset │←───┴───→│ State 3 │
└──────────┘ └──────────┘ │ 读Cmd │
└──────────┘
│
│ Cmd读完成
▼
┌──────────────┐
│ State 4+ │
│ 读数据 Data[] │
└──────┬───────┘
│
┌──────────────────────┴──────────────────────┐
│ │
▼ ▼
Length == State-2 其他情况
(帧接收完成) │
│ ▼
▼ State++
┌───────────────────────────┐
│ DwinRvDatF = 1 │
│ DwinRvFIFO = DwinRvFrme │
│ State = 0 (Reset) │
└───────────────────────────┘
六、业务层:触摸/按键数据处理
6.1 屏幕扫描函数
c
/* diwen.c */
uint16_t TouchKey = 0; /* 全局按键值 */
uint16_t ScreenScan(void)
{
uint16_t keyvalue = 0xFFFF; /* 无按键默认值 */
if(DwinRvDatF) { /* 检查数据就绪标志 */
struct DwinFrame fram;
fram = DwinRvFIFO;
DwinRvDatF = 0; /* 清除标志 */
/* 处理读RAM命令 (0x83) - 触摸/按键 */
if(fram.CmdStyle == 0x83) {
if((fram.Datas[0] == 0x00) &&
(fram.Datas[1] == 0x00) &&
(fram.Length == 6)) {
/* 提取按键值: 高字节<<8 + 低字节 */
TouchKey = (fram.Datas[3] << 8) + fram.Datas[4];
keyvalue = 0xEEEE; /* 有效按键标记 */
return keyvalue;
}
}
/* 处理读寄存器命令 (0x81) - RTC时间读取 */
else if(fram.CmdStyle == 0x81) {
if((fram.Datas[0] == 0x20) &&
(fram.Datas[1] == 0x10) &&
(fram.Length == 0x13)) {
/* 解析RTC时间数据 */
DateNow.Year = 2000 + BCDToUint8(fram.Datas[2]);
DateNow.Month = BCDToUint8(fram.Datas[3]);
DateNow.Day = BCDToUint8(fram.Datas[4]);
DateNow.Week = BCDToUint8(fram.Datas[5]);
DateNow.Hour = BCDToUint8(fram.Datas[6]);
DateNow.Mint = BCDToUint8(fram.Datas[7]);
DateNow.Secd = BCDToUint8(fram.Datas[8]);
}
}
}
return keyvalue; /* 无按键返回0xFFFF */
}
6.2 按键值定义
c
/* diwen.h - 迪文屏按键码定义 */
#define KEY_0 0x0030
#define KEY_1 0x0031
#define KEY_2 0x0032
#define KEY_3 0x0033
#define KEY_4 0x0034
#define KEY_5 0x0035
#define KEY_6 0x0036
#define KEY_7 0x0037
#define KEY_8 0x0038
#define KEY_9 0x0039
#define KEY_BACKWARD 0x0043 /* ⏮ 后退 */
#define KEY_UP 0x0044 /* ⬆️ 上 */
#define KEY_FORWARD 0x0045 /* ⏭ 前进 */
#define KEY_LEFT 0x0046 /* ⬅️ 左 */
#define KEY_DOWN 0x0047 /* ⬇️ 下 */
#define KEY_RIGHT 0x0048 /* ➡️ 右 */
#define KEY_BACKSPACE 0x007F /* ⌫ 退格 */
#define KEY_CANCEL 0x000E /* ❌ 取消 */
#define KEY_OK 0x000C /* ✅ 确认 */
#define KEY_MINUS 0x002D /* ➖ 减号 */
#define KEY_POINT 0x002E /* 🔴 小数点 */
/* 功能键 */
#define KEY_FN0 0xF000
#define KEY_FN1 0xF001
#define KEY_FN2 0xF002
/* ... */
七、完整数据流整合
7.1 定时任务调度
在主循环或定时器中断中调用接收处理:
c
/* Obj_LcdUart.c - App_Lcd_PageCheck() */
void App_Lcd_PageCheck(void)
{
if(g_SysBaseTime.f2s) { /* 2秒周期 */
GetPICNow(); /* 查询当前页面ID */
ComLcdDiwenObj.pSendFunc(); /* 发送队列处理 */
App_Screen_Recv(); /* ⭐ 接收处理 - 关键! */
/* 检查页面是否切换 */
if((uint8_t)ObjLcd.ms_Win.Gui_Id_ReadBack != (uint8_t)ObjLcd.ms_Win.Gui_Id) {
SwitchPage(ObjLcd.ms_Win.Gui_Id); /* 切换页面 */
ComLcdDiwenObj.pSendFunc();
}
}
}
/* 接收处理函数 */
void App_Screen_Recv(void)
{
int16_t ret;
uint16_t DataLen;
uint8_t frameBuf[COM3_FRAM_MAX_SIZE];
/* 从变长队列取出一帧 */
ret = Dat_deUnFixQueueNode(qHandleCom3Rx, frameBuf, &DataLen);
if(E_QUEUE_NORMAL == ret) {
/* 逐字节送入协议解析器 */
for(uint16_t i = 0; i < DataLen; i++) {
DwinRvBuf = frameBuf[i];
Dwin_Rv_Handler();
}
}
}
7.2 GUI调度器中的按键处理
c
/* Obj_LcdUart.c - App_GuiSchedule() */
void App_GuiSchedule(void)
{
uint16_t key = ScreenScan(); /* 获取按键值 */
switch(ObjLcd.ms_Win.Gui_Id) {
case WinID_Gui_StandBy:
Gui_Standby(key); /* 传递按键到各页面处理 */
break;
case WinID_Gui_Det:
Gui_Det(key);
break;
case WinID_Gui_Listen:
Gui_Listen(key);
break;
case WinID_Gui_RecvParmSet:
Gui_RecvParmSet(key);
break;
case WinID_Gui_SysParmSet:
Gui_SysParmSet(key);
break;
default:
break;
}
}
/* 页面处理函数示例 */
uint16_t Gui_Standby(uint16_t key)
{
switch(key) {
case KEY_1:
/* 进入检测模式 */
return WinID_Gui_Det;
case KEY_2:
/* 进入聆听模式 */
return WinID_Gui_Listen;
case KEY_3:
/* 进入参数设置 */
return WinID_Gui_RecvParmSet;
case KEY_OK:
/* 确认操作 */
break;
case KEY_ESC:
/* 返回/取消 */
break;
default:
break;
}
return WinID_Gui_StandBy; /* 保持当前页面 */
}
八、知识点延伸:为什么这样设计?
8.1 多层缓冲的哲学
┌─────────────────────────────────────────────────────────────────┐
│ 📊 缓存层级一览表 │
├───────────┬─────────────┬───────────────────┬───────────────────┤
│ 层级 │ 位置 │ 容量 │ 速度 │
├───────────┼─────────────┼───────────────────┼───────────────────┤
│ DMA缓冲 │ 硬件(SRAM) │ ~4KB │ 最快(无CPU介入) │
│ Ring FIFO │ 内存(SRAM) │ ~4KB │ 快(单拷贝) │
│ 变长队列 │ 内存(SRAM) │ ~8KB │ 快(双拷贝) │
│ 协议解析 │ CPU寄存器 │ ~256B │ 最快(即时处理) │
└───────────┴─────────────┴───────────────────┴───────────────────┘
"这就像餐厅的传菜流程:厨房(DMA)出菜到缓冲区(Ring FIFO),服务员(变长队列)按桌(帧)整理,最后客人(业务层)优雅用餐。"
8.2 中断与轮询的抉择
本设计采用中断驱动+定时轮询的混合模式:
- DMA+Idle中断:硬件层接收,不占用CPU
- 定时任务:协议解析+业务处理,避免在中断中做复杂操作
"在中断里做复杂操作?那是新手的做法。老鸟的原则是:中断只做'存钱','花钱'的事让主循环来。"
九、总结与代码模板
9.1 四层架构总结
| 层级 | 名称 | 职责 | 关键函数 |
|---|---|---|---|
| L1 | DMA硬件层 | 接收数据到SRAM | HAL_UART3_RxIdleCallback() |
| L2 | 环形FIFO | 解耦生产/消费 | ring_fifo_write/read() |
| L3 | 变长队列 | 按帧存储 | Dat_en/deUnFixQueueNode() |
| L4 | 协议解析 | 帧解析+业务处理 | Dwin_Rv_Handler() / ScreenScan() |
9.2 快速移植模板
如果你想把这套架构用到自己的项目,只需:
- 复制3个文件 :
ring_fifo.c/h,UnfixQueue.c/h - 替换DMA/串口初始化 :
MyUartInit()+HAL_UART3_RxIdleCallback() - 适配协议解析 :根据你的协议修改
Dwin_Rv_Handler()状态机 - 对接业务层 :在
ScreenScan()后接你的处理逻辑
📢 最后的最后:
代码写得好,bug救得早。但比救bug更重要的是------别在凌晨三点debug的时候发现家里没咖啡了。
祝各位嵌入式老铁们,写的代码都能一次跑通,调的bug都能一眼看穿。
本文代码基于STM32F429 + HAL库实战项目,迪文屏型号为T5L DGUS屏。