STM32F401 驱动 OV7670 摄像头 + 屏幕实时显示
STM32F401 驱动 OV7670 摄像头 + 屏幕实时显示
STM32F401 驱动 OV7670 摄像头 +
一、前言
最近想给手上的 STM32F401RCT6 开发板加一双"眼睛",用 OV7670 摄像头把采集到的画面实时显示到 1.44 寸的 ST7735 屏幕上。
最终效果:摄像头对准什么,屏幕上就实时显示什么,约 15~20 帧/秒。
📷显示效果图:
二、实物清单
1. OV7670 摄像头
OV7670 是一款很经典的低成本图像传感器,最大可以输出 VGA(640×480)画面,支持 RGB565、YUV 等多种格式。
它自己会按固定节奏"吐"像素数据,但它不管你有没有接受数据,数据源源不断地输出。如果我们一边读一边还要干别的,很容易漏数据。
📷OV7670 摄像头模块实物图:
2. FIFO(AL422B)
我用的是带 FIFO 的 OV7670 模块。
FIFO 相当于一个大仓库(AL422B,384KB):
- 摄像头负责"往仓库里搬货"(写数据);
- 单片机什么时候有空,再去"仓库取货"(读数据)。
两者各干各的、互不打扰,单片机不用跟摄像头的节奏死磕,读得慢一点也不会丢帧。对于内存小、主频不高的单片机来说,FIFO 的存在可以大大缓解单片机的压力和提高帧数。
3. ST7735 屏幕
ST7735 是一块 SPI 接口的小彩屏,1.44 寸、 128×160。
4. SCCB 是什么
要给摄像头"下命令"(设置分辨率、格式等),需要一个配置通道,叫 SCCB。你可以直接把它当成 I2C 来用——时序兼容,本文就用 STM32 的硬件 I2C 来写寄存器。
三、原理:一帧图像是怎么"跑"出来的
整条数据流可以概括成三步:
摄像头按设置输出像素 → FIFO 暂存一整帧 → 单片机从 FIFO 读出来刷到屏幕
A[OV7670 摄像头] -->|像素数据| B[AL422B FIFO 仓库]
B -->|单片机空闲时读出| C[STM32 单片机]
C -->|SPI| D[ST7735 屏幕]
1. VSYNC:一帧的开始信号
OV7670 每开始输出新的一帧,就会在 **VSYNC** 引脚上给一个脉冲(上升沿)。我们把它接到单片机的**外部中断**上,靠它来"数帧":
第 1 个 VSYNC 上升沿**:复位 FIFO 写指针,让摄像头开始往 FIFO 里写这一帧;
第 2 个 VSYNC 上升沿**:说明下一帧要来了、上一帧已经完整,于是关掉写使能,把这一帧"冻结"在 FIFO 里;
单片机看到"帧冻结"标志后,安心地把整帧读出来,不会读到半帧、不会撕裂。
A[等待] -->|第1个 VSYNC 上升沿:复位写指针 + 开写 WEN=1| B[采集中]
B -->|第2个 VSYNC 上升沿:关写 WEN=0 冻结本帧| C[帧就绪]
C -->|主循环读完一帧并清标志| A
2. RGB565
摄像头输出 16 位色 RGB565:高字节 = 红 5 位 + 绿高 3 位,低字节 = 绿低 3 位 + 蓝 5 位。读取时先读高字节、再读低字节,拼成一个 16 位数。
3. 内存不足的问题
我的F401 只有 64KB RAM,而一帧 QVGA(320×240)RGB565 有 320×240×2 = 153600 字节,约 150KB,根本放不下。
解决办法:不缓存整帧,读一行、发一行到屏幕。摄像头输出 320×240,我们每 2 个像素取 1 个、每 2 行取 1 行,抽成 160×120 再显示,全程只需要 320 字节的行缓冲。
四、接线
| OV7670 模块引脚 | STM32 引脚 | 说明 |
|---|---|---|
| VCC3.3 | 3V3 | 只接 3.3V,别接 5V |
| GND | GND | 公共地 |
| OV_SCL (SIOC) | PB6 | SCCB 时钟(I2C1_SCL) |
| OV_SDA (SIOD) | PB7 | SCCB 数据(I2C1_SDA) |
| OV_VSYNC | PB14 | 帧同步(EXTI14 上升沿) |
| D0~D7 | PA0~PA7 | 8 位数据总线(必须连续接) |
| FIFO_RCK | PB8 | 读时钟 |
| FIFO_OE | PB9 | 输出使能(低有效) |
| FIFO_WRST | PB10 | 写复位(低有效) |
| FIFO_RRST | PB12 | 读复位(低有效) |
| FIFO_WEN | PB13 | 写允许(高有效) |
屏幕(ST7735)接线:
| ST7735 引脚 | STM32 引脚 |
|---|---|
| SCK | PB3(SPI1_SCK) |
| MOSI | PB5(SPI1_MOSI) |
| MISO | PB4(可不接) |
| CS | PA8 |
| DC | PB2 |
| RST | PC13 |
| VCC/GND | 3V3/GND |
说明:数据总线 D0~D7 必须占 8 个连续引脚,这样代码里一句
GPIOA->IDR & 0xFF就能一次读一个字节。所以这里把 8 位数据放到了 PA0~PA7,屏幕的 SPI1 只好挪到备用引脚 PB3/PB4/PB5。
五、CubeMX 配置
📷
- 时钟:HSI → PLL → 84MHz;
- SPI1(屏幕):PB3=SCK / PB4=MISO / PB5=MOSI,Full-Duplex Master、分频 8(10.5MHz)、8bit、MSB、CPOL Low、CPHA 1Edge;
- I2C1(SCCB):PB6/PB7,Standard Mode 100kHz;
- GPIO 输入:PA0~PA7(数据总线);
- GPIO 输出:PB8(RCK, Low/Very High)、PB9(OE, High)、PB10(WRST, High)、PB12(RRST, High)、PB13(WEN, Low)、PA8(CS, High)、PB2(DC, High)、PC13(RST, High);
- EXTI:PB14 = GPIO_EXTI14,上升沿;
- NVIC:勾选
EXTI line[15:10] interrupts,优先级 5。
六、代码讲解
1. SCCB 写寄存器(用硬件 I2C)
OV7670 的 7 位地址是 0x21,HAL 里 DevAddress 要左移一位,即 0x42。
#define OV7670_ADDR (0x21u << 1) // = 0x42
static void ov7670_write_reg(uint8_t reg, uint8_t val)
{
HAL_I2C_Mem_Write(&hi2c1, OV7670_ADDR, reg,
I2C_MEMADD_SIZE_8BIT, &val, 1, 100);
}
2. 初始化寄存器
先软复位(写 0x12=0x80),再逐条写入官方提供的寄存器表(QVGA 320×240、RGB565 输出)。表比较长(一百多条),这里列几个关键的:
| 寄存器 | 值 | 作用 |
|---|---|---|
| 0x12 | 0x14 | COM7:QVGA + RGB 输出 |
| 0x40 | 0x10 | COM15:RGB565 格式 |
| 0x3a | 0x04 | TSLB:输出时序 |
| 0x11 | 0x00 | CLKRC:时钟不分频 |
3. VSYNC 中断:用两个上升沿"卡"出一帧
static volatile uint8_t ov_sta = 0; // 0=等待 1=采集中 2=帧就绪
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin != GPIO_PIN_14) return; // PB14 = VSYNC
if (ov_sta == 0) {
// 第 1 个上升沿:复位写指针,开始采集本帧
WRST=0; WRST=1;
WEN=1;
} else if (ov_sta == 1) {
// 第 2 个上升沿:本帧完整,冻结 FIFO
WEN=0;
WRST=0; WRST=1;
}
ov_sta++;
}
4. 读一个像素(严格照官方时序)
#define DATA_READ() ((uint8_t)(GPIOA->IDR & 0xFF)) // 读 D0~D7
static inline uint16_t ov7670_read_pixel(void)
{
uint16_t c;
RCK_LOW(); c = DATA_READ(); // 高字节
RCK_HIGH();
RCK_LOW(); c = (c << 8) | DATA_READ(); // 低字节
RCK_HIGH();
return c;
}
5. 读出一帧并抽点显示
主循环里,当 ov_sta == 2(帧就绪)时,复位读指针,然后逐行读 FIFO:偶数行保留(再每 2 像素取 1)、奇数行丢弃,边读边用 SPI 发到屏幕,最终 320×240 抽成 160×120,上下加 4 行黑边铺满 160×128。
void OV7670_DrawToLCD(void)
{
static uint8_t line[160 * 2]; // 只占 320 字节的行缓冲
/* 复位读指针 */
RRST=0; RCK=0; RCK=1; RCK=0; RRST=1; RCK=1;
ST7735_Select();
ST7735_SetAddressWindow(0, 0, 159, 127);
for (int sy = 0; sy < 240; sy++) {
if (sy & 1) { // 奇数行:读了丢弃
for (int x = 0; x < 320; x++) ov7670_read_pixel();
} else { // 偶数行:2 取 1 保存并显示
uint8_t *p = line;
for (int x = 0; x < 160; x++) {
uint16_t c = ov7670_read_pixel();
*p++ = c >> 8; // 高字节先发
*p++ = c & 0xFF;
ov7670_read_pixel(); // 丢弃
}
ST7735_WriteData(line, sizeof(line));
}
}
ST7735_Unselect();
}
主循环:
if (OV7670_FrameReady()) {
OV7670_DrawToLCD();
OV7670_ClearFlag();
}
更多推荐






所有评论(0)