
简介一套亲测可用的STM32录音机完整实验工程源于“实验45 录音机实验”面向嵌入式初学者与进阶开发者针对性解决音频采集、编码、存储及文件系统集成等实际问题完整覆盖从ADC采样、内存缓冲到SD卡写入的录音链路。资源共160个文件压缩包仅845KB以68个.h头文件和65个.c源码文件为主支撑HAL库驱动、FATFS文件系统与JPEG编码等核心模块另含Keil工程文件、启动文件、hex固件、PNG原理图截图及TXT说明文档方便直接打开编译、烧录与对照学习。当前已有1860人学习下载适合需要实际运行录音机程序、参考完整工程结构或用于课程设计与毕业设计的开发者。包内附编译好的固件与操作说明可结合源码逐行理解ADC采样率配置、环形缓冲管理、文件写入机制以及按键录音、停止、播放的控制逻辑快速上手STM32录音功能的开发与调试。1. 把 STM32 折腾成录音机真正难的是数据不丢帧把 STM32 折腾成一台能随身带、随手录的录音机比想象中麻烦。最难的其实不是采集而是怎么让声音数据在 SD 卡写满之前不丢帧。这套亲测可用的录音机程序用 I2S 接口接数字麦克风DMA 把音频流送进内存再通过 FATFS 落盘为 16bit/16kHz 标准 WAV 文件录音、停录、自动分段都齐了。适合电子竞赛作品、嵌入式课程设计或者任何需要离线语音采集的场合。整个工程代码量不大但把数据链路、文件系统、异常处理都走通了一遍值得照着跑一遍再改。2. 整体架构与硬件选型从 MEMS 麦克风到 SD 卡的完整数据链路2.1 硬件架构三件套如何协作这套系统采用最常见的录音硬件三件套数字 MEMS 麦克风、主控芯片、MicroSD 卡模块。主控用的是带 I2S 外设的 STM32F4 系列麦克风是 INMP441 这类 I2S 输出的数字麦克风——它内部自带 ADC直接把模拟声音转成 PDM/I2S 数字信号送到 MCU省掉了一级运放和模拟电路。SD 卡走 SDIO 模式读写速度能到几 MB/s16kHz 单声道 16bit 的码率只有 32KB/s富余量非常大。数据链路是这样的麦克风的 DOUT 引脚按位输出采样数据I2S 外设负责把位流按帧收进来DMA 在每次收到半缓冲区数据时触发中断主循环把这块数据搬运到一个应用层缓冲区攒满一页 512 字节就通过 FATFS 写进 SD 卡。整条链路里没有 CPU 逐字节搬运的环节所以 CPU 占用很低录音过程中跑个 OLED 刷新或者按键扫描都不影响。MEMS麦克风(INMP441) | I2S(BCLK/WS/DOUT) STM32(I2S2 DMA1_Stream3) | DMA双缓冲 - SRAM 应用层环形缓冲 | FATFS 文件写入 MicroSD(SDIO 4bit)这里有个选型要点如果用的是不带 I2S 的芯片退而求其次用 ADC 采样也没问题但录音质量会差不少——ADC 的采样率和精度受限于 PCLK 分频和参考电压稳定性而数字麦克风把时钟和量化都封闭在芯片内部抗干扰明显更好。我一般建议新手直接上 I2S 方案代码复杂度没高多少音质上限却高一个档次。2.2 工程目录结构驱动层与业务层分离工程采用标准库 分层结构驱动、中间件、应用层分开这样后面换屏、换按键、加编码器都不需要动音频核心逻辑。Record/ ├── Core/ │ ├── main.c // 主循环与状态机 │ ├── stm32f4xx_it.c // I2S DMA 中断、SDIO 中断 │ └── system_stm32f4xx.c ├── Drivers/ │ ├── BSP/ │ │ ├── bsp_i2s.c // I2S 引脚与 DMA 初始化 │ │ ├── bsp_sdio_sd.c // SD 卡底层驱动 │ │ └── bsp_led_key.c // 按键与 LED │ └── Middlewares/ │ ├── fatfs/ // FATFS 源码 │ └── wav_encoder.c // WAV 头写入与文件管理 ├── App/ │ ├── recorder.c // 录音状态机、缓冲管理 │ ├── vad.c // 静音检测 │ └── file_mgr.c // 文件名递增与空间检查 └── MDK-ARM/一个容易被忽略的设计细节wav_encoder.c被单独拆出来是因为 WAV 文件头需要在录音结束后回填文件大小字段这个动作涉及文件偏移写入和状态机耦合在一起容易越改越乱。拆开之后录音逻辑只管往文件里写数据头文件处理在停止录音时统一做一次。2.3 录音状态机空闲、录音、停止三态状态机放在recorder.c里用枚举定义三个状态REC_IDLE、REC_RUNNING、REC_STOP。按键短按触发启动/停止长按则作废当前录音文件。实际运行中还有一个隐性状态叫REC_PENDING——按键已经按下但文件还没创建成功此时 LED 快速闪烁提示用户别动。很多刚接触的朋友会把状态机写成 flag 变量is_recording 1 / 0到处判断。这个项目用函数指针表做状态迁移每个状态下能响应的事件都不一样避免出现“停止状态下还往文件里写数据”这种逻辑漏洞。状态表本身很简单但结构清晰后面加暂停、加声控录音都能直接扩展。typedef enum { REC_IDLE, REC_PENDING, REC_RUNNING, REC_STOP } RecState; static RecState g_state REC_IDLE; void recorder_handle_key(void) { switch (g_state) { case REC_IDLE: state_to_pending(); break; case REC_RUNNING: state_to_stop(); break; default: break; // PENDING 状态下忽略按键 } }代码逻辑很直白空闲时按一下进入 PENDING录音中按一下进入 STOPPENDING 阶段按键无效。注意 PENDING 状态的存在——创建文件、写 WAV 头需要几十毫秒如果此时再来一次按键程序不会把半初始化的文件暴露出来。这个设计对用户体感影响很大实际录制时你按完键要等半秒再说话就是因为文件系统在准备。3. 文件层实现WAV 头字节级构造与 FATFS 长文件名3.1 WAV 头结构44 字节不能错WAV 文件本质是 RIFF 格式头部固定 44 字节前面 12 字节描述文件类型后面分为 fmt 块和 data 块。我最开始图省事把块大小字段乱填结果录音文件拷到电脑上能播但放进剪辑软件就识别不了后来才发现是byteRate算错了。typedef struct { char chunkID[4]; // RIFF uint32_t chunkSize; // 36 dataSize录音结束后回填 char format[4]; // WAVE char subchunk1ID[4]; // fmt uint32_t subchunk1Size; // 16PCM 无压缩 uint16_t audioFormat; // 1 表示 PCM uint16_t numChannels; // 1 单声道 uint32_t sampleRate; // 16000 uint32_t byteRate; // sampleRate * blockAlign uint16_t blockAlign; // numChannels * bitsPerSample / 8 uint16_t bitsPerSample; // 16 char subchunk2ID[4]; // data uint32_t subchunk2Size; // 实际音频字节数 } WAVHeader;字段含义有必要逐一说清chunkSize等于 36 加上 data 区字节数它描述的是从format字段之后到文件末尾的总长度byteRate是每秒的字节数播放器靠它计算播放进度blockAlign是单帧字节数对单声道 16bit 来说是 2。这几个字段互相关联最容易出错的是chunkSize忘记了包含 fmt 块那 24 字节。写入策略是创建文件时先把 44 字节头全部写入其中subchunk2Size暂填 0录音过程中f_write返回的累计字节数就是 data 区大小停止录音时用f_lseek回到文件头填好chunkSize和subchunk2Size再f_sync刷盘。这样即使录音中途断电文件头虽然不完整但 data 区的 PCM 数据全在用十六进制工具还能把裸数据抢救出来。3.2 FATFS 挂载与文件名递增SD 卡要先挂载文件系统才能操作FATFS 的挂载代码看起来短但里面有个坑f_mount第一个参数传 NULL 可以做卸载第二个参数opt传 1 表示立即挂载如果 SD 卡没插好这里会返回FR_NOT_READY。常见做法是轮询几次每次间隔 100ms给 SD 卡上电稳定留出时间。FATFS g_fs; FRESULT res; res f_mount(g_fs, 0:, 1); if (res ! FR_OK) { // 挂载失败LED 双闪提示 } // 生成形如 R0001.WAV 的文件名 uint16_t idx 1; FILINFO fno; char path[16]; do { sprintf(path, 0:R%04d.WAV, idx); res f_stat(path, fno); } while (res FR_OK); // 文件已存在则序号加一文件名扫描这段值得讲一下f_stat返回FR_OK表示文件存在返回FR_NO_FILE表示没有这个文件。所以循环条件是res FR_OK就一直递增序号直到找到不存在的编号。这套逻辑简单可靠但如果卡里文件超过几千个每次启动扫描会变慢实际项目中可以加一个索引文件缓存上次的序号启动时读出来 1 即可。注意 FATFS 默认配置FF_USE_LFN是 0也就是不支持长文件名文件名最长 8.3 格式。R0001.WAV这种名字恰好满足但如果你想改成record_20250412_0930.wav必须在ffconf.h里把FF_USE_LFN改为 1且工程要启用ff_convert相关源码否则f_open会直接返回FR_INVALID_NAME。3.3 分块写入与缓存刷盘策略SD 卡按扇区读写单次写 512 字节效率最高1 字节零散写入会让文件系统层疯狂做读-改-写录音产生爆音往往就是这个原因。工程里应用层维护一个 4KB 缓冲攒满 8 个扇区才调一次f_write。#define SECTOR_SIZE 512 #define WRITE_BATCH 8 // 一次写入 8 个扇区 static uint8_t s_cache[WRITE_BATCH * SECTOR_SIZE]; static uint16_t s_cache_len 0; void file_write_audio(const uint8_t *pcm, uint16_t len) { memcpy(s_cache s_cache_len, pcm, len); s_cache_len len; if (s_cache_len sizeof(s_cache)) { UINT wrote; f_write(g_file, s_cache, s_cache_len, wrote); s_cache_len 0; } }f_write的第四个参数返回实际写入字节数不能忽略。SD 卡写满或者文件系统出错时wrote会小于请求长度如果代码不检查后续数据会错位产生的 WAV 文件就是前半段正常后半段全是杂音。另一个关键点是f_sync它的作用是把 FATFS 内部缓冲刷到 SD 卡录音停止时调用一次这个项目实测下来不调f_sync直接f_close偶尔会丢最后几 KB 数据表现为录音尾部声音突然中断。4. 核心代码拆解I2S DMA 双缓冲与静音自动分段4.1 I2S DMA 双缓冲配置I2S 接收配置是整套程序的地基。既然是接收I2S_Mode要设成I2S_Mode_Rx数据格式 16bit 标准 Philips 模式。重点在 DMA启用DMA_Mode_Circular循环模式同时配置双缓冲让 DMA 在内存 A 和内存 B 之间交替填充这样 CPU 处理 A 的时候 DMA 正在填 B两边不打架。void bsp_i2s_dma_init(uint16_t *buf_a, uint16_t *buf_b, uint16_t half_len) { DMA_InitTypeDef dma; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); DMA_DeInit(DMA1_Stream3); dma.DMA_Channel DMA_Channel_5; // I2S2_RX 映射 dma.DMA_PeripheralBaseAddr (uint32_t)I2S2-DR; // 外设地址固定 dma.DMA_Memory0BaseAddr (uint32_t)buf_a; dma.DMA_DIR DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize half_len; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不自增 dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Stream3, dma); DMA_DoubleBufferModeCmd(DMA1_Stream3, ENABLE); DMA_MemoryTargetConfig(DMA1_Stream3, (uint32_t)buf_b, DMA_Memory_1); DMA_ITConfig(DMA1_Stream3, DMA_IT_TC, ENABLE); }这里几个参数解释一下外设地址必须指向 I2S2 的数据寄存器I2S2-DRDMA 从该地址读到的就是 I2S 外设已经拼好的 16bit 采样值PeripheralDataSize和MemoryDataSize都是半字16bit跟麦克风输出位宽一致BufferSize是每个缓冲区的半字数。整个配置里最容易错的是 DMA 通道号与芯片型号强相关F407 的 I2S2_RX 在 DMA1_Stream3 的 Channel_5换成 F429 或 F103 都不一定相同必须查对应的 datasheet 而不是照抄。4.2 中断回调与数据搬移DMA 传输完成中断是数据链路的“节拍器”。每填满一个半缓冲区就进入一次中断此时要判断刚填满的是 A 还是 B然后把它交给应用层处理。void DMA1_Stream3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream3, DMA_IT_TCIF3)) { uint32_t cur DMA_GetCurrentMemoryTarget(DMA1_Stream3); if (cur (uint32_t)DMA_Memory_0) { // 当前目标是 A说明 B 刚被填满 recorder_feed(rec_buf_b, HALF_BUF_SIZE); } else { recorder_feed(rec_buf_a, HALF_BUF_SIZE); } DMA_ClearITPendingBit(DMA1_Stream3, DMA_IT_TCIF3); } }这段逻辑第一次看容易被绕进去我拆开讲DMA 双缓冲模式下DMA_GetCurrentMemoryTarget返回的是下一轮要写入的目标不是刚写完的目标。所以当返回值是DMA_Memory_0说明刚填满的是 B应处理 B反过来返回值是DMA_Memory_1就处理 A。这个对应关系写反了不会立刻崩溃但录音所有数据会整整慢一个半缓冲周期声音听起来像隔了一层水。中断里只做数据搬运和相关计算不直接调f_write。原因很简单f_write涉及 SD 卡 SPI/SDIO 通信耗时可能几毫秒在中断里调用会阻塞后续 DMA 中断造成采样点丢失。实际项目中我在recorder_feed里只做数据拷贝到应用缓冲然后置一个标志位主循环轮询到这个标志才刷盘。4.3 静音检测与自动分段这个项目比普通录音机多一个实用功能静音检测。它的场景是录讲座、采访总有大段没人说话的空白自动把静音段切掉回放时不用手动拖进度条。检测算法用 RMS均方根能量每处理一个半缓冲区算一次低于阈值的连续帧数达到上限就触发分段。#define SILENCE_THRESHOLD 300 // 静音阈值采集值平方的均值开根 #define SILENCE_MAX_FRAMES 120 // 连续 120 帧静音约 1.5 秒则分段 static uint16_t g_silence_cnt 0; void vad_process(const int16_t *pcm, uint16_t len) { uint64_t sum 0; for (uint16_t i 0; i len; i) { sum (uint32_t)(pcm[i] * pcm[i]); } uint32_t rms (uint32_t)sqrt(sum / len); if (rms SILENCE_THRESHOLD) { g_silence_cnt; if (g_silence_cnt SILENCE_MAX_FRAMES) { recorder_split_file(); // 关闭当前文件开启新文件 g_silence_cnt 0; } } else { g_silence_cnt 0; } }两个参数需要按实际环境调SILENCE_THRESHOLD的绝对值取决于麦克风增益和录音环境底噪安静屋子里阈值 300 合适到了机房这种地方背景噪音的 RMS 可能直接超过 500导致永远不触发分段。我的习惯是录音开始的前 2 秒不做检测采集这 2 秒的 RMS 平均值再乘 2 作为自动阈值这样换环境不用重新编译。SILENCE_MAX_FRAMES是连续静音帧数1.5 秒这个值试听下来比较自然太短会把正常停顿也切掉太长又失去分段意义。分段动作recorder_split_file内部做的事情是先把当前文件的 WAV 头回填完整f_close然后再走一次创建文件的流程生成新的文件名。注意此时要重新写一遍 WAV 头因为新文件是一个全新的 44 字节头开始。5. 避坑手记亲测录音程序时最容易翻车的 5 个问题5.1 录音文件全是杂音数据看起来是乱的现象录完的文件播放出来是刺耳的嘶嘶声用十六进制工具看 PCM 数据数值在满幅之间跳变明显不是正常语音波形。原因麦克风和主控之间的 I2S 位时钟和数据线没接对。INMP441 这类麦克风的 DOUT 是在 BCLK 下降沿输出数据而 STM32 的 I2S 外设默认在上升沿采样两边相位不一致采到的全是错位数据。解决在I2S_InitStructure里把I2S_CPOL从默认的I2S_CPOL_Low改为I2S_CPOL_High翻转一下采样时钟沿。改完再用示波器量 BCLK 和 DOUT 的时序关系确认数据稳定落在采样窗口中间。提示不要盲目调I2S_CPOL。如果用的是带 MCLK 输出的麦克风还要检查主时钟是否在 256 倍采样率附近偏离太多会导致内部 PLL 失锁同样会有杂音。5.2 录音正常但文件大小永远是 0现象按键流程都对LED 也正常闪烁录音结束后 SD 卡里出现一个 WAV 文件但大小是 44 字节——只有文件头没有任何音频数据。原因数据卡在缓存层。主循环轮询的标志位没有置起来recorder_feed里的数据一直堆在内存缓冲区到停止录音时f_close把未写完的缓存丢弃了。根源是 DMA 中断里置标志和主循环清标志的临界区处理不当主循环在标志置位之前以为没有新数据。解决把“中断置标志、主循环清标志”改成“主循环比较缓冲写索引”中断里更新g_write_idx主循环对比g_process_idx两者不等就处理中间的区间。这样不依赖 volatile 标志的置位顺序也天然支持处理不完时缓存积压。5.3 录音时长比实际多了或少了回放音调不对现象录了 10 秒的声音播放器显示 13.2 秒播放速度明显变慢男声变成了低沉版。原因I2S 采样率的实际值跟 STM32 系统时钟配置对不上。F4 的 I2S 时钟来自 PLLI2SR这个分频值和库函数里I2S_AudioFreq_16k的期望值不一致时外设会就近取一个频率实际采样率偏高或偏低文件头里写的 16000 却是死的于是播放器按 16kHz 播一个实际为 13kHz 采样率录出来的数据声音就变慢了。解决录音前用计数函数验证实际采样率。把 GPIO 翻转频率等于采样率的波形用频率计测同时对照RCC_PLLI2SConfig的分频参数计算理论值。我之前踩过的是外部晶振 25MHz 的板子库函数默认按 8MHz 计算分频差出一大截改成RCC_PLLI2SConfig(192, 4, 2)这类匹配晶振的参数才正常。5.4 写入 SD 卡过程中掉电卡里出现打不开的文件现象录音时直接拔电源重新上电后某个 WAV 文件打不开甚至f_mount返回错误卡在电脑上也提示文件系统损坏。原因FATFS 内部有目录项缓存写入大块数据时文件系统元数据是延后落盘的掉电导致目录项和实际数据不一致。另一个因素是文件写入过程中突然断开FAT 表条目成了悬空链表FATFS 扫描到坏链后可能直接卡死。解决应用层定时刷盘我设计为每写 4KB 调一次f_sync这样最坏情况只丢最后 4KB 数据文件系统的一致性由 FATFS 的写日志保证。硬件上增加一个掉电检测中断用单片机的 VBAT 引脚在电压跌到 3.0V 时触发中断里放弃写缓存、直接刷盘同时板子加一个大电容维持几十毫秒电足够完成收尾。5.5 长时间录音后系统卡死复位才能恢复现象连续录半小时以上偶尔出现录音中断、文件损坏、整机死机复位后长时间内不再复现。原因SD 卡在长时间写入时出现坏块或者电压跌落SDIO 控制器进入忙状态f_write阻塞在等待 SD 卡响应的循环里。另一个隐蔽原因是 DMA 中断里处理数据耗时太长导致 DMA 传输溢出中断没有被及时服务缓冲区索引错乱。解决给 SDIO 底层加上超时机制while等待SD_WaitFlag的时候计数超过 1 秒直接返回错误由上层做一次重试。DMA 溢出则开启DMA_IT_FE错误中断溢出时丢弃整个半缓冲并复位索引保证后续数据从干净位置开始。6. 二次开发加采样率切换、LED 指示与循环录音拿到源码别急着改先把main.c里的状态机和recorder.c的接口弄清楚改造基本都在应用层完成不用动 DMA 和 FATFS 底层。下面是三个我建议优先尝试的改动方向。6.1 按键切换采样率把采样率从固定的 16kHz 改成 8k/16k/22.05k 三档切换核心是改两处一是调I2S_InitStructure里的I2S_AudioFreq二是重算 WAV 头的byteRate。注意要在关闭 I2S 和 DMA 的状态下改参数否则外设正在运行时分频突变会产生一段垃圾数据。void recorder_set_sample_rate(uint32_t rate) { recorder_stop(); // 先安全停止当前录音 bsp_i2s_set_freq(rate); wav_set_sample_rate(rate); // 更新 WAV 头填充参数 }改完试听一下 8kHz 音质语音可懂没问题但乐器录音的高频滚降明显且静音检测阈值要相应调低——采样率减半后同等声音的 RMS 值会变化。6.2 LED 状态指示多色 LED 简单映射状态机空闲常亮绿、录音中每秒闪烁、缓冲满时快速闪烁表示写入中、SD 卡错误红灯快闪。这个改动用不上 RTOS直接在状态迁移函数里预留的recorder_event_callback钩子里实现即可。放一个回调示例状态迁移后触发事件LED 控制逻辑集中在bsp_indicator_update一个函数比在状态机的 case 里到处写GPIO_SetBits干净得多。6.3 循环录音与存储保护循环录音是既能练手又实用的功能设一个最大文件数录满后自动删除最旧的文件保证新录音永远有空间。需要把启动时的文件名扫描改成完整遍历把所有R*.WAV的文件序号存起来删除时取最小值对应文件。删除操作前先f_stat确认文件存在避免删到一半文件已被手动清理。我做这类嵌入式项目养成的习惯是每一次改动后把录音文件拷到电脑上用 Audacity 看波形确认没有毛刺和丢帧再提交代码。特别是改完 DMA 缓冲长度后一定要连续录音半小时以上模拟真实使用的长时场景。希望这次拆解能帮到你至少在你会不会也踩一遍爆音和文件损坏的坑这件事上少一点玄学。本文还有配套的精品资源点击获取