小智ESP32代码(1):软件框架

这两天阅读了小智ESP32客户端的main.cc、application.cc和application.h这三个文件,从这些文件可以看出系统大致的框架。

目录结构

xiaozhi-esp32/
├── CMakeLists.txt                   # CMake构建配置
├── docs/                            # 文档目录
├── main/                            # 主要源代码目录
│   ├── application.cc               # 主应用程序
│   ├── application.h                # 主应用程序头文件
│   ├── assets/                      # 资源文件目录
│   ├── audio/                       # 音频处理模块
│   ├── boards/                      # 开发板支持目录
│   ├── display/                     # 显示模块
│   ├── led/                         # LED控制模块
│   └── protocols/                   # 通信协议模块
├── partitions/                      # 分区表配置目录
└── scripts/                         # 工具脚本目录

整体框架概览:分层与核心控制模式

该项目采用 "入口层 + 核心控制层 + 功能模块" 的三层架构:

  • 入口层(main.cc):负责系统初始化与启动,是程序的起点
  • 核心控制层(Application类):通过Application类实现全局状态管理、模块协调与事件调度
  • 功能模块层(音频/显示/网络等):音频服务、显示驱动、网络协议等具体功能实现

这种架构的核心优势在于解耦:核心控制层不关心具体模块的实现细节,仅通过接口进行调度,便于模块替换与功能扩展。

入口层解析:main.cc的启动流程

main.cc是 ESP-IDF 框架规定的程序入口,其代码如下:

extern "C" void app_main(void) {
    // 1. 初始化系统事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // 2. 初始化NVS(非易失性存储)
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 3. 启动应用核心
    auto& app = Application::GetInstance();
    app.Start();
    app.MainEventLoop();
}
关键技术点解析
  • extern "C"
    使用extern "C"告诉编译器, 其修饰的app_main函数是按照 C 语言的规则进行编译和链接的,而不是 C++ 的规则。这样,使其能被 ESP32 的 C 语言启动代码正确识别和调用。
  • app_main函数
    不同于标准 C 的main函数,ESP-IDF 使用app_main作为应用入口,由框架在 FreeRTOS 任务中调用,默认栈大小可通过 menuconfig 配置。
  • 系统事件循环
    esp_event_loop_create_default()创建的是 ESP-IDF 的系统事件循环,用于处理 WiFi、蓝牙等系统级事件。该循环与Application类中的event_group_形成两层事件机制:
    • 系统事件循环:处理底层硬件 / 协议栈事件(如 WiFi 连接状态变化)
    • 应用事件组:处理业务逻辑事件(如 "唤醒词检测"、"音频发送")
  • NVS 初始化
    NVS(Non-Volatile Storage)是 ESP32 的非易失性存储系统,用于保存 WiFi 配置、设备状态等需要断电保留的数据。
  • Application 实例启动
    通过Application::GetInstance()获取单例实例,调用Start()完成模块初始化,最后进入MainEventLoop()处理业务事件 —— 这标志着系统从 "启动阶段" 进入 "运行阶段"。

核心控制层:Application类的设计与实现

Application类是整个系统的 "大脑",采用单例模式实现全局唯一实例,负责协调所有功能模块。

1. 单例模式的实现与必要性
class Application {
public:
    static Application& GetInstance() {
        static Application instance; // 局部静态变量保证线程安全(C++11后)
        return instance;
    }
    // 禁止拷贝与赋值,确保实例唯一
    Application(const Application&) = delete;
    Application& operator=(const Application&) = delete;
    // ...
private:
    Application(); // 私有构造函数,防止外部实例化
    ~Application();
};

在嵌入式系统中使用单例的必要性:

  • 硬件资源唯一性:如音频 codec、显示屏等硬件接口只能被一个实例管理,避免并发冲突
  • 全局状态一致性:设备状态(如 "升级中"、"空闲")需要全局唯一的管理者,防止状态混乱
  • 资源节约:在 RAM 有限的 ESP32 中,重复创建对象会浪费资源
2. 核心成员变量

Application类通过成员变量维护系统核心资源与状态,关键变量解析如下:

成员变量 类型 作用解析
device_state_ volatile DeviceState 设备状态枚举(如kDeviceStateIdle、kDeviceStateUpgrading),使用volatile确保多任务可见性
event_group_ EventGroupHandle_t FreeRTOS 事件组,用于多任务间事件同步(如MAIN_EVENT_WAKE_WORD_DETECTED)
main_tasks_ std::deque<std::function<void()>> 主线程任务队列,通过Schedule()方法提交任务,实现异步操作串行化
protocol_ std::unique_ptr 网络协议接口(MQTT/WebSocket),采用智能指针管理生命周期
audio_service_ AudioService 音频服务实例,处理录音、播放、唤醒词检测等
clock_timer_handle_ esp_timer_handle_t 系统定时器,用于周期性任务(如OnClockTimer()回调)
3. 核心方法解析

主事件循环:MainEventLoop()

void Application::MainEventLoop() {
    while (true) {
        // 等待事件触发(超时等待,单位为tick)
        EventBits_t bits = xEventGroupWaitBits(event_group_, 
            MAIN_EVENT_SCHEDULE | MAIN_EVENT_SEND_AUDIO | ..., // 关注的事件掩码
            pdTRUE, // 处理后清除事件位
            pdFALSE, // 不需要等待所有事件,任意一个即可
            portMAX_DELAY); // 无限等待

        // 处理"任务调度"事件
        if (bits & MAIN_EVENT_SCHEDULE) {
            std::lock_guard<std::mutex> lock(mutex_);
            while (!main_tasks_.empty()) {
                auto task = main_tasks_.front();
                main_tasks_.pop_front();
                task(); // 执行任务
            }
        }

        // 处理其他事件(如唤醒词检测、音频发送等)
        // ...
    }
}

模块交互与通信机制

Application类与各功能模块的交互通过两种核心机制实现:

1. 事件组(event_group_)

基于 FreeRTOS 的EventGroupHandle_t,用于模块间的异步通知:

  • 发送事件:xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
  • 等待事件:xEventGroupWaitBits(...)(如MainEventLoop()中)
2. 任务调度(main_tasks_)

通过Schedule()方法提交任务到主线程队列,实现异步操作串行化:

void Application::Schedule(std::function<void()> callback) {
    std::lock_guard<std::mutex> lock(mutex_);
    main_tasks_.push_back(callback); // 将待处理的callback压栈,该callback即将在MainEventLoop()中处理
    xEventGroupSetBits(event_group_, MAIN_EVENT_SCHEDULE); // 触发调度事件
}

举例:

    protocol_->OnAudioChannelClosed([this, &board]() {
        board.SetPowerSaveMode(true);
        Schedule([this]() {
            auto display = Board::GetInstance().GetDisplay();
            display->SetChatMessage("system", "");
            SetDeviceState(kDeviceStateIdle);
        });  // Schedule()中的lambda表达式(匿名函数)被压入main_tasks_栈,通过置位MAIN_EVENT_SCHEDULE位,触发在MainEventLoop()中执行该lambda表达式(匿名函数)
    });

用AI看代码,用AI写文档,有AI真方便啊!

posted @ 2025-08-23 17:50  icuic  阅读(130)  评论(0)    收藏  举报