news 2026/5/9 11:12:33

从 ESP32-S3 到 AI 多模态:一次嵌入式学习与踩坑之旅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 ESP32-S3 到 AI 多模态:一次嵌入式学习与踩坑之旅

序言

2026年以来,一直沉寂,主要原因有两个:AI和嵌入式。

想做AI相关内容的产品,那么就绕不开嵌入式硬件开发,而嵌入式就是我的短板,几度彷徨,几度放弃,最终还是决定干起来,可能没有专业的嵌入式工程师干得好,但至少会改代码,下载烧录。

而AI的发展更是迅猛,随着claude的应用,几乎可以脱手给这些ai工具去编程,能留下的就是古法编程非遗传承人这个头衔了。

不管怎么发展,多学点也许无益收入,但爱好在此,也可以聊慰平生了…

1、 ESP32 家族简介:为什么选择 ESP32-S3

嵌入式一直是一个看起来门槛很高,但真正开始后又会上瘾的领域。过去我更多做的是上层应用与服务端开发,对 MCU、RTOS、硬件驱动这些内容了解并不深。直到最近,因为一次偶然的机会和决定,我真正开始深入学习 ESP32,并最终把阿里云多模态 SDK 跑在了 ESP32-S3 上。

整个过程里,有惊喜,也有大量踩坑。从编译问题,到日志系统崩溃,再到 FreeRTOS 时间精度问题,几乎把一个嵌入式项目会遇到的典型问题踩了个遍。

提到嵌入式开发板,现在已经很难绕开 ESP32。以下广告内容,未收费,乐鑫可以联系我。
ESP32 是乐鑫(Espressif)推出的一系列高性价比 MCU 芯片,最大的特点是:

  • WiFi + 蓝牙集成

  • 性能强

  • 价格低

  • 开源生态成熟

  • ESP-IDF 非常完善

目前 ESP32 家族已经很多分支:

芯片

特点

ESP8266

老牌 WiFi MCU

ESP32

双核 Xtensa

ESP32-C3

RISC-V ,低功耗

ESP32-S2

单核,USB

ESP32-S3

AI 指令集 + USB + 双核

ESP32-P4

更偏高性能应用

而我最终选择的是:ESP32-S3

原因主要是双核性能不错,以及比较适合音频和AI SDK,S3 增加了 AI / DSP 指令,虽然不能真正跑大模型,但:音频处理、FFT和 VAD会更高效。
当然还有入手容易,在ESP-IDF加持下,其中大量驱动、网络、FreeRTOS、OTA、文件系统都已经非常成熟,学习起来不费劲。

2、购买ESP32-S3 开发板

随便某宝,某东,顺手的事情,大家可以酌情购买。刚开始的时候其实还有点“轻视”它,觉得:一个 MCU 能干什么?
结果真正深入后才发现:ESP32 的生态已经远远超出传统 MCU。

学习方式简单粗暴,开个B站大会员, 开始学习正点原子的 ESP32 教程。跟着教程一步步来。
我学习了GPIO、UART、SPI、I2C、FreeRTOS就开始动手自己捣鼓了。

以前总觉得:RTOS 很神秘。真正接触后才发现:FreeRTOS 本质上就是“任务调度 + 同步机制”。

一些概念,例如Task、Queue、Semaphore、Mutex 和上位机概念一致,学起来其实并不复杂。

让老师也上镜下,感谢感谢!!!

3、集成阿里云多模态 SDK

学以致用,那么我们就不要和自己客气了,上来就干。真正让我开始“深度踩坑”的,是集成阿里云多模态 SDK。

阿里云给出了模板文件,我们只需要实现内存、时间、互斥、存储就可以了。

按照这个流程对模型进行调用。

sdk结构如下:

我们实现抽象层实现,也即板子功能的实现。

创建一个c文件,代码如下:

#include"hal_util_mem.h" #include"hal_util_mutex.h" #include"hal_util_random.h" #include"hal_util_storage.h" #include"hal_util_time.h" #include<util_log.h> #include<stdio.h> #include<sys/time.h> #include<inttypes.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include"freertos/FreeRTOS.h" #include"freertos/task.h" #include"freertos/semphr.h" #include"nvs_flash.h" #include"esp_timer.h" #include"esp_log.h" #ifdef__cplusplus extern"C"{ #endif // 日志标签 #defineTAG"UTIL" #ifndefMUTEX_WAIT_FOREVER #defineMUTEX_WAIT_FOREVER(-1) #endif #define_log_levelUTIL_LOG_LV_DEBUG staticconstchar*STORAGE_NAMESPACE ="aliyun_store"; /** * util_malloc - 分配指定大小的内存块。 * @size: 需要分配的内存大小,以字节为单位。 * * 本函数通过调用标准库函数malloc来分配内存,目的是为了提供一个更健壮的内存分配方法。 * 它可能包含了额外的错误检查或者内存管理策略,以提高程序的稳定性和性能。 * * 返回值: 返回指向所分配内存的指针,如果内存分配失败,则返回NULL。 */ void*util_malloc(int32_t size) { void*ptr =malloc(size); if(!ptr){ UTIL_LOG_E("malloc failed, size: %d", size); returnNULL; } memset(ptr,0, size); return ptr; } /** * 释放动态分配的内存。 * * 本函数旨在释放之前通过动态分配获得的内存空间,以避免内存泄漏。 * 它接受一个指向动态分配内存区域的指针,并将其设置为NULL,以防止悬挂指针的出现。 * * @param ptr 指向动态分配内存区域的指针。如果为NULL,函数将不执行任何操作。 * 在释放内存后,此指针将被设置为NULL。 */ voidutil_free(void*ptr) { if(ptr){ free(ptr); ptr =NULL; } } void*util_realloc(void*ptr,int32_t size) { void*new_ptr =realloc(ptr, size); if(!new_ptr){ UTIL_LOG_E("realloc failed, size: %d", size); returnNULL; } return new_ptr; } /** * 初始化随机数生成器 * * @param seed 用于初始化随机数生成器的种子值 * * @return 返回初始化结果,0表示成功,非0表示失败 * * 此函数通过对随机数生成器进行初始化,以确保后续生成的随机数序列具有良好的随机性 * 种子值的选择对生成的随机数序列有重要影响,相同的种子值会生成相同的随机数序列 */ int32_tutil_random_init(uint32_t seed) { srand(seed); return0; } /** * 生成一个随机数 * * @return 返回生成的随机数 * * 在调用此函数之前,应确保随机数生成器已经通过util_random_init函数成功初始化 * 此函数生成的随机数是基于初始化时提供的种子值产生的 */ uint32_tutil_random(void) { returnrand(); } /** * @brief擦除存储器 * * 该函数用于擦除存储器中的所有数据。在调用此函数之前,应确保不再需要存储器中的任何信息, * 因为擦除操作将删除所有数据,且此操作不可逆。 * * @return int32_t 返回擦除操作的结果。如果返回值为0,表示擦除成功;如果返回值非0,表示擦除过程中出现错误。 */ int32_tutil_storage_erase(void) { nvs_handle_t handle; if(nvs_open(STORAGE_NAMESPACE, NVS_READWRITE,&handle)!= ESP_OK){ return-1; } esp_err_t err =nvs_erase_all(handle); nvs_commit(handle); nvs_close(handle); return err == ESP_OK ?0:-1; } /** * @brief存储数据到存储器 * * 该函数将指定的数据存储到存储器中。在调用此函数之前,应确保数据的正确性和完整性, * 因为存储操作将覆盖存储器中的现有数据。 * * @param data 指向要存储的数据的指针。数据类型为uint8_t,即无符号的8位整数。 * @param size 要存储的数据的大小,以字节为单位。数据类型为uint32_t,即无符号的32位整数。 * @return int32_t 返回存储操作的结果。如果返回值为0,表示存储成功;如果返回值非0,表示存储过程中出现错误。 */ int32_tutil_storage_storage(uint8_t*data,uint32_t size) { nvs_handle_t handle; if(nvs_open(STORAGE_NAMESPACE, NVS_READWRITE,&handle)!= ESP_OK){ return-1; } esp_err_t err =nvs_set_blob(handle,"data", data, size); nvs_commit(handle); nvs_close(handle); return err == ESP_OK ?0:-1; } /** * @brief从存储器加载数据 * * 该函数从存储器中加载指定大小的数据。在调用此函数之前,应确保提供的数据指针指向的内存区域足够大, * 以容纳从存储器加载的数据。 * * @param data 指向用于存储从存储器加载的数据的缓冲区的指针。数据类型为uint8_t,即无符号的8位整数。 * @param size 要加载的数据的大小,以字节为单位。数据类型为uint32_t,即无符号的32位整数。 * @return int32_t 返回加载操作的结果。如果返回值为0,表示加载成功;如果返回值非0,表示加载过程中出现错误。 */ int32_tutil_storage_load(uint8_t*data,uint32_t size) { nvs_handle_t handle; if(nvs_open(STORAGE_NAMESPACE, NVS_READONLY,&handle)!= ESP_OK){ return-1; } size_t required_size = size; esp_err_t err =nvs_get_blob(handle,"data", data,&required_size); nvs_close(handle); return err == ESP_OK ?0:-1; } /** * 获取当前时间戳(毫秒级) * * 此函数用于获取当前的时间戳,精确到毫秒该时间戳通常用于计算时间差、 * 记录事件发生时间等场景 * * 返回:当前时间戳(毫秒级) */ int64_tutil_now_ms(void) { int64_t val =esp_timer_get_time()/1000; ESP_LOGI(TAG,"util_now_ms = %ld",(long)val); return val; } /** * 毫秒级睡眠函数 * * 此函数使当前线程暂停执行指定毫秒数,用于控制程序执行节奏、等待事件发生等 * * 参数 ms:需要暂停的毫秒数 */ voidutil_msleep(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } /** * 获取当前时间戳 * * 此函数用于获取当前的时间戳,即从1970年1月1日00:00:00 UTC开始到现在的毫秒数 * 它没有输入参数,返回一个int64_t类型的值,代表当前的时间戳 * * @return int64_t 当前时间戳,单位为毫秒 */ int64_tutil_get_timestamp(void) { time_t now =time(NULL); ESP_LOGI(TAG,"util_get_timestamp = %ld",(long)now); if(now <1700000000){ return0; } return(int64_t)now *1000LL; } /** * 检查时间戳功能是否已初始化 * * 此函数用于检查时间戳相关功能是否已经初始化如果返回真(非零),则表示 * 时间戳功能可用;如果返回假(零),则可能需要进行初始化操作或者避免使用时间戳功能 * * 返回:如果时间戳功能已初始化,则返回非零,否则返回零 */ uint8_tutil_timestamp_inited(void) { if(util_get_timestamp()<1732982400000){ return0; }else{ return1; } } /***************************************************** * Function: util_mutex_create * Description: 创建一个互斥锁对象。 * Parameter: 无。 * Return: util_mutex_t * --- 返回指向互斥锁结构体的指针。 ****************************************************/ util_mutex_t*util_mutex_create(void) { SemaphoreHandle_t m =xSemaphoreCreateMutex(); if(!m){ UTIL_LOG_E("mutex create failed"); } return(util_mutex_t*)m; } /***************************************************** * Function: util_mutex_delete * Description: 删除指定的互斥锁对象。 * Parameter: * mutex --- 指向互斥锁结构体的指针。 * Return: 无。 ****************************************************/ voidutil_mutex_delete(util_mutex_t*mutex) { if(mutex){ vSemaphoreDelete((SemaphoreHandle_t)mutex); } } /***************************************************** * Function: util_mutex_lock * Description: 对指定的互斥锁进行加锁操作,带超时机制。 * Parameter: * mutex --- 指向互斥锁结构体的指针。 * timeout --- 加锁等待超时的时间,单位为毫秒(ms),可设为 MUTEX_WAIT_FOREVER 表示无限等待。 * Return: int32_t --- 返回操作结果(util_result_t)。 ****************************************************/ int32_tutil_mutex_lock(util_mutex_t*mutex,int32_t timeout) { if(mutex ==NULL){ return-1; } TickType_t ticks =0; if(timeout == MUTEX_WAIT_FOREVER){ ticks = portMAX_DELAY; }elseif(timeout <=0){ ticks =0; }else{ ticks =(timeout + portTICK_PERIOD_MS -1)/ portTICK_PERIOD_MS; /* 防止 timeout 很小被转换成 0 tick */ if(ticks ==0){ ticks =1; } } BaseType_t ret = xSemaphoreTake((SemaphoreHandle_t)mutex, ticks); return(ret == pdTRUE)?0:-1; } /***************************************************** * Function: util_mutex_unlock * Description: 对指定的互斥锁进行解锁操作。 * Parameter: * mutex --- 指向互斥锁结构体的指针。 * Return: int32_t --- 返回操作结果(util_result_t)。 ****************************************************/ int32_tutil_mutex_unlock(util_mutex_t*mutex) { if(!mutex)return-1; BaseType_t ret = xSemaphoreGive((SemaphoreHandle_t)mutex); return(ret == pdTRUE)?0:-1; } #ifdef__cplusplus } #endif c运行

4、编译与崩溃:最后发现不是日志系统的问题

编写很简单,直接查找esp32直接的函数进行实现即可,但是到了时间函数这块,怎么测试都是崩溃,找了阿里的专家,依然无法解决。

打开deepseek,它告诉我是时间函数的问题,没有注册时间定时器,或者NSTP,经过一天的研究和尝试,发现都没有用,最后在阿里专家的提醒下,终于定位到问题语句。

PRId64, 这个格式好像有点印象,正点原子教程老师有讲到,好像需要编译器支持。

打开配置,仔细阅读,终于找到罪魁祸首。

默认启用的nano模式,精简化了lib库,但也丧失了64位功能,禁用后果然搞定。

5、互斥锁时间不准确

再次编译,烧录程序。
阿里的测试函数已经返回了可观的绿色。

仅有一个红色,终于悬着的心落下来了。第一次集成sdk,眼看就要完成第一步了。

再次询问阿里工程师,得到回复: mutex锁时间不对。

刚开始以为Mutex 有 bug,timeout 逻辑错误,后来深入 FreeRTOS 才发现:timeout 本质上是 Tick 调度。

ESP-IDF 默认:CONFIG_FREERTOS_HZ = 100
也就是:1 tick = 10ms

因此:导致精度不够,那这也是 RTOS 的正常现象了,知道原因后,我想起来教程里有设置FreeRTOS的时钟频率。

找到配置

问题立刻解决。

当然,顺便在Deepseek上学习了下,如此设置的优点:

  1. mutex timeout 更精准

  2. delay 更精准

  3. WebSocket 调度更稳定

当然代价是:

  1. Tick 中断增加

  2. CPU 调度更频繁

  3. 功耗略微上升

但对于 AI 实时项目来说,这个代价完全值得。

结果终于全绿!

6、总结:嵌入式远比想象中有趣

这次 ESP32-S3 学习过程,让我最大的感受是:一块小小的板子里面竟然藏着一个大大的世界!

从 ESP32-S3 入门到集成阿里云多模态 SDK 的过程,本质上是一场“从写应用代码到被底层教育”的转变。踩得每一个小坑,背后都有硬件与系统调度的真实约束在里面。

这段经历最大的收获不是功能实现,而是理解了嵌入式系统的真实世界:没有绝对精确,只有资源与调度的权衡。写代码不再只是“逻辑正确”,还要“贴着硬件活着”。 当然其丰富的世界,并不是这么短时间就可以把握的,路仍在脚下延申…

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 10:58:55

终极指南:如何让Figma界面秒变中文,3分钟解决设计语言障碍

终极指南&#xff1a;如何让Figma界面秒变中文&#xff0c;3分钟解决设计语言障碍 【免费下载链接】figmaCN 中文 Figma 插件&#xff0c;设计师人工翻译校验 项目地址: https://gitcode.com/gh_mirrors/fi/figmaCN 还在为Figma的英文界面感到头疼吗&#xff1f;对于中文…

作者头像 李华
网站建设 2026/5/9 10:58:09

cann-recipes-infer 贡献指南

贡献指南 【免费下载链接】cann-recipes-infer 本项目针对LLM与多模态模型推理业务中的典型模型、加速算法&#xff0c;提供基于CANN平台的优化样例 项目地址: https://gitcode.com/cann/cann-recipes-infer 本项目欢迎广大开发者体验并参与贡献&#xff0c;在参与社区贡…

作者头像 李华
网站建设 2026/5/9 10:53:29

李辉《曾国藩日记》笔记:能忍,是一个人野心和信息的表现!

李辉《曾国藩日记》笔记&#xff1a;能忍&#xff0c;是一个人野心和信息的表现&#xff01;原文&#xff1a;同治元年十二月卅日早饭后清理文件。旋见客三次。写沅弟信一件。与程四世兄围棋三局。中饭请赵岵存便饭&#xff0c;坐无他客&#xff0c;与之畅谈&#xff0c;未正散…

作者头像 李华
网站建设 2026/5/9 10:46:47

Python金融数据抓取终极指南:10分钟掌握同花顺问财自动化技巧

Python金融数据抓取终极指南&#xff1a;10分钟掌握同花顺问财自动化技巧 【免费下载链接】pywencai 获取同花顺问财数据 项目地址: https://gitcode.com/gh_mirrors/py/pywencai 还在为获取A股数据而烦恼吗&#xff1f;每天手动导出Excel、复制粘贴股票信息&#xff0c…

作者头像 李华
网站建设 2026/5/9 10:44:54

multi-modal/多模态大模型与multi-mode/多模态驾驶动作分布:自动驾驶语境下“多模态”概念的本质区别

在人工智能和自动驾驶领域,“多模态”是一个非常高频的概念。但在不同语境下,“多模态”所表达的含义并不完全相同。 例如,我们经常会看到两个看似相近的概念: 多模态大模型,英文通常为 multimodal foundation model 多模态驾驶动作分布,英文常见为 multi-mode driving …

作者头像 李华
网站建设 2026/5/9 10:41:00

快速解锁QQ音乐加密文件:Mac用户的终极音乐格式转换指南

快速解锁QQ音乐加密文件&#xff1a;Mac用户的终极音乐格式转换指南 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认…

作者头像 李华