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 /* 连续稳定次数 */
状态机逻辑:
- 未按下状态:连续 3 次(60ms)采样到低电平 → 进入”按下保持”状态
- 按下保持状态:等待电平变高(松开)→ 在松开沿发送一次事件
- 松开后强制冷却 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 寿命
- 行级缓存减少不必要的总线流量