ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32录音机源码解析:从ADC采样到WAV存储的完整实现

STM32录音机源码解析:从ADC采样到WAV存储的完整实现 简介这是一份基于STM32的录音机完整设计源码面向嵌入式开发初学者和想深入理解音频采集链路的工程师可用于课程设计、项目参考或自学实践。压缩包共249个文件包含C语言源码与头文件、Keil工程文件、hex固件、原理图与硬件布局图、操作演示视频及多个语言编码模块整体约47.25MB。源码覆盖系统初始化、GPIO与ADC配置、DMA音频数据搬运、PCM/AAC编码、FATFS文件系统、按键与屏幕交互、中断服务与FreeRTOS任务调度等关键模块并带有工程备份和编译中间文件方便比对不同阶段的构建结果。已有570人学习下载是理解STM32从硬件寄存器到上层录音应用的综合性实战资源。1. 拿到STM32录音机设计源码先别急着编译如果你下载过“基于STM32录音机设计源码.zip”这类压缩包大概率会发现里面是一堆HAL库工程文件、随后是ADC、DMA、FATFS、WAV处理等代码。很多人第一反应是找zip压缩包密码破解工具或者直接解压到Keil里编译但常踩的坑是编译通过却录不出声音或者录出来的WAV文件播放器打不开。问题的根源不在于源码本身而在于没把录音机这条数据链路先理清楚。STM32录音机设计的核心不是麦克风而是采样率控制、DMA搬运和文件系统写入这三件事的配合。本文会从源码结构出发讲清楚这套设计应该怎么看、怎么改、怎么调顺便把采样率、存储格式、文件大小计算、SD卡写入对齐这些参数都落到实处。适合正在做基于STM32的毕业设计、嵌入式音频数据采集以及想把录音功能集成进现有嵌入式工程的人。2. STM32录音机源码的数据链路ADC采样、DMA搬运与WAV头写入2.1 为什么录音机源码离不开DMA和环形缓冲STM32内置的ADC模块负责把麦克风输出的模拟电压转换成数字值但ADC有一个硬件限制单次转换结束需要CPU去读数据寄存器。如果录音采样率是16kHz每采样点间隔62.5微秒CPU在几十微秒内去读一次还能接受但一旦工程里还要处理LCD刷新、按键扫描或者SD卡写入CPU响应不及时就会出现掉码。所以真正能用的STM32录音机源码一定不会让CPU逐个读ADC而是用DMA把ADC转换结果连续搬运到内存缓冲区。常见做法是开两个等长缓冲区用ADCDMA的半传输和传输完成中断轮流填充。ADC连续采样时DMA先填满第一个半缓冲区触发半传输中断CPU去把这一半数据写入SD卡或暂存DMA继续填第二个半缓冲区填满后触发传输完成中断CPU再去处理。这样采集和写入在时间上交错开录音不会因为文件系统写入的阻塞而断流。// 伪代码示意双缓冲切换逻辑 uint16_t rec_buf[2][1024]; // 两个1024采样点缓冲区 volatile uint8_t buf_idx 0; // 当前待处理缓冲区索引 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { process_audio_data(rec_buf[buf_idx], 1024); buf_idx (buf_idx 1) % 2; // 切换缓冲区 } }这段代码里的process_audio_data就是后面要写的WAV数据写入函数。注意这里的逻辑说明rec_buf大小取决于DMA配置如果配置的是DMA_NORMAL模式回调触发后必须手动重启ADC采集才能继续如果用DMA_CIRCULAR模式加双缓冲中断则不需要重启但必须保证处理数据的时间小于一个缓冲区填满的时间。1024个16kHz采样点对应约64ms留给你做SD卡写入的时间窗口其实很充裕但文件系统的擦写操作偶尔会超过这个时间所以源码里一般会把process_audio_data放进FATFS的异步写入或者用队列缓存典型解法。2.2 在源码里找WAV文件头的填充逻辑ADC出来的原始PCM数据不能直接播放绝大多数录音机源码都会封装成WAV格式因为这个格式只需要在文件开头写一个44字节的文件头后面原样塞PCM数据就行最省事。源码里你一定会看到一个类似wav_header_init的函数它就是填这个44字节头。很多人录完音文件头是坏的就是因为只在录音开始时写了头部而没有在停止录音时再回写一次文件大小字段。typedef struct { char riff[4]; // RIFF uint32_t file_size; // 文件总字节数-8 char wave[4]; // WAVE char fmt[4]; // fmt uint32_t fmt_len; // 16 uint16_t audio_fmt; // 1-PCM uint16_t ch_num; // 声道数 uint32_t sample_rate;// 采样率 uint32_t byte_rate; // 采样率*声道*位深/8 uint16_t block_align; // 声道*位深/8 uint16_t bits_sample; // 位深 char data[4]; // data uint32_t data_len; // PCM数据长度 } wav_header_t; void wav_header_fill(wav_header_t *hdr, uint32_t sample_rate, uint16_t bits, uint16_t ch) { memset(hdr, 0, sizeof(wav_header_t)); memcpy(hdr-riff, RIFF, 4); memcpy(hdr-wave, WAVE, 4); memcpy(hdr-fmt, fmt , 4); hdr-fmt_len 16; hdr-audio_fmt 1; hdr-ch_num ch; hdr-sample_rate sample_rate; hdr-bits_sample bits; hdr-block_align ch * bits / 8; hdr-byte_rate sample_rate * hdr-block_align; memcpy(hdr-data, data, 4); // file_size和data_len先填0停止录音时再回写 }参数说明如下byte_rate的单元是字节每秒WAV播放器用它配合data_len来算播放时长如果sample_rate和实际ADC采样时钟不一致播放速度就会偏高或偏低这是源码里最隐蔽的“录音时长对不上”的原因。录音停止时必须用文件末尾位置减8得到file_size减文件头44字节得到data_len然后重新f_lseek到文件开头覆盖写入一次。很多开发者在写这段时直接把文件头写在SD卡然后又用f_open的追加模式写PCM结果头被PCM数据覆盖播放器直接报错。2.3 先从源码里确认采样率与时钟配置录音机的采样率不是代码里写多少就是多少它由ADC时钟和采样周期共同决定。STM32的ADC时钟来自APB2但APB2预分频后必须让ADCCLK落在约0.6MHz到36MHz之间不同系列不完全一样。在源码里找RCC_GetClocksFreq相关调用或者直接看CubeMX生成的HAL_RCC_GetClockConfig确认APB2频率再看ADC配置的ClockPrescaler和SamplingPeriod。常见的组合系统时钟72MHzAPB272MHzADC预分频6得到12MHz ADC时钟采样时间1.5周期加上12.5周期固定采样时间总采样周期14周期则ADC采样率约12MHz/14≈857kHz。注意这是单次转换能跑的上限实际录音采样率还会受定时器触发控制。源里如果用定时器触发ADC那就是把定时器频率当采样率和上面说的ADC转换时间无关。这块最容易把人搞晕。我的经验是先在数据手册里查一下芯片的ADC采样时间公式再回源码里核对定时器分频系数同时把晶振电容计算那套请放一边因为录音机对频偏敏感的是音频采样率不是系统时钟本身。采样率定时器时钟预分频PSC自动重载ARR说明8kHz72MHz8999电话语音质量16kHz72MHz4499语音识别常用44.1kHz72MHz1629CD音质频繁中断这张表是定时器触发ADC的典型参数其中定时器实际频率等于时钟/(PSC1)/(ARR1)。如果源码用的是16kHz但你录音文件播放听起来变快或变慢先拿示波器量定时器输出脚或者用TIM_GetCounter验证频率而不是一上来就改WAV头的采样率字段。3. 把STM32录音机源码编译烧录环境配置和参数调整3.1 用STM32CubeMX重新生成初始化代码而不覆盖用户代码拿到一个来自zip的源码工程第一件事不是全部替换成自己的库版本而是用STM32CubeMX打开工程里的.ioc文件确认芯片型号和外设配置。如果源码作者用的芯片型号和你手上的板子不一样比如源码是STM32F103C8T6你的是STM32F103RCT6CubeMX会提示换芯片。这里注意stm32 cube 程序更改单片机型号可以直接在CubeMX里的Project - Settings改但改完以后引脚分配可能不兼容最好逐个核对。CubeMX重新生成代码的坑在于它会覆盖main.c里的用户代码除非你把自定义代码放在USER CODE BEGIN和USER CODE END之间。源码包里如果作者的录音逻辑写在while(1)里而你用CubeMX重新生成这部分逻辑会被抹掉。常见做法是生成前先备份原始工程生成后拿Beyond Compare或diff对比main.c把录音状态机迁移到新工程的用户代码区。3.2 Keil工程下的三个必调参数打开.uvprojx工程文件后在Options for Target里必须检查三个参数否则录音机要么死机要么爆噪。第一个是Target - Code Generation - ARM Compiler版本。HAL库和旧版固件库对编译器版本要求不同如果你看到一堆#error或者__STATIC_INLINE相关的编译错误大概率是编译器版本太高。我一般会把编译器降级到与工程作者一致的版本或者在源码里把宏定义修正为兼容写法而不是直接升级编译器。第二个是C/C - Optimize。调试阶段建议选Level 0 (-O0)因为ADC中断里如果开了优化局部变量可能被优化掉导致处理缓冲区时索引错乱。生产再改-O2也不迟。注意有些源码用了volatile但没对齐-O2下会插入意外的乱序执行录音数据偶尔会有毛刺。第三个是Target - Read/Write Memory Areas里的IRAM大小。如果堆栈设置过小FATFS写入时会出现HardFault。尤其当源码用f_printf打印调试日志时栈占用会显著增大。建议把Stack Size从默认0x400改成0x800Heap保持0x200即可。同时确认分散加载文件里有没有把SRAM分成多个区ADC缓冲区最好放在连续内存中跨区会导致DMA传输不完整。// 在main.c中设置DMA缓冲区注意16位对齐 __attribute__((aligned(4))) uint16_t dma_buf[4096];这里的aligned(4)是关键。STM32的DMA支持字和半字传输如果缓冲区地址不是2的倍数半字传输可能错位对齐到4可以避免缓存一致性问题。很多源码里缓冲区定义成普通数组编译后落在地址0x20000000之后没对齐录音数据前几秒正常后面全乱。把数组改成上面这种写法问题立刻消失。另外如果芯片有D-Cache比如F4系列的部分型号DMA缓冲区需要配置成MPU_Region_2GB非缓存属性否则CPU读到的数据和DMA写入数据不一致。3.3 烧录与调试ST-Link Utility和串口打印烧录用STM32 ST-LINK Utility或者新版CubeProgrammer都行。命令行烧录比较适合自动化比如用ST-LINK_CLI.exeST-LINK_CLI.exe -p build\recorder.hex -Rst -Run这条命令把编译好的hex文件烧进去然后复位并运行。烧录前确认Settings - Debug - Connect under Reset是否勾选因为如果源码里把JTAG引脚复用了直接连接会失败。如果烧录后芯片不跑先按复位键再看Debug (printf) Viewer有没有输出。源码里常见printf重定向到UART1可以接一个USB转TTL看输出。注意波特率和源码里_RHEA_UART配置一致通常是115200或921600。看到ADC启动后的首行日志说明DMA和定时器已经正常工作。4. 录音机源码里最容易被忽视的边界存储、电量与格式4.1 SD卡读写与FATFS批处理录音机源码的存储部分通常使用SPI接口或SDIO接口驱动SD卡再挂FATFS文件系统。SPI模式接线少但速度慢16kHz、16位单声道数据量是32KB/sSPI在4分频下也能满足SDIO模式适合高采样率或双声道但引脚冲突多源码调试难度也大。很多源码作者会在README里写明用的是SPI还是SDIO如果是SPI检查有没有提高SD卡分频系数到正确值。对于STM32F1系列SPI时钟最高18MHzPCLK2/2如果直接按初始化时的高频跑部分SD卡会不稳定。FATFS写入要养成检查返回值的习惯因为SD卡偶尔出现写入超时。源码里如果看到这样一段说明作者已经处理了写入重试FRESULT res f_write(fil, pcm_buf, write_len, br); if (res ! FR_OK || br ! write_len) { f_close(fil); record_error_cnt; }这里有几个要点。write_len最好是512的整数倍。SD卡一个扇区是512字节FATFS底层写操作如果数据量不是扇区对齐会先做读-改-写增加了耗时且容易受到掉电影响。充一次电录好几个小时中途突然写失败多半就是跨扇区写入导致写入放大。我自己的习惯是把DMA缓冲区大小设为1024 * sizeof(uint16_t)这样每次写入2KB完美对齐扇区。另外f_write的br实际写入字节数必须和write_len比较不能只看FR_OK。有些廉价SD卡在没写完时也返回FR_OK导致文件头回写时长度不对。4.2 录音时长计算与文件大小限制FATFS默认支持的单个文件最大大小是4GBFAT32限制但在STM32上4GB录音根本不现实。实际限制来自内部缓冲区、文件系统簇大小和内存卡速度。以16kHz、16位单声道为例每秒产生32KB数据一分钟1.92MB一小时115.2MB。一张32GB的卡理论上能录278小时但FATFS的f_write失败概率会随时间增长尤其当SD卡用了大量FLASH磨损均衡时写入延迟会突然飙升导致录音断流。采样率(kHz)位深(bit)声道数每小时文件大小建议单文件时长816157.6 MB4小时16161115.2 MB2小时48162691.2 MB20分钟482421.04 GB10分钟上表是估算值实际加上WAV头和管理信息略有出入。设计录音机时通常会设置一个最大录音时长到点自动分段保存。注意分段时不要急着重开文件先f_sync把当前文件刷新到SD卡再关闭文件创建新文件。f_sync是很多源码里漏掉的关键操作不调用它的话断电后最近几十秒的数据可能丢失。4.3 处理进度与异常恢复录音过程中用户可能直接拔卡、突然断电源码里处理不当会导致整个FATFS损坏。成熟的录音机设计会在录音前把SD卡去挂载录音结束后重新挂载并且关键节点后写一个状态文件比如rec.info记录当前录音时间和文件序号。下次上电开机时先读取这个文件如果发现上次录音异常中断可以选择修复WAV头或者丢弃尾部不完整的数据。异常恢复的另一个细节是ADC和定时器的停止顺序。停止录音时如果先停ADC再停DMA会出现残留数据没搬完导致最后一次写入长度不足512。正确顺序是先HAL_TIM_PWM_Stop停止触发源再HAL_ADC_Stop_DMA停止DMA然后检查DMA剩余数据量__HAL_DMA_GET_COUNTER把残余数据处理掉再执行f_sync。比如HAL_TIM_Base_Stop_IT(htim3); HAL_ADC_Stop_DMA(hadc1); uint32_t remain __HAL_DMA_GET_COUNTER(hadc1.DMA_Handle); uint32_t processed bufsize - remain; if (processed 0) { f_write(fil, temp_buf, processed, br); } f_sync(fil); f_close(fil);这里processed的单位是16位采样点个数写入字节数要乘以2。DMA计数器是剩余个数不是剩余字节单位搞错会把WAV写入长度算错导致播放时卡顿。5. 在源码基础上做性能校准用真实文件验证录音质量5.1 用FFT和波形查看器验证采样率漂移录音源码跑通只是第一步录出来的WAV文件还要验证采样率是否准确、数据是否连续。推荐用Python的wave模块读文件再用scipy.fft看频谱。如果录制1kHz正弦波但FFT峰值出现在990Hz或1010Hz说明实际采样率与写入WAV头里的采样率不一致。这时候直接改WAV头里的sample_rate字段没用必须回到底层调整定时器预分频。import wave import numpy as np import scipy.signal as sig with wave.open(test.wav, rb) as w: sr w.getframerate() data w.readframes(w.getnframes()) pcm np.frombuffer(data, dtypenp.int16) f, t, Zxx sig.stft(pcm, fssr, nperseg1024) # 找到峰值频率和输入信号频率对比代码里sr是WAV头记录的采样率如果和实际ADC采样率有偏差时频谱上整个信号频带会整体平移。我一般用已知频率的信号从Speaker输出接进录音机录30秒后做STFT观察频率误差是否小于1%。如果偏了就把定时器ARR加减一两个数直到FFT峰值落到正确频点。注意每次调整后要同步修改WAV头里的采样率字段因为播放器读的是头。5.2 增益调节和抗混叠滤波录音机源码的ADC采集范围默认是0到3.3V而麦克风经过放大后信号通常叠加一个1.65V直流偏压。如果你拿到源码之后直接用录出来的声音会很小原因是PCM数据没有做直流偏置校正也没有做增益放大。在源码里找类似(adc_val - 2048) * gain的语句这就是直流消除和增益控制。比如STM32F103的12位ADC中位值是2048adc_val - 2048后信号范围约-2048到2047乘以1.0时就是原声乘以2.0时音量提升6dB。但增益不能随便拉。如果输入信号过大adc_val会达到ADC满量程而削波产生刺耳噪点。解决方法是加一个限幅器或者在前级调整麦克风放大电路电阻。有人说“源码里已经加了抗混叠滤波”但大多数简易录音机设计只在ADC前端放了一个RC低通截止频率大概几十kHz对于24kHz采样率来说不够。如果你要正经录音至少用两阶RC低通截止频率设在采样频率的0.48倍左右。这部分改动通常不在源码里而是硬件原理图但源码里的数字滤波可以补救比如在写入WAV前跑一遍移动平均或一阶IIR高通。5.3 扩展添加I2S麦克风或WAV压缩当你想把录音机从“能出声”做到“接近消费级”最值得扩展的方向是把ADC采样换成I2S数字麦克风比如INMP441。I2S接口直接输出24位PCM数据省掉了模拟前级和抗混叠滤波电路而且STM32的I2S外设自带DMA搬运源码改动相对集中只需将原来HAL_ADC_Start_DMA换成HAL_I2S_Receive_DMA并将数据处理函数中的采样值从12位扩展到左对齐的24位。要注意I2S模式选择是I2S_MODE_MASTER_RX还是SLAVE_RX以及Standard是飞利浦还是MSB格式麦克风数据手册都会写清楚。如果源码里录音格式是WAV改成压缩格式需要引入音频编码库。常见做法是接一个小型音频编码器芯片如VS1053它自带MP3编码STM32只需要发命令和搬运PCM数据。这种方案适合想避开软件算法复杂度的场景副作用是硬件成本和PCB面积增加。不换芯片的话可以考虑用ADPCM压缩STM32源码里实现一个简单IMA-ADPCM编码器只需要不到200行代码能把数据量压缩到四分之一但WAV播放器需要支持ADPCM格式兼容性不如PCM。具体选哪种取决于你录音机源码面向的场景是语音回放还是长时间环境记录。本文还有配套的精品资源点击获取
返回列表