资讯动态

基于STM32F103RBT6的WAV音乐播放器:FATFS+DMA+DAC全解析

发布时间:2026/9/12 12:35:31 来源:尧图企业网站定制
简介基于STM32F103RBT6单片机设计的音乐播放器课程设计源码面向嵌入式方向的高校学生适合作为课程设计、期末大作业或毕业设计参考。压缩包共含243个文件大小约41.08MB以C语言源文件与头文件为主配合Keil工程配置、编译生成的hex与axf固件以及bmp、jpg、png等界面素材另有PDF、Word文档和CHM帮助手册便于查看说明与二次开发。目前已有216人学习下载。项目代码注释细致初学者也能较快上手系统涵盖音频播放、按键交互、界面显示等核心功能模块划分清晰且经过严格调试可直接部署。通过学习这份源码可以理解STM32标准外设库中定时器、中断、RCC时钟、ADC、I2C等驱动模块的实际用法并对照主程序梳理音频输出的完整流程界面图片与资源文件展示了开机登录、音符等视觉设计适合借鉴到其他单片机项目中用于答辩展示或功能扩展。1. 拿到高分课设源码先拆解 stm32f103RBT6 播放器的数据流课设选题表上出现「基于 stm32f103RBT6 单片机音乐播放器」时多数人会把 .zip 解压后直接烧录然后卡在「有波形没声音」这关。一个能拿高分的播放器项目核心不是那块 2 寸扬声器而是能不能讲清楚一首 WAV 文件从 SD 卡到 PA4 引脚的完整路径。这个项目源码适合想弄懂 FATFS、DMA 和 DAC 的电子方向学生也适合拿现成代码改产品的工程师。先把 stm32f103RBT6 的资源对到音频场景里128KB Flash 放固件和 FatFs20KB RAM 塞双缓冲72MHz 主频和 DMA 让 CPU 不用反复搬数据。搞明白这条线后面的代码调试才有方向。2. 解码链路与选型 stm32f103RBT6 为什么能当音乐播放器音乐播放器本质是一个采样系统把离散的 PCM 数据按固定时间间隔送给数模转换器每次转换的间隔决定了音调。stm32f103RBT6 之所以能在课设里扛起这个任务是因为它同时具备三个条件FATFS 可以管理 SD 卡上的文件、DMA 可以把内存数据按照外设请求批量搬运、内部 DAC 可以把数字量变成电压。缺少任何一项都要靠 CPU 中断里翻转 GPIO播放质量立刻下降。2.1 WAV 格式是最稳的播放素材2.1.1 解析 WAV 就是读一个 44 字节头一首 44.1kHz/16bit 的 CD 音质歌曲是立体声文件每秒数据量约 172KBstm32f103RBT6 的 RAM 只有 20KB若是硬解 MP3 还要留出至少 10KB 解码缓冲十几条数据流挤在一起不爆才怪。课设最稳的做法是先把素材转成单声道、16kHz、16bit这样每秒只需要 32KBSD 卡按 SPI 4MHz 波特率也能跑赢实时播放。WAV 没有压缩和解码链f_read 读回来的就是可以送到 DAC 的 PCM 数据。2.1.2 用 ffmpeg 统一素材参数很多网上下载的音频格式五花八门直接用会导致解析失败。开始写代码前先对所有素材做一次统一转换ffmpeg -i input.mp3 -ar 16000 -ac 1 -sample_fmt s16 output.wav这条命令把输入音频强制转成 16kHz 采样率-ar 16000、单声道-ac 1、有符号 16 位 PCM-sample_fmt s16。转换完成后用十六进制工具打开能看到文件头从“RIFF”开始这就是 WAV 的标准签名。后续代码里只需要检查这 4 个字节即可过滤掉绝大多数非法文件。2.2 内部 DAC 和外部 I2S DAC 的两条路线2.2.1 内部 DAC 的 12 位裁剪stm32f103RBT6 有两个 12 位 DAC 通道分别从 PA4、PA5 输出。内部 DAC 的参考电压等于 VREF通常 3.3V每个码字对应约 0.8mV。播放 WAV 时16 位的有符号样本无法直接进 12 位寄存器一般做法是先把样本右移 4 位变成 12 位有符号范围再加 2048 偏置让静音时输出大约 1.65V。这样做付出的代价是信噪比约 66dB耳机上能听到底噪但接功放验证逻辑没问题。2.2.2 I2S 外部 DAC 的接线复杂性如果追求音质可利用 SPI1 的 I2S 模式接 PCM5102 等外部解码芯片。但 I2S 需要 SCK、WS、SD、MCK 多根时钟线且与 SD 卡使用的 SPI1 引脚冲突。课设时间紧张时优先保功能外部 I2S 方案留到扩展阶段再做。方案所需引脚音质CPU 占用适合答辩方向内部 DACPA4/PA512bit底噪明显极低DMA搬运讲 DAC 原理 / DMA 双缓冲I2S外部DACPA5/PA7 等 4-5 个16/24bit噪底干净极低DMA搬运讲 I2S 帧格式 / 音频时钟2.3 stm32f103RBT6 的资源账为什么不是 51 单片机很多课程设计会用 51 单片机模拟播放但 51 多数型号没有 DAC只能用 PWM 加积分电路CPU 在中断里高频翻转寄存器播放长文件时完全无法保证节奏。stm32f103RBT6 的优势在于有 DMA音频搬运不占用 CPU主循环还能处理按键和 OLED。芯片FlashRAMDACDMA播放一首 3 分钟 WAV 的难度51 单片机4KB~64KB128B~2KB多数无几乎无需要用 PWM 软件模拟CPU 全程满载STM32F103C8T664KB20KB2x12bit有可以跑但 Flash 紧张调试信息要省STM32F103RBT6128KB20KB2x12bit有可以跑Flash 够放 FATFS 界面字库选择 RBT6 而不仅是 C8T6多出来的 64KB Flash 对显示中文歌名、加入调试串口日志很有帮助。答辩时老师更关心你能否解释清楚为什么选择这个型号而不是只会说“功能更多”。3. 引脚分配与 CubeMX 初始化把 SD 卡和 DAC 挂到不冲突的引脚上拿到项目源码第一步就是检查 CubeMX 的 pinout。很多所谓高分源码烧进去没反应是因为 SD 卡挂在 SPI1同时又把 SPI1 复用成 I2S 去接外部 DACPA5 被两个外设抢占。这里给出一个不打架的分配方案。3.1 一组不冲突的引脚表功能引脚外设说明SD_CSPB12SPI2_NSS软件控制防止与 SPI1 的 I2S 冲突SD_SCKPB13SPI2_SCKSPI 时钟SD_MISOPB14SPI2_MISO卡数据输出SD_MOSIPB15SPI2_MOSI卡数据输入DAC_OUTPA4DAC1_OUT1输出到耳机/LM386KEY_PLAYPB0EXTI0播放/暂停KEY_SWITCHPB1EXTI1上一首/下一首SD 卡模块型号很多若板载电平转换VCC 可接 5V若没有转换只能接 3.3V。PB13-PB15 是 SPI2 的默认复用功能与调试串口不冲突也可以留出 PA5 给后续 I2S 扩展。3.2 CubeMX 关键参数速查表启动 STM32CubeMX芯片选择 STM32F103RBTx按表格配置分类配置项值SYSDebugSerial WireRCCHSECrystal/Ceramic ResonatorClockSYSCLKPLLHCLK72MHzDACOUT1Enabled (PA4)Output Buffer EnabledDACTriggerTimer 3 TRGO EventSPI2ModeFull-Duplex MasterSPI2Data Size8 BitSPI2Prescaler初始 64SCK≈1.125MHz后续代码中调整TIM3Trigger Output (TRGO)Update EventTIM3Prescaler0TIM3Period4499配合 72MHz 得 16kHzDMADMA1 Channel 3MEMORY_TO_PERIPHHalf WordCircularTIM3 Period 等于 4499 对应 16kHz 采样率公式是采样率 定时器时钟 / (PSC1) / (ARR1) 72MHz / 1 / 4500 16000Hz。如果改成 22050HzARR 需要同步调整否则声音会明显变调。DMA 使用 DMA1 通道 3对应 DAC_CH1 的硬件请求。3.3 在代码里把初始化顺序补全CubeMX 生成工程后main 函数里的初始化顺序是固定的但要注意先挂载文件系统再播放int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_DAC_Init(); MX_SPI2_Init(); MX_TIM3_Init(); f_mount(fs, /, 1); ... }这里MX_DMA_Init()一定要放在MX_DAC_Init()之前因为 DAC 的 DMA 句柄依赖 DMA 时钟使能。f_mount(fs, /, 1)中最后一个参数1表示立即挂载返回FR_OK后才能继续打开文件。SD 卡上电后需要至少 74 个时钟周期才能进入 SPI 模式CubeMX 默认波特率可能过快建议在挂载前临时把 SPI2 预分频调到 128 或 256挂载成功后再提高。SPI 重新配置的常用写法是修改hspi2.Init.BaudRatePrescaler后重新调用HAL_SPI_Init但要注意重新初始化会重置 SPI 状态卡在 SPI 模式中可能握手失败。更稳妥的做法是在代码里直接写一个SD_Init_LowSpeed()函数在挂载完成后不再触碰时钟分频。4. WAV 头解析与双缓冲播放FATFS 读文件、DMA 喂 DAC播放器的核心循环只有两件事从 SD 卡读文件把数据转换成 DAC 可用的格式DMA 在后台把缓冲数组里的样本持续送到 DAC。这两件事靠“半缓冲”和“全缓冲”两个中断标志握手。4.1 解析 WAV 头找出 data 偏移文件系统挂载后先只读前 44 字节。WAV 的 fmt 块和 data 块位置是固定的但为了兼容不同写入工具建议按字节解析不要直接定义结构体强转避免编译器对齐造成错位。uint8_t hdr[44]; UINT br; f_lseek(fil, 0); f_read(fil, hdr, 44, br); if (memcmp(hdr[0], RIFF, 4) || memcmp(hdr[8], WAVE, 4)) return -1; uint16_t audio_format hdr[20] | (hdr[21] 8); if (audio_format ! 1) // 1 表示 PCM return -2; uint32_t sample_rate hdr[24] | (hdr[25] 8) | (hdr[26] 16) | (hdr[27] 24); uint32_t data_size hdr[40] | (hdr[41] 8) | (hdr[42] 16) | (hdr[43] 24);audio_format必须为 1即线性 PCM如果出现 3 或 6可能是 IEEE Float 或 A-Law不能直接播放。data_size用来计算总时长也用来判断文件什么时候读完。实际的 PCM 数据从偏移 44 开始所以播放前要执行f_lseek(fil, 44)。4.2 双缓冲与 12 位 DAC 的数据换算内部 DAC 寄存器只有 12 位而 WAV 的采样点是有符号 16 位换算关系是uint16_t dac_value (uint16_t)((sample_s16 32768) 4);这样 -32768 对应 00 对应 204832767 对应 4095。如果源文件是 8 位无符号 PCM则直接dac_value sample_u8 4。DMA 配置成循环模式内存地址指向半字数组外设地址指向DAC_DHR12R1。启动代码HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)pcm_buf, BUFFER_SAMPLES, DAC_ALIGN_12B_R);pcm_buf是预处理好的 12 位右对齐数组BUFFER_SAMPLES是数组长度。DAC_ALIGN_12B_R告诉 HAL 数据已经右对齐不需要再移位。参数推荐值计算依据BUFFER_SAMPLES4096约 0.25 秒 16kHzHALF_SAMPLES2048约 0.125 秒DMA ModeCIRCULAR支持半满/全满中断临时缓冲无直接复用 pcm_buf 的两个半区这里不额外开临时缓冲能省下 4KB RAM。关键前提是半满中断触发时DMA 正在读后半段所以可以安全写前半段全满中断触发时正好相反。4.3 用 HAL 回调切换 DMA 缓冲DMA1 通道 3 在循环模式下会产生半传输和传输完成中断。在 CubeMX 生成的stm32f1xx_it.c里中断服务函数会调用HAL_DMA_IRQHandler随后触发回调volatile uint8_t dma_half_transfer 0; volatile uint8_t dma_transfer_complete 0; void HAL_DMA_HalfCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma-Instance DMA1_Channel3) dma_half_transfer 1; } void HAL_DMA_TxCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma-Instance DMA1_Channel3) dma_transfer_complete 1; }主循环等待标志然后填充对应的半区if (dma_half_transfer) { dma_half_transfer 0; fill_buf(0); } if (dma_transfer_complete) { dma_transfer_complete 0; fill_buf(1); }fill_buf直接往目标半区写数据int fill_buf(uint8_t half) { uint16_t *buf pcm_buf half * HALF_SAMPLES; UINT br; if (f_read(fil, buf, HALF_SAMPLES * 2, br) ! FR_OK) return 0; for (int i 0; i br / 2; i) { int16_t s (int16_t)buf[i]; buf[i] (uint16_t)((s 32768) 4); } return br HALF_SAMPLES * 2; }f_read读取的字节数直接存储在目标缓冲中因为 STM32 和 WAV 文件都是小端buf[i]的字节序天然一致。循环里把有符号样本转成 12 位 DAC 值。返回值为 0 表示已经到达文件末尾此时可以补 2048 或停止播放。5. 排错清单SD卡不识别、爆音、只放一遍就停的三个排查点代码跑起来后声音不对才是真正的问题来源。下面按出现频率排先验证文件格式再看时序。5.1 音频素材没有按要求转码播放器对音频格式极不宽容。命令行检查ffprobe -v error -show_entries streamcodec_name,sample_rate,channels,bits_per_sample -of defaultnoprint_wrappers1 song.wav期望输出是codec_namepcm_s16le、sample_rate16000、channels1、bits_per_sample16。如果出现pcm_mp3或sample_rate48000播放器要么不出声要么严重变速。pcm_s16le表示有符号 16 位小端 PCM大端格式会让声音变成白噪声。5.2 f_mount 返回 FR_NOT_READY 时查这四件事现象可能原因验证方法f_mount 返回 FR_NOT_READYSPI 时钟过快手动把 SPI 预分频设成 256重试挂载成功但 f_open 返回 FR_NO_FILE文件名大小写或长文件名改成 8.3 短文件名如 SONG.WAV打开文件后读 0 字节SD 卡带分区表用disk_initialize中的扇区偏移或重新格式化播放卡顿SD 卡写保护或旧卡换一张 Class10 卡格成 FAT32要特别注意很多 Type-C 转接卡会生成 GPT 分区表FatFs 默认FF_MULTI_PARTITION未开启时无法访问。最简单的解决办法是用官方格式化工具把卡格式化为 FAT32并在 Windows 下只建一个分区。5.3 爆音与噪声的排查顺序爆音的本质是 DAC 输出不连续。按照常见程度依次检查缓冲区切换速度慢、定时器触发频率不稳、DAC 输出没有低通滤波。可以先在 PA4 与功放之间串一个 1kΩ 电阻和 220nF 电容组成截止频率约 720Hz 的低通滤波能消掉不少高频噪声。然后拿逻辑分析仪抓 PA4看波形是否在缓冲区切换点出现周期性缺口。如果有说明fill_buf里的f_read占用了太长时间可以把HALF_SAMPLES调小一半并检查 SD 卡是否工作在 4MHz 以上的 SPI 速率。5.4 只放一遍就停检查文件指针是否越界常见错误是代码不知道 WAV 的 data 长度读到文件末尾后直接退出播放。正确做法是用data_size记录实际音频长度每次播放完f_lseek回到 44并继续循环。判断播放结束的条件是br HALF_SAMPLES * 2此时补 2048而不是直接 break。只有按下停止键才跳出循环。6. 从“能响”到“课设答辩”OLED 歌词、按键控制和播放时长验证如果已经实现连续播放可以再加两个功能把分数拉高。6.1 用 OLED 显示当前曲目和播放状态0.96 寸 OLED 用软件模拟 SPI 驱动只需要 PB3、PB4 两个引脚不占用硬件 SPI2。主循环里定时刷新void OLED_ShowPlaying(const char *name, uint16_t total_sec, uint16_t cur_sec) { OLED_Clear(); OLED_ShowString(0, 0, name); char buf[20]; snprintf(buf, sizeof(buf), %d:%02d / %d:%02d, cur_sec / 60, cur_sec % 60, total_sec / 60, total_sec % 60); OLED_ShowString(0, 2, buf); }total_sec可以直接从 WAV 头计算data_size / byte_rate而byte_rate sample_rate * channels * bits_per_sample / 8。cur_sec每次填完一个半缓冲加一次计数16kHz 下 2048 个样本对应 0.128 秒。6.2 验证播放速度是否漂移答辩时老师如果问“你怎么证明播放速度是对的”用 SysTick 对一下即可uint32_t t0 HAL_GetTick(); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)pcm_buf, BUFFER_SAMPLES, DAC_ALIGN_12B_R); // 播放结束后 printf(played %d ms, file claims %d ms\n, HAL_GetTick() - t0, total_sec * 1000);把HAL_GetTick()的差值除以 1000再和data_size / byte_rate比较误差能控制在 0.2% 以内才算真正调通了定时器和 DMA 的配合。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价