4. 图像采集基础:DCMI接口原理与配置,DMA双缓冲机制实现,JPEG与RGB565格式转换

图像采集是边缘AI视觉应用的第一道关卡。说白了,摄像头数据进不来,后面算法再牛也没用。STM32的DCMI接口就是专门干这活的——它能把摄像头传来的并行数据,有条不紊地塞进内存。

我个人习惯把DCMI理解成一个“数据搬运工”。它不负责处理图像内容,只管把像素时钟、行同步、帧同步这些时序信号对齐,然后把数据一股脑儿推给DMA。嗯,这里要注意:DCMI本身没有大容量FIFO,所以DMA必须跟上节奏,否则数据就会丢。

4.1 DCMI接口硬件原理

DCMI是一个同步并行接口,支持8位、10位、12位、14位数据宽度。实际项目中,8位模式用得最多——因为大部分OV系列摄像头输出就是8位。

它的核心信号就这么几根:

信号名 方向 作用
DCMI_PIXCLK 输入 像素时钟,每个上升沿采集一次数据
DCMI_HSYNC 输入 行同步,标志一行数据开始/结束
DCMI_VSYNC 输入 帧同步,标志一帧图像开始/结束
DCMI_D[0:7] 输入 8位并行数据总线

我在项目中遇到过一个问题:摄像头输出的VSYNC极性跟STM32默认配置相反。结果图像一直采集不到,折腾了半天才发现是极性没配对。所以,拿到摄像头先看数据手册,确认VSYNC和HSYNC的有效电平。

避坑指南: 我曾经因为PIXCLK频率太高(超过DCMI最大允许值),导致采集的图像出现条纹。STM32F4系列的DCMI最高支持约54MHz像素时钟,F7/H7系列可以到108MHz。选摄像头时务必算一下。

4.2 DCMI初始化配置

配置DCMI其实不复杂,关键是把几个结构体填对。我用的是STM32CubeMX生成的初始化代码,但有些细节需要手动调整。

// DCMI初始化结构体
DCMI_InitTypeDef hdcmi_init;

hdcmi_init.SynchroMode = DCMI_SYNCHRO_HARDWARE;  // 硬件同步模式
hdcmi_init.PCKPolarity = DCMI_PCKPOLARITY_RISING; // 像素时钟上升沿采集
hdcmi_init.VSPolarity = DCMI_VSPOLARITY_HIGH;     // VSYNC高电平有效
hdcmi_init.HSPolarity = DCMI_HSPOLARITY_HIGH;     // HSYNC高电平有效
hdcmi_init.CaptureRate = DCMI_CR_ALL_FRAME;       // 捕获所有帧
hdcmi_init.ExtendedData = DCMI_EXTEND_DATA_8B;    // 8位数据模式

HAL_DCMI_Init(&hdcmi, &hdcmi_init);

你想想看,为什么CaptureRate要配成ALL_FRAME?因为做图像识别需要每一帧数据,不能丢帧。如果只是预览显示,可以配成隔帧捕获来省带宽。

4.3 DMA双缓冲机制

单缓冲的问题很明显:DCMI往缓冲区写数据时,CPU不能读,否则读到的是半帧残影。双缓冲就是解决这个问题的——一个缓冲区在“后台”被DCMI填数据,另一个在“前台”被CPU或算法处理。

我建议用DMA的双缓冲模式(Double Buffer Mode),而不是手动切换指针。STM32的DMA支持硬件自动切换,省心省力。

// 定义两个缓冲区
#define FRAME_WIDTH  320
#define FRAME_HEIGHT 240
#define BUFFER_SIZE  (FRAME_WIDTH * FRAME_HEIGHT * 2)  // RGB565每个像素2字节

uint8_t frame_buffer0[BUFFER_SIZE] __attribute__((aligned(32)));
uint8_t frame_buffer1[BUFFER_SIZE] __attribute__((aligned(32)));

// 配置DMA双缓冲
HAL_DMAEx_ConfigMuxSync(&hdma_dcmi, DMA_SYNC_FREE);
HAL_DMAEx_ConfigMuxSync(&hdma_dcmi, DMA_SYNC_FREE);

// 启动DCMI捕获,使用双缓冲
HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, 
                    (uint32_t)frame_buffer0, 
                    BUFFER_SIZE / 2);  // 注意:这里是半字计数

// 注册DMA传输完成回调
void HAL_DCMI_LineEventCallback(DCMI_HandleTypeDef *hdcmi)
{
    // 每完成一行数据,可以在这里做行处理
    // 但通常我们等帧完成回调
}

void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
    // 一帧采集完成,切换缓冲区
    // 此时frame_buffer0或frame_buffer1中有一帧完整数据
    // 另一个缓冲区正在被DCMI填充
    process_image(current_buffer);
}
经验之谈: 缓冲区地址要32字节对齐,否则DMA传输效率会下降。我踩过这个坑——没对齐时,DCMI采集速度一快就丢数据。另外,BUFFER_SIZE的计算别忘了乘以2(RGB565每个像素2字节)。

4.4 JPEG与RGB565格式转换

摄像头输出的原始数据通常是JPEG压缩格式(比如OV2640),但STM32的AI库(比如STM32Cube.AI)输入要求是RGB565或RGB888。所以,格式转换是绕不开的一步。

JPEG解码在STM32上是个体力活。硬件JPEG编解码器(STM32F7/H7系列有)能大幅加速,但F4系列只能靠软件。我推荐用TinyJPEG或picojpeg这类轻量级库,内存占用小。

转换流程大致如下:

  1. 从DCMI缓冲区拿到JPEG数据流
  2. 调用JPEG解码库,逐MCU解码
  3. 将解码后的YUV数据转为RGB565
  4. 把RGB565数据存入AI模型的输入缓冲区
// JPEG解码 + YUV转RGB565 简化示例
void jpeg_to_rgb565(uint8_t *jpeg_data, uint32_t jpeg_len, 
                    uint8_t *rgb565_out, uint32_t width, uint32_t height)
{
    // 初始化JPEG解码器
    tjdec_handle_t *jdec = tjdec_init();
    
    // 设置输入缓冲区
    tjdec_set_input_buffer(jdec, jpeg_data, jpeg_len);
    
    // 读取JPEG头信息
    int w, h;
    tjdec_read_header(jdec, &w, &h);
    
    // 解码到YUV缓冲区
    uint8_t *yuv_buffer = malloc(w * h * 3 / 2);  // YUV420格式
    tjdec_decode(jdec, yuv_buffer, TJDEC_PIX_FMT_YUV420);
    
    // YUV420转RGB565
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            int y_idx = y * w + x;
            int u_idx = (y / 2) * (w / 2) + (x / 2);
            int v_idx = u_idx;
            
            int Y = yuv_buffer[y_idx];
            int U = yuv_buffer[w * h + u_idx] - 128;
            int V = yuv_buffer[w * h * 5 / 4 + v_idx] - 128;
            
            // YUV转RGB
            int R = Y + 1.402 * V;
            int G = Y - 0.344 * U - 0.714 * V;
            int B = Y + 1.772 * U;
            
            // 钳位到0-255
            R = (R < 0) ? 0 : (R > 255) ? 255 : R;
            G = (G < 0) ? 0 : (G > 255) ? 255 : G;
            B = (B < 0) ? 0 : (B > 255) ? 255 : B;
            
            // 打包为RGB565
            uint16_t rgb565 = ((R >> 3) << 11) | 
                              ((G >> 2) << 5)  | 
                              (B >> 3);
            
            ((uint16_t *)rgb565_out)[y * w + x] = rgb565;
        }
    }
    
    free(yuv_buffer);
    tjdec_free(jdec);
}
性能关键点: 软件JPEG解码在STM32F4上大约需要100-300ms(取决于图像尺寸和JPEG压缩率)。如果帧率要求高(比如30fps),必须用硬件JPEG或降低分辨率。我做过测试:320x240的JPEG在F429上软解约120ms,而H743的硬件JPEG只要8ms。

4.5 整体数据流架构

把上面几个环节串起来,就是完整的图像采集链路。我画了一张图,帮你理清数据流向。

DCMI图像采集与处理数据流 摄像头 OV2640/OV5640 DCMI接口 并行数据捕获 DMA双缓冲 乒乓传输 缓冲区 JPEG/RGB 8位并行 DMA请求 JPEG解码 软件/硬件 格式转换 YUV→RGB565 AI推理 输入张量 JPEG数据流 YUV数据 RGB565 关键说明: 1. 摄像头输出JPEG压缩数据,经DCMI+DMA存入缓冲区 2. JPEG解码在CPU或硬件JPEG模块中完成 3. 格式转换将YUV转为RGB565,供AI模型输入 4. 双缓冲确保采集与处理并行,不丢帧

这张图把整个链路画清楚了。从摄像头到DCMI,再到DMA双缓冲,然后经过JPEG解码和格式转换,最终喂给AI推理引擎。每个环节的延迟都会影响整体帧率。

优化建议: 如果AI模型输入要求RGB888,可以在格式转换时直接输出24位RGB,但内存占用会增大50%。我一般用RGB565,因为STM32的DMA传输16位效率更高,而且人眼对颜色精度不敏感,模型精度损失可以忽略。

嗯,图像采集这块就讲这么多。记住一个原则:数据流要通畅,瓶颈要提前解决。DCMI配置对了,DMA双缓冲用上了,格式转换优化好了,后面的AI推理才能跑得稳。