在嵌入式开发中,数据采集是一个经典需求。但当数据量大、频率高(例如 IMU 原始数据)且对实时性要求极高时,看似简单的“串口收发”就会变成一场硬仗。

本文将完整记录我们如何使用 ESP32-S3 构建一个高可靠性的 IMU 数据采集系统。该系统能够以 3Mbps 波特率 接收 UART 数据,通过 MAVLink 协议 解析,并将其无损保存到板载 Flash 的 FATFS 文件系统中。

1. 项目需求与挑战

  • 目标:采集 IMU 传感器的原始数据用于后续校准。
  • 硬件:ESP32-S3 (主控) + 外部 IMU 模块。
  • 接口:UART,波特率 3,000,000 (3Mbps)
  • 协议:MAVLink v2。
  • 挑战
    1. 极高的吞吐量:3Mbps 意味着每秒约 300KB 数据,硬件 FIFO (128B) 填满仅需 400 微秒。
    2. 资源冲突:SPI Flash 写入速度慢且会阻塞 CPU(禁用 Cache),极易导致串口丢包。
    3. 内存限制:需要缓存大量数据,但 ESP32 内部 RAM 有限且碎片化严重。

2. 核心架构设计

为了解决上述挑战,我们采用了一种 “RAM 缓冲 + 延迟写入” 的架构:

  1. 接收层 (ISR):启用 IRAM 中断,保证在任何情况下(包括 Flash 写操作期间)都能及时搬运硬件 FIFO 数据。
  2. 缓冲层 (RAM):使用 std::deque 实现全内存缓冲,规避内存碎片问题。
  3. 存储层 (FATFS):采集结束后一次性写入文件,避免“边收边写”造成的实时性破坏。

3. 关键实现步骤

步骤一:配置高速 UART (启用 IRAM)

这是实现“零丢包”的第一步。标准 UART 配置在 Flash 写入期间会因为 XIP (Execute In Place) 机制被阻塞,导致 FIFO 溢出。解决方法是将中断服务程序 (ISR) 放入内部 RAM (IRAM)。

// main.cpp

// 1. 配置参数
uart_config_t uart_config = {
    .baud_rate = 3000000, // 3Mbps
    .data_bits = UART_DATA_8_BITS,
    .parity    = UART_PARITY_DISABLE,
    .stop_bits = UART_STOP_BITS_1,
    .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
    .source_clk = UART_SCLK_DEFAULT,
};

// 2. 安装驱动 (关键点:ESP_INTR_FLAG_IRAM)
// 减小 Buffer 到 8KB 以节省内存给数据缓存,依靠 IRAM 中断保证安全
uart_driver_install(UART_NUM_1, 8192, 0, 0, NULL, ESP_INTR_FLAG_IRAM);
uart_param_config(UART_NUM_1, &uart_config);

步骤二:集成 MAVLink 解析

使用 MAVLink 官方 C 库进行流式解析。我们创建了一个独立的 FreeRTOS 任务来处理接收。

// main.cpp - custom_uart_task

mavlink_message_t msg;
mavlink_status_t status;
uint8_t data[2048];

while (1) {
    // 阻塞读取 UART 数据
    int len = uart_read_bytes(UART_NUM_1, data, sizeof(data), pdMS_TO_TICKS(10));
    
    if (len > 0) {
        // 逐字节送入解析器
        for (int i = 0; i < len; ++i) {
            if (mavlink_parse_char(MAVLINK_COMM_0, data[i], &msg, &status)) {
                // 解析成功,处理消息
                if (msg.msgid == MAVLINK_MSG_ID_FACTORY_TEST) {
                    // 提取 payload 并推送到处理队列
                    // ...
                }
            }
        }
    }
}

步骤三:设计缓冲策略 (std::deque)

这是解决 内存分配失败 (OOM) 的关键。当我们需要缓存 100KB+ 数据时,使用 malloc 申请连续内存很容易失败。C++ 的 std::deque 由多个小内存块组成,对内存碎片非常友好。

// imu_calibration.cpp

#include <deque>

// 全局缓冲区
static std::deque<uint8_t> g_ram_buffer;

// 数据接收回调
void imu_process_data_stream(const uint8_t* data, uint16_t len) {
    if (s_calib_saving_enabled) {
        try {
            // 将数据追加到 deque 尾部
            g_ram_buffer.insert(g_ram_buffer.end(), data, data + len);
        } catch (...) {
            // 优雅处理内存耗尽:停止接收,保留已有数据
            ESP_LOGE(TAG, "RAM Full! Stopping collection.");
            s_calib_saving_enabled = false;
        }
    }
}

步骤四:文件保存 (FATFS)

采集结束后,我们将 RAM 中的数据一次性写入 Flash。这里要注意 栈溢出 问题:不要在函数内部定义大数组。

// imu_calibration.cpp

bool imu_stop_calibration(void) {
    s_calib_saving_enabled = false;

    if (g_calib_imu_file && !g_ram_buffer.empty()) {
        ESP_LOGI(TAG, "Writing %zu bytes...", g_ram_buffer.size());

        // 申请 4KB 临时缓存 (堆分配,防止爆栈)
        const size_t CHUNK_SIZE = 4096;
        uint8_t* chunk = (uint8_t*)malloc(CHUNK_SIZE);
        
        if (chunk) {
            size_t total = g_ram_buffer.size();
            size_t processed = 0;
            
            // 分块写入
            while (processed < total) {
                size_t remain = total - processed;
                size_t copy_len = (remain > CHUNK_SIZE) ? CHUNK_SIZE : remain;
                
                // 从 deque 拷贝到连续 buffer
                for(size_t i=0; i<copy_len; i++) {
                    chunk[i] = g_ram_buffer[processed + i];
                }
                
                fwrite(chunk, 1, copy_len, g_calib_imu_file);
                processed += copy_len;
            }
            free(chunk);
        }
        
        // 清理资源
        g_ram_buffer.clear();
        g_ram_buffer.shrink_to_fit(); // 释放内存回系统
        fclose(g_calib_imu_file);
    }
    return true;
}

4. 避坑指南(经验总结)

在调试过程中,我们解决了一系列棘手问题:

问题 1:频繁丢包

  • 现象:每次日志打印 Writing batch 时,紧接着就会出现序列号跳变。
  • 原因fwrite 触发 Flash 扇区擦除,阻塞 CPU 长达数十毫秒。
  • 解决
    1. UART 驱动启用 ESP_INTR_FLAG_IRAM
    2. 放弃“边收边写”,改为“RAM 缓冲 + 结束写入”。

问题 2:系统重启 (Guru Meditation Error)

  • 现象:执行保存操作时,系统报错 Stack overflow in task uTask
  • 原因:在 4KB 栈空间的任务中定义了 uint8_t buffer[4096] 局部变量,直接撑爆。
  • 解决:大块内存务必使用 malloc 在堆上分配,或者增大任务栈大小(例如 xTaskCreate(..., 8192, ...))。

问题 3:无法分配内存

  • 现象:尝试 malloc(40000) 失败,尽管系统显示剩余内存 > 100KB。
  • 原因:内存碎片化导致没有足够大的连续块。
  • 解决:使用 std::deque 替代 std::vector 或大数组。

5. 最终效果

经过上述优化,我们的系统达到了生产级稳定性:

  • 通信速率:3,000,000 bps 满跑。
  • 数据完整性:连续采集 2000+ 包(约 100KB),0 丢包
  • 系统稳定性:长时间运行无内存泄漏,无崩溃。

这套方案不仅适用于 IMU 采集,也适用于任何 ESP32 平台上的高速数据透传与存储场景。


希望这篇文章能为你的 ESP32 开发之路提供参考!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐