尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用LVGL lv_chart做动态心电图:实时波形从模拟器到STM32的完整实践

用LVGL lv_chart做动态心电图:实时波形从模拟器到STM32的完整实践 LVGL的demo跑起来容易跑出感觉难。很多人在官方例程里把按钮、滑块玩了一遍之后就不知道该拿这套框架做什么了——控件都会用但拼不出一个像样的界面。图表控件(lv_chart)是我认为最适合打破这种僵局的入口尤其是动态心电图这个效果视觉冲击力强、数据逻辑简单、还顺带把LVGL的定时器、刷新机制、局部重绘这些核心技术点全串了一遍。这篇文章就用一个完整的动态心电图例子讲清楚怎么在5分钟内用lv_chart把一个会动的绿色波形放到屏幕上代码可以直接复制到你的LVGL工程里跑。这个demo适合谁一类是刚开始接触LVGL、想找一个有展示效果的项目练手的新手另一类是已经在做产品、需要在屏幕上显示实时波形ADC采样、心率、温湿度曲线等的开发者。前者可以拿它练手后者可以把代码里的波形数组替换成自己的真实数据当作一个可复用模板。1. 为什么心电图是lv_chart练手的最佳题材1.1 图表控件的真实定位LVGL提供了一堆控件但大部分控件是围绕交互设计的按钮要按、滑块要拖、列表要滚。图表控件(lv_chart)不一样它围绕数据工作把一组数组映射成坐标系里的折线、柱状图或散点。不要小看这个区别在实际产品里能直观展示数据的控件往往比花哨的交互控件更关键。我见过不少用LVGL做出来的智能穿戴、工业仪表、医疗小设备最核心的界面就是一条波形心率波形、呼吸波形、电机电流波形、电池放电曲线。这些界面用其他控件拼不出来只能靠图表控件。而动态心电图正好是这个类别里最典型、最直观的一个场景它需要持续的滚动刷新天然考验你对LVGL数据更新和重绘机制的理解。玩通这一个案例再去做其他图表类页面就是改改数据源、调调坐标轴的事。1.2 先搞懂lv_chart的数据模型再开始写代码把心电图效果做出来不在于记住API而在于理解lv_chart的数据模型。它不是一个随意画线的画板而是一个数组的可视化控件。控件内部维护着一个点数组每个点对应坐标系里的一个采样值图表负责把这些点连成线。自定义图表前必须理解四个概念序列series一条独立曲线。同一个chart可以挂多条序列每个序列有自己的颜色、绑定的y轴。心电监护仪上的多导联波形就是靠多序列叠加出来的。点数量point count一条序列缓冲区的长度也就是图表横向最多展示多少个采样点。点越多波形越宽但刷新的计算量也越大。Y轴范围range数据映射的上下界。数据超出范围就会被裁剪视觉上表现为波形撞到顶或撞到底。更新模式update mode这是动态效果的关键。SHIFT模式像心电监护仪的打印纸新数据从右边推进旧数据整体左移CIRCULAR模式像一个环形缓冲波形在固定窗口内不断覆盖旧数据。心电监护仪那种流淌的感觉必须用SHIFT。有一点容易踩坑lv_chart底层就是数组调用更新函数时它会把整段数据往左挪一位再在末尾写入新值。这意味着动态波形的刷新成本与point count成正比而不是与屏幕宽度成正比。point count给得太大弱芯片上会明显卡顿给得太小波形又看不出连续趋势。后面我会给出一个比较平衡的取值参考。2. 最快的启动路径PC模拟器里先跑通2.1 为什么先不要碰板子我见过太多新手拿到开发板后第一件事就是把LVGL往板子上烧结果卡在屏幕驱动、SPI配置、编译报错这些地方还没看到图表长什么样就劝退了。做UI效果的合理顺序应该是先在PC模拟器里验证逻辑和视觉再往板子上移植。LVGL官方提供了PC模拟器工程主流方案是VSCode CMake SDL2。在Windows和Linux上都能跑。模拟器里的入口就是main循环里持续调用lv_timer_handler()界面显示在SDL2窗口里鼠标可以模拟触摸。你在模拟器里写的业务代码和板子上跑的完全一样移植的时候不需要改任何UI逻辑只处理底层显示和输入驱动就行。我自己的经验是模拟器上调试一个界面的成本是十几秒而板子上烧录一次往往要几十秒甚至几分钟。动态心电图这种需要反复调速度、调波形、调颜色的场景在模拟器里调效率高太多。2.2 模拟器工程里要改的几个开关模拟器工程跑起来的前提是lv_conf.h配置正确。默认配置一般可以直接用但建议确认三个地方LV_USE_CHART图表控件的总开关。自定义精简配置时容易把它关掉导致编译报“lv_chart_create未定义”。LV_MEM_SIZELVGL动态内存池大小。模拟器里内存宽裕8.3版本可以直接给到128KB以上方便后续加页面。9.x版本默认走系统堆就不太依赖这个宏。LV_COLOR_DEPTH模拟器窗口一般用ARGB8888不用改。板子如果只用RGB565模拟器里先按32位调试最后移植时再改回16位处理一遍颜色即可。还有一个隐蔽的配置项是LVGL刷新周期。老版本叫LV_DISP_DEF_REFR_PERIOD默认30ms左右刷新一次脏矩形9.x之后宏名有变化。如果后面发现曲线一顿一顿可以把这个值往20ms甚至16ms调但要结合主循环繁忙程度综合判断不是越小越好。2.3 验证demo的完整流程假设你已经有一个能运行的LVGL模拟器工程接下来的流程很短新建一个c文件把第三节的完整代码复制进去。在main初始化LVGL之后调用lv_demo_ecg()。编译运行看到绿色波形持续向左滚动就成功了。确认主循环里有lv_timer_handler()否则定时器和刷新都不会工作。这步跑通之后剩下的就是调参数。我试过很多次只要波形数据、定时器周期、chart配置三件事是匹配的动态效果几乎零成本。真正花时间的往往是后面调成像心电监护仪的过程。3. 动态心电图完整代码与逐段拆解3.1 完整代码下面这段代码基于LVGL 8.3编写在9.x下同样可用API差异我会在第五部分说明。代码里不依赖具体硬件模拟器和STM32等单片机都能直接跑。/* 基于LVGL 8.3 / 9.x 的动态心电图示例 * 调用 lv_demo_ecg() 即可在屏幕上看到波形效果 */ #include lvgl.h #define ECG_POINT_CNT 60 /* 图表上同时显示的点数 */ #define ECG_WAVE_SIZE 57 /* 一个心动周期的波形点数 */ #define ECG_TICK_MS 20 /* 定时器周期单位毫秒 */ #define ECG_POINT_PER_TICK 2 /* 每个定时器周期推进的采样点数 */ static lv_obj_t *chart; static lv_chart_series_t *ser_line; static uint16_t wave_pos 0; /* 一个心动周期的PQRST波形数据模拟心电监护仪 */ static const int16_t ecg_wave[ECG_WAVE_SIZE] { 100, 100, 102, 104, 106, 108, 110, 112, 110, 108, 105, 100, 96, 90, 84, 76, 68, 60, 70, 85, 110, 150, 210, 280, 360, 420, 460, 480, 450, 390, 320, 250, 190, 145, 118, 100, 95, 88, 82, 78, 80, 85, 93, 100, 108, 118, 126, 130, 128, 122, 112, 100, 98, 97, 99, 100, 100, 100 }; static void ecg_timer_cb(lv_timer_t *timer) { (void)timer; for (int i 0; i ECG_POINT_PER_TICK; i) { lv_chart_set_next_value(chart, ser_line, ecg_wave[wave_pos]); wave_pos; if (wave_pos ECG_WAVE_SIZE) wave_pos 0; } } void lv_demo_ecg(void) { chart lv_chart_create(lv_scr_act()); lv_obj_set_size(chart, 480, 220); lv_obj_center(chart); lv_obj_set_style_bg_color(chart, lv_color_black(), 0); lv_obj_set_style_border_width(chart, 0, 0); lv_obj_set_style_radius(chart, 8, 0); lv_obj_set_style_pad_all(chart, 8, 0); lv_chart_set_type(chart, LV_CHART_TYPE_LINE); lv_chart_set_point_count(chart, ECG_POINT_CNT); lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 550); lv_chart_set_update_mode(chart, LV_CHART_UPDATE_MODE_SHIFT); lv_chart_set_div_line_count(chart, 8, 12); lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_X, 10, 5, 6, 3, true, 40); lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_Y, 10, 5, 5, 3, true, 40); ser_line lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_GREEN), LV_CHART_AXIS_PRIMARY_Y); lv_obj_set_style_line_width(chart, 2, LV_PART_ITEMS); lv_obj_set_style_size(chart, 0, LV_PART_INDICATOR); /* * 预热先把图表填满一个周期的样本。 * 如果在SHIFT模式下不预热启动时会看到一条从0跳上来的竖线。 */ for (int i 0; i ECG_POINT_CNT; i) { lv_chart_set_next_value(chart, ser_line, ecg_wave[wave_pos]); wave_pos; if (wave_pos ECG_WAVE_SIZE) wave_pos 0; } /* 用LVGL定时器驱动波形滚动周期20ms */ lv_timer_create(ecg_timer_cb, ECG_TICK_MS, NULL); }3.2 逐段拆解波形数据、chart配置、预热与定时器代码看起来不多但每个决定都有原因。先看波形数据真实的PQRST波包括P波、QRS波群、T波如果临时用正弦和指数衰减去拟合代码会很乱。直接用一个静态查找表把想要的心电形状固化下来是可控性最高的方案。数组里的100是基线值R波尖峰拉到480。以后接入真实ADC数据时只需要把数据源替换掉这个数组其余逻辑不用动。再看chart配置。point count设60range设0~550。range为什么留这么大余量因为R波峰在480如果range刚好卡到480尖峰看起来就像贴在顶边留一点余量反而有医疗设备那种从容感。update mode设为SHIFT这是滚动效果的灵魂。div line count控制网格线8行12列比较像心电图纸的方格。然后是两处容易被忽略的样式LV_PART_ITEMS里的line width设成2让曲线有厚度LV_PART_INDICATOR里的size设成0去掉数据点的小圆点。如果不关掉数据点60个点会变成一串圆珠观感立刻崩掉。预热那段循环是我踩过坑之后专门加的。LVGL chart在SHIFT模式下如果初始数据没填充默认值都是0屏幕左侧会出现一条从底部拉到当前位置的竖线像故障信号。预热代码在启动时先用一个周期的样本把缓冲区填满屏幕一出现就是完整波形不会有一闪而过的竖线。最后是定时器。lv_timer_create创建的是LVGL定时器由主循环的lv_timer_handler统一调度。为什么不直接用线程加delay因为LVGL界面需要持续处理刷新、事件、动画如果用独立线程塞数据还得考虑线程安全和刷新时机。lv_timer是LVGL自己管理的不会干扰主循环这个设计对动态刷新场景非常合适。3.3 滚动速度与心率换算动态心电图要像真的滚动速度不能随便拍脑袋。速度由三个变量决定波形数组长度、每次回调塞入的点数、定时器周期。换算公式是心率(bpm) ≈ 60 × ECG_POINT_PER_TICK × 1000 / (ECG_WAVE_SIZE × ECG_TICK_MS)把代码里的值代入60 × 2 × 1000 / (57 × 20)约等于105bpm。这个速度在屏幕上看起来偏快适合演示紧张感。想降到正常心率把ECG_POINT_PER_TICK改成1就变成约52bpm或者在波形数组里多塞几个平直段拉长两次R波之间的间隔。想要精确到60bpm可以把timer周期调到35ms每tick推2个点ECG_WAVE_SIZE保持57算下来约60.2bpm。这个公式我在换不同波形数组时经常用到建议你也记下来比瞎试参数高效得多。4. 从能跑到像台心电监护仪参数与波形优化4.1 让波形更接近真实PQRST基础版本跑通后第一件想做的事通常是让波形更“真”。两个方向最有效改数组细节和加密采样点。真实心电信号里有ST段形态、T波对称度这些细节。想模拟病态模式比如心动过速、心室早搏直接改数组不同区段的值就行。我会把一套公共波形抽出来当模板再定义几种病态模式切换模板就能做出不同监护效果。以后接入真实数据时同样保留这个模板思维一套界面就能适配多种数据源。57个点描述一个周期轮廓够用但放大后会有轻微折角感。把数组扩到100个点以上R波可以画出更圆滑的上升下降沿。代价是同样的显示宽度下每次需要推进更多点才能保持滚动速度。在PC模拟器上可以随便试到了板子上就要看MCU性能和屏幕刷新率。以STM32F103这种Cortex-M3跑LVGL100个点的chart完全没压力瓶颈在屏幕不在CPU。4.2 多通道、噪声和基线漂移想要更像多导联监护仪加第二条序列是性价比最高的操作。做法很简单再add一次series换个颜色取波形数组时把wave_pos往后偏移十几个位置。两条波形看起来同步又错峰视觉层次立刻立体起来。我习惯绿色和黄绿色各一路放在黑色背景上非常像医院监护仪。真实采集信号里面还有噪声和基线漂移。噪声模拟可以用伪随机抖动比如在采样值上叠加一个[-4, 4]的随机数。注意别在LVGL的timer回调里大量调用rand量大会拖慢刷新更好的做法是预生成一个噪声数组按索引循环取值。基线漂移则用一个缓慢变化的低频正弦波叠加到100的基线上波形会像真实病人呼吸时那样整体上下浮动。这些效果对代码改动都很小但对演示效果的提升却非常大。4.3 视觉调参对照表下面这张表是我反复调出来的参数区间直接对着调就行参数作用建议值ECG_POINT_CNT图表缓冲点数决定显示宽度60~120ECG_WAVE_SIZE一个心动周期样本数控制波形细节57~120ECG_POINT_PER_TICK每次定时器推进点数影响滚动速度1~4ECG_TICK_MS定时器周期影响整体刷新节奏16~40Y轴range数据上下界留余量避免撞顶撞底R峰值的1.1倍左右line width曲线粗细1~3div line count网格密度模拟心电图纸8行12列左右这里有一个取舍要重点说point count太大屏幕上波形动态范围广但每次左移需要更新的点也多弱屏上会卡。point count太小一个心动周期还没显示完就滑出屏幕观感局促。我通常让point count略大于或等于wave size让屏幕正好保留一个完整心跳这是最耐看的比例。5. 移植到STM32后会踩的坑刷新、内存与API差异5.1 为什么模拟器流畅上板就卡顿这是我在真机上遇到最多的打击。模拟器里曲线丝般顺滑烧到开发板上就变成一格一格地跳。核心原因几乎都在屏幕刷新的底层而不是LVGL画图本身。lv_chart每次set_next_value都会让图表区域进入脏矩形LVGL在下一个刷新周期重绘整个区域然后调用你的flush函数把像素推到屏幕。如果flush是SPI阻塞发送发送一帧320×240真彩色全屏数据的时间会非常长波形自然卡。如果你用的是STM32F103加ILI9341 SPI屏SPI时钟压到40MHz时一屏RGB565全刷约150KB纯阻塞发送耗时在5ms左右加上LVGL重绘、业务逻辑、看门狗喂狗帧率会掉得很难看。这种情况下优化顺序应该是先查SPI是否用了DMA再查flush是否及时调用lv_disp_flush_ready最后才轮到LVGL的配置参数。5.2 SPI屏刷新问题的排查链路如果你也遇到波形一跳一跳按这个顺序排查看lv_conf里的刷新周期。老版本LV_DISP_DEF_REFR_PERIOD默认30ms主循环一忙还会被拉长。先调到20ms试效果。查底层flush函数。LVGL渲染完成后会调用display driver注册的flush_cb你的DMA传输完成中断里有没有调用lv_disp_flush_ready如果SPI发送用轮询等待等待时间内整个UI都被卡住。看是否开了双缓冲。LVGL推荐用两个buffer配合DMA乒乓一个buffer在送显时另一个可以继续渲染。没有双缓冲帧率上限很低。最后才查chart参数。point count从120降到60或者把div line count从12×8改小减少重绘面积。注意网格线也属于chart背景绘制密密麻麻的网格会额外吃掉刷新时间。我自己的记录是同一块ILI9341屏从轮询SPI改成DMA双缓冲动态波形的流畅度提升了至少两倍。很多LVGL卡其实是底层发送没做好这个结论我验证过很多次。5.3 内存与序列开销lv_chart序列的每个点本质是一个坐标数据LVGL 8.3里坐标类型受LV_COORD_BITS影响9.x以后统一为int32_t。point count为60时单序列内存占用可以忽略不计。真正吃内存的是framebufferRGB565全屏320×240约150KB双缓冲就要300KB这已经超过很多F103的RAM总量。所以当你看到lv_mem分配失败或者硬件错误先不要怀疑是chart导致的去数一数屏幕buffer占了多少。如果RAM实在紧张几个方向可以考虑降低分辨率或用单色屏关闭不必要的配色深度减少LVGL全局缓冲精简界面同时显示的控件数量。图表控件本身不是内存大头别为了省内存去疯狂缩减point count那样效果会明显变差不值。5.4 LVGL 8.x到9.x的API差异速查很多教程还在用8.x但新工程默认拉到的可能已经是9.x。这两个版本在chart这个demo上的API变化不大但确实有几处会影响编译差异点LVGL 8.3LVGL 9.x坐标类型lv_coord_t16或32位int32_t刷新周期宏LV_DISP_DEF_REFR_PERIODLV_DEF_REFR_PERIOD动态内存LV_MEM_SIZE统一配置默认走系统堆可配置create类API返回lv_obj_t*相同chart常用APIlv_chart_create/set_range/add_series等基本相同如果你从8.x升级到9.x遇到chart编译报错先看是不是类型宽度问题再看颜色宏和style设置。这类兼容问题在官方迁移文档里写得很详细直接对着文档改就行不用大动代码。最后聊一点个人经验。这个心电图效果是我给手头仪表项目做调试页时偶然玩出来的。当时只是想验证lv_chart能不能接受高频数据看到绿色波形在屏幕上滚动起来的那一刻整个项目的调试体验都提升了——以前我只能看一串串十六进制数现在能直观看到信号形态很多异常一眼就能认出来。以后你要是接到带屏幕产品的需求比如在嵌入式设备上显示实时波形这个demo可以直接当模板把ecg_wave数组换成ADC采样的缓冲区把定时器改成数据到达就更新UI逻辑基本不用动。用LVGL做动态图表坑主要集中在那三件事更新模式、缓冲内存、底层刷新。这三件事摸透了图表控件会成为你手里很有价值的一张牌。
返回列表