文章目录
-
- 一、方案整体设计与原理说明
-
- 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麦克风阵列接线
| 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舵机接线
| 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摄像头接线
| 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)
| PB6 | SCL | I2C1时钟线(上拉4.7kΩ) |
| PB7 | SDA | I2C1数据线(上拉4.7kΩ) |
| 3.3V | VCC | OLED供电 |
| GND | GND | 共地 |
(5)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 环境准备
- 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[len–1–i] = temp + '0';
if(i == dot) num_buf[len–1–i–1] = '.';
}
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指令固件烧录
- 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以下卡 |
网硕互联帮助中心





评论前必须登录!
注册