3、图像传感器驱动与数据采集:OV2640摄像头驱动、DCMI接口配置、DMA双缓冲采集、JPEG/MJPEG数据流获取

好,咱们进入第三章。这一章讲的是怎么把图像数据从摄像头“搬”到STM32的内存里。说白了,就是搞定OV2640这颗摄像头,配好DCMI接口,再用DMA双缓冲把数据安安稳稳地收进来。

我刚开始做这个的时候,觉得不就是读个摄像头嘛,能有多难?结果第一次调通,图像全是花屏,找了两天原因才发现是DCMI的同步信号极性配反了。嗯,这种坑踩过一次就记住了。

3.1 OV2640摄像头驱动要点

OV2640是一颗200万像素的CMOS图像传感器,支持输出JPEG压缩数据和原始RGB/YUV数据。咱们做边缘AI,大部分场景用JPEG输出就够了——数据量小,传输快。

驱动OV2640,核心就两件事:

  • 初始化寄存器:通过SCCB总线(兼容I2C)写入配置序列
  • 配置输出格式:选择JPEG、RGB565或YUV422

我个人习惯把初始化序列放在一个常量数组里,结构清晰,方便调试。像这样:

// OV2640 JPEG初始化序列(部分)
const uint8_t ov2640_jpeg_init[][2] = {
    {0xFF, 0x01},  // 切换到Bank1
    {0x12, 0x80},  // 复位
    // ... 省略中间几十行配置
    {0xDA, 0x00},  // 输出JPEG数据
    {0xFF, 0x00},  // 切回Bank0
};

这里有个坑:OV2640的寄存器分多个Bank,写之前必须先切Bank。我见过有人忘了切Bank,结果配置了半天全写到了同一个Bank里,摄像头根本不工作。

注意:OV2640上电后默认输出UXGA(1600x1200)的YUV数据。如果你要JPEG输出,必须显式配置压缩相关寄存器。我曾经偷懒没配全,结果输出的是YUV而不是JPEG,DCMI那边解析全乱套了。

3.2 DCMI接口配置实战

STM32的DCMI(Digital Camera Interface)是一个并行接口,专门用来接摄像头。它支持8位、10位、12位、14位数据宽度。OV2640输出8位数据,所以咱们配成8位模式。

DCMI的关键配置项:

配置项 说明 推荐值
数据宽度 DCMI_DCR寄存器的EDM位 8位(0b00)
同步方式 硬件同步(VSYNC/HSYNC)或内嵌同步 硬件同步
极性 VSYNC/HSYNC/PIXCLK的有效电平 根据摄像头手册配置
捕获模式 连续模式或快照模式 连续模式

配置极性的那一步,我建议你仔细看OV2640的数据手册。VSYNC是高电平有效还是低电平有效?HSYNC呢?搞反了,图像就是歪的或者全是条纹。我那次花屏,就是VSYNC极性配反了。

DCMI的初始化代码大致长这样:

void DCMI_Init(void)
{
    // 使能DCMI时钟
    RCC->AHBENR |= RCC_AHBENR_DCMIEN;
    
    // 配置DCMI控制寄存器
    DCMI->CR = 0;
    DCMI->CR |= DCMI_CR_CM_CONTINUOUS;  // 连续模式
    DCMI->CR |= DCMI_CR_EDM_8B;         // 8位数据
    DCMI->CR |= DCMI_CR_VSPOL;          // VSYNC高有效
    DCMI->CR |= DCMI_CR_HSPOL;          // HSYNC高有效
    DCMI->CR |= DCMI_CR_PCKPOL;         // PIXCLK上升沿采样
    
    // 使能DCMI
    DCMI->CR |= DCMI_CR_ENABLE;
}

3.3 DMA双缓冲采集机制

为什么用双缓冲?你想想看,如果只有一个缓冲区,DCMI往里面写数据的时候,CPU就不能读——读到的数据可能是一半旧的、一半新的,图像就撕裂了。

双缓冲的思路很简单:两个缓冲区轮流用。DCMI往缓冲区A写的时候,CPU处理缓冲区B的数据;下一帧,角色互换。

STM32的DMA支持双缓冲模式(Double Buffer Mode),硬件自动切换,不需要CPU干预。配置要点:

  • 使能DMA的循环模式(Circular Mode)
  • 设置两个内存地址:DMA_M0AR和DMA_M1AR
  • 开启传输完成中断,在中断里切换缓冲区指针

代码示例:

#define FRAME_BUFFER_SIZE    (320 * 240 * 2)  // 320x240 RGB565

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

void DMA_Init_DoubleBuffer(void)
{
    // 配置DMA2_Stream1(DCMI专用)
    DMA2_Stream1->PAR = (uint32_t)&DCMI->DR;  // 外设地址
    DMA2_Stream1->M0AR = (uint32_t)frame_buffer0;  // 缓冲区0
    DMA2_Stream1->M1AR = (uint32_t)frame_buffer1;  // 缓冲区1
    DMA2_Stream1->NDTR = FRAME_BUFFER_SIZE / 2;    // 传输次数(半字)
    
    DMA2_Stream1->CR |= DMA_SxCR_DBM;   // 使能双缓冲
    DMA2_Stream1->CR |= DMA_SxCR_CIRC;  // 循环模式
    DMA2_Stream1->CR |= DMA_SxCR_TCIE;  // 传输完成中断
    DMA2_Stream1->CR |= DMA_SxCR_EN;    // 使能DMA
}
小技巧:缓冲区地址建议做32字节对齐,这样DMA传输效率更高。用__attribute__((aligned(32)))就能搞定。

3.4 JPEG/MJPEG数据流获取

OV2640输出JPEG数据时,一帧图像就是一串JPEG码流。MJPEG(Motion JPEG)就是连续的多帧JPEG流,每一帧独立压缩,帧与帧之间没有依赖关系。

获取JPEG数据流的关键是找到帧起始和帧结束标记:

  • SOI(Start of Image):0xFF 0xD8
  • EOI(End of Image):0xFF 0xD9

DCMI+DMA双缓冲模式下,数据是连续写入缓冲区的。我们需要在DMA传输完成中断里,扫描缓冲区,找到SOI和EOI,把完整的一帧数据提取出来。

我一般这么处理:

void DMA2_Stream1_IRQHandler(void)
{
    if (DMA2->LISR & DMA_LISR_TCIF1)
    {
        // 清除中断标志
        DMA2->LIFCR |= DMA_LIFCR_CTCIF1;
        
        // 获取当前有效缓冲区
        uint8_t *current_buffer;
        if (DMA2_Stream1->CR & DMA_SxCR_CT)
            current_buffer = frame_buffer1;
        else
            current_buffer = frame_buffer0;
        
        // 查找JPEG帧头帧尾
        process_jpeg_frame(current_buffer, FRAME_BUFFER_SIZE);
    }
}

这里有个实际经验:OV2640输出的JPEG数据可能不是从缓冲区起始位置开始的。也就是说,SOI可能出现在缓冲区的中间位置。所以扫描的时候不能只查开头,要全缓冲区搜索。

核心要点:JPEG数据流是变长的,一帧图像压缩后的大小不固定。缓冲区大小要能容纳最大可能的帧。我一般按未压缩数据的1/10估算——320x240的RGB565是153600字节,JPEG大概15-30KB,留够余量。

3.5 整体数据流架构

把上面几块串起来,整个数据流是这样的:

图像数据采集架构 OV2640 图像传感器 DCMI接口 并行数据捕获 DMA控制器 双缓冲传输 Buf0 Buf1 CPU处理 JPEG解析/AI推理 数据流向说明 ① 8位并行数据 ② VSYNC/HSYNC同步 ③ PCLK像素时钟

从图上能看明白:OV2640输出8位并行数据,DCMI负责捕获并拼成完整的像素数据,DMA把数据搬到双缓冲区,CPU在中断里处理已经填满的那个缓冲区。

这个架构的好处是:CPU几乎不用管数据搬运的事,专心做图像处理就行。我实测过,320x240的JPEG流,DMA占用率不到5%,CPU有95%以上的算力留给AI推理。

经验之谈:双缓冲的切换时机要把握好。我习惯在DMA传输完成中断里切换,同时用一个全局变量标记当前可用的缓冲区。CPU主循环里轮询这个变量,有数据就处理,处理完再清标记。这样不会丢帧,也不会重复处理。