PA15 引脚复用

STM32H562 的 PA15 默认被 CubeMX 配置为 JTDI(JTAG 数据输入),但本工程只用 SWD(PA13/PA14)下载调试,JTDI 处于未驱动状态。与其浪费一个引脚,不如把它重新配置为 GPIO 输入,接一个按键用于切换 LCD 显示开关。

static void app_key_gpio_init(void)
{
    GPIO_InitTypeDef gpio_init = {0};
    APP_KEY_GPIO_CLK_ENABLE();

    gpio_init.Pin   = GPIO_PIN_15;
    gpio_init.Mode  = GPIO_MODE_INPUT;
    gpio_init.Pull  = GPIO_PULLUP;       /* 内部上拉 */
    gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &gpio_init);
}

按键另一端接 GND,按下时 PA15 读到低电平。

软件消抖策略

机械按键按下时会产生 5-20ms 的抖动,直接采样会误触发。采用 连续稳定计数 消抖:

#define APP_KEY_POLL_PERIOD_MS        20U   /* 轮询周期 */
#define APP_KEY_DEBOUNCE_STABLE_COUNT 3U    /* 连续稳定次数 */

状态机逻辑:

  1. 未按下状态:连续 3 次(60ms)采样到低电平 → 进入”按下保持”状态
  2. 按下保持状态:等待电平变高(松开)→ 在松开沿发送一次事件
  3. 松开后强制冷却 200ms,防止连击
if (g_key_is_down == 0U)
{
    /* 尝试识别新的按下 */
    if (pressed != 0U)
    {
        stable_down_cnt++;
        if (stable_down_cnt >= APP_KEY_DEBOUNCE_STABLE_COUNT)
        {
            g_key_is_down = 1U;
            stable_down_cnt = 0U;
        }
    }
    else stable_down_cnt = 0U;
}
else
{
    /* 等待松开 */
    if (pressed == 0U)
    {
        g_key_is_down = 0U;
        app_event_post(APP_EVENT_LCD_TOGGLE);
        rt_thread_mdelay(200);  /* 冷却期 */
    }
}

为什么选择 松开沿触发 而不是按下沿?两个原因:

  • 避免按下瞬间的噪声直接触发
  • 长按不会重复触发,用户预期更自然

按键线程与显示线程的协作

按键线程不直接操作 LCD,只发送 APP_EVENT_LCD_TOGGLE 事件位。所有 LCD 操作都在显示线程里串行执行,避免 FMC 总线时序被抢占破坏。

按键线程 ──(event)──> 显示线程 ──> lcd_display_off / lcd_display_on

显示线程收到事件后,还会做 toggle 节流:250ms 内的重复 toggle 直接丢弃。

if ((received & APP_EVENT_LCD_TOGGLE) != 0U)
{
    rt_tick_t since_last = now - g_lcd_last_toggle_tick;
    if (since_last < rt_tick_from_millisecond(250))
    {
        received &= ~APP_EVENT_LCD_TOGGLE;  /* 丢弃 */
    }
    else
    {
        g_lcd_last_toggle_tick = now;
        /* 执行实际的开关操作 */
    }
}

这道节流配合按键线程的 200ms 冷却期,双重保险防止快速连击导致 HardFault。

LCD 电源状态机

LCD 有两种电源状态:

状态背光液晶显示功耗
ON开启开启~50mA
OFF关闭关闭~1mA

切换路径:

  • ON → OFF:先关液晶显示,再关背光(给用户”按一下立刻全黑”的反馈)
  • OFF → ON:先开背光,再开液晶显示,然后触发整屏重绘
if (g_lcd_power_on)
{
    lcd_display_off();
    LCD_BL(0);
    g_lcd_power_on = 0;
}
else
{
    LCD_BL(1);
    lcd_display_on();
    g_lcd_power_on = 1;
    need_full_redraw = 1;  /* 重绘整屏 */
}

关屏状态下显示线程跳过所有渲染和维护,节省 FMC 流量与 CPU 开销。

防残影策略

长时间显示静态内容的 LCD 容易出现 VCOM 极化导致的残影。PipeMonitor 采用两层维护:

1. 周期性整屏重绘(30 分钟)

清空行级缓存,强制重写所有像素:

#define LCD_FULL_REDRAW_PERIOD_MS    (30UL * 60UL * 1000UL)

if ((now - g_lcd_last_full_redraw_tick) >= rt_tick_from_millisecond(LCD_FULL_REDRAW_PERIOD_MS))
{
    app_lcd_draw_layout();  /* 清屏 + 重画标题 + 重置缓存 */
    g_lcd_last_full_redraw_tick = now;
}

2. 周期性 LCD IC 重初始化(6 小时)

重新发送驱动 IC 的 gamma/VCOM/时序寄存器配置,恢复出厂初始状态:

#define LCD_REINIT_PERIOD_MS         (6UL * 60UL * 60UL * 1000UL)

if ((now - g_lcd_last_reinit_tick) >= rt_tick_from_millisecond(LCD_REINIT_PERIOD_MS))
{
    LCD_BL(0);      /* 关背光避免花屏可见 */
    lcd_init();     /* 重发 IC 初始化序列 */
    LCD_BL(1);
    g_lcd_last_reinit_tick = now;
    need_full_redraw = 1;
}

重初始化期间短暂关闭背光,用户看不到花屏过程。

行级缓存优化

LCD 刷新最耗时的是 FSMC 写入。为了减少不必要的写入,采用行级文本缓存:

static char g_lcd_cached_lines[11][48];
static uint16_t g_lcd_cached_colors[11];

static void app_lcd_update_line(rt_uint8_t index, rt_uint16_t y,
                                 const char *text, uint16_t color)
{
    /* 内容和颜色都没变化,跳过 */
    if (strcmp(g_lcd_cached_lines[index], text) == 0 &&
        g_lcd_cached_colors[index] == color)
        return;

    /* 擦除旧区域,写入新文本 */
    lcd_fill(8, y, 319, y + 23, WHITE);
    lcd_show_string(8, y, 312, 24, 24, text, color);

    /* 更新缓存 */
    strncpy(g_lcd_cached_lines[index], text, 47);
    g_lcd_cached_colors[index] = color;
}

大多数时候只有数值变化的行会被重绘,静态标签(如 “FLOW :“)永远不会触发写入。

背光超时自动关屏

如果启用了 LCD_AUTO_OFF_TIMEOUT_MS,按键无操作超时后自动关屏:

if (LCD_AUTO_OFF_TIMEOUT_MS > 0 &&
    (now - g_lcd_last_key_tick) >= rt_tick_from_millisecond(LCD_AUTO_OFF_TIMEOUT_MS))
{
    LCD_BL(0);
    lcd_display_off();
    g_lcd_power_on = 0;
}

当前配置设为 0(禁用),屏幕保持常开。对于电池供电的场景可以开启以省电。

小结

按键和 LCD 看似简单,但在嵌入式系统里有很多细节:

  • 引脚复用避免硬件改动
  • 软件消抖 + 松开沿触发 + 冷却期,三层防护
  • 按键线程与显示线程解耦,保护 FMC 总线时序
  • 周期性重绘 + IC 重初始化,延长 LCD 寿命
  • 行级缓存减少不必要的总线流量

后续阅读