☰
STM32驱动OV7670实现JPEG无线图传完整链路
2026/10/28 6:56:24 网站建设 项目流程

简介:本资源面向嵌入式开发学习者与电子竞赛选手,提供一套基于STM32微控制器驱动OV7670摄像头、完成图像采集与JPEG压缩后经无线模块传输的完整工程源码。项目横跨嵌入式系统设计、数字图像处理与无线通信三大方向,适合具备一定C语言与单片机基础、希望深入掌握摄像头驱动与图像数据链路的开发者参考实践。压缩包共184个文件,约852KB,以c源文件与h头文件为核心,配合s汇编启动文件、o编译目标文件及icf、ewp等IAR工程配置,另含少量htm、doc说明与hex固件,覆盖从底层外设驱动到工程构建的完整结构。目前已有5308人学习下载。读者可从中获取STM32外设配置、OV7670寄存器设置、JPEG压缩算法实现及无线传输协议对接等关键代码,并借助工程文件快速搭建调试环境,理解图像采集到传输的完整流程,为同类嵌入式视觉项目积累可复用的开发经验。

1. STM32 驱动 OV7670 做 JPEG 无线图传:一条能跑通的链路长什么样

OV7670 这颗摄像头模组在 STM32 圈子里算是老面孔了,价格便宜、资料多,但真正把它和 JPEG 压缩、无线传输串成一条完整链路的人并不多。大多数人卡在两个地方:一是 OV7670 输出的是 RGB565 或 YUV 原始数据,一帧 QVGA 就是 150KB 起步,STM32 的 SRAM 根本存不下;二是无线模块的带宽有限,不压缩根本传不动。这个方案要解决的核心问题就是:用 STM32 把 OV7670 的原始图像数据读进来,在片内完成 JPEG 编码,再通过无线模块把压缩后的数据发出去。适合做物联网网关、远程监控、毕业设计这类需要「采集—压缩—传输」完整链路的场景。下面按硬件选型、驱动配置、JPEG 编码、无线发送、避坑排查的顺序,把每个环节的参数和代码都摊开讲。

2. 硬件选型与数据通路:为什么不是随便拿块 STM32 就能跑

2.1 OV7670 带不带 FIFO 决定了你的驱动写法

OV7670 模组市面上有两种:带 AL422B FIFO 的和不带的。这个区别直接决定你的代码架构。不带 FIFO 的模组,PCLK、VSYNC、HREF 三个同步信号直接接到 STM32,你得用 DCMI 接口或者 GPIO 中断在 PCLK 上升沿逐个字节读,对时序要求极高,稍微有点中断延迟就丢行。带 FIFO 的模组,OV7670 先把一帧数据写进 AL422B,STM32 再慢慢读出来,时序压力小很多,但 FIFO 容量只有 384KB,存一帧 QVGA RGB565 刚好够,存 VGA 就不行了。

我一般推荐用带 FIFO 的版本做第一版验证,跑通之后再考虑换 DCMI 直连方案。带 FIFO 的接线方式如下:

信号STM32 引脚说明
FIFO_RCLKPA0读时钟,下降沿读数据
FIFO_RRSTPA1读指针复位
FIFO_OEPA2输出使能,低有效
FIFO_WRSTPA3写指针复位
FIFO_WENPA4写使能
FIFO_WEPA5写时钟
D0-D7PC0-PC78 位数据总线

VSYNC 和 HREF 在带 FIFO 的方案里不接 STM32,直接由模组内部逻辑控制 FIFO 写入。STM32 只需要控制 FIFO 的读写指针和时钟。

2.2 STM32 型号选择:SRAM 和主频是两个硬指标

JPEG 编码需要一块连续的内存做缓冲区。QVGA(320×240)RGB565 原始数据是 153600 字节,JPEG 编码过程中还需要额外的临时空间,保守估计要 200KB 以上的 RAM。STM32F103 系列最多 64KB SRAM,根本不够。F407 有 192KB,勉强够但很紧张。F429 或 F767 有 256KB 以上,跑起来从容很多。

主频方面,JPEG 编码是计算密集型任务。F103 跑 72MHz,编码一帧 QVGA 大概要 300-500ms,帧率上不去。F407 跑 168MHz,能压到 100ms 以内。如果对帧率有要求,建议至少上 F407。

提示:如果手头只有 F103,可以把分辨率降到 QQVGA(160×120),原始数据只有 38KB,编码时间也能接受,但画质就那样了。

2.3 无线模块的带宽账要先算清楚

JPEG 压缩比通常在 10:1 到 20:1 之间,QVGA 一帧压缩后大概 8-15KB。如果要做 10fps,需要 80-150KB/s 的稳定带宽。常见无线模块的实际吞吐:

模块理论速率实际稳定吞吐能否支撑 10fps
NRF24L012Mbps100-200KB/s勉强,需分包重传
ESP826672Mbps500KB/s-1MB/s轻松
HC-05 蓝牙2.1Mbps80-120KB/s困难
LoRa37.5kbps2-5KB/s完全不行

我一般用 ESP8266 做透传,AT 固件配好 TCP 连接后直接往串口丢数据就行,省事。NRF24L01 便宜但分包逻辑要自己写,丢包重传处理不好就是满屏花屏。

3. OV7670 驱动配置:寄存器怎么写、时钟怎么设

3.1 SCCB 总线时序与寄存器写入

OV7670 用 SCCB 总线配置寄存器,协议跟 I2C 几乎一样,但没有 ACK 位。STM32 可以用硬件 I2C 也可以 GPIO 模拟。我一般用 GPIO 模拟,因为 SCCB 的时序跟标准 I2C 有细微差别,硬件 I2C 有时候会卡在等待 ACK 上。

// SCCB 写寄存器,模拟 I2C 时序 void SCCB_Write(uint8_t reg, uint8_t data) { SCCB_Start(); SCCB_SendByte(0x42); // OV7670 写地址 SCCB_WaitAck(); // SCCB 不检查 ACK,但时序上要留时间 SCCB_SendByte(reg); // 寄存器地址 SCCB_WaitAck(); SCCB_SendByte(data); // 写入数据 SCCB_WaitAck(); SCCB_Stop(); }

逻辑说明:SCCB_Start 拉低 SDA 再拉低 SCL,SCCB_SendByte 从高位到低位逐位输出,每发一位拉高 SCL 再拉低。参数说明:0x42 是 OV7670 的写地址(7 位地址 0x21 左移一位),读地址是 0x43。SCCB_WaitAck 里不要真的去读 SDA 判断 ACK,OV7670 在有些寄存器写入后不会拉低 SDA,死等会卡住,给几个微秒延时就行。

3.2 关键寄存器配置:输出格式、分辨率、时钟分频

OV7670 上电后默认输出 RGB565,但分辨率、帧率、时钟分频都要手动配。下面是一组 QVGA RGB565 的常用配置:

// OV7670 QVGA RGB565 初始化序列(部分关键寄存器) void OV7670_Init(void) { SCCB_Write(0x12, 0x80); // 复位所有寄存器 delay_ms(100); SCCB_Write(0x12, 0x00); // 清除复位 // 时钟配置:PLL 倍频,输入时钟 24MHz 时输出 24MHz SCCB_Write(0x11, 0x80); // 时钟分频,0x80 表示不分频 // 输出格式:RGB565 SCCB_Write(0x12, 0x04); // 输出格式控制 SCCB_Write(0x8C, 0x00); // RGB565 低字节 SCCB_Write(0x40, 0xD0); // RGB565 高字节,D0 表示 RGB565 // 分辨率:QVGA 320x240 SCCB_Write(0x0C, 0x04); // 水平控制 SCCB_Write(0x3E, 0x19); // 垂直控制 SCCB_Write(0x72, 0x11); // 缩放控制 SCCB_Write(0x73, 0x00); // 缩放控制 // 帧率:30fps SCCB_Write(0x6B, 0x0A); // PLL 控制 SCCB_Write(0x2D, 0x00); // 帧率调整 }

逻辑说明:0x12 寄存器是复位和格式控制,写 0x80 复位,写 0x04 表示输出 RGB565。0x40 寄存器的 bit7-bit4 控制 RGB 格式,0xD0 对应 RGB565。0x0C 和 0x3E 控制分辨率和窗口,0x72 和 0x73 是缩放控制,QVGA 需要开缩放。参数说明:如果输入时钟不是 24MHz,0x11 的分频值要重新算。比如输入 12MHz,要写 0x40 做 2 倍频。

3.3 FIFO 读取时序:什么时候读、读多少

带 FIFO 的模组,读取流程是:先复位写指针,等一帧写满,再复位读指针,然后逐个时钟读出来。关键点是 VSYNC 信号的处理——虽然 VSYNC 不接 STM32,但你可以通过 FIFO 的 WE 信号或者直接延时来判断一帧是否写满。

// 从 FIFO 读取一帧 QVGA RGB565 数据 void FIFO_ReadFrame(uint8_t *buf, uint32_t len) { uint32_t i; FIFO_WRST_L(); // 复位写指针 delay_us(10); FIFO_WRST_H(); delay_ms(50); // 等一帧写满,QVGA 30fps 约 33ms,留余量 FIFO_RRST_L(); // 复位读指针 delay_us(10); FIFO_RRST_H(); FIFO_OE_L(); // 使能输出 for(i = 0; i < len; i++) { FIFO_RCLK_L(); delay_us(1); buf[i] = GPIO_ReadData(); // 读 8 位数据 FIFO_RCLK_H(); delay_us(1); } FIFO_OE_H(); // 关闭输出 }

逻辑说明:先复位写指针让 FIFO 从头开始写,延时等一帧数据写入完成,再复位读指针从头读。FIFO_RCLK 下降沿读数据,所以先拉低再读。参数说明:len 是 QVGA RGB565 的字节数,320×240×2 = 153600。delay_us(1) 是给 FIFO 输出稳定的时间,太快了数据可能没建立好。如果发现读出来的图像有规律性噪点,先把延时加大到 2-3us 试试。

4. JPEG 编码:在 STM32 上怎么压、压完怎么发

4.1 用 TinyJPEG 还是自己写?我选 TinyJPEG

STM32 上跑 JPEG 编码,常见方案有三种:一是用硬件 JPEG 编码器(比如 STM32F767 自带),二是移植 TinyJPEG 这类轻量库,三是自己写 DCT 和霍夫曼编码。硬件编码器最省事但芯片贵,自己写工作量太大,TinyJPEG 是平衡点——代码量小、可移植、压缩质量够用。

TinyJPEG 的核心是两级 DCT 和标准霍夫曼表。它把图像分成 8×8 块,每块做 DCT 变换,量化后做霍夫曼编码。在 F407 上跑 QVGA 大概 80-120ms 一帧,F103 上要 400ms 以上。

// TinyJPEG 编码调用示例 #include "tinyjpeg.h" void JPEG_EncodeFrame(uint8_t *rgb_buf, uint8_t *jpeg_buf, uint32_t *jpeg_len) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); // 设置输出缓冲区 jpeg_mem_dest(&cinfo, &jpeg_buf, jpeg_len); cinfo.image_width = 320; cinfo.image_height = 240; cinfo.input_components = 3; // RGB cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, 60, TRUE); // 质量 60,压缩比约 15:1 jpeg_start_compress(&cinfo, TRUE); while (cinfo.next_scanline < cinfo.image_height) { JSAMPROW row = &rgb_buf[cinfo.next_scanline * 320 * 3]; jpeg_write_scanlines(&cinfo, &row, 1); } jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); }

逻辑说明:jpeg_mem_dest 把输出写到内存缓冲区而不是文件,适合嵌入式。jpeg_set_quality 设 60 是压缩比和画质的平衡点,设 30 压缩比更高但块效应明显,设 90 画质好但一帧 30KB 以上,无线传输压力大。参数说明:input_components 设 3 表示 RGB 三通道,如果 OV7670 输出的是 YUV422,要先转成 RGB 再喂给编码器,或者直接改 TinyJPEG 的输入格式。

4.2 RGB565 转 RGB888:颜色空间转换的坑

OV7670 输出 RGB565,每个像素 2 字节,TinyJPEG 要 RGB888,每个像素 3 字节。转换公式:

// RGB565 转 RGB888 void RGB565_to_RGB888(uint8_t *src, uint8_t *dst, uint32_t pixels) { uint32_t i; uint16_t rgb565; uint8_t r, g, b; for(i = 0; i < pixels; i++) { rgb565 = (src[i*2] << 8) | src[i*2+1]; r = (rgb565 >> 11) & 0x1F; // 高 5 位 g = (rgb565 >> 5) & 0x3F; // 中 6 位 b = rgb565 & 0x1F; // 低 5 位 // 扩展到 8 位,低位补高位 dst[i*3] = (r << 3) | (r >> 2); dst[i*3+1] = (g << 2) | (g >> 4); dst[i*3+2] = (b << 3) | (b >> 2); } }

逻辑说明:RGB565 的 R 和 B 是 5 位,G 是 6 位。扩展到 8 位时不能简单左移,要把高位补到低位,否则颜色会偏暗。比如 R 的 5 位值 0x1F 左移 3 位是 0xF8,再或上 0x07 才是 0xFF。参数说明:pixels 是像素总数,QVGA 是 76800。转换后数据量从 153600 字节变成 230400 字节,内存要留够。

4.3 无线发送:分包、缓冲、丢包处理

JPEG 数据一帧 8-15KB,无线模块的 MTU 通常只有几百字节到 1KB,必须分包。以 ESP8266 透传为例,串口波特率 921600 时实际吞吐约 80KB/s,一帧 10KB 要 125ms,加上编码时间,整体帧率大概 3-5fps。

// 通过 ESP8266 发送 JPEG 数据 void Wireless_SendJPEG(uint8_t *jpeg_buf, uint32_t len) { uint32_t sent = 0; uint32_t chunk; while(sent < len) { chunk = (len - sent > 1024) ? 1024 : (len - sent); // 发送包头:4 字节长度 + 2 字节序号 UART_SendByte((chunk >> 24) & 0xFF); UART_SendByte((chunk >> 16) & 0xFF); UART_SendByte((chunk >> 8) & 0xFF); UART_SendByte(chunk & 0xFF); // 发送数据 UART_SendBuf(jpeg_buf + sent, chunk); sent += chunk; delay_ms(5); // 给模块缓冲时间 } }

逻辑说明:每包带 4 字节长度头,接收端先读长度再读数据。delay_ms(5) 是防止发送太快模块缓冲区溢出。参数说明:chunk 最大 1024 是 ESP8266 透传的推荐值,设太大容易丢包。如果丢包严重,把 chunk 降到 512 并加大延时。

5. 避坑与排查:调不通的时候先看这几个地方

5.1 图像花屏、颜色错乱

现象:读出来的图像有规律性条纹,或者颜色完全不对。

原因:FIFO 读时钟太快,数据没建立好;或者 RGB565 字节序搞反了。

解决:先把 FIFO_RCLK 的延时从 1us 加到 3us,看条纹是否消失。颜色不对的话,检查 RGB565 的高低字节顺序,OV7670 默认高字节在前,但有些模组是反的,把(src[i*2] << 8) | src[i*2+1]改成(src[i*2+1] << 8) | src[i*2]试试。

5.2 JPEG 编码后图像全黑或全白

现象:编码成功但解码出来是纯色。

原因:RGB888 缓冲区没初始化,或者 TinyJPEG 的输入格式设错了。

解决:先确认 RGB565 转 RGB888 的输出是否正确,可以先把转换后的数据直接通过串口发到电脑上看。如果转换没问题,检查cinfo.in_color_space是不是设成了JCS_RGB,设成JCS_GRAYSCALE就会输出灰度图。

5.3 无线传输丢包导致图像下半部分缺失

现象:接收端收到的图像上半部分正常,下半部分花屏或缺失。

原因:发送太快,模块缓冲区溢出,后面的包被丢了。

解决:在每包之间加延时,从 5ms 加到 10ms。或者降低串口波特率,921600 降到 460800,牺牲速度换稳定性。如果用的是 NRF24L01,要加 ACK 重传机制,发送方等接收方确认后再发下一包。

5.4 STM32 跑着跑着死机

现象:连续运行几分钟后程序卡死,看门狗复位。

原因:内存溢出,JPEG 编码的临时缓冲区越界了。

解决:检查 TinyJPEG 的jpeg_mem_dest分配的缓冲区大小,至少要给 2 倍原始数据量。QVGA RGB888 是 230KB,JPEG 输出缓冲区给 30KB,但编码过程中的临时空间要额外留 50KB 以上。用__attribute__((section(".ccmram")))把缓冲区放到 CCM RAM 里,减轻主 RAM 压力。

5.5 帧率上不去,只有 1-2fps

现象:整体链路能跑通但帧率极低。

原因:JPEG 编码耗时太长,或者无线发送阻塞了采集。

解决:先测编码时间,在编码前后翻转一个 GPIO,用示波器看脉宽。如果编码超过 200ms,考虑降分辨率到 QQVGA 或者换 F429/F767。如果编码没问题但发送慢,把无线模块的波特率拉满,ESP8266 可以到 1.5Mbps。另外采集和发送可以做成双缓冲,采集一帧的同时发送上一帧,能提高整体吞吐。

6. 进阶技巧:用双缓冲和 DMA 把帧率再拉一截

单缓冲方案里,采集、编码、发送是串行的,CPU 大部分时间在等 FIFO 和等串口。改成双缓冲后,采集填 buffer A 的时候,CPU 去编码 buffer B,编码完的 buffer B 通过 DMA 发给无线模块,同时采集继续填 buffer A。这样三个环节并行,帧率能提升 50% 以上。

具体做法是开两块 RGB565 缓冲区,每块 153600 字节,F407 的 192KB RAM 放不下两块,得用外部 SRAM 或者把分辨率降到 QQVGA。如果坚持 QVGA,可以用 F429 的 256KB RAM,或者外挂一颗 23LC1024 这类 SPI SRAM。

DMA 发送这块,把串口的 TX 配成 DMA 模式,JPEG 编码完后直接启动 DMA 传输,CPU 不用管。传输完成中断里切换缓冲区状态标志。注意 DMA 传输期间不能修改缓冲区内容,所以双缓冲的状态机要写对:缓冲区只有三种状态——空闲、采集中、发送中,状态切换用原子操作或者关中断保护。

// 双缓冲状态机伪代码 typedef enum { BUF_IDLE, BUF_FILLING, BUF_SENDING } buf_state_t; buf_state_t buf_state[2]; uint8_t *rgb_buf[2]; uint8_t *jpeg_buf[2]; void Main_Loop(void) { uint8_t idx; for(idx = 0; idx < 2; idx++) { if(buf_state[idx] == BUF_IDLE) { buf_state[idx] = BUF_FILLING; FIFO_ReadFrame(rgb_buf[idx], 153600); buf_state[idx] = BUF_SENDING; JPEG_EncodeFrame(rgb_buf[idx], jpeg_buf[idx], &jpeg_len); UART_DMA_Send(jpeg_buf[idx], jpeg_len); // DMA 传输完成中断里把状态改回 BUF_IDLE } } }

逻辑说明:主循环轮询两个缓冲区的状态,空闲的就拿去采集。采集完直接编码,编码完启动 DMA 发送,状态改成 SENDING。DMA 传输完成中断里把状态改回 IDLE,下一轮循环就能复用。参数说明:jpeg_len 是编码后的实际长度,DMA 发送要用这个长度而不是缓冲区大小。如果发现图像撕裂,说明采集和发送用了同一块缓冲区,检查状态切换是不是漏了关中断保护。

这套方案我在 F407 上跑过,QVGA 能稳定在 8-10fps,QQVGA 能到 20fps 以上。关键是把每个环节的耗时摸清楚,哪里慢就优化哪里。无线模块的带宽往往是瓶颈,先把有线串口调通,确认图像和编码没问题,再换无线模块。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询