4. 图像采集基础:DCMI接口原理与配置,DMA双缓冲机制实现,JPEG与RGB565格式转换
图像采集是边缘AI视觉应用的第一道关卡。说白了,摄像头数据进不来,后面算法再牛也没用。STM32的DCMI接口就是专门干这活的——它能把摄像头传来的并行数据,有条不紊地塞进内存。
我个人习惯把DCMI理解成一个“数据搬运工”。它不负责处理图像内容,只管把像素时钟、行同步、帧同步这些时序信号对齐,然后把数据一股脑儿推给DMA。嗯,这里要注意:DCMI本身没有大容量FIFO,所以DMA必须跟上节奏,否则数据就会丢。
4.1 DCMI接口硬件原理
DCMI是一个同步并行接口,支持8位、10位、12位、14位数据宽度。实际项目中,8位模式用得最多——因为大部分OV系列摄像头输出就是8位。
它的核心信号就这么几根:
| 信号名 | 方向 | 作用 |
|---|---|---|
| DCMI_PIXCLK | 输入 | 像素时钟,每个上升沿采集一次数据 |
| DCMI_HSYNC | 输入 | 行同步,标志一行数据开始/结束 |
| DCMI_VSYNC | 输入 | 帧同步,标志一帧图像开始/结束 |
| DCMI_D[0:7] | 输入 | 8位并行数据总线 |
我在项目中遇到过一个问题:摄像头输出的VSYNC极性跟STM32默认配置相反。结果图像一直采集不到,折腾了半天才发现是极性没配对。所以,拿到摄像头先看数据手册,确认VSYNC和HSYNC的有效电平。
4.2 DCMI初始化配置
配置DCMI其实不复杂,关键是把几个结构体填对。我用的是STM32CubeMX生成的初始化代码,但有些细节需要手动调整。
// DCMI初始化结构体
DCMI_InitTypeDef hdcmi_init;
hdcmi_init.SynchroMode = DCMI_SYNCHRO_HARDWARE; // 硬件同步模式
hdcmi_init.PCKPolarity = DCMI_PCKPOLARITY_RISING; // 像素时钟上升沿采集
hdcmi_init.VSPolarity = DCMI_VSPOLARITY_HIGH; // VSYNC高电平有效
hdcmi_init.HSPolarity = DCMI_HSPOLARITY_HIGH; // HSYNC高电平有效
hdcmi_init.CaptureRate = DCMI_CR_ALL_FRAME; // 捕获所有帧
hdcmi_init.ExtendedData = DCMI_EXTEND_DATA_8B; // 8位数据模式
HAL_DCMI_Init(&hdcmi, &hdcmi_init);
你想想看,为什么CaptureRate要配成ALL_FRAME?因为做图像识别需要每一帧数据,不能丢帧。如果只是预览显示,可以配成隔帧捕获来省带宽。
4.3 DMA双缓冲机制
单缓冲的问题很明显:DCMI往缓冲区写数据时,CPU不能读,否则读到的是半帧残影。双缓冲就是解决这个问题的——一个缓冲区在“后台”被DCMI填数据,另一个在“前台”被CPU或算法处理。
我建议用DMA的双缓冲模式(Double Buffer Mode),而不是手动切换指针。STM32的DMA支持硬件自动切换,省心省力。
// 定义两个缓冲区
#define FRAME_WIDTH 320
#define FRAME_HEIGHT 240
#define BUFFER_SIZE (FRAME_WIDTH * FRAME_HEIGHT * 2) // RGB565每个像素2字节
uint8_t frame_buffer0[BUFFER_SIZE] __attribute__((aligned(32)));
uint8_t frame_buffer1[BUFFER_SIZE] __attribute__((aligned(32)));
// 配置DMA双缓冲
HAL_DMAEx_ConfigMuxSync(&hdma_dcmi, DMA_SYNC_FREE);
HAL_DMAEx_ConfigMuxSync(&hdma_dcmi, DMA_SYNC_FREE);
// 启动DCMI捕获,使用双缓冲
HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS,
(uint32_t)frame_buffer0,
BUFFER_SIZE / 2); // 注意:这里是半字计数
// 注册DMA传输完成回调
void HAL_DCMI_LineEventCallback(DCMI_HandleTypeDef *hdcmi)
{
// 每完成一行数据,可以在这里做行处理
// 但通常我们等帧完成回调
}
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
// 一帧采集完成,切换缓冲区
// 此时frame_buffer0或frame_buffer1中有一帧完整数据
// 另一个缓冲区正在被DCMI填充
process_image(current_buffer);
}
4.4 JPEG与RGB565格式转换
摄像头输出的原始数据通常是JPEG压缩格式(比如OV2640),但STM32的AI库(比如STM32Cube.AI)输入要求是RGB565或RGB888。所以,格式转换是绕不开的一步。
JPEG解码在STM32上是个体力活。硬件JPEG编解码器(STM32F7/H7系列有)能大幅加速,但F4系列只能靠软件。我推荐用TinyJPEG或picojpeg这类轻量级库,内存占用小。
转换流程大致如下:
- 从DCMI缓冲区拿到JPEG数据流
- 调用JPEG解码库,逐MCU解码
- 将解码后的YUV数据转为RGB565
- 把RGB565数据存入AI模型的输入缓冲区
// JPEG解码 + YUV转RGB565 简化示例
void jpeg_to_rgb565(uint8_t *jpeg_data, uint32_t jpeg_len,
uint8_t *rgb565_out, uint32_t width, uint32_t height)
{
// 初始化JPEG解码器
tjdec_handle_t *jdec = tjdec_init();
// 设置输入缓冲区
tjdec_set_input_buffer(jdec, jpeg_data, jpeg_len);
// 读取JPEG头信息
int w, h;
tjdec_read_header(jdec, &w, &h);
// 解码到YUV缓冲区
uint8_t *yuv_buffer = malloc(w * h * 3 / 2); // YUV420格式
tjdec_decode(jdec, yuv_buffer, TJDEC_PIX_FMT_YUV420);
// YUV420转RGB565
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int y_idx = y * w + x;
int u_idx = (y / 2) * (w / 2) + (x / 2);
int v_idx = u_idx;
int Y = yuv_buffer[y_idx];
int U = yuv_buffer[w * h + u_idx] - 128;
int V = yuv_buffer[w * h * 5 / 4 + v_idx] - 128;
// YUV转RGB
int R = Y + 1.402 * V;
int G = Y - 0.344 * U - 0.714 * V;
int B = Y + 1.772 * U;
// 钳位到0-255
R = (R < 0) ? 0 : (R > 255) ? 255 : R;
G = (G < 0) ? 0 : (G > 255) ? 255 : G;
B = (B < 0) ? 0 : (B > 255) ? 255 : B;
// 打包为RGB565
uint16_t rgb565 = ((R >> 3) << 11) |
((G >> 2) << 5) |
(B >> 3);
((uint16_t *)rgb565_out)[y * w + x] = rgb565;
}
}
free(yuv_buffer);
tjdec_free(jdec);
}
4.5 整体数据流架构
把上面几个环节串起来,就是完整的图像采集链路。我画了一张图,帮你理清数据流向。
这张图把整个链路画清楚了。从摄像头到DCMI,再到DMA双缓冲,然后经过JPEG解码和格式转换,最终喂给AI推理引擎。每个环节的延迟都会影响整体帧率。
嗯,图像采集这块就讲这么多。记住一个原则:数据流要通畅,瓶颈要提前解决。DCMI配置对了,DMA双缓冲用上了,格式转换优化好了,后面的AI推理才能跑得稳。