当前位置: 首页 > news >正文

政安晨【开源人工智能硬件】【ESP乐鑫篇】 —— 详细分享小智(78/xiaozhi-esp32)AI终端开源硬件的嵌入式开发经验笔记

政安晨的个人主页:政安晨

欢迎 👍点赞✍评论⭐收藏

希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正!

 这是笔者尝试应用小智AI终端的经验之谈,也算是一次基于小智AI终端应用开发的复盘。

目录

评价一下小智项目的意义

经验一:选好合适的IDF版本

经验二:了解其软件架构

经验三:系统崩溃的时候你不要崩溃

结束语


小智AI(ESP32)的项目地址:https://github.com/78/xiaozhi-esp32https://github.com/78/xiaozhi-esp32

评价一下小智项目的意义

小智AI项目的技术架构特点

小智AI项目以低成本、高效率的ESP系列芯片为核心,结合轻量化AI算法,实现了语音交互、设备控制等智能化功能。其技术栈包括ESP-IDF开发框架、Opus组件、MCP服务组件等工具链,显著降低了AI模型在微控制器上的部署门槛。

对硬件选型的借鉴价值

ESP芯片的Wi-Fi/蓝牙双模集成、低功耗特性与小智AI的场景需求高度契合。项目验证了ESP32-C3等型号在实时性要求较高的语音处理任务中的可行性,为同类产品提供了硬件选型参考,尤其适合智能家居、可穿戴设备等低成本场景。

算法优化与边缘计算的实践意义

小智AI通过模型量化、剪枝等技术将AI模型应用压缩,解决了ESP芯片内存限制问题。这一方案证明边缘端AI推理可在资源受限设备上运行,减少了云端依赖,为隐私敏感型应用(如家庭安防)提供了技术路径。

开发效率与生态整合的启示

项目采用模块化设计,将传感器驱动、通信协议等封装为可复用组件,缩短了开发周期。其开源社区贡献的代码库(如语音唤醒固件)可直接适配同类ESP产品,加速了开发者生态的协同创新。

成本控制与量产可行性

小智AI的BOM成本控制在20美元以内,证明了ESP芯片在大规模量产中的经济性优势。其硬件设计(如PCB天线优化方案)和OTA升级机制,为智能硬件产品的可维护性提供了标准化模板。

用户场景适配的创新思路

项目通过离线语音指令识别和场景联动规则引擎,解决了网络不稳定环境下的用户体验问题。这种“轻智能”设计对农业物联网、工业边缘节点等特殊场景具有参考价值。

(注:以上分析基于公开技术文档及ESP官方案例,实际开发需结合实际情况。)

小智AI框架的出现帮助广大开发者打通了乐鑫方案产品化的快捷路径,但想要完全掌握小智AI终端框架却不是那么容易,对开发者还是有一定的门槛要求:

1. 对C/C++语言要熟练掌握(如果仅靠AI编程帮你解决嵌入式智能硬件开发工作,那恐怕你会踩很多坑。)

2. 要基本了解MCU(含RTOS系统)的基本运行原理。

3.要懂一点硬件电路的知识,包括射频通信等。

4. 要有产品化思维,知道怎样测试、怎样发现问题、怎样解决问题。

等等。

小智的开源项目中有一个关于MCP协议多终端控制的语音交互入口图:

基本操作本篇经验笔记中不多谈,我们将在这里重点关注某些相对比较关键的问题。

经验一:选好合适的IDF版本

小智的不同版本对IDF的本版要求是对应的,代码更新迭代也比较快,这部分要多留意。

## IDF Component Manager Manifest File
dependencies:waveshare/esp_lcd_sh8601: 1.0.2espressif/esp_lcd_ili9341: ==1.2.0espressif/esp_lcd_gc9a01: ==2.0.1espressif/esp_lcd_st77916: ^1.0.1espressif/esp_lcd_axs15231b: ^1.0.0espressif/esp_lcd_st7796:version: 1.3.2rules:- if: target not in [esp32c3]espressif/esp_lcd_spd2010: ==1.0.2espressif/esp_io_expander_tca9554: ==2.0.0espressif/esp_lcd_panel_io_additions: ^1.0.178/esp_lcd_nv3023: ~1.0.078/esp-wifi-connect: ~2.4.378/esp-opus-encoder: ~2.4.078/esp-ml307: ~3.2.578/xiaozhi-fonts: ~1.3.2espressif/led_strip: ^2.5.5espressif/esp_codec_dev: ~1.3.6espressif/esp-sr: ~2.1.1espressif/button: ~4.1.3espressif/knob: ^1.0.0espressif/esp32-camera: ^2.0.15espressif/esp_lcd_touch_ft5x06: ~1.0.7espressif/esp_lcd_touch_gt911: ^1waveshare/esp_lcd_touch_cst9217: ^1.0.3espressif/esp_lcd_touch_cst816s: ^1.0.6lvgl/lvgl: ~9.2.2esp_lvgl_port: ~2.6.0espressif/esp_io_expander_tca95xx_16bit: ^2.0.0espressif2022/image_player: ==1.1.0~1espressif2022/esp_emote_gfx: ^1.0.0espressif/adc_mic: ^0.2.0espressif/esp_mmap_assets: '>=1.2'txp666/otto-emoji-gif-component: ~1.0.2espressif/adc_battery_estimation: ^0.2.0# SenseCAP Watcher Boardwvirgil123/esp_jpeg_simd:version: 1.0.0rules:- if: target in [esp32s3]wvirgil123/sscma_client:version: 1.0.2rules:- if: target in [esp32s3]tny-robotics/sh1106-esp-idf:version: ^1.0.0rules:- if: idf_version >= "5.4.0"waveshare/esp_lcd_jd9365_10_1:version: '*'rules:- if: target in [esp32p4]waveshare/esp_lcd_st7703:version: '*'rules:- if: target in [esp32p4]espressif/esp_lcd_ili9881c:version: ^1.0.1rules:- if: target in [esp32p4]espressif/esp_hosted:version: '2.0.17'rules:- if: target in [esp32h2, esp32p4]espressif/esp_wifi_remote:version: '*'rules:- if: target in [esp32p4]espfriends/servo_dog_ctrl:version: ^0.1.8rules:- if: target in [esp32c3]## Required IDF versionidf:version: '>=5.4.0'

基于自己硬件的情况在boards文件夹下创建型号产品,你可以参考box开发板的配置。

这个里面的坑也还是属于基本操作,自己探索。

IO口配置正确,电池电量的读取,顺着代码往下看,看不懂不要急,从app.Start()开始看:

    // Launch the applicationauto& app = Application::GetInstance();app.Start();app.MainEventLoop();
}

经验二:了解其软件架构

当你仔细阅读Application::Start()的时候你会发现,整套软件好像都是用c++写的,如果你以前是熟悉C的,你可能不太适应。其实,你再仔细看一下就会发现:C++仅是用来对应用方法的封装,核心实现还是基于C的。

void Application::Start() {auto& board = Board::GetInstance();SetDeviceState(kDeviceStateStarting);/* Setup the display */auto display = board.GetDisplay();/* Setup the audio service */auto codec = board.GetAudioCodec();audio_service_.Initialize(codec);audio_service_.Start();AudioServiceCallbacks callbacks;callbacks.on_send_queue_available = [this]() {xEventGroupSetBits(event_group_, MAIN_EVENT_SEND_AUDIO);};callbacks.on_wake_word_detected = [this](const std::string& wake_word) {xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);};callbacks.on_vad_change = [this](bool speaking) {xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE);};audio_service_.SetCallbacks(callbacks);/* Start the clock timer to update the status bar */esp_timer_start_periodic(clock_timer_handle_, 1000000);/* Wait for the network to be ready */board.StartNetwork();// Update the status bar immediately to show the network statedisplay->UpdateStatusBar(true);// Check for new firmware version or get the MQTT broker addressOta ota;CheckNewVersion(ota);// Initialize the protocoldisplay->SetStatus(Lang::Strings::LOADING_PROTOCOL);// Add MCP common tools before initializing the protocolMcpServer::GetInstance().AddCommonTools();if (ota.HasMqttConfig()) {protocol_ = std::make_unique<MqttProtocol>();} else if (ota.HasWebsocketConfig()) {protocol_ = std::make_unique<WebsocketProtocol>();} else {ESP_LOGW(TAG, "No protocol specified in the OTA config, using MQTT");protocol_ = std::make_unique<MqttProtocol>();}protocol_->OnNetworkError([this](const std::string& message) {last_error_message_ = message;xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR);});protocol_->OnIncomingAudio([this](std::unique_ptr<AudioStreamPacket> packet) {if (device_state_ == kDeviceStateSpeaking) {audio_service_.PushPacketToDecodeQueue(std::move(packet));}});protocol_->OnAudioChannelOpened([this, codec, &board]() {board.SetPowerSaveMode(false);if (protocol_->server_sample_rate() != codec->output_sample_rate()) {ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion",protocol_->server_sample_rate(), codec->output_sample_rate());}});protocol_->OnAudioChannelClosed([this, &board]() {board.SetPowerSaveMode(true);Schedule([this]() {auto display = Board::GetInstance().GetDisplay();display->SetChatMessage("system", "");SetDeviceState(kDeviceStateIdle);});});protocol_->OnIncomingJson([this, display](const cJSON* root) {// Parse JSON dataauto type = cJSON_GetObjectItem(root, "type");if (strcmp(type->valuestring, "tts") == 0) {auto state = cJSON_GetObjectItem(root, "state");if (strcmp(state->valuestring, "start") == 0) {Schedule([this]() {aborted_ = false;if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) {SetDeviceState(kDeviceStateSpeaking);}});} else if (strcmp(state->valuestring, "stop") == 0) {Schedule([this]() {if (device_state_ == kDeviceStateSpeaking) {if (listening_mode_ == kListeningModeManualStop) {SetDeviceState(kDeviceStateIdle);} else {SetDeviceState(kDeviceStateListening);}}});} else if (strcmp(state->valuestring, "sentence_start") == 0) {auto text = cJSON_GetObjectItem(root, "text");if (cJSON_IsString(text)) {ESP_LOGI(TAG, "<< %s", text->valuestring);Schedule([this, display, message = std::string(text->valuestring)]() {display->SetChatMessage("assistant", message.c_str());});}}} else if (strcmp(type->valuestring, "stt") == 0) {auto text = cJSON_GetObjectItem(root, "text");if (cJSON_IsString(text)) {ESP_LOGI(TAG, ">> %s", text->valuestring);Schedule([this, display, message = std::string(text->valuestring)]() {display->SetChatMessage("user", message.c_str());});}} else if (strcmp(type->valuestring, "llm") == 0) {auto emotion = cJSON_GetObjectItem(root, "emotion");if (cJSON_IsString(emotion)) {Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {display->SetEmotion(emotion_str.c_str());});}} else if (strcmp(type->valuestring, "mcp") == 0) {auto payload = cJSON_GetObjectItem(root, "payload");if (cJSON_IsObject(payload)) {McpServer::GetInstance().ParseMessage(payload);}} else if (strcmp(type->valuestring, "system") == 0) {auto command = cJSON_GetObjectItem(root, "command");if (cJSON_IsString(command)) {ESP_LOGI(TAG, "System command: %s", command->valuestring);if (strcmp(command->valuestring, "reboot") == 0) {// Do a reboot if user requests a OTA updateSchedule([this]() {Reboot();});} else {ESP_LOGW(TAG, "Unknown system command: %s", command->valuestring);}}} else if (strcmp(type->valuestring, "alert") == 0) {auto status = cJSON_GetObjectItem(root, "status");auto message = cJSON_GetObjectItem(root, "message");auto emotion = cJSON_GetObjectItem(root, "emotion");if (cJSON_IsString(status) && cJSON_IsString(message) && cJSON_IsString(emotion)) {Alert(status->valuestring, message->valuestring, emotion->valuestring, Lang::Sounds::P3_VIBRATION);} else {ESP_LOGW(TAG, "Alert command requires status, message and emotion");}
#if CONFIG_RECEIVE_CUSTOM_MESSAGE} else if (strcmp(type->valuestring, "custom") == 0) {auto payload = cJSON_GetObjectItem(root, "payload");ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root));if (cJSON_IsObject(payload)) {Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() {display->SetChatMessage("system", payload_str.c_str());});} else {ESP_LOGW(TAG, "Invalid custom message format: missing payload");}
#endif} else {ESP_LOGW(TAG, "Unknown message type: %s", type->valuestring);}});bool protocol_started = protocol_->Start();SetDeviceState(kDeviceStateIdle);has_server_time_ = ota.HasServerTime();if (protocol_started) {std::string message = std::string(Lang::Strings::VERSION) + ota.GetCurrentVersion();display->ShowNotification(message.c_str());display->SetChatMessage("system", "");// Play the success sound to indicate the device is readyaudio_service_.PlaySound(Lang::Sounds::P3_SUCCESS);}// Print heap statsSystemInfo::PrintHeapStats();
}

小智这套软件的基本思想是将业务逻辑剥离出来,在main文件夹的一级目录中,这些文件主要有:

application.cc

mcp_server.cc

ota.cc

核心应用业务逻辑就在上述3个文件中,再在audio文件夹中实现音频处理的逻辑,diplay文件夹中实现显示处理的逻辑,protocols文件夹中实现通信逻辑的应用封装,并尽可能地把硬件有关的变化及操作封闭在boards中,这就是最主要的思想,并用面向对象的类和方法固化下来,形成可扩展的基本框架。

 在这个基础上,在各自业务逻辑中,类的方法里调用乐鑫IDF的接口(C语言实现),发现IDF的某些接口无法适配,或者第三方协议栈或库更好用的时候,使用外置组件封装,这些都在managed_componets中,其中有乐鑫开发的,也有78(虾哥)开发的,大家看代码的时候带着managed_componets看是有益处的。

其实,架构说到这里,就基本是全部了,对于懂得小伙伴,上面两段话很重要。接下来就是一些实践经验了。

经验三:系统崩溃的时候你不要崩溃

开发嵌入式系统,尤其是基于开源框架开发嵌入式系统,内存泄露等引起的系统崩溃是家常便饭,大家要习惯和适应,刚开始的时候可以将ESP的调试功能打开,观察内存泄露的过程,比如引用的空handle,重复释放内存,临时数组在新的进程周期内失效等,都可能是你经常面临的事情。

尤其是这套框架迭代周期快,BUG一定是层出不穷的,不要慌。

使用小智的开源项目时,尽量下载打了版本tag的代码(如 v x.x.x )这样的代码经过了基本测试,固化了版本,相对比较可靠。

同时,手头要有至少2个版本的软件做对比测试,出问题的时候,跑一跑上一版软件试试看。

小智的新版服务地址是从OTA接口拿的:


bool Ota::CheckVersion() {auto& board = Board::GetInstance();auto app_desc = esp_app_get_description();// Check if there is a new firmware version availablecurrent_version_ = app_desc->version;ESP_LOGI(TAG, "Current version: %s", current_version_.c_str());std::string url = GetCheckVersionUrl();if (url.length() < 10) {ESP_LOGE(TAG, "Check version URL is not properly set");return false;}auto http = SetupHttp();//http->SetTimeout(10000); // 10秒超时//add by zachenstd::string data = board.GetJson();std::string method = data.length() > 0 ? "POST" : "GET";http->SetContent(std::move(data));if (!http->Open(method, url)) {ESP_LOGE(TAG, "Failed to open HTTP connection");return false;}vTaskDelay(pdMS_TO_TICKS(200));// add by zachenauto status_code = http->GetStatusCode();if (status_code != 200) {ESP_LOGE(TAG, "Failed to check version, status code: %d", status_code);http->Close(); //add by zachenreturn false;}data = http->ReadAll();http->Close();///* //zachen// 添加空数据检查if (data.empty()) {ESP_LOGE(TAG, "Received empty response from server");return false;}ESP_LOGI(TAG, "Received response: %s", data.c_str());//*/// Response: { "firmware": { "version": "1.0.0", "url": "http://" } }// Parse the JSON response and check if the version is newer// If it is, set has_new_version_ to true and store the new version and URLcJSON *root = cJSON_Parse(data.c_str());if (root == NULL) {ESP_LOGE(TAG, "Failed to parse JSON response");return false;}has_activation_code_ = false;has_activation_challenge_ = false;cJSON *activation = cJSON_GetObjectItem(root, "activation");if (cJSON_IsObject(activation)) {cJSON* message = cJSON_GetObjectItem(activation, "message");if (cJSON_IsString(message)) {activation_message_ = message->valuestring;}cJSON* code = cJSON_GetObjectItem(activation, "code");if (cJSON_IsString(code)) {activation_code_ = code->valuestring;has_activation_code_ = true;}cJSON* challenge = cJSON_GetObjectItem(activation, "challenge");if (cJSON_IsString(challenge)) {activation_challenge_ = challenge->valuestring;has_activation_challenge_ = true;}cJSON* timeout_ms = cJSON_GetObjectItem(activation, "timeout_ms");if (cJSON_IsNumber(timeout_ms)) {activation_timeout_ms_ = timeout_ms->valueint;}}has_mqtt_config_ = false;cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt");if (cJSON_IsObject(mqtt)) {Settings settings("mqtt", true);cJSON *item = NULL;cJSON_ArrayForEach(item, mqtt) {if (item != NULL && item->string != NULL) {if (cJSON_IsString(item)) {if (settings.GetString(item->string) != item->valuestring) {settings.SetString(item->string, item->valuestring);}} else if (cJSON_IsNumber(item)) {if (settings.GetInt(item->string) != item->valueint) {settings.SetInt(item->string, item->valueint);}}}}has_mqtt_config_ = true;} else {ESP_LOGI(TAG, "No mqtt section found !");}has_websocket_config_ = false;cJSON *websocket = cJSON_GetObjectItem(root, "websocket");if (cJSON_IsObject(websocket)) {Settings settings("websocket", true);cJSON *item = NULL;cJSON_ArrayForEach(item, websocket) {if (item != NULL && item->string != NULL) {if (cJSON_IsString(item)) {if (settings.GetString(item->string) != item->valuestring) {settings.SetString(item->string, item->valuestring);}} else if (cJSON_IsNumber(item)) {if (settings.GetInt(item->string) != item->valueint) {settings.SetInt(item->string, item->valueint);}}}}has_websocket_config_ = true;} else {ESP_LOGI(TAG, "No websocket section found!");}has_server_time_ = false;cJSON *server_time = cJSON_GetObjectItem(root, "server_time");if (cJSON_IsObject(server_time)) {cJSON *timestamp = cJSON_GetObjectItem(server_time, "timestamp");cJSON *timezone_offset = cJSON_GetObjectItem(server_time, "timezone_offset");if (cJSON_IsNumber(timestamp)) {// 设置系统时间struct timeval tv;double ts = timestamp->valuedouble;// 如果有时区偏移,计算本地时间if (cJSON_IsNumber(timezone_offset)) {ts += (timezone_offset->valueint * 60 * 1000); // 转换分钟为毫秒}tv.tv_sec = (time_t)(ts / 1000);  // 转换毫秒为秒tv.tv_usec = (suseconds_t)((long long)ts % 1000) * 1000;  // 剩余的毫秒转换为微秒settimeofday(&tv, NULL);has_server_time_ = true;}} else {ESP_LOGW(TAG, "No server_time section found!");}has_new_version_ = false;cJSON *firmware = cJSON_GetObjectItem(root, "firmware");if (cJSON_IsObject(firmware)) {cJSON *version = cJSON_GetObjectItem(firmware, "version");if (cJSON_IsString(version)) {firmware_version_ = version->valuestring;}cJSON *url = cJSON_GetObjectItem(firmware, "url");if (cJSON_IsString(url)) {firmware_url_ = url->valuestring;}if (cJSON_IsString(version) && cJSON_IsString(url)) {// Check if the version is newer, for example, 0.1.0 is newer than 0.0.1has_new_version_ = IsNewVersionAvailable(current_version_, firmware_version_);if (has_new_version_) {ESP_LOGI(TAG, "New version available: %s", firmware_version_.c_str());} else {ESP_LOGI(TAG, "Current is the latest version");}// If the force flag is set to 1, the given version is forced to be installedcJSON *force = cJSON_GetObjectItem(firmware, "force");if (cJSON_IsNumber(force) && force->valueint == 1) {has_new_version_ = true;}}} else {ESP_LOGW(TAG, "No firmware section found!");}cJSON_Delete(root);return true;
}

这个接口在初始化的时候如果有时候出现死机,可以考虑像我上面的代码那样增加一个延时,基本可以解决OTA初始化死机问题。


结束语

时间有限,我先分享这些经验,今后会陆续在这篇文章中更新小智AI嵌入式终端开发中的相关经验。

谢谢各位小伙伴的阅览,祝工作愉快。(你热爱它,它就是一件很好玩的事。嘻嘻。)

http://www.dtcms.com/a/316114.html

相关文章:

  • C语言---文件操作
  • 上传文件至华为云OBS
  • 分布式微服务--Nacos 集群部署
  • 【CTF】命令注入绕过技术专题:变量比较与逻辑运算
  • Spring Boot 整合 Thymeleaf
  • 【qt5_study】1.Hello world
  • 中国地级及以上城市人均GDP数据集(1990-2022年)
  • 【运动控制框架】WPF运动控制框架源码,可用于激光切割机,雕刻机,分板机,点胶机,插件机等设备,开箱即用
  • 37.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--增加Github Action
  • 400V降24V,200mA,应用领域:从生活到工业的 “全能电源管家”
  • Windows 11 使用Windows Hello使用人脸识别登录失败,重新录入人脸识别输入PIN后报Windows Hello安装程序白屏无响应的问题解决
  • LeetCode347.前K个高频元素(hash表+桶排序)
  • scikit-learn工具介绍
  • 五十、【Linux系统shell脚本】case语句 、 函数及中断控制演示
  • kafka部署集群模式
  • 力扣-128.最长连续序列
  • # Kafka 消费堆积:从现象到解决的全链路分析
  • AI智能体开发流程与产品设计
  • Java商城开发的难点与解决方案
  • ShapeLLM-Omni 论文解读
  • JVM(Java Virtual Machine,Java 虚拟机)超详细总结
  • 《Linux编译器:gcc/g++食用指南》
  • 【Golang】本地缓存go-cache
  • 前端实用工具方法 —— 持续更新中...
  • 暑期算法训练.14
  • 朴素贝叶斯(Naive Bayes)算法详解
  • 前端实现大模型流式响应方案
  • 播放器音频后处理实践(一)
  • LeetCode——2683. 相邻值的按位异或
  • 3. 为什么 0.1 + 0.2 != 0.3