简介:一套基于STM32的超声波监视器完整工程源码,面向嵌入式开发与STM32学习者,适用于智能安防、实验室监控等场景。项目将HC-SR04测距与OV7670图像采集结合,当检测到物体靠近时自动触发拍照,并利用FATFS文件系统将图像数据写入存储卡,从而实现具备入侵记录能力的实用监视系统。资源包共312个文件,其中以C源文件、头文件及工程配置文件为主,包含uvprojx工程、链接脚本、编译中间文件与少量图片;压缩包仅8.25MB,结构清晰,适合对照学习或在此基础上二次开发。目前已有1429人学习下载,覆盖传感器驱动、图像采集、文件系统移植等关键环节。读者可重点分析HC-SR04距离触发逻辑、OV7670数据读取时序、FATFS底层接口对接以及主循环任务调度,配套的txt说明与Keil工程配置也能帮助快速编译调试,避免常见坑点。 这个项目标题看着挺唬人,其实就是把三种常见外设拼成一个“声控拍照”的小系统:用 HC-SR04 超声波传感器测距离,STM32 当大脑,检测到物体进入设定范围后,触发 OV7670 摄像头拍下一帧图像。做安防监控、实验平台、毕设过渡都很合适,尤其适合手里已经有三件套但不知道能怎么组合的新手。
文章会把整个系统拆开,从硬件选型、引脚接线、核心寄存器配置,到软件触发逻辑、调试踩坑,一条线走完。不堆晦涩理论,只讲能落地的东西,每个关键参数都会解释为什么这么定,让你看完不仅能复现,还能根据自己需求改出花样来。
1. 整体设计方案与核心思路拆解
1.1 为什么选这三个核心模块
先说为什么是这个组合。OV7670 是一颗经典的 30 万像素 CMOS 摄像头,输出格式支持 RGB565、YUV422 等,数据接口是 DVP 并行口。它的优势是便宜、资料多、配置流程成熟,各大论坛一搜一大把参考代码;缺点也明显,没有内置 JPEG 压缩,需要额外 FIFO(如 AL422B)或用 STM32 的 DCMI 接口直采。STM32 这边正好大多带 DCMI 外设,所以选带 DCMI 的型号(如 F407、F429)可以省掉 FIFO 芯片,直接并行抓取数据。HC-SR04 就更常规了,一个 GPIO 发触发脉冲,一个 GPIO 读回波高电平宽度,就能算出距离,成本几块钱,探测范围 2cm 到 4m,完全够用。
三个模块的组合逻辑是典型的“事件驱动”结构:超声波负责“感知”,STM32 负责“决策”,摄像头负责“记录”。系统平时处于低功耗等待状态,只有超声波检测到目标进入设定距离才启动摄像头抓拍,拍完继续休眠。这种设计比起让摄像头连续运行再靠算法判断,功耗低得多,数据处理量也小得多。在实际项目里,很多低功耗监测设备都是这个思路。
1.2 系统工作流程和各模块职责
把整个工作过程拆开看,大概是这样一个状态流转:
- STM32 上电,完成时钟配置、GPIO 初始化、DCMI 配置、OV7670 寄存器初始化,进入待机。
- 主循环里周期性触发 HC-SR04,每次触发后通过输入捕获或外部中断测量回波高电平时间,换算成距离。
- 距离值经过滤波判断,若连续多次小于设定阈值(比如 50cm),判定为“有目标靠近”。
- 判定成立后,STM32 配置 DCMI 开始接收 OV7670 输出的一帧 RGB565 数据,存入内部 SRAM 或外部 SRAM。
- 一帧图像采集完成后,可以将原始数据通过串口发送给上位机,或写入 SD 卡,也可直接做简单的颜色识别或车牌定位。
这套流程里有个容易忽略的关键点:OV7670 和 HC-SR04 的供电电压不同。HC-SR04 一般 5V 供电,OV7670 模块通常 3.3V(有些模块自带稳压),而 STM32 的 GPIO 容忍电平有限,接线时必须处理好电平转换,否则轻则数据错乱,重则烧坏引脚。我习惯在 HC-SR04 的 Echo 输出脚串联一个 1kΩ 电阻再进 STM32,Trig 用 STM32 输出 3.3V,实验证明 HC-SR04 能稳定识别这个高电平。
2. 硬件选型与连接细节
2.1 器件清单与注意事项
动手之前先把料备齐,这里列一份我常用的清单,都是比较耐操的选择:
| 器件 | 推荐型号/规格 | 说明 |
|---|---|---|
| 主控 | STM32F407VET6 或 STM32F429 | 带 DCMI,性能足够,F429 可外扩 SDRAM |
| 摄像头 | OV7670 模块(带 24MHz 有源晶振版本) | 尽量选带稳压和晶振的成品模块,踩坑少 |
| 超声波 | HC-SR04(5V 供电版本) | 最普及,别买那种山寨焊工极差的 |
| 显示屏 | 2.4 寸 SPI 屏幕(可选) | 实时显示采集结果用 |
| 存储 | MicroSD 卡模块(SPI) | 存原始图像 |
| 电平转换 | 2 路双向电平转换模块或 1k 电阻 | 关键,防止烧 GPIO |
| 稳压 | AMS1117-3.3 | 给 STM32 和摄像头供电,独立一点更好 |
选择模块时有几个注意点。OV7670 一定要确认模块上的晶振是否是 24MHz,有些廉价模块把晶振省了,需要 STM32 的 MCO 引脚直接输出时钟,这种配置很折腾,建议直接买带晶振版本。HC-SR04 模块的型号版本很多,有些新款带温补或者改成了 3.3V 供电,买之前看商家说明,老版 5V 供电在淘宝最主流,用来入门完全没问题。
2.2 引脚分配与接线参考
引脚分配上,我采用的是最直接的 DCMI 直连方式,接线表如下:
| STM32F407 引脚 | 外设引脚 | 说明 |
|---|---|---|
| PE4 | OV7670 D7 | 数据线 |
| PE5 | OV7670 D6 | 数据线 |
| PE6 | OV7670 D5 | 数据线 |
| PE7 | OV7670 D4 | 数据线 |
| PE8 | OV7670 D3 | 数据线 |
| PE9 | OV7670 D2 | 数据线 |
| PE10 | OV7670 D1 | 数据线 |
| PE11 | OV7670 D0 | 数据线 |
| PA4 | OV7670 PCLK | 像素时钟输入 |
| PA6 | OV7670 VSYNC | 帧同步 |
| PA8 | OV7670 HREF | 行同步 |
| PB6 | OV7670 SCL | SCCB 时钟 |
| PB7 | OV7670 SDA | SCCB 数据 |
| PC1 | HC-SR04 Echo | 回波输入,串 1k 电阻 |
| PC2 | HC-SR04 Trig | 触发输出 |
| PE2 | 摄像头复位 | 复位控制 |
注意,DCMI 数据引脚不是随便配的,每个芯片的 DCMI 数据线对应固定引脚组,不是任意 GPIO 都能用。建议打开芯片数据手册的 DCMI 引脚映射表确认,不要靠猜。我第一次做的时候想当然把数据线接到了 PB 口一堆脚上,结果 DCMI 寄存器怎么配都不出数据,查了半天才发现引脚映射不对。
供电部分也要分开处理。STM32F407 用 3.3V 供电,OV7670 模块若自带稳压可直接供 3.3V 或 5V(看模块说明),HC-SR04 则用 5V。共用电源时,STM32 和 HC-SR04 要共地,否则回波信号参考电位不一致,测距必然乱跳。建议用一个 USB 转串口模块的 5V 作为总输入,经 AMS1117 再分出 3.3V。
2.3 时钟和复位电路的设计考量
STM32 外部晶振我用的 8MHz 无源晶振,配置 PLL 倍频到 168MHz,这是 F407 的常规操作。晶振的两个负载电容,网上计算工具很多,实际 18pF 到 22pF 都能跑得动,但注意要选用 C0G 材质电容,温度稳定性好,不会出现冬天能启动夏天不启动的诡异问题。复位电路用经典的 10k 上拉加 100nF 电容到地即可。
OV7670 模块的复位引脚,在初始化之前要拉低至少 1ms 再拉高,保证摄像头内部寄存器回到上电默认状态。SCCB 时序本质上与 I2C 非常接近,用 STM32 的硬件 I2C 也能跑,但我实测下来软件模拟 I2C 更省心,可以随便换引脚,还能避开硬件 I2C 的 bug 坑。延迟函数尤其重要,OV7670 上电后要等几毫秒,等内部晶振稳定再发配置命令。
3. 软件核心实现:从初始化到图像采集
3.1 代码结构规划
软件层面我拆成四个模块:
ov7670.c:OV7670 寄存器初始化、SCCB 读写函数、配置命令表。hc_sr04.c:超声波测距驱动,采用输入捕获方式测量回波高电平时间。dcmi_capture.c:DCMI 外设配置、DMA 传输、帧完成中断回调。main.c:主循环状态机、距离判断、触发采集、串口输出。
代码分模块的好处是后面想换传感器(比如换成红外测距)或者换摄像头(换成 OV2640),不用重写整个工程,只替换对应模块就行。这也是为什么推荐用库函数或 HAL 库,而不是直接操作寄存器——可读性好,项目后期维护省心。
3.2 OV7670 关键寄存器配置
OV7670 上电后内部寄存器默认输出是 YUV422 格式,不是我们想要的 RGB565,所以第一步必须通过 SCCB 写入寄存器,切换输出格式和数据时序。配置表和网上流传版本大体相同,但有几个关键寄存器值得单独说:
| 寄存器地址 | 写入值 | 含义 |
|---|---|---|
| 0x12 | 0x80 | 重置整个芯片 |
| 0x12 | 0x06 | 使用 RGB 输出,且 QVGA 尺寸 |
| 0x40 | 0x10 | RGB565 格式,RGB 顺序 |
| 0x11 | 0x03 | 内部 PCLK 分频,配合输入时钟调整帧率 |
| 0x3d | 0x83 | 输出时序相关,PCLK 极性设置 |
| 0x17 | 0x13 | HSTART 等,窗口位置 |
| 0x18 | 0x01 | HSTOP |
| 0x32 | 0xB6 | HREF 相关 |
| 0x19 | 0x02 | VSTART |
| 0x1A | 0x7A | VSTOP |
| 0x03 | 0x04 | VREF 等 |
| 0x0C | 0x00 | 关闭自动增益控制调整个数 |
| 0x3E | 0x00 | 关闭 PLL 倍频,直接用外部时钟 |
这里最容易出问题的是 0x3d 寄存器的 PCLK 极性,不同模块(不同批次)实际输出的沿可能不同。如果采集到的图像出现错位、颜色花屏,先试着把这个寄存器的值改成 0x83 或 0x82 翻转极性,多数情况下能解决。另一个重点是 0x40 寄存器,只有设置为 0x10 才能输出 RGB565,否则输出的是 RGB444 或其他格式,图像颜色会完全不对。
窗口配置这块,QVGA 分辨率对应 320x240,实际有效区域要从 OV7670 的感光区中间截取。寄存器 0x17、0x18、0x19、0x1A 的组合决定窗口起始和结束位置,如果看到的图像有黑边或偏移,就是这几个寄存器没配好。网上推荐值多,但不同模块仍有差异,调试时可以通过串口回传整帧数据后用上位机软件查看效果,逐步微调。
3.3 HC-SR04 测距驱动的实现思路
HC-SR04 的使用非常简单:Trig 引脚拉高 10us 以上,模块自动发出 8 个 40kHz 脉冲,然后把 Echo 拉高,高电平持续时长就是超声波往返时间。距离计算公式是:
距离(cm) = Echo高电平时间(us) / 58这个 58 是怎么来的?声速在空气中约 340m/s,换算成 cm/us 是 0.034cm/us,超声波来回走了两倍距离,所以单程时间需要除以 2,因此距离 = 时间(us) * 0.034 / 2 ≈ 时间(us) / 58.8。所以如果测得高电平时间是 3000us,距离约等于 51.7cm。
我用的是 STM32 定时器输入捕获功能。初始化 TIM2 通道 1,捕获上升沿和下降沿,记录两次捕获值之差就是高电平时间。相比用外部中断加 Delay 轮询的死等方式,输入捕获不占用 CPU,测量精度还高得多。每次测量前,Trig 输出一段 15us 高电平,然后等待捕获完成标志,超时则返回一个超出量程的值(比如 999cm)。
3.4 DCMI 采集 RGB565 图像帧
STM32 的 DCMI 外设可以接收外部摄像头并行数据,工作在硬件同步模式下,由 VSYNC 和 HREF 信号标记帧与行的边界。配置时,几组关键的参数:
- 选择 8 位数据宽度,对应 OV7670 的 D0-D7。
- 设置 VSYNC 和 HREF 极性,这要与 OV7670 输出匹配,通常 VSYNC 低有效,HREF 高有效。
- 嵌入同步关闭,使用独立同步模式。
- 连续模式单帧采集,即收到一帧 VSYNC 后自动产生帧结束事件。
为了不让 CPU 在接收图像时被打断太多,我用 DMA 把 DCMI 数据直接搬运到内存缓冲区。缓冲区大小按一帧算:320x240x2 字节 = 153600 字节,大约 150KB。F407 的内部 SRAM 是 192KB,勉强够放一帧,但如果同时还想跑系统、放栈、放协议缓冲区,就会非常紧张。更稳妥的做法是分配在外部 SRAM 或 SDRAM 里。F429 带 SDRAM 控制器,外扩一块 8MB 的 SDRAM 就舒服多了,能缓存多帧数据,方便做连续抓拍。
DCMI 初始化代码片段(HAL 库风格):
DCMI_HandleTypeDef hdcmi; hdcmi.Instance = DCMI; hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; // 硬件同步 hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; // 像素时钟上升沿采样,如果花屏改下降沿 hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; // VSYNC低有效 hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_HIGH; // HREF高有效 hdcmi.Init.CaptureRate = DCMI_CR_ALL_FRAME; // 捕获全部帧 hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; // 8位数据线 hdcmi.Init.JPEGMode = DCMI_MODE_SNAPSHOT; // 快照模式,抓单帧 hdcmi.Init.ByteSelectMode = DCMI_BSM_ALL; hdcmi.Init.LineSelectMode = DCMI_LSM_ALL; hdcmi.Init.LineNumber = 240; // 一帧240行配置完 DCMI 后调用HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_SNAPSHOT, (uint32_t)frame_buffer, 320/2),注意最后一个参数是像素个数除以 2,因为 DMA 传输的是 32 位字,每个字包含两个像素(两个 16 位 RGB565)。这个细节如果搞错,缓冲区会溢出或者只收到一半数据。
4. 开发环境搭建与调试实战
4.1 STM32 开发环境选择
这里想结合目前比较常用的几种开发方式说下我的体验。官方主推的 STM32CubeIDE 集成了 CubeMX 配置工具、编译器和调试器,对新手最友好,生成工程时选 HAL 库就行。使用它的STM32CubeMX点点选选就能把时钟、GPIO、DCMI、定时器都配置好,省去查寄存器的大量时间。但要注意,CubeMX 生成的 DCMI 相关代码在某些芯片版本上有小坑,比如 HREF 极性配置和实际时序不一致,需要手动调整hdcmi.Init.HSPolarity。
很多人装好 STM32CubeIDE 后,下载程序时遇到“Error: no STM32 target found!”或者提示调试认证问题,这通常有几种原因:一是 ST-Link 驱动没装好,Windows 设备管理器里能看到未知设备;二是目标板供电不正常,VDD 没接好导致 SWD 无法握手;三是连接线太长或者杜邦线接触不良。解决办法是先在 STM32CubeProgrammer 里测试连接,确认能读到芯片 ID 再回 IDE 调试。还有一个很隐蔽的问题,如果代码里把 SWDIO/SWCLK 引脚复用成了普通 GPIO,下载一次后第二次就连接不上了,这时需要用 BOOT0 拉高进入系统存储器模式,用串口或 ST-Link 的“Under reset”方式擦除 Flash。
VSCode 配 EIDE 插件或者 PlatformIO 也是一种方式,适合喜欢 Vim、Remote 开发的人。EIDE 插件可以支持 SCONS 或 CMake 构建,也能调用 ST-Link 命令行工具烧录。我个人觉得如果你主要是单文件调试,CubeIDE 就够用;如果项目有大量外部库、要做版本管理,VSCode 的工作区体验更好些。
4.2 晶振电容计算与时钟树配置
系统时钟对摄像头采集稳定性的影响比很多人想象的大。OV7670 的 PCLK 输出是它与 STM32 DCMI 之间的同步信号,如果 STM32 系统时钟抖动太大或者 PCLK 采样点选得不对,图像就会出现横条纹。
晶振负载电容的计算公式是Cl = (C1 * C2) / (C1 + C2) + Cstray,其中 Cstray 是 PCB 走线和引脚寄生电容,一般在 3pF 到 7pF。假如选用 8MHz 晶振要求负载电容 18pF,C1=C2=20pF,则 (20*20)/(20+20)+4=14pF,偏低;改成 22pF 时计算得 15pF,也是偏低;实际配 27pF 才能接近 18pF。所以网上很多人直接套 20pF 实际是启动慢的隐患。如果你首件样品能正常启动但低温下起振失败,多半就是负载电容偏小或者 C0G 材质没选对。
在 CubeMX 里,时钟树我按照外部 8MHz、PLL 倍频至 168MHz 来配置。APB2 总线上的定时器时钟通常为 84MHz,DCMI 挂在 AHB1 上,时钟 168MHz。这没问题,关键是要保证 DCMI 的像素时钟 PCLK 不超过 DCMI 最大工作频率。OV7670 在 QVGA RGB565 输出时,PCLK 大约在 12MHz 到 24MHz,明显低于 DCMI 上限,所以时间裕量充足。
4.3 从初始化到采集的完整流程
这里给一个我在 main 函数里实现的状态机伪代码:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DCMI_Init(); MX_TIM2_Init(); // 定时器输入捕获 USART1_UART_Init(); // 串口调试 OV7670_Init(); // SCCB 写入寄存器 while (1) { uint16_t distance = HC_SR04_Measure(); if (distance < TRIGGER_DISTANCE && distance > 0) { DCMI_Snapshot(frame_buffer); // 简单滤波防止误触发 if (HC_SR04_Measure() < TRIGGER_DISTANCE) { USART_SendImage(frame_buffer, 320 * 240 * 2); HAL_Delay(2000); // 抓拍间隔,避免连续触发 } } HAL_Delay(100); } }有人可能会问为什么要二次确认测距结果。超声波模块偶尔会收到杂散回波,比如旁边墙壁反射、模块自身抖动,单次测量可能误判。连续两次确认能过滤掉大多数随机错误,代价是多花 60ms 左右,完全可接受。这个思路还可以扩展成一连 N 次都低于阈值再触发,适合要求更严的场景。
5. 常见问题与排查技巧实录
5.1 图像黑屏或全花屏
这是 OV7670 项目里最常见的现象,没有之一。排查顺序我建议这样来:
先确认 SCCB 是不是真的写进去了。用示波器探 SDA/SCL,或者写个读 ID 的函数,读 0x1A 寄存器(厂商 ID),正确应返回 0x76。如果读出来是 0xFF 或 0x00,说明 SCCB 时序或引脚接线有问题。很多模块的 SDA/SCL 需要上拉电阻,如果模块内部没有,要在外部加 4.7k 到 3.3V。
再检查 DCMI 的时序配置。黑屏通常意味着 DMA 没有收到数据或者收到了全 0,跳线、DCMI 使能、复位顺序都要看。花屏则多半是 PCLK 极性反了,或者数据线顺序接错。最好用逻辑分析仪抓一下 VSYNC、HREF、PCLK 三根线的波形,看帧同步和行同步是否正常。第一次调 DCMI 时,我直接跳过 HREF、VSYNC 引脚确认这一步,结果花了整整两天找数据线问题,实际上就是一组线序搞反了。
调试技巧上,可以初始配置后抓取单行数据输出,用串口传到上位机,检查一行数据的 RGB565 值是否符合预期。如果一行数据看起来有规律但颜色不对,大概率是 RGB 顺序反了,修改 0x40 寄存器里 RGB 顺序的位即可。
5.2 测距异常跳动
HC-SR04 测量值跳动的原因,八成是供电不稳或者 Trig 脉宽时间不够。模块内部发射电路需要瞬间电流,如果电源线细或者 5V 被其他外设拉低,回波幅度会波动,测出的距离自然不稳。建议用一个独立的 100uF 电解电容在 HC-SR04 的 VCC 和 GND 之间去耦,效果立竿见影。我的板子上这个电容是必焊的。
软件层面也要加滤波。简单的中值滤波或者滑动平均都可以,取 5 次测量排序取中间值,基本能滤掉单点毛刺。如果仍跳得厉害,检查一下 Echo 引脚是否和某个电机或继电器靠得太近,电磁干扰也会造成捕获时间异常。测量超时时返回一个特定错误码而不是 0,否则主循环会误认为目标近在咫尺,触发拍照。
5.3 连接 ST-Link 时提示“No STM32 Target Found”
好多人拿到板子第一件事就是下载,结果就卡在这一步。除了前面说的驱动问题之外,还有几种容易踩坑的场景:目标板由 ST-Link 供电时,如果板上有大电容或者其他耗电单元,ST-Link 的 3.3V 输出能力不足,握手失败,这时改用外部供电并共地;板子上电顺序不对时,也可能是 ST-Link 先识别到了非调试状态,可以在 STM32CubeProgrammer 的连接设置里改成“Connect under reset”,用硬件复位保证 CPU 停在起始位置再连接。
如果你的开发板用的是自制的最小系统板,SWD 接口特别要注意 SWDIO 和 SWCLK 上各接一个 10k 上拉电阻。很多人画的板子这俩脚直接连 ST-Link,省略了上拉,结果导致 ST-Link 在高速通信时电平不稳定,时好时坏。还有一个常见操作,程序里用了GPIO_PIN_6和GPIO_PIN_7做别的功能,却不小心把 SWDIO/SWCLK 复用了,这种问题只能通过 BOOT0 拉高进系统引导去擦除。
5.4 摄像头帧率上不去或采集卡顿
OV7670 在 QVGA RGB565 输出下,理论上帧率可以达到 30fps,但 STM32 通过 DCMI+DMA 抓一帧 150KB 数据再通过串口发送,速度会严重拖后腿。如果只是做“超声波触发拍照”,慢一点无所谓;但如果想做连续预览, 115200 波特率的串口肯定不够,至少用 921600 或者 USB 虚拟串口。
帧率不足还有一个隐藏原因:DCMI 的快照模式下,如果上一帧还没有传输完成,下一帧的 VSYNC 不会被响应,导致丢帧。解决办法是在帧完成中断里快速处理数据并重新启动新一轮 DMA。中断优先级要设置得比其他外设高,避免被串口中断阻塞错过 VSYNC。另外,DMA 传输完成回调里不要再做耗时操作,比如把数据转存到 SD 卡,要放到主循环里异步处理,否则中断持续时间太长,会影响后续帧采集。
6. 进阶改进思路
这个项目做完其实只是开始,往上还有不少可以玩的方向。比如把图像输出到 SD 卡做成一个真正的“触发式监控记录仪”;或者在上位机对 RGB565 数据进行简单处理,识别图像中特定颜色的物体位置;再比如给 STM32 挂一个 ESP8266,把超声波触发拍照的结果直接推到局域网,搞一个低成本的智能猫眼原型。
有人还问过能不能直接识别车牌,OV7670 的 30 万像素在光线好、距离合适(30cm 到 1m)的情况下确实能拍到车牌轮廓,但受限于分辨率和没有 ISP 处理,识别率不如专用摄像头。可以在 STM32 端对图像做二值化和边缘提取,减少上位机压力,再配合模板匹配,简单数字识别可以跑通。如果想追求更高的识别率,建议换 OV2640 或 OV5640,像素更高,但寄存器配置难度也上了一个台阶,后续想深入的朋友可以先从 OV7670 练手,把 DCMI 和图像缓存流程跑通,再迁移到更高分辨率模组上会轻松很多。
本文还有配套的精品资源,点击获取