用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣

2026-11-103

7 寸 DSI 大屏 + MAX98357 I2S 功放 + 原创动态角色,从点亮屏幕、联网对话到让角色真正动起来的完整踩坑记录。

本篇所有硬件均采购自 DFRobot 创客商城:FireBeetle 2 ESP32-P4 开发板、7 英寸 DSI 电容触摸屏、MAX98357 I2S 功放模块。固件基于开源项目 xiaozhi-esp32 v2.5.3(MIT 协议)二次开发,构建环境为 ESP-IDF release/v6.1。

完整源码、原创角色资源与编译好的固件已开源:Gitee:yuntian365/xiaozhi-p4-dfrobot。
用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_1.webp

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_2.webp

开源资料:本项目全部代码、板型补丁、角色资源和编译好的固件都在上面的公开仓库(MIT 协议),clone 下来即可编译或直接烧录:

git clone https://gitee.com/yuntian365/xiaozhi-p4-dfrobot.git
仓库目录 内容
xiaozhi-esp32/ 已应用全部改动、可直接编译的源码树
board-overlay/ 独立改动补丁包,方便对照上游或移植到新版固件
live2d/ 原创角色资源、生成脚本与 assets 分区镜像
firmware/ 编译好的整片/分区镜像、C6 从机固件与 SHA256 清单
docs/ 编译烧录详解 BUILD.md 与原始交接记录

仓库里的 README.md 是命令准稿,docs/BUILD.md 记录了完整的 Windows 环境坑与排错表。


演示视频

workbuddy,deepseek harness 等,可使用自定义大模型,这个项目便是我现在使用硅基流动的大模型完成的。注册硅基流动,可免费获得 16 元模型使用代金券。注册地址:https://cloud.siliconflow.cn/i/KwyEBX3e,使用以上地址注册,你得 16 元代金券,我也得 16 元代金券(我的快用没了)。

一、为什么要做这台机器

小智 AI 是近两年国内创客圈很火的开源语音助手固件:本地唤醒、Opus 音频流、接云端大模型、还能用 MCP 控制外设。官方已经适配了一百多种板卡,刷个现成固件就能聊天。

但我手上这块 FireBeetle 2 ESP32-P4 有点特别——它有 MIPI-DSI 接口、32MB PSRAM 和 360MHz 双核 RISC-V,性能远超常见的 ESP32-S3,却没有现成的小智板型;Wi-Fi 也不是芯片自带的,而是靠板载 ESP32-C6 模组经 SDIO 扩展。再加上官方那套表情包在 7 寸大屏上显得太小、太单调,于是给自己定了三个目标:

  1. 把小智 AI 完整移植到 P4,点亮 7 寸 DSI 触摸屏并接通音频;
  2. 让设备稳定联网,能真正对话(这一步花的时间比预想多得多);
  3. 做一套原创的、会呼吸会摆动的动态角色,替换掉默认表情。

整个项目从环境搭建到最终跑通,最有价值的其实不是代码,而是中间那几个「看着像软件 bug、其实是硬件链路问题」的坑。下面按复现顺序完整写下来。


用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_3.webp

二、成品效果与参数

项目 参数
主控 ESP32-P4 rev1.0,双核 360MHz RISC-V,16MB Flash / 32MB PSRAM
显示 7 英寸 800×480 DSI 电容触摸屏,RGB565,5 点触控
音频输入 板载 PDM 数字麦克风
音频输出 MAX98357 I2S 数字功放 + 4Ω/8Ω 喇叭
联网 板载 ESP32-C6-MINI-1,SDIO 4-bit 40MHz,ESP-Hosted slave v2.13.1
固件 xiaozhi-esp32 v2.5.3(zh-CN),ESP-IDF release/v6.1
角色资源 21 张情绪静帧 + 3 组透明 GIF 动画,共 31 个 emotion 条目
开机 约 9.4 秒完成 Wi-Fi、握手、MQTT 连接与激活

几个自己比较满意的点:

  • 深色主题:light / dark 两套皮肤都改成深蓝底 + 浅色字,银白发角色不会糊在背景里;
  • 状态联动:待机播眨眼呼吸动画,说话播音口型动画,不用服务端下发 emotion;
  • 伪 Live2D 肢体动作:头、发梢、双臂、胸腔各自按不同频率运动,不再是整张图缩放;
  • 零额外硬件升级 C6:借现有 SDIO 链路做从机 OTA,不用 USB-TTL 飞线。

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_4.webp

图里特意把音频输出单独画在 P4 下面:服务端合成的 TTS 音频经 MQTT / WebSocket 回到 P4 之后,才由 P4 的 I2S 外设(GPIO21/22/23)送给 MAX98357。功放是纯本地外设,既不联网、也不直连服务端;屏幕表情则完全走本地 assets 分区,与网络链路相互独立。


三、硬件清单(全部来自 DFRobot 创客商城)

序号 名称 货号 数量 商城参考价 购买链接 在项目中的作用
1 FireBeetle 2 ESP32-P4 AI 开发板 DFR1172 1 ¥115.00 创客商城商品页 主控,跑小智固件与 LVGL 界面
2 800×480 TFT 树莓派 DSI 电容触摸屏(7 英寸) DFR0678 1 ¥359.00 创客商城商品页 显示与触控,FPC 排线直连
3 MAX98357 I2S 功放模块 DFR0954 1 ¥30.00 创客商城商品页 把 P4 的 I2S 数字音频放大推喇叭
4 4Ω / 8Ω 小喇叭、PH2.0 线、杜邦线、USB-C 数据线 — 若干 — 商城配套配件区 供电与连接

三件核心板卡/模块全部出自 DFRobot 创客商城,资料也直接引用官方产品维库:DFR1172 维库、DFR0678 维库、DFR0954 维库。参考价为 2026-11-10 商城页面显示,库存与价格以实时页面为准。

3.1 为什么一定要外接功放

这是新手最容易卡住的一点:FireBeetle 2 ESP32-P4 板载了 PDM 麦克风,但没有 DAC、没有功放、也没有喇叭。所以拾音不用管,放音必须自己接一片 I2S 功放。

选 MAX98357 的原因很直接:它是 I2S 数字输入的 D 类功放,不需要额外 codec,P4 的 I2S 引脚直连即可;3.3V~5V 宽电压供电,5V / 4Ω 下输出 2.5W 以上,做桌面小音箱音量够用;板载 PH2.0 喇叭接口,接喇叭不用焊;SD 引脚还能切换左右声道/混合声道,官方默认混合声道,单喇叭刚好。模块是邮票孔设计,面包板直插或后期贴到自己画的底板上都方便。

3.2 屏幕为什么能直接接

DFR0678 本来是给树莓派设计的 DSI 屏,而 DFR1172 的两通道 MIPI-DSI 接口官方明确标注「兼容树莓派 4B DSI 屏幕线序」,一根 FPC 排线就能连上,不用转接板。屏内自带 TC358762XBG(DSI→RGB 桥)和一颗屏上 MCU,上电后自行完成初始化,主机只需要通过 FPC 上的 I2C 管触摸与背光。这块屏 800×480、5 点电容触控、60Hz、发光率 280 cd/m²、显示区域 154.8×85.92 mm,做桌面伴侣尺寸和观感都合适;包装里附 150mm / 50mm 两种 FPC 反向异面软排线,装机时按实际走线长度选。


四、硬件连接

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_5.webp

4.1 MAX98357 ↔ P4 接线表

MAX98357 引脚 P4 引脚 说明
VIN 5V 建议 5V,可发挥 2.5W 输出
GND GND 共地
BCLK GPIO21 I2S 位时钟
LRC / WS GPIO22 I2S 帧时钟
DIN GPIO23 I2S 数据输出
SD 悬空或接 GPIO 官方默认混合声道;接低电平可关断功放
OUTP / OUTN 喇叭(PH2.0) 接 4Ω 或 8Ω 喇叭
GAIN 按需接电阻 不接为 9dB,接地 12dB,可调整音量

固件侧对应的是 NoAudioCodecSimplexPdm:一路 PDM 输入负责录音,一路 I2S 输出负责放音,两路方向不同,因此不需要 ES8311 之类的全双工 codec。

4.2 板卡引脚总表

功能 引脚 / 地址
DSI FPC I2C SCL = GPIO8,SDA = GPIO7
触摸屏 FT5x06,I2C 0x38,需 X/Y 双镜像
背光控制 屏上 MCU,I2C 0x45,寄存器 0x86
PDM 麦克风 CLK = GPIO12,DATA = GPIO9
I2S 输出 BCLK = GPIO21,WS = GPIO22,DOUT = GPIO23
C6 SDIO D0=14、D1=15、D2=16、D3=17、CLK=18、CMD=19、EN=54
其他 BOOT = GPIO35,LED = GPIO3

接线提示:先用屏幕自带的 FPC 排线把屏接好并确认背光亮,再接功放;喇叭别在功放上电时插拔。DSI 排线较脆,翻盖卡扣要压到位。


五、软件环境与版本

组件 版本 / 说明
小智 AI 固件 78/xiaozhi-esp32 v2.5.3,MIT 协议
ESP-IDF release/v6.1(v6.1.0),需勾选 esp32p4 target
Python 3.11(IDF 独立 venv),esptool 5.6.0
构建工具 CMake、Ninja、RISC-V GCC 15.2.0
烧录 16MB Flash / dio / 80m,串口 460800
角色资源生成 Python + Pillow / NumPy / OpenCV / rembg(u2netp)

官方资料入口:小智 AI 官网、固件仓库、ESP-IDF 仓库。


六、制作步骤

建议先 git clone 上面的仓库对照着看:xiaozhi-esp32/ 里已经是应用了全部改动的可编译源码树,不想自己拼补丁的话直接进这个目录编译即可。下面按复现顺序把关键步骤和踩过的坑写清楚。

6.1 新增一块自定义板型

小智的板型都放在 main/boards/<厂商>/<板名>/ 下。我新建了 dfrobot/firebeetle-2-esp32-p4/,包含四个文件:

文件 作用
config.h 全部硬件宏:屏尺寸、I2C、背光、触摸镜像、DSI/DPI 时序、音频引脚
config.json 板型 / target / variant / OTA 名 + sdkconfig 追加项
firebeetle-2-esp32-p4.cc DSI PHY 供电、DSI bus/DBI IO/DPI panel、I2C 背光、FT5x06 触摸、BOOT、音频 codec
README.md 接线表、编译烧录命令、调试顺序

同时修改 main/Kconfig.projbuild 新增板型选项、main/CMakeLists.txt 增加 800×480 对应的字体与表情集合分支。板型标识写成独立的 firebeetle-2-esp32-p4-rpi7,避免借用别的板型后被官方 OTA 覆盖。

6.2 显示驱动走「免 DSI 命令配置」路径

这里有个选型取舍:社区里有现成的 esp_lcd_rpi_dsi 驱动,但它输出 RGB888,与小智 LVGL 缓冲区默认 RGB565 的假设不兼容,而且 GPL-2.0 静态链进 MIT 工程有许可证顾虑。所以按 DFRobot 维库已验证的方式来做:

  • 1-lane DSI @ 1250 Mbps,clock_lane_force_hs=1,DPI 30MHz,RGB565;
  • LDO3 输出 2.5V 给 DSI PHY;
  • 桥芯片和 LCD 由屏上 MCU 自行初始化,主机不发 DSI 初始化命令;
  • 主机只通过 0x45 / 0x86 管背光。

显示层复用 MipiLcdDisplay,触摸用 lvgl_port_add_touch() 挂进 LVGL。

如果出现「有背光但白屏」,把 config.h 里的 RPI_DSI_FULL_POWER_ON_SEQUENCE 打开重编译——7 寸和 5 寸屏的屏上 MCU 固件可能不同,需要完整上电序列。

6.3 编译(Windows 下有三个必踩的坑)

. "<IDF 安装路径>\Microsoft.v6.1.PowerShell_profile.ps1"
$env:PYTHONPATH = ""
$env:CCACHE_DISABLE = "1"
cd C:\xz\xiaozhi-esp32
python scripts/build.py dfrobot/firebeetle-2-esp32-p4 `
  --name firebeetle-2-esp32-p4-rpi7 --language zh-CN
python scripts/fix_ninja_long_cmd.py
& "<ninja.exe 路径>" -C build -j 4 all

三个坑按出现频率排序:

  1. 清空 PYTHONPATH:某些 IDE/工具会注入 sitecustomize.py,拦截 IDF 组件管理器的缓存清理,导致 configure 卡住。
  2. Windows 命令行超长:项目路径一长,project_elf_src_esp32p4.c.obj 的 INCLUDES 能到 2~3 万字符,超过 32767 上限。-DCMAKE_NINJA_FORCE_RESPONSE_FILE=ON 对这条 edge 无效,得用脚本把它改成 response file(includes.rsp)。把项目放在短路径如 C:\xz 能直接避开。
  3. ccache 挂死:EIM 默认开启 ccache,编译 esp-dsp 的汇编文件时 ccache.exe 会无子进程、CPU 占用 0 地卡住。除了 CCACHE_DISABLE=1,更彻底的做法是把 build/CMakeFiles/rules.ninja 里的 command = ccache 前缀剥掉。

另外两点经验:idf.py reconfigure(切换语言也会触发)会重新生成 ninja 规则,覆盖上面两个补丁,每次 reconfigure 后要重跑;只改 application.cc 时不要 reconfigure,直接 ninja -C build -j 4 all 增量编译即可。裸跑 ninja 需要补齐 IDF_PATH、ESP_IDF_VERSION=6.1.0、IDF_TARGET=esp32p4、IDF_PYTHON_ENV_PATH,否则 ldgen.py 解析 Kconfig 里的 $(ESP_IDF_VERSION) 会展开为空而失败。

6.4 分区表与烧录

偏移 分区 大小
0x2000 bootloader —
0x8000 partition table —
0xd000 otadata —
0xf000 phy_init —
0x20000 ota_0(app) 0x3f0000
0x410000 ota_1 0x3f0000
0x800000 assets 8 MB

生成整片合并镜像:

python -m esptool --chip esp32p4 merge_bin `
  -o merged_0x0.bin --flash_mode dio --flash_freq 80m --flash_size 16MB `
  0x2000 build\bootloader\bootloader.bin `
  0x8000 build\partition_table\partition-table.bin `
  0xd000 build\ota_data_initial.bin `
  0x20000 build\xiaozhi.bin `
  0x800000 build\generated_assets.bin

烧录(COM 号以设备管理器为准):

python -m esptool --chip esp32p4 -p COM4 -b 460800 `
  --before default_reset --after hard_reset write_flash `
  --flash-mode dio --flash-freq 80m --flash-size 16MB `
  0x0 merged_0x0.bin

很重要:整片从 0x0 烧录会擦掉 NVS 里的 Wi-Fi 凭据,设备会重新进入配网模式(热点 Xiaozhi-XXXX,配网页 192.168.4.1)。后续只改固件或资源时,建议按分区单独烧,不动 NVS:

python -m esptool --chip esp32p4 -p COM4 -b 460800 write_flash `
  0x20000 xiaozhi.bin 0x800000 xiaozhi-live2d-assets.bin

配网完成后,按 BOOT 唤醒,设备会播报 6 位验证码;到 xiaozhi.me 控制台 添加设备绑定即可对话。


七、踩坑实录(本文最值钱的部分)

7.1 开机无限重启:P4 rev1.0 的 DMA 内存不够

第一次烧完,串口疯狂重启,断言停在:

assert failed: sdio_mempool_create sdio_drv.c:273 (buf_mp_g)

原因很硬件:ESP32-P4 rev1.0 内部 DMA-capable RAM 只有约 84KB(RETENT_RAM 62KB + RAM 18KB),另外那块 357KB 的 RAM 没有 DMA 属性;而 esp_hosted 在 main() 之前的构造函数阶段就要建两个 DMA 池,约 38464B + 47680B,刚好把 84KB 吃满还要溢出。

修复只需一个配置项,写在 config.json 的 sdkconfig 追加项里:

CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM=y

让内存池落到 32MB PSRAM,重启循环立刻消失。这个坑不看芯片手册基本猜不到,也是 P4 + ESP-Hosted 组合的必修课。

7.2 反复「无法连接服务」:根因在 C6 从机固件

联网后设备频繁弹「无法连接服务,请稍后再试」,443(版本检查)和 8883(MQTT)双双超时。我按常规思路排查了一圈:

  • PC 在同一网络下测 TCP + TLS:443 与 8883 都在 0.1~0.2 秒内握手成功,证书 CN 正常 → 不是服务端、不是宽带;
  • 把 MQTT_CONNECT_TIMEOUT_MS 和 network.timeout_ms 从 10s 放宽到 40s → 错误只是从「超时」变成 Software caused connection abort,无效,回退;
  • 关掉开机版本检查 → 另一条错误消失了,但 MQTT 照样连不上,还多出 Display: System time not set (tm_year=70) 每 10 秒刷屏。

真正的线索一直在启动日志里,只是看起来像无害警告:

W ... Version mismatch: Host [2.13.0] > Co-proc [0.0.0]

板载 ESP32-C6 的出厂 ESP-Hosted 从机固件太旧,不报告版本、不报告 transport size,与 P4 侧 host 2.13.0 协议不匹配,导致设备侧所有 TCP/TLS 连接异常。软件层怎么改都绕不过去。

不用 USB-TTL 的升级办法:SDIO OTA

DFRobot 维库给的方案是 USB-TTL 接 C6 测试点刷出厂固件,需要额外接线。但 esp_hosted 组件自带从机 OTA 示例和 public API:

esp_hosted_slave_ota_begin();
esp_hosted_slave_ota_write(uint8_t* data, uint32_t len);
esp_hosted_slave_ota_end();
esp_hosted_slave_ota_activate();

于是完全复用项目已跑通的 SDIO 链路:

  1. 下载与目标 host 匹配的从机镜像 network_adapter_esp32c6.bin v2.13.1(1,228,160 B,App version: 2.13.1);
  2. 把镜像烧到 P4 空闲的 ota_1 分区 0x410000(4MB 够放);
  3. 临时固件在 ActivationTask() 开头(此时 SDIO 已就绪)读 ota_1,按 ESP32 镜像头算出长度,begin → 1480 B/块共 830 块 write → end;
  4. 旧从机不支持 GetCoprocessorFwVersion 和显式 activate,按官方示例跳过即可,end 后 esp_restart() 让 P4/C6 重新同步;
  5. 成功后擦掉 ota_1 前 4KB 防重复执行,移除一次性代码、烧回正式 app、erase-region 0x410000 0x3f0000 清场。

C6 采用双 OTA 分区,写入校验通过才激活,失败会保留原固件,全程 NVS 与 assets 不动。升级后日志变成:

I (2559) transport: got co-processor transport size: 1536
I (2580) transport: co-processor transport size matches host transport size: 1536
I (8629) Application: Boot config handshake ok
I (9635) MQTT: Connected to endpoint
I (9636) Application: Activation done

Version mismatch 警告、「无法连接服务」、tm_year=70 全部消失,开机约 9.7 秒完成激活。如果日志里长期有 Host [x] > Co-proc [0.0.0],先别调超时,先升从机固件。

7.3 开机「检查新版本失败」

CheckNewVersion() 里的 CheckVersion() 并不只是比版本号,它还负责把 MQTT/WebSocket 配置、激活数据和 server_time 写进 NVS。整个跳过会让凭据刷新不了、系统时间停在 1970;保留原流程又会在网络抖动时弹「检查新版本失败」并指数退避重试 10 次。

折中方案是单次握手:开机只请求一次 CheckVersion(),不显示检查状态、不比较版本、不 OTA、不重试,失败静默沿用 NVS 缓存;保留激活码流程和 MarkCurrentVersionValid()(OTA 回滚标记);InitializeProtocol() 的协议选择同时参考 NVS 缓存。

#define SKIP_BOOT_VERSION_CHECK 1
// ...
auto check = ota_->CheckVersion();
if (!check) {
    ESP_LOGW(TAG, "Boot config handshake failed, keep cached settings: %s", ...);
} else {
    ESP_LOGI(TAG, "Boot config handshake ok (firmware version check disabled)");
}
ota_->MarkCurrentVersionValid();

配套还有一个必加的补丁:第一次实现没给 HTTP 设超时,开机日志停在 Ota: Current version: 2.5.3 后 60 秒无输出,整个启动流程被挂死。在 main/ota.cc 的 SetupHttp() 里加 http->SetTimeout(15000) 后,10 秒内失败就静默回落。

7.4 系统弹窗找不到表情

错误提示界面用的是 warning、cancel、cloud_download、cloud_off、link、download、gear 这 7 个系统级 emotion 名,不在角色情绪列表里。缺失时固件会 fallback 到字体图标并打 Emoji not found: xxx。解决办法是在 index.json 里把它们作为别名指向已有静帧(cloud_off → sad.png、gear → thinking.png),多个 name 指向同一文件,assets 只存一份图,体积不变。

7.5 显示 / 触摸方向

触摸芯片 FT5x06 需要 X/Y 双镜像(TOUCH_MIRROR_X / TOUCH_MIRROR_Y),否则点击位置和显示位置对不上;显示方向异常时先查 config.h 里的 DPI 时序与镜像宏。这两项和屏幕批次有关,建议留成宏方便现场调。


八、进阶:给小智做一个「会动」的原创角色

8.1 为什么不是真 Live2D

严格说这不是 Cubism 的 .moc3,而是一套伪 Live2D:在 PC 端用 Python 逐帧生成带连续形变的透明 GIF,设备端仍走原有的 LvglGif 播放链路。

理由很实际:P4 上引入 Cubism runtime 要 SDK 授权、额外 RAM 和实时网格计算;而小智固件本来就有完整的表情通道——assets 分区里的 index.json 声明 emoji_collection,Assets::LvglStrategy::Apply() 把每个文件包装成 LvglRawImage,LcdDisplay::SetEmotion() 按名字切换,LvglGif 自动循环播放。只要产出符合规格的图片,设备端零 C++ 改动、零新增解码开销。

规格约束:8-bit RGBA PNG 或透明 GIF,文件名即 emotion 名且不超过 31 字节,LVGL 按原尺寸居中显示。本项目定的是 280×340。

8.2 资源包怎么进设备

用 build_assets_bin.py 在编译产物 generated_assets.bin 上只替换 emoji_collection 条目,保留原有字体与语音模型,重新按官方格式算 checksum:

文件头  <III   12 字节
表项    <32sIIHH  44 字节
文件前缀  "ZZ"
checksum = sum(payload) & 0xFFFF

输出 xiaozhi-live2d-assets.bin,单独烧到 0x800000 即可,不用重编译固件。资源清单是 21 张情绪静帧 + 3 组动画 + 7 个系统别名 = 31 个条目 / 24 个文件。

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_6.webp

8.3 深色主题

银白发角色放在默认浅色背景上会糊成一片。assets.cc 会消费 index.json 里的 skin,LvglTheme::ParseColor() 接受 #RRGGBB,于是打包脚本注入:

"skin": {
  "light": {"text_color": "#E8F2FF", "background_color": "#101826"},
  "dark":  {"text_color": "#EAF4FF", "background_color": "#0B1220"}
}

两套主题都覆盖,不论设备当前用哪套,角色都站在深色背景上;background_color 会同时应用到 chat_background_color。注意 skin 是在激活流程里由 assets.Apply() 应用的,配网模式阶段仍是固件默认主题,配网完成后才变深色。

8.4 状态联动,以及「说话时闭眼」的 bug

在 main/application.cc 里加一个辅助函数,并挂到状态机:

状态 emotion 效果
idle / connecting / listening idle 呼吸 + 眨眼动画
speaking / notifying speaking 口型开合动画
DismissAlert() 回到待机 idle 恢复待机动画
Activate() 末尾 idle 开机即播
template <typename DisplayPtr>
void SetCharacterEmotion(DisplayPtr display, const char* emotion) {
    if (display->SetEmotion(emotion)) {
        ESP_LOGI(TAG, "Character emotion: %s", emotion);
    } else {
        display->SetEmotion("neutral");   // 没有自定义包时回退
    }
}

做完却发现「说话时角色不动,而且是闭着眼」。逐帧检查 speaking.gif 明明是睁眼 + 口型变化的动画,问题不在资源:服务端 "llm" 消息会在 TTS 流期间持续下发 emotion,把 speaking.gif 替换成了静帧,而 happy.png / sleepy.png 恰好都是闭眼图;更糟的是每来一个分句就重设一次,GIF 不断从第 0 帧重启。

修复是在 llm 分支判断状态:

auto state = GetDeviceState();
if (state == kDeviceStateSpeaking || state == kDeviceStateNotifying) {
    ESP_LOGI(TAG, "Skip server emotion '%s' while speaking", emotion_str.c_str());
    return;   // 不重新 SetEmotion,避免重启 GIF
}
display->SetEmotion(emotion_str.c_str());

非说话状态仍照常显示服务端下发的情绪表情。

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_7.webp

8.5 伪 Live2D 肢体动作

只做眨眼和缩放,角色看着还是「一张会呼吸的贴纸」。难点在于:手里的立绘是一张合成图,没有分层 PSD,硬切图层一定会露接缝。所以改用连续网格变形——make_live2d.py 里的 pose_warp() 构造 2D 高斯权重场,再用 cv2.remap 对整张 RGBA 做一次重采样:

head  = gauss(yy, 105, 95)                                  # 头部
lhair = gauss(xx, 55, 48)  * clip((yy - 110) / 150, 0, 1)  # 左侧长发
rhair = gauss(xx, 225, 48) * clip((yy - 110) / 150, 0, 1)  # 右侧长发
larm  = gauss(xx, 32, 36)  * clip((yy - 195) / 110, 0, 1)  # 左臂
rarm  = gauss(xx, 248, 36) * clip((yy - 195) / 110, 0, 1)  # 右臂
chest = gauss(yy, 245, 55) * gauss(xx, 140, 85)             # 胸腔

dx = (2.6*amp*sin(p)*head + 1.8*amp*sin(p+1.2)*lhair
      - 1.7*amp*sin(p+1.2)*rhair + 2.4*amp*sin(0.75*p+2.1)*larm
      - 2.4*amp*sin(0.75*p+2.1)*rarm)
dy = 1.4*amp*sin(p+0.4)*head + 1.8*amp*(0.5-0.5*cos(p))*chest
# 软阈值把远端微小位移归零;底边 40px 锚定防止露出透明缝
dx = sign(dx) * maximum(abs(dx) - 0.12, 0.0)
dy = sign(dy) * maximum(abs(dy) - 0.12, 0.0)
out = cv2.remap(arr, (xx - dx), (yy - dy), cv2.INTER_LINEAR,
                borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0, 0))

关键是各部位用不同频率和相位:头 sin(p)、发梢 sin(p+1.2) 左右反相、手臂 sin(0.75p+2.1) 左右反相、胸腔 0.5-0.5cos(p)。如果全用同一个相位,看起来就像整张图在平移,反而更假。三组动画的幅度分别取 1.0 / 1.15 / 1.35,静帧不调用 pose_warp,21 张 PNG 保持不动。

帧数从 12/6/8 提到 20/12/14 帧 @ 90ms(共 46 帧,比原来 120ms 明显顺滑)。

用 FireBeetle 2 ESP32-P4 做一台「会动」的小智 AI 桌面伴侣_image_8.webp

8.6 一场 8MB 分区里的体积战争

第一版加上全局 sway 后,GIF 从 1.35 MB 涨到约 2.17 MB,直接把 8MB assets 分区撑爆。压回来靠三招:

  1. 去掉全局位移,只保留局部权重场,让静止区域像素逐帧完全一致,GIF 帧间差分才压得下来;
  2. 固定 96 色调色板:首帧量化一次,后续帧复用同一调色板,dither=0,避免逐帧量化产生整片噪点;
  3. 软阈值 0.12px 把权重场远端的微小位移归零,既保持连续不露接缝,又减少变化像素。

最终三组 GIF 为 880,857 + 527,391 + 613,157 B,整个资源包 8,282,027 B(7.898 MiB),而分区上限 8 MiB——只剩约 104 KiB。这是一个必须写清楚的边界:后面要加表情,得先降 colors(96→64)、减 happy_anim 帧数或砍掉不常用静帧。也正因为要控体积,动作幅度做得偏克制,是「轻微但连续」而不是大幅挥手。


九、效果与实机验证

最终开机日志(COM4,115200):

I (615)  Board: UUID=... SKU=firebeetle-2-esp32-p4-rpi7
I (631)  LcdDisplay: Initialize LVGL library
I (2559) transport: got co-processor transport size: 1536
I (2580) transport: co-processor transport size matches host transport size: 1536
I (6718) WifiStation: Got IP: 192.168.0.106
I (6736) Assets: The assets map size is 8128 KB (partition 8192 KB)
I (7078) Assets: The checksum calculation time is 335 ms
I (7083) Assets: Refreshing display theme...
I (7142) Application: Character emotion: idle
I (8409) Application: Boot config handshake ok (firmware version check disabled)
I (9402) MQTT: Connected to endpoint
I (9402) Application: Activation done
I (9487) Application: Character emotion: idle

assets map size 从上一版的 7488 KB 变成 8128 KB,说明新资源包确实生效;全日志没有 Emoji not found、没有 GIF 解码错误、没有 Version mismatch。剩下三条启动告警(I2S DMA 对齐、0 mac type is incorrect、SDIO 复位从机)都是已知无害项。

也遇到过一次偶发状况:开机两次 WiFi disconnected, reason: 2、37 秒才拿到 IP,随后 443/8883 超时;同一网络下 PC 测两个端口都通,重启一次即恢复。这属于 Wi-Fi 认证超时的环境波动,不是 C6 固件回退,也不是资源问题。


十、复现建议与成本

  • 直接用开源仓库起步:git clone https://gitee.com/yuntian365/xiaozhi-p4-dfrobot.git,firmware/ 里有烧好即用的镜像,xiaozhi-esp32/ 是完整源码,省去自己拼板型补丁和重新打包资源。
  • 先跑通最小系统:只接屏幕 + USB-C,确认背光和显示正常,再接功放,最后处理联网。分步排查比一次全接上省很多时间。
  • 优先升级 C6 从机固件:如果日志里有 Co-proc [0.0.0],别在 MQTT 超时参数上浪费时间。
  • 用短路径:C:\xz 这类路径能直接绕开 Windows 超长命令行的坑。
  • 只烧改动的分区:改固件烧 0x20000,改资源烧 0x800000,别每次都整片 0x0,否则要反复配网。
  • 硬件成本:三件核心模块按商城参考价合计约 ¥504(115 + 359 + 30),加上喇叭、排线与结构件即可开工;库存与价格以商城实时页面为准。
  • 风险提示:DSI 排线和 5V 供电别接反;喇叭不要在功放上电时热插拔;若后续扩展到 220V 电器控制,请只用成品继电器模块并做好绝缘。

十一、总结与下一步

这台机器最后变成了一个放在桌上的、有自己表情和动作的语音助手:喊一声唤醒,7 寸深色屏幕上银白发角色会转头、飘发梢、轻轻摆手臂;说话时口型跟着 TTS 走,播完回到待机眨眼。

回头看,技术上有三条经验最值得留下来:

  1. P4 + ESP-Hosted 必须先配 MEMPOOL_PREFER_SPIRAM,这是 rev1.0 芯片的硬约束;
  2. 「连不上服务器」先查从机固件版本,再查网络参数,日志里那行 Version mismatch 不是无害警告;
  3. 在 MCU 上做「Live2D」不一定要上引擎,把复杂度前移到 PC 端逐帧生成,设备端只做 GIF 播放,是资源受限场景下很划算的取舍。

还在收尾和想继续做的:

  • 屏幕方向、触摸方向与外接功放的音质/音量做最终实机微调;
  • 把角色图拆成真正的分层素材(头/发/双臂/躯干独立层),动作幅度能再大一个量级;
  • 给设备加个 3D 打印外壳,把板、屏、功放和喇叭收成一体;
  • 借小智的 MCP 能力,让角色状态和家里的灯、传感器联动起来。

附录 A:常用命令速查

# 拉取本项目完整源码与固件
git clone https://gitee.com/yuntian365/xiaozhi-p4-dfrobot.git

# 查看串口
Get-PnpDevice -Class Ports

# 只烧 app(保留 NVS 配网)
python -m esptool --chip esp32p4 -p COM4 -b 460800 write_flash 0x20000 xiaozhi.bin

# 只烧 assets(换表情/主题,无需重编译固件)
python -m esptool --chip esp32p4 -p COM4 -b 460800 write_flash 0x800000 xiaozhi-live2d-assets.bin

# 擦除临时 OTA 分区
python -m esptool --chip esp32p4 -p COM4 erase-region 0x410000 0x3f0000

# 重新生成角色资源与 assets 镜像
python make_live2d.py
python build_assets_bin.py --base <IDF 工程>\build\generated_assets.bin --out xiaozhi-live2d-assets.bin

附录 B:资料链接

资料 链接
本项目 Gitee 源码仓库(源码 + 固件 + 角色资源) https://gitee.com/yuntian365/xiaozhi-p4-dfrobot
DFRobot 创客商城 https://www.dfrobot.com.cn/
FireBeetle 2 ESP32-P4(DFR1172) https://www.dfrobot.com.cn/goods-4172.html
DFR1172 产品维库 https://wiki.dfrobot.com.cn/SKU_DFR1172_FireBeetle_2_ESP32_P4
7″ DSI 触摸屏(DFR0678) https://www.dfrobot.com.cn/goods-3005.html
MAX98357 I2S 功放(DFR0954) https://www.dfrobot.com.cn/goods-3573.html
小智 AI 固件仓库(MIT) https://github.com/78/xiaozhi-esp32
小智 AI 官网 / 控制台 https://xiaozhi.me/
ESP-IDF https://github.com/espressif/esp-idf
ESP-Hosted 从机固件参考 https://github.com/espressif/esp-hosted-mcu

社区延伸阅读

DFRobot 官方在 DFR1172 商品页里也整理了一批社区案例,做这块板时参考过,推荐给准备动手的朋友:

案例 链接
FireBeetle 2 ESP32 P4 适配小智 AI https://mc.dfrobot.com.cn/thread-394203-1-1.html
FireBeetle 2 ESP32-P4 驱动 800×480 显示屏 https://mc.dfrobot.com.cn/thread-395549-1-1.html
P4 使用树莓派兼容 CSI 接口摄像头(ESP-IDF) https://mc.dfrobot.com.cn/thread-394160-1-1.html

本篇在这些工作的基础上,重点补了「C6 从机固件导致的联网故障」与「原创动态角色资源」两块内容。


本项目基于开源固件二次开发,遵循原项目 MIT 协议;完整源码、角色资源与编译好的固件见 Gitee 仓库。角色形象与动画资源为本项目原创。文中参数与日志均来自实机验证,硬件信息以 DFRobot 官方页面为准。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

所需材料/产品

加载全部

    800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

    800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

    800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

    MAX98357 I2S功放模块

    MAX98357 I2S功放模块

    MAX98357 I2S功放模块

    FireBeetle 2 ESP32 P4 AI开发板

    FireBeetle 2 ESP32 P4 AI开发板

    FireBeetle 2 ESP32 P4 AI开发板

相关推荐

评论(0)
- 没有更多了 -

创作许可协议

本项目采用 CC BY(署名) 进行许可。

相关推荐

所需材料/产品

800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

800x480 TFT 树莓派 DSI 电容触摸屏 (7英寸)

MAX98357 I2S功放模块

MAX98357 I2S功放模块

MAX98357 I2S功放模块

FireBeetle 2 ESP32 P4 AI开发板

FireBeetle 2 ESP32 P4 AI开发板

FireBeetle 2 ESP32 P4 AI开发板