ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LVGL的lv_meter组件实战:从零到一快速打造嵌入式汽车仪表盘

LVGL的lv_meter组件实战:从零到一快速打造嵌入式汽车仪表盘 最近在做一块车载仪表盘的Demo甲方给的时间特别紧第一天还在纠结用什么方案第二天就要看到能动的效果。以前遇到这种需求第一反应是拿Canvas自己画刻度、画指针、算角度一套三角函数写下来小半天就没了还得考虑不同分辨率下的缩放问题。这次我直接用了LVGL自带的lv_meter组件结果从新建工程到指针转起来前后确实只花了5分钟。这篇文章就把这套打法完整记录下来从组件选型到完整代码再补上我在实际移植和性能调优过程中踩过的坑给准备用LVGL做仪表盘UI的朋友一个可以直接抄的作业。lv_meter是LVGL从8.0开始提供的内置组件专门用于实现仪表盘、压力表、温度表这类环形指示器。它把刻度线、数值弧线、指针几种元素全部封装好了你需要做的就是往一个meter对象上挂不同的指示器然后给指示器塞数值。它和你自己用draw事件画仪表最大的区别在于组件的核心计算全部走内部渲染管线自带局部刷新和脏矩形标记改一个数值只重绘受影响区域这在嵌入式单片机上非常关键。要说这个方案适合谁我觉得覆盖面挺广的。玩STM32、ESP32的老铁想在TFT屏上搞个像样的仪表界面用它正合适做上位机或者PC端HMI的LVGL有模拟器工程Windows和Linux都能跑开发调试速度飞快哪怕只是给学生毕设做个温湿度仪表lv_meter也能让你用一两百行代码就把界面做得像模像样。下面就按我实际开发的顺序一步步说。1. 为什么我选了lv_meter而不是自绘或者其它控件先说结论LVGL里能做仪表盘的方案其实有三种自绘Canvas、弧线弧条拼装、还有lv_meter。自绘Canvas看着自由度高什么样式都能画但代价是你得自己处理抗锯齿、角度换算、局部刷新而且Canvas的buffer在低配单片机上根本不敢开大弧线弧条拼装适合做进度条那种简单的圆环想呈现汽车仪表那种带刻度和指针的复杂盘面样式上很难做精细lv_meter本身就是为表盘场景生的你想要的刻度、指针、量程、颜色分段全都有现成接口。从渲染性能来看lv_meter内置的绘制逻辑会主动把变更区域标记为invalid然后只刷新这部分比你自己在draw回调里动不动刷全屏要高效得多。指针每次微调一个角度重绘范围只是指针扫过的那一小块扇形区域这在主频只有几十兆赫兹的MCU上体感差距会非常明显。另外还要考虑一个维护成本的问题。我见过不少工程用图片素材做表盘背景然后把指针旋转角度绑到lv_img上这种方案的资源占用其实不小高清表盘图在RGB565下动不动就上百KBFlash小的芯片放不下而且旋转时缩放滤波处理不做的话指针边缘锯齿非常难看。lv_meter所有元素都是矢量绘制不占额外图片资源改量程、改刻度颜色在代码里改一行就完事后期如果要出好几个不同风格的表盘复用和修改都灵活得多。2. 环境准备模拟器验证和芯片移植要先确认版本差异lv_meter在LVGL 8.x和9.x里都存在但API细节有过调整。我平时主力开发环境是LVGL 8.3 STM32F407当然也经常在PC模拟器上先调样式。做这类UI开发强烈建议你先在PC模拟器里把交互逻辑跑通再往板子上搬。模拟器可以无视Flash和RAM的限制改代码秒级看到效果效率比交叉编译烧录循环高一截。LVGL 9.x自带的模拟器工程已经升级成了SDL2驱动而8.x时代大部分是基于SDL1.2的老工程如果你的系统是新装的Ubuntu或者Windows建议直接用9.x模拟器省去一坨兼容性问题。真机移植的话不管你是裸机还是FreeRTOS跑lvgl驱动层只要能提供lv_tick和lv_disp_flush回调就能把lv_meter用起来。很多人问FreeRTOS里跑LVGL会不会有冲突其实LVGL官方就推荐在RTOS里跑只需要注意把lv_timer_handler放到一个独立任务里并且保证刷新回调disp_flush不要被信号量卡死就行。下面给一个最精简的初始化骨架这是我在STM32裸机环境下常用的写法static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[240 * 10]; void lvgl_init(void) { lv_init(); lv_disp_draw_buf_init(draw_buf, buf, NULL, 240 * 10); lv_disp_drv_init(disp_drv); disp_drv.hor_res 240; disp_drv.ver_res 240; disp_drv.flush_cb my_disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); lv_tick_set_cb(my_tick_get_ms); }一个比较容易踩的坑是颜色深度。模拟器默认32位色深真机屏幕多数是RGB565如果两边的LV_COLOR_DEPTH不一致同一个配色在不同平台上显色会有差异所以配模拟器的时候最好直接也设成16位色。另外lv_conf.h里的LV_MEM_SIZE不要开太小lv_meter虽然不耗显存但它的刻度calibration数据和各指示器对象都要从LVGL堆里分配我见过有人把LV_MEM_SIZE设到4KB结果meter建到一半返回NULL画面直接白屏。如果你也有类似怀疑可以先调用lv_mem_monitor打印一下剩余空间再排查。3. 一次性说透核心接口刻度、量程、指示器怎么搭配lv_meter的工作方式可以理解成把一个圆盘分成三层叠加先是scale定义刻度分布然后是tick线定义视觉刻度最后可以叠加多个indicator指示器在scale上做显示。indicator类型有arc、scale ticks、needle line、needle img四种。实际做汽车表盘只需要掌握前三种就够arc做量程内的彩色弧线ticks做高亮刻度needle line做指针。我觉得最容易让人懵的地方是scale和indicator是解耦的。同一个meter上可以挂多个scale每个scale有自己的量程和起始角度然后指示器必须绑定其中一个scale。比如一个meter想做车速表和转速表合在同一块屏里理论上可以在同一个圆形上定义两套scale一套0到240另一套0到8然后各自挂指针。不过实际产品设计中车速和转速通常还是分两个meter对象比较清晰这样各自动画互不干扰。还有一个小技巧lv_meter_add_needle_line的最后一个参数是指针尾部延长线的长度传正数会在指针反方向拉出一条尾巴传负数则表示指针从中心反向缩短。很多人不知道这个参数做转速表那种超长红色指针效果时它就是关键。具体到API的调用顺序我强烈建议严格遵循建载体→配scale→配指示器→设初始值的顺序。特别是set_indicator_value之前如果scale的范围还没设置好指示器内部会使用默认值容易出现指针飞出界外的bug。4. 完整代码实战车速表转速表一次搞定直接上硬货。下面这段代码是在LVGL 8.3下验证过的完整实现我把它封装成了一个create_speedometer函数调用一次就在屏幕上创建一个表盘。这段代码我实际测试过能用但如果你用的是9.x部分接口要做调整差异我会在后面列出来。#include lvgl.h /** * brief 创建一个带有配色分段的汽车车速表 * param parent 父对象 * param x x坐标 * param y y坐标 * return 创建好的lv_meter对象 */ lv_obj_t *create_speedometer(lv_obj_t *parent, lv_coord_t x, lv_coord_t y) { lv_obj_t *meter lv_meter_create(parent); lv_obj_set_pos(meter, x, y); lv_obj_set_size(meter, 200, 200); /* 1. 创建刻度分布对象 */ lv_meter_scale_t *scale lv_meter_add_scale(meter); /* 2. 配置量程0~240扫描范围从 270度起逆时针扫到 90度 也就是画一个从正上方开始、逆时针绕了3/4圈的圆环 */ lv_meter_set_scale_range(meter, scale, 0, 240, 270, 90); /* 3. 添加普通刻度线每 240/2012 一个短刻度线宽2px长10px灰色 */ lv_meter_set_scale_ticks(meter, scale, 21, 2, 10, lv_palette_main(LV_PALETTE_GREY)); /* 4. 添加主刻度线每 240/460 一个主刻度线宽4px长15px黑色文字间距10 */ lv_meter_set_scale_major_ticks(meter, scale, 4, 4, 15, lv_color_black(), 10); /* 5. 添加一段覆盖0~180范围的绿色弧线 */ lv_meter_indicator_t *arc_green lv_meter_add_arc(meter, scale, 12, lv_palette_main(LV_PALETTE_GREEN), 0); lv_meter_set_indicator_start_value(meter, arc_green, 0); lv_meter_set_indicator_value(meter, arc_green, 180); /* 6. 添加一段覆盖180~240范围的红色弧线 */ lv_meter_indicator_t *arc_red lv_meter_add_arc(meter, scale, 12, lv_palette_main(LV_PALETTE_RED), 0); lv_meter_set_indicator_start_value(meter, arc_red, 180); lv_meter_set_indicator_value(meter, arc_red, 240); /* 7. 添加一个带尾巴的白色指针指针根部反向延长20px */ lv_meter_indicator_t *needle lv_meter_add_needle_line(meter, scale, 4, lv_palette_main(LV_PALETTE_BLUE), 20); lv_meter_set_indicator_value(meter, needle, 0); /* 8. 中心圆点装饰 */ lv_obj_t *dot lv_obj_create(meter); lv_obj_set_size(dot, 16, 16); lv_obj_remove_style_all(dot); lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); lv_obj_set_style_bg_color(dot, lv_palette_main(LV_PALETTE_BLUE), 0); lv_obj_align(dot, LV_ALIGN_CENTER, 0, 0); return meter; }然后是转速表它和车速表最大的不同是量程和红色限制区域的位置。转速表通常会在近红区做一个渐变警示从6000转开始套一条高亮刻度线。实现起来其实就是在主刻度之外再叠加一层indicator类型的刻度线lv_obj_t *create_tachometer(lv_obj_t *parent, lv_coord_t x, lv_coord_t y) { lv_obj_t *meter lv_meter_create(parent); lv_obj_set_pos(meter, x, y); lv_obj_set_size(meter, 200, 200); lv_meter_scale_t *scale lv_meter_add_scale(meter); lv_meter_set_scale_range(meter, scale, 0, 8000, 270, 90); lv_meter_set_scale_ticks(meter, scale, 21, 2, 10, lv_palette_main(LV_PALETTE_GREY)); lv_meter_set_scale_major_ticks(meter, scale, 5, 4, 15, lv_color_black(), 10); /* 近红区高亮刻度6000~8000这段用indicator让刻度变红 */ lv_meter_indicator_t *red_ticks lv_meter_add_scale_ticks(meter, scale, 3, 4, 15, lv_palette_main(LV_PALETTE_RED)); lv_meter_set_indicator_start_value(meter, red_ticks, 6000); lv_meter_set_indicator_value(meter, red_ticks, 8000); lv_meter_indicator_t *needle lv_meter_add_needle_line(meter, scale, 4, lv_palette_main(LV_PALETTE_RED), 20); lv_meter_set_indicator_value(meter, needle, 0); return meter; }在main函数里把这两个表放上屏幕顺便加上一个数字速度显示void ui_dashboard_create(void) { lv_obj_t *scr lv_scr_act(); lv_obj_t *speed_meter create_speedometer(scr, 10, 10); create_tachometer(scr, 230, 10); /* 数字车速显示 */ lv_obj_t *speed_label lv_label_create(speed_meter); lv_obj_set_style_text_font(speed_label, lv_font_montserrat_24, 0); lv_label_set_text_fmt(speed_label, %d, 0); lv_obj_align(speed_label, LV_ALIGN_CENTER, 0, -20); }如果你用的是LVGL 9.x有几个改动点需要知道lv_meter_add_scale不再返回lv_meter_scale_t*而是返回一个uint32_t的索引后续所有set_scale函数里的scale参数都从指针变成了这个索引值另外lv_obj_remove_style_all改成了lv_obj_remove_style_all保持兼容但建议不用而lv_palette_main这种调色板宏在9.x里依然保留。整体迁移成本不大核心概念完全一致。5. 让指针动起来动画绑定与真实数据源的处理表盘光有静态指针肯定不行现实世界里仪表数据是一直变的。我推荐的做法是传感器或总线数据通过一个周期回调更新目标值再用lv_anim把指针从当前值平滑地插值到目标值。这样UI永远处于平滑过渡状态不会出现数值跳变的生硬感。下面这段是把速度值更新到指针上的配套逻辑static lv_obj_t *g_speed_meter; static lv_meter_indicator_t *g_needle; /* 动画回调LVGL的anim计算中间值后通过这个回调把值写到指示器上 */ static void set_needle_value(void *obj, int32_t value) { lv_meter_set_indicator_value((lv_obj_t *)obj, g_needle, value); } /* 外部传入要显示的速度值单位km/h */ void dashboard_set_speed(int32_t target_speed) { lv_anim_t a; lv_anim_init(a); lv_anim_set_var(a, g_speed_meter); lv_anim_set_exec_cb(a, set_needle_value); lv_anim_set_values(a, lv_meter_get_indicator_value(g_speed_meter, g_needle), target_speed); lv_anim_set_time(a, 500); lv_anim_set_path_cb(a, lv_anim_path_ease_out); lv_anim_start(a); }注意lv_anim_set_values里的起点是当前指示器已经显示的值而不是上一次的目标值。如果两次动画还没结束就又来了新值直接取指示器实时值会让动画衔接更自然。在模拟数据源时我喜欢用lv_timer每200ms产生一次随机增量模拟急加速、匀速、松油门滑行几个阶段这样在演示时表盘状态很真实。实际项目里我更建议在RTOS的传感器任务里更新目标值UI任务只负责取出最新的目标值并启动动画。比如你从CAN总线上拿到的速度报文通常是100ms一帧如果每一帧都直接set_indicator_value而不做平滑指针会因为报文抖动而疯狂抖动反而应该对数据做一次滤波或者滞后处理再喂给动画。关于数值映射lv_meter是按原始量程存值的所以喂给指针的值必须在0到240之间。如果传感器给的是ADC值0到4095就需要先做比例换算speed adc_raw * 240 / 4095这个乘除顺序很重要先乘后除会溢出int16建议用int32临时变量或者直接用浮点再取整。实际做产品时我一般把这类原始值到工程值的映射统一放到一个middleware层UI层永远只接收用户看得懂的工程值这样逻辑比较干净。6. 真机移植优化避免UI卡顿的几个关键配置PC模拟器上跑得丝滑不代表STM32上也能稳住60帧。lv_meter这个组件本身对CPU的消耗不算夸张但整个LVGL的渲染管线如果没配对照样能卡成PPT。我总结了几条最影响体验的配置大家可以对着自己的lv_conf.h逐条检查。6.1 显示buffer和刷新模式LVGL的缓冲区推荐两个或者三个。单缓冲在部分刷新模式下也能跑但每次LVGL把一帧合成完才开始flush屏幕刷新期间CPU是闲等的双缓冲允许一个缓冲在flush到屏幕时另一个缓冲已经在渲染下一帧流畅度明显提升。内存实在紧张至少保证单个buf不小于屏幕宽度的行数的1/10低于这个值会导致整屏重绘被切得非常碎刷新效率暴跌。我做过一个实测数据在320x240的RGB565屏上buf从一行提升到十行之后帧耗时能降低40%左右。6.2 DMA加速flush做好DMA搬运之后lv_meter这种需要频繁局部刷新的场景收益很大。flush回调里把buf里的数据交给DMA去传CPU可以在DMA传输的同时继续渲染下一帧。关于DMA的配置有一点必须提醒很多MCU的DMA传输完成后需要清中断标志而且DMA缓冲要和LCD控制器的读取时序配合好否则容易花屏。稳妥做法是参照你的屏厂例程里的flush实现不要自己凭空写。6.3 颜色深度和两字节对齐LV_COLOR_DEPTH要跟屏幕一致16位色深配RGB565屏是标准组合。如果要开透明效果就得用32位色深但32位色深会让刷新带宽翻倍MCU平台通常不建议这么做。另外lv_conf.h里的LV_COLOR_16_SWAP选项在ST7789、ILI9341这类常见屏幕驱动里一般是置1的因为这类屏的RGB字节序是反的。如果发现颜色偏色或者边缘有紫边先检查这个宏。6.4 严重卡顿时的三板斧排查法很多人问UI界面卡顿怎么查我自己的排查顺序是这样先打开LVGL事件计时定位是渲染慢还是flush慢再用lvtimer观察LVGL心跳是否被中断特别是FreeRTOS下如果lv_tick_set_cb用了错误的时基整个动画节奏会失灵。最后查内存内存碎片化严重时lv_meter创建多个对象的过程中可能频繁触发defrag这个在低内存机型上会出现周期性卡顿。我遇到过最奇葩的一种卡是lv_label在表盘中心每秒更新数字时把整个仪表盘的invalid区域全带出来了指针重绘频率被迫翻倍。解决方式其实很简单给那个label设置一个独立的样式开启lv_obj_set_style_local_opa之后它只重绘自己那一个小区域不再拖累整个meter。这也是我经常对新手说的UI卡顿不一定是指针组件本身的问题很可能是周围某个不起眼的小组件在作怪。7. 我踩过的几个lv_meter坑和对应的解决方式最后按惯例把踩过的坑集中列一下这些不是网上复制来的全是我自己真机调出来的教训对号入座可以省很多时间。第一个坑指针旋转方向和视觉方向相反。lv_meter的角度系统和屏幕坐标系是一样的0度在正右方且逆时针为正。很多车载表盘的起点是正上方所以通常把start_angle设成270度或者-90度。但如果你设的是270度到90度这种顺时针扫过270度的写法指针转起来会和直觉相反。最简单的判断方法是先随便设一个值看指针指向的方向是否符合预期不行就把start和end换一下。第二个坑指示器变成一条直线看不到弧线。一般是因为arc的宽度参数设置成了0或者arc的start_value和end_value之间的区间太小。注意lv_meter_add_arc的第三个参数是arc线宽不是半径千万别填成负数。有人用-1想表示自动填充模式结果一整段弧线直接消失排查了一下午。第三个坑同时创建多个仪表后不同表盘的刻度样式互相污染。我当时在同一个meter上挂了两套scale结果第二套scale创建后第一套的刻度显示就乱了。后来我才意识到meter在创建scale时会同步重建内部渲染上下文而tick的刷新可能延迟到下一次invalid视觉上就像偶尔串线。解决方案是不同表盘尽量用独立的meter对象别硬凑在一个对象里。第四个坑lv_meter_set_indicator_value的入参类型是int32_t但很多人的业务值是uint16_t类型提升后越界导致指针直接跳过半个表盘。这种问题在静态检查不严的环境里很容易漏掉建议在赋值前主动做一次clamp把值限定在量程范围内函数内部虽然也会处理边界但提前clamp能避免它做多余的动画补偿。第五个坑是关于内存的。LV_MEM_SIZE在16MB Flash、192KB RAM的芯片上可以给到16KB或更高但在一些只有20KB RAM的MCU上如果同时建两个meter加若干label很容易撞上限。此时建议调低LV_MEM_SIZE并打开LV_MEM_CUSTOM宏配合malloc/free使用。lvmeter经过裁剪后开销其实不大正常情况下两个表盘加几个label8KB左右内存就能跑得很稳。8. 把表盘做得更精致从能用到好看的进阶调整基础的lv_meter拉出来之后视觉效果还是偏裸的离产品级UI还有距离。我一般会在样式层继续打磨这几点圆弧外圈加阴影光晕、表盘底色改成深色渐变加深邃感、刻度文字用等宽字体避免数字跳动时宽度变化导致整排文字抖动、指针中心加一个金属质感的圆点。实现外圈光晕不需要额外画图直接给meter对象的style加box_shadow_width就行。深色底色可以用css式的背景渐变LVGL 8.3支持lv_obj_set_style_bg_grad_color配合角度渐变方向这块在样表上非常出效果。数字跳动导致文字宽度变问题可以给label设置一个固定宽度然后居中对齐或者直接用lv_label_set_text_fmt把数字格式化成fixed width比如lv_label_set_text_fmt(label, %3d, speed)。别小看这个细节我用%3d对齐后整个仪表的精致感立刻上了一个档次。关于字体仪表盘上数字最好用LVGL自带的montserrat字体它有等宽变体或者自己转换一款阿拉伯数字字体保证所有数字宽度一致。另外指针的颜色建议和红区警示色统一这样紧急情况下红色指针压进红色区域时视觉冲击力很强驾驶员一眼就能感知到危险状态。9. 给新手的最后建议我写这篇的时候一直在想lv_meter最打动我的其实不是省了那几行三角函数而是它把仪表盘这个高频UI场景从底层渲染到状态管理都标准化了。你现在花10分钟学会这个组件以后在做物联网设备、车载娱乐系统、工业HMI项目时都能直接复用。如果觉得这套东西还不够用LVGL官方还带了一个lv_demo_meter的示例工程里面有三四个不同风格的表盘源码写法比我这里更花哨新手拿去看一遍也会有很大收获。最后再分享一个开发习惯先模拟器调样式再上真机跑性能。我在PC模拟器上把表盘配色、指针长度、刻度密度全部敲定之后到板子上几乎没有改过一行和界面相关的代码唯一调的就是UART速度数据的解析。这套流程已经帮我在三个项目里稳定产出过仪表盘界面了效率真的高。希望这篇文章也能帮你在下一块屏幕上快速转起指针来。
返回列表