简介:基于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 放固件和 FatFs,20KB 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 音质歌曲是立体声,文件每秒数据量约 172KB;stm32f103RBT6 的 RAM 只有 20KB,若是硬解 MP3 还要留出至少 10KB 解码缓冲,十几条数据流挤在一起不爆才怪。课设最稳的做法是先把素材转成单声道、16kHz、16bit,这样每秒只需要 32KB,SD 卡按 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 占用 | 适合答辩方向 |
|---|---|---|---|---|
| 内部 DAC | PA4/PA5 | 12bit,底噪明显 | 极低(DMA搬运) | 讲 DAC 原理 / DMA 双缓冲 |
| I2S+外部DAC | PA5/PA7 等 4-5 个 | 16/24bit,噪底干净 | 极低(DMA搬运) | 讲 I2S 帧格式 / 音频时钟 |
2.3 stm32f103RBT6 的资源账,为什么不是 51 单片机
很多课程设计会用 51 单片机模拟播放,但 51 多数型号没有 DAC,只能用 PWM 加积分电路,CPU 在中断里高频翻转寄存器,播放长文件时完全无法保证节奏。stm32f103RBT6 的优势在于有 DMA,音频搬运不占用 CPU,主循环还能处理按键和 OLED。
| 芯片 | Flash | RAM | DAC | DMA | 播放一首 3 分钟 WAV 的难度 |
|---|---|---|---|---|---|
| 51 单片机 | 4KB~64KB | 128B~2KB | 多数无 | 几乎无 | 需要用 PWM 软件模拟,CPU 全程满载 |
| STM32F103C8T6 | 64KB | 20KB | 2x12bit | 有 | 可以跑,但 Flash 紧张,调试信息要省 |
| STM32F103RBT6 | 128KB | 20KB | 2x12bit | 有 | 可以跑,Flash 够放 FATFS + 界面字库 |
选择 RBT6 而不仅是 C8T6,多出来的 64KB Flash 对显示中文歌名、加入调试串口日志很有帮助。答辩时,老师更关心你能否解释清楚为什么选择这个型号,而不是只会说“功能更多”。
3. 引脚分配与 CubeMX 初始化:把 SD 卡和 DAC 挂到不冲突的引脚上
拿到项目源码,第一步就是检查 CubeMX 的 pinout。很多所谓高分源码烧进去没反应,是因为 SD 卡挂在 SPI1,同时又把 SPI1 复用成 I2S 去接外部 DAC,PA5 被两个外设抢占。这里给出一个不打架的分配方案。
3.1 一组不冲突的引脚表
| 功能 | 引脚 | 外设 | 说明 |
|---|---|---|---|
| SD_CS | PB12 | SPI2_NSS(软件控制) | 防止与 SPI1 的 I2S 冲突 |
| SD_SCK | PB13 | SPI2_SCK | SPI 时钟 |
| SD_MISO | PB14 | SPI2_MISO | 卡数据输出 |
| SD_MOSI | PB15 | SPI2_MOSI | 卡数据输入 |
| DAC_OUT | PA4 | DAC1_OUT1 | 输出到耳机/LM386 |
| KEY_PLAY | PB0 | EXTI0 | 播放/暂停 |
| KEY_SWITCH | PB1 | EXTI1 | 上一首/下一首 |
SD 卡模块型号很多,若板载电平转换,VCC 可接 5V;若没有转换,只能接 3.3V。PB13-PB15 是 SPI2 的默认复用功能,与调试串口不冲突,也可以留出 PA5 给后续 I2S 扩展。
3.2 CubeMX 关键参数速查表
启动 STM32CubeMX,芯片选择 STM32F103RBTx,按表格配置:
| 分类 | 配置项 | 值 |
|---|---|---|
| SYS | Debug | Serial Wire |
| RCC | HSE | Crystal/Ceramic Resonator |
| Clock | SYSCLK | PLL,HCLK=72MHz |
| DAC | OUT1 | Enabled (PA4),Output Buffer Enabled |
| DAC | Trigger | Timer 3 TRGO Event |
| SPI2 | Mode | Full-Duplex Master |
| SPI2 | Data Size | 8 Bit |
| SPI2 | Prescaler | 初始 64(SCK≈1.125MHz),后续代码中调整 |
| TIM3 | Trigger Output (TRGO) | Update Event |
| TIM3 | Prescaler | 0 |
| TIM3 | Period | 4499(配合 72MHz 得 16kHz) |
| DMA | DMA1 Channel 3 | MEMORY_TO_PERIPH,Half Word,Circular |
TIM3 Period 等于 4499 对应 16kHz 采样率,公式是:采样率 = 定时器时钟 / (PSC+1) / (ARR+1) = 72MHz / 1 / 4500 = 16000Hz。如果改成 22050Hz,ARR 需要同步调整,否则声音会明显变调。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 对应 0,0 对应 2048,+32767 对应 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_SAMPLES | 4096 | 约 0.25 秒 @16kHz |
| HALF_SAMPLES | 2048 | 约 0.125 秒 |
| DMA Mode | CIRCULAR | 支持半满/全满中断 |
| 临时缓冲 | 无 | 直接复用 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 stream=codec_name,sample_rate,channels,bits_per_sample -of default=noprint_wrappers=1 song.wav期望输出是codec_name=pcm_s16le、sample_rate=16000、channels=1、bits_per_sample=16。如果出现pcm_mp3或sample_rate=48000,播放器要么不出声,要么严重变速。pcm_s16le表示有符号 16 位小端 PCM;大端格式会让声音变成白噪声。
5.2 f_mount 返回 FR_NOT_READY 时查这四件事
| 现象 | 可能原因 | 验证方法 |
|---|---|---|
| f_mount 返回 FR_NOT_READY | SPI 时钟过快 | 手动把 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 的配合。
本文还有配套的精品资源,点击获取