STM32录音机实战:定时器+ADC+DMA构建音频采集与回放系统
简介这份资源面向具备一定嵌入式基础的STM32开发者提供一套可直接运行的录音机程序实例帮助读者理解音频采集、编码存储与文件系统集成的完整实现路径。压缩包共160个文件以68个.h头文件与65个.c源文件为主体涵盖ADC配置、FAT文件系统、LCD驱动等模块另含12张png界面截图、4个txt说明及Keil工程文件整体约845KB结构紧凑便于按模块查阅。资源围绕音频输入、PCM编码、内存管理、SD卡文件系统与控制逻辑等关键环节展开配套实验代码、原理图与步骤指南可帮助读者掌握STM32CubeMX配置、HAL库驱动编写及JTAG/SWD调试方法。目前已有1862人学习适合作为嵌入式音频应用开发的实践参考。1. 从一块闲置的 STM32 板子说起这个录音机程序到底能干什么手头有块 STM32 开发板跑过点灯、串口打印然后呢很多人卡在这一步——外设都认识凑一起就不知道怎么下手。这个录音机程序正好填这个空它把麦克风采集、ADC 采样、数据缓冲、存储写入、回放输出串成一条完整链路跑通之后你手里就有一个能录能放的嵌入式小设备而不是一堆孤立的例程。适合两类人刚学完外设想找个综合项目练手的以及想给现有产品加录音功能的。它不依赖复杂中间件核心就是定时器触发采样加 DMA 搬运代码量可控改起来心里有底。下面按“能干什么、怎么跑、怎么改”三层拆开讲。2. 整体架构与目录结构一条音频数据从麦克风到存储的完整链路2.1 系统分层与数据流这个程序的结构不复杂但分层是清楚的。从上到下大致是四层应用层负责按键扫描和状态机空闲、录音、回放、停止音频采集层负责定时器触发 ADC 采样缓冲与传输层用 DMA 把 ADC 数据搬到内存环形缓冲区存储层负责把缓冲区数据写进外部存储介质常见做法是 SPI Flash 或 SD 卡具体看板子资源。回放时数据流反向走一遍存储读出来经 DAC 或 PWM 输出到喇叭。为什么这么分因为录音是典型的“生产者快、消费者慢”场景。ADC 按固定采样率持续产数据写存储的速度跟不上中间必须有个缓冲区兜着。用 DMA 而不是中断逐字节搬是为了把 CPU 从高频中断里解放出来——8kHz 采样率下每秒 8000 次中断CPU 基本别干别的了。DMA 搬完一个半缓冲再通知一次CPU 只在半满和全满两个点介入负担小一个数量级。状态机是另一个关键设计。录音和回放不能同时进行按键抖动又容易误触发所以用显式状态机管理每个状态只响应合法事件。常见做法是枚举加 switch简单直接调试时打断点也方便。2.2 目录组织与关键文件工程一般按功能分目录而不是把所有 .c 堆在根下。下面这个结构是我拆这类项目时最常见的组织方式具体文件名可能不同但职责划分基本一致project/ ├── Core/ │ ├── Src/ │ │ ├── main.c # 初始化、主循环、状态机调度 │ │ ├── audio.c # 录音/回放核心逻辑 │ │ ├── buffer.c # 环形缓冲区管理 │ │ └── storage.c # 存储介质读写封装 │ └── Inc/ │ ├── audio.h │ ├── buffer.h │ └── storage.h ├── Drivers/ │ ├── CMSIS/ # 内核相关 │ └── STM32xx_HAL_Driver/ # 外设驱动 └── MDK-ARM/ 或 STM32CubeIDE/ # 工程文件看一个工程先看 main.c 的初始化顺序再看 audio.c 的采样配置基本就能判断它靠不靠谱。初始化顺序错了是新手最常见的翻车点比如 DMA 还没配好就开了定时器第一包数据直接丢。2.3 采样参数怎么定采样率、位深、缓冲区大小这三个参数决定了整个系统的资源占用和音质必须一起考虑。8kHz、16 位单声道是语音场景的经典组合每秒 16KB 数据对 STM32 的 RAM 和 Flash 都友好。如果板子带外部 SRAM 或 SD 卡可以上 16kHz 甚至 44.1kHz但缓冲区要相应放大。缓冲区大小有个经验公式缓冲区时长至少要是存储写入最坏延迟的两倍。比如写一个 Flash 扇区要 20ms那缓冲区至少存 40ms 音频8kHz 下就是 640 个采样点。太小会溢出丢数据太大占 RAM 又增加延迟。我一般先按 1KB 到 4KB 起步跑起来看有没有溢出标志再调。3. 核心模块拆解定时器、ADC、DMA 三件套怎么配3.1 定时器触发 ADC 采样录音的节奏由定时器定不能让 ADC 自由跑。自由跑的采样率随电压和温度漂移录出来的声音会变调。用定时器更新事件触发 ADC 转换采样间隔就是硬性的、可预测的。// 定时器配置8kHz 采样率假设系统时钟 72MHz void TIM_Sample_Init(void) { // 预分频 72 - 计数时钟 1MHz TIM3-PSC 72 - 1; // 自动重装 125 - 1MHz / 125 8kHz 更新频率 TIM3-ARR 125 - 1; // 更新事件作为 TRGO 输出去触发 ADC TIM3-CR2 | TIM_CR2_MMS_1; TIM3-CR1 | TIM_CR1_CEN; }PSC 和 ARR 的算法是计数时钟 系统时钟 / (PSC1)更新频率 计数时钟 / (ARR1)。这里 72MHz / 72 1MHz1MHz / 125 8kHz。改采样率就改 ARR比如要 16kHz 就把 ARR 设成 62。注意 ARR 必须是整数算出来有小数就调 PSC 凑别硬取整否则采样率偏差累积起来音调就不对了。3.2 ADC 与 DMA 环形缓冲ADC 配成外部触发、连续转换模式DMA 配成循环模式缓冲区一分为二。DMA 填满前半段触发一次中断填满后半段再触发一次这就是所谓的双缓冲半传输加全传输。CPU 处理前半段时DMA 在写后半段互不干扰。#define BUF_SIZE 1024 static uint16_t adc_buf[BUF_SIZE]; void ADC_DMA_Init(void) { // ADC 外部触发上升沿对应定时器 TRGO ADC1-CR2 | ADC_CR2_EXTTRIG | ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1; ADC1-CR2 | ADC_CR2_ADON | ADC_CR2_DMA; // DMA1 通道1循环模式半字宽度 DMA1_Channel1-CPAR (uint32_t)ADC1-DR; DMA1_Channel1-CMAR (uint32_t)adc_buf; DMA1_Channel1-CNDTR BUF_SIZE; DMA1_Channel1-CCR | DMA_CCR_CIRC | DMA_CCR_HTIE | DMA_CCR_TCIE; DMA1_Channel1-CCR | DMA_CCR_EN; }CNDTR 设成缓冲区总长度循环模式下 DMA 搬完自动回到起点。HTIE 和 TCIE 分别使能半传输和全传输中断中断里只做一件事置标志位让主循环去处理数据。中断服务函数里千万别做耗时操作否则下一次半传输中断来的时候你还没处理完数据就被覆盖了。3.3 环形缓冲区与存储写入DMA 缓冲区是“生产端”存储写入是“消费端”中间用环形缓冲区解耦。写指针由中断推进读指针由主循环推进两者相遇就是溢出。typedef struct { uint8_t *data; uint16_t size; volatile uint16_t wr; volatile uint16_t rd; } ringbuf_t; // 中断里调用写入新采样数据 uint16_t ringbuf_put(ringbuf_t *rb, const uint8_t *src, uint16_t len) { uint16_t free rb-size - ((rb-wr - rb-rd rb-size) % rb-size); if (len free) len free; // 空间不够就截断记录溢出 for (uint16_t i 0; i len; i) { rb-data[rb-wr] src[i]; rb-wr (rb-wr 1) % rb-size; } return len; }wr 和 rd 用 volatile 修饰因为它们在中断和主循环两个上下文里被改。取模运算保证指针回绕。返回值是实际写入长度小于请求长度就说明溢出了主循环里可以据此点亮一个告警灯或者降采样率。存储写入按扇区对齐攒够一个扇区再写别一个采样点写一次Flash 擦写寿命经不起这么折腾。4. 编译、烧录与上板验证从工程打开到听到第一段回放4.1 工程导入与编译拿到工程先确认两件事用的哪个 IDEKeil MDK 还是 STM32CubeIDE以及芯片型号对不对。打开工程文件后在工程设置里核对 Device 型号型号选错编译能过但烧进去跑飞。时钟配置也要看一眼HSE 晶振频率和板子上实际焊的是否一致不一致的话定时器算出来的采样率全是错的。编译前检查启动文件是否匹配芯片容量。小容量芯片比如 64KB Flash和大容量芯片的中断向量表不一样用错了链接能过但中断进不去。编译输出里如果出现 “region RAM overflowed”说明缓冲区开太大了先把 BUF_SIZE 减半再试。4.2 烧录与首次上电检查烧录用 ST-Link 或板载调试器都行。烧完先别急着按录音键打开串口看初始化日志——靠谱的工程会在每个外设初始化后打印一行状态。如果串口没输出先查波特率和引脚复用再查时钟使能有没有漏。上电后按顺序验证先测定时器有没有在跑示波器看 TRGO 引脚或者读计数器寄存器看是否变化再测 ADC 有没有数据DMA 缓冲区前几个值是否随环境声音波动最后才测存储写入。跳过前两步直接测录音出了问题根本分不清是采集坏了还是存储坏了这是血泪经验。4.3 录音回放全流程验证验证录音质量有个土办法录一段固定频率的蜂鸣声回放时用手机上的频谱软件看峰值频率对不对。8kHz 采样率下1kHz 正弦波回放出来峰值应该在 1kHz 附近偏了就说明采样率配置有问题。再录一段人声听有没有断续——断续通常是缓冲区溢出连续但变调是采样率错有底噪是 ADC 参考电压不稳。回放通路如果是 PWM 加滤波注意滤波截止频率要低于采样率的一半否则会有混叠噪声。如果是 DAC 输出检查输出缓冲有没有使能没使能的话带负载能力很差接耳机声音小得可怜。5. 避坑与排查录音程序最容易翻车的五个地方5.1 录出来的声音全是噪声现象回放时只有沙沙声听不出原始声音。原因多半是 ADC 引脚配置错了——配成了模拟输入但没关内部上拉下拉或者参考电压引脚悬空。解决检查 GPIO 模式寄存器模拟输入模式下上下拉必须关闭万用表量 VREF 引脚电压是否稳定。另一个可能是 DMA 搬的是错误通道读到的全是 0xFFFF。5.2 录音中途丢数据、回放有咔哒声现象录音时长够了但回放中间有规律性的断裂。原因存储写入耗时超过缓冲区时长环形缓冲区溢出。解决先把 BUF_SIZE 翻倍验证是不是这个原因确认后优化写入逻辑——攒整扇区再写、用 DMA 做存储传输、或者降低采样率。别在中断里直接写 Flash那是给自己找麻烦。5.3 采样率对但音调不对现象录放流程都正常就是声音变尖或变沉。原因定时器时钟源算错了。很多人忘了 APB 预分频器对定时器时钟有倍频效应——APB1 分频不为 1 时定时器时钟是 APB1 的两倍。解决查参考手册的时钟树图确认 TIMxCLK 实际频率重新算 PSC 和 ARR。这个坑几乎每个新手都会踩一次。5.4 烧录后程序不运行现象编译无报错烧录成功但板子毫无反应。原因启动文件与芯片容量不匹配或者中断向量表偏移没设对。解决核对芯片型号对应的启动文件检查 system_stm32xx.c 里的 VECT_TAB_OFFSET。如果用了 bootloader应用区的向量表要重映射。5.5 回放有高频啸叫现象喇叭里持续高频噪声盖过正常音频。原因PWM 载波频率落在音频带内或者滤波电路参数不对。解决把 PWM 频率提到 20kHz 以上超出人耳范围重新设计 RC 滤波截止频率。如果是 DAC 方案检查输出运放有没有自激反馈电阻布局是否合理。6. 二次开发改造把单声道录音机改成可配置的音频采集节点6.1 参数化改造一份配置管住所有采样参数原程序里采样率、缓冲区大小这些数字散落在各个文件里改一个要翻三处。我一般第一步就是把它们收拢到一个头文件用宏统一管理改配置只动一个地方。// audio_config.h —— 所有音频参数集中在这里 #define AUDIO_SAMPLE_RATE 8000 // 采样率 Hz #define AUDIO_BITS 16 // 位深 #define AUDIO_CHANNELS 1 // 声道数 #define AUDIO_BUF_MS 100 // 缓冲区时长 ms // 由上面推导不要手改 #define AUDIO_BUF_SAMPLES (AUDIO_SAMPLE_RATE * AUDIO_BUF_MS / 1000) #define AUDIO_BUF_BYTES (AUDIO_BUF_SAMPLES * AUDIO_BITS / 8 * AUDIO_CHANNELS)这样改采样率只动一个宏缓冲区大小自动跟着变。定时器的 PSC 和 ARR 也可以在初始化函数里用宏算出来而不是写死。改造完记得重新验证一遍采样率参数联动最容易出隐蔽错误。6.2 加一个文件头让录音数据自带描述信息裸 PCM 数据最大的问题是脱离程序就不知道采样率是多少回放端没法正确解析。改造思路是在每段录音开头写一个固定长度的文件头记录采样率、位深、时长、数据长度。回放时先读头再决定怎么解析数据就有了自描述能力。typedef struct { uint32_t magic; // 固定标识比如 0x41554449 (AUDI) uint16_t sample_rate; uint8_t bits; uint8_t channels; uint32_t data_len; // 后续音频数据字节数 } wav_header_t;写入时机是按下录音键、真正开始存数据之前。注意结构体对齐问题跨平台传输时最好手动按字节序列化别直接 memcpy 结构体否则不同编译器对齐方式不一样读出来全是乱的。6.3 验证改造是否成功的方法改完别只看“能录能放”要做量化验证。录一段 10 秒已知内容检查文件头里的 data_len 是否等于 采样率 × 位深/8 × 声道数 × 10误差超过 1% 就说明有丢帧。再用频谱软件比对原始信号和回放信号的峰值频率偏差在 1% 以内算合格。最后做一次长时间录音比如 5 分钟看缓冲区溢出标志有没有被置位这是检验系统稳定性的硬指标。6.4 一个容易忽略的细节停止录音时的收尾按下停止键的瞬间DMA 里可能还有半个缓冲区的数据没写进存储定时器也还在跑。正确的收尾顺序是先关定时器停止触发再等 DMA 当前传输完成然后把环形缓冲区里剩余数据全部刷进存储最后更新文件头里的 data_len。顺序错了要么丢尾巴要么文件头长度和实际数据对不上。我早期图省事直接关中断结果每次录音结尾都少几十毫秒回放时最后半个字总是听不清。从那以后我每次做录音收尾都强制走一遍“停触发、等传输、刷缓冲、改头”四步再没出过截尾的问题。希望帮到你。本文还有配套的精品资源点击获取