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里,摄像头根本不工作。
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
}
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可能出现在缓冲区的中间位置。所以扫描的时候不能只查开头,要全缓冲区搜索。
3.5 整体数据流架构
把上面几块串起来,整个数据流是这样的:
从图上能看明白:OV2640输出8位并行数据,DCMI负责捕获并拼成完整的像素数据,DMA把数据搬到双缓冲区,CPU在中断里处理已经填满的那个缓冲区。
这个架构的好处是:CPU几乎不用管数据搬运的事,专心做图像处理就行。我实测过,320x240的JPEG流,DMA占用率不到5%,CPU有95%以上的算力留给AI推理。