前言
本篇博文基于 STM32F103C8T6 最小系统板,搭配数字声音检测模块、I2C‑OLED 屏幕,实现声音触发检测、拍手识别、触发次数统计,并且把状态和计数实时显示在 OLED 上。 项目使用PA0 引脚外部中断 EXTI0捕获短促的声音脉冲,同时搭配 GPIO 轮询读取电平,解决单纯轮询容易丢失短脉冲的问题,适合 STM32 外部中断入门教学实验。
视频讲解:STM32驱动声音检测模块+OLED显示项目完整讲解(外部中断捕获拍手声音)
http://STM32驱动声音检测模块+OLED显示项目完整讲解(外部中断捕获拍手声音)
注意:本项目使用的是数字声音检测模块 DO/OUT 输出,不能测量分贝、声音频率,仅判断声音是否超过设定阈值。
一、项目实现功能与工作流程
演示目标
拍手、敲击等声音被麦克风采集,声音检测模块将声信号转为数字电平信号;STM32 捕获电平跳变,记录声音触发事件与累计触发次数,最终在 OLED 屏幕展示声音检测状态和计数。

整体工作流程:
通俗概括:声音检测模块把声音转换成数字电平,STM32 捕获电平变化,将声音状态和触发次数输出到 OLED 屏幕。
二、硬件接线说明
⚠️重点提醒:所有器件 GND 必须共地,否则会出现乱触发、检测失效问题!
| 声音模块 OUT | PA0 | 数字输出,接 GPIO 输入 + EXTI0 外部中断 |
| 声音模块 VCC | 5V | 声音检测模块供电 |
| 声音模块 GND | GND | 电源共地 |
| OLED SCL | PB8 | 软件 I2C 时钟线 |
| OLED SDA | PB9 | 软件 I2C 数据线 |
| OLED VCC | 3.3V | OLED 屏幕供电 |
| OLED GND | GND | OLED 电源地 |
硬件注意事项
- AO:模拟输出,输出随声音变化的模拟电压,需要 ADC 采集,可以获取声音强弱;
- DO/OUT:数字比较器输出,只有高低电平,仅代表声音是否超过阈值;
- 电位器:调节比较器参考阈值,也就是声音触发灵敏度。
本项目只用数字 OUT 引脚,只能判断 “有没有触发”,无法获取真实响度、分贝。
三、声音检测模块硬件原理
数字声音检测模块由麦克风、信号放大电路、电压比较器、灵敏度调节电位器四部分组成。
声音本质是空气压力波动,声波传到麦克风,麦克风振膜发生振动,生成微弱交流电信号;微弱信号送入板载放大电路放大,之后送入电压比较器。 电位器输出一个参考电压阈值,放大后的声音信号和参考阈值做比较。
本项目使用模块为低电平有效逻辑:
| 声音未超过阈值 | 高电平 1 | 安静状态 |
| 声音超过阈值 | 低电平 0 | 检测到声音 |
声音触发瞬间,OUT 会从高电平跳转到低电平,产生下降沿,STM32 依靠这个下降沿触发外部中断。
不同厂商模块有效电平可能相反!如果你的模块发声输出高电平,需要修改代码的判断逻辑和中断触发边沿。
四、STM32 程序检测原理:GPIO 轮询 + EXTI 外部中断
项目同时使用两种检测方式,取长补短。
1. GPIO 轮询读取电平
直接调用库函数读取 PA0 引脚当前电平,判断高低,识别声音状态。 但是轮询存在短板:主循环每 20ms 才读取一次引脚,如果声音产生的低电平脉冲持续时间小于 20ms,轮询就会直接漏掉这个声音事件。
2. EXTI0 外部中断捕获短脉冲
PA0 映射到 EXTI0 外部中断,配置为下降沿触发。 当 OUT 引脚高变低产生下降沿,硬件自动置位中断标志,CPU 暂停主循环,跳转到中断服务函数。
中断服务函数里面只做两件事:标记声音待处理事件、触发计数自增,清除中断挂起标志。 ❗重要原则:中断服务函数不做 OLED 屏幕刷新,OLED 刷新属于耗时操作,放在中断里面会拉长中断执行时间,影响系统实时性。
3. 为什么轮询和中断一起用
- 外部中断:专门捕捉非常短促的下降沿脉冲,防止短声音事件被漏掉;
- GPIO 轮询:读取引脚实时电平,可以识别持续时间比较长的声音触发。
两者结合,既可以捕捉短促拍手,也可以识别长时间持续触发状态。
4. 共享变量与临界区问题
中断服务函数会修改事件标志、触发计数,主循环也会读取这两个全局变量。 如果主循环读取一半,突然进入中断改写变量,会拿到错乱的数据。 解决方案:访问这两个共享变量的时候,短暂关闭全局中断,读取 / 修改完成再恢复中断。 同时变量加上volatile关键字,告诉编译器:变量会被中断异步修改,不要做编译器优化,每次都从内存读取真实值。
5. OLED 显示保持逻辑
声音触发脉冲持续时间很短,如果检测到就立刻显示 YES,会一闪而过人眼看不清。 程序设置显示保持周期:每一轮主循环 20ms,保持 25 个周期,20ms × 25 = 500ms。 触发之后,就算声音已经消失,屏幕仍然维持 500ms 的 SOUND:YES 显示。
这个 500ms 仅仅是显示效果,并不是代表声音持续了 500ms。
OLED 屏幕输出示例:
SOUND CHECK
OUT: PA0
SOUND: YES
COUNT:0000000001
显示字段释义:
| OUT: PA0 | 声音模块输出接 PA0 引脚 |
| SOUND: YES | 检测到声音,或者 500ms 显示保持还没结束 |
| SOUND: NO | 无声音触发,保持时间结束 |
| COUNT | 外部中断捕获的下降沿总次数 |
五、源代码
main.c、SoundCheck.c、SoundCheck.h相关驱动代码。
main.c代码
/* Sound detector and OLED teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "oled.h"
#include "SoundCheck.h"
#define SAMPLE_PERIOD_MS 20U
#define SOUND_HOLD_CYCLES 25U
static void SoundCheck_ShowState(u8 detected, u32 trigger_count);
int main(void)
{
u8 detected;
u8 last_detected;
u8 hold_cycles;
u32 trigger_count;
u32 last_trigger_count;
delay_init();
SoundCheck_Init();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
last_detected = 0xFFU;
last_trigger_count = 0xFFFFFFFFUL;
hold_cycles = 0U;
while(1)
{
/* EXTI0 keeps short sound pulses from being missed between samples. */
if(SoundCheck_FetchEvent() != 0U)
{
hold_cycles = SOUND_HOLD_CYCLES;
}
if(SoundCheck_IsSoundDetected() != 0U)
{
hold_cycles = SOUND_HOLD_CYCLES;
}
if(hold_cycles > 0U)
{
detected = 1U;
hold_cycles–;
}
else
{
detected = 0U;
}
trigger_count = SoundCheck_GetTriggerCount();
if((detected != last_detected) ||
(trigger_count != last_trigger_count))
{
SoundCheck_ShowState(detected, trigger_count);
last_detected = detected;
last_trigger_count = trigger_count;
}
delay_ms(SAMPLE_PERIOD_MS);
}
}
static void SoundCheck_ShowState(u8 detected, u32 trigger_count)
{
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"SOUND CHECK", 16);
OLED_ShowString(0, 16, (u8 *)"OUT: PA0", 16);
if(detected != 0U)
{
OLED_ShowString(0, 32, (u8 *)"SOUND: YES", 16);
}
else
{
OLED_ShowString(0, 32, (u8 *)"SOUND: NO", 16);
}
OLED_ShowString(0, 50, (u8 *)"COUNT:", 12);
OLED_ShowNum(42, 50, trigger_count, 10, 12);
OLED_Refresh();
}
SoundCheck.c代码
#include "SoundCheck.h"
#include "stm32f10x.h"
#include "stm32f10x_exti.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "misc.h"
/* 声音检测模块固定连接到 PA0,并使用 EXTI0 产生下降沿中断。 */
#define SOUND_CHECK_GPIO_PORT GPIOA
#define SOUND_CHECK_GPIO_PIN GPIO_Pin_0
#define SOUND_CHECK_GPIO_CLOCK RCC_APB2Periph_GPIOA
#define SOUND_CHECK_EXTI_LINE EXTI_Line0
/* 对外公开的当前电平、声音状态及累计触发次数。 */
uint8_t SoundCheck_Level = SOUND_CHECK_LEVEL_HIGH;
uint8_t SoundCheck_Value = SOUND_CHECK_STATE_QUIET;
volatile uint32_t SoundCheck_TriggerCount = 0U;
/* 中断置位的事件标志,以及模块初始化状态。 */
static volatile uint8_t SoundCheck_EventPending = 0U;
static uint8_t SoundCheck_Initialized = 0U;
/*
* 进入临界区并返回进入前的全局中断状态。
* 保存 PRIMASK 可避免在原本已经关中断的环境中错误地重新开中断。
*/
static uint32_t SoundCheck_EnterCritical(void)
{
uint32_t primask;
primask = __get_PRIMASK();
__disable_irq();
return primask;
}
/* 按进入临界区前的状态恢复全局中断。 */
static void SoundCheck_ExitCritical(uint32_t primask)
{
if(primask == 0U)
{
__enable_irq();
}
}
/* 读取 PA0,并按照“低电平有效”规则同步公开状态变量。 */
static void SoundCheck_UpdateState(void)
{
if(GPIO_ReadInputDataBit(SOUND_CHECK_GPIO_PORT,
SOUND_CHECK_GPIO_PIN) == Bit_SET)
{
SoundCheck_Level = SOUND_CHECK_LEVEL_HIGH;
SoundCheck_Value = SOUND_CHECK_STATE_QUIET;
}
else
{
SoundCheck_Level = SOUND_CHECK_LEVEL_LOW;
SoundCheck_Value = SOUND_CHECK_STATE_DETECTED;
}
}
/* 初始化声音检测所需的输入引脚、外部中断和中断控制器。 */
void SoundCheck_Init(void)
{
GPIO_InitTypeDef gpio_init;
EXTI_InitTypeDef exti_init;
NVIC_InitTypeDef nvic_init;
uint32_t primask;
SoundCheck_Initialized = 0U;
primask = SoundCheck_EnterCritical();
SoundCheck_EventPending = 0U;
SoundCheck_TriggerCount = 0U;
SoundCheck_ExitCritical(primask);
RCC_APB2PeriphClockCmd(SOUND_CHECK_GPIO_CLOCK | RCC_APB2Periph_AFIO,
ENABLE);
/* 使用内部上拉,使模块未触发或断开连接时 PA0 保持为高电平。 */
gpio_init.GPIO_Pin = SOUND_CHECK_GPIO_PIN;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SOUND_CHECK_GPIO_PORT, &gpio_init);
/* 将 PA0 映射到 EXTI0,仅在有效的下降沿到来时触发中断。 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
exti_init.EXTI_Line = SOUND_CHECK_EXTI_LINE;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
exti_init.EXTI_LineCmd = ENABLE;
EXTI_Init(&exti_init);
EXTI_ClearITPendingBit(SOUND_CHECK_EXTI_LINE);
/* 配置 EXTI0 的抢占优先级和响应优先级,并使能其中断通道。 */
nvic_init.NVIC_IRQChannel = EXTI0_IRQn;
nvic_init.NVIC_IRQChannelPreemptionPriority = 2U;
nvic_init.NVIC_IRQChannelSubPriority = 0U;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init);
/* 初始化完成前先同步一次引脚状态,避免首次读取返回默认值。 */
SoundCheck_UpdateState();
SoundCheck_Initialized = 1U;
}
/* 兼容旧工程命名的初始化接口。 */
void SoundCheck_IO_Init(void)
{
SoundCheck_Init();
}
/* 返回初始化状态;非零表示已经初始化。 */
uint8_t SoundCheck_IsInitialized(void)
{
return SoundCheck_Initialized;
}
/* 读取 OUT 引脚原始电平,并在需要时自动初始化模块。 */
uint8_t SoundCheck_ReadOutLevel(void)
{
if(SoundCheck_Initialized == 0U)
{
SoundCheck_Init();
}
SoundCheck_UpdateState();
return SoundCheck_Level;
}
/* 读取经过有效电平转换后的声音状态。 */
uint8_t SoundCheck_ReadState(void)
{
(void)SoundCheck_ReadOutLevel();
return SoundCheck_Value;
}
/* 将声音状态转换为便于条件判断的布尔返回值。 */
uint8_t SoundCheck_IsSoundDetected(void)
{
return (SoundCheck_ReadState() == SOUND_CHECK_STATE_DETECTED) ? 1U : 0U;
}
/* 兼容旧工程命名的声音检测接口。 */
uint8_t SoundCheck_Read(void)
{
return SoundCheck_IsSoundDetected();
}
/* 记录一次有效边沿;计数达到 uint32_t 上限后保持饱和。 */
void SoundCheck_NotifyActiveEdge(void)
{
SoundCheck_EventPending = 1U;
if(SoundCheck_TriggerCount < 0xFFFFFFFFUL)
{
SoundCheck_TriggerCount++;
}
}
/* 原子地取出事件标志,并将其清零,防止主程序与中断并发访问。 */
uint8_t SoundCheck_FetchEvent(void)
{
uint32_t primask;
uint8_t event_pending;
primask = SoundCheck_EnterCritical();
event_pending = SoundCheck_EventPending;
SoundCheck_EventPending = 0U;
SoundCheck_ExitCritical(primask);
return event_pending;
}
/* 查询事件标志但不消费事件。 */
uint8_t SoundCheck_PeekEvent(void)
{
return SoundCheck_EventPending;
}
/* 通过取出事件的方式清除待处理标志。 */
void SoundCheck_ClearEvent(void)
{
(void)SoundCheck_FetchEvent();
}
/* 在临界区内读取 32 位触发计数,保证读取结果一致。 */
uint32_t SoundCheck_GetTriggerCount(void)
{
uint32_t primask;
uint32_t trigger_count;
primask = SoundCheck_EnterCritical();
trigger_count = SoundCheck_TriggerCount;
SoundCheck_ExitCritical(primask);
return trigger_count;
}
/* 原子清零触发计数和待处理事件。 */
void SoundCheck_ResetTriggerCount(void)
{
uint32_t primask;
primask = SoundCheck_EnterCritical();
SoundCheck_TriggerCount = 0U;
SoundCheck_EventPending = 0U;
SoundCheck_ExitCritical(primask);
}
/* 获取模块完整状态快照;空指针会返回参数错误。 */
uint8_t SoundCheck_ReadData(SoundCheck_Data *data)
{
uint32_t primask;
if(data == 0)
{
return SOUND_CHECK_ERROR_ARGUMENT;
}
(void)SoundCheck_ReadOutLevel();
data->level = SoundCheck_Level;
data->state = SoundCheck_Value;
primask = SoundCheck_EnterCritical();
data->event_pending = SoundCheck_EventPending;
data->trigger_count = SoundCheck_TriggerCount;
SoundCheck_ExitCritical(primask);
return SOUND_CHECK_OK;
}
/* 处理 EXTI0 中断:登记有效边沿,并清除硬件中断挂起位。 */
void SoundCheck_EXTI_IRQHandler(void)
{
if(EXTI_GetITStatus(SOUND_CHECK_EXTI_LINE) != RESET)
{
SoundCheck_NotifyActiveEdge();
EXTI_ClearITPendingBit(SOUND_CHECK_EXTI_LINE);
}
}
/* EXTI0 中断入口。 */
void EXTI0_IRQHandler(void)
{
SoundCheck_EXTI_IRQHandler();
}
SoundCheck.h代码
#ifndef __SOUND_CHECK_H
#define __SOUND_CHECK_H
#include <stdint.h>
/*
* STM32F103C8T6 声音检测模块接线:
* VCC -> 5V
* OUT -> PA0(外部中断 EXTI0)
* GND -> GND
*
* 本项目使用的数字声音检测模块为低电平有效:当声音强度超过模块上
* 电位器设定的阈值时,OUT 引脚输出低电平。
*/
/* OUT 引脚的原始电平。 */
#define SOUND_CHECK_LEVEL_LOW 0U
#define SOUND_CHECK_LEVEL_HIGH 1U
/* 将原始电平转换后得到的声音检测状态。 */
#define SOUND_CHECK_STATE_QUIET 0U
#define SOUND_CHECK_STATE_DETECTED 1U
/* 接口返回值。 */
#define SOUND_CHECK_OK 0U
#define SOUND_CHECK_ERROR_ARGUMENT 1U
/* 声音检测模块的完整状态快照。 */
typedef struct
{
uint8_t level; /* OUT 引脚当前电平。 */
uint8_t state; /* 当前声音状态:安静或检测到声音。 */
uint8_t event_pending; /* 是否存在尚未处理的触发事件。 */
uint32_t trigger_count; /* 自初始化或复位计数以来的触发次数。 */
} SoundCheck_Data;
/* 为简单教学及调试示例保留的兼容变量。 */
extern uint8_t SoundCheck_Level;
extern uint8_t SoundCheck_Value;
extern volatile uint32_t SoundCheck_TriggerCount;
/* 初始化 GPIO、EXTI0 和 NVIC,并清除事件及触发计数。 */
void SoundCheck_Init(void);
/* 兼容旧代码的初始化接口,内部直接调用 SoundCheck_Init。 */
void SoundCheck_IO_Init(void);
/* 查询模块是否已经初始化。 */
uint8_t SoundCheck_IsInitialized(void);
/* 读取 OUT 引脚原始电平;未初始化时会自动完成初始化。 */
uint8_t SoundCheck_ReadOutLevel(void);
/* 读取转换后的声音状态。 */
uint8_t SoundCheck_ReadState(void);
/* 检测当前是否有声音,检测到声音时返回 1,否则返回 0。 */
uint8_t SoundCheck_IsSoundDetected(void);
/* 兼容旧代码的声音检测接口。 */
uint8_t SoundCheck_Read(void);
/* 读取并清除待处理事件;有事件时返回 1。 */
uint8_t SoundCheck_FetchEvent(void);
/* 只查询待处理事件,不清除事件标志。 */
uint8_t SoundCheck_PeekEvent(void);
/* 清除待处理事件。 */
void SoundCheck_ClearEvent(void);
/* 原子读取累计触发次数。 */
uint32_t SoundCheck_GetTriggerCount(void);
/* 原子清零累计触发次数,并同时清除待处理事件。 */
void SoundCheck_ResetTriggerCount(void);
/* 记录一次有效触发边沿,通常由中断服务程序调用。 */
void SoundCheck_NotifyActiveEdge(void);
/* 将当前电平、状态、事件标志和触发次数复制到 data。 */
uint8_t SoundCheck_ReadData(SoundCheck_Data *data);
/* 由 EXTI0_IRQHandler 调用;与其他中断服务程序整合时也可直接调用。 */
void SoundCheck_EXTI_IRQHandler(void);
#endif
核心 API 函数说明
| SoundCheck.c | SoundCheck_Init() | 初始化 PA0 上拉输入、EXTI0 外部中断、NVIC 中断优先级 |
| SoundCheck.c | SoundCheck_ReadOutLevel() | 读取 PA0 原始 GPIO 高低电平 |
| SoundCheck.c | SoundCheck_IsSoundDetected() | 根据电平判断是否检测到声音 |
| SoundCheck.c | SoundCheck_FetchEvent() | 读取并且清除待处理声音事件标志 |
| SoundCheck.c | SoundCheck_GetTriggerCount() | 原子操作读取下降沿累计计数 |
| SoundCheck.c | SoundCheck_EXTI_IRQHandler() | EXTI0 中断服务函数,记录事件,清除中断标志 |
| main.c | SoundCheck_ShowState() | OLED 屏幕打印声音状态与计数值 |
六、实物演示章节
6.1 实物器材清单
- STM32F103C8T6 最小系统板 ×1
- 数字声音检测模块(麦克风 + 比较器) ×1
- 0.96 寸 I2C OLED 显示屏 ×1
- 杜邦线若干
- USB 下载器、供电电源
6.2 实物演示操作步骤
不同模块电位器旋转方向效果不一样,不要死记顺时针 / 逆时针,以实际屏幕现象为准。


6.3 演示过程现象说明
6.4 常见实物故障排查
| OLED 一直 SOUND: YES | 灵敏度过高、OUT 持续低电平、接线错误 | 调节电位器,万用表测量 OUT 引脚电平 |
| 永远 SOUND: NO,拍手无反应 | 模块没供电、没有共地、OUT 接线断开、阈值调太高 | 检查 VCC/GND,确认 PA0 接线,调高灵敏度 |
| 安静环境下计数不停上涨 | 电源干扰、环境噪音、电位器处在阈值临界点 | 远离开关电源,重新微调电位器阈值 |
| 拍一次手,计数跳多次 | 信号抖动反复触发比较器输出边沿 | 硬件增加迟滞电路,或者软件增加消抖延时 |
七、项目局限性以及改进拓展思路
数字声音检测模块只能做简单的声音有无触发,存在不少局限:
拓展升级方案
八、总结
本实验通过声音检测模块,把现实世界的声音振动转换成单片机能够识别的数字电平信号。 利用 STM32 的 EXTI 外部中断捕获转瞬即逝的下降沿脉冲,弥补单纯轮询检测丢失短事件的缺陷;再通过事件标志、计数变量、500ms 显示保持,将肉眼很难捕捉的短促声音事件直观展示在 OLED 屏幕。 适合学习 STM32 外部中断、GPIO 输入、临界区共享变量处理的入门实验。







