【项目经验】小智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 loop
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Initialize NVS flash for WiFi configuration
    esp_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 application
    Application::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_TYPE
    prompt "Board Type"
    default BOARD_TYPE_BREAD_COMPACT_WIFI
    help
        Board type. 开发板类型
    config BOARD_TYPE_BREAD_COMPACT_WIFI
        bool "面包板新版接线(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_SIMPLEX
        static 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);
#else
        static 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);
#endif
        return &audio_codec;
    }

    virtual Display* GetDisplay() override {
        return display_;
    }

先来看看GetAudioCodec(),这个函数返回了AudioCodec对象,也就NoAudioCodecSimplex的父类。在这个项目中,这种实现方式很常见,无论是board还是codec等,都是先创建了一个抽象类,再去实现具体子类,对应文件夹下有很多子类实现。这就是面向对象的编程思想,C语言没有类的概念,也就没有继承和多态,得益于esp32的高性能,可以使用C++来实现应用层代码,大大提高了开发效率和代码的复用能力,当我们需要适配自己的开发板和外设时,只需要实现对应的子类,再修改一下配置,上层的应用代码根本不需要修改。

类NoAudioCodecSimplex的构造函数就很简单了,配置了I2S,初始化了扬声器和麦克风。

未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值