侧边栏壁纸
博主头像
天寅阁 博主等级

忽有故人心头过,回首山河已是秋。

  • 累计撰写 23 篇文章
  • 累计创建 6 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

第三章 黑屏风波:从源码编译“小智AI”固件,我的第一次实战踩坑(小智AI源码初探)

Administrator
2026-04-29 / 0 评论 / 0 点赞 / 0 阅读 / 0 字
温馨提示:
本文最后更新于2026-04-29,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

上一篇,我们成功让 ESP32-S3 开发板运行了 hello_world 和官方的“小智AI”固件(回顾点这里:板子到了!我的第一次硬件“烧录”与踩坑实录 (ESP32-S3初体验))。故事并没有结束,反而因为一个小家伙的加入,变得更加有趣和“硬核”了。

“爸爸,我可以跟男生聊天吗?”

自从把那个能跑“小智AI”的小盒子带回家,我那四岁的儿子就对它产生了浓厚的兴趣。看着他一本正经地对着盒子说“你好,小智”,然后听着里面传出的回答,两个人居然能你一问我一答地聊上好久。

晚上睡觉前,他突然很认真地问我:“爸爸,我可以跟男生聊天吗?”(控制台默认使用的是“台湾女友”角色。是默认的,不是我选的 )
我愣了一下,解释道:“这是AI,不是真人。”
“什么是 AI?” 小家伙反问,我惊讶地发现,这可能是他第一次把这两个英文字母说得如此清晰标准。紧接着,他又追问:“那你能让他换成男生吗?”

“呃...可以,我研究研究。” 我故作镇定地回答。(心里暗自庆幸,好像之前在控制台里瞟到过切换角色的选项,不然这次就要让他失望了!)

第二天一早,小家伙刚起床就直奔电脑桌,拿起小盒子就开始喊:“你好,小智!你好,小智!” 连喊几声没反应(因为我还没插电),小脸蛋上写满了失望。我赶紧过去接好电源。因为还没来得及修改配置,传出来的依然是小姐姐的声音,小家伙跟她说话时,居然还有点害羞。

“别玩了,该出门了,幼儿园又要迟到了”,我打断他们的对话,“跟小智说再见吧,晚上再玩。”

晚上下班回来,赶紧把 AI 的声音配置换成了一个男声。

儿子放学回家的第一句话就是:“爸爸,你把他换成男生了吗?”
“你试试吧?” 我笑着说。

“你好,小智!”

随着盒子里传出一阵清晰的男声,我看到儿子脸上瞬间绽放出激动和惊喜的表情。那一刻,说实话,我心里挺感动的。作为一个常年忙于工作、似乎没能为孩子做太多“酷”事情的父亲,这一次,好像终于为你做了点什么特别的。

晚上,儿子和“男版小智”聊得不亦乐乎。“为什么恐龙会灭绝?”、“世界上真的有僵尸吗?”、“我不想上幼儿园。”... 甚至还让小智给他唱歌。但好景不长,因为他有些单词发音还不标准,AI 经常听不懂他在说什么,沟通开始出现障碍。最后,小家伙终于失去了耐心,生气地拔掉了连接线:“不玩了!”

看着他气鼓鼓的样子,我若有所思:或许以后可以针对这种儿童口音识别、或者更具引导性的对话做些改进?不过,现在我还不知道具体要做什么,以及怎么做。但这个小插曲,更加坚定了我深入了解这个项目的决心——至少,我得能自己修改和编译它的代码才行!

编译源码:从信心满满到一脸懵逼

回到我的学习主线。上一章结尾立了两个 Flag,今天就来完成吧。编译小智AI的固件源码,并烧录到 ESP32-S3 上,为后续的源码调试打下基础。

第一步,从 GitHub 克隆源码仓库。打开工程目录一看,结构非常清晰,main 目录、根目录下的 CMakeLists.txt... 这整体结构,和我之前跑通的官方 hello_world 示例项目如出一辙。

根目录和 main 目录下都有 CMakeLists.txt 文件。看到这里,我心里大概有数了:编译过程应该和 hello_world 差不多,用 ESP-IDF 的那套命令就行。

那就试试吧!打开 ESP-IDF 命令行工具:

idf.py set-target esp32s3
#(这一步象征性的执行了一下,并没有做任何配置,为之后的问题埋下的隐患)
idf.py menuconfig 
idf.py build

出乎意料的顺利!没有报错,程序编译成功完成了!

那么接下来,烧录到板子上看看能不能运行起来。

idf.py -p COM3 flash

烧录也顺利完成了!心中一阵窃喜。RESET一下(我可太熟练了)。

嗯?咋回事?屏幕还是黑的???

这下我有点慌了。之前reset完,屏幕就亮了,然后应该进入配网模式才对。现在怎么一片漆黑?这是啥情况?

不死心,连接监视器看看终端输出:

idf.py -p COM3 monitor

终端倒是有日志在滚动输出,而且看信息,Wi-Fi 相关的任务似乎也正常启动了。这说明程序本身应该是运行起来了,但为什么屏幕不亮呢?

暂时没什么头绪,不过至少程序运行起来了,既然能调试,那就看看源码吧。

源码探索:先看看大体结构

http://main.cc 应该是主程序入口了。打开一看,app_main 函数异常简洁:

Application::GetInstance().Start();

核心逻辑都在 Application 这个类里。 指挥Copilot 帮我梳理一下这个应用的主要流程:

(describe main application flow)

  1. 入口点 (http://main.cc): 初始化系统(事件循环、NVS 闪存),然后调用 Application::GetInstance().Start()。

  2. 核心类 (application.h/cc): 定义了应用的状态机 (DeviceState 枚举) 和核心功能。

  3. 启动流程 (Start() 方法):

  • 初始化硬件 (显示屏、音频等)

  • 启动音频循环 (AudioLoop)

  • 检查新版本

  • 初始化通信 (MQTT/WebSocket)

  • 进入主事件循环 (MainEventLoop)

  • 状态流转: 根据文档和实际观察,有 Idle -> Connecting -> Listening -> Speaking -> Idle 等状态转换。

  • 核心事件循环: MainEventLoop 处理状态和通信,AudioLoop 处理音频。

对照代码里的 DeviceState 枚举:

enum DeviceState {
    kDeviceStateUnknown,
    kDeviceStateStarting,
    kDeviceStateWifiConfiguring,
    kDeviceStateIdle,
    kDeviceStateConnecting,
    kDeviceStateListening,
    kDeviceStateSpeaking,
    kDeviceStateUpgrading,
    kDeviceStateActivating,
    kDeviceStateFatalError
};

确实和我之前观察到的状态能对应上。

程序首次启动进入“配网模式”。
wifi配置完成后,进入“待命”状态。
通过“你好,小智”唤起应用,然后程序处于“聆听中...”状态。
当我结束讲话后,小智进行回复,此时程序处于“说话中...”状态。
当它结束讲话后,又回到“聆听中...”状态。

WifiConfiguring、Idle、Listening、Speaking 都很直观。其他的状态暂时不管。

仔细看 Application::Start() 的实现,第一步就是获取硬件操作的句柄:

auto& board = Board::GetInstance();

这里的 Board 应该就是开发板相关的抽象了。看看 Board::GetInstance() 是怎么实现的:

    static Board& GetInstance() {
        static Board* instance = static_cast<Board*>(create_board());
        return *instance;
    }

Board 是个基类(或者说接口),create_board() 是一个通过宏 DECLARE_BOARD 定义的全局函数,用来创建具体的 Board 子类实例。这显然是一个工厂模式!通过这种方式将具体板级类的创建与使用分离。这样的设计让不同开发板的代码可以独立编译,实现了板级代码的模块化。

问题来了: 项目支持多种开发板,它是如何决定 create_board() 具体创建哪一个板子的实例呢?

再次指挥Copilot,解释 DECLARE_BOARD 的工作机制:

#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
    return new BOARD_CLASS_NAME(); \
}

(Copilot 解释)

  • Board::GetInstance() 调用 create_board() 创建实例。

  • 每个具体的板级实现(比如 http://lichuang_board.cc)会继承 Board 并使用 DECLARE_BOARD(LichuangBoard) 来定义自己的 create_board 函数。

  • 关键在于链接时:通过 CMake 配置 (CMakeLists.txt) 或 条件编译(预处理指令),控制最终只有一个 create_board 函数的实现被编译和链接进最终的固件里。

“CMake 配置... 条件编译...” 看到这里,脑子里“卧槽”一声!难道是这个原因?烧录了新的固件之后,屏幕不亮,是固件和开发板“不匹配”?程序以为自己运行在别的板子上,用了错误的 GPIO 引脚或者驱动去初始化屏幕?如果是这样的话......

柳暗花明:配置正确的开发板

赶紧回到 main/CMakeLists.txt 文件,果然看到了类似这样的条件编译逻辑:

elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV)
    set(BOARD_TYPE "lichuang-dev")

因为之前下载官方预编译固件时

文件名里看到过 lichuang-dev 的字样,所以我猜我需要设置的就是 CONFIG_BOARD_TYPE_LICHUANG_DEV 这个编译配置项。

那么,如何在编译时设置这个配置项呢?Copilot 给出了几种方式,推荐的方式还是用 menuconfig。

再次运行 idf.py menuconfig。这次就更有目的性了,仔细查找和“Board”相关的配置项。找了一圈,没看到直接叫 "Board Configuration" 的。但有一个菜单名字很特别:"Xiaozhi Assistant Configuration",隐约感觉这就是我要找的东西。

点进去一看,果然!里面赫然有 "Board Type" 的选项!当前显示的是默认的或其他板子类型。

进去看看。列表里找到了一个名字很像的:“立创实战派 ESP32-S3开发板” (虽然中文显示有点不清楚,但能认出来)。选中它,保存,退出 menuconfig。

决战时刻:重新编译与烧录

怀着忐忑而期待的心情,重新执行编译命令:

idf.py build

编译完成!

开始有点紧张了。这下屏幕该亮起来了吧?

再次让板子进入下载模式(按住 BOOT,按一下 RESET,松开 BOOT)。

深吸一口气,执行烧录命令:

idf.py -p COM3 flash

没报错!烧录完成!

按下 RESET 按钮...

哈哈哈哈!屏幕亮起来啦! 成功进入配网模式!

程序的固件版本号,从之前官方固件的 1.6.0 变成了 1.6.1 。这与刚刚通过源码编译的固件版本一致。

今日小结

今天真是收获巨大的一天:

  • 第一次尝试从源码编译小智AI固件,虽然遇到了“黑屏”问题。

  • 通过分析代码、借助 Copilot,理解了项目的板级抽象和配置机制。

  • 成功定位并解决了因未配置正确板型导致的黑屏问题。

  • 最终成功编译、烧录并运行了自己构建的固件!

这意味着,已经打通了从源码到硬件运行的全流程,为接下来深入研究代码、甚至尝试修改和定制功能铺平了道路。Nice!

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
  3. QQ打赏

    qrcode qq

评论区