试用 LVGL Pro,一套完整的工具包,助您高效构建、测试、分享和交付 UI!
LVGL
案例展示

Meta 的 Muse Gadgets 如何用 LVGL 绘制界面

Meta 开源的 Muse Gadget SDK 使用 LVGL 9.5 绘制屏幕界面。本文介绍它如何在 23 款开发板上共用一套 UI、节省内存,并在桌面上运行。

乔瓦尼·鲍尔迈斯特乔瓦尼·鲍尔迈斯特18 分钟阅读
Meta 的 Muse Gadgets 如何用 LVGL 绘制界面

2026 年 10 月 2 日,Meta 发布了 Muse Gadget SDK。借助它,你可以把一块现成的 ESP32 开发板或树莓派变成 Muse 的设备。Muse 是 Meta 的个人 AI 智能体:按住按键说话,回答会以字幕形式显示在屏幕上。

这个 SDK 以 Apache 2.0 许可证开源。打开 ESP32 固件就会发现,屏幕 UI 由 LVGL 9.5 绘制。本文介绍固件如何使用 LVGL,以及代码中四个值得其他 LVGL 项目借鉴的技巧。

facebookincubator/muse-gadget-sdk

Open source SDK to build Muse gadgets

C2.7k624

Muse 是什么#

Muse 是 Meta Superintelligence Labs 推出的个人 AI 智能体。Meta 于 2026 年 9 月 8 日在美国推出 Muse,支持 iOS、Android 和 muse.ai,底层模型是 Meta 自研的 Muse Spark。Muse 不只是聊天应用,而是一个智能体:它可以发送邮件、预订行程、填写表单,关闭应用后仍会继续工作。在发送或购买之前,它会先征得你的同意。

这次发布规模很大。据 Seoul Economic Daily 引用的 Bloomberg 数据,Muse 在美国同时登上 App Store 和 Google Play 免费榜第一,并在 13 天内在北美突破 250 万次下载。

AI 智能体为什么需要硬件#

Muse 运行在云端和手机上,Muse Gadgets 则把它带进了物理世界。Meta Superintelligence Labs 产品负责人 Nat Friedman 称这是一个“副业项目”,目标是 “basically enabling hardware AI monitors and Physical AI with Muse”。一个 gadget 可以是带屏幕和通话按键的桌面伴侣、电子墨水状态屏,也可以是替 Muse 执行命令的树莓派。

Meta 自己只做了一款设备:Muse Home Link。它是一个 USB-C 适配器,采用 ESP32-C5,配备 8 MB 闪存和 8 MB PSRAM。Meta 生产了 5,000 台,免费送给美国的 Muse 订阅用户。其余的交给社区:买一块支持的开发板,烧录固件,再与 Muse 应用配对即可。截至 10 月 26 日,也就是发布 24 天后,代码仓库已获得超过 2,400 个 star 和 530 个 fork。

这对嵌入式 UI 意味着什么#

模型并不运行在 gadget 上。设备只负责录音、显示文字和动画表情,并通过 Wi-Fi 或蓝牙与云端通信。因此硬件可以是普通的 ESP32 开发板:大多数受支持的开发板都是 Waveshare、M5Stack、Seeed 和 Espressif 的现成 ESP32-S3 开发板。在这样的设备上,屏幕几乎就是用户看到的全部。UI 既要在圆形 AMOLED 和小尺寸 LCD 上好看,又要与 Wi-Fi 和蓝牙共用芯片的内部 RAM。

这正是 LVGL 的设计目标。由于 SDK 是开源的,你可以逐行看到一支大型团队如何解决这些问题。


LVGL 在 SDK 中的位置#

SDK 分为两部分。ESP32 Device SDK 是基于 ESP-IDF 6.0.1 构建的 C 固件,Linux Device SDK 是运行在树莓派或其他 Linux 设备上的 Python 服务。

LVGL 位于 ESP32 固件中。组件清单为 UI 固定了两个依赖:

esp32/components/muse/idf_component.yml
lvgl/lvgl:
  version: "9.5.0"
espressif/esp_lvgl_adapter:
  version: "0.6.4"
yaml

Kconfig 把它称为“完整 UI”:动画头像、按键通话、设置和电池状态。它运行在带彩色屏幕的开发板上。并非所有 gadget 都使用它:

  • 电子墨水屏开发板,例如 Seeed reTerminal E1002,使用自己的面板驱动,不通过 LVGL 绘制状态画面。
  • 没有屏幕的设备,例如 Muse Home Link 适配器,选择“None”,不构建 UI。
  • Linux SDK 没有屏幕界面。

所以准确的说法是:只要 Muse gadget 带有彩色屏幕,你在屏幕上看到的内容就由 LVGL 绘制。


一套 UI,23 款开发板#

固件列出了 23 款可以运行完整 UI 的开发板,涵盖五个 ESP32 系列:ESP32、ESP32-S3、ESP32-C3、ESP32-C6,以及实验性的 ESP32-S31。屏幕从 128x128 的 LCD 到 800x480 的面板不等,中间还有圆形 AMOLED、方形 LCD 和竖屏 LCD。

它们共用一个 UI 文件 muse_ui.c,约 1,800 行,并不为每块开发板单独编写布局文件。程序在启动时读取屏幕尺寸,然后从两种布局中选择一种:

  • 完整布局:适用于约 466 px 的圆形屏幕及类似屏幕,包含进度环、电平表和设置页。
  • 紧凑布局:适用于约 128 px 的小屏幕,头像占满屏幕,文字叠加在上面。

UI 使用标准的 LVGL 控件:用 lv_tileview 在头像和设置之间滑动切换,用 arc 显示进度环,用 label 显示字幕,用 flex 布局排列设置项。每个开发板文件只负责连接面板和触摸控制器。大多数开发板文件借助 Espressif 的 esp_lvgl_adapter 完成这项工作,所以新增一块开发板,主要就是一段显示初始化序列和一张引脚表。


四个值得借鉴的技巧#

代码中有详细的注释,不仅说明做了什么,还解释了为什么这样做。下面四个技巧适用于大多数 ESP32 上的 LVGL 项目,其中不少也适用于任何 MCU。

1. 把 LVGL 的堆放到 PSRAM#

LVGL 允许替换自身的内存分配器。固件设置了 CONFIG_LV_USE_CUSTOM_MALLOC=y,并在 muse_lv_mem.c 中提供了自己的分配器。文件开头的注释说明了原因:

esp32/components/muse/muse_lv_mem.c
/*
 * LVGL allocator: widgets and styles live in PSRAM. With the C library malloc,
 * ESP-IDF puts every allocation under 16 KB in internal RAM, and a few hundred
 * settings widgets would starve Wi-Fi and the display's DMA bounce buffers.
 */
#define LV_CAPS (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)
 
void *lv_malloc_core(size_t size)
{
    void *p = heap_caps_malloc(size, LV_CAPS);
    return p ? p : malloc(size);
}
 
void lv_free_core(void *p)
{
    heap_caps_free(p);
}
c

注释的意思是:控件和样式都放在 PSRAM 中。如果使用 C 库的 malloc,ESP-IDF 会把所有小于 16 KB 的分配放进内部 RAM,几百个设置控件就会挤占 Wi-Fi 和显示 DMA 缓冲区所需的内存。

每个控件都很小,所以 ESP-IDF 默认把它们放在内部 RAM 中。几百个控件累积起来就很可观,而 Wi-Fi 和 DMA 缓冲区也必须位于内部 RAM。这个分配器优先把 LVGL 的分配放到 PSRAM,只有 PSRAM 空间不足时才使用内部 RAM。列表中有些开发板没有 PSRAM,例如 M5Stack Cardputer ADV,在这些开发板上,回退到 malloc 让同一份代码依然可用。

小贴士

分配器文件只包含 LVGL 调用的符号。链接器可能会丢弃这样的文件,转而使用 LVGL 的默认分配器。SDK 在组件的 CMakeLists.txt 中使用 WHOLE_ARCHIVE 来避免这种情况。如果你的自定义分配器似乎没有生效,请先检查这一点。

2. 逐条带解码头像#

头像是一个 64x64 的像素画网格。在 466 px 的圆形屏幕上,它有几百像素宽。按 16 位色深计算,这个尺寸的一整帧要占用数百 KB 内存。LVGL 本身也能缩放图像,但软件缩放每一帧都要消耗 CPU 时间。

固件两种方式都没有用。它注册了一个自定义图像解码器,头像以一张图像的形式交给 LVGL,像素由解码器按需生成:

esp32/components/muse/muse_ui.c
/*
 * Muse reaches LVGL as an image whose pixels are made on demand: a decoder
 * scales the 64 px grid up one strip of rows at a time, so the full-size frame
 * never sits in RAM and LVGL never runs its (slow) image transform.
 *
 * LVGL splits each refresh into a tile per draw unit and draws them at once,
 * each with its own open decoder, so each one takes its own strip: with one
 * shared strip, a tile blended rows the other had just scaled into it.
 */
#define STRIP_ROWS 16
#define STRIPS LV_DRAW_SW_DRAW_UNIT_CNT
c

注释的意思是:解码器每次把 64 px 网格中的一条行带放大,所以完整尺寸的帧从不驻留在 RAM 中,LVGL 也不需要运行较慢的图像变换。LVGL 会为每个绘制单元分配一块区域并同时绘制,每个单元各自打开解码器,因此每个单元都要有自己的行带。

解码器使用 LVGL 的 get_area 回调函数。open 回调函数并不解码图像,只是给调用方分配一个行带缓冲区。随后 LVGL 反复调用 get_area,每次调用都把网格的下 16 行放大到这个行带中:

esp32/components/muse/muse_ui.c
static lv_result_t muse_dec_get_area(lv_image_decoder_t *dec, lv_image_decoder_dsc_t *dsc,
                                     const lv_area_t *full, lv_area_t *area)
{
    int32_t y1 = area->y1 == LV_COORD_MIN ? full->y1 : area->y2 + 1;
    if (y1 > full->y2) return LV_RESULT_INVALID;
    int32_t y2 = LV_MIN(y1 + STRIP_ROWS - 1, full->y2);
    int32_t w = lv_area_get_width(full);
    lv_draw_buf_t *buf = lv_draw_buf_reshape(dsc->user_data, LV_COLOR_FORMAT_RGB565, w, y2 - y1 + 1, LV_STRIDE_AUTO);
    muse_pixel_scale((uint16_t *)buf->data, buf->header.stride / sizeof(uint16_t), full->x1, full->x2, y1, y2);
    area->x1 = full->x1; area->x2 = full->x2;
    area->y1 = y1;       area->y2 = y2;
    dsc->decoded = buf;
    return LV_RESULT_OK;
}
c

这是原始代码的精简版本。内存开销从一整帧变成了两个 16 行的行带。

上面注释的后半部分很重要。ESP32-S3 开发板把 LV_DRAW_SW_DRAW_UNIT_CNT 设为 2,LVGL 的软件渲染器因此会并行绘制屏幕的两个部分。每个绘制单元都会单独打开解码器。如果共用一个行带,一个单元混合的可能是另一个单元刚刚写入的行。解决方法是每个绘制单元各用一个行带,所以 STRIPS 等于 LV_DRAW_SW_DRAW_UNIT_CNT。采用单核芯片的开发板(例如 ESP32-C6)只使用一个绘制单元和一个行带。

警告

如果你编写的解码器使用共享的临时缓冲区,请在多个绘制单元下测试它。在单个绘制单元下工作正常的解码器,启用第二个绘制单元后可能会出错。

私有头文件

解码器回调函数通过 LVGL 的公开 API 注册。但回调函数会读写 lv_image_decoder_dsc_t 的字段,例如 user_data 和 decoded,而这些字段声明在 lv_image_decoder_private.h 中。私有头文件可能在 LVGL 版本之间发生变化,SDK 因此把 LVGL 精确固定在 9.5.0。如果你借鉴这个解码器,也请固定 LVGL 版本,并在升级时检查解码器。

3. 只重绘变化的单元格#

LVGL 只重绘被标记为无效的区域。如果每一帧动画都让整个头像无效,LVGL 就会重绘整个头像。而大多数帧只有少数单元格发生变化,例如眨眼时的眼睛。

因此,固件保存了上一帧每个单元格的颜色。每一帧都找出每一行中变化的单元格,为每行生成一个矩形,然后合并相邻的矩形:

esp32/components/muse/muse_ui.c
#define DIRTY_RECTS_MAX 8       /* with the meter and labels, well inside LVGL's 32 areas */
#define DIRTY_MERGE_PX 2048     /* pixels that cost about one walk of the widget tree */
c

合并规则是一种成本权衡。每多一个无效区域,LVGL 就要多遍历一次控件树;每合并一次,LVGL 就要多画一些没有变化的像素。当多画的像素成本低于一次控件树遍历时,固件就合并两个矩形,并让区域总数远低于 LVGL 默认的 32 个上限。相关函数是 invalidate_muse(),它对剩下的每个矩形调用 lv_obj_invalidate_area()。

4. 断言失败时重启设备#

默认情况下,LVGL 断言失败后会进入死循环。对于摆在用户家里的设备,这意味着屏幕卡死,而且没有日志。固件在组件的 CMakeLists.txt 中改变了这一行为:

esp32/components/muse/CMakeLists.txt
# LVGL spins forever on a failed assert unless LV_ASSERT_HANDLER is set,
# which its Kconfig can't do. Abort, so the panic handler prints a backtrace
# and reboots. PUBLIC reaches LVGL and every file that includes lvgl.h.
idf_component_get_property(lvgl_lib lvgl__lvgl COMPONENT_LIB)
target_compile_definitions(${lvgl_lib} PUBLIC
    "LV_ASSERT_HANDLER_INCLUDE=<stdlib.h>"
    "LV_ASSERT_HANDLER=abort()\;")
cmake

abort() 会触发 ESP-IDF 的 panic 处理程序,打印回溯信息并重启芯片。改动很小,但对任何使用 ESP-IDF 的 LVGL 项目都有用。LVGL 的 Kconfig 中没有 LV_ASSERT_HANDLER 选项,默认值是 while(1);,所以必须通过编译定义来设置。


在桌面上运行 Muse 界面#

不需要开发板也能看到 UI。SDK 包含一个适用于 Linux 和 macOS 的 SDL 模拟器。它编译正式版的 muse_ui.c 和头像渲染器,用小型主机适配层替代 ESP-IDF、FreeRTOS、Wi-Fi、蓝牙和电源服务。窗口大小为 412x412,与 Seeed SenseCAP Watcher 的屏幕相同。

你需要 CMake 3.24 或更高版本,以及支持 C11 的编译器,不需要 ESP-IDF。在仓库根目录下运行:

cmake -S esp32/simulator -B esp32/simulator/build -DCMAKE_BUILD_TYPE=Debug
cmake --build esp32/simulator/build --parallel
./esp32/simulator/build/muse_simulator
bash

构建过程会下载 LVGL 9.5.0,与固件版本一致。如果系统中有兼容的 SDL2 就直接使用,否则会下载 SDL2。README 使用 -G Ninja 指定 Ninja,但默认生成器同样可用。

在窗口中,F1 到 F7 依次切换启动、空闲、聆听、思考、说话、错误和关闭状态。按住空格键说话,按 P 保存截图。鼠标相当于触摸,向左拖动即可打开设置页。

模拟器也可以在没有窗口的情况下运行。场景文件设置 UI 状态,模拟器把结果保存为图像:

./esp32/simulator/build/muse_simulator \
  --headless \
  --scenario esp32/simulator/tests/scenarios/thinking.txt \
  --run-ms 250 \
  --screenshot thinking.ppm
bash

SDK 的测试在 CI 中使用这种模式检查 UI。这也是 LVGL SDL 驱动为所有项目提供的做法:让 UI 代码不依赖硬件调用,同一份代码就能同时为开发板和桌面构建。

提示

模拟器不会模拟 ESP32-S3 CPU、PSRAM 或内存压力。上面的技巧 1 和技巧 2 只有在真实硬件上才能看到效果。


开始动手#

如果想制作自己的 Muse gadget,请从 SDK 代码仓库和 Muse Gadgets 网站开始。配对设备需要 Muse 账号和 SDK 令牌。如果想在自己的 LVGL 项目中使用这些技巧,侧边栏中的文档链接对每一项都有说明。

本文所有代码均来自 SDK 于 2026 年 10 月 16 日提交的 f60347a。

关于作者

乔瓦尼·鲍尔迈斯特
乔瓦尼·鲍尔迈斯特

解决方案工程团队负责人

LVGL 解决方案工程团队负责人,为全球客户打造基于 LVGL 的嵌入式 UI。

认识博客背后的作者们

了解那些分享 LVGL 知识的优秀作者们

查看作者

订阅我们的通讯 不错过任何关于 LVGL 的新闻。我们每月最多发送 2 封邮件。

LVGL

LVGL 是最受欢迎的免费开源嵌入式图形库,支持任何 MCU、MPU 和显示类型,助您构建精美的用户界面。

我们还提供 UI 设计、实现和咨询等服务。

© 2026 LVGL。保留所有权利。
YouTubeGitHubLinkedIn