嵌入式MCU与迪文屏通信:DMA+环形FIFO+变长队列+状态机完整手册

嵌入式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半满/满中断里处理?

中断类型 触发时机 适用场景
RxHalfCpltCallback DMA传输到一半 大数据缓冲,防止FIFO溢出
RxCpltCallback DMA传输完成 固定长度帧
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 快速移植模板

如果你想把这套架构用到自己的项目,只需:

  1. 复制3个文件ring_fifo.c/h, UnfixQueue.c/h
  2. 替换DMA/串口初始化MyUartInit() + HAL_UART3_RxIdleCallback()
  3. 适配协议解析 :根据你的协议修改 Dwin_Rv_Handler() 状态机
  4. 对接业务层 :在 ScreenScan() 后接你的处理逻辑

📢 最后的最后:

代码写得好,bug救得早。但比救bug更重要的是------别在凌晨三点debug的时候发现家里没咖啡了

祝各位嵌入式老铁们,写的代码都能一次跑通,调的bug都能一眼看穿。


本文代码基于STM32F429 + HAL库实战项目,迪文屏型号为T5L DGUS屏。

相关推荐
BackCatK Chen4 小时前
STM32保姆级入门教程|第7章:串口通信(USART)收发数据 + printf重定向打印调试(功能超详细+CubeIDE手把手)
stm32·串口通信·usart·stm32cubeide·printf重定向·嵌入式调试·中断接收
12.=0.5 小时前
【stm32_5】Systick嘀嗒定时器、解析时钟源、分析时钟树、应用Systick设计延时
c语言·stm32·单片机·嵌入式硬件
达不溜的日记5 小时前
CAN总线网络传输层CanTp详解
网络·stm32·嵌入式硬件·网络协议·网络安全·信息与通信·信号处理
森利威尔电子-7 小时前
森利威尔SL6129兼容 AL8805 / AL8806,输入电压 5.5V - 30V,最大输出电流 1.2A
单片机·嵌入式硬件·集成电路·芯片·电源芯片
FreakStudio7 小时前
嘉立创开源:应该是全网MicroPython教程最多的开发板
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy
史蒂芬_丁7 小时前
TI F28P65 使用 ePWM 模块模拟 SPI 时钟的详细方法
单片机·嵌入式硬件·fpga开发
冷凝雨8 小时前
复旦微FM33 MCU 底层开发指南——UART
stm32·单片机·串口·uart·fm33lc0·复旦微电子
ting_zh9 小时前
基于 STM32F407 Discovery 向 W25Q16 SPI Flash 烧录固件
stm32·spi flash
白掰虾9 小时前
STM32CubeMX2教程——STM32C5 UART
stm32·单片机·嵌入式硬件·mcu·usart·stm32cubemx2·stm32c542