【项目经验】小智ai源码学习记录
项目文件结构
拿到一个新的项目,要先了解文件结构,理清都有什么。
打开main文件夹能看到如下结构,其中,最重要的就是下方这四个文件。
main.cc是程序入口。
CMakeLists.txt是编译用到的配置文件,可以通过这个文件了解到项目都包含哪些文件。
idf_component.yml配置的是项目依赖的库,从下图能看到,编译后这些组件会被下载到managed_components文件夹下。
kconfig.projbuild是项目的配置文件,打开SDK配置编辑器就能看到相关的配置内容。
功能模块
就像分析一个函数一样,分析项目的功能也是类似的思路,先找到一个入口,不断深入,直到出口。小智ai接受一段音频,通过麦克风将数据给到esp32-s3,由esp32解析后通过协议发送到后端服务器,等接收到返回数据后再由喇叭和LCD屏幕输出。(也就是搞懂数据流)
我这里使用的是master分支(2025年7月16日)。基于面向对象的思想,整个项目被分为五大模块:板级模块boards、音频接受与发送audio_codecs、音频处理audio_processing、显示模块display和协议模块protocols。我们从主函数入手。
extern "C" void app_main(void)
{// Initialize the default event loopESP_ERROR_CHECK(esp_event_loop_create_default());// Initialize NVS flash for WiFi configurationesp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_LOGW(TAG, "Erasing NVS flash to fix corruption");ESP_ERROR_CHECK(nvs_flash_erase());ret = nvs_flash_init();}ESP_ERROR_CHECK(ret);// Launch the applicationApplication::GetInstance().Start();
主函数很简洁,做了三件事,初始化事件循环event_loop,初始化nvs_flash,调用Application的start()函数。进入start函数,能看到先创建了一个board,再由board创建display和codec
void Application::Start() {auto& board = Board::GetInstance();SetDeviceState(kDeviceStateStarting);/* Setup the display */auto display = board.GetDisplay();/* Setup the audio codec */auto codec = board.GetAudioCodec();......
}
抽象类Board实现了开发板级的抽象,这里使用了工厂模式,可以通过配置来实现在编译时,创建不同的开发板对象。
GetInstance()调用create_board()来创建开发板对象。
public:static Board& GetInstance() {static Board* instance = static_cast<Board*>(create_board());return *instance;}
而create_board()则根据DECLARE_BOARD的配置,去new了一个对象。
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \return new BOARD_CLASS_NAME(); \
}
打开SDK配置编辑器,找到Xiaozhi Assistant,修改Board Type选项,我这里选择的是面包板新版接线(WiFi),对应的是BOARD_TYPE_BREAD_COMPACT_WIFI。在Kconfig.projbuild文件能看到下面的内容。
choice BOARD_TYPEprompt "Board Type"default BOARD_TYPE_BREAD_COMPACT_WIFIhelpBoard type. 开发板类型config BOARD_TYPE_BREAD_COMPACT_WIFIbool "面包板新版接线(WiFi)"depends on IDF_TARGET_ESP32S3
之后在CMakeLists.txt文件内可以找到如下内容。
# 根据 BOARD_TYPE 配置添加对应的板级文件
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)set(BOARD_TYPE "bread-compact-wifi")
这里将BOARD_TYPE 配置成了bread-compact-wifi。我们可以到boards/bread-compact-wifi文件夹下的compact_wifi_board.cc文件内看到
DECLARE_BOARD(CompactWifiBoard);
再回到create_board(),就能知道这里执行的是new CompactWifiBoard(),创建了Board的实现类CompactWifiBoard。
后面创建codec和display都是执行的由CompactWifiBoard实现的方法。
virtual AudioCodec* GetAudioCodec() override {
#ifdef AUDIO_I2S_METHOD_SIMPLEXstatic NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN);
#elsestatic NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
#endifreturn &audio_codec;}virtual Display* GetDisplay() override {return display_;}
先来看看GetAudioCodec(),这个函数返回了AudioCodec对象,也就NoAudioCodecSimplex的父类。在这个项目中,这种实现方式很常见,无论是board还是codec等,都是先创建了一个抽象类,再去实现具体子类,对应文件夹下有很多子类实现。这就是面向对象的编程思想,C语言没有类的概念,也就没有继承和多态,得益于esp32的高性能,可以使用C++来实现应用层代码,大大提高了开发效率和代码的复用能力,当我们需要适配自己的开发板和外设时,只需要实现对应的子类,再修改一下配置,上层的应用代码根本不需要修改。
类NoAudioCodecSimplex的构造函数就很简单了,配置了I2S,初始化了扬声器和麦克风。
未完待续…