☰
LVGL lv_meter仪表盘开发实战:刻度、指针与动画
2026/10/2 21:14:53 网站建设 项目流程

做嵌入式界面的人,多半都有过自己画圆环、画刻度的经历。尤其是汽车仪表盘这种圆形表盘,早年我都是用 lv_line 加 lv_arc 一笔一笔拼,坐标还用 sin/cos 去算,一个表盘写完眼睛都快花掉。后来 LVGL 自带控件里 lv_meter 越来越成熟,刻度、量程弧、指针全都内置了,相当于把一道绘图题变成了配置题。这篇文章就围绕“LVGL 仪表盘实战”展开,目标是用 lv_meter 在 5 分钟内跑出一个能实时转针的汽车仪表 UI,代码会完整贴在后面。不管你是刚接触 LVGL 的新手,还是在 STM32、ESP32 上做产品的老手,都能直接用这套思路。

1. 为什么汽车仪表UI适合用lv_meter来做

1.1 lv_meter让表盘开发从绘图变成配置

没有 lv_meter 之前,画一个表盘要做什么?先是确定圆心和半径,然后循环计算每个刻度点的坐标,调用 lv_canvas 或 lv_draw_line 一条条画,量程弧也要自己算起点终点然后调 lv_draw_arc,最后还要单独建一个 image 或 line 当指针,绑定旋转动画,处理 anchor 中心点。一套下来几百行代码,而且换个颜色、改个量程,又得重新算坐标。

lv_meter 把这些全部打包成了一个控件。你只创建 meter,然后添加一个 scale,往 scale 上挂刻度和指示器。指示器分四种:弧线、指针线、指针图片、刻度线。每个指示器都可以单独绑定数值范围,控件内部自己处理坐标变换和重绘。也就是说,你从“会画图的人”变成了“会配置的人”。

我做个对比你就明白了:

对比项手绘方案(line + arc)lv_meter方案
开发时间半天起半小时以内
改量程重算坐标、改绘制逻辑改一个函数参数
指针动画手动计算旋转矩阵直接设 indicator value
多表盘复用每个表盘单独写函数封装成一个创建函数,反复调用
资源开销控件多、内存碎片多单控件内置,开销更集中

用一句生活化的类比:手绘方案像是从买菜洗菜切菜开始做一顿饭,lv_meter 相当于买了净菜,下锅炒一下就行。省下来的时间,都值得花在布局、配色、动效这些“体验”上。

1.2 汽车仪表UI的几个硬指标

汽车仪表盘这类界面,和普通手机 APP 界面有几个明显不同的地方:

圆形或半圆形的表盘占大多数,比如车速表、转速表、油量表、水温表;表盘上要有量程色带,常见的是绿色正常区、黄色警示区、红色危险区;指针需要实时响应外部数据,而且响应要平滑,不能一跳一跳;界面层级里还要有数字显示,跟指针形成“指针看趋势、数字看精确值”的双通道信息。

这些需求恰好都是 lv_meter 擅长的。一个 meter 控件上可以挂多个 arc 指示器,所以绿弧、黄弧、红弧可以叠加在一起;可以挂多个 needle 或 line,一个表盘上甚至能显示两根指针;角度范围和值范围可以自由映射,想做成 180 度半圆表盘、270 度大圆弧表盘,或者 360 度整圆仪表,都只是一个参数的事。

这套方案能用的地方也不只是汽车。工业 HMI 上的压力表、温度表,家电上的功率表,健身器材上的速度/里程表,只要是“圆形弧线 + 指针 + 量程”这一类视觉,都可以直接套。这也是我把它单独拿出来写一篇的原因,覆盖面太广了。

1.3 LVGL 9.x里lv_meter的变化与新玩法

如果你之前用的是 LVGL 8.x,切到 9.x 之后会感觉到 meter 区域有几个明显变化。

LVGL 8 的 meter 里,scale 是在 meter 内部隐式管理的一个对象,API 虽然也是lv_meter_add_scale,但很多能力是有限的。LVGL 9 把 scale 真正变成了可以独立配置的对象,刻度相关的所有参数都围绕这个 scale 展开,配置逻辑更统一。新增的一些渲染能力也受益于 9.x 的整体渲染架构升级,比如抗锯齿效果比老版本平滑不少。

代码层面,大部分函数名是兼容的。你在网上搜到的老教程,如果是基于 LVGL 8 写的,通常改几个头文件路径和初始化流程就能跑起来。真正要注意的反而是工程配置,比如lv_conf.h里有没有打开LV_USE_METER,字体有没有包含需要的数字字符集,这些比 API 差异更容易卡住人。

2. lv_meter控件核心机制:刻度、弧、指针到底怎么工作

2.1 四类indicator对象,配置前先搞清楚

lv_meter 的体系可以拆成两层理解:第一层是 scale,负责定义表盘的“骨架”;第二层是 indicator,负责在骨架之上显示“内容”。

scale 决定三件事:表盘的量程最小值、最大值,表盘弧线的起始角度、结束角度,以及刻度的数目、长度、粗细和颜色。一个 meter 可以挂多个 scale,这意味着同一个仪表控件里可以同时存在两套不同的刻度系统,比如内圈白色、外圈黄色,视觉层次一下就出来了。

indicator 有四种:

  • arc:弧线指示器。通常用来做量程色带,也可以做成“进度条”效果。
  • needle_line:直线指针。最常用,适合车速、转速这类指针式仪表。
  • needle_img:图片指针。适合有特殊造型需求的指针,比如箭头形、飞机形。
  • line:刻度线。这个其实已经包含在 scale 配置里,但也可以单独当成 indicator 来用。

什么叫“指示器”?你可以理解成“贴在表盘上的一个可移动标记”。arc 是沿着弧线移动长度,needle 是绕着圆心旋转角度,它们都绑定到一个 scale 上,由 scale 的 value 到角度映射关系来决定显示位置。正因为是配置式,你可以在同一个 scale 上挂好几个指示器,互相不冲突。

2.2 角度范围、量程映射和旋转中心

lv_meter 里最让人头晕的就是角度。LVGL 的屏幕坐标系是 y 轴向下,所以 0 度在正右方,90 度在正下方,逆时针方向角度减小、顺时针方向角度增大。也就是说,跟数学课本里的坐标系是反的。

lv_meter_set_scale_range(meter, scale, min, max, start_angle, end_angle)的前三个参数好理解,关键是后两个。它的内部逻辑是:从start_angle开始,沿着角度增大的方向走到end_angle,画出一段扇形弧。如果你写start=270, end=90,那 270 度(正上方)出发,顺时针经过 0 度走到 90 度(正下方),正好走 270 度。这就是经典的车速表开口朝下的形状。

如果你写反了,比如start=90, end=270,那弧线就变成了一个开口朝上的倒 U 形,看起来非常别扭。我早期踩过这个坑,后来总结出一个检查方法:先把 min、max 设成 0 到 1,随便拖一个 indicator,看它从哪边开始、往哪边动,方向对不对一目了然。

旋转中心默认是控件的正中心。指针粗细由 width 决定,长度则由 r_mod 控制。r_mod 可以为负,表示指针从中心反向伸出,这在飞机仪表那种“指针从边缘指向中心”的造型里很有用。

2.3 最小可运行的meter骨架

不管多复杂的表盘,代码骨架永远是这几行:

lv_obj_t *meter = lv_meter_create(parent); lv_obj_set_size(meter, 240, 240); lv_obj_center(meter); lv_meter_scale_t *scale = lv_meter_add_scale(meter); lv_meter_set_scale_ticks(meter, scale, 31, 2, 12, lv_palette_main(LV_PALETTE_GREY)); lv_meter_set_scale_major_ticks(meter, scale, 6, 4, 20, lv_palette_main(LV_PALETTE_GREY), 12); lv_meter_set_scale_range(meter, scale, 0, 100, 270, 90); lv_meter_indicator_t *needle = lv_meter_add_needle_line(meter, scale, 4, lv_palette_main(LV_PALETTE_ORANGE), -20); lv_meter_set_indicator_value(meter, needle, 50);

这段代码跑起来,你就能看到一个带刻度和指针的圆形表盘。后续要做的所有高级效果,都是在这个骨架基础之上加参数、加指示器。所以先把骨架跑通,后面自然就顺了。

3. 5分钟实战:跑起来一个汽车仪表盘

3.1 第一步:PC模拟器环境搭建(新手别直接上板子)

我强烈建议第一步不要碰板子,先在 PC 模拟器上把效果调出来。LVGL 的渲染逻辑跟平台解耦,同样一套代码在模拟器上跑和板上跑几乎一致,差别只是帧率。模拟器调试快,还能直接截图、打断点,比反复烧录固件效率高太多。

两种常用的跑法:

用官方 PC 模拟器工程。下载 lv_port_pc_vscode 这类模板,里面已经配好了 CMake,打开终端执行 cmake 和 make,编译完直接弹出窗口看到 LVGL demo。这种方式最贴合“纯 LVGL 开发”的流程,适合想要完整掌控代码的人。

用在线模拟器。比如官方提供的 Simulator 网页版,以及 SquareLine Studio 这类可视化工具,可以拖拽控件生成 UI 代码。有些同事习惯用工具出界面初稿,再导出手改逻辑,效率也很高。但要注意,工具生成的工程版本和你目标板上的 LVGL 版本要尽量一致,否则迁移的时候 API 差异会让人崩溃。

如果只想验证 lv_meter 本身,模拟器里跑官方示例代码就够了。跑通后再考虑往 STM32、ESP32 上移植,那时候你手里的代码已经验证过逻辑,硬件问题只剩下底层驱动和资源调优,难度直线下降。

3.2 第二步:创建表盘刻度、量程弧和指针

现在做一个车速表,量程 0 到 240 km/h,表盘角度范围从 135 度到 45 度,也就是一个约 270 度的大圆弧,开口朝下。

先创建 meter 并设置尺寸,然后添加 scale,配置刻度和量程:

lv_obj_t *meter = lv_meter_create(parent); lv_obj_set_size(meter, 300, 300); lv_obj_center(meter); lv_meter_scale_t *scale = lv_meter_add_scale(meter); lv_meter_set_scale_ticks(meter, scale, 31, 2, 12, lv_palette_main(LV_PALETTE_GREY)); lv_meter_set_scale_major_ticks(meter, scale, 6, 4, 20, lv_palette_main(LV_PALETTE_BLUE), 14); lv_meter_set_scale_range(meter, scale, 0, 240, 135, 45);

这里lv_meter_set_scale_ticks配置的是小刻度,参数依次是:刻度数量 31、宽度 2 像素、长度 12 像素、颜色灰色。lv_meter_set_scale_major_ticks配置的是主刻度,数量 6、宽度 4、长度 20,最后一个参数 14 是数字标签与主刻度末端之间的距离,大于 0 就会在对应位置显示数值文字。

然后加量程色带。用绿色弧线覆盖 0 到 160,黄色弧线覆盖 160 到 200,红色弧线覆盖 200 到 240:

lv_meter_indicator_t *arc_green = lv_meter_add_arc(meter, scale, 10, lv_palette_main(LV_PALETTE_GREEN), 0); lv_meter_set_indicator_start_value(meter, arc_green, 0); lv_meter_set_indicator_end_value(meter, arc_green, 160); lv_meter_indicator_t *arc_yellow = lv_meter_add_arc(meter, scale, 10, lv_palette_main(LV_PALETTE_YELLOW), 0); lv_meter_set_indicator_start_value(meter, arc_yellow, 160); lv_meter_set_indicator_end_value(meter, arc_yellow, 200); lv_meter_indicator_t *arc_red = lv_meter_add_arc(meter, scale, 10, lv_palette_main(LV_PALETTE_RED), 0); lv_meter_set_indicator_start_value(meter, arc_red, 200); lv_meter_set_indicator_end_value(meter, arc_red, 240);

最后加一根指针,初始值指向 0:

lv_meter_indicator_t *needle_speed = lv_meter_add_needle_line(meter, scale, 4, lv_palette_main(LV_PALETTE_ORANGE), -20); lv_meter_set_indicator_value(meter, needle_speed, 0);

到这里,一个完整表盘的静态部分就结束了。注意lv_meter_add_arc的第五个参数表示弧线相对外圆的半径偏移量,0 表示正好紧贴刻度线内侧;如果你希望弧线往里收一点,给个正数,比如 10,弧线就会整体缩进 10 像素。

3.3 第三步:用定时器让指针动起来

静态表盘没意思,要让指针动起来。最简单的方案是用lv_timer,每隔 100 毫秒更新一次数值,模拟车速变化:

static lv_obj_t *meter_speed; static lv_meter_indicator_t *needle_speed; static void speed_timer_cb(lv_timer_t *timer) { static int32_t speed = 0; static int8_t delta = 5; speed += delta; if (speed >= 240 || speed <= 0) { delta = -delta; } lv_meter_set_indicator_value(meter_speed, needle_speed, speed); } void create_dashboard(lv_obj_t *parent) { meter_speed = lv_meter_create(parent); /* ... 上面的表盘初始化代码 ... */ lv_timer_create(speed_timer_cb, 100, NULL); }

注意lv_meter_set_indicator_value调用后不需要手动触发重绘,lv_meter 内部会自动把对应的区域标记为无效并请求重绘。指针移动的时候,LVGL 只会刷新 meter 所在的矩形区域,不是整屏刷新,所以开销可控。

如果你想让指针不是硬跳,而是像机械指针那样带一点缓动效果,可以用lv_anim动画:

static void needle_anim_cb(void *obj, int32_t value) { lv_meter_set_indicator_value(meter_speed, needle_speed, value); } void set_speed_with_anim(int32_t target_speed) { lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, meter_speed); lv_anim_set_exec_cb(&a, needle_anim_cb); lv_anim_set_values(&a, current_speed, target_speed); lv_anim_set_time(&a, 500); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); lv_anim_start(&a); }

这个方式在真实车载项目里更好用,因为外部车速传感器传进来的值往往是突变的,直接赋值会让指针显得很“电子”,加个 500 毫秒的缓动动画立刻就有油润感了。

3.4 完整代码:车速表直接抄作业

我把上述代码拼成一个完整函数,你直接复制到模拟器工程里就能看效果:

#include "lvgl.h" static lv_obj_t *meter_speed; static lv_meter_indicator_t *needle_speed; static void speed_timer_cb(lv_timer_t *timer) { static int32_t speed = 0; static int8_t delta = 5; speed += delta; if (speed >= 240 || speed <= 0) { delta = -delta; } lv_meter_set_indicator_value(meter_speed, needle_speed, speed); } void create_car_dashboard(lv_obj_t *parent) { meter_speed = lv_meter_create(parent); lv_obj_set_size(meter_speed, 300, 300); lv_obj_center(meter_speed); lv_meter_scale_t *scale = lv_meter_add_scale(meter_speed); lv_meter_set_scale_ticks(meter_speed, scale, 31, 2, 12, lv_palette_main(LV_PALETTE_GREY)); lv_meter_set_scale_major_ticks(meter_speed, scale, 6, 4, 20, lv_palette_main(LV_PALETTE_BLUE), 14); lv_meter_set_scale_range(meter_speed, scale, 0, 240, 135, 45); lv_meter_indicator_t *arc_green = lv_meter_add_arc(meter_speed, scale, 10, lv_palette_main(LV_PALETTE_GREEN), 0); lv_meter_set_indicator_start_value(meter_speed, arc_green, 0); lv_meter_set_indicator_end_value(meter_speed, arc_green, 160); lv_meter_indicator_t *arc_yellow = lv_meter_add_arc(meter_speed, scale, 10, lv_palette_main(LV_PALETTE_YELLOW), 0); lv_meter_set_indicator_start_value(meter_speed, arc_yellow, 160); lv_meter_set_indicator_end_value(meter_speed, arc_yellow, 200); lv_meter_indicator_t *arc_red = lv_meter_add_arc(meter_speed, scale, 10, lv_palette_main(LV_PALETTE_RED), 0); lv_meter_set_indicator_start_value(meter_speed, arc_red, 200); lv_meter_set_indicator_end_value(meter_speed, arc_red, 240); needle_speed = lv_meter_add_needle_line(meter_speed, scale, 4, lv_palette_main(LV_PALETTE_ORANGE), -20); lv_meter_set_indicator_value(meter_speed, needle_speed, 0); lv_timer_create(speed_timer_cb, 100, NULL); }

运行效果就是屏幕上出现一个开口朝下的车速表,指针每 100 毫秒跳一次,从 0 冲到 240 再掉回来,绿色黄色红色三个弧形色带层次分明。到了这一步,5 分钟目标基本达成。

4. 从单个表盘到整车仪表面板:布局、毛玻璃与性能

4.1 双表盘布局和视觉分层

单个表盘跑通之后,下一步自然是把它变成一整块汽车仪表。一般布局是左边转速表、右边车速表,中间留一块区域显示挡位、里程、油量等信息。

LVGL 里布局容器用lv_obj加 flex 或 grid 就行。比如创建一个大容器,设置 flex 为 row,再往里面丢两个 meter,间距用lv_obj_set_style_pad_column控制:

lv_obj_t *panel = lv_obj_create(parent); lv_obj_set_size(panel, 800, 320); lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_ROW); lv_obj_set_style_pad_all(panel, 20, 0); lv_obj_set_style_pad_column(panel, 40, 0); lv_obj_center(panel); create_speed_meter(panel); create_rpm_meter(panel);

视觉上要注意分层。两个表盘尺寸最好不要完全一样,车速表大一点、转速表小一点,中间信息区再做点层次,这样主次分明。不要让两个表盘像双胞胎一样并排立那儿,用户第一眼看过去不知道先看哪个。

表盘之间的信息区可以用lv_label叠加,比如显示当前挡位 D、总里程 12345 km。这部分不是 lv_meter 的核心,但同时出现在界面上,整体感会强很多。

4.2 毛玻璃效果在低资源设备上怎么做

这几年“毛玻璃”风格从手机系统一路火到嵌入式 UI,热搜词里都有“lvgl 毛玻璃”。LVGL 9.x 对模糊效果确实有相关支持,可以在部分场景下做出实时模糊背景的面板。

但我要泼一盆冷水:实时模糊在带 GPU 或足够 CPU 余量的平台上很出效果,在普通 Cortex-M 上代价不小。一个 800x480 的界面,模糊区域一多,渲染开销很容易吃掉大半 CPU,帧率直接掉到能用和不能用之间。

我的习惯做法是“伪毛玻璃”:先准备一张预先模糊过的静态底图,放在面板底下,面板本身用半透明白色或半透明深色覆盖,再配合圆角和 1 像素描边,视觉上已经很接近毛玻璃,开销却只是多了一次 alpha blend。如果确实想要实时模糊,优先把模糊区域缩小,不要整个面板都上,只对局部小卡片做,性能可控。

总之,毛玻璃是加分项,不是必需品。做嵌入式界面,先把功能逻辑跑通,再考虑视觉装饰,永远不要本末倒置。

4.3 刷新率、内存和渲染加速的取舍

仪表盘这种 UI,最怕的是刷新的时候闪烁或者撕裂。LVGL 里通常通过配置缓冲区来缓解。RGB565 颜色格式下,一块 320x240 的屏幕全屏缓冲要 320x240x2 = 153600 字节,也就是 150 KB,很多 MCU 的内存放不下。所以实际产品里常用单缓冲加局部刷新,或者双缓冲加 DMA 传输。

对 lv_meter 这种控件,指针移动时它会把整个 meter 控件的区域标记为脏区域。如果 meter 是 300x300,那每次指针动一下,就是 90 平方毫米的重绘量。因此表盘不要做得比实际需要更大,尺寸越大,刷新的像素数是指数增长。

关于渲染加速,热搜词里有人问“T113-S3 的 G2D 适不适合做 LVGL 渲染加速”。我的判断是:这类 2D 引擎主要擅长填色、拷贝、颜色格式转换、混合这些操作,适合在 LVGL 的 flush 回调里做显存的搬运和合成,能明显降低 CPU 拷贝负担。但 lv_meter 画刻度、画弧线、画指针本身依赖 LVGL 的软件渲染器,2D 引擎帮不上多少忙。所以不要指望加个 G2D 之后所有控件都飞起来,重点是看你的瓶颈到底在绘制还是拷贝。先用软件渲染跑一遍,用日志量一下每帧耗时,再决定要不要把加速器接进去。

5. 踩坑记录与实战排查

5.1 常见问题的症状与解决办法

我把这几年用到 lv_meter 遇到过的典型问题整理成一张表,适合直接贴在工位上排查用:

症状常见原因解决办法
表盘完全不显示lv_conf.h里LV_USE_METER没打开检查配置宏,打开后重新编译
刻度方向反了start_angle 和 end_angle 写反记住 LVGL 0 度在右侧,角度顺时针增长
数字文字变成方块字体没有包含数字字符集启用对应字体并勾选 ASCII 或数字范围
指针不转动传入lv_meter_set_indicator_value的 indicator 指针不对确认用的是lv_meter_add_needle_line的返回值
指针转起来很卡meter 尺寸太大或缓冲区太小缩小表盘区域,调整显示缓冲区大小
量程弧盖住了刻度arc 的宽度太大,或半径偏移设置不当调整 arc 宽度和 r_mod 偏移量
表盘周边有黑影父容器没有裁剪或样式背景没处理设置父容器背景透明或开启裁剪

其中“数字文字变成方块”这个问题最容易让人困扰。因为 LVGL 默认字体大多只包含 ASCII 基本字符,如果你设了 label_gap 显示刻度数字,但字体没包含数字,就会出现空方块。解决办法是在lv_conf.h里选择包含 ASCII 的字体,或者用字体转换工具把数字字符集包含进去。

5.2 在STM32/ESP32类MCU上移植的注意事项

把模拟器代码往 MCU 上搬,最常见的问题不是 lv_meter 本身,而是工程层面的配置。

内存方面,meter 控件本身动态分配内存,每个 scale、indicator 都会占用一小块。一个表盘分别挂 3 个弧、1 个指针、1 个 scale,几十个刻度,整体开销大概在几百字节到几 KB 之间,要看具体配置。如果出现花屏、随机崩溃,优先检查LV_MEM_SIZE是不是太小。

如果使用 FreeRTOS,LVGL 不是线程安全的,所有lv_*函数必须在同一个任务里调用。推荐的做法是单独建一个 UI 任务,循环调用lv_timer_handler(),业务数据通过队列或全局变量传递,在 UI 任务里统一更新控件。不要在多个任务里同时调lv_meter_set_indicator_value,轻则数据错乱,重则 hardfault。

新版 ESP-IDF 的 LVGL 组件已经做得比较完善,通过组件管理器直接拉取依赖就能跑起来,省去很多手写移植的麻烦。STM32 上如果平时习惯 Keil,也可以用现成的移植模板,把lv_conf.h和显示驱动接口对上就行。总之原则是:先确认基础 demo 能在板子上点亮屏幕,再往上加 lv_meter 的代码,这样定位问题范围会小很多。

5.3 工程化小建议:版本、字体、代码结构

最后给几条工程上的心得。

版本必须锁死。LVGL 8 和 9 的 API 有不少差异,网上代码混着抄很容易踩坑。项目一开始就选定大版本,升级前要评估 meter 相关 API 的变动。如果团队人多,把这套 meter 初始化封装成一个模块,输入参数是量程、角度范围、主刻度数量、颜色表,返回 meter 对象的指针,统一调用,后面换肤改样式只需要改模块内部。

字体一定要规划。表盘上的数字是刚需,数字字体最好单独抽出来,不要为一个小数字标签带上整套中文字体,内存会吃很大。嵌入式设备上字体资源往往比图像资源还容易超预算。

多做一层“值域层”。不要把业务数据直接塞给 meter,比如油量表、水温表都有各自的线性或非线性映射关系,可以先写一层数据转换函数,把物理量换算成 0 到 240 的标准化值,再给 meter 显示。以后换传感器、换量程,界面代码一行都不用动。

我后来做表盘类项目,基本默认第一选择就是 lv_meter,不太愿意回到手写弧线那种模式了。只要把 scale、arc、needle 这三个概念理清楚,后面不管是做成圆形、半圆还是多表盘组合,都是配置项的事情。希望这篇文章能帮你少走几步弯路,直接看到效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询