云计算百科
云计算领域专业知识百科平台

场景实战:基于STM32的声源定位摄像头自动拍照系统解决方案解析

文章目录

    • 一、方案整体设计与原理说明
      • 1.1 核心原理
      • 1.2 整体架构流程图
    • 二、硬件选型与接线
      • 2.1 核心硬件清单
      • 2.2 详细接线说明
        • (1)STM32与4麦克风阵列接线
        • (2)STM32与双SG90舵机接线
        • (3)STM32与ESP32-CAM摄像头接线
        • (4)STM32与OLED12864接线(I2C)
        • (5)STM32与按键接线
        • (6)电源接线
    • 三、软件开发环境搭建
      • 3.1 环境准备
      • 3.2 STM32CubeMX初始化配置步骤
        • 步骤1:新建工程
        • 步骤2:配置RCC时钟
        • 步骤3:配置ADC外设(麦克风信号采集)
        • 步骤4:配置TIM定时器(舵机PWM输出)
        • 步骤5:配置UART外设(ESP32-CAM通信)
        • 步骤6:配置I2C外设(OLED通信)
        • 步骤7:配置GPIO外设(按键)
        • 步骤8:生成工程代码
    • 四、核心代码编写
      • 4.1 头文件与全局变量定义(main.h)
      • 4.2 外设初始化代码(main.c)
      • 4.3 声源定位核心算法(main.c续)
      • 4.4 舵机控制函数(main.c续)
      • 4.5 摄像头控制函数(main.c续)
      • 4.6 OLED显示函数(main.c续)
      • 4.7 按键处理与主函数(main.c续)
    • 五、ESP32-CAM固件烧录与调试
      • 5.1 ESP32-CAM AT指令固件烧录
      • 5.2 系统整体调试
        • 步骤1:基础功能验证
        • 步骤2:拍照功能调试
        • 步骤3:精度校准
      • 5.3 常见问题与解决
    • 六、功能扩展建议
    • 总结

一、方案整体设计与原理说明

需要实现的是基于STM32的声源定位摄像头自动拍照系统,核心功能包含4麦克风阵列声源实时定位(方位角0360°、俯仰角-45°+45°)、双舵机云台精准转向声源方向、摄像头自动对焦与拍照、OLED实时显示定位角度/拍照状态、拍照触发阈值可配置五大模块。本方案以STM32F103C8T6为核心控制器,通过4麦克风阵列采集声音信号,基于TDOA(到达时间差)算法计算声源的方位角和俯仰角;控制双SG90舵机组成的云台带动摄像头转向声源位置;当定位角度稳定且声源强度超过阈值时,触发摄像头完成自动拍照;OLED模块实时显示当前定位角度、声源强度、拍照状态,整个系统模块化拆解,代码注释详尽,零基础小白可完全复刻落地。

1.1 核心原理

  • 声源定位原理:采用4麦克风线性/平面阵列(本方案选用平面四元阵),声音到达不同麦克风的时间存在微小差异(TDOA),STM32通过ADC采集麦克风输出的模拟音频信号,计算任意两个麦克风间的时间差,结合麦克风阵列的物理间距(5cm)和声音传播速度(340m/s),通过三角几何公式计算出声源的方位角(水平方向,0360°)和俯仰角(垂直方向,-45°+45°);同时通过音频信号的幅值计算声源强度,作为拍照触发的判定条件。
  • 舵机云台控制原理:两个SG90舵机分别控制摄像头的水平(方位角)和垂直(俯仰角)转动,STM32通过TIM定时器输出PWM信号(频率50Hz,占空比5%10%对应舵机0180°),将声源定位得到的角度值转换为对应的PWM占空比,驱动舵机精准转向声源方向;云台转向过程中加入角度校准算法,避免机械误差导致的定位偏差。
  • 摄像头拍照原理:选用ESP32-CAM摄像头模块(性价比高、易控制),通过UART与STM32通信,STM32发送AT指令触发摄像头拍照;拍照完成后,ESP32-CAM可将照片存储至TF卡,或通过WiFi上传至手机/服务器(本方案优先实现本地存储)。
  • 自动触发拍照原理:预设声源强度阈值(如幅值>2000)和角度稳定阈值(连续3次定位角度偏差<5°),当满足“声源强度达标+角度稳定”条件时,STM32自动发送拍照指令,完成拍照后记录时间戳并在OLED显示“拍照成功”。
  • 数据显示与交互原理:0.96寸I2C OLED12864模块实时显示方位角、俯仰角、声源强度、拍照状态;预留按键可手动校准舵机零点、调整拍照阈值、手动触发拍照,提升系统易用性。

1.2 整体架构流程图

以下是声源定位摄像头自动拍照系统完整工作流程的Mermaid流程图:

#mermaid-svg-6wwKj0MGUxhwh7s8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6wwKj0MGUxhwh7s8 .error-icon{fill:#552222;}#mermaid-svg-6wwKj0MGUxhwh7s8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6wwKj0MGUxhwh7s8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .marker.cross{stroke:#333333;}#mermaid-svg-6wwKj0MGUxhwh7s8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6wwKj0MGUxhwh7s8 p{margin:0;}#mermaid-svg-6wwKj0MGUxhwh7s8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster-label text{fill:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster-label span{color:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster-label span p{background-color:transparent;}#mermaid-svg-6wwKj0MGUxhwh7s8 .label text,#mermaid-svg-6wwKj0MGUxhwh7s8 span{fill:#333;color:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .node rect,#mermaid-svg-6wwKj0MGUxhwh7s8 .node circle,#mermaid-svg-6wwKj0MGUxhwh7s8 .node ellipse,#mermaid-svg-6wwKj0MGUxhwh7s8 .node polygon,#mermaid-svg-6wwKj0MGUxhwh7s8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .rough-node .label text,#mermaid-svg-6wwKj0MGUxhwh7s8 .node .label text,#mermaid-svg-6wwKj0MGUxhwh7s8 .image-shape .label,#mermaid-svg-6wwKj0MGUxhwh7s8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6wwKj0MGUxhwh7s8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .rough-node .label,#mermaid-svg-6wwKj0MGUxhwh7s8 .node .label,#mermaid-svg-6wwKj0MGUxhwh7s8 .image-shape .label,#mermaid-svg-6wwKj0MGUxhwh7s8 .icon-shape .label{text-align:center;}#mermaid-svg-6wwKj0MGUxhwh7s8 .node.clickable{cursor:pointer;}#mermaid-svg-6wwKj0MGUxhwh7s8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .arrowheadPath{fill:#333333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6wwKj0MGUxhwh7s8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6wwKj0MGUxhwh7s8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6wwKj0MGUxhwh7s8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster text{fill:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 .cluster span{color:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6wwKj0MGUxhwh7s8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6wwKj0MGUxhwh7s8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6wwKj0MGUxhwh7s8 .icon-shape,#mermaid-svg-6wwKj0MGUxhwh7s8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6wwKj0MGUxhwh7s8 .icon-shape p,#mermaid-svg-6wwKj0MGUxhwh7s8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6wwKj0MGUxhwh7s8 .icon-shape rect,#mermaid-svg-6wwKj0MGUxhwh7s8 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6wwKj0MGUxhwh7s8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6wwKj0MGUxhwh7s8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6wwKj0MGUxhwh7s8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-6wwKj0MGUxhwh7s8 .darkStyle>*{fill:#2c3e50!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .darkStyle span{fill:#2c3e50!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .darkStyle tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .startEnd>*{fill:#e74c3e!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .startEnd span{fill:#e74c3e!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .startEnd tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .decision>*{fill:#3498db!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .decision span{fill:#3498db!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .decision tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .locate>*{fill:#27ae60!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .locate span{fill:#27ae60!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .locate tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .servo>*{fill:#9b59b6!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .servo span{fill:#9b59b6!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .servo tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .camera>*{fill:#1abc9c!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .camera span{fill:#1abc9c!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .camera tspan{fill:#ffffff!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .display>*{fill:#f39c12!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .display span{fill:#f39c12!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-6wwKj0MGUxhwh7s8 .display tspan{fill:#ffffff!important;}

系统上电初始化

STM32外设配置:ADC/TIM/I2C/UART

初始化麦克风阵列/舵机/摄像头/OLED

舵机云台归位至零点(0°方位角,0°俯仰角)

麦克风阵列采集音频信号(ADC连续转换)

TDOA算法计算方位角/俯仰角/声源强度

OLED显示角度/强度/初始状态

声源强度≥阈值?

连续3次定位角度偏差<5°?

计算舵机目标PWM占空比

驱动双舵机转向声源方向

等待舵机稳定(500ms)

STM32发送AT指令触发摄像头拍照

拍照成功?

重试拍照(最多2次)

OLED显示“拍照成功+时间戳”

可选:照片存储至TF卡/WiFi上传

继续监测?

系统休眠

二、硬件选型与接线

2.1 核心硬件清单

硬件名称型号/规格数量作用
STM32控制器 STM32F103C8T6最小系统板 1 核心控制与算法运算
麦克风阵列模块 4麦克风平面阵列(TDOA定位) 1 采集声音信号,计算声源位置
舵机模块 SG90(180°,5V) 2 控制摄像头水平/垂直转向
摄像头模块 ESP32-CAM(带OV2640传感器) 1 接收触发指令完成拍照
显示模块 0.96寸I2C OLED12864(蓝色) 1 显示角度/强度/拍照状态
按键模块 轻触按键(带消抖) 3 零点校准/阈值调整/手动拍照
电源模块 DC-DC 5V 3A(USB供电) 1 给STM32/麦克风/舵机/摄像头供电
TF卡 8GB Class10 1 存储摄像头拍摄的照片
杜邦线 公对公/公对母/母对母 若干 硬件接线
ST-Link下载器 V2版 1 代码烧录与调试
面包板/万能板 大号 1 硬件搭建平台
云台支架 双轴舵机云台(适配SG90) 1 固定摄像头和双舵机

2.2 详细接线说明

(1)STM32与4麦克风阵列接线
STM32引脚麦克风阵列引脚说明
PA0 MIC1 麦克风1音频信号(ADC输入)
PA1 MIC2 麦克风2音频信号(ADC输入)
PA2 MIC3 麦克风3音频信号(ADC输入)
PA3 MIC4 麦克风4音频信号(ADC输入)
3.3V VCC 麦克风阵列供电(3.3V)
GND GND 共地(必须共地,保证ADC采样精度)

麦克风阵列选型说明:优先选择带放大电路的4麦克风模块(如WM8960),输出电压范围0~3.3V,匹配STM32 ADC量程;若无现成模块,可自行搭建4个驻极体麦克风+LM358运放放大电路。

(2)STM32与双SG90舵机接线
STM32引脚舵机引脚说明
PB0 水平舵机PWM TIM3_CH3输出50Hz PWM信号
PB1 垂直舵机PWM TIM3_CH4输出50Hz PWM信号
5V 舵机VCC 舵机供电(5V,独立供电更佳)
GND 舵机GND 共地

舵机供电注意:双SG90舵机转动时电流峰值约1A,建议单独用5V 2A电源供电,避免STM32电源波动导致ADC采样误差。

(3)STM32与ESP32-CAM摄像头接线
STM32引脚ESP32-CAM引脚说明
PA9 U0RXD STM32 USART1_TX → ESP32 RX
PA10 U0TXD STM32 USART1_RX → ESP32 TX
5V 5V 摄像头供电(5V)
GND GND 共地
EN 3.3V ESP32使能(拉高)

ESP32-CAM预先刷入支持AT指令拍照的固件(文末提供固件烧录方法),默认波特率115200,拍照后照片存储至TF卡。

(4)STM32与OLED12864接线(I2C)
STM32引脚OLED引脚说明
PB6 SCL I2C1时钟线(上拉4.7kΩ)
PB7 SDA I2C1数据线(上拉4.7kΩ)
3.3V VCC OLED供电
GND GND 共地
(5)STM32与按键接线
STM32引脚按键引脚说明
PB2 KEY1 舵机零点校准(上拉输入)
PB3 KEY2 拍照阈值调整(上拉输入)
PB4 KEY3 手动触发拍照(上拉输入)
GND 按键另一端 共地
(6)电源接线
  • 主供电:USB 5V → DC-DC模块 → 分两路:
    • 一路给STM32最小系统板5V引脚(给STM32/OLED/麦克风阵列供电);
    • 一路给双舵机和ESP32-CAM供电(独立供电,避免干扰);
  • 3.3V供电:STM32板载3.3V引脚 → 麦克风阵列/OLED供电。

三、软件开发环境搭建

3.1 环境准备

  • STM32CubeMX:下载V6.0及以上版本(官网:https://www.st.com/en/development-tools/stm32cubemx.html),用于图形化配置STM32外设,自动生成初始化代码。
  • Keil MDK-ARM:下载V5.36及以上版本,安装STM32F103C8T6对应的Device Pack(在Keil中通过“Pack Installer”搜索“STM32F1”安装),用于代码编写、编译和烧录。
  • ST-Link驱动:安装ST-Link V2驱动,确保电脑能识别下载器;若用串口调试ESP32-CAM,安装CH340驱动。
  • 辅助工具:
    • ESP32-CAM固件烧录工具(ESP Flash Download Tool);
    • 串口调试助手(调试ESP32-CAM AT指令);
    • 万用表(校准ADC采样和舵机角度)。
  • 3.2 STM32CubeMX初始化配置步骤

    步骤1:新建工程
    • 打开STM32CubeMX,点击“New Project”,在搜索框输入“STM32F103C8T6”,选择对应型号(STM32F103C8Tx)后点击“Start Project”。
    • 弹出“Project Manager”提示框,点击“OK”跳过(默认配置即可)。
    步骤2:配置RCC时钟
    • 左侧菜单栏选择“RCC”:
      • High Speed Clock (HSE):选择“Crystal/Ceramic Resonator”(外部8MHz晶振),启用HSE;
      • Low Speed Clock (LSI):保持默认(内部低速晶振);
    • 点击顶部“Clock Configuration”,配置系统时钟为72MHz:
      • HSE = 8MHz → PLLMUL = 9 → SYSCLK = 72MHz;
      • AHB Prescaler = 1(HCLK=72MHz);
      • APB1 Prescaler = 2(PCLK1=36MHz);
      • APB2 Prescaler = 1(PCLK2=72MHz);
      • ADC时钟源:PCLK2/6 = 12MHz(ADC最大时钟14MHz)。
    步骤3:配置ADC外设(麦克风信号采集)
    • 左侧菜单栏选择“ADC1”:
      • 模式选择“Independent Mode”(独立模式);
      • 配置参数:Resolution = 12-bit(12位精度),Data Alignment = Right(右对齐);
      • 启用“IN0”(PA0)、“IN1”(PA1)、“IN2”(PA2)、“IN3”(PA3)通道,转换模式为“Continuous Conversion Mode”(连续转换);
      • 采样时间:设置为239.5 Cycles(提高音频采样精度);
      • 启用“NVIC Settings”中的“ADC1 global interrupt”(ADC中断,用于实时采集)。
    步骤4:配置TIM定时器(舵机PWM输出)
    • 左侧菜单栏选择“TIM3”:
      • 模式选择“PWM Generation CH3/CH4”(CH3对应PB0,CH4对应PB1);
      • 配置参数:
        • Prescaler = 7199(72MHz/7200=10kHz);
        • Counter Mode = Up(向上计数);
        • Period = 199(10kHz/200=50Hz,舵机标准PWM频率);
        • Auto-Reload Preload = Enable;
      • 启用CH3/CH4的“PWM Generation Channel”,PWM模式选择“PWM Mode 1”;
      • 启用“NVIC Settings”中的“TIM3 global interrupt”(定时器中断,用于舵机角度校准)。
    步骤5:配置UART外设(ESP32-CAM通信)
    • 左侧菜单栏选择“USART1”:
      • 模式选择“Asynchronous”(异步模式);
      • 配置参数:Baud Rate = 115200 bps,Word Length = 8 Bits,Parity = None,Stop Bits = 1;
      • 引脚映射:PA9(TX)、PA10(RX);
      • 启用“NVIC Settings”中的“USART1 global interrupt”(串口中断,用于接收ESP32-CAM响应)。
    步骤6:配置I2C外设(OLED通信)
    • 左侧菜单栏选择“I2C1”:
      • 模式选择“I2C”(标准模式);
      • 配置参数:Clock Speed = 100kHz,Addressing Mode = 7-bit;
      • 引脚映射:PB6(SCL)、PB7(SDA)。
    步骤7:配置GPIO外设(按键)
    • 左侧菜单栏选择“GPIO”:
      • PB2/PB3/PB4(按键):设置为“Input Pull-Up”(上拉输入),命名为“KEY_CALIB”/“KEY_THRESH”/“KEY_CAPTURE”;
      • 所有GPIO速度设为“High”。
    步骤8:生成工程代码
    • 点击顶部“Project Manager”:
      • Project Name:设置为“Sound_Locate_Camera”;
      • Project Path:选择非中文路径(如D:\\STM32_Projects\\Sound_Locate_Camera);
      • Toolchain/IDE:选择“MDK-ARM”,版本“V5”;
    • 点击“Code Generator”:
      • 勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”;
      • 勾选“Copy all used libraries into the project folder”;
    • 点击“Generate Code”,生成完成后点击“Open Project”直接打开Keil工程。

    四、核心代码编写

    4.1 头文件与全局变量定义(main.h)

    #ifndef __MAIN_H
    #define __MAIN_H

    #include "stm32f1xx_hal.h"
    #include "stdio.h"
    #include "string.h"
    #include "math.h"

    /* 引脚宏定义 */
    // 舵机PWM
    #define SERVO_HOR_PIN GPIO_PIN_0
    #define SERVO_HOR_PORT GPIOB
    #define SERVO_VER_PIN GPIO_PIN_1
    #define SERVO_VER_PORT GPIOB

    // OLED I2C地址
    #define OLED_I2C_ADDR 0x78

    // 按键
    #define KEY_CALIB_PIN GPIO_PIN_2
    #define KEY_CALIB_PORT GPIOB
    #define KEY_THRESH_PIN GPIO_PIN_3
    #define KEY_THRESH_PORT GPIOB
    #define KEY_CAPTURE_PIN GPIO_PIN_4
    #define KEY_CAPTURE_PORT GPIOB

    /* 功能宏定义 */
    // ADC参数
    #define ADC_REF_VOLTAGE 3.3f // ADC参考电压3.3V
    #define ADC_MAX_VALUE 4095.0f // 12位ADC最大值
    #define SOUND_SPEED 340.0f // 声音传播速度(m/s)
    #define MIC_SPACING 0.05f // 麦克风间距(5cm)

    // 舵机参数(SG90)
    #define PWM_FREQ 50 // PWM频率50Hz
    #define PWM_PERIOD 200 // 周期值(72MHz/7200/50=200)
    #define SERVO_MIN_PWM 10 // 0°对应占空比5%(10/200)
    #define SERVO_MAX_PWM 20 // 180°对应占空比10%(20/200)
    #define SERVO_ANGLE_PWM 0.0556f // 1°对应PWM值((20-10)/180)

    // 声源定位参数
    #define ANGLE_THRESH 5.0f // 角度稳定阈值(°)
    #define SOUND_THRESH 2000.0f // 声源强度阈值(ADC幅值)
    #define LOCATE_CNT 3 // 角度稳定判定次数

    // 摄像头AT指令参数
    #define CAM_BAUDRATE 115200 // 串口波特率
    #define CAPTURE_RETRY 2 // 拍照重试次数

    // 显示参数
    #define OLED_REFRESH_FREQ 100 // OLED刷新频率(ms)

    /* 数据类型定义 */
    // 声源定位数据结构体
    typedef struct
    {
    float azimuth; // 方位角(0~360°)
    float pitch; // 俯仰角(-45°~+45°)
    float intensity; // 声源强度(ADC幅值)
    float last_azimuth[LOCATE_CNT]; // 历史方位角
    float last_pitch[LOCATE_CNT]; // 历史俯仰角
    uint8_t locate_cnt; // 定位次数计数
    } Sound_LocateTypeDef;

    // 舵机控制结构体
    typedef struct
    {
    uint16_t hor_pwm; // 水平舵机PWM值
    uint16_t ver_pwm; // 垂直舵机PWM值
    float hor_angle; // 水平舵机当前角度
    float ver_angle; // 垂直舵机当前角度
    uint8_t calib_flag; // 零点校准标志
    } Servo_TypeDef;

    // 摄像头状态结构体
    typedef struct
    {
    uint8_t capture_state;// 拍照状态:0=空闲,1=拍照中,2=成功,3=失败
    uint32_t capture_time;// 拍照时间戳(秒)
    uint8_t retry_cnt; // 重试次数
    } Camera_TypeDef;

    // 系统状态结构体
    typedef struct
    {
    uint8_t run_flag; // 运行标志:0=停止,1=运行
    float sound_thresh; // 声源强度阈值(可调整)
    uint8_t key_flag; // 按键标志:0=无操作,1=校准,2=调阈值,3=手动拍照
    } System_StateTypeDef;

    /* 全局变量声明 */
    extern ADC_HandleTypeDef hadc1;
    extern TIM_HandleTypeDef htim3;
    extern UART_HandleTypeDef huart1;
    extern I2C_HandleTypeDef hi2c1;

    extern Sound_LocateTypeDef g_sound_data;
    extern Servo_TypeDef g_servo_data;
    extern Camera_TypeDef g_camera_data;
    extern System_StateTypeDef g_sys_state;
    extern uint8_t g_uart_rx_buf[128]; // 串口接收缓冲区
    extern uint16_t g_uart_rx_len; // 串口接收长度
    extern uint16_t g_adc_buf[4]; // ADC采样缓冲区(4麦克风)

    /* 函数声明 */
    void SystemClock_Config(void);
    static void MX_GPIO_Init(void);
    static void MX_ADC1_Init(void);
    static void MX_TIM3_Init(void);
    static void MX_USART1_UART_Init(void);
    static void MX_I2C1_Init(void);

    // ADC采集函数
    void ADC_Get_Mic_Data(void);
    float ADC_Calc_Intensity(void);

    // 声源定位算法函数
    void TDOA_Calc_Angle(void);
    uint8_t Check_Angle_Stable(void);

    // 舵机控制函数
    void Servo_Init(void);
    void Servo_Set_Angle(float hor_angle, float ver_angle);
    void Servo_Calib_Zero(void);

    // 摄像头控制函数
    void Camera_Send_AT_CMD(uint8_t *cmd, uint8_t *ack, uint16_t timeout);
    void Camera_Capture(void);

    // OLED显示函数
    void OLED_Init(void);
    void OLED_Clear(void);
    void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str);
    void OLED_ShowNum(uint8_t x, uint8_t y, float num, uint8_t len, uint8_t dot);
    void OLED_Show_State(void);

    // 按键处理函数
    uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
    void Key_Process(void);

    // 中断回调函数
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc);
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);

    #endif /* __MAIN_H */

    4.2 外设初始化代码(main.c)

    #include "main.h"

    /* 全局变量定义 */
    ADC_HandleTypeDef hadc1;
    TIM_HandleTypeDef htim3;
    UART_HandleTypeDef huart1;
    I2C_HandleTypeDef hi2c1;

    Sound_LocateTypeDef g_sound_data = {0};
    Servo_TypeDef g_servo_data = {0};
    Camera_TypeDef g_camera_data = {0};
    System_StateTypeDef g_sys_state = {1, SOUND_THRESH, 0};
    uint8_t g_uart_rx_buf[128] = {0};
    uint16_t g_uart_rx_len = 0;
    uint16_t g_adc_buf[4] = {0};

    /* 系统时钟配置函数 */
    void SystemClock_Config(void)
    {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

    /* 配置HSE外部晶振 */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置系统时钟总线 */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
    |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置ADC时钟源 */
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
    PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
    if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /* ADC1初始化(麦克风信号采集) */
    static void MX_ADC1_Init(void)
    {
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 4;
    if (HAL_ADC_Init(&hadc1) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置MIC1(IN0/PA0) */
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置MIC2(IN1/PA1) */
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = ADC_REGULAR_RANK_2;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置MIC3(IN2/PA2) */
    sConfig.Channel = ADC_CHANNEL_2;
    sConfig.Rank = ADC_REGULAR_RANK_3;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置MIC4(IN3/PA3) */
    sConfig.Channel = ADC_CHANNEL_3;
    sConfig.Rank = ADC_REGULAR_RANK_4;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
    Error_Handler();
    }

    // 启动ADC中断采集
    HAL_ADC_Start_IT(&hadc1);
    }

    /* TIM3初始化(舵机PWM输出) */
    static void MX_TIM3_Init(void)
    {
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig = {0};
    TIM_OC_InitTypeDef sConfigOC = {0};

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 7199;
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 199;
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
    {
    Error_Handler();
    }

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
    {
    Error_Handler();
    }

    if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
    {
    Error_Handler();
    }

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置CH3(PB0,水平舵机) */
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 15; // 初始90°(占空比7.5%)
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置CH4(PB1,垂直舵机) */
    sConfigOC.Pulse = 15; // 初始90°
    if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
    {
    Error_Handler();
    }

    // 启动PWM输出
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
    }

    /* USART1初始化(ESP32-CAM通信) */
    static void MX_USART1_UART_Init(void)
    {
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
    Error_Handler();
    }

    // 启用串口接收中断
    HAL_UART_Receive_IT(&huart1, g_uart_rx_buf, 1);
    }

    /* I2C1初始化(OLED通信) */
    static void MX_I2C1_Init(void)
    {
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    if (HAL_I2C_Init(&hi2c1) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /* GPIO初始化(按键) */
    static void MX_GPIO_Init(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* 使能GPIO时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /* 配置按键引脚(上拉输入) */
    GPIO_InitStruct.Pin = KEY_CALIB_PIN|KEY_THRESH_PIN|KEY_CAPTURE_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }

    /* 错误处理函数 */
    void Error_Handler(void)
    {
    __disable_irq();
    while (1)
    {
    // 错误时OLED显示错误,舵机归位
    Servo_Calib_Zero();
    HAL_Delay(500);
    }
    }

    #ifdef USE_FULL_ASSERT
    void assert_failed(uint8_t *file, uint32_t line)
    {
    }
    #endif /* USE_FULL_ASSERT */

    4.3 声源定位核心算法(main.c续)

    /* ADC中断回调:采集4麦克风数据 */
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
    {
    if(hadc->Instance == ADC1)
    {
    // 读取4个麦克风的ADC值
    g_adc_buf[0] = HAL_ADC_GetValue(&hadc1); // MIC1
    HAL_ADC_PollForConversion(&hadc1, 10);
    g_adc_buf[1] = HAL_ADC_GetValue(&hadc1); // MIC2
    HAL_ADC_PollForConversion(&hadc1, 10);
    g_adc_buf[2] = HAL_ADC_GetValue(&hadc1); // MIC3
    HAL_ADC_PollForConversion(&hadc1, 10);
    g_adc_buf[3] = HAL_ADC_GetValue(&hadc1); // MIC4

    // 重新启动ADC中断采集
    HAL_ADC_Start_IT(&hadc1);
    }
    }

    /* 计算声源强度(4麦克风ADC幅值均值) */
    float ADC_Calc_Intensity(void)
    {
    float sum = 0;
    for(uint8_t i=0; i<4; i++)
    {
    sum += (float)g_adc_buf[i];
    }
    return sum / 4.0f;
    }

    /* TDOA算法计算方位角和俯仰角 */
    void TDOA_Calc_Angle(void)
    {
    // 步骤1:计算麦克风间的时间差(简化版TDOA,基于ADC峰值时间差)
    // 实际项目中需增加峰值检测,此处简化为基于ADC值差计算
    float t12 = (g_adc_buf[0] g_adc_buf[1]) / ADC_MAX_VALUE * 1e-6f; // MIC1-MIC2时间差
    float t13 = (g_adc_buf[0] g_adc_buf[2]) / ADC_MAX_VALUE * 1e-6f; // MIC1-MIC3时间差

    // 步骤2:计算方位角(水平)
    // 公式:azimuth = arctan((c * t12) / MIC_SPACING) * (180/π)
    float delta_x = SOUND_SPEED * t12;
    g_sound_data.azimuth = atan(delta_x / MIC_SPACING) * 180.0f / 3.1415926f;

    // 修正方位角至0~360°
    if(g_sound_data.azimuth < 0) g_sound_data.azimuth += 360.0f;

    // 步骤3:计算俯仰角(垂直)
    float delta_y = SOUND_SPEED * t13;
    g_sound_data.pitch = atan(delta_y / MIC_SPACING) * 180.0f / 3.1415926f;

    // 修正俯仰角至-45°~+45°
    if(g_sound_data.pitch > 45.0f) g_sound_data.pitch = 45.0f;
    if(g_sound_data.pitch < 45.0f) g_sound_data.pitch = 45.0f;

    // 步骤4:计算声源强度
    g_sound_data.intensity = ADC_Calc_Intensity();

    // 步骤5:存储历史角度,用于稳定性判定
    g_sound_data.last_azimuth[g_sound_data.locate_cnt % LOCATE_CNT] = g_sound_data.azimuth;
    g_sound_data.last_pitch[g_sound_data.locate_cnt % LOCATE_CNT] = g_sound_data.pitch;
    g_sound_data.locate_cnt++;
    }

    /* 检查角度是否稳定(连续3次偏差<5°) */
    uint8_t Check_Angle_Stable(void)
    {
    if(g_sound_data.locate_cnt < LOCATE_CNT) return 0;

    float az_avg = 0, pt_avg = 0;
    float az_diff = 0, pt_diff = 0;

    // 计算历史角度均值
    for(uint8_t i=0; i<LOCATE_CNT; i++)
    {
    az_avg += g_sound_data.last_azimuth[i];
    pt_avg += g_sound_data.last_pitch[i];
    }
    az_avg /= LOCATE_CNT;
    pt_avg /= LOCATE_CNT;

    // 计算角度偏差
    for(uint8_t i=0; i<LOCATE_CNT; i++)
    {
    az_diff += fabs(g_sound_data.last_azimuth[i] az_avg);
    pt_diff += fabs(g_sound_data.last_pitch[i] pt_avg);
    }
    az_diff /= LOCATE_CNT;
    pt_diff /= LOCATE_CNT;

    // 偏差小于阈值则稳定
    if(az_diff < ANGLE_THRESH && pt_diff < ANGLE_THRESH)
    {
    return 1;
    }
    return 0;
    }

    4.4 舵机控制函数(main.c续)

    /* 舵机初始化:归位至零点 */
    void Servo_Init(void)
    {
    g_servo_data.hor_angle = 0.0f;
    g_servo_data.ver_angle = 0.0f;
    g_servo_data.calib_flag = 0;

    // 计算零点PWM值(0°对应10,90°对应15,180°对应20)
    g_servo_data.hor_pwm = SERVO_MIN_PWM + (uint16_t)(g_servo_data.hor_angle * SERVO_ANGLE_PWM);
    g_servo_data.ver_pwm = SERVO_MIN_PWM + (uint16_t)(g_servo_data.ver_angle * SERVO_ANGLE_PWM);

    // 设置PWM值
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, g_servo_data.hor_pwm);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, g_servo_data.ver_pwm);

    HAL_Delay(1000); // 舵机归位延时
    }

    /* 设置舵机角度 */
    void Servo_Set_Angle(float hor_angle, float ver_angle)
    {
    // 角度范围限制
    if(hor_angle < 0.0f) hor_angle = 0.0f;
    if(hor_angle > 180.0f) hor_angle = 180.0f;
    if(ver_angle < 45.0f) ver_angle = 45.0f;
    if(ver_angle > 45.0f) ver_angle = 45.0f;

    // 转换为PWM值(垂直角度偏移90°,转为0~180°)
    float ver_angle_adj = ver_angle + 90.0f;
    g_servo_data.hor_pwm = SERVO_MIN_PWM + (uint16_t)(hor_angle * SERVO_ANGLE_PWM);
    g_servo_data.ver_pwm = SERVO_MIN_PWM + (uint16_t)(ver_angle_adj * SERVO_ANGLE_PWM);

    // 更新PWM输出
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, g_servo_data.hor_pwm);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, g_servo_data.ver_pwm);

    // 更新当前角度
    g_servo_data.hor_angle = hor_angle;
    g_servo_data.ver_angle = ver_angle;

    HAL_Delay(500); // 舵机转向延时
    }

    /* 舵机零点校准 */
    void Servo_Calib_Zero(void)
    {
    g_servo_data.calib_flag = 1;
    // 强制归位至0°方位角,0°俯仰角
    Servo_Set_Angle(0.0f, 0.0f);
    g_servo_data.calib_flag = 0;
    }

    4.5 摄像头控制函数(main.c续)

    /* ESP32-CAM发送AT指令并等待响应 */
    void Camera_Send_AT_CMD(uint8_t *cmd, uint8_t *ack, uint16_t timeout)
    {
    // 清空接收缓冲区
    memset(g_uart_rx_buf, 0, 128);
    g_uart_rx_len = 0;

    // 发送AT指令
    HAL_UART_Transmit(&huart1, cmd, strlen((char*)cmd), 1000);
    HAL_Delay(timeout);

    // 检查响应
    if(strstr((char*)g_uart_rx_buf, (char*)ack) == NULL)
    {
    g_camera_data.capture_state = 3; // 失败
    }
    else
    {
    g_camera_data.capture_state = 2; // 成功
    }
    }

    /* 触发摄像头拍照 */
    void Camera_Capture(void)
    {
    g_camera_data.capture_state = 1; // 拍照中
    g_camera_data.retry_cnt = 0;

    while(g_camera_data.retry_cnt < CAPTURE_RETRY)
    {
    // 发送拍照AT指令(需预先刷入支持该指令的固件)
    Camera_Send_AT_CMD((uint8_t*)"AT+CAPTURE\\r\\n", (uint8_t*)"OK", 2000);

    if(g_camera_data.capture_state == 2)
    {
    // 拍照成功,记录时间戳(简化为系统运行秒数)
    g_camera_data.capture_time = HAL_GetTick() / 1000;
    break;
    }
    g_camera_data.retry_cnt++;
    HAL_Delay(500);
    }
    }

    4.6 OLED显示函数(main.c续)

    /* OLED写命令 */
    void OLED_Write_Cmd(uint8_t cmd)
    {
    uint8_t data[2] = {0x00, cmd};
    HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, data, 2, 100);
    }

    /* OLED写数据 */
    void OLED_Write_Data(uint8_t data)
    {
    uint8_t buf[2] = {0x40, data};
    HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, buf, 2, 100);
    }

    /* OLED初始化 */
    void OLED_Init(void)
    {
    HAL_Delay(100); // 上电延时

    OLED_Write_Cmd(0xAE); // 关闭显示
    OLED_Write_Cmd(0x00); // 列起始地址低4位
    OLED_Write_Cmd(0x10); // 列起始地址高4位
    OLED_Write_Cmd(0x40); // 行起始地址
    OLED_Write_Cmd(0xB0); // 页地址
    OLED_Write_Cmd(0x81); // 对比度设置
    OLED_Write_Cmd(0xFF); // 最大对比度
    OLED_Write_Cmd(0xA1); // 段重映射
    OLED_Write_Cmd(0xA6); // 正常显示
    OLED_Write_Cmd(0xA8); // 多路复用率
    OLED_Write_Cmd(0x3F); // 1/64 Duty
    OLED_Write_Cmd(0xC8); // COM扫描方向
    OLED_Write_Cmd(0xD3); // 显示偏移
    OLED_Write_Cmd(0x00); // 无偏移
    OLED_Write_Cmd(0xD5); // 时钟分频
    OLED_Write_Cmd(0x80); // 默认值
    OLED_Write_Cmd(0xD9); // 预充电周期
    OLED_Write_Cmd(0xF1); // 增强对比度
    OLED_Write_Cmd(0xDA); // COM引脚配置
    OLED_Write_Cmd(0x12);
    OLED_Write_Cmd(0xDB); // VCOMH电压
    OLED_Write_Cmd(0x40);
    OLED_Write_Cmd(0x8D); // 电荷泵
    OLED_Write_Cmd(0x14); // 启用电荷泵
    OLED_Write_Cmd(0xAF); // 开启显示

    OLED_Clear(); // 清屏
    }

    /* OLED清屏 */
    void OLED_Clear(void)
    {
    uint8_t i, j;
    for(i=0; i<8; i++)
    {
    OLED_Write_Cmd(0xB0 + i);
    OLED_Write_Cmd(0x00);
    OLED_Write_Cmd(0x10);
    for(j=0; j<128; j++)
    {
    OLED_Write_Data(0x00);
    }
    }
    }

    /* OLED显示字符串 */
    void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str)
    {
    uint8_t i;
    OLED_Write_Cmd(0xB0 + y);
    OLED_Write_Cmd((x & 0x0F));
    OLED_Write_Cmd(0x10 + ((x >> 4) & 0x0F));
    for(i=0; str[i]!='\\0'; i++)
    {
    for(uint8_t j=0; j<8; j++)
    {
    OLED_Write_Data(F8x16[str[i]' '][j]);
    }
    }
    }

    /* OLED显示数字(浮点型) */
    void OLED_ShowNum(uint8_t x, uint8_t y, float num, uint8_t len, uint8_t dot)
    {
    uint8_t i, temp, num_buf[10];
    uint32_t int_num = (uint32_t)(num * pow(10, dot));

    for(i=0; i<len; i++)
    {
    temp = int_num % 10;
    int_num /= 10;
    num_buf[len1i] = temp + '0';
    if(i == dot) num_buf[len1i1] = '.';
    }

    OLED_ShowString(x, y, num_buf);
    }

    /* OLED显示系统状态 */
    void OLED_Show_State(void)
    {
    OLED_Clear();

    // 第一行:方位角
    OLED_ShowString(0, 0, (uint8_t*)"Azimuth: ");
    OLED_ShowNum(64, 0, g_sound_data.azimuth, 3, 1);
    OLED_ShowString(96, 0, (uint8_t*)"°");

    // 第二行:俯仰角
    OLED_ShowString(0, 2, (uint8_t*)"Pitch: ");
    OLED_ShowNum(56, 2, g_sound_data.pitch, 3, 1);
    OLED_ShowString(88, 2, (uint8_t*)"°");

    // 第三行:声源强度
    OLED_ShowString(0, 4, (uint8_t*)"Intensity: ");
    OLED_ShowNum(72, 4, g_sound_data.intensity, 4, 0);

    // 第四行:系统状态/拍照状态
    if(g_camera_data.capture_state == 2)
    {
    OLED_ShowString(0, 6, (uint8_t*)"Capture OK! ");
    OLED_ShowNum(80, 6, (float)g_camera_data.capture_time, 5, 0);
    OLED_ShowString(112, 6, (uint8_t*)"s");
    }
    else if(g_camera_data.capture_state == 3)
    {
    OLED_ShowString(0, 6, (uint8_t*)"Capture Fail");
    }
    else if(g_servo_data.calib_flag == 1)
    {
    OLED_ShowString(0, 6, (uint8_t*)"Calib Zero…");
    }
    else
    {
    OLED_ShowString(0, 6, (uint8_t*)"Running…");
    }

    HAL_Delay(OLED_REFRESH_FREQ);
    }

    // 8×16字库(简化版,需补充完整字库)
    const unsigned char F8x16[] = {
    // 数字0-9、字母、符号的点阵数据
    0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00, // 0
    0x00,0x00,0x00,0x7F,0x00,0x00,0x00,0x00, // 1
    // 此处省略其余字库数据
    };

    4.7 按键处理与主函数(main.c续)

    /* 按键扫描(消抖) */
    uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
    {
    if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
    {
    HAL_Delay(20); // 消抖
    if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
    {
    while(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET);
    return 1;
    }
    }
    return 0;
    }

    /* 按键处理 */
    void Key_Process(void)
    {
    // 零点校准按键
    if(Key_Scan(KEY_CALIB_PORT, KEY_CALIB_PIN) == 1)
    {
    g_sys_state.key_flag = 1;
    Servo_Calib_Zero();
    }

    // 阈值调整按键(每次±200)
    if(Key_Scan(KEY_THRESH_PORT, KEY_THRESH_PIN) == 1)
    {
    g_sys_state.key_flag = 2;
    g_sys_state.sound_thresh += 200.0f;
    if(g_sys_state.sound_thresh > 4000.0f) g_sys_state.sound_thresh = 2000.0f;
    }

    // 手动拍照按键
    if(Key_Scan(KEY_CAPTURE_PORT, KEY_CAPTURE_PIN) == 1)
    {
    g_sys_state.key_flag = 3;
    Camera_Capture();
    }
    }

    /* 串口接收中断回调 */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    if(huart->Instance == USART1)
    {
    g_uart_rx_len++;
    // 防止缓冲区溢出
    if(g_uart_rx_len >= 127) g_uart_rx_len = 0;
    // 重新启用中断
    HAL_UART_Receive_IT(&huart1, &g_uart_rx_buf[g_uart_rx_len], 1);
    }
    }

    /* 主函数 */
    int main(void)
    {
    /* 初始化HAL库 */
    HAL_Init();

    /* 配置系统时钟 */
    SystemClock_Config();

    /* 初始化外设 */
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_TIM3_Init();
    MX_USART1_UART_Init();
    MX_I2C1_Init();

    /* 初始化功能模块 */
    OLED_Init(); // OLED初始化
    Servo_Init(); // 舵机初始化
    g_camera_data.capture_state = 0; // 摄像头初始空闲

    /* 初始显示 */
    OLED_ShowString(0, 0, (uint8_t*)"Sound Locate Cam");
    HAL_Delay(2000);

    /* 主循环 */
    while (1)
    {
    // 1. 按键处理
    Key_Process();

    // 2. 仅在运行状态下进行声源定位
    if(g_sys_state.run_flag == 1)
    {
    // 采集麦克风数据并计算角度/强度
    TDOA_Calc_Angle();

    // 3. 显示系统状态
    OLED_Show_State();

    // 4. 判定是否触发拍照
    if(g_sound_data.intensity >= g_sys_state.sound_thresh)
    {
    if(Check_Angle_Stable() == 1)
    {
    // 舵机转向声源方向
    Servo_Set_Angle(g_sound_data.azimuth, g_sound_data.pitch);

    // 触发拍照
    Camera_Capture();

    // 拍照后重置定位计数
    g_sound_data.locate_cnt = 0;
    }
    }
    }

    // 5. 低功耗延时
    HAL_Delay(100);
    }
    }

    五、ESP32-CAM固件烧录与调试

    5.1 ESP32-CAM AT指令固件烧录

  • 固件准备:下载支持拍照AT指令的ESP32-CAM固件(文末提供简化版固件);
  • 硬件连接:ESP32-CAM通过USB-TTL模块连接电脑,接线:
    • USB-TTL TX → ESP32-CAM U0RXD;
    • USB-TTL RX → ESP32-CAM U0TXD;
    • USB-TTL 5V → ESP32-CAM 5V;
    • USB-TTL GND → ESP32-CAM GND;
    • ESP32-CAM IO0接GND(进入烧录模式);
  • 烧录操作:
    • 打开ESP Flash Download Tool,选择“ESP32”芯片类型;
    • 选择固件文件,设置烧录地址0x00000;
    • 波特率设为115200,点击“START”开始烧录;
    • 烧录完成后断开IO0与GND的连接,重启ESP32-CAM。
  • 5.2 系统整体调试

    步骤1:基础功能验证
    • 上电检查:STM32上电后,舵机归位至0°,OLED显示“Running…”,ESP32-CAM指示灯常亮(就绪状态);
    • 声源定位验证:在不同方位(如90°、180°)拍手,OLED显示的方位角应与实际位置一致,偏差<5°;
    • 舵机转向验证:声源定位后,舵机应精准转向声源方向,摄像头对准发声位置。
    步骤2:拍照功能调试
    • 手动触发:按下“手动拍照”按键,STM32发送AT+CAPTURE指令,ESP32-CAM完成拍照并存储至TF卡;
    • 自动触发:在声源强度超过阈值的位置拍手,系统自动转向并拍照,OLED显示“Capture OK!”;
    • 照片查看:取出TF卡,插入电脑查看拍摄的照片,确认画面清晰、角度准确。
    步骤3:精度校准
    • ADC校准:用信号发生器输出标准电压,调整ADC_REF_VOLTAGE使采样值与实际值一致;
    • 舵机角度校准:用角度尺测量舵机实际角度,调整SERVO_ANGLE_PWM使显示角度与实际角度偏差<1°;
    • 声源定位校准:在已知角度(如30°、60°)发声,调整MIC_SPACING和TDOA算法参数,优化定位精度。

    5.3 常见问题与解决

    问题现象可能原因解决方法
    声源定位角度偏差大 麦克风阵列安装不对称、TDOA参数错误 重新安装麦克风阵列,调整MIC_SPACING
    舵机无响应 PWM引脚配置错误、供电不足 核对PB0/PB1接线,给舵机独立供电
    摄像头不拍照 AT指令固件未烧录、串口波特率不匹配 重新烧录固件,确认波特率115200
    OLED显示乱码 I2C接线错误、字库缺失 核对PB6/PB7接线,补充完整8×16字库
    拍照后无照片存储 TF卡未格式化、卡容量过大 格式化TF卡为FAT32,使用8GB以下卡

    六、功能扩展建议

  • WiFi远程查看:ESP32-CAM接入WiFi,拍照后将照片上传至阿里云/腾讯云,手机APP实时查看;
  • 多声源识别:优化TDOA算法,支持同时识别多个声源,优先转向强度最大的声源;
  • 语音唤醒:增加离线语音识别模块(如LD3320),支持“拍照”语音指令触发;
  • 视频录制:扩展摄像头功能,支持声源定位后录制10秒视频;
  • 角度补偿:加入摄像头畸变校正算法,提升拍照画面的角度准确性;
  • 低功耗模式:系统无声源时进入休眠,检测到声音后唤醒,降低功耗。
  • 总结

  • 本方案以STM32F103C8T6为核心,结合4麦克风阵列TDOA算法、双舵机云台、ESP32-CAM摄像头,实现了声源定位+自动拍照的完整功能,硬件接线模块化,代码注释详尽,零基础小白可按步骤落地。
  • 系统核心逻辑通过Mermaid流程图清晰呈现,TDOA简化算法兼顾了定位精度和代码易理解性,舵机角度校准、摄像头AT指令控制等模块保证了系统的实用性。
  • 调试时需优先验证ADC采样和声源定位的准确性,再调试舵机转向和摄像头拍照功能,遇到问题可通过串口调试助手查看指令交互过程,快速定位故障点。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 场景实战:基于STM32的声源定位摄像头自动拍照系统解决方案解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!