3、图像采集与DMA传输:DMA原理简介、配置DMA进行图像采集、双缓冲机制、帧同步与行同步信号处理

好,咱们进入第三章。图像采集这块,说白了就是让摄像头把数据塞进内存。但CPU去搬数据?太慢了,也太浪费了。这时候,DMA就该登场了。

3.1 DMA原理简介——为什么需要它?

DMA,全称Direct Memory Access,直接存储器访问。名字挺唬人,其实干的事很简单:它是个硬件搬运工

你想想看,如果没有DMA,CPU得亲自去读摄像头接口的FIFO,然后写到内存里。每读一个像素,CPU就被打断一次。对于VGA级别的图像(640x480),一帧就有30多万个像素。CPU啥也别干了,光搬数据就累死。

DMA的作用就是:CPU告诉它“从哪搬,搬到哪,搬多少”,然后它自己干,干完了通知CPU一声。CPU就可以腾出手来做图像处理、跑算法,或者干脆休眠省电。

核心要点:DMA传输不占用CPU内核,它和CPU共享总线,但通过总线仲裁来协调。说白了,DMA是CPU的“好帮手”,帮它干脏活累活。

我个人习惯把DMA分成三类:

  • 内存到内存:比如把一帧图像从缓冲区A拷贝到缓冲区B。这个最慢,因为要同时读写内存。
  • 外设到内存:这就是咱们图像采集的场景。摄像头接口(比如DCMI、CSI)产生数据,DMA直接搬进内存。
  • 内存到外设:比如把图像数据通过SPI或并口发送出去。显示驱动常用。

嗯,这里要注意:DMA不是万能的。它适合“大数据块、低频率中断”的场景。如果你要搬几个字节,那中断处理反而更快,因为DMA的配置开销太大了。

3.2 配置DMA进行图像采集——实战步骤

好,咱们直接上手。以STM32的DCMI(数字摄像头接口)为例,配合DMA来采集图像。我当年第一次调这个,折腾了两天才跑通,踩了不少坑。

配置流程大致如下:

  1. 初始化摄像头:通过I2C或SPI配置摄像头寄存器,输出格式、分辨率、帧率。
  2. 配置DCMI接口:设置数据线宽度(8位/10位/12位/14位)、同步信号极性、捕获模式。
  3. 配置DMA:设置源地址(DCMI数据寄存器)、目的地址(内存缓冲区)、传输数据量、传输模式。
  4. 启动传输:先使能DMA,再使能DCMI捕获。

来看一段核心代码,这是DMA配置的部分:

// DMA配置结构体
DMA_InitTypeDef DMA_InitStruct;

// 使能DMA2时钟(DCMI通常挂在DMA2上)
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

// 选择DMA2数据流1(具体看芯片手册)
DMA_InitStruct.DMA_Channel = DMA_Channel_1;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&DCMI->DR;  // 外设地址:DCMI数据寄存器
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)frame_buffer;  // 内存地址:帧缓冲区
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;          // 方向:外设到内存
DMA_InitStruct.DMA_BufferSize = 640 * 480 * 2;               // 传输大小:一帧数据(RGB565格式)
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;         // 内存地址递增
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位数据
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;                 // 循环模式,用于连续采集
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

DMA_Init(DMA2_Stream1, &DMA_InitStruct);

// 使能DMA传输完成中断
DMA_ITConfig(DMA2_Stream1, DMA_IT_TC, ENABLE);

// 使能DMA流
DMA_Cmd(DMA2_Stream1, ENABLE);

我的经验:配置DMA时,最容易出错的是数据宽度。摄像头输出RGB565,每个像素16位,所以数据宽度要设成HalfWord(半字)。如果你设成Byte,那数据就全乱了,颜色会错位。我曾经因为这个查了一整天,最后发现是数据宽度没对齐。

3.3 双缓冲机制——让采集和处理并行

单缓冲的问题很明显:DMA正在往缓冲区A写数据,CPU想处理这帧图像,但数据还没写完。要么等,要么读到一半的数据——那画面就撕裂了。

双缓冲就是解决这个问题的。说白了,就是准备两个缓冲区:

  • 缓冲区A:DMA正在往里写数据(前台缓冲)
  • 缓冲区B:CPU正在处理里面的数据(后台缓冲)

当DMA写完一帧,触发中断。在中断里,交换两个缓冲区的角色。DMA开始往B写,CPU处理A里的数据。如此循环往复。

实现方式有两种:

方式 说明 适用场景
软件切换 在DMA传输完成中断里,手动修改DMA的内存目标地址 DMA不支持硬件双缓冲时使用
硬件双缓冲 DMA控制器原生支持双缓冲,自动切换 性能要求高,减少中断开销

软件切换的代码片段:

// 定义两个缓冲区
uint16_t frame_buffer_A[640 * 480];
uint16_t frame_buffer_B[640 * 480];
volatile uint8_t active_buffer = 0;  // 0表示A,1表示B

// DMA传输完成中断服务函数
void DMA2_Stream1_IRQHandler(void) {
    if(DMA_GetITStatus(DMA2_Stream1, DMA_IT_TCIF1)) {
        // 清除中断标志
        DMA_ClearITPendingBit(DMA2_Stream1, DMA_IT_TCIF1);
        
        // 切换缓冲区
        if(active_buffer == 0) {
            // 当前DMA写的是A,下一帧写B
            DMA_MemoryTargetConfig(DMA2_Stream1, (uint32_t)frame_buffer_B, DMA_MemoryTarget_0);
            active_buffer = 1;
            // CPU可以处理frame_buffer_A中的数据
            process_image(frame_buffer_A);
        } else {
            DMA_MemoryTargetConfig(DMA2_Stream1, (uint32_t)frame_buffer_A, DMA_MemoryTarget_0);
            active_buffer = 0;
            process_image(frame_buffer_B);
        }
    }
}

注意:在中断里做图像处理是大忌!中断服务函数要短平快,只做标志位切换和缓冲区交换。真正的图像处理算法,放在主循环里做。我曾经见过有人把整个JPEG解码放在中断里,结果系统直接卡死。

3.4 帧同步与行同步信号处理——时序是关键

摄像头输出的不是一坨数据,而是有节奏的。这个节奏由两个信号控制:

  • VSYNC(帧同步):一帧开始或结束的标志。高电平有效或低电平有效,看配置。
  • HSYNC(行同步):一行数据开始或结束的标志。

DCMI接口就是靠这两个信号来“切分”数据流的。配置时要注意:

  1. 极性选择:摄像头输出的VSYNC和HSYNC是正极性还是负极性?得和摄像头数据手册对上。
  2. 捕获模式:是VSYNC上升沿开始捕获,还是下降沿?
  3. 数据使能信号:有些摄像头还有PCLK(像素时钟)和DEN(数据使能),DCMI可以配置成用DEN来选通数据。

来看DCMI的配置代码:

DCMI_InitTypeDef DCMI_InitStruct;

// 使能DCMI时钟
RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_DCMI, ENABLE);

// DCMI配置
DCMI_InitStruct.DCMI_CaptureMode = DCMI_CaptureMode_Snap;    // 快照模式(单帧)或连续模式
DCMI_InitStruct.DCMI_SynchroMode = DCMI_SynchroMode_Hardware; // 硬件同步(VSYNC/HSYNC)
DCMI_InitStruct.DCMI_PCKPolarity = DCMI_PCKPolarity_Rising;  // 像素时钟上升沿采样
DCMI_InitStruct.DCMI_VSPolarity = DCMI_VSPolarity_Low;       // VSYNC低电平有效
DCMI_InitStruct.DCMI_HSPolarity = DCMI_HSPolarity_Low;       // HSYNC低电平有效
DCMI_InitStruct.DCMI_CaptureRate = DCMI_CaptureRate_All;     // 所有像素都捕获
DCMI_InitStruct.DCMI_ExtendedDataMode = DCMI_ExtendedDataMode_8b; // 8位数据模式

DCMI_Init(&DCMI_InitStruct);

// 使能DCMI
DCMI_Cmd(ENABLE);

这里有个坑:帧同步信号的毛刺。摄像头上电瞬间,VSYNC可能会抖动,产生虚假的帧起始信号。我遇到过这种情况:一上电,DCMI就触发了一次捕获,但数据全是乱的。

避坑指南:我曾经在项目里加了一个软件去抖机制。在检测到VSYNC有效后,启动一个短延时(比如100微秒),再确认VSYNC仍然有效,才认为真正的帧起始到了。虽然浪费了一点时间,但可靠性大大提升。

另外,行同步信号的处理也很关键。DMA传输时,每行数据是连续写入内存的。但有些摄像头在行消隐期会输出无效数据。这时候,你可以配置DCMI的行长度寄存器,让它只捕获有效像素,跳过消隐区。

嗯,总结一下这一章的核心:

  • DMA是图像采集的“搬运工”,解放CPU
  • 双缓冲让采集和处理并行,避免画面撕裂
  • 帧同步和行同步信号是时序的“节拍器”,极性配置不能错

下一章,咱们聊聊图像预处理——去噪、色彩校正这些。到时候你会发现,采集回来的数据,往往不是能直接用的。