1. 项目概述与核心价值最近在整理工作室的物料翻出来好几块STM32F103C8T6的核心板还有之前做项目剩下的TFT屏和TF卡槽模块。看着这些零散的元件我就在想能不能用它们攒一个有点实用价值的小玩意儿正好手头缺一个便携的录音设备用来记录一些临时的会议要点或者灵感片段于是“基于STM32的录音笔”这个想法就冒出来了。这不仅仅是一个简单的数据采集存储它涉及到音频信号的模拟前端处理、单片机内部ADC的精准控制、文件系统的管理、大容量存储的读写以及最终的声音回放算是一个比较综合的嵌入式系统练手项目。这个系统设计的核心目标是打造一个功能完整、性能稳定的离线录音与回放设备。它需要能够通过麦克风采集环境声音将模拟的音频信号转换为数字数据然后以便于管理和播放的文件格式如WAV存储到TF卡中。同时通过一块TFT屏幕用户可以直观地看到录音状态、文件列表、播放进度等信息并通过按键进行录音、停止、播放、选择文件等操作。最终实现一个从“声波”到“数字文件”再到“声波”的完整闭环。无论是对于嵌入式入门者想挑战综合应用还是对于有经验的开发者需要一个快速原型验证工具这个项目都有不错的参考价值。2. 系统整体设计与核心思路拆解2.1 核心功能模块定义与选型考量一个完整的录音笔系统可以拆解为以下几个核心模块音频采集模块负责将声音的物理振动转换为电信号。这里我选择了一款常见的驻极体麦克风ECM模块。它内部集成了前置放大器输出的是模拟电压信号。选它的理由很简单成本低、易获取、接口简单VCC GND AUDIO_OUT。对于语音频段300Hz-3.4kHz的采集其性能完全足够。主控与处理模块这是系统的大脑。STM32F103C8T6也就是常说的“蓝桥杯”或“最小系统板”核心芯片是首选。它拥有72MHz的主频内置12位ADC以及足够的外设如I2S、SPI、定时器、DMA性价比极高。其ADC的采样率足以支持语音级别的音频数字化。存储模块用于保存录音文件。SD卡通过SPI模式连接或TF卡实质是微型SD卡是最佳选择。它们容量大从几百MB到几十GB、价格便宜、且可以通过文件系统如FATFS进行管理方便在电脑上直接读取录音文件。我选择了TF卡因为它更小巧更适合便携设备。显示与人机交互模块为了提供良好的用户体验一块TFT屏幕必不可少。我手头是一块1.44寸的SPI接口TFT屏驱动芯片通常是ST7735或ILI9341。它分辨率适中128x128或128x160刷新率对于显示菜单和进度条绰绰有余。交互方面采用几个独立的机械按键来实现“录音”、“播放/暂停”、“上一曲/下一曲”、“停止/退出”功能简单可靠。音频回放模块将存储的数字音频数据还原成声音。STM32F103的DAC精度和驱动能力有限直接推动耳机或扬声器效果不佳。因此我增加了一个专用的音频编解码芯片或一个简单的PWM驱动电路。为了追求更好的音质我选用了VS1053B芯片。它不仅能通过I2S从STM32接收PCM数据并解码播放还支持多种音频格式解码如MP3、WAV、OGG为后续功能扩展如播放MP3音乐留有余地。2.2 系统工作流程与数据流分析整个系统的工作流程围绕着两个核心状态展开录音和播放。录音流程用户按下“录音”键系统在TF卡根目录或指定文件夹下创建一个新的WAV文件并写入WAV文件头包含采样率、位数、声道数等信息。STM32配置一个定时器产生固定频率的中断例如8kHz。每次中断触发时启动ADC对麦克风模块的AUDIO_OUT引脚进行采样。ADC转换完成的数据通过DMA直接存储器访问自动搬运到内存中的一个缓冲区。这样做的好处是解放了CPUCPU只在缓冲区满或半满时进行处理效率极高。当缓冲区数据达到一定量如512字节CPU将这些原始的PCM脉冲编码调制音频数据追加写入到刚才创建的WAV文件中。同时TFT屏刷新显示展示一个跳动的麦克风图标、当前录音时长和文件大小。用户按下“停止”键系统停止ADC采样将缓冲区剩余数据写入文件并更新WAV文件头中的文件大小字段然后关闭文件。一次录音完成。播放流程用户进入文件浏览界面TFT屏列出TF卡中所有的WAV文件。用户选择目标文件后按下“播放”键。系统打开该文件跳过WAV文件头开始读取PCM数据。读取的数据通过I2S接口以流的形式发送给VS1053B芯片。VS1053B接收到PCM数据后将其转换为模拟音频信号从耳机接口输出。TFT屏显示播放进度条、当前播放时间和文件名。用户可进行暂停、停止、切换上一曲/下一曲操作。注意在录音流程中ADC的采样率和DMA的搬运节奏必须严格同步否则会产生音频失真或杂音。在播放流程中从TF卡读取数据的速度、I2S发送数据的速度以及VS1053B解码的速度也需要匹配否则会导致播放卡顿或爆音。这需要通过合理的缓冲区设计和中断优先级管理来解决。3. 硬件电路设计与关键细节解析3.1 主控与电源电路设计STM32F103C8T6的最小系统电路是基础包括3.3V稳压电路、复位电路、boot模式选择电路以及晶振电路。这里我强调几个在音频项目中特别需要注意的点模拟与数字电源隔离STM32有独立的模拟电源引脚VDDA和VSSA。为了获得更干净的ADC采样结果必须将VDDA连接到一个干净的3.3V模拟电源上。最简单的做法是从3.3V数字电源VDD经过一个磁珠或一个0欧姆电阻再并联一个10uF和0.1uF的电容到地形成AVDD供给VDDA。VSSA则必须直接连接到电源地平面。这一步是降低底噪的关键。ADC参考电压STM32的ADC参考电压默认来自VDDA。因此一个稳定、低噪声的VDDA直接决定了ADC的精度。如果条件允许可以使用一颗专用的低压差线性稳压器LDO如AMS1117-3.3单独为模拟部分供电。退耦电容在每一个VDD/VSS对附近尽可能靠近芯片引脚放置一个0.1uF的陶瓷电容。这是抑制高频噪声的标准操作必须严格执行。3.2 音频输入麦克风电路设计驻极体麦克风模块输出的是交流耦合的音频信号其电压中心点通常在VCC/2附近假设模块设计如此。STM32的ADC只能测量0-3.3V的正电压。因此我们需要确保麦克风输出的信号落在ADC的测量范围内。直流偏置如果麦克风模块输出没有直流偏置就需要在信号进入ADC之前通过电阻分压网络例如两个等值电阻将信号抬升到1.65VVDD/2左右。我的模块自带偏置所以省去了这一步。抗混叠滤波根据奈奎斯特采样定理采样频率必须大于信号最高频率的两倍。我们设定采样率为8kHz那么能无失真还原的最高频率是4kHz。为了滤除高于4kHz的噪声防止混叠需要在ADC输入引脚前添加一个简单的RC低通滤波器。我选择了一个1kΩ电阻和一个0.01uF电容截止频率约16kHz先做初步滤波更精细的滤波可以在软件中完成。输入保护在ADC输入引脚处可以串联一个100Ω的小电阻并并联一个肖特基二极管到VDD和GND用于钳位电压防止异常高压冲击损坏单片机ADC引脚。3.3 音频输出VS1053B接口设计VS1053B与STM32主要通过SPI和I2S接口通信。SPI接口用于配置VS1053B的寄存器设置音量、采样率、模式等。连接STM32的SPI1或SPI2的SCK MISO MOSI并选择一个GPIO作为CS片选信号。I2S接口用于传输音频数据流。连接STM32的I2S2或I2S3的WS字选即左右声道时钟 CK串行时钟 SD串行数据。STM32作为I2S主设备产生时钟VS1053B作为从设备接收。其他关键信号DREQ这是一个非常重要的信号它是VS1053B的数据请求引脚高电平表示其内部缓冲区有空闲可以接收新的数据。STM32必须检测这个引脚的状态只有当DREQ为高时才能通过I2S发送数据。通常将此引脚连接到STM32的外部中断引脚或配置为输入查询。RESET硬件复位引脚低电平有效。音频输出VS1053B的左右声道输出经过一个简单的RC滤波网络滤除DAC产生的高频开关噪声后直接驱动耳机。如果需要驱动扬声器则需要后级功放电路。3.4 TFT屏与TF卡接口设计这两者都使用SPI接口可以共用一组SPI总线通过不同的GPIO片选信号来区分设备。SPI模式与速度TFT屏对SPI速度要求不高几MHz即可。但TF卡在初始化后为了达到较高的读写速度满足音频数据流写入需求需要切换到高速SPI模式通常可以达到25MHz甚至更高。STM32的SPI时钟需要根据两者分别配置。接线注意TF卡的SPI模式除了CS MOSI MISO SCK四根线必须连接上拉电阻通常10kΩ到3.3V。特别是数据线MOSI MISO和时钟线SCK这能保证信号稳定。TFT屏的背光控制引脚LED通常需要串联一个限流电阻如100Ω并通过一个三极管或MOS管由STM32控制以调节亮度或开关背光省电。SD卡检测引脚CD/DAT3有些卡槽模块会引出这个引脚可以用来检测TF卡是否插入。这是一个非常有用的功能可以在软件中实现热插拔检测。4. 软件架构与核心驱动实现4.1 底层驱动ADC与DMA配置录音的核心是ADC以固定频率采样。我们使用定时器TIM来触发ADC转换。// 示例使用TIM2触发ADC1 DMA1搬运 void Audio_Record_Init(uint32_t sample_rate) { // 1. 初始化ADC1 // 配置为独立模式、12位分辨率、扫描模式关闭、连续转换关闭由外部触发 // 通道配置为对应麦克风的引脚 ADC_InitTypeDef ADC_InitStructure; // ... 具体配置略 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; // TIM2触发 ADC_Init(ADC1, ADC_InitStructure); // 2. 初始化DMA1通道1用于ADC1 // 配置为从外设ADC1-DR到内存buffer的循环传输数据宽度半字16位 DMA_InitTypeDef DMA_InitStructure; // ... 具体配置略 DMA_Init(DMA1_Channel1, DMA_InitStructure); // 3. 初始化TIM2 // 配置为向上计数产生更新事件UEV作为TRGO输出 // 时钟72MHz 预分频器PSC设为71 则计数器时钟为1MHz。 // 自动重载值ARR设为 1000000 / sample_rate - 1。例如8kHz采样率则ARR124。 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // ... 具体配置略 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件触发TRGO // 4. 使能DMA、ADC、TIM DMA_Cmd(DMA1_Channel1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }这段代码的关键在于定时器ARR的计算它直接决定了采样率。DMA配置为循环模式并开启“半传输完成”和“传输完成”中断。这样当DMA搬运了半缓冲区和整个缓冲区的数据时都会产生中断。在中断服务函数中我们只需处理已经填满的那“半区”数据写入文件而DMA会继续向另一半缓冲区填充新数据实现了“乒乓缓冲”极大提高了效率也避免了数据丢失。4.2 中间件FATFS文件系统移植与使用要让STM32像电脑一样读写TF卡里的文件必须使用文件系统。FATFS是一个为嵌入式系统设计的通用FAT文件系统模块。移植从FATFS官网下载源码。我们需要修改diskio.c文件实现底层的磁盘读写接口disk_readdisk_writedisk_initialize等。这些接口内部调用的是SD卡的SPI读写函数。使用FATFS fs; // 文件系统对象 FIL file; // 文件对象 UINT bw; // 写入字节数 // 挂载文件系统 f_mount(fs, 0:, 1); // “0:” 对应物理驱动器0 // 创建并打开一个WAV文件用于写入 f_open(file, 0:/REC001.WAV, FA_CREATE_ALWAYS | FA_WRITE); // 写入WAV文件头44字节 f_write(file, wav_header, sizeof(wav_header), bw); // 在DMA中断中将音频缓冲区数据写入文件 void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_HT1)) { // 半传输完成 f_write(file, buffer_half, BUFFER_HALF_SIZE, bw); f_sync(file); // 定期同步防止数据丢失 } if(DMA_GetITStatus(DMA1_IT_TC1)) { // 传输完成 f_write(file, buffer, BUFFER_HALF_SIZE, bw); f_sync(file); } DMA_ClearITPendingBit(DMA1_IT_HT1 | DMA1_IT_TC1); } // 录音停止时关闭文件 f_close(file);实操心得f_sync()函数会将缓存的数据真正写入物理设备。在录音过程中定期调用如在每个缓冲区写入后可以避免因突然断电而丢失大量数据。但频繁调用会影响写入速度需要权衡。对于语音录音每秒调用几次是安全的。4.3 应用逻辑多任务管理与用户界面整个系统没有使用RTOS而是在一个超级循环main loop中通过状态机来管理不同的任务录音、播放、文件浏览。typedef enum { SYS_IDLE, SYS_RECORDING, SYS_PLAYING, SYS_BROWSING } SystemState_t; SystemState_t sys_state SYS_IDLE; int main(void) { // 硬件初始化 Hardware_Init(); // 文件系统初始化 FATFS_Init(); // 显示主界面 GUI_ShowMainMenu(); while(1) { key KEY_Scan(); // 扫描按键 switch(sys_state) { case SYS_IDLE: if(key KEY_REC) { sys_state SYS_RECORDING; Audio_StartRecording(); GUI_ShowRecordingScreen(); } else if(key KEY_PLAY) { sys_state SYS_BROWSING; GUI_ShowFileBrowser(); } break; case SYS_RECORDING: if(key KEY_STOP) { Audio_StopRecording(); sys_state SYS_IDLE; GUI_ShowMainMenu(); } else { // 更新录音时长显示 GUI_UpdateRecordTime(); } break; case SYS_PLAYING: // 检查VS1053的DREQ 发送音频数据 Audio_Playback_Service(); // 更新播放进度条 GUI_UpdatePlayProgress(); if(key KEY_PAUSE) { Audio_Pause(); } else if(key KEY_STOP) { Audio_Stop(); sys_state SYS_IDLE; GUI_ShowMainMenu(); } break; case SYS_BROWSING: // 处理上下键选择文件确认键播放 // ... break; } // 处理其他后台任务如SD卡检测 SD_Detect_Service(); } }这种基于状态机的轮询架构在资源有限的单片机上非常高效。关键在于每个状态下的服务函数如Audio_Playback_Service要设计成非阻塞的执行时间很短避免影响按键响应和其他任务。5. 核心功能实现与调试实录5.1 WAV文件格式的生成与解析WAV文件是微软开发的一种无损音频格式其文件头包含了音频的所有参数信息使得播放器能够正确解析。一个标准的PCM WAV文件头长度为44字节。在开始录音前我们需要在内存中预先构造一个WAV文件头但其中的“文件总大小”和“数据块大小”字段是未知的。一个常见的做法是先创建一个全零的44字节文件头写入文件。开始录音不断追加PCM数据。录音结束时获取最终的文件大小和音频数据大小。将文件指针移动到文件开头用正确的值更新文件头中的这两个字段再写回文件。typedef struct __attribute__((packed)) { // RIFF块 uint32_t ChunkID; // RIFF uint32_t ChunkSize; // 文件总大小 - 8 uint32_t Format; // WAVE // fmt子块 uint32_t Subchunk1ID; // fmt uint32_t Subchunk1Size; // 16 for PCM uint16_t AudioFormat; // 1 for PCM uint16_t NumChannels; // 1 for mono, 2 for stereo uint32_t SampleRate; // 8000, 16000, etc. uint32_t ByteRate; // SampleRate * NumChannels * BitsPerSample/8 uint16_t BlockAlign; // NumChannels * BitsPerSample/8 uint16_t BitsPerSample; // 16 // data子块 uint32_t Subchunk2ID; // data uint32_t Subchunk2Size; // 音频数据的大小 } WAV_Header_t;在播放时我们首先读取文件的前44字节解析出SampleRateNumChannelsBitsPerSample等关键信息并以此配置VS1053B的采样率。然后跳过44字节直接从“data”块开始读取PCM数据发送给VS1053B。5.2 基于VS1053B的高质量音频回放VS1053B的驱动相对复杂但遵循“先配置后送数”的原则。初始化与复位拉低RESET引脚至少1ms然后拉高等待至少1.5ms让芯片启动。通过SPI发送软件复位命令SCI_MODE寄存器写入0x0804。配置音频参数通过SPI写入SCI_AUDATA寄存器设置采样率例如0xAC44代表44.1kHz但我们需要根据WAV头信息设置如0x1F40代表8kHz。设置SCI_VOL寄存器控制音量。发送数据这是核心循环。不断检测DREQ引脚。当DREQ为高时表示VS1053B可以接收至少32字节的数据。此时通过SDI即I2S的SD线连续发送32字节的音频数据。数据必须连续发送中间不能有大的延迟否则会产生“咔嗒”声。void VS1053_SendAudioData(uint8_t *data, uint16_t len) { uint16_t i 0; while(i len) { if(VS1053_DREQ_IS_HIGH()) { // 检测DREQ引脚 // 拉低VS1053的XDCS数据片选 VS1053_XDCS_LOW(); // 通过SPI或GPIO模拟SPI发送数据 for(uint8_t j0; j32 ilen; j, i) { SPI_SendByte(data[i]); } VS1053_XDCS_HIGH(); } } }处理结束当文件播放完毕需要向VS1053B发送一个“结束填充”。通常发送2048个字节的0x00以确保缓冲区内的所有数据都被解码播放完毕然后发送“播放结束”命令。5.3 TFT屏图形化界面设计在资源受限的单片机上实现图形界面核心是“局部刷新”和“状态缓存”。显示驱动首先实现最基本的画点、画线、填充矩形、显示字符和字符串的函数。这些函数基于SPI向屏幕发送命令和数据。界面分层将界面分为背景层和内容层。例如文件浏览界面背景是固定的菜单框和标题栏内容是滚动的文件列表。刷新时只刷新内容层变化的区域。字体与图标将需要显示的汉字和图标做成点阵数组存放在一个单独的font.c或pic.c文件中。为了节省Flash空间通常只提取需要的字符如数字、英文字母、少量常用汉字。进度条实现进度条实际上是一个填充的矩形。计算当前进度对应的像素宽度width (current_value * total_pixel_width) / max_value。每次更新时先清除旧的进度条区域用背景色填充再根据新的宽度画一个填充矩形。抗闪烁技巧在更新动态区域如时间、进度条前可以先将屏幕的该区域设置为“写RAM”模式然后连续发送所有像素数据最后再结束通信。避免在每次画图操作前后都发送命令减少通信次数视觉上会更流畅。6. 系统优化、问题排查与实测心得6.1 性能优化与功耗控制双缓冲与DMA如前所述ADC使用DMA双缓冲是保证录音流畅性的基石。同样在播放时从SD卡读取数据也可以使用双缓冲。一个缓冲区用于向VS1053B发送数据另一个缓冲区用于从SD卡预读取下一段数据。SPI时钟优化TF卡在初始化阶段需要用低速时钟400kHz初始化完成后可以切换到高速模式如18MHz或24MHz。高速SPI是保证大文件连续读写不卡顿的关键。TFT屏的SPI时钟可以单独设置通常几MHz就够用。降低功耗设备在待机时可以进入低功耗模式。关闭TFT屏背光通过控制MOS管。将STM32未使用的GPIO设置为模拟输入模式最省电。停止所有定时器关闭ADC、DAC等外设时钟。让STM32进入SLEEP或STOP模式。当有按键按下时通过外部中断唤醒系统。实测中在STOP模式下系统整体电流可以从30mA左右降至1mA以下。6.2 常见问题与排查实录在调试过程中我遇到了几个典型问题这里记录下排查思路问题录音文件在电脑上播放速度极快声音尖细。排查这是最经典的采样率问题。电脑播放器依据WAV文件头中的SampleRate字段播放。如果这个值设置错误比如你实际是8kHz采样但文件头里写成了44.1kHz电脑就会用44.1kHz的速度去播放8kHz的数据导致速度变快、音调变高。解决仔细检查代码中构造WAV文件头时SampleRate、ByteRate等字段的值是否正确计算并写入。使用十六进制编辑器打开生成的WAV文件查看偏移地址0x18-0x1B四个字节的值是否正确8kHz 0x00001F40。问题录音或播放时有持续的“嗡嗡”底噪声。排查这通常是电源噪声或地线干扰。解决步骤 a.检查电源用示波器查看模拟部分供电VDDA的波形看是否有明显的纹波。加强模拟电源的滤波增加电容容值或使用π型滤波。 b.检查地线确保模拟地VSSA和数字地VSS在一点共地且走线尽可能粗短。 c.检查麦克风电路麦克风模块的供电是否稳定输出耦合电容是否合适ADC输入端的RC低通滤波器参数是否需要调整降低截止频率 d.软件滤波在ADC采样后加入一个软件数字滤波器如一阶IIR低通滤波器可以有效抑制高频噪声。问题播放音乐时偶尔会出现“咔哒”一声的爆音。排查这通常是数据流不连续导致的。VS1053B的缓冲区空了但没有及时得到新的数据。解决 a.优化数据供给确保从SD卡读取数据的速度足够快。检查SPI时钟是否已提到最高SD卡读写函数是否高效可以使用f_read函数一次读取多个扇区。 b.增大缓冲区增加用于播放的音频数据缓冲区大小。例如从512字节增加到2048字节。 c.提高数据发送优先级将检测DREQ和发送数据的任务放在主循环的最高优先级位置或者放在一个高优先级的定时器中断里确保一旦缓冲区有空闲就能立刻被填充。问题插入TF卡后系统无法识别f_mount返回FR_NO_FILESYSTEM。排查文件系统初始化失败。解决步骤 a.硬件连接首先用万用表检查TF卡座的引脚是否有虚焊特别是SPI的四根数据线。检查上拉电阻是否焊好。 b.SPI时序确保SPI的相位和极性CPOL CPHA设置正确。对于SD卡模式0CPOL0 CPHA0或模式3CPOL1 CPHA1通常是正确的。 c.初始化序列SD卡的上电、发送74个时钟脉冲、CMD0、CMD8、CMD55、ACMD41等初始化命令序列必须严格遵循规范。可以单步调试检查每个命令的响应是否正确。 d.卡兼容性有些大容量或超高速的TF卡可能兼容性不好。尝试换用不同品牌、不同容量如4GB 8GB的卡。初期调试建议使用容量较小16GB、速度等级为Class4或Class10的卡成功率更高。6.3 功能扩展思路这个基础框架搭建好后有很多可以扩展的方向音频压缩目前存储的是无损PCM的WAV格式占用空间大。可以集成一个软件编码库如Speex Opus将音频实时压缩为更小的格式存储大幅延长录音时间。语音触发录音增加一个简单的语音活动检测VAD算法。只有检测到有效声音时才启动录音和存储静默时则暂停进一步节省存储空间和电量。USB声卡功能利用STM32的USB接口实现USB Audio Device功能。这样这个设备可以连接到电脑被识别为一个外置的USB麦克风或扬声器。蓝牙音频传输增加一个蓝牙音频模块如BK3266 JL AC692X系列实现蓝牙录音笔或蓝牙音箱的功能。经过从硬件焊接、驱动编写到软件调试的全过程这个基于STM32的录音笔最终成功实现了设计的所有基础功能。它不仅仅是一个工具更是一个涵盖了模拟电路、数字信号处理、嵌入式协议、文件系统、人机交互等多个领域的综合实践项目。过程中遇到的每一个问题从底噪到爆音从文件系统挂载失败到显示闪烁都加深了对嵌入式系统“牵一发而动全身”的理解。最大的体会是在资源受限的单片机环境下任何功能的实现都需要在性能、资源、功耗和复杂度之间做出精心的权衡与设计。