首页 / 资讯中心 / 文章详情

RT-Thread W60X 上移植 LVGL:缓冲、刷新与内存避坑指南

RT-Thread W60X 上移植 LVGL:缓冲、刷新与内存避坑指南 ★ FEATURED ARTICLE
简介RT-Thread_W60X_SDK.7z 面向基于 Realtek W60X 系列 Wi-Fi 微控制器进行物联网开发的嵌入式工程师与进阶学习者提供以 RT-Thread 实时操作系统为核心的软件开发套件涵盖 BSP 初始化、驱动、中间件、RTOS 组件、示例代码与开发工具并集成 LVGL 图形库便于在资源受限设备上构建稳定应用和图形界面。压缩包体积约 228.22MB页面显示文件总数为 0、类型明细暂无有效数据具体内容需以解压目录为准。已有 1321 人学习下载说明其在 W60X 平台开发中具备一定参考价值。读者可从中梳理从底层驱动、网络通信、任务调度到 GUI 移植的开发脉络借助示例与文档熟悉工程组织方式减少环境搭建和基础排错耗时也可先通过目录结构判断组件完整度再结合示例验证 Wi-Fi 连接、外设驱动与界面移植等关键环节作为方案评估与功能扩展的起点。1. 解压前先明确三件事RT-Thread_W60X_SDK 里到底有什么为什么一上手就容易遇到问题RT-Thread_W60X_SDK.7z属于“一个压缩包对应一套完整软件栈”的那种资源并非单个 Demo。解压后你会看到 RT-Thread 内核与 BSP、W60X 片内外设库以及一层把 RTT LVGL 接起来的移植代码。目标硬件是 W60X 系列 WiFi SoCCortex-M3 内核片上 Flash 在 1MB 量级、SRAM 在 200KB 上下具体以对应型号手册为准。这类资源的价值在于它直接跳过了“内核怎么起飞”这一段上电、时钟、串口、SPI 屏幕和 lvgl 的显示驱动接口都已经有架子你需要决定的是裁剪哪些、缓冲区留多大、刷新走阻塞还是 DMA。它适合已经能点亮一块 SPI 屏、想把 GUI 搬到 MCU 上、又不愿意从零写 BSP 的人如果你只想跑个hello world这份包对你是过重的。2. 从压缩包到能编译目录定位、scons 构建与三处必改配置第一次打开这个包最容易出错的地方在于目录层级还没认清就直接用 IDE 打开工程代码本身倒在其次。包里通常有三块内容rt-thread/内核 bsp/w60x板级支持、厂家提供的外设库GPIO、SPI、UART、WiFi 协议栈的底层实现、以及applications/或examples/下的示例入口。真正需要你改的永远只有后两块和 BSP 里的rtconfig.h内核目录建议保持原样否则以后想升级内核时diff 出来的改动会让你很头疼。2.1 先认清哪几个文件夹属于“可改区”我的习惯是解压后先不打开任何工程文件先在命令行把目录树看一遍重点确认三件事BSP 目录在哪、rtconfig.h在哪、链接脚本.ld/.sct/.icf在哪。这三个文件决定了后面所有的内存问题。命令很简单但能省掉半小时的盲目点击# 只看两层目录结构避免被内核里的上千个文件刷屏 find . -maxdepth 2 -type d | sort # 定位关键文件配置头、链接脚本、启动文件 find . -name rtconfig.h -o -name *.ld -o -name *.sct -o -name startup_*.s第一条命令给出整体骨架第二条把三类关键文件一次性列全。参数上-maxdepth 2是刻意的再深一层就是内核源码看了没有意义。如果你在输出里发现rtconfig.h同时出现在bsp/和某个template/下认准跟你的板子同名的那个另一个是模板改了不起作用——这是新手最常踩的第一个坑。2.2 工具链就位从 env 到第一次 scons这个包的标准构建方式是 scons。你需要 RT-Thread 的 env 工具或者自己把 arm-none-eabi 工具链加进 PATH再装 Python 版 scons。确认工具链在不在一条命令就够不要凭记忆判断arm-none-eabi-gcc -v # 能看到版本号说明 PATH 没问题 scons --version # 没装的话用 pip install scons 补接下来进 BSP 目录构建。第一次构建我一般不加-j让报错顺序保持可读跑通之后再并行cd rt-thread/bsp/w60x # 第一次单线程输出干净方便定位第一个错误 scons # 之后日常构建并行机器几核就写几 scons -j8 # 需要 MDK/IAR 工程时用同一份配置反向生成 scons --targetmdk5这里有个容易忽略的点scons --targetmdk5生成的工程和你手改的.uvprojx是两回事重新生成会覆盖掉你在 IDE 里加的源文件和宏定义。所以要么全程用 scons 命令行构建要么全程用 IDE别混着来。混用的典型症状是“命令行编译过了IDE 里报一堆 undefined reference”——因为 IDE 工程里还留着上次生成的旧文件列表。2.3 menuconfig 里绕不开的几项menuconfig是这套体系的核心但第一次进去容易被几百个选项淹没。跟 LVGL 相关的其实就几项堆、设备框架、SPI、tick 频率。我一般直接改rtconfig.h比在菜单里翻找快也更容易进版本管理/* rtconfig.h —— 与 LVGL 相关的必改项 */ #define RT_TICK_PER_SECOND 1000 /* 必须是 1000LVGL 的毫秒节拍依赖它 */ #define RT_USING_HEAP /* 开堆LVGL 内存池要挂到 rt_malloc 上 */ #define RT_USING_DEVICE /* 设备框架SPI 设备模型的前提 */ #define RT_USING_SPI #define BSP_USING_SPI0 /* 屏幕挂在哪条 SPI 上就开哪条 */ #define RT_USING_PIN /* 复位脚、背光脚、DMA 完成脚都要用 */ #define RT_USING_FINSH /* 保留 msh排查阶段全靠它 */ #define FINSH_USING_MSHRT_TICK_PER_SECOND这一行值得单独说。很多 BSP 模板里默认是 100看起来只是“调度粒度粗一点”实际上会让 LVGL 的动画、lv_timer_handler的周期全部慢十倍表现出来就是“界面像卡住了一样”。这个坑我在不同板子上见过不止一次属于改一行值能省一下午的类型。RT_USING_HEAP不开的话后面LV_MEM_CUSTOM那套配置直接编译不过RT_USING_FINSH则是为了在串口里敲list_thread、free看运行时状态排查阶段比任何调试器都快。2.4 烧进去、看到 banner 才算数编译产物一般是.elf加一个转换出来的.bin。W60X 常见的下载方式是通过串口让芯片进下载模式拉某个 BOOT 脚再上电或者按住板上的下载键打开厂家提供的串口下载工具选 bin 文件波特率 115200 起步、能跑 921600 就跑快的。验证标准只有一个串口终端里出现 RT-Thread 的版本 banner 和msh /提示符。出现了说明内核、时钟、串口、堆都是通的可以往上叠 LVGL没出现别急着往下走先敲几下回车看有没有回显有回显没 banner 是启动流程卡住没回显是串口参数或引脚不对——这两种故障的方向完全不同先把它们分开。3. LVGL 显示先算内存账再定缓冲方案与刷新通路LVGL 在 MCU 上跑不动十次有九次不是 CPU 慢是缓冲区开错了。W60X 这颗片子的 SRAM 在 200KB 上下而一个 240×240 的 RGB565 整屏帧缓冲是 240×240×2 115200 字节也就是 112KB 出头。整屏双缓冲要 225KB——直接把 SRAM 吃干净内核、协议栈和栈全都没地方待。所以这个平台上做 LVGL第一件事是算账第二件事才是写代码。3.1 三种缓冲方案的取舍方案缓冲大小240×240 RGB565适用场景代价整屏单缓冲约 112KB几乎不占别的 RAM 的纯显示板刷新时无法同时渲染撕裂明显整屏双缓冲约 225KBSRAM 富余、要跑动画这个平台上基本放不下部分缓冲N 行240×N×2 字节绝大多数场景需要多次 flushDMA 要够快我一般从“20 行”起步240×20×2 9600 字节不到 10KB留出的余量足够内核、WiFi 协议栈和几个线程栈。LVGL 官方对部分缓冲的建议是“至少屏幕的 1/10”240 行的 1/10 是 24 行所以 20 行是略保守的下限实测能跑就先跑卡了再往上加。/* lv_port_disp.c —— 部分缓冲初始化 */ #define MY_DISP_HOR_RES 240 #define MY_DISP_VER_RES 240 #define BUF_LINES 20 /* 缓冲行数先 20卡再加 */ static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_1[MY_DISP_HOR_RES * BUF_LINES]; void lv_port_disp_init(void) { /* 第二块传 NULL单缓冲模式LVGL 会复用同一块内存 */ lv_disp_draw_buf_init(draw_buf, buf_1, NULL, MY_DISP_HOR_RES * BUF_LINES); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res MY_DISP_HOR_RES; disp_drv.ver_res MY_DISP_VER_RES; disp_drv.flush_cb disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }lv_disp_draw_buf_init的第四个参数是“像素个数”单位为像素而不是字节这一点错了不会报错只会让你的界面上半部分正常、下半部分串位。传NULL作为第二块缓冲即进入单缓冲模式LVGL 会在每次 flush 完成后再复用同一块内存——所以 flush 必须是“完成后才通知”的语义下面这一节就是讲这个。3.2 flush 回调阻塞版和 DMA 版差在哪flush_cb是整个移植里唯一真正需要写的函数。它的行为决定了画面会不会撕裂。阻塞版最直观把窗口开好把像素推出去等 SPI 传完再告诉 LVGL “我用完了”。/* 阻塞版简单但 CPU 全耗在等 SPI 上 */ static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint16_t w area-x2 - area-x1 1; uint16_t h area-y2 - area-y1 1; lcd_set_window(area-x1, area-y1, area-x2, area-y2); /* 只设窗口不发数据 */ lcd_write_pixels_blocking((const uint8_t *)color_p, (uint32_t)w * h * 2); lv_disp_flush_ready(drv); /* 像素已经落地这块缓冲可以还给 LVGL */ }DMA 版把“等”这一步砍掉但顺序不能错lv_disp_flush_ready必须在 DMA 传输完成之后调用不能启动完就立刻调用。我见过把 ready 写在lcd_dma_start后面的代码表现是花屏、局部重影——因为 LVGL 以为缓冲空出来了立刻往里写下一帧而 DMA 还在读上一帧的数据。/* DMA 版启动即返回在传输完成中断里通知 LVGL */ static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint16_t w area-x2 - area-x1 1; uint16_t h area-y2 - area-y1 1; lcd_set_window(area-x1, area-y1, area-x2, area-y2); lcd_dma_start((const uint8_t *)color_p, (uint32_t)w * h * 2); /* 这里绝对不能调 lv_disp_flush_ready留给中断 */ } /* SPI/DMA 传输完成中断里调用 */ void lcd_dma_done_isr(void) { lv_disp_flush_ready(disp_drv); /* 中断上下文调用函数本身是安全的 */ }参数上只有一个地方需要盯w * h * 2里的 2 是 RGB565 每像素两字节如果你把LV_COLOR_DEPTH改成 32这里必须跟着变成 4否则每次只发一半数据画面会是“半个屏幕正常半个屏幕花”。3.3 心跳tick 和 handler 是两件事别混LVGL 需要两个“心跳”一个是时间基准lv_tick_inc()告诉它过去了多少毫秒一个是lv_timer_handler()让它去跑动画、重绘、控件逻辑。这两件事性质完全不同前者要在中断或高精度定时器里做后者必须在线程里做。最干净的做法是用LV_TICK_CUSTOM直接读 RT-Thread 的系统滴答省掉一个定时器/* lv_conf.h —— 让 LVGL 直接读 RT-Thread 的 tick不额外占用硬件定时器 */ #define LV_TICK_CUSTOM 1 #define LV_TICK_CUSTOM_INCLUDE rtthread.h #define LV_TICK_CUSTOM_SYS_TIME_EXPR \ ((uint32_t)(rt_tick_get() * 1000ULL / RT_TICK_PER_SECOND))这里用1000ULL而不是1000是为了避免rt_tick_get()在高频率下乘出 32 位溢出的中途结果。前提是RT_TICK_PER_SECOND已经设成 1000否则精度只有 10ms动画会一顿一顿的。handler 放线程里周期 5ms 是个经验值太长了响应迟钝太短了线程空转吃 CPU。栈给 4KB 起步LVGL 的函数嵌套比看上去深。static void lvgl_thread_entry(void *param) { lv_init(); lv_port_disp_init(); lv_port_indev_init(); /* 有按键或触摸时才有这一步 */ while (1) { lv_timer_handler(); /* 处理重绘、动画、输入事件 */ rt_thread_mdelay(5); } } /* 在初始化阶段创建优先级低于显示刷新相关的线程即可 */ rt_thread_t tid rt_thread_create(lvgl, lvgl_thread_entry, RT_NULL, 4096, 15, 10); rt_thread_startup(tid);rt_thread_mdelay(5)之后线程主动让出 CPU比用rt_thread_delay(1)空转友好得多。优先级给 15 是相对保守的选择别把它设得比 WiFi 协议栈线程还高否则网络收包会被 UI 刷新挤掉表现出来是“界面很流畅但连不上网”。3.4 输入设备按键与触摸的接入点没有输入设备的 GUI 只能是演示。W60X 这种平台常见两种几个 GPIO 按键或者一颗 I2C 电容触摸。按键用lv_indev_drv_t的read_cb返回LV_INDEV_STATE_PRESSED/RELEASED配合lv_group_t做焦点切换——这一套在 LVGL 里是标准流程写法固定。static void key_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { static uint32_t last_key 0; /* 你的按键扫描函数返回 0 表示无按键 */ uint32_t act_key board_key_scan(); if (act_key ! 0) { >/* lv_conf.h —— 内存池改用 rt_malloc/rt_free */ #define LV_MEM_CUSTOM 1 #define LV_MEM_CUSTOM_INCLUDE rtthread.h #define LV_MEM_CUSTOM_ALLOC rt_malloc #define LV_MEM_CUSTOM_FREE rt_free #define LV_MEM_CUSTOM_REALLOC rt_realloc这么改的好处是可以用 msh 里的free命令一眼看到剩余堆排查时不用猜。代价是 LVGL 的对象和你的应用共享同一个堆如果应用里有频繁申请释放的代码会加速碎片化——所以开了这个之后界面尽量在初始化阶段一次性建完别在运行中反复创建销毁控件。不开自定义分配器的话LV_MEM_SIZE建议不低于 32KB。一个带几十个控件、几张图片的界面16KB 通常不够症状是创建控件时lv_obj_create返回 NULL然后你对着空屏幕发呆。4.2 字节序白屏和花屏的根因LV_COLOR_DEPTH 16配 SPI 屏必开LV_COLOR_16_SWAP。原因是 LVGL 内部按小端存 RGB565而多数 SPI 屏控制器期望高字节先到。不开的话红蓝互换、颜色整体发绿、或者干脆一片灰白。/* lv_conf.h —— RGB565 字节序交换SPI 屏基本都要开 */ #define LV_COLOR_DEPTH 16 #define LV_COLOR_16_SWAP 1判断方法很直接拿一个纯红色矩形去测。显示成蓝色就是字节序反了把LV_COLOR_16_SWAP从 0 改 1或反之。如果显示成一片白那不是字节序问题是窗口设置错了——ST7789 这类控制器有 0/80 两种列偏移lcd_set_window里少加一个偏移量整幅画面就跑到可视区外面去了。4.3 编译选项与组件裁剪LVGL 在-O0下会慢到让人怀疑人生尤其是带透明、圆角、阴影的控件。这个平台上我一般用-Os代码体积和速度平衡得比较好纯性能敏感的部分可以单独用-O2。裁剪比优化更有效。默认配置里打开了一堆用不上的控件和字体每一个都占 Flash 和 RAM/* lv_conf.h —— 只留用得上的Flash 和 RAM 都能省一大截 */ #define LV_USE_CHART 0 #define LV_USE_TABLE 0 #define LV_USE_CALENDAR 0 #define LV_USE_KEYBOARD 0 #define LV_FONT_MONTSERRAT_14 1 #define LV_FONT_MONTSERRAT_20 0 /* 不用的字号全关 */ #define LV_FONT_DEFAULT lv_font_montserrat_14字体是大头。一个 20px 的 Montserrat 全字符集能占几十 KB Flash关掉不用的字号往往比优化代码省得多。片上 Flash 只有 1MB 量级LVGL 加上你的应用很容易吃掉三分之一留点余量给后面加功能。4.4 用数据说话别靠手感调到这里剩下的优化必须靠测量。我常用的三个指标很土但很有效单次 flush 的耗时、lv_timer_handler()的单次执行时间、以及堆和栈的水位。第一个用 GPIO 翻转加逻辑分析仪测进入 flush 拉高lv_disp_flush_ready之前拉低。第二个在 handler 前后各读一次rt_tick_get()超过 10ms 就打印出来。第三个直接用 mshmsh / free # 看堆的剩余和最大空闲块 msh / list_thread # 看每个线程的栈使用峰值lvgl 线程别超过 70%free输出里的“最大空闲块”比“剩余总量”更重要——总量还剩 30KB 但最大块只有 2KB说明碎片严重这时候再加控件必然失败。list_thread的栈使用率长期在 80% 以上就得加栈别等它溢出后出现难以定位的异常。5. 避坑与排查五条从白屏到卡死的现场记录这些坑的共同点是编译通过、烧录成功、启动正常问题只在特定条件下冒出来。按“现象 → 原因 → 解决”记下来下次遇到能少绕几圈。5.1 显示类白屏、花屏、撕裂现象一烧完程序背光亮着屏幕全白串口 banner 正常。原因基本是显示窗口没设对。W60X 接的这类 SPI 屏控制器普遍有列/行偏移常见是 0 或 80lcd_set_window里如果不加偏移写入的像素全落在可视区之外。另一种可能是复位时序不对控制器压根没初始化成功。 解决先用纯色填充整个 GRAM 验证屏和时序是否正常再上 LVGL。如果纯色能显示、LVGL 白屏问题就在窗口或缓冲纯色也白问题在 SPI 初始化或复位脚。现象二颜色整体偏红变蓝、或者发绿。原因是 RGB565 的字节序。LVGL 内部小端存储SPI 控制器按大端接收。 解决切换LV_COLOR_16_SWAP用纯红矩形验证。注意这个宏改完必须全量重编只改头文件有时候增量编译不生效表现出来是“改了没反应”。现象三画面刷新时有明显横向撕裂快速滑动列表尤其明显。原因是单缓冲模式下DMA 还在读缓冲CPU 已经开始写下一帧或者lv_disp_flush_ready调用时机早于传输完成。 解决检查 ready 是不是写在了 DMA 启动之后立刻执行。如果是阻塞式 SPI确认lcd_write_pixels_blocking真的等到了传输结束标志位而不是发完就返回。5.2 运行类死机、内存、卡顿现象四跑几分钟到几十分钟后死机串口打印 assertion failed 或者直接无输出。两个高概率原因LVGL 内存池耗尽导致lv_obj_create返回 NULL 后继续解引用或者 lvgl 线程栈溢出踩到相邻线程的数据。 解决先看free的最大空闲块再看list_thread的栈峰值。栈给到 4KB 起步、内存池按 32KB 起配然后把界面创建集中到初始化阶段运行期不再动态增删控件。开LV_USE_LOG和LV_USE_ASSERT能让这类问题在第一次发生时就打印出来而不是等到几十分钟后。现象五界面能显示但操作响应慢半拍动画一顿一顿。排查顺序别搞反先看RT_TICK_PER_SECOND是不是 100再看lv_timer_handler的调用周期是不是被拉长到了几十毫秒最后才看 flush 耗时。我遇到过最隐蔽的一次是 tick 频率设对了但 handler 所在线程优先级太低被 WiFi 收包线程长期抢占表现出来就是“UI 一卡一卡的”——把 lvgl 线程优先级提两档就解决了代价是网络吞吐略降。提示这五类问题的排查顺序建议固定为“屏 → 色 → 时序 → 内存 → 调度”。从下往上查会让你在错误的方向上耗掉大量时间。6. 从能跑到能用三个量化指标和一套自检习惯把界面点亮只是及格线。真正决定这套 RTT LVGL 组合能不能上产品的是三组能测出来的数单帧 flush 时间、lv_timer_handler单次耗时、以及堆栈水位。我的做法是把它们做成开机自检的一部分而不是等出问题再去量。单帧 flush 时间用 GPIO 加逻辑分析仪测最准进入回调拉高一个引脚lv_disp_flush_ready前拉低抓几十个周期取平均。240×20 行的一块DMA 版通常在几百微秒量级如果测出来是几毫秒说明 SPI 时钟配低了或者还在走阻塞发送。lv_timer_handler的耗时用rt_tick_get()前后夹一下超过 10ms 就rt_kprintf打一行出来配合界面操作能立刻定位到是哪个控件在拖后腿——圆角、阴影、半透明这三样在 M3 上都是性能黑洞。第三个指标最容易被忽略把free和list_thread的输出做成一条 msh 命令运行一段时间后敲一次把“最大空闲块”和“lvgl 线程栈峰值”记下来。这两个数一旦开始单向恶化说明有泄漏或者碎片在累积这时候改比崩溃之后再改成本低得多。再往上走一步是让刷新更“值钱”。LVGL 的重绘是按脏区来的界面上如果有一块区域在频繁刷新比如进度条、时钟其他区域就会被反复重绘。把这类高频更新的控件单独放进一个容器用lv_obj_invalidate精确控制失效范围比调 SPI 时钟的收益大得多。图片资源也一样转成 C 数组后用lv_img_dsc_t指向 Flash 常量区别让它落到 RAM 里——一张 100×100 的 RGB565 图就是 20KB放两张这个平台的 RAM 就紧张了。血泪经验是每次改动lv_conf.h之后我都会强制走一遍完整的清理重编而不是增量编译。这个坑我栽过不止一次——改了LV_COLOR_16_SWAP只编了一个文件烧进去颜色还是反的然后花二十分钟怀疑硬件。清理重编多花的三十秒比在错误方向上排查半小时划算太多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站