屏幕资源
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 局部刷新 + 延迟背光点亮,优化启动体验