STM32声音传感器测试程序:ADC+DMA采集与RMS计算
简介面向STM32开发者与物联网初学者的实战测试资源围绕声音/噪音传感器数据采集展开基于STM32F103C8T6开发板验证通过杜邦线连接传感器到PB6程序持续监测环境声音达到有效分贝后控制PB9用户LED亮起清晰演示了传感器阈值检测与GPIO控制的典型流程适合入门外设驱动、信号采集和物联网节点调试场景。压缩包共164个文件约4.69MB主要包含C源码、H头文件、工程备份、文档说明与可烧录HEX固件KEIL工程结构完整目录组织清楚便于按需查阅。已有5044人学习下载代码在F103系列上可直接迁移仅需修改KEIL芯片型号与Flash容量即可运行。这份资料能让读者在短时间内掌握传感器数据读取、LED状态指示和工程适配方法也为后续扩展声音触发报警、语音控制等物联网应用提供了可复用的代码基座。1. 先搞清声音传感器测试程序要解决什么再动手手头有一块 LM393 声音检测模块想把它塞进 STM32 物联网节点做噪音监测或声控。很多人上来就套例程结果串口打印值要么恒为 0要么满屏 4095。其实问题多半出在三个地方分不清 AO 和 DO、供电电压选错、采样后没扣直流偏置。下面这份测试程序代码会跑通整条链路选型接线、CubeMX 配置、ADC 加 DMA 采集、RMS 计算、串口打印最后用状态机做阈值滞回消抖。这是物联网毕业设计和智能环境监测里最常卡住的一步。记住一个判断声音传感器测的从来不是「音量」而是麦克风信号在 ADC 上的电压波动幅度。2. 声音/噪音传感器模块选型与 STM32 采集原理2.1 模块上那两个引脚差在哪AO 是模拟信号DO 是比较器输出市面上一二十块钱的声音传感器模块KY-037、KY-038 这类结构都差不多一颗驻极体麦克风一级电压跟随或放大一颗 LM393 比较器外加一个可调电位器。AO 引脚给出的是麦克风处理后的模拟电压静音时稳定在电源电压一半附近有声音时叠加交流波动DO 引脚接在 LM393 后面电位器决定比较阈值声音超过阈值就翻转输出 0/1。很多教程让你直接读 DO 判断有没有声音但 DO 是经过比较器量化的一次性判决结果你既拿不到声音大小也调不准触发点对噪音监测没有意义。测试阶段必须读 AO把 ADC 原始值拿回来自己算 RMS阈值、告警、上报才能由固件控制而不是靠螺丝刀拧电位器。选型时注意两点一是部分廉价模块的 AO 只有偏置电压加很小的摆幅几十到几百 mV要选带放大级的模块二是 LM393 是开集电极输出有些板子自带拉到 VCC 的上拉电阻有些不带直接测 DO 一直是低电平先查上拉再怀疑传感器。另外如果你之前只用过 51 单片机、STC 单片机它们片内 ADC 精度和 DMA 能力都偏弱采集声音波形要么外挂 ADC要么只能做 0/1 判断。STM32 的 12 位 ADC 加循环 DMA 让连续采样变成标配这也是这个标题把 STM32 作为承载平台的原因。2.2 直流偏置、信号摆幅与 12 位 ADC 的换算关系STM32F103 的 ADC 是 12 位输入范围 0 到 VREFVREF 通常直接接 VDD 即 3.3V一个 LSB 对应约 0.806mV。模块 3.3V 供电时静音直流偏置点约为 1.6~1.7V换算成 ADC 读数在 2048 附近。声音到来时AO 在偏置点上下波动波动幅度越大扣掉偏置后计算得到的绝对值或均方根越大这就是声音大小的原始度量。比如正常说话让 AO 波动 ±300mV对应读数摆动约 ±372 个 LSB拍手瞬间可能到 ±800mV接近 ±1000 个 LSB。这里最容易翻车的是供电电压。很多模块丝印写着 3.3~5V你图省事接了 5V静音点变成 2.5V音量一大 AO 就会超过 3.3VADC 读数长时间停在 4095数据看起来像是满量程噪声。正确做法是模块和 STM32 同用 3.3V 供电或者用电阻分压加电压跟随器把 AO 压到 2V 以内再进 ADC。3.3V 供电的代价是最大摆幅变小、灵敏度略降可以用板上的灵敏度电位器补偿但测试程序阶段优先保证不超出量程。2.3 接线规范与上电自检先确认偏置点再谈算法接线按下面这张表来这是声音传感器测试程序最基础的一步模块引脚接 STM32说明AOPA1ADC1_IN1模拟电压输入CubeMX 里把 PA1 配为模拟通道DO悬空本阶段不读 DO避免混淆VCC3.3V严禁 5V防止 AO 超过 ADC 量程GNDGND必须共地不共地会看到满量程噪声接完线先做上电自检静音时用万用表量 AO 对地电压应该在 1.5~1.8V 之间说话时电压明显波动。没有万用表就先写一段最简单次转换代码打印 20 个 ADC 读数/* 最简验证单次转换循环读 AO确认直流偏置点 */ HAL_ADC_Start(hadc1); for (uint8_t i 0; i 20; i) { HAL_ADC_PollForConversion(hadc1, 10); /* 阻塞等转换完成超时 10ms */ uint32_t v HAL_ADC_GetValue(hadc1); if (i % 5 0) /* 每 5 次打印一次减少刷屏 */ { printf(AO%lu mV\r\n, v * 3300 / 4096); } HAL_Delay(50); } HAL_ADC_Stop(hadc1);说明v * 3300 / 4096把 12 位读数换算成毫伏静音读数应在 1500~1800 之间前几百毫秒读数偏离是上电稳定过程不是故障。若读数长时间贴 0 或 4095先查供电和共地别急着调滤波算法。这一步确认偏置点正常再进入下一章配 DMA 连续采集。3. 在 STM32 开发环境里用 CubeMX 配置 ADC 连续采集与 DMA3.1 工程选型、芯片包安装与 Keil MDK 环境最常见做法是用 STM32F103C8T6 最小系统板CubeMX 生成初始化代码Keil MDK 编译下载。CubeMX 首次建工程要先安装对应芯片系列的固件包在 Help → Manage Embedded Software Packages 里下载 F1 系列包。这个环节报错大多是版本不匹配CubeMX 新版要求固件包较新老教程截图里的下载入口对不上按提示安装当前可用版本即可。编译端要注意 Keil C51 和 Keil MDK 是两套不同的安装装了 C51给 51 单片机用的打不开 STM32 工程需要单独安装 MDK-ARM 以及 STM32F1 的 Device Family Pack打开 .uvprojx 时才不会提示找不到设备。很多做过 51 开发的人卡在这一步就是把 C51 和 MDK 混为一谈了。至于物联网学习需要什么软件CubeMX 加 Keil MDK 就是 stm32 项目最标准的组合不需要额外装复杂工具链。工程层面如果不想用 F1也可以选 STM32G0 系列。G0 的 ADC 带硬件过采样器能把多次采样合成一个值做声级测量少写一段软件均值滤波缺点是中文教程少遇到问题查得慢。如果你后面往低功耗甚至无源物联网方向走G0 的低功耗模式和硬件过采样反而更合适但测试程序图省心我一般还是 F1。另外 CubeMX 里 Project → Change Device 可以换同系列不同型号比如从 C8T6 换到 CBT6 增大 Flash引脚分配大部分保留不用重新画一遍。3.2 时钟树配置ADC 时钟必须压在 14MHz 以下F103 的 ADC 挂在 APB2 总线上ADC 时钟由 PCLK2 分频得到规格书上 ADC 时钟上限是 14MHz。系统时钟 72MHz 时预分频最小取 6即 ADC 时钟 12MHzCubeMX 里 ADC Prescaler 选 /6 即可。别把预分频调成 2 想提高采样率ADC 时钟超规格会导致转换结果随机跳动这是数据看起来有噪声的隐藏原因之一。提示ADC 预分频调小并不能让声音测得更准超过 14MHz 只会让转换结果发飘。单次转换时间等于采样周期加 12.5 个 ADC 时钟周期不同采样周期的表现差异很大采样周期单次转换时间 12MHz极限采样率适用场景1.51.17μs857kHz低阻抗源、纯电平测量28.53.42μs293kHz普通模拟量温度、分压55.55.67μs176kHz声音传感器模块推荐239.521.0μs47.6kHz高阻抗源、信号源内阻大对声音模块选 55.5 周期AO 引脚前级有一定输出阻抗采样电容建立电压需要时间采样周期太短读数会偏低且漂。这里的 176kHz 是 ADC 单通道连转的极限速率不是说你采到了 176kHz 的声波麦克风模块通带一般也就 100Hz~10kHz声级测量用这个余量完全够。3.3 CubeMX 里 ADC、DMA 与串口的关键设置以下按 F103C8T6、HAL 库、PA1 接 AO 为例列配置项外设设置项取值说明ADC1Channel 1PA1使能 IN1 通道ADC1Continuous ConversionEnabled连续转换模式ADC1DMA Continuous RequestsEnabled让 DMA 持续跟转换走ADC1Sampling Time55.5 Cycles见 3.2 节ADC1Scan ConversionDisabled只采一个通道不用扫描DMA1 Channel1ModeCircular循环模式不停止DMA1 Channel1DirectionPeripheral To Memory外设到内存DMA1 Channel1Data WidthWord/Word与缓冲数组配套USART1Mode115200 8N1测试打印用开中断要点在 DMA 数据宽度F1 的 ADC 数据寄存器是 32 位的ST 官方例程按 Word 取出所以缓冲数组声明成uint32_t数组DMA 宽度配 Word/Word。如果抄到别的例程用uint16_t数组DMA 宽度要相应改 Half Word数组类型和宽度必须配套否则数据错位高字节低字节混在一起。这是 stm32 例程移植时非常常见的问题。串口用 USART1115200 8N1发送暂时用阻塞方式即可测试程序的数据量很小没必要一开始就上串口 DMA。3.4 生成代码后必补的 printf 重定向CubeMX 生成的工程直接调用 printf 不会输出到串口必须重定向 fputc/* usart.c 文件末尾追加同时勾选 Keil 的 MicroLIB */ #include stdio.h int fputc(int ch, FILE *f) { while (!(USART1-SR USART_SR_TXE)) { } /* 等发送数据寄存器空 */ USART1-DR ch; /* 写入要发送的字节 */ return ch; }说明USART_SR_TXE位置位表示发送寄存器空可以写入下一个字节轮询这个标志是最简单的阻塞发送方式。注意两点一是 Keil 里 Target → Code Generation 要勾选 Use MicroLIB不勾的话 printf 的重定向可能不生效二是 F1 系列 USART1 挂在 APB2 上如果换串口这里要换成对应的串口实例比如用 USART2 就把 USART1 全部改成 USART2寄存器地址不同不能混用。4. 测试程序代码实现ADCDMA 循环采集、RMS 计算与串口打印4.1 为什么用连续转换加 DMA 循环而不是定时去读如果直接在 while(1) 里调HAL_ADC_PollForConversion读值主循环只要插入一句 printf采样时刻就会跟着抖动算出来的 RMS 一会儿高一会儿低。DMA 循环模式让 ADC 在后台全速转换结果不停往缓冲区写CPU 只需要在整块缓冲区填满后进一次中断处理数据。以 1024 点缓冲区、55.5 周期采样为例满块间隔约 5.8ms一次回调理完 1024 个点printf 阻塞或延时完全不影响采样节奏。唯二要记住的数字缓冲区 1024 点窗口时长 5.8ms。这个窗口对 100Hz 以上的声音包含多个周期RMS 有统计意义如果你的环境里有 50Hz 工频干扰想平均掉把缓冲区扩到 4096 点窗口变成约 23ms代价是临界点响应略慢。4.2 核心代码RMS 与峰值开头定义缓冲区和全局结果量#define SAMPLE_NUM 1024 static uint32_t adc_raw[SAMPLE_NUM]; /* DMA 循环写入类型必须 uint32_t */ static volatile uint32_t sound_rms 0; /* RMS 计算结果单位 ADC LSB */ static volatile uint32_t sound_peak 0; /* 窗口内峰值单位 ADC LSB */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { uint64_t sum 0; uint32_t peak 0; for (uint32_t i 0; i SAMPLE_NUM; i) { int32_t v (int32_t)adc_raw[i] - 2048; /* 扣除 1.65V 直流偏置 */ uint32_t a (v 0) ? (uint32_t)(-v) : (uint32_t)v; if (a peak) peak a; /* 统计窗口峰值 */ sum (uint64_t)(v * v); /* 累加平方必须 64 位 */ } sound_rms (uint32_t)sqrt((double)sum / SAMPLE_NUM); sound_peak peak; } }说明2048 是静音偏置点对应的读数v 是去掉直流后的交流分量RMS 等于平方和均值再开方量纲是 ADC LSB最大约 2048。peak 记录窗口内最大波动幅度用来判断有没有削波——如果 peak 经常顶着 2048说明信号饱和了要调灵敏度电位器或降低增益。累加平方必须用uint64_t1024 个点的平方和能到几亿uint32_t 会溢出。sqrt 在这个位置每 5.8ms 才执行一次对 F103 不是负担。主程序里启动转换并周期打印HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_raw, SAMPLE_NUM); while (1) { printf(rms%lu peak%lu\r\n, sound_rms, sound_peak); HAL_Delay(200); }参数说明HAL_ADC_Start_DMA第三个参数传的是采样点数不是字节数这行代码之后 ADC 就不停往 adc_raw 里写打印周期 200ms 对应每秒 5 个结果方便人工观察也方便后面 Python 画曲线。注意回调里不要调 printf 和 HAL_Delay中断回调里做长时间操作会把其他中断饿死只需要更新全局变量。4.3 数据跳变不是 bug短时 RMS 本来就在跳打印一会儿就会发现 rms 从 30 跳到 300 再落回 50这不是程序问题。声音包络本来起伏就大说一个字的瞬间能量远高于停顿拍手更是尖峰。如果想看环境噪音水平这种慢变量对 RMS 再做一次一阶滤波#define EMA_N 16 static uint32_t ema_rms 0; /* 在 ConvCplt 回调里更新一次 */ ema_rms (uint32_t)((ema_rms * (EMA_N - 1) sound_rms) / EMA_N);这一行是整数版的指数滑动平均本质是每个新采样占 1/16 权重旧值占 15/16。EMA_N 越小响应越快、毛刺越多越大越平滑但时延变大8 到 32 之间常用。它和 SAMPLE_NUM 的分工不同SAMPLE_NUM 决定单次 RMS 的统计窗口EMA 决定平滑的时间常数两级滤波各管一段别用其中一个替掉另一个。4.4 串口数据帧与 Python 上位机画线测试阶段打印文本行最简单但后面接网关或数据采集器时建议改用固定长度二进制帧偏移含义说明0~1帧头0xAA 0x552数据长度固定 43~4RMSuint16 小端5~6PEAKuint16 小端7校验前 7 字节累加低 8 位发送代码void send_frame(uint16_t rms, uint16_t peak) { uint8_t f[8]; f[0] 0xAA; f[1] 0x55; f[2] 4; f[3] rms 0xFF; f[4] rms 8; f[5] peak 0xFF; f[6] peak 8; f[7] (uint8_t)(f[0] f[1] f[2] f[3] f[4] f[5] f[6]); HAL_UART_Transmit(huart1, f, 8, 10); }说明小端就是低字节在前和 ARM 内存布局一致累加校验能挡住大多数单字节错误串口测试够用。如果以后走 Modbus 网关这个帧的负载段可以整体映射进 Modbus 保持寄存器校验那段直接复用。PC 端用 Python 收文本行并画曲线验证传感器响应import serial import matplotlib.pyplot as plt ser serial.Serial(COM5, 115200, timeout1) # Linux 下写 /dev/ttyUSB0 data [] for _ in range(400): line ser.readline().decode(utf-8, errorsignore) if rms in line: data.append(int(line.split(rms)[1].split()[0])) plt.figure(figsize(9, 3)) plt.plot(data, lw0.8) plt.xlabel(sample (200ms/point)) plt.ylabel(RMS / LSB) plt.grid(alpha0.3) plt.show()说明脚本在测试程序跑起来后直接执行400 个点对应约 80 秒对着模块拍手时曲线出现尖峰说话时出现一段抬高的包络说明整个采集链路是通的。Windows 下先关掉串口助手再运行脚本否则端口被占用会报错。4.5 测试阶段最容易踩的三个坑第一个坑是把 DO 当模拟量用DO 出来的只是 0/1阈值在板载电位器上打印出来只有两种值无法标定也不能算噪音水平。第二个坑是缓冲区类型和 DMA 宽度不匹配uint16_t数组配 Word 宽数据高 16 位和低 16 位错位结果完全乱掉这个在 3.3 节说过。第三个坑是模块灵敏度电位器拧到最大小声音也饱和peak 天天顶 2048先调回一半再测。这三个问题占掉声音传感器测试阶段的大部分排错时间。5. 给测试程序收尾当单片机遇上状态机阈值滞回加计数消抖5.1 单阈值判断为什么不可靠拿到稳定的 sound_rms 之后最自然的报警写法是 rms 大于阈值就置位。实际跑起来会发现说一句话时 rms 在 100 到 400 之间来回摆动超过阈值又跌回输出沿抖成一串驱动蜂鸣器就是响一下停一下再响一下。光靠滤波压不住脉冲声的快速起伏标准做法是滞回加计数用状态机把抖动挡在外面。5.2 滞回加计数的三状态机typedef enum { ST_IDLE, /* 无声音 */ ST_CONFIRM, /* 疑似有声音计数确认 */ ST_TRIG /* 确认触发 */ } snd_state_t; #define T_ON 200 /* 触发阈值拍手/说话 RMS */ #define T_OFF 100 /* 释放阈值低于此才算静音恢复 */ #define CONFIRM_CNT 3 snd_state_t snd_st ST_IDLE; uint8_t confirm 0; void sound_sm_run(void) { switch (snd_st) { case ST_IDLE: if (sound_rms T_ON) { confirm 0; snd_st ST_CONFIRM; } break; case ST_CONFIRM: if (sound_rms T_ON) { if (confirm CONFIRM_CNT) { snd_st ST_TRIG; } /* 连续过阈才动作 */ } else { snd_st ST_IDLE; } /* 中间掉下来就放弃 */ break; case ST_TRIG: if (sound_rms T_OFF) snd_st ST_IDLE; /* 低于释放阈值才复位 */ break; default: snd_st ST_IDLE; break; } }说明T_ON 与 T_OFF 故意拉开距离就是滞回触发条件比释放条件苛刻声音在阈值附近振荡时状态不会来回翻最多在 ST_CONFIRM 里多待一会儿。CONFIRM_CNT 是防抖计数必须连续 3 次过阈才转 TRIG相当于用时间维度换可靠性。这个状态机每 20ms 在主循环里跑一次反应延迟最多 60ms人感知不到。5.3 阈值标定与现场验证阈值不能靠猜标定按三步走先静音跑 30 秒打印 RMS 取基线再正常说话或拍手记峰值T_ON 取基线的 3~5 倍并留余量最后录三段静音→说话→静音→拍手→静音共 60 秒数据用 4.4 节的 Python 脚本保存数状态机触发次数确认不多触发也不漏触发。参考取值见下表场景T_ONT_OFF备注安静办公室声控15080基线约 30~50车间噪音报警600350先采 10 分钟环境数据再定拍手触发900500要求信噪比高防误触状态机稳定后把 ST_TRIG 分支换成 GPIO 拉高控制继电器或经 ESP8266 走 MQTT 上报物联网平台节点就闭环了。之后把 T_ON、T_OFF、CONFIRM_CNT 从宏改成结构体带版本号写进内部 Flash 最后一个扇区上电先读取无有效版本就用默认值量产时每台设备现场标定一次不用为调一个阈值反复改代码重刷固件。本文还有配套的精品资源点击获取