4、摄像头驱动与数据采集:OV2640/OV7670驱动原理、DCMI接口配置、DMA传输,获取第一帧图像
好,咱们进入第四章。这一章可以说是整个课程的分水岭——前面讲理论,从这章开始,你就要真正跟硬件打交道了。
摄像头驱动,说白了就是让MCU和摄像头“说上话”。我刚开始做这个的时候,以为接上线就能出图像,结果折腾了两天才发现是初始化顺序搞反了。嗯,咱们今天就把这些坑一个个填平。
4.1 OV2640与OV7670:选谁?怎么驱动?
先说说这两款经典的摄像头模组。OV7670是30万像素的老将,OV2640是200万像素的升级版。我个人习惯,如果只是做简单的颜色识别、二维码扫描,OV7670完全够用,还便宜。但如果你要做人脸检测或者稍微复杂点的视觉任务,OV2640是底线。
它们的驱动原理其实是一样的,都是通过SCCB总线(跟I2C几乎一样)来配置内部寄存器。你想想看,摄像头就像一台相机,你要告诉它:输出多大分辨率、用什么格式、帧率多少。这些信息就写在寄存器里。
核心要点:驱动摄像头 = 通过SCCB写寄存器 + 通过DCMI读像素数据
我曾经在一个项目中,OV7670死活不出图像,后来发现是寄存器0x12(输出格式控制)没配置对。默认输出的是RGB565,而我的DCMI配置成了YUV。这种低级错误,排查起来真的很痛苦。
4.1.1 SCCB初始化流程
初始化流程其实不复杂,但顺序很重要。我总结了一个“三步走”策略:
- 复位摄像头:拉低RESET引脚至少10ms,再拉高。这一步是让摄像头回到已知状态。
- 配置时钟:OV7670需要24MHz的XCLK输入,OV2640可以到25MHz。注意,STM32的MCO引脚可以直接输出这个时钟。
- 写入寄存器序列:这是最关键的。每个摄像头都有官方推荐的初始化序列,几百个寄存器要按顺序写。
我的小技巧:不要自己手写寄存器配置。去官网下载初始化数组,直接复制到代码里。我见过太多人因为写错一个寄存器值,图像颜色全乱的。
下面是一个简化的OV7670初始化代码片段:
// SCCB写寄存器函数
uint8_t SCCB_WriteReg(uint8_t reg, uint8_t value) {
// 启动I2C通信
I2C_Start();
// 发送设备地址(写)
I2C_SendByte(0x42);
// 等待应答
if (!I2C_WaitAck()) return 1;
// 发送寄存器地址
I2C_SendByte(reg);
I2C_WaitAck();
// 发送数据
I2C_SendByte(value);
I2C_WaitAck();
// 停止通信
I2C_Stop();
return 0;
}
// 初始化OV7670(QVGA格式,RGB565)
void OV7670_Init(void) {
// 复位
HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_SET);
HAL_Delay(10);
// 写入初始化序列
for (int i = 0; ov7670_init_regs[i][0] != 0xFF; i++) {
SCCB_WriteReg(ov7670_init_regs[i][0], ov7670_init_regs[i][1]);
}
}
4.2 DCMI接口配置:让数据流进来
摄像头配置好了,它就开始往外吐像素数据了。但MCU怎么接住这些数据?靠的就是DCMI——STM32的数字摄像头接口。
DCMI说白了就是一个并行数据接收器。它有几个关键信号:
| 信号名 | 作用 | 说明 |
|---|---|---|
| DCMI_D[0:7] | 数据线 | 8位并行数据,每个PCLK时钟传一个字节 |
| DCMI_PCLK | 像素时钟 | 由摄像头提供,每个上升沿采集数据 |
| DCMI_HSYNC | 行同步 | 高电平表示一行数据正在传输 |
| DCMI_VSYNC | 帧同步 | 高电平表示一帧数据开始/结束 |
配置DCMI的时候,有几点要特别注意:
- 数据格式要匹配:摄像头输出RGB565,DCMI就要配置成RGB565模式。否则数据解析出来全是乱的。
- 同步极性要正确:OV7670的VSYNC是高电平有效,但有些摄像头是低电平有效。配置反了,DCMI永远收不到帧开始信号。
- 时钟边沿选择:我建议用PCLK的上升沿采集数据。下降沿采集容易受信号抖动影响。
注意:DCMI的时钟频率不能太高。STM32F4系列最高支持到54MHz,但实际使用中,超过30MHz就容易丢数据。我一般控制在20MHz左右,稳定第一。
4.3 DMA传输:别让CPU干苦力
好,现在DCMI能收到数据了。但问题来了——一帧QVGA(320x240)的RGB565图像,数据量是320×240×2 = 153,600字节。如果让CPU一个一个字节去读,那CPU啥也别干了,光搬数据就累死。
这时候DMA就该上场了。DMA说白了就是一个“数据搬运工”,它可以在不占用CPU的情况下,把DCMI收到的数据直接搬到内存里。
配置DMA时,我习惯用双缓冲模式。为什么?
你想想看,如果只有一个缓冲区,DMA正在往里面写数据,CPU想处理上一帧图像,就得等DMA写完。这一等,帧率就掉下来了。
双缓冲就不一样了:DMA往缓冲区A写第N帧,CPU同时处理缓冲区B里的第N-1帧。写完了,DMA自动切到缓冲区B,CPU切到缓冲区A。这样流水线作业,帧率能翻倍。
// DMA双缓冲配置示例
// 定义两个缓冲区
uint8_t frame_buffer_1[320 * 240 * 2]; // 缓冲区A
uint8_t frame_buffer_2[320 * 240 * 2]; // 缓冲区B
// 配置DMA
void DMA_Config(void) {
// 使能DMA2时钟
__HAL_RCC_DMA2_CLK_ENABLE();
// 配置DMA流
hdma_dcmi.Instance = DMA2_Stream1;
hdma_dcmi.Init.Channel = DMA_CHANNEL_1;
hdma_dcmi.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_dcmi.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增
hdma_dcmi.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma_dcmi.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_dcmi.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_dcmi.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_dcmi.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_dcmi);
// 关联DMA到DCMI
__HAL_LINKDMA(&hdma_dcmi, hdma_dcmi, hdma_dcmi);
// 启动DMA双缓冲传输
HAL_DMAEx_ChangeMemory(&hdma_dcmi, 0, (uint32_t)frame_buffer_1, DMA_MEMORY_0);
HAL_DMAEx_ChangeMemory(&hdma_dcmi, 1, (uint32_t)frame_buffer_2, DMA_MEMORY_1);
}
4.4 获取第一帧图像:从零到一
好了,所有配置都完成了。现在我们来获取第一帧图像。这一步其实很简单,但第一次看到图像出现在屏幕上时,那种成就感真的很棒。
流程是这样的:
- 启动DCMI捕获
- 等待VSYNC中断(表示一帧开始)
- DMA自动把数据搬到缓冲区
- 等待传输完成中断(表示一帧结束)
- 处理缓冲区中的图像数据
// 启动捕获
void DCMI_StartCapture(void) {
// 使能DCMI
HAL_DCMI_Start_DMA(&hdcmi, DMA_MODE_CONTINUOUS,
(uint32_t)frame_buffer_1,
(320 * 240 * 2) / 4); // 传输的字数
}
// 帧传输完成回调
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi) {
// 切换缓冲区指针
current_buffer = (current_buffer == frame_buffer_1) ?
frame_buffer_2 : frame_buffer_1;
// 处理图像(比如显示到LCD)
ProcessImage(current_buffer);
}
避坑指南:我第一次调试时,图像一直出不来。后来发现是DMA传输的字数算错了。DCMI的DMA传输是以字(4字节)为单位的,所以传输字数 = 总字节数 / 4。对于QVGA RGB565,就是153600 / 4 = 38400个字。这个坑,我替你们踩过了。
4.5 实战经验总结
最后,分享几个我在项目中积累的经验:
- 先测SCCB通信:不要一上来就搞DCMI。先用逻辑分析仪抓SCCB波形,确认能正确读写寄存器。这一步能排除一半的问题。
- 用示波器看PCLK:如果DCMI收不到数据,先看PCLK有没有波形。没有波形,说明摄像头没工作,检查电源和复位。
- 第一帧可能不完整:DMA启动后,第一帧数据可能只有半帧。我习惯丢弃第一帧,从第二帧开始处理。
- 注意内存对齐:DMA缓冲区要4字节对齐。用__attribute__((aligned(4)))声明,否则DMA会报错。
嗯,这一章的内容就到这里。下一章我们会讲图像预处理——拿到原始图像数据后,怎么把它变成算法能用的格式。到时候见。