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 字节的行缓冲

四、接线

D0~D7 数据

SCL/SDA 配置

VSYNC 帧同步

FIFO 控制

SCK/MOSI

CS/DC/RST

OV7670 摄像头模块

PA0~PA7

PB6/PB7

PB14

PB8/9/10/12/13

ST7735 屏幕

PB3/PB5

PA8/PB2/PC13

OV7670 模块引脚STM32 引脚说明
VCC3.33V3只接 3.3V,别接 5V
GNDGND公共地
OV_SCL (SIOC)PB6SCCB 时钟(I2C1_SCL)
OV_SDA (SIOD)PB7SCCB 数据(I2C1_SDA)
OV_VSYNCPB14帧同步(EXTI14 上升沿)
D0~D7PA0~PA78 位数据总线(必须连续接
FIFO_RCKPB8读时钟
FIFO_OEPB9输出使能(低有效)
FIFO_WRSTPB10写复位(低有效)
FIFO_RRSTPB12读复位(低有效)
FIFO_WENPB13写允许(高有效)

屏幕(ST7735)接线:

ST7735 引脚STM32 引脚
SCKPB3(SPI1_SCK)
MOSIPB5(SPI1_MOSI)
MISOPB4(可不接)
CSPA8
DCPB2
RSTPC13
VCC/GND3V3/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 输出)。表比较长(一百多条),这里列几个关键的:

寄存器作用
0x120x14COM7:QVGA + RGB 输出
0x400x10COM15:RGB565 格式
0x3a0x04TSLB:输出时序
0x110x00CLKRC:时钟不分频

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();
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐