简介这份资源面向嵌入式开发学习者与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 怎么把像素交给 STM322.1 OV7670 的输出格式与 STM32 侧接口选型OV7670 支持 RAW RGB、RGB565、YUV422 等输出格式但这颗传感器本身不具备 JPEG 输出能力它只负责把感光阵列的模拟量转成数字像素流。很多人第一次用会误以为 OV7670 能直接出 JPEG翻遍寄存器手册才发现根本没有这个选项。所以 JPEG 压缩必须在 STM32 侧完成这是整个方案里最吃算力和内存的一步。STM32 这边接 OV7670 常见两种方式一种是带 FIFO 的模组比如 AL422B传感器先把一帧写进 FIFOSTM32 再慢慢读对 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 不够可以降到 QQVGA160×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 或 PNGBMP 不做压缩一帧 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 到 100Q 越高画质越好、体积越大。在 STM32 上常见做法是预置一张中等质量的量化表不动态调整省去计算缩放表的开销。质量因子一帧 QVGA 压缩后大小画质评价适用场景Q308~12KB有明显块效应低带宽、只求看清轮廓Q5015~22KB可接受细节有损失一般监控、巡检Q7525~35KB较好肉眼难辨需要看清文字或细节Q9045~60KB接近原始带宽充足、画质优先无线模块的速率决定了你能用多高的 Q。比如常见的 NRF24L01 速率是 2Mbps实际有效吞吐大概 100KB/s 左右传一帧 Q50 的图要 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 Q75 大概需要 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~505%~15%降一档40~652%~5%保持50~752%升一档60~85调整的步长建议 10太小了响应慢太大了画质波动明显。每次调整后至少观察 10 帧再决定下一步避免震荡。5.3 验证方法用串口把 JPEG 码流存成文件调试阶段最直接的办法是把无线接收端换成串口把码流通过 UART 发到电脑用串口助手存成 .jpg 文件双击看能不能打开。能打开说明编码和分片重组没问题打不开就往前查编码库和缓冲区。这个办法比盯着 LCD 看靠谱得多因为 LCD 显示还涉及解码和刷屏多一层干扰。我一般会在编码输出后先算一下码流的长度和头尾标记打印到串口len23456, headFFD8, tailFFD9。三个值都对再走无线。如果 head 不对查编码库初始化tail 不对查输出缓冲区大小len 异常大或异常小查质量因子和图像内容。从那以后我每次调 JPEG 链路都强制走一遍这个检查省得在无线环节瞎猜。希望帮到你。本文还有配套的精品资源点击获取