从 ESP32-S3 到 AI 多模态:一次嵌入式学习与踩坑之旅
- 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
- 📢本文作者:由webmote 原创
- 📢作者格言:2025年,一个巨大的转折点,开启自由职业,技术栈.NET、VUE、嵌入式C、大量低价接私活中,欢迎dddd…
序言
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
// 日志标签
#define TAG "UTIL"
#ifndef MUTEX_WAIT_FOREVER
#define MUTEX_WAIT_FOREVER (-1)
#endif
#define _log_level UTIL_LOG_LV_DEBUG
static const char *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);
return NULL;
}
memset(ptr, 0, size);
return ptr;
}
/**
* 释放动态分配的内存。
*
* 本函数旨在释放之前通过动态分配获得的内存空间,以避免内存泄漏。
* 它接受一个指向动态分配内存区域的指针,并将其设置为NULL,以防止悬挂指针的出现。
*
* @param ptr 指向动态分配内存区域的指针。如果为NULL,函数将不执行任何操作。
* 在释放内存后,此指针将被设置为NULL。
*/
void util_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);
return NULL;
}
return new_ptr;
}
/**
* 初始化随机数生成器
*
* @param seed 用于初始化随机数生成器的种子值
*
* @return 返回初始化结果,0表示成功,非0表示失败
*
* 此函数通过对随机数生成器进行初始化,以确保后续生成的随机数序列具有良好的随机性
* 种子值的选择对生成的随机数序列有重要影响,相同的种子值会生成相同的随机数序列
*/
int32_t util_random_init(uint32_t seed)
{
srand(seed);
return 0;
}
/**
* 生成一个随机数
*
* @return 返回生成的随机数
*
* 在调用此函数之前,应确保随机数生成器已经通过util_random_init函数成功初始化
* 此函数生成的随机数是基于初始化时提供的种子值产生的
*/
uint32_t util_random(void)
{
return rand();
}
/**
* @brief擦除存储器
*
* 该函数用于擦除存储器中的所有数据。在调用此函数之前,应确保不再需要存储器中的任何信息,
* 因为擦除操作将删除所有数据,且此操作不可逆。
*
* @return int32_t 返回擦除操作的结果。如果返回值为0,表示擦除成功;如果返回值非0,表示擦除过程中出现错误。
*/
int32_t util_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_t util_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_t util_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_t util_now_ms(void)
{
int64_t val = esp_timer_get_time() / 1000;
ESP_LOGI(TAG, "util_now_ms = %ld", (long)val);
return val;
}
/**
* 毫秒级睡眠函数
*
* 此函数使当前线程暂停执行指定毫秒数,用于控制程序执行节奏、等待事件发生等
*
* 参数 ms:需要暂停的毫秒数
*/
void util_msleep(uint32_t ms)
{
vTaskDelay(pdMS_TO_TICKS(ms));
}
/**
* 获取当前时间戳
*
* 此函数用于获取当前的时间戳,即从1970年1月1日00:00:00 UTC开始到现在的毫秒数
* 它没有输入参数,返回一个int64_t类型的值,代表当前的时间戳
*
* @return int64_t 当前时间戳,单位为毫秒
*/
int64_t util_get_timestamp(void)
{
time_t now = time(NULL);
ESP_LOGI(TAG, "util_get_timestamp = %ld", (long)now);
if (now < 1700000000) {
return 0;
}
return (int64_t)now * 1000LL;
}
/**
* 检查时间戳功能是否已初始化
*
* 此函数用于检查时间戳相关功能是否已经初始化如果返回真(非零),则表示
* 时间戳功能可用;如果返回假(零),则可能需要进行初始化操作或者避免使用时间戳功能
*
* 返回:如果时间戳功能已初始化,则返回非零,否则返回零
*/
uint8_t util_timestamp_inited(void)
{
if (util_get_timestamp() < 1732982400000) {
return 0;
} else {
return 1;
}
}
/*****************************************************
* 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: 无。
****************************************************/
void util_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_t util_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;
} else if (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_t util_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
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上学习了下,如此设置的优点:
- mutex timeout 更精准
- delay 更精准
- WebSocket 调度更稳定
当然代价是:
- Tick 中断增加
- CPU 调度更频繁
- 功耗略微上升
但对于 AI 实时项目来说,这个代价完全值得。
结果终于全绿!
6、总结:嵌入式远比想象中有趣
这次 ESP32-S3 学习过程,让我最大的感受是:一块小小的板子里面竟然藏着一个大大的世界!
从 ESP32-S3 入门到集成阿里云多模态 SDK 的过程,本质上是一场“从写应用代码到被底层教育”的转变。踩得每一个小坑,背后都有硬件与系统调度的真实约束在里面。
这段经历最大的收获不是功能实现,而是理解了嵌入式系统的真实世界:没有绝对精确,只有资源与调度的权衡。写代码不再只是“逻辑正确”,还要“贴着硬件活着”。 当然其丰富的世界,并不是这么短时间就可以把握的,路仍在脚下延申…
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)