屏幕资源

PipeMonitor 使用的是一块 320x480 的 TFT LCD,通过 FSMC 16 位并行接口驱动,RGB565 色彩格式。在这块 3.5 寸屏幕上,需要容纳:

  • 标题栏 + 设备 ID + 心跳指示
  • 瞬时流量主视觉(含进度条)
  • 累计流量、流速、压力、温压温度 4 个数据卡片
  • 6 路 PT100 温度小卡片
  • 底部告警状态栏

布局策略

整个界面采用卡片式布局,每个数据卡片固定尺寸,数值变化时只刷新数值标签,不触动布局:

static void lcd_ui_create_metric_card(lv_obj_t *parent,
    lcd_ui_row_id_t row, const char *title, const char *unit,
    int32_t x, int32_t y, int32_t width, int32_t height, uint32_t accent_hex)
{
    // 创建卡片容器
    lv_obj_t *card = lcd_ui_create_box(parent, x, y, width, height, ...);
    // 左侧彩色强调条
    lcd_ui_create_accent_mark(card, accent_hex, height);
    // 标题
    lcd_ui_label(card, title, ...);
    // 数值(动态标签,右对齐,固定宽度裁剪)
    g_lcd_ui_rows[row].value = lcd_ui_label(card, "--", ...);
    lcd_ui_prepare_dynamic_label(g_lcd_ui_rows[row].value, value_width, value_height);
    // 单位
    g_lcd_ui_rows[row].unit = lcd_ui_label(card, unit, ...);
    // 底部比例条
    g_lcd_ui_rows[row].bar = lv_bar_create(card);
}

动态标签的配置细节:

static void lcd_ui_prepare_dynamic_label(lv_obj_t *label, int32_t width, int32_t height)
{
    lv_obj_set_size(label, width, height);
    lv_label_set_long_mode(label, LV_LABEL_LONG_CLIP);
    lv_obj_set_style_bg_opa(label, LV_OPA_TRANSP, 0);
}

关键点:数值标签设置 LV_LABEL_LONG_CLIP 裁剪模式,固定尺寸,背景透明。数值从 123.456 变成 89.1 时,标签区域缩小后由父级浅色背景”清底”,不会留下残影。

瞬时流量主视觉

流量是首屏最重要的数据,使用了 128px 高的面板 + 大号 24pt 字体 + 8px 粗进度条:

// 大号数值
g_lcd_ui_flow_value = lcd_ui_label(flow_panel, "--", 18, 44,
    &lv_font_montserrat_24, text_color);
// 宽进度条
g_lcd_ui_flow_bar = lv_bar_create(flow_panel);
lv_obj_set_size(g_lcd_ui_flow_bar, 268, 8);

进度条以 200 L/min 为满量程,实时显示当前流量占比。

比例条设计

每个数据卡片底部都有一个小比例条(3px 高),用颜色表示当前值在量程中的位置:

static int32_t lcd_ui_scale_percent(rt_uint8_t valid, float value, float full_scale)
{
    if (valid == 0U) return 0;
    if (full_scale <= 0.0f) return 100;
    int32_t percent = (int32_t)((value * 100.0f / full_scale) + 0.5f);
    if (percent < 0) percent = 0;
    else if (percent > 100) percent = 100;
    return percent;
}

不同卡片使用不同的强调色——压力用橙色、温度用绿色、流速用青色,视觉上快速区分数据类型。

告警状态栏

底部 40px 高的状态栏显示最新告警或”正常”状态:

if (app_alarm_copy_latest(&alarm) != RT_EOK)
{
    lv_label_set_text(g_lcd_ui_alarm_text, "正常  本地数据有效");
    lv_obj_set_style_bg_color(g_lcd_ui_alarm_bar, lv_color_hex(0xE8F6EF), 0);
}
else
{
    // 根据告警级别切换颜色:info=绿, warn=橙, critical=红
    color = lcd_ui_alarm_color(alarm.severity);
    lv_label_set_text(g_lcd_ui_alarm_text, alarm_text);
    lv_obj_set_style_bg_color(g_lcd_ui_alarm_bar, bg_color, 0);
}

告警条只读取最新告警快照,不消费告警队列,不影响上行和 App 通知链路。

心跳脉冲动画

左上角有一个 8x8 的圆点,每 500ms 在两种绿色之间切换,形成”呼吸”效果:

g_lcd_ui_pulse_phase ^= 1U;
lv_obj_set_style_bg_color(g_lcd_ui_heart_dot,
    (g_lcd_ui_pulse_phase != 0U) ?
    lv_color_hex(LCD_UI_COLOR_OK_HEX) : lv_color_hex(0x9AD7C1), 0);

旁边显示心跳计数和运行时长,一眼就能判断设备是否正常运行。

flush 回调与局部刷新

LVGL v9 的 PARTIAL 渲染模式下,只有变化的区域才会触发 flush:

static void lcd_ui_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
    // 计算脏区域尺寸
    width = x2 - x1 + 1;
    height = y2 - y1 + 1;

    // 通过 FSMC 并行接口写入 LCD
    tftlcd_color_fill_stride(x1, y1, x2, y2, pixels, stride_pixels);
    lv_display_flush_ready(disp);
}

由于数值标签固定尺寸、每 500ms 才刷新一次,大部分 flush 调用只涉及几十个像素的脏区域,FSMC 写入耗时极短。

背光控制

LCD 首帧完整写入后才点亮背光,避免复位时看到白屏逐块刷新:

if (lcd_ui_init_lvgl() != RT_EOK) { ... }

// 首帧完整渲染
lv_refr_now(g_lcd_ui_disp);
// 再打开背光
lcd_backlight_control_set(RT_TRUE);

支持按键控制的背光开关,详见按键与 LCD 电源管理

raw 旁路模式

代码中保留了 LCD_UI_BYPASS_LVGL 宏控制的 raw 模式,绕过 LVGL 直接用 tftlcd_show_string 绘制文字。这个模式用于:

  • 排查 LVGL 框架本身的问题(如 flush 脏区域异常)
  • 排查 LCD 驱动层的硬件问题(如 FMC 映射写路径异常)
  • 排查颜色异常(如 0x0000 偏红)

raw 模式还内置了帧缓冲渲染(LCD_UI_RAW_FRAMEBLIT),先画到内存帧缓冲再整体写入 LCD,用来隔离”逐像素写”和”stride 写”的差异。

小结

  • 卡片式固定布局,数值变化只刷新标签,不触发重排
  • 流量主视觉用大字体 + 粗进度条,强调首屏最关键数据
  • 比例条用颜色区分数据类型,快速视觉定位
  • 告警栏只读快照不消费队列,不影响上行链路
  • 心跳脉冲动画提供设备运行状态的直观反馈
  • flush 局部刷新 + 延迟背光点亮,优化启动体验

后续阅读