【STM32H757XIH6 双核】驱动 OV5640 摄像头模块 -> OV5640 三缓冲采集 + 双缓冲 DMA2D 搬运图像
1. 引言
在 STM32 上使用摄像头,读取芯片 ID、配置寄存器并采集一张图像,只是把摄像头运行起来的第一步。要把摄像头画面连续显示到 LCD,还需要协调图像采集、内存搬运和屏幕刷新之间的关系。
摄像头按照自身时序不断输出像素,LCD 则按照显示时序读取帧缓冲。如果采集程序正在修改 LCD 读取的同一块内存,屏幕上就可能同时出现前后两帧的内容,产生画面撕裂。而采用“采集一帧、显示一帧、再开始下一次采集”的方式,又会在两次采集之间引入等待,不利于连续预览。
本文基于 STM32H757 工程,使用 OV5640 输出 800×480 的 RGB565 图像,由 CM7 完成摄像头配置、连续采集和 LCD 显示。整个数据链路如下:
OV5640
│ 8 位并行接口,输出 RGB565 像素数据
▼
DCMI + DMA1_Stream0
│ 交替接收,每块为 800×16 像素
▼
AXI SRAM 双块缓冲
│ DMA2D 将已完成的数据块复制到对应位置
▼
SDRAM 三帧缓冲
│ 完整帧发布,等待 LTDC 换帧
▼
LTDC Layer 0
│ 垂直消隐期切换帧缓冲地址
▼
LCD
这里使用了两个层级的缓冲机制:
- 双块缓冲用于连续接收摄像头数据,让 DMA 接收下一块时,DMA2D 可以搬运上一块。
- 三帧缓冲用于管理完整图像,将正在采集、等待显示和正在显示的帧分开。
后文先介绍 OV5640 的接线、DCMI 配置和驱动移植,再说明三帧缓冲的切换规则,以及双块缓冲配合 DMA2D 拼接整帧的实现方式,最后结合串口日志说明采集和显示效果。
2. OV5640 接线与信号
OV5640 摄像头模块
┌────────────────────┐
│ │
3.3 V ─┤ VCC │
GND ──┤ GND │
│ │
PB6 ────────►│ SIOC / SCL │ SCCB 时钟
PB7 ◄───────►│ SIOD / SDA │ SCCB 数据
│ │
PH2 ────────►│ RESET │ 硬件复位
PH3 ────────►│ PWDN │ 掉电控制
│ │
│ │
PA6 ◄────────│ PCLK │ 像素时钟
PA4 ◄────────│ HREF / HSYNC │ 行同步
PG9 ◄────────│ VSYNC │ 场同步
│ │
PC6 ◄────────│ D0 │
PC7 ◄────────│ D1 │
PC8 ◄────────│ D2 │
PC9 ◄────────│ D3 │
PC11 ◄────────│ D4 │
PD3 ◄────────│ D5 │
PB8 ◄────────│ D6 │
PB9 ◄────────│ D7 │
└────────────────────┘
箭头表示信号方向:► 表示 STM32 输出到摄像头,◄ 表示摄像头输出到 STM32,◄──► 表示双向信号。
2.1 信号连接表
| OV5640 信号 | STM32H757 CM7 引脚 | 工程中的功能 | 信号方向 |
|---|---|---|---|
SIOC / SCL |
PB6 |
软件 SCCB 时钟 | STM32 → OV5640 |
SIOD / SDA |
PB7 |
软件 SCCB 数据 | 双向 |
RESET |
PH2 |
摄像头硬件复位 | STM32 → OV5640 |
PWDN |
PH3 |
摄像头掉电控制 | STM32 → OV5640 |
D0 |
PC6 |
DCMI_D0 |
OV5640 → STM32 |
D1 |
PC7 |
DCMI_D1 |
OV5640 → STM32 |
D2 |
PC8 |
DCMI_D2 |
OV5640 → STM32 |
D3 |
PC9 |
DCMI_D3 |
OV5640 → STM32 |
D4 |
PC11 |
DCMI_D4 |
OV5640 → STM32 |
D5 |
PD3 |
DCMI_D5 |
OV5640 → STM32 |
D6 |
PB8 |
DCMI_D6 |
OV5640 → STM32 |
D7 |
PB9 |
DCMI_D7 |
OV5640 → STM32 |
PCLK |
PA6 |
DCMI_PIXCLK |
OV5640 → STM32 |
HREF |
PA4 |
DCMI_HSYNC |
OV5640 → STM32 |
VSYNC |
PG9 |
DCMI_VSYNC |
OV5640 → STM32 |
VCC |
外部供电 | 摄像头电源 | 电源输入 |
GND |
系统地 | 信号参考地 | 电源地 |
2.2 DCMI 配置

- 数据宽度:8 位。
- 数据映射:
D0到DCMI_D0,依次对应到D7到DCMI_D7。 PCLK采样边沿:上升沿。VSYNC极性:高电平有效。HREF/HSYNC极性:低电平有效。- JPEG 模式:关闭。
NVIC 配置:

DMA 配置:

- DMA 普通模式: 传输完成一次就停止。
- Increment Address:
Peripheral:❌ 不递增(DCMI 的数据寄存器地址固定,每次读都是同一个地址,不能开递增)
Memory:✅ 4 Increment → 内存地址每次 + 4 字节(Word 对齐,存图像数据) - Use Fifo:
Threshold:Full→ FIFO 满了之后一次性触发传输(H7 的 DMA FIFO 深度是 4 个 Word)
FIFO 作用:缓冲 DCMI 过来的数据流,减少总线突发访问次数,提升稳定性,摄像头采集场景很常用。Threshold=Full 代表攒满 4 个 Word 才触发一次 Burst。 - Data Width
Peripheral:Word(32bit)
Memory:Word(32bit)
DCMI 接口为 8 位,图像格式为 RGB565,每像素 16 位,DMA 按 32 位 Word 搬运,意味着DMA在搬运时是以4个字节为一个单位进行的。 - Burst Size (突发长度):
Peripheral (外设端): Single (单次)。每次外设(DCMI)发来请求,DMA只从外设寄存器读取1个Word(32位)。
Memory (存储器端): 4 Increment (4次递增)。当FIFO达到Full阈值后,DMA会向内存连续突发写入4个Word(即16字节),并且内存地址会自动递增。
组合效果:外设每次给1个Word,DMA的FIFO慢慢攒数据,攒满(根据FIFO深度,如果是16字节,刚好对应4个Word)之后,一次性以突发模式(Burst)写入内存。这样能极大减少对总线(AHB)的占用,提高系统整体性能。
需要注意:
- 内存对齐问题,在Memory端配置了 4 Increment 的突发长度,这就意味着目标内存地址必须按16字节对齐。
- DCMI数据宽度匹配,确保在DCMI外设的配置中,数据宽度设置与这里的 Word (32位) 一致。
- FIFO阈值与突发匹配,Full 阈值加上 4 Increment 是一个经典搭配。如果您的内存非常紧张或者总线很忙,这种配置是最优的。
2.3 复位和掉电控制逻辑
2.3.1 RESET(PH2)
PH2 = 0:硬件复位有效。PH2 = 1:摄像头正常运行。
2.3.2 PWDN(PH3)
-
PH3 = 1:摄像头进入掉电状态。 -
PH3 = 0:摄像头退出掉电状态。工程初始化流程为:
RESET = 1,PWDN = 1。RESET = 0,等待 20 ms。PWDN = 0,等待 5 ms。RESET = 1,等待 20 ms。- 再执行一次硬件复位,然后通过 SCCB 读取芯片 ID。
2.4 SCCB 配置
- SCCB 类型:GPIO 软件模拟时序。
SCL:PB6。SDA:PB7。- 工程定义的 OV5640 SCCB 地址:
0x3C。
当前PB6和PB7在 GPIO 初始化中配置为高速推挽输出,并由软件 SCCB 驱动。
3. OV5640 驱动源码

其中ov5640_cfg.h、ov5640_dcmi.c/h、ov5640.c/h基于正点原子驱动移植并修改,这里就给出软件 I2C 驱动的 ov5640_sccb.c/h 源码,后续会给出相关代码的修改事项:
3.1 ov5640_sccb.c
/**
****************************************************************************************************
* @file ov5640_sccb.c
* @brief OV5640模块SCCB接口驱动代码(软件模拟,PB6=SCL, PB7=SDA)
****************************************************************************************************
*/
#include "ov5640_sccb.h"
#include "bsp_delay.h"
#define ov5640_SCCB_WRITE 0x00
#define ov5640_SCCB_READ 0x01
/** 将SDA配置为带上拉的推挽输出。 */
static void ov5640_sccb_set_sda_output(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
gpio_init_struct.Pin = OV5640_SDA_Pin;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(OV5640_SDA_GPIO_Port, &gpio_init_struct);
}
/** 将SDA配置为带上拉的输入,用于读取传感器返回数据。 */
static void ov5640_sccb_set_sda_input(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
gpio_init_struct.Pin = OV5640_SDA_Pin;
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(OV5640_SDA_GPIO_Port, &gpio_init_struct);
}
/** 产生SCCB时序所需的短延时。 */
static inline void ov5640_sccb_delay(void)
{
delay_us(5);
}
/** 产生SCCB起始条件。 */
static void ov5640_sccb_start(void)
{
OV5640_SCCB_SDA(1);
OV5640_SCCB_SCL(1);
ov5640_sccb_delay();
OV5640_SCCB_SDA(0);
ov5640_sccb_delay();
OV5640_SCCB_SCL(0);
}
/** 产生SCCB停止条件。 */
static void ov5640_sccb_stop(void)
{
OV5640_SCCB_SDA(0);
ov5640_sccb_delay();
OV5640_SCCB_SCL(1);
ov5640_sccb_delay();
OV5640_SCCB_SDA(1);
ov5640_sccb_delay();
}
/** 按高位优先发送一个字节,并产生第九个时钟周期。 */
static void ov5640_sccb_write_byte(uint8_t dat)
{
int8_t dat_index;
uint8_t dat_bit;
for (dat_index = 7; dat_index >= 0; dat_index--)
{
dat_bit = (dat >> dat_index) & 0x01;
OV5640_SCCB_SDA(dat_bit);
ov5640_sccb_delay();
OV5640_SCCB_SCL(1);
ov5640_sccb_delay();
OV5640_SCCB_SCL(0);
}
OV5640_SCCB_SDA(1);
ov5640_sccb_delay();
OV5640_SCCB_SCL(1);
ov5640_sccb_delay();
OV5640_SCCB_SCL(0);
}
/** 从SDA读取一个字节,并发送从机响应时钟。 */
static void ov5640_sccb_read_byte(uint8_t *dat)
{
int8_t dat_index;
uint8_t dat_bit;
ov5640_sccb_set_sda_input();
*dat = 0;
for (dat_index = 7; dat_index >= 0; dat_index--)
{
ov5640_sccb_delay();
OV5640_SCCB_SCL(1);
dat_bit = OV5640_SCCB_READ_SDA();
*dat |= (dat_bit << dat_index);
ov5640_sccb_delay();
OV5640_SCCB_SCL(0);
}
ov5640_sccb_delay();
OV5640_SCCB_SCL(1);
ov5640_sccb_delay();
OV5640_SCCB_SCL(0);
ov5640_sccb_delay();
OV5640_SCCB_SDA(0);
ov5640_sccb_delay();
ov5640_sccb_set_sda_output();
}
void ov5640_sccb_init(void)
{
/* 发送停止条件,使总线回到空闲状态。 */
ov5640_sccb_stop();
}
/** 向指定寄存器写入一个字节。 */
void ov5640_sccb_3_phase_write(uint8_t id_addr, uint16_t sub_addr, uint8_t dat)
{
ov5640_sccb_start();
ov5640_sccb_write_byte((id_addr << 1) | ov5640_SCCB_WRITE);
ov5640_sccb_write_byte((uint8_t)(sub_addr >> 8) & 0xFF);
ov5640_sccb_write_byte((uint8_t)sub_addr & 0xFF);
ov5640_sccb_write_byte(dat);
ov5640_sccb_stop();
}
/** 写入寄存器地址,为后续读取操作设置当前寄存器。 */
void ov5640_sccb_2_phase_write(uint8_t id_addr, uint16_t sub_addr)
{
ov5640_sccb_start();
ov5640_sccb_write_byte((id_addr << 1) | ov5640_SCCB_WRITE);
ov5640_sccb_write_byte((uint8_t)(sub_addr >> 8) & 0xFF);
ov5640_sccb_write_byte((uint8_t)sub_addr & 0xFF);
ov5640_sccb_stop();
}
/** 从指定设备读取一个字节。 */
void ov5640_sccb_2_phase_read(uint8_t id_addr, uint8_t *dat)
{
ov5640_sccb_start();
ov5640_sccb_write_byte((id_addr << 1) | ov5640_SCCB_READ);
ov5640_sccb_read_byte(dat);
ov5640_sccb_stop();
}
3.2 ov5640_sccb.h
/**
****************************************************************************************************
* @file ov5640_sccb.h
* @brief OV5640模块SCCB接口驱动代码(软件模拟,PB6=SCL, PB7=SDA)
****************************************************************************************************
*/
#ifndef __OV5640_SCCB_H
#define __OV5640_SCCB_H
#include "main.h"
/* SCCB软件时序IO操作(SCL/SDA由GPIO直接控制)。 */
#define OV5640_SCCB_SCL(x) do{ x ? \
HAL_GPIO_WritePin(OV5640_SCL_GPIO_Port, OV5640_SCL_Pin, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(OV5640_SCL_GPIO_Port, OV5640_SCL_Pin, GPIO_PIN_RESET); \
}while(0)
#define OV5640_SCCB_SDA(x) do{ x ? \
HAL_GPIO_WritePin(OV5640_SDA_GPIO_Port, OV5640_SDA_Pin, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(OV5640_SDA_GPIO_Port, OV5640_SDA_Pin, GPIO_PIN_RESET); \
}while(0)
#define OV5640_SCCB_READ_SDA() HAL_GPIO_ReadPin(OV5640_SDA_GPIO_Port, OV5640_SDA_Pin)
/* SCCB总线初始化及寄存器读写操作。 */
void ov5640_sccb_init(void); /* 将总线恢复为空闲状态 */
void ov5640_sccb_3_phase_write(uint8_t id_addr, uint16_t sub_addr, uint8_t dat); /* 写寄存器 */
void ov5640_sccb_2_phase_write(uint8_t id_addr, uint16_t sub_addr); /* 写寄存器地址 */
void ov5640_sccb_2_phase_read(uint8_t id_addr, uint8_t *dat); /* 读寄存器数据 */
#endif
4. OV5640 应用源码

4.1 app_ov5640.c
/**
* @file app_ov5640.c
* @brief OV5640摄像头简单初始化测试
*/
#include "app_ov5640.h"
#include "ov5640.h"
#include "lcd_ltdc.h"
#include "dcmi.h"
#include "ov5640_dcmi.h"
#include "cmsis_os2.h"
#include "mylog.h"
/**
* @brief 初始化 OV5640,检查芯片并切换一次摄像头 LED
*/
void app_ov5640_init(void)
{
uint8_t ret;
LOG_INFO("CM7","OV5640 init start");
ret = ov5640_init();
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7","OV5640 init FAILED (err=%d)", ret);
return;
}
ov5640_led_on();
ov5640_led_off();
LOG_INFO("CM7","OV5640 init OK, chip ID verified");
}
/**
* @brief 配置 OV5640 输出 800x480 RGB565 色条并采集到 LTDC 帧缓冲
*/
void app_ov5640_capture_colorbar(void)
{
uint8_t ret = 0;
LOG_INFO("CM7","OV5640 capture colorbar start");
/* 1. 设置输出格式为 RGB565 */
if (ov5640_set_output_format(OV5640_OUTPUT_FORMAT_RGB565) != OV5640_EOK)
{
LOG_ERROR("CM7","OV5640 set output format failed (err=%d)", ret);
return;
}
/* 2. 设置输出尺寸为 800x480 */
if (ov5640_set_output_size(800, 480) != OV5640_EOK)
{
LOG_ERROR("CM7","OV5640 set output size failed (err=%d)", ret);
return;
}
/* 3. 设置测试模式为 颜色条测试 */
if (ov5640_set_test_pattern(OV5640_TEST_PATTERN_COLOR_BAR) != OV5640_EOK)
{
LOG_ERROR("CM7","OV5640 set test pattern failed (err=%d)", ret);
return;
}
/* 4. 开始捕获 */
if (ov5640_get_frame(LTDC_FRAME_BUF_ADDR, // 0xD0000000
OV5640_GET_TYPE_DTS_16B_INC, // RGB565 = 2字节/像素
NULL) != OV5640_EOK) // 帧数据传输前无需要完成的工作
{
LOG_ERROR("CM7","OV5640 get frame failed (err=%d)", ret);
return;
}
LOG_INFO("CM7","Colorbar frame captured to 0x%08X", LTDC_FRAME_BUF_ADDR);
}
/**
* @brief 采集色条和真实画面并打印像素信息,随后持续采集真实画面
* @return 配置或采集失败时返回 OV5640 错误码;正常运行时持续采集,不返回
*/
uint8_t app_ov5640_run(void)
{
uint8_t ret;
volatile uint16_t *frame = (volatile uint16_t *)LTDC_FRAME_BUF_ADDR;
uint16_t green = frame[800U * 240U + 350U];
uint32_t green_mismatches = 0;
uint32_t x;
LOG_INFO("CM7", "CAMERA_TEST_COLORBAR_START");
ret = ov5640_set_output_format(OV5640_OUTPUT_FORMAT_RGB565);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL format=%u", ret);
return ret;
}
ret = ov5640_set_output_size(800, 480);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL size=%u", ret);
return ret;
}
ret = ov5640_set_light_mode(OV5640_LIGHT_MODE_ADVANCED_AWB);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL light=%u", ret);
return ret;
}
ret = ov5640_set_color_saturation(OV5640_COLOR_SATURATION_3);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL sat=%u", ret);
return ret;
}
ret = ov5640_set_brightness(OV5640_BRIGHTNESS_3);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL bright=%u", ret);
return ret;
}
ret = ov5640_set_contrast(OV5640_CONTRAST_2);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL contrast=%u", ret);
return ret;
}
ret = ov5640_set_hue(OV5640_HUE_6);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL hue=%u", ret);
return ret;
}
ret = ov5640_set_exposure_level(OV5640_EXPOSURE_LEVEL_2);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL exposure=%u", ret);
return ret;
}
ret = ov5640_set_sharpness_level(OV5640_SHARPNESS_5);
if (ret != OV5640_EOK)
{
LOG_ERROR("CM7", "CAMERA_TEST_COLORBAR_FAIL sharpness=%u", ret);
return ret;
}
/* 三帧 SDRAM:显示帧、采集帧、待显示帧。DMA 持续接收,LTDC 在消隐期换帧。 */
/* 避开从 0xD0110000 开始的 JPEG 工作缓冲,采集帧放在独立区域。 */
LOG_INFO("CM7", "CAMERA_STREAM_START");
ret = ov5640_dcmi_stream_start(LTDC_FRAME_BUF_ADDR, 0xD0300000U, 800U * 480U * 2U);
if (ret != OV5640_DCMI_EOK)
{
LOG_ERROR("CM7", "CAMERA_STREAM_START_FAIL=%u", ret);
return ret;
}
for (;;)
{
ret = ov5640_dcmi_stream_poll();
if (ret != OV5640_DCMI_EOK)
{
ov5640_dcmi_stream_stop();
ov5640_dcmi_stream_log_error();
LOG_ERROR("CM7", "CAMERA_STREAM_FAIL=%u DCMI=0x%lX DMA=0x%lX",
ret, (unsigned long)hdcmi.ErrorCode,
(unsigned long)hdcmi.DMA_Handle->ErrorCode);
return ret;
}
osDelay(1);
}
}
4.2 app_ov5640.h
/**
* @file app_ov5640.h
* @brief OV5640摄像头简单初始化测试
*/
#ifndef __APP_OV5640_H
#define __APP_OV5640_H
#include <stdint.h>
void app_ov5640_init(void);
void app_ov5640_capture_colorbar(void);
uint8_t app_ov5640_run(void);
#endif /* __APP_OV5640_H */
5. OV5640 三缓冲采集图像
5.1 为什么需要三个完整帧缓冲
连续预览时,摄像头采集和 LCD 显示是两个独立过程。摄像头可能已经完成一帧,但 LCD 还在扫描上一帧,不能立即把正在显示的内存交给采集程序覆盖。
因此,本工程在 SDRAM 中准备了三个完整帧缓冲,并通过状态变量管理它们的用途:
| 角色 | 对应状态 | 用途 |
|---|---|---|
| 显示帧 | display | LTDC 当前读取的完整帧,采集端不能覆盖 |
| 采集帧 | capture | DMA2D 正在逐块写入的完整帧 |
| 待显示帧 | pending | 已经接收并搬运完成,等待切换到 LCD 的完整帧 |
这些角色并不固定绑定某个地址,而是随采集和显示进度轮换。没有待显示帧时,第三个缓冲可以处于空闲状态,供后续采集使用。
三缓冲的作用是为采集与显示之间的时序差异提供余量:完成的帧可以等待显示,摄像头同时继续接收下一帧。
5.2 帧缓冲大小与内存分配
本工程的图像尺寸为 800×480,格式为 RGB565,每个像素占 2 字节,因此一帧的大小为:
800 × 480 × 2 = 768000 字节
= 750 KiB
= 0xBB800 字节
应用层通过下面的接口启动连续采集:
ov5640_dcmi_stream_start(
LTDC_FRAME_BUF_ADDR,
0xD0300000U,
800U * 480U * 2U
);
启动函数使用第一个参数作为初始显示帧地址,第二个参数作为初始采集帧地址,并在采集帧后面再分配一个完整帧:
g_stream.addr[0] = display_addr;
g_stream.addr[1] = capture_addr;
g_stream.addr[2] = capture_addr + frame_size;
对应的地址如下:
| 帧槽 | 起始地址 | 大小 | 初始用途 |
|---|---|---|---|
| 帧槽 0 | 0xD0000000 | 750 KiB | 显示帧 |
| 帧槽 1 | 0xD0300000 | 750 KiB | 采集帧 |
| 帧槽 2 | 0xD03BB800 | 750 KiB | 空闲帧 |
三个完整帧实际占用 2250 KiB,不包含地址之间的空隙。将采集区域放在 0xD0300000,是为了避开工程中从 0xD0110000 开始的 JPEG 工作缓冲。
5.3 完整帧的发布与角色切换
本工程将一帧分成 30 个数据块。只有最后一个块也完成 DMA2D 搬运后,当前采集缓冲才包含一张完整图像,此时才能把它发布给显示端。
例如,初始状态为:
帧槽 0:正在显示
帧槽 1:正在采集
帧槽 2:空闲
帧槽 1 接收完成后,如果没有其他待显示帧,就将它标记为 pending,并把帧槽 2 作为下一帧的采集目标:
帧槽 0:正在显示
帧槽 1:等待显示
帧槽 2:正在采集
等 LTDC 真正切换到帧槽 1 后,帧槽 0 才可以重新参与后续采集。
这里需要保护两类缓冲:正在显示的帧不能覆盖,等待显示的完整帧也不能覆盖。 当前实现如果已经存在 pending 帧,就保留它;后续完成的采集帧暂不发布,采集端继续复用当前采集槽。
因此,这套机制允许部分采集帧不进入显示流程,并不保证每个采集帧都显示一次。这样可以在显示端暂时跟不上时,继续接收摄像头数据,同时保护已提交的完整图像。
5.4 在垂直消隐期切换显示地址
采集完一帧,并不意味着可以在任意时刻切换 LCD 的帧缓冲地址。如果 LTDC 正在扫描画面时切换地址,同一屏内容仍可能来自不同帧。
本工程在 ov5640_dcmi_stream_poll() 中发现待显示帧后,先设置新的地址,再申请垂直消隐期重载:
HAL_LTDC_SetAddress_NoReload(
&hltdc,
g_stream.addr[pending],
0U
);
HAL_LTDC_Reload(
&hltdc,
LTDC_RELOAD_VERTICAL_BLANKING
);
设置地址后,程序还会等待 SRCR.VBR 清零,确认重载已完成,然后才更新 display 状态并清空 pending。
也就是说,申请换帧和换帧生效是两个步骤。 旧显示帧在重载完成前仍然受到保护,不能提前交给采集端使用。
这一节描述的是完整帧的管理方式。下一节介绍这些完整帧如何通过两个小块缓冲和 DMA2D 逐步接收、拼接出来。
6. OV5640 双缓冲 + DMA2D 搬运图像
6.1 双缓冲接收的是图像块
这里的“双缓冲”位于 AXI SRAM,每个缓冲只保存 16 行图像。工程中的定义如下:
#define CAMERA_BLOCK_LINES (16U)
#define CAMERA_DMA_WORDS (800U * CAMERA_BLOCK_LINES * 2U / 4U)
#define CAMERA_FRAME_BYTES (800U * 480U * 2U)
#define CAMERA_BLOCKS (480U / CAMERA_BLOCK_LINES)
static uint32_t camera_line_buf[2][CAMERA_DMA_WORDS]
__attribute__((aligned(32)));
每个数据块的大小为:
800 × 16 × 2 = 25600 字节 = 25 KiB
DMA 按 32 位 Word 搬运,所以每次传输的数据单元数量为:
25600 ÷ 4 = 6400 个 Word
两个块缓冲合计占用 50 KiB,一张 480 行的图像则需要:
480 ÷ 16 = 30 个块
这使连续接收只需要在 AXI SRAM 中准备两个小缓冲,完整图像仍保存在容量更大的外部 SDRAM 中。
6.2 DMA 在两个块缓冲之间交替接收
连续流启动时,通过以下调用设置 DMA 的两个内存目标:
HAL_DMAEx_MultiBufferStart_IT(
dma,
(uint32_t)&DCMI->DR,
(uint32_t)camera_line_buf[0],
(uint32_t)camera_line_buf[1],
CAMERA_DMA_WORDS
);
DMA 从 DCMI->DR 读取数据,先写入一个块缓冲。完成 6400 个 Word 的传输后,硬件切换到另一个块缓冲,并触发完成回调。
正常情况下,接收和搬运交替进行:
DMA 写入块缓冲 0
↓ 块完成,切换目标
DMA 写入块缓冲 1 DMA2D 搬运块缓冲 0
↓ 块完成,切换目标
DMA 写入块缓冲 0 DMA2D 搬运块缓冲 1
↓
持续交替
两个完成回调都指向 camera_dma_complete()。回调通过 DMA 的 CT 位判断刚刚完成的是哪个缓冲。
需要注意,完成时 DMA 已经切换到下一接收目标,所以 CT 指向的是当前正在写入的缓冲,其反值才对应刚完成的缓冲:
uint8_t finished =
(dma->CR & DMA_SxCR_CT) ? 0U : 1U;
6.3 DMA2D 将数据块拼接成完整图像
每次块完成后,DMA2D 都把这个 800×16 的矩形数据复制到 SDRAM 当前采集帧的对应位置。
目标地址由当前帧地址和块序号计算:
目标地址 = 当前采集帧地址 + 块序号 × 25600
例如:
| 块序号 | 对应图像行 | 相对帧首地址的偏移 |
|---|---|---|
| 0 | 第 0~15 行 | 0 字节 |
| 1 | 第 16~31 行 | 25600 字节 |
| 2 | 第 32~47 行 | 51200 字节 |
| … | … | … |
| 29 | 第 464~479 行 | 742400 字节 |
对应的关键代码为:
DMA2D->FGMAR = (uint32_t)camera_line_buf[finished];
DMA2D->OMAR =
g_stream.addr[g_stream.capture]
+ (uint32_t)segment * CAMERA_DMA_WORDS * 4U;
DMA2D->NLR = (800U << 16) | CAMERA_BLOCK_LINES;
DMA2D->CR = DMA2D_CR_START;
DMA2D 的输入和输出都设置为 RGB565,输入、输出行偏移均为 0。因此,这里的 DMA2D 负责原样复制像素,并按照块序号拼接完整帧,没有执行缩放、格式转换或图层混合。
第 30 个块搬运完成后,程序才执行上一节介绍的完整帧发布逻辑,再从第 0 块开始接收下一帧。
6.4 搬运必须赶在块缓冲复用之前完成
双块缓冲为搬运提供了时间窗口,但这个窗口是有限的。DMA 写入另一个缓冲后,还会回来复用刚完成的缓冲。如果 DMA2D 尚未读完其中的数据,DMA 就开始覆盖它,搬运出的图像可能出现块内数据混合。
因此,DMA2D 必须在源块缓冲再次被 DMA 使用前完成复制。
当前工程在 DMA 完成回调中启动 DMA2D,并轮询等待搬运结束。等待设置了有限次数,同时检查 DMA2D 的传输错误和配置错误。
这种实现让像素复制由硬件完成,CPU 主要负责确定源地址、目标地址、块序号和帧状态。不过 CPU 仍然需要处理中断并等待 DMA2D 完成,因此不能把它理解为完全不占用 CPU 的异步流水线。
6.5 Cache 一致性与缓冲区访问约定
STM32H7 使用 D-Cache 时,CPU 和 DMA 对同一块内存的访问需要考虑缓存一致性。
本工程在连续流启动前,对完整帧区域和双块缓冲执行:
SCB_CleanInvalidateDCache_by_Addr(...);
启动后的像素流转过程为:
DMA 写入块缓冲
↓
DMA2D 读取块缓冲、写入完整帧
↓
LTDC 读取完整帧
流运行期间,CPU 不读写这些像素缓冲,因此换帧时没有再进行整帧 Cache 维护。
这个处理方式依赖明确的访问约定。如果后续加入 CPU 图像处理、像素检查或在摄像头帧上绘图,就需要重新安排缓冲区所有权和 Cache 维护,不能直接沿用“运行期间不处理 Cache”的做法。
此外,摄像头搬运和 LCD 的部分绘图操作都可能使用 DMA2D。如果需要并发执行,应统一管理 DMA2D 的使用,避免一个操作修改另一个操作正在使用的寄存器。
7. OV5640 提高采集和显示 FPS
7.1 区分采集 FPS 与显示 FPS
连续预览包含摄像头输出、DCMI 接收、DMA2D 搬运和 LTDC 显示等环节。判断性能时,需要分别观察采集和显示的进度。
本工程统计两种 FPS:
| 指标 | 计数时机 | 含义 |
|---|---|---|
| capture | 一帧的 30 个数据块全部接收并完成 DMA2D 搬运 | 每秒完成的完整图像数量 |
| display | LTDC 垂直消隐期重载完成,更新显示帧状态 | 每秒切换到 LCD 的新图像数量 |
这里的显示 FPS 是摄像头画面的更新频率,不等同于 LCD 的扫描刷新率。即使 LCD 刷新较快,如果摄像头每秒只提供约 30 张新图像,屏幕也只能以相应速度更新摄像头内容。
优化的目标是减少链路中的等待和阻塞,让接收到的完整图像及时进入显示流程,同时保证画面完整、没有明显采集错误。
7.2 使用连续采集,减少帧间的软件等待
单帧快照适合验证摄像头配置和检查像素,但反复调用快照接口时,通常需要经历:
启动采集 → 等待一帧完成 → 停止采集
→ 软件处理 → 再启动下一次采集
这些步骤会引入帧间的软件间隙。如果再叠加延时、打印或整帧处理,就容易降低有效采集帧率。
当前工程在启动时将 DCMI 设置为连续模式,并通过 DMA 硬件双缓冲持续接收数据。启动成功后,任务主要调用:
ov5640_dcmi_stream_poll();
osDelay(1);
ov5640_dcmi_stream_poll() 负责换帧、状态检查和 FPS 统计,像素接收由 DCMI 与 DMA 持续执行。这里的 osDelay(1) 用于让出任务执行时间,并不表示每采集一帧都停止等待,也不能据此推算摄像头帧率。
7.3 通过分块搬运重叠接收与复制
800×480 的 RGB565 图像每帧占 768000 字节。如果等整帧接收结束后,再串行复制整帧,会增加图像进入显示阶段前的等待。
当前工程将图像分成 30 个块,每块 16 行。DMA 接收下一块时,DMA2D 可以搬运刚完成的上一块,使接收和复制在时间上重叠。
这种方式主要减少整帧接收后的集中搬运等待,并将像素复制交给硬件完成。但它仍有一个关键约束:DMA2D 必须在源块缓冲被 DMA 再次使用前完成搬运。
块大小也会影响运行开销。块越小,完成中断越频繁;块越大,占用的内部 SRAM 越多,每次搬运的数据也越多。当前使用 16 行一块,在约 30 FPS 时,每秒大约产生:
30 块/帧 × 30 帧/秒 = 900 次块完成回调/秒
当前代码在回调中轮询等待 DMA2D 完成。如果继续优化,应先测量回调耗时和搬运时间,再判断是否需要调整块大小或改为异步完成处理,不能仅凭减少中断次数就认定帧率一定提高。
7.4 减少显示环节对采集的阻塞
三帧缓冲将正在显示、等待显示和正在采集的图像分开,使采集端可以在 LTDC 等待换帧时继续工作。
显示端只修改 LTDC 的帧缓冲地址,不额外复制一张完整图像。地址在垂直消隐期生效,随后旧显示帧才被释放供后续采集使用。
这套机制为采集与显示之间的时序差异提供了缓冲,但不会直接提高摄像头本身的输出帧率。如果显示端长期跟不上,当前实现会保留已有待显示帧,部分后续采集帧不会进入显示流程。
因此,可以结合两个计数定位问题:
- capture 和 display 接近,说明完成的图像大多能及时显示。
- capture 长期明显高于 display,应检查换帧轮询、任务调度和 LTDC 重载是否及时完成。
- 两者都偏低,应进一步检查摄像头输出时序、曝光条件,以及接收和搬运环节是否存在阻塞。
这些现象用于缩小排查范围,最终原因还需要结合错误日志和耗时测量确认。
7.5 合理配置摄像头时钟,兼顾吞吐与稳定性
摄像头输出帧率与内部时钟、行帧时序和曝光条件有关。提高像素时钟可以增加瞬时数据吞吐,但也会给 DCMI、DMA 和内存访问带来更大的压力。出现采集错误时,即使输入时钟更快,有效完整帧率也可能下降。
当前 RGB565 配置表中的相关寄存器值为:
{0x3035, 0x31},
{0x3036, 0x98},
{0x3824, 0x10},
这些参数需要结合完整的时钟和输出时序配置理解。不能只修改其中一个分频值,就承诺得到某个确定 FPS。
前面调试过程中降低 PCLK 的做法,目的是改善接收稳定性,为采集链路提供时序余量。它应与“提高有效帧率”联系起来理解:先保证图像能够完整、连续地接收,再根据实际测量逐步调整输出时序。
7.6 控制内存访问、Cache 维护和日志开销
以 30 FPS 计算,仅摄像头有效像素数据量就达到:
800 × 480 × 2 × 30
= 23040000 字节/秒
≈ 23.04 MB/s
完整链路还包含 DMA2D 的读写以及 LTDC 的持续读取,因此实际内存访问量高于这个数值。DMA2D 能减少 CPU 的像素复制工作,但不会消除搬运所需的总线带宽。
当前工程采用了以下方式控制额外开销:
- 像素由 DMA、DMA2D 和 LTDC 流转,CPU 不逐像素处理。
- 流启动前维护缓冲区 Cache,运行期间遵守 CPU 不访问像素缓冲的约定,避免每帧执行整帧 Cache 维护。
- 换帧时切换地址,不进行额外整帧复制。
- FPS 日志约每秒输出一次,不在每个块完成中断中打印。
这些做法有助于减少软件开销,但需要保持对应的使用条件。如果加入 CPU 图像处理或绘图,就必须补充正确的 Cache 维护;如果加入其他 DMA2D 操作,也需要协调外设使用,避免影响摄像头搬运。
7.7 FPS 统计与当前结果
程序按照统计窗口内的帧数增量计算 FPS:
FPS = 新增帧数 × 1000 ÷ 实际经过的毫秒数
统计代码使用实际经过时间,而不是直接假定任务恰好每 1000 ms 执行一次,可以减小调度延迟对结果的影响。
本文已有日志中,采集与显示帧率约为 28~30 FPS,例如:
CAMERA_FPS capture=30 display=29
CAMERA_FPS capture=29 display=30
CAMERA_FPS capture=30 display=30
完整帧搬运完成和 LTDC 换帧完成发生在不同时间。某一帧可能在上一统计窗口完成采集,在下一窗口完成显示,加上整数计算,单个窗口内两种 FPS 相差 1 帧是可以出现的。应结合多个连续窗口观察,而不是只比较一行日志。
这些日志说明,当前 800×480 RGB565 配置下,采集与显示的计数接近,链路能够维持约 30 FPS 的图像更新。由于文章没有提供各项改动前后的统一对照测试,这里不单独量化双缓冲、DMA2D 或三帧缓冲各自带来的提升幅度。后续优化可以在相同分辨率、光照和显示配置下,记录采集 FPS、显示 FPS、错误次数及搬运耗时,再判断改动是否有效。
8. 注意事项
8.1 HSYNC 极性、采样边沿配置
HSYNC 极性配置和采样边沿错误,导致 DCMI 采集时序不匹配。
问题表现
OV5640 的行同步信号有效电平与 DCMI 初始配置不一致。原配置使用高电平有效,实际调试中需要改为低电平有效,像素时钟采样边沿改为上升沿有效。极性不匹配会造成 DCMI 对行边界判断错误,进而出现图像错位、颜色异常、帧数据不完整或无法正常结束采集等问题。
解决方式
- 将生成代码
CM7/Core/Src/dcmi.c中的hdcmi.Init.HSPolarity同步改为DCMI_HSPOLARITY_LOW。 - 将生成代码
CM7/Core/Src/dcmi.c中的hdcmi.Init.PCKPolarity同步改为DCMI_PCKPOLARITY_RISING。 - 删除测试函数中临时直接修改 DCMI 寄存器的做法,统一由初始化配置管理。
效果
HSYNC 极性和采样边沿固定在工程配置和生成代码中,避免仅靠运行时临时修改寄存器导致配置来源不一致。
涉及文件:CM7/Core/Src/dcmi.c。

8.2 摄像头输出尺寸配置
摄像头输出尺寸可能配置失败但未被发现。
问题表现
程序请求 OV5640 输出 800×480,但原有设置函数写入寄存器后没有读取寄存器进行确认。如果寄存器写入失败、位域处理错误或摄像头没有按预期应用配置,后续 DCMI 仍会按照 800×480 处理,可能导致帧长度和实际输出不一致。
解决方式
在 ov5640_set_output_size() 中完成寄存器写入后重新读取输出尺寸,并与请求值比较:
- 读回尺寸与 800×480 一致时继续执行。
- 任意一个尺寸不一致时返回
OV5640_ERROR。 - 同时打印实际尺寸和期望尺寸,便于定位配置问题。
效果
尺寸配置从“只写不验”变成了“写入后读回校验”,避免错误配置继续进入 DMA 采集阶段。
涉及文件:CM7/BSP/CAMERA/ov5640.c。

8.3 OV5640 PCLK 过高
OV5640 PCLK 过高,采集时序压力较大。
问题表现
在 DCMI、DMA 和摄像头之间的时序裕量不足时,较高的像素时钟会增加采集错误、颜色异常和数据不稳定的可能性,尤其是在较大分辨率连续采集时更明显。
解决方式
将 RGB565 配置中的 OV5640 寄存器 0x3824 从 0x04 修改为 0x10,提高 PCLK 分频系数,从而降低摄像头像素时钟。
同时增加颜色条中心行的多个采样点和连续 80 像素一致性统计,用于观察降低时钟后是否仍有明显像素异常。
涉及文件:CM7/BSP/CAMERA/ov5640_cfg.h、CM7/APP/CAMERA/app_ov5640.c。

10. 串口打印结果
____ ___ ___ _____ _ ___ _ ____ _____ ____
| __ ) / _ \ / _ \|_ _| | / _ \ / \ | _ \| ____| _ \
| _ \| | | | | | | | | | | | | | |/ _ \ | | | | _| | |_) |
| |_) | |_| | |_| | | | | |___| |_| / ___ \| |_| | |___| _ <
|____/ \___/ \___/ |_| |_____|\___/_/ \_\____/|_____|_| \_\
BOOT Tag: Bootloader-CameraDoubleCPU-1.0.0-20260801
============= flash partition table ==============
---------------- CM7 (Bank1, 1MB) ----------------
| name | offset | size | value |
--------------------------------------------------
| bootloader | 0x08000000 | 0x00020000 | 128KB |
| slot A | 0x08020000 | 0x00060000 | 384KB |
| reserve(S4) | 0x08080000 | 0x00020000 | 128KB |
| slot B | 0x080a0000 | 0x00060000 | 384KB |
---------------- CM4 (Bank2, 1MB) ----------------
| name | offset | size | value |
--------------------------------------------------
| stub | 0x08100000 | 0x00020000 | 128KB |
| slot A | 0x08120000 | 0x00060000 | 384KB |
| reserve(S4) | 0x08180000 | 0x00020000 | 128KB |
| slot B | 0x081a0000 | 0x00060000 | 384KB |
==================================================
USART1 Baudrate: 115200, Pins: PA9/PA10
EEPROM Peripheral: I2C4, Pins: PD12/PD13
[I][BOOT] EEPROM ready (BL24C16F @ I2C4)
[I][BOOT] EEPROM ready (dual copy @ 0x0000 / 0x0100, ver=2)
[I][BOOT] CM7 slot A: valid
[I][BOOT] CM7 slot B: valid
[I][BOOT] CM4 slot A: valid
[I][BOOT] CM4 slot B: valid
[I][BOOT] CM7 EE: active=0 pending=0 trial=0 flags=0x00 seq=2
[I][BOOT] CM7 EE ver A=0 B=0 | slotA valid=1 slotB valid=1
[I][BOOT] CM4 EE: active=2 pending=2 trial=0 flags=0x00 | ver A=0 B=0
[I][BOOT] jump slot 0 @ 0x08020000
[I][BOOT] vec SP=0x24080000 Reset=0x08039905
[I][BOOT] hold CM4 reset, then jump...
[I][BOOT] Preparing CM7 app at 0x08020000
[I][BOOT] Vector: MSP=0x24080000, Reset_Handler=0x08039905
[I][BOOT] CM4 boot selection disabled
[I][BOOT] IRQs disabled
[I][BOOT] SysTick stopped and NVIC cleared
[I][BOOT] Jump flag: addr=0x2001fff0, value=0xb00710ad
[I][BOOT] VTOR set to 0x08020000
[I][BOOT] Switching MSP to 0x24080000; jumping to 0x08039905
[I][BOOT] Jumping to APP Reset_Handler...
_ ____ ____ _ ___ ____ _ _____ ___ ___ _ _
/ \ | _ \ | _ \ | | |_ _| / ___| / \ |_ _||_ _| / _ \ | \ | |
/ _ \ | |_) || |_) || | | | | | / _ \ | | | | | | | || \| |
/ ___ \ | __/ | __/ | |___ | | | |___ / ___ \ | | | | | |_| || |\ |
/_/ \_\|_| |_| |_____||___| \____|/_/ \_\ |_| |___| \___/ |_| \_|
[I][APP] EEPROM ready (BL24C16F @ I2C4)
[I][APP] EEPROM ready (dual copy @ 0x0000 / 0x0100, ver=2)
[I][APP] EE copy0 valid=1 seq=2 | copy1 valid=0 seq=0
[I][APP] CM7 running slot=0 base=0x08020000 | EE active=0 pending=0 flags=0x00
[I][APP] CM4 EE active=2 pending=2 flags=0x00 verA=0 verB=0
[I][APP] EEPROM read/write OK @ 0x0200
[I][APP] CM4 handoff slot=2 base=0x08120000 ee=1
[I][APP] CM4 boot core enabled
[I][CM7] OpenAMP init OK
[I][CM7] SDRAM init OK
[I][CM7] LCD ID:4384
[I][CM7] LCD init OK
[I][CM7] OV5640 init start
[I][CM7] OV5640 chip ID=0x5640
[I][CM7] OV5640 init OK, chip ID verified
[I][CM7] main() user init OK
[I][CM7] FreeRTOS init OK
[I][CM7] CAMERA_TEST_COLORBAR_[I][CM4] main() user init OK
[I][CM4] FreeRTOS init OK
[I][CM7] OV5640 output readback=800x480 expected=800x480
[I][CM7] CAMERA_STREAM_START
[I][CM7] CAMERA_STREAM_FRAME_PASS frames=1 display=0xD0300000
[I][CM7] CAMERA_FPS capture=28 display=28
[I][CM7] CAMERA_FPS capture=30 display=29
[I][CM7] CAMERA_FPS capture=29 display=30
[I][CM7] CAMERA_FPS capture=30 display=30
[I][CM7] CAMERA_FPS capture=29 display=29
[I][CM7] CAMERA_FPS capture=30 display=29
[I][CM7] CAMERA_FPS capture=29 display=30
[I][CM7] CAMERA_FPS capture=30 display=29
[I][CM7] CAMERA_FPS capture=29 display=30
[I][CM7] CAMERA_FPS capture=30 display=30
[I][CM7] CAMERA_FPS capture=29 display=29
[I][CM7] CAMERA_FPS capture=30 display=29
[I][CM7] CAMERA_FPS capture=29 display=30
11. 总结

本文完成了 OV5640 在 STM32H757 上的驱动移植与连续预览:通过软件 SCCB 配置摄像头,使用 DCMI 和 DMA 接收 800×480 RGB565 图像,再由 DMA2D 搬运到外部 SDRAM,最终通过 LTDC 显示到 LCD。
整个流程中,各层缓冲承担了不同职责。AXI SRAM 中的两个块缓冲交替接收像素,DMA2D 将每个块复制到完整帧的对应位置;SDRAM 中的三个帧缓冲则管理采集、待显示和正在显示的图像,LTDC 在垂直消隐期应用新的显示地址。
要实现稳定的连续预览,除了摄像头寄存器配置正确,还需要保证几个关键条件:DCMI 同步极性和采样边沿与输入信号匹配,实际输出尺寸与程序中的帧大小一致,DMA2D 在块缓冲复用前完成搬运,以及采集端不覆盖正在显示或等待显示的完整帧。
本文展示的串口日志中,采集与显示帧率约为 28~30 FPS。其中 capture 统计完成接收并搬运的完整帧,display 统计 LTDC 完成换帧的次数,两者并非同一个事件。由于统计窗口和换帧生效时刻不同,日志中偶尔出现 capture=29、display=30 或相反的情况,并不矛盾。这里的显示 FPS 表示摄像头画面的更新次数,也不等同于 LCD 的扫描刷新率。
当前实现为连续摄像头预览提供了基础。如果后续增加图像处理、JPEG 编码或画面叠加,需要在现有链路上进一步协调帧缓冲所有权、Cache 一致性和 DMA2D 使用,使各处理阶段能够安全地共享图像数据。
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/Skyrim-sssuuu/p/23212977

浙公网安备 33010602011771号
https://orcid.org/0000-0001-5102-772X