小智AI机器人 - 代码框架梳理2
文章目录
- 前言
- 1.简介
- 2. main.cc: 应用程序的入口
- 3. application.h/cc:主应用逻辑实现
- Application 类
- Application.c中的功能
- 4. background_task.h/cc:后台任务处理
- 1. 功能定位差异
- 2. 实现机制对比
- 任务队列与资源管理
- 执行模式
- 3 应用场景示例
- background_task 典型用例
- application 典型用例
- 5. settings.h/cc:配置参数管理
- 6. system_info.h/cc:系统信息管理
前言
本文主要是第2篇关于小智AI的文章,我想在这里和大家一起梳理下小智AI的核心应用部分的代码。
核心应用是只main下面直属的那些文件,通过了解核心应用部分的代码能够帮助我们理解小智AI是怎么进行启动的,以及其整体的工作机制大概是怎么样的。
前面的文章:
小智AI机器人 - 代码框架梳理1
小智AI gitcode地址(这个访问起来比较快):https://gitcode.com/gh_mirrors/xia/xiaozhi-esp32
1.简介
我们这里说的核心应用文件是指main
目录下不在其它子文件夹的文件。
一般情况下我们是对某个子模块单独创建一个文件夹,所以在子文件夹中的大都是某个模块的内容,而直接在main
目录下的为核心内容。
但是这里有个例外是把ota
也放到了main
目录下,没有单独存放到一个文件夹下方,实际上再创建个ota
文件夹,把ota
的内容放进去更合适。这里不放进去的原因我觉得有一部分是,作者在制定软件规范时,规定模块与模块之间不能相互调用,但是ota
过程中可能会用到一些其它模块的内容,例如联网检查版本,拉取升级文件等等,所以如果把ota
放到一个单独的ota
模块中的话,因为其会调用其它组件,所以就不符合组件之间的解耦设计规范了。
我们能够看到ota
是只有application
中会去调用的,但是ota
内部的话则是会去调用很多其他组件的东西。
2. main.cc: 应用程序的入口
上面我们可以看到这个文件中,只有一个app_main()
,看到这里很多人可能会有疑问,为啥不是main
呢?难道代码不是从这里开始运行的吗?
确实不是从这里开始运行的,真正的main
在esp_idf
的框架中。乐鑫应该是为了方便大家后续的开发,所以专门设计了这样的一个框架,该框架内有个main()
函数,它会负责初始化各种所需的硬件和组件。
然后创建一个线程,该线程的回调函数中最终负责调用app_main()
。
简单来说大家可以把我们的应用也当作一个esp_idf
框架中的组件,这个组件叫做应用组件。框架会负责调用到我们的应用组件,调用到之后怎么处理就是我们自己说的算了。
本项目中app_main的作用
在这里app_main
也只是整个应用的一个过渡,小智的代码整体上采用C++结合面向对象的方式进行开发。
我们可以看到,最终是创建了一个 应用的application
对象,并启动这个对象。大家可以理解app_main
启动了一个叫做application
的小机器人,然后点一下这个小机器人的start,它就开始慢慢的工作了
3. application.h/cc:主应用逻辑实现
application
中放置的是如何创建一个application
对象,以及使用该对象所具备的能力。
对于该项目来说,其它的代码都是组件,如何将这些组件配合使用起来,连接起来就是由application对象所负责的。
看到这里估计大家就对application有了一个大概的认识。
接下来我们看一下代码层面的内容
Application 类
public部分
public
是一个对象所提供的外部引用该对象时,能够调用的接口。
一般情况下我们可以理解为该对象告知到外部自己有哪些能力以及允许外部所使用的能力。
本来在我的设想中,Application 应该属于整个框架的最上层,除了app_main能够调用它,其它组件内的东西应该是不允许调用到它的,所以它对外提供的能力应该只有start就够了。
但是实际上我们看对外提供的能力还是很多的。
我们可以看下除了main到底哪里在调用applicaiton
看来不光是application调用了这些组件,这些组件也在使用application提供的能力啊。
private部分
这些就是只有对象所实现的函数内部可以调用的,相当于咱们用C写的,在.c文件里面标明static的部分。
Application.c中的功能
application
具体有哪些功能在其接口处已经体现出来了。
然后就是它作为应用所承担的,例如初始化组件、进行基础的配置、让整个应用跑起来啊之类的。
我们把Application:Start
的注释提取出来能够更加直观的看到其具备哪些功能
Application:Start()
{/* Setup the display *//* Setup the audio codec *//* Wait for the network to be ready */// Check for new firmware version or get the MQTT broker address// Initialize the protocol// Wait for the new version check to finish// Enter the main event loop }
最后的这个*Enter the main event loop还是比较重要的
我们可以看到start后最终进入到了MainEventLoop,这里是一个专门等待接收SCHEDULE_EVENT
事件的死循环,在有SCHEDULE_EVENT
事件触发时会执行tasks列表中的所有的任务,当没有SCHEDULE_EVENT
事件触发时,则会阻塞等待。
xiaozhi AI的业务,一部分是靠esp_idf的定时器去运行的,这里的定时器实际上是创建了一个专门的线程去执行定时器的调度。一部分是借助application的schdule和background_task的schdule进行一些业务的后台运行
只不过这个线程的创建这里使用的是一个特殊的任务创建函数xTaskCreatePinnedToCore
,它是 ESP-IDF 中用于创建 FreeRTOS 任务并绑定到指定 CPU 核心的核心 API,通过这种任务绑定机制,开发者可充分利用 ESP32 双核特性实现硬实时系统设计。(如果有两个CPU核,这种做法可以提供CPU的利用率和整体运行效率)
4. background_task.h/cc:后台任务处理
background + task顾名思义就是后台任务处理。
它和application中所实现的schedule实际上有类似的功能,都是后台任务处理或者说异步处理。
不过两者之间仍然有一些差异。
1. 功能定位差异
模块 | 核心功能 | 技术特性 |
---|---|---|
background_task | 管理低优先级后台任务(如日志写入、传感器数据缓存) | 异步批量处理,允许任务延迟执行 |
application | 处理高实时性核心业务(如语音交互响应、设备控制指令) | 同步/高优先级响应,硬实时要求 |
2. 实现机制对比
任务队列与资源管理
-
background_task
- 队列结构:使用
std::list
存储任务,通过std::mutex
和std::condition_variable
实现线程安全。 - 资源检查:任务添加前检查活跃任务数(默认上限30)和剩余内存(低于10KB触发警告),防止资源耗尽。
- 代码片段:
void BackgroundTask::Schedule(std::function<void()> callback) {std::lock_guard<std::mutex> lock(mutex_);if (active_tasks_ >= 30 && heap_caps_get_free_size(MALLOC_CAP_INTERNAL) < 10000) {ESP_LOGW(TAG, "资源不足,拒绝新任务");}main_tasks_.emplace_back([this, cb = std::move(callback)]() { /*...*/ }); }
- 队列结构:使用
-
application
- 事件驱动:通过FreeRTOS的
xEventGroupSetBits
触发任务执行,结合xEventGroupWaitBits
实现非阻塞等待。 - 实时性保障:任务立即执行,无批量处理延迟,适合毫秒级响应场景(如语音唤醒中断)。
- 事件驱动:通过FreeRTOS的
执行模式
维度 | background_task | application |
---|---|---|
任务触发 | 条件变量唤醒(condition_variable_ ) | 事件组标志位触发(SCHEDULE_EVENT ) |
执行频率 | 批量处理(每次循环处理所有排队任务) | 单任务即时执行 |
优先级 | 低(通常绑定到Core 0,优先级1-3) | 高(绑定到Core 1,优先级5-24) |
3 应用场景示例
background_task 典型用例
- 日志记录:异步写入SD卡或发送至服务器,避免阻塞主线程。
- 传感器数据聚合:周期性读取温湿度传感器,缓存数据后批量处理。
- 网络心跳包维护:定时发送MQTT心跳,防止连接超时。
application 典型用例
- 语音交互:实时处理麦克风输入,调用ASR(语音识别)和LLM(大模型)生成响应。
- 设备控制:立即执行“开灯”、“调节温度”等指令,通过GPIO操作硬件。
- 中断响应:处理物理按钮的单击/长按事件,触发配网或状态切换。
5. settings.h/cc:配置参数管理
system_info中提供的接口,主要是围绕管理保存到flash中的各种配置信息所设计的。
有各种获取和擦除配置项信息的功能。例如保存和修改或者删除wifi信息。
6. system_info.h/cc:系统信息管理
上面提供的各种接口都是用于获取系统信息的,例如我们可以知道通过调用GetFreeHeapSize()
当前还剩下多少堆内存,
通过调用GetMacAddress()
获取mac地址,进而生成密钥或者用于发送消息时填充mac地址。