• 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由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上学习了下,如此设置的优点:

  1. mutex timeout 更精准
  2. delay 更精准
  3. WebSocket 调度更稳定

当然代价是:

  1. Tick 中断增加
  2. CPU 调度更频繁
  3. 功耗略微上升

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

结果终于全绿!
在这里插入图片描述

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

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

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

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐