STM32H7 + 迪文屏 DGUS 开发实战:从零构建工业级时间设置界面

摘要 :本文基于 STM32H7 与迪文屏(DWIN DGUS)的联合开发实战,详细讲解如何利用 UART IDLE 中断 实现高效串口通信,如何设计状态机 管理多输入框交互,以及FreeRTOS 任务实现光标闪烁效果。文中包含完整的代码框架、踩坑记录与性能优化建议。

一、系统架构设计

1.1 硬件架构

  • 主控:STM32H7 系列(480MHz,高性能场景)

  • 显示屏:迪文屏 DGUS 串口屏(TTL 电平,115200bps)

  • 通信:USART6 直连,使用 IDLE 中断机制

1.2 软件架构(分层设计)

复制代码
┌─────────────────────────────────────────────┐
│  FreeRTOS 应用层                             │
│  ├── vTask_CursorBlink (200ms 闪烁任务)     │
│  └── 业务逻辑任务                             │
├─────────────────────────────────────────────┤
│  迪文屏协议层 (dealusart2.c)                  │
│  ├── 帧解析 (0x5A 0xA5 协议)               │
│  ├── 状态机 (InputState)                     │
│  └── 发送封装 (Dealusart2_Sendfix)           │
├─────────────────────────────────────────────┤
│  硬件抽象层                                  │
│  ├── USART6 IDLE 中断驱动                   │
│  └── RTC 实时时钟 (HAL 库)                   │
└─────────────────────────────────────────────┘

二、核心技术实现详解

2.1 IDLE 中断接收机制(高效串口通信)

痛点 :传统 HAL 库的 HAL_UART_Receive 在中断中使用阻塞方式(1000ms 超时),会导致系统卡顿甚至死机。

解决方案:直接操作寄存器,非阻塞方式:

复制代码
void USART6_IRQHandler(void)
{
    uint8_t receive_data = 0;
    
    // RXNE 处理:直接读 RDR 寄存器(非阻塞)
    if(__HAL_UART_GET_FLAG(&huart6, UART_FLAG_RXNE) != RESET) {
        receive_data = (uint8_t)(huart6.Instance->RDR & 0xFF);
        if(uart1_rx_len < UART1_RX_BUF_SIZE - 1) {
            uart1_rx_buf[uart1_rx_len++] = receive_data;
        }
    }
    
    // IDLE 中断:帧结束检测(关键!)
    if(__HAL_UART_GET_FLAG(&huart6, UART_FLAG_IDLE) != RESET) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart6);  // 必须立即清除标志
        
        if(glrun && uart1_rx_len > 3) {
            // 解析迪文屏协议帧...
        }
        uart1_rx_clear();  // 清空缓冲区准备下一帧
    }
}

关键点

  • IDLE 中断:当串口空闲超过 1 字节时间时触发,天然适合"帧结束检测"

  • 零拷贝 :直接操作 RDR 寄存器,避免 HAL 库的开销

  • 双缓冲uart1_rx_buf 接收,g_uart_proc_buf 处理(可扩展)

2.2 迪文屏协议解析引擎

迪文屏协议格式固定(帧头 0x5A 0xA5),但容易踩坑的是地址字节序(大端模式)。

复制代码
// 帧解析核心逻辑(大端转小端)
if((*p0 == 0x5a) && (*(p0+1) == 0xa5)) {
    cmdlen = *(p0+2);  // 数据长度
    cmd = *(p0+3);     // 命令字(0x83 为触控返回)
    
    // 地址解析(大端:高位在前)
    tmp = (unsigned char *)&addr;
    *tmp = *(p0+5);       // 低字节
    *(tmp+1) = *(p0+4);   // 高字节(注意顺序!)
    
    // 数据解析
    tmp = (unsigned char *)&data;
    *tmp = *(p0+8);       // 低字节
    *(tmp+1) = *(p0+7);   // 高字节
}

踩坑记录 :曾误用 if((*p0=0x5a))(赋值而非比较),导致死循环,务必使用 ==

2.3 多输入框状态机设计

时间设置界面包含 6 个输入框(年月日时分秒),采用状态机管理焦点:

复制代码
typedef enum {
    INPUT_NONE = 0,
    INPUT_YEAR,    // 4 位输入
    INPUT_MONTH,   // 2 位输入
    INPUT_DAY,     // 2 位输入
    INPUT_HOUR,    // 2 位输入
    INPUT_MIN,     // 2 位输入
    INPUT_SEC      // 2 位输入
} InputState;

volatile InputState g_active_input = INPUT_NONE;
volatile uint16_t g_input_buffer = 0;    // 输入缓冲
volatile uint8_t  g_input_digits = 0;    // 已输入位数

输入处理逻辑(支持满位自动提交):

复制代码
static void HandleDigitInput(uint8_t digit)
{
    uint8_t max_digits = (g_active_input == INPUT_YEAR) ? 4 : 2;
    
    if(g_input_digits < max_digits) {
        g_input_buffer = g_input_buffer * 10 + digit;
        g_input_digits++;
        
        // 实时更新显示(迪文屏立即响应)
        uint16_t vp_addr = VP_DISP_YEAR + (g_active_input - 1);
        Dealusart2_Sendfix(vp_addr, g_input_buffer);
    }
}

2.4 光标闪烁效果(FreeRTOS 任务)

光标采用图标变量(而非文本变量)控制,通过任务实现 200ms 周期性闪烁:

复制代码
void vTask_CursorBlink(void *pvParameters)
{
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(200));
        
        if(g_active_input != INPUT_NONE) {
            ToggleCursor();  // 在图标 0(隐藏)和 1(显示)间切换
        }
        
        // 页面切换延迟加载(解决时序问题)
        if(g_time_page_entered) {
            vTaskDelay(pdMS_TO_TICKS(150));  // 等待迪文屏页面就绪
            SyncRTCVariables();              // 同步 RTC
            LoadCurrentTimeToDisplay();      // 加载当前时间
            HideAllCursors();
            g_time_page_entered = 0;
        }
    }
}

设计亮点

  • 分离关注点:中断只负责接收,显示逻辑在任务中处理

  • 延时加载:解决"页面切换时数据不加载"的时序问题(迪文屏页面刷新需要时间)

2.5 RTC 时间同步与显示

读取顺序陷阱 :STM32 RTC 必须先读 Time 再读 Date,否则日期会锁定在旧值。

复制代码
static void SyncRTCVariables(void)
{
    RTC_DateTypeDef sDate;
    RTC_TimeTypeDef sTime;
    
    // 关键:必须先 GetTime,再 GetDate!
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    
    rtc_year = sDate.Year + 2000;  // 年份偏移量转换
    rtc_month = sDate.Month;
    // ...
}

static void LoadCurrentTimeToDisplay(void)
{
    // 批量发送到迪文屏(VP 地址连续)
    Dealusart2_Sendfix(VP_DISP_YEAR,  rtc_year);
    Dealusart2_Sendfix(VP_DISP_MONTH, rtc_month);
    // ... 日时分秒
}

三、线程安全与信号量设计

由于中断和 FreeRTOS 任务都会调用串口发送,必须加互斥保护:

复制代码
static SemaphoreHandle_t uart2sendlock = NULL;

int Dealusart2_Sendfix(unsigned short addr, unsigned short data)
{
    // 中断上下文判断
    if (__get_IPSR() != 0) {
        // 中断中:使用 FromISR 版本
        if(xSemaphoreTakeFromISR(uart2sendlock, pdTRUE) != pdTRUE) return 0;
    } else {
        // 任务中:普通 Take
        if(xSemaphoreTake(uart2sendlock, 200) != pdTRUE) return 0;
    }
    
    // 组装帧:0x5A 0xA5 + 长度 + 0x82 + 地址(大端) + 数据(大端)
    uart1_tx_buf[0]=0x5a;
    uart1_tx_buf[1]=0xa5;
    uart1_tx_buf[2]=0x5;  // 长度 5
    uart1_tx_buf[3]=0x82; // 写指令
    // ... 地址和数据填充(大端序)
    
    HAL_UART_Transmit(&huart6, uart1_tx_buf, 8, 0xffff);
    
    // 释放信号量(根据上下文选择)
    if (__get_IPSR() != 0) {
        xSemaphoreGiveFromISR(uart2sendlock, pdTRUE);
    } else {
        xSemaphoreGive(uart2sendlock);
    }
}

四、踩坑记录与解决方案

问题现象 根因分析 解决方案
显示 1792 而非 2 DGUS 控件配置为 4 字节,MCU 只发 2 字节 DGUS 侧改为"2 字节整数",或 MCU 连续发送高 16 位+低 16 位
光标显示方块 混淆文本变量(ASCII)与图标变量(索引) 图标变量发送 0x0000(图标 0),而非 0x0020(ASCII 空格)
连续发送后显示混乱 迪文屏处理速率跟不上,帧丢失 每次发送后延时 50-100ms(osDelay
页面切换不加载时间 迪文屏页面刷新有延迟,MCU 发送过早 设置标志位,在任务中延时 150ms 后加载
RTC 时间不同步 GetDate 在 GetTime 之前,导致日期锁定 严格先 GetTimeGetDate
中断死机 HAL_UART_Receive 阻塞 1000ms 导致看门狗复位 改用寄存器直接读取 huart6.Instance->RDR

五、VP 地址规划建议

合理的地址规划是维护的基础:

功能 VP 地址 类型 说明
时间显示 0x1000-0x1005 2 字节整数 年月日时分秒,连续地址
光标控制 0x1100-0x1105 图标变量 0=隐藏,1=下划线
触控输入 0x2000-0x2005 按键返回 输入框激活地址
数字键盘 0x9150-0x9159 按键返回 0-9 数字键
页面切换 0x4000 按键返回 进入时间设置页
RTC 设置 0x9040-0x904A 按键返回 年/月/日/时/分/秒/设置

六、总结与展望

本文介绍的架构已成功应用于工业相机控制界面,稳定运行。核心设计思想:

  1. 中断轻量化:只接收不处理,避免阻塞

  2. 状态机解耦:输入逻辑与显示逻辑分离

  3. 延时补偿:充分考虑迪文屏的异步特性

后续优化方向

  • 改用 DMA + IDLE 中断,进一步降低 CPU 占用

  • 实现迪文屏"批量写"(0x82 指令支持连续地址写入),减少通信次数

  • 添加 CRC 校验,增强通信可靠性


附录:关键宏定义参考

复制代码
#define VP_DISP_YEAR     0x1000
#define VP_CURSOR_BASE   0x1100
#define CURSOR_ICON_HIDE 0x0000
#define CURSOR_ICON_SHOW 0x0001
#define KEY_0            0x9150
#define RTC_Y            0x9040  // 年输入框触控地址

希望这篇博客能为您的嵌入式 GUI 开发提供参考!如有问题,欢迎在评论区交流。

相关推荐
Z文的博客7 小时前
嵌入式MCU与迪文屏通信:DMA+环形FIFO+变长队列+状态机完整手册
stm32·单片机·串口·dma·中断·串口dma·嵌入式单片机
BackCatK Chen8 小时前
STM32保姆级入门教程|第7章:串口通信(USART)收发数据 + printf重定向打印调试(功能超详细+CubeIDE手把手)
stm32·串口通信·usart·stm32cubeide·printf重定向·嵌入式调试·中断接收
12.=0.8 小时前
【stm32_5】Systick嘀嗒定时器、解析时钟源、分析时钟树、应用Systick设计延时
c语言·stm32·单片机·嵌入式硬件
达不溜的日记9 小时前
CAN总线网络传输层CanTp详解
网络·stm32·嵌入式硬件·网络协议·网络安全·信息与通信·信号处理
森利威尔电子-10 小时前
森利威尔SL6129兼容 AL8805 / AL8806,输入电压 5.5V - 30V,最大输出电流 1.2A
单片机·嵌入式硬件·集成电路·芯片·电源芯片
FreakStudio10 小时前
嘉立创开源:应该是全网MicroPython教程最多的开发板
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy
qq_4416857510 小时前
CC26xx开发 第一节 前期准备
嵌入式硬件
史蒂芬_丁10 小时前
TI F28P65 使用 ePWM 模块模拟 SPI 时钟的详细方法
单片机·嵌入式硬件·fpga开发
LinuxRos10 小时前
I2C子系统与驱动开发:从协议到实战
linux·人工智能·驱动开发·嵌入式硬件·物联网