4. 图像数据采集:DMA配置与帧缓冲管理、VSYNC/HSYNC同步信号处理、数据格式解析
图像采集,说白了就是让摄像头传感器把光信号变成数字信号,再搬到内存里。这一步做不好,后面所有算法都是白搭。我见过不少项目,图像处理算法调得天花乱坠,结果一查原始数据就有问题——丢帧、错位、颜色不对。嗯,今天我们就来把这层窗户纸捅破。
4.1 同步信号:VSYNC与HSYNC的脾气
摄像头传感器输出图像时,不是一股脑全丢过来的。它有一套握手协议,核心就是两个信号:VSYNC(垂直同步)和HSYNC(水平同步)。
- VSYNC:一帧开始的标志。高电平或低电平跳变,告诉控制器“新的一帧来了”。
- HSYNC:一行开始的标志。每扫完一行像素,就拉一下。
你想想看,如果没有这两个信号,你怎么知道哪一行是第一行?哪个像素是第一个?我曾经在一个项目中,把VSYNC和HSYNC的极性搞反了,结果图像上下颠倒、左右镜像,折腾了两天才发现是寄存器配错了。
关键点:VSYNC/HSYNC的极性(高有效还是低有效)必须和传感器数据手册严格一致。大部分传感器允许通过寄存器配置极性,但默认值不一定符合你的控制器要求。
4.2 DMA配置:别让CPU干苦力
图像数据量有多大?拿一个500万像素的RAW10来说,一帧就是5MB。如果每秒30帧,那就是150MB/s的数据吞吐量。让CPU一个一个像素去搬?不现实。这时候DMA(直接存储器访问)就是救星。
我个人习惯把DMA配置分成三步:
- 源地址:一般是摄像头接口的FIFO数据寄存器,固定地址。
- 目的地址:内存中的帧缓冲地址,通常是链表或环形缓冲。
- 传输长度:一帧的总字节数,或者一行的大小(取决于你按帧触发还是按行触发)。
我的经验:尽量使用双缓冲(ping-pong buffer)。一个缓冲在DMA写入,另一个缓冲给CPU处理。这样CPU和DMA可以并行工作,帧率能提升近一倍。
下面是一个典型的DMA初始化代码片段(以STM32为例):
// 配置DMA传输:从DCMI接口到内存
DMA_InitTypeDef DMA_InitStruct = {0};
// 源地址:DCMI数据寄存器(固定)
DMA_InitStruct.PeriphBaseAddr = (uint32_t)&DCMI->DR;
// 目的地址:帧缓冲1
DMA_InitStruct.MemoryBaseAddr = (uint32_t)frame_buffer1;
// 传输方向:外设到内存
DMA_InitStruct.Direction = DMA_PERIPH_TO_MEMORY;
// 传输大小:一帧数据量(例如640*480*2字节)
DMA_InitStruct.BufferSize = 640 * 480 * 2;
// 外设地址递增:不需要,因为每次都读同一个寄存器
DMA_InitStruct.PeriphInc = DMA_PINC_DISABLE;
// 内存地址递增:需要,数据要连续存放
DMA_InitStruct.MemInc = DMA_MINC_ENABLE;
// 配置完成后,使能DMA
HAL_DMA_Init(&hdma_dcmi, &DMA_InitStruct);
注意:DMA传输完成中断里,一定要及时切换缓冲指针。我曾经因为中断处理函数里做了太多运算,导致下一帧数据覆盖了当前缓冲,画面出现撕裂。解决办法:中断里只做指针切换和标志位设置,数据处理放到主循环或任务里。
4.3 帧缓冲管理:环形缓冲与双缓冲
帧缓冲管理,说白了就是怎么安排内存,让采集和处理不打架。常用的方案有两种:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 双缓冲 | 实现简单,延迟低 | 缓冲切换时可能丢一帧 | 实时性要求高的场景 |
| 环形缓冲 | 不丢帧,适合连续采集 | 管理复杂,内存占用大 | 高帧率、需要回溯的场景 |
我个人更推荐环形缓冲,尤其是做视频流处理时。你想想看,如果处理速度跟不上采集速度,双缓冲会直接覆盖旧帧,而环形缓冲至少能保留最近N帧,给你一个缓冲余地。
环形缓冲的核心逻辑:
// 环形缓冲结构体
typedef struct {
uint8_t *buffers[BUFFER_COUNT]; // 缓冲指针数组
volatile uint32_t write_index; // DMA当前写入的缓冲索引
volatile uint32_t read_index; // CPU当前读取的缓冲索引
} ring_buffer_t;
// DMA完成中断中:更新写索引
void DMA_Complete_IRQHandler(void) {
// 切换到下一个缓冲
ring_buf.write_index = (ring_buf.write_index + 1) % BUFFER_COUNT;
// 更新DMA目的地址
DMA->M0AR = (uint32_t)ring_buf.buffers[ring_buf.write_index];
}
// 主循环中:读取最新帧
void process_frame(void) {
if (ring_buf.read_index != ring_buf.write_index) {
// 处理当前帧
process_image(ring_buf.buffers[ring_buf.read_index]);
// 更新读索引
ring_buf.read_index = (ring_buf.read_index + 1) % BUFFER_COUNT;
}
}
避坑指南:我曾经在环形缓冲里忘了处理“写指针追上读指针”的情况,结果导致未处理的帧被覆盖,图像出现跳帧。解决方案:在写指针更新前,检查是否即将追上读指针,如果是,则丢弃旧帧或降低处理帧率。
4.4 数据格式解析:RAW、YUV、JPEG
摄像头传感器输出的原始数据,通常有三种格式:RAW、YUV、JPEG。每种格式的解析方式完全不同。
4.4.1 RAW格式
RAW是最原始的数据,每个像素只记录一个颜色分量(R、G、B之一),通过拜耳阵列排列。解析RAW的关键是去马赛克(demosaic),也就是把缺失的颜色插值出来。
常见的RAW格式有:RAW8、RAW10、RAW12。位数越高,动态范围越大,但数据量也越大。
我的经验:RAW10在嵌入式系统中很常见,但很多DMA控制器只支持8位或16位传输。我的做法是把两个RAW10像素拼成一个32位字,或者用16位模式传输,高位补零。这样虽然浪费了一点带宽,但兼容性好。
4.4.2 YUV格式
YUV把亮度(Y)和色度(U、V)分开。人眼对亮度更敏感,所以色度可以压缩。常见的YUV格式有:
- YUV422:每两个像素共享一组UV,数据量是RAW的2/3。
- YUV420:每四个像素共享一组UV,压缩率更高。
解析YUV时,要注意存储顺序。有的传感器输出是YUYV,有的是UYVY,搞错了颜色就全乱了。
// YUYV转RGB888示例
void yuyv_to_rgb(uint8_t *yuyv, uint8_t *rgb, int width, int height) {
for (int i = 0; i < width * height / 2; i++) {
uint8_t y0 = yuyv[i * 4 + 0];
uint8_t u = yuyv[i * 4 + 1];
uint8_t y1 = yuyv[i * 4 + 2];
uint8_t v = yuyv[i * 4 + 3];
// 像素0
rgb[i * 6 + 0] = y0 + 1.402 * (v - 128); // R
rgb[i * 6 + 1] = y0 - 0.344 * (u - 128) - 0.714 * (v - 128); // G
rgb[i * 6 + 2] = y0 + 1.772 * (u - 128); // B
// 像素1
rgb[i * 6 + 3] = y1 + 1.402 * (v - 128);
rgb[i * 6 + 4] = y1 - 0.344 * (u - 128) - 0.714 * (v - 128);
rgb[i * 6 + 5] = y1 + 1.772 * (u - 128);
}
}
4.4.3 JPEG格式
JPEG是压缩格式,传感器内部集成了JPEG编码器。解析JPEG需要软件解码库(如libjpeg-turbo),或者硬件JPEG解码器。
JPEG的好处是数据量小,但解码需要额外的计算资源。我建议:如果MCU性能足够,用JPEG可以大幅降低存储和传输带宽;如果MCU性能有限,还是用YUV或RAW更省心。
注意:JPEG是有损压缩,压缩率越高,图像质量越差。在医疗、工业检测等场景,尽量用RAW或无损压缩格式。
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。从传感器输出同步信号,到DMA搬运数据,再到帧缓冲管理,最后解析成可用格式。每一步都环环相扣。
4.6 本章小结
图像数据采集,说白了就是三个字:搬、管、解。搬靠DMA,管靠帧缓冲,解靠格式解析。每一步都有坑,但只要你理解了同步信号的脾气、DMA的配置套路、缓冲管理的策略,以及不同格式的解析方法,就能稳稳地把图像数据拿到手。
嗯,我记得刚入行时,第一次调摄像头驱动,花了整整一周才看到第一帧图像。现在回头看,其实就是VSYNC极性配反了。所以,别怕踩坑,踩过了就记住了。