☰
STM32驱动OV7670实现JPEG无线图传方案详解
2026/10/28 16:11:09 网站建设 项目流程

简介:这份资源面向嵌入式开发学习者与STM32实践者,围绕OV7670摄像头图像采集、JPEG压缩与无线传输这一完整链路展开,适合希望打通数字图像处理与无线通信环节的中级开发者参考。压缩包共184个文件,约852KB,以C源码与头文件为主体,包含43个c、46个h文件,另有40个汇编文件、29个编译产物及4个ICF链接配置,并附带少量htm、ewd、ewp等工程与说明文档,覆盖STM32外设驱动、图像采集与工程配置等模块。目前已有5308人学习下载。资源涉及STM32对OV7670寄存器的配置、JPEG有损压缩算法在资源受限平台上的实现思路,以及通过无线模块发送图像数据的接口设计,读者可据此理解从摄像头采集到压缩传输的完整流程,并借鉴其工程目录组织与调试方法,为同类嵌入式视觉项目提供可复用的参考框架。

1. 从一颗 OV7670 到 JPEG 无线图传:这套 STM32 方案到底能跑通什么

手里有一块 OV7670、一片 STM32、一个无线模块,想把画面传到电脑或另一块屏上,这是很多做图像采集的工程师绕不开的第一道坎。OV7670 本身输出的是 RAW RGB 或 YUV 原始数据,一帧 QVGA 就是 320×240×2 字节,接近 150KB,直接通过串口或无线模块往外怼,带宽根本扛不住,而且上位机收到的是一堆无法直接显示的裸数据。这套资源的核心价值就在于把「采集—压缩—传输」这条链路完整串起来:STM32 驱动 OV7670 抓取图像,在片内完成 JPEG 压缩,再通过无线模块把压缩后的码流发出去。JPEG 压缩能把一帧 QVGA 图像压到 10KB 到 30KB 量级,压缩比通常在 5:1 到 15:1 之间,具体取决于图像内容和质量因子设置,这样无线传输的压力就小了一个数量级。适合谁看?正在做基于 STM32 的图像采集课程设计、毕业设计,或者想给物联网网关加一路低分辨率视觉能力的从业者。如果你手上是带 FIFO 的 OV7670 模组,接线和时序会简单不少;不带 FIFO 的版本对 STM32 的 GPIO 翻转速度和 DCMI 时序要求更苛刻,这套代码里对两种情况的处理方式不一样,后面会拆开讲。

2. 硬件链路与数据流:OV7670 怎么把像素交给 STM32

2.1 OV7670 的输出格式与 STM32 侧接口选型

OV7670 支持 RAW RGB、RGB565、YUV422 等输出格式,但这颗传感器本身不具备 JPEG 输出能力,它只负责把感光阵列的模拟量转成数字像素流。很多人第一次用会误以为 OV7670 能直接出 JPEG,翻遍寄存器手册才发现根本没有这个选项。所以 JPEG 压缩必须在 STM32 侧完成,这是整个方案里最吃算力和内存的一步。

STM32 这边接 OV7670 常见两种方式:一种是带 FIFO 的模组,比如 AL422B,传感器先把一帧写进 FIFO,STM32 再慢慢读,对 GPIO 速度要求低,F103 系列也能跑;另一种是不带 FIFO,直接靠 DCMI 接口或 GPIO 并行抓,这时候 PCLK 频率、HSYNC/VSYNC 的极性配置错一点就是花屏或者全黑。资源里用的是 DCMI + DMA 的方案,DCMI 负责同步信号解析,DMA 把数据搬到帧缓冲区,CPU 不参与搬运,留给 JPEG 编码用。

选型上要注意:STM32F103 没有 DCMI 接口,只能走 GPIO 模拟时序或者用带 FIFO 的模组;STM32F407/F429 这类带 DCMI 的型号才是这套代码的主战场。如果你手上是 F103 最小系统板,别硬套 DCMI 方案,改成 FIFO 读取模式,代码里对应的宏要切过去。

2.2 帧缓冲区规划与内存分配

一帧 RGB565 QVGA 是 153600 字节,F407 的 SRAM 有 192KB,放一帧勉强够,但 JPEG 编码过程中还需要额外的临时空间。常见做法是开两块缓冲区:一块给 DCMI DMA 写入,一块给 JPEG 编码读取,用双缓冲切换避免撕裂。如果 SRAM 不够,可以降到 QQVGA(160×120),一帧只要 38400 字节,压缩后的码流也更小,无线传输更稳。

// 帧缓冲区定义,放在 SRAM 里,注意对齐 #define FRAME_WIDTH 320 #define FRAME_HEIGHT 240 #define FRAME_SIZE (FRAME_WIDTH * FRAME_HEIGHT * 2) // RGB565 每像素 2 字节 __attribute__((aligned(4))) uint8_t frame_buffer_a[FRAME_SIZE]; __attribute__((aligned(4))) uint8_t frame_buffer_b[FRAME_SIZE]; // 当前 DMA 正在写入的缓冲区指针 volatile uint8_t *active_frame = frame_buffer_a; volatile uint8_t *ready_frame = frame_buffer_b; volatile uint8_t frame_ready_flag = 0; // DCMI DMA 传输完成回调,切换缓冲区 void DCMI_DMA_CompleteCallback(void) { if (active_frame == frame_buffer_a) { active_frame = frame_buffer_b; ready_frame = frame_buffer_a; } else { active_frame = frame_buffer_a; ready_frame = frame_buffer_b; } frame_ready_flag = 1; // 通知主循环有新帧可编码 }

这段代码的关键点在于aligned(4),DCMI 的 DMA 对地址对齐有要求,不对齐可能触发 HardFault。frame_ready_flag用 volatile 修饰,因为它在中断里被改写、在主循环里被读取,编译器优化时如果不加 volatile 可能把判断优化掉,这是血泪经验。双缓冲的切换逻辑要保证 DMA 不会写到正在被 JPEG 编码读取的那块内存,否则画面会出现上半帧新、下半帧旧的撕裂现象。

2.3 DCMI 与 DMA 的初始化配置

DCMI 的配置项里,HSYNC 和 VSYNC 的极性最容易翻车。OV7670 默认输出 VSYNC 高电平有效、HSYNC 低电平有效,但不同模组的电平转换电路可能反相,配置错了就是全黑或者只有一行数据。PCLK 极性也要和传感器寄存器里设置的输出边沿一致,通常选上升沿采样。

void DCMI_Init_For_OV7670(void) { DCMI_HandleTypeDef hdcmi; hdcmi.Instance = DCMI; hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; // 硬件同步 hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; // 上升沿采样 hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_HIGH; // VSYNC 高有效 hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_LOW; // HSYNC 低有效 hdcmi.Init.CaptureRate = DCMI_CR_ALL_FRAME; // 全帧捕获 hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; // 8 位数据 hdcmi.Init.JPEGMode = DCMI_JPEG_DISABLE; // 不用硬件 JPEG HAL_DCMI_Init(&hdcmi); // 启动 DMA,搬运到帧缓冲区 HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_SNAPSHOT, (uint32_t)active_frame, FRAME_SIZE / 4); }

CaptureRate设成全帧捕获,如果设成隔行或者裁剪模式,出来的图像尺寸和预期对不上。ExtendedDataMode选 8 位,因为 OV7670 的 D0-D7 是 8 位并行输出。HAL_DCMI_Start_DMA最后一个参数是传输字数,注意单位是 32 位字,所以是FRAME_SIZE / 4,这里写错会导致 DMA 搬一半就停。启动顺序也有讲究:先配置 OV7670 的寄存器(通过 SCCB 写),再启动 DCMI DMA,否则可能抓到一帧无效数据。

3. JPEG 压缩在 STM32 上怎么落地:从 RGB565 到码流

3.1 为什么选 JPEG 而不是 BMP 或 PNG

BMP 不做压缩,一帧 QVGA 就是 150KB,无线模块传一帧要好几秒,完全没法做实时预览。PNG 是无损压缩,算法复杂度比 JPEG 高,在 STM32 上跑一帧要几百毫秒甚至上秒级,而且压缩比不一定比 JPEG 好。JPEG 是有损压缩,但可以通过质量因子控制画质和体积的平衡,在 Cortex-M4 上跑一帧 QVGA 大约 50 到 150 毫秒,配合无线模块的速率,能做到每秒 2 到 5 帧的准实时传输。对于监控、巡检这类场景,这个帧率够用了。

资源里用的 JPEG 编码库是常见的 picojpeg 或者 TinyJPEG 的裁剪版,这类库的特点是代码量小、内存占用可控,适合单片机。编码流程是:RGB565 转 YCbCr → 分 8×8 块做 DCT → 量化 → 熵编码。其中 DCT 和量化是算力大头,Cortex-M4 带 FPU 的话可以用浮点加速,但更常见的做法是用定点数查表,避免浮点运算的开销。

3.2 RGB565 转 YCbCr 的定点实现

JPEG 内部用的是 YCbCr 色彩空间,所以第一步要把 RGB565 解包成 R、G、B 三个分量,再转成 Y、Cb、Cr。浮点公式是:

Y = 0.299R + 0.587G + 0.114B Cb = -0.169R - 0.331G + 0.5B + 128 Cr = 0.5R - 0.419G - 0.081B + 128

在 STM32 上直接跑浮点会慢,常见做法是放大成整数运算,用移位代替除法。

// RGB565 转 YCbCr,定点实现,放大 256 倍 void RGB565_to_YCbCr(uint16_t pixel, uint8_t *y, uint8_t *cb, uint8_t *cr) { uint8_t r = (pixel >> 11) & 0x1F; // 5 位红 uint8_t g = (pixel >> 5) & 0x3F; // 6 位绿 uint8_t b = pixel & 0x1F; // 5 位蓝 // 扩展到 8 位 r = (r << 3) | (r >> 2); g = (g << 2) | (g >> 4); b = (b << 3) | (b >> 2); // 定点运算,系数放大 256 倍 int32_t y_val = ( 77 * r + 150 * g + 29 * b) >> 8; int32_t cb_val = (-43 * r - 85 * g + 128 * b) >> 8; int32_t cr_val = (128 * r - 107 * g - 21 * b) >> 8; *y = (uint8_t)(y_val); *cb = (uint8_t)(cb_val + 128); *cr = (uint8_t)(cr_val + 128); }

系数 77、150、29 分别是 0.299、0.587、0.114 乘以 256 取整的结果。右移 8 位等价于除以 256。Cb 和 Cr 要加 128 偏移,因为 JPEG 的色度分量是无符号的,范围 0 到 255。这段代码每个像素执行一次,QVGA 就是 76800 次,所以循环里的乘法尽量用移位和加法替代,有些实现会把系数进一步简化成 64 倍或者 128 倍来减少溢出风险。

3.3 量化表与质量因子的取舍

JPEG 的压缩比主要靠量化表控制,量化步长越大,高频信息丢得越多,压缩比越高但画质越差。标准 JPEG 有两张量化表,分别给亮度和色度。质量因子 Q 从 1 到 100,Q 越高画质越好、体积越大。在 STM32 上,常见做法是预置一张中等质量的量化表,不动态调整,省去计算缩放表的开销。

质量因子一帧 QVGA 压缩后大小画质评价适用场景
Q=308~12KB有明显块效应低带宽、只求看清轮廓
Q=5015~22KB可接受,细节有损失一般监控、巡检
Q=7525~35KB较好,肉眼难辨需要看清文字或细节
Q=9045~60KB接近原始带宽充足、画质优先

无线模块的速率决定了你能用多高的 Q。比如常见的 NRF24L01 速率是 2Mbps,实际有效吞吐大概 100KB/s 左右,传一帧 Q=50 的图要 0.2 秒,加上编码时间,整体帧率在 2 到 3 帧。如果换成串口透传模块,115200 波特率下每秒只能传 11KB 左右,一帧就要 2 秒,基本只能做单帧抓拍。

3.4 编码后的码流组织与无线发送

JPEG 编码输出的是标准 JPEG 码流,以 FFD8 开头、FFD9 结尾。无线发送时不能直接一包怼出去,因为无线模块有单包长度限制,NRF24L01 一包最多 32 字节。常见做法是把码流按 32 字节分片,每片加一个包头,包含帧序号、分片序号、总片数,接收端按序号重组。

// JPEG 码流分片发送,每片 32 字节 #define WIRELESS_PAYLOAD 32 #define HEADER_SIZE 4 void send_jpeg_stream(uint8_t *jpeg_buf, uint32_t jpeg_len) { uint16_t total_packets = (jpeg_len + WIRELESS_PAYLOAD - 1) / WIRELESS_PAYLOAD; uint8_t packet[WIRELESS_PAYLOAD + HEADER_SIZE]; for (uint16_t i = 0; i < total_packets; i++) { packet[0] = (i >> 8) & 0xFF; // 分片序号高字节 packet[1] = i & 0xFF; // 分片序号低字节 packet[2] = (total_packets >> 8) & 0xFF; packet[3] = total_packets & 0xFF; uint16_t offset = i * WIRELESS_PAYLOAD; uint16_t chunk = jpeg_len - offset; if (chunk > WIRELESS_PAYLOAD) chunk = WIRELESS_PAYLOAD; memcpy(&packet[HEADER_SIZE], jpeg_buf + offset, chunk); wireless_send(packet, HEADER_SIZE + chunk); HAL_Delay(2); // 给无线模块留发送间隔,避免丢包 } }

包头里放分片序号和总片数,接收端收到后按序号排序,丢包时能知道缺哪一片。HAL_Delay(2)这个间隔是经验值,NRF24L01 连续发送太快会丢包,加个小延时能明显改善。如果用的是带 ACK 的可靠传输模式,可以不加延时,但吞吐会下降。接收端重组时要注意,JPEG 码流必须完整才能解码,缺一片整帧就废了,所以要么用可靠传输,要么在应用层做重传请求。

4. 避坑与排查:这套链路最容易翻车的五个地方

4.1 图像全黑或全白

现象:DCMI 启动后,帧缓冲区里全是 0x00 或 0xFF,串口打印出来一片黑或一片白。

原因:最常见的是 OV7670 的寄存器没配置成功。SCCB 时序和 I2C 不完全一样,用硬件 I2C 去写 OV7670 经常失败,因为 SCCB 不支持重复起始条件。另一个原因是 PCLK 极性配反了,数据在错误的边沿被采样。

解决:先用逻辑分析仪抓 SCCB 的 SCL/SDA 波形,确认寄存器写入有 ACK。如果用的是硬件 I2C,改成 GPIO 模拟 SCCB 时序。PCLK 极性在 DCMI 初始化和 OV7670 寄存器里都要设成一致,通常传感器侧设成上升沿输出,STM32 侧设成上升沿采样。

4.2 图像花屏或错位

现象:能出图,但画面撕裂、颜色错乱、或者图像整体偏移。

原因:帧缓冲区地址不对齐,DMA 搬运时地址递增方向错了;或者 HSYNC/VSYNC 极性配错,导致行列计数错位。还有一种情况是 RGB565 的高低字节顺序反了,颜色会变成负片效果。

解决:检查帧缓冲区是否 4 字节对齐,DMA 配置里MemInc要设成递增。HSYNC 极性用示波器看实际波形,OV7670 默认 HSYNC 低有效,但有些模组加了反相器。RGB565 字节顺序在 DCMI 的ExtendedDataMode里可以调,8 位模式下如果颜色不对,试试把高低字节交换。

4.3 JPEG 编码后码流无法解码

现象:无线传回来的数据存成 .jpg 文件,打不开,或者解码出来是灰屏。

原因:码流头尾不完整,FFD8 或 FFD9 被截断;或者分片重组时顺序错了,中间缺片。还有一种可能是 JPEG 编码库的输出缓冲区溢出,码流被截断。

解决:在发送前打印码流的前 4 字节和后 4 字节,确认是 FFD8 开头、FFD9 结尾。分片重组时按序号排序,缺片就丢弃整帧并请求重传。编码输出缓冲区要留足空间,QVGA Q=75 大概需要 40KB 缓冲,不够的话降分辨率或降质量因子。

4.4 无线传输丢包严重

现象:接收端收到的帧经常缺片,图像解码失败率高。

原因:无线模块的发送速率超过了实际吞吐能力,或者发送间隔太短,模块内部缓冲区溢出。电源不稳也会导致无线模块复位,尤其是 NRF24L01 对电源纹波很敏感。

解决:在每包之间加 2 到 5 毫秒延时,或者改用带 ACK 的可靠模式。电源端加 10uF 和 0.1uF 电容滤波,NRF24L01 的 VCC 引脚旁边必须放 0.1uF。如果环境干扰大,换频道或者降低空中速率。

4.5 帧率远低于预期

现象:理论算下来能跑 5 帧,实际只有 1 帧不到。

原因:JPEG 编码耗时比预期长,尤其是没开 FPU 或者用了浮点运算。另一个大头是内存拷贝,RGB565 转 YCbCr 如果逐像素调用函数,函数调用开销会累积。

解决:把 RGB565 转 YCbCr 的代码内联到循环里,避免函数调用。开启 STM32 的 FPU 和编译器优化 -O2。如果还是慢,降低分辨率到 QQVGA,或者把质量因子降到 30。DMA 搬运和 JPEG 编码可以并行,用双缓冲让编码和采集重叠,能省掉一帧的等待时间。

5. 进阶技巧:把帧率和稳定性再往上推一截

5.1 用双缓冲加 DMA 实现采集与编码并行

前面提到的双缓冲不只是防撕裂,还能让采集和编码真正并行。DCMI DMA 在写 buffer A 的时候,CPU 去编码 buffer B,等 DMA 写完 A 触发中断,切换角色。这样一帧的周期取决于采集和编码中较慢的那个,而不是两者之和。实测在 F407 上,QVGA 采集一帧约 30 毫秒,JPEG 编码约 80 毫秒,串行做要 110 毫秒,并行后降到 80 毫秒左右,帧率提升 30% 以上。

// 主循环里的并行处理逻辑 while (1) { if (frame_ready_flag) { frame_ready_flag = 0; uint8_t *encode_buf = (uint8_t *)ready_frame; // 编码当前帧,同时 DMA 在采集下一帧 uint32_t jpeg_len = jpeg_encode(encode_buf, FRAME_SIZE, jpeg_out_buf); if (jpeg_len > 0) { send_jpeg_stream(jpeg_out_buf, jpeg_len); } } // 其他任务,比如按键扫描、状态灯 }

这里的关键是jpeg_encode执行期间,DCMI DMA 已经在往另一块缓冲区写数据了,不需要 CPU 干预。frame_ready_flag在 DMA 完成中断里置位,主循环检测到就处理。注意jpeg_out_buf要足够大,并且不能和帧缓冲区重叠。

5.2 动态调整质量因子适配无线带宽

固定质量因子在带宽波动时要么浪费带宽、要么丢包。更好的做法是根据发送成功率动态调 Q。接收端定期回传丢包率,发送端如果发现丢包率超过 10%,就把 Q 降一档;低于 2% 就升一档。这样在带宽好的时候画质自动提升,带宽差的时候优先保连通。

丢包率动作质量因子范围
>15%降两档30~50
5%~15%降一档40~65
2%~5%保持50~75
<2%升一档60~85

调整的步长建议 10,太小了响应慢,太大了画质波动明显。每次调整后至少观察 10 帧再决定下一步,避免震荡。

5.3 验证方法:用串口把 JPEG 码流存成文件

调试阶段最直接的办法是把无线接收端换成串口,把码流通过 UART 发到电脑,用串口助手存成 .jpg 文件,双击看能不能打开。能打开说明编码和分片重组没问题,打不开就往前查编码库和缓冲区。这个办法比盯着 LCD 看靠谱得多,因为 LCD 显示还涉及解码和刷屏,多一层干扰。

我一般会在编码输出后先算一下码流的长度和头尾标记,打印到串口:len=23456, head=FFD8, tail=FFD9。三个值都对,再走无线。如果 head 不对,查编码库初始化;tail 不对,查输出缓冲区大小;len 异常大或异常小,查质量因子和图像内容。从那以后我每次调 JPEG 链路都强制走一遍这个检查,省得在无线环节瞎猜。希望帮到你。

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

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

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

立即咨询