简介:这份资源面向具备一定嵌入式基础的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 的算法是:计数时钟 = 系统时钟 / (PSC+1),更新频率 = 计数时钟 / (ARR+1)。这里 72MHz / 72 = 1MHz,1MHz / 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 工程导入与编译
拿到工程先确认两件事:用的哪个 IDE(Keil 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。顺序错了要么丢尾巴,要么文件头长度和实际数据对不上。我早期图省事直接关中断,结果每次录音结尾都少几十毫秒,回放时最后半个字总是听不清。从那以后我每次做录音收尾都强制走一遍“停触发、等传输、刷缓冲、改头”四步,再没出过截尾的问题。希望帮到你。
本文还有配套的精品资源,点击获取