基于STM32的音频播放系统,实现SD卡读取音频文件(WAV格式)→ PWM输出播放。
这个方案使用定时器PWM + DMA,支持16位/44.1kHz立体声/单声道音频,可直接驱动扬声器。
一、系统架构
SD卡 (FATFS)
↓
WAV文件
↓
解码 → 音频数据缓冲区
↓
DMA传输
↓
定时器PWM输出
↓
低通滤波器 → 音频功放 → 喇叭
支持格式:
- WAV格式(16位PCM)
- 8kHz/16kHz/44.1kHz采样率
- 单声道/立体声
二、硬件连接
1、SD卡(SPI模式)
SD卡 → STM32
CS → PA4
SCK → PA5
MISO → PA6
MOSI → PA7
2、PWM音频输出
TIM1_CH1 (PA8) → 左声道
TIM1_CH2 (PA9) → 右声道
必须加RC低通滤波(PWM是数字方波,必须滤波才能得到模拟音频)
三、核心数据结构
c
// audio_player.h
#ifndef __AUDIO_PLAYER_H
#define __AUDIO_PLAYER_H
#include "stm32f4xx_hal.h"
#include "fatfs.h"
// 音频状态
typedef enum {
AUDIO_STOP = 0,
AUDIO_PLAYING,
AUDIO_PAUSED,
AUDIO_ERROR
} Audio_State_t;
// WAV文件头结构
typedef struct {
char chunk_id[4]; // "RIFF"
uint32_t chunk_size;
char format[4]; // "WAVE"
char subchunk1_id[4]; // "fmt "
uint32_t subchunk1_size;
uint16_t audio_format; // 1 = PCM
uint16_t num_channels; // 1或2
uint32_t sample_rate; // 如44100
uint32_t byte_rate;
uint16_t block_align;
uint16_t bits_per_sample; // 8, 16
char subchunk2_id[4]; // "data"
uint32_t subchunk2_size; // 音频数据大小
} WAV_Header_t;
// 音频播放器结构
typedef struct {
Audio_State_t state;
FIL file;
WAV_Header_t header;
// DMA双缓冲区
uint16_t buffer[2][4096];
uint8_t current_buffer; // 0或1
uint32_t bytes_remaining;
// 播放控制
uint32_t position; // 当前位置(字节)
uint32_t total_size; // 总大小
float volume; // 音量0.0-1.0
// 硬件相关
TIM_HandleTypeDef *htim;
uint32_t channel_left;
uint32_t channel_right;
} Audio_Player_t;
// 函数声明
void Audio_Init(Audio_Player_t *player);
uint8_t Audio_Play_File(Audio_Player_t *player, const char *filename);
void Audio_Stop(Audio_Player_t *player);
void Audio_Pause(Audio_Player_t *player);
void Audio_Resume(Audio_Player_t *player);
void Audio_Set_Volume(Audio_Player_t *player, float volume);
#endif
四、WAV文件解析
c
// wav_decoder.c
#include "audio_player.h"
#include <string.h>
// 检查WAV文件头
uint8_t WAV_Parse_Header(FIL *file, WAV_Header_t *header)
{
UINT bytes_read;
// 读取文件头
f_read(file, header, sizeof(WAV_Header_t), &bytes_read);
if (bytes_read != sizeof(WAV_Header_t)) {
return 0;
}
// 检查"RIFF"和"WAVE"
if (memcmp(header->chunk_id, "RIFF", 4) != 0 ||
memcmp(header->format, "WAVE", 4) != 0) {
return 0;
}
// 检查PCM格式
if (header->audio_format != 1) { // 1 = PCM
return 0;
}
// 检查位深度(只支持16位)
if (header->bits_per_sample != 16) {
return 0;
}
// 检查采样率(支持常见采样率)
if (header->sample_rate != 8000 &&
header->sample_rate != 16000 &&
header->sample_rate != 44100) {
return 0;
}
return 1;
}
五、PWM音频输出驱动
1、定时器PWM配置
c
// audio_pwm.c
#include "audio_player.h"
// PWM定时器初始化
void PWM_Audio_Init(Audio_Player_t *player, uint32_t sample_rate)
{
TIM_HandleTypeDef htim1;
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
// 1. 定时器时钟配置
// 系统时钟168MHz,我们希望PWM频率 = 采样率 * 256
// 这样每个PWM周期有256级占空比
uint32_t pwm_freq = sample_rate * 256;
uint32_t tim_clock = 168000000; // 168MHz
// 计算预分频和自动重载值
uint32_t prescaler = 0;
uint32_t period = (tim_clock / pwm_freq) - 1;
if (period > 65535) {
prescaler = period / 65536;
period = 65535;
}
// 2. 初始化TIM1
htim1.Instance = TIM1;
htim1.Init.Prescaler = prescaler;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = period;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim1);
// 3. 配置PWM通道(左声道)
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比0
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2);
// 4. 死区时间配置
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 0;
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig);
// 5. 启动PWM
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
player->htim = &htim1;
player->channel_left = TIM_CHANNEL_1;
player->channel_right = TIM_CHANNEL_2;
}
2、DMA传输配置
c
// audio_dma.c
#include "audio_player.h"
// DMA初始化
void DMA_Audio_Init(Audio_Player_t *player)
{
// 使用DMA1 Stream5 (TIM1_CH1/CH2)
DMA_HandleTypeDef hdma_tim1_ch1;
__HAL_RCC_DMA1_CLK_ENABLE();
// 配置DMA
hdma_tim1_ch1.Instance = DMA1_Stream5;
hdma_tim1_ch1.Init.Channel = DMA_CHANNEL_6;
hdma_tim1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim1_ch1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim1_ch1.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim1_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_tim1_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_tim1_ch1.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_tim1_ch1.Init.Priority = DMA_PRIORITY_HIGH;
hdma_tim1_ch1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_tim1_ch1);
// 连接DMA到TIM1
__HAL_LINKDMA(player->htim, hdma[TIM_DMA_ID_CC1], hdma_tim1_ch1);
__HAL_LINKDMA(player->htim, hdma[TIM_DMA_ID_CC2], hdma_tim1_ch1);
// 使能DMA传输完成中断
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
__HAL_DMA_ENABLE_IT(&hdma_tim1_ch1, DMA_IT_TC | DMA_IT_HT);
}
六、音频播放核心逻辑
1、音频播放器初始化
c
// audio_player.c
#include "audio_player.h"
#include <string.h>
Audio_Player_t audio_player;
void Audio_Init(Audio_Player_t *player)
{
memset(player, 0, sizeof(Audio_Player_t));
player->state = AUDIO_STOP;
player->volume = 1.0f; // 默认最大音量
player->current_buffer = 0;
}
2、播放WAV文件
c
// 播放WAV文件
uint8_t Audio_Play_File(Audio_Player_t *player, const char *filename)
{
FRESULT res;
UINT bytes_read;
// 1. 打开文件
res = f_open(&player->file, filename, FA_READ);
if (res != FR_OK) {
return 0;
}
// 2. 解析WAV头
if (!WAV_Parse_Header(&player->file, &player->header)) {
f_close(&player->file);
return 0;
}
// 3. 初始化PWM
PWM_Audio_Init(player, player->header.sample_rate);
// 4. 初始化DMA
DMA_Audio_Init(player);
// 5. 计算剩余字节数
player->total_size = player->header.subchunk2_size;
player->bytes_remaining = player->total_size;
player->position = sizeof(WAV_Header_t);
// 6. 预填充两个缓冲区
Fill_Audio_Buffer(player, 0); // 缓冲区0
Fill_Audio_Buffer(player, 1); // 缓冲区1
// 7. 启动DMA传输
HAL_DMA_Start_IT(player->htim->hdma[TIM_DMA_ID_CC1],
(uint32_t)player->buffer[0],
(uint32_t)&player->htim->Instance->CCR1,
player->header.num_channels * 1024); // 1024个样本
// 8. 启动定时器
HAL_TIM_PWM_Start_DMA(player->htim, player->channel_left,
(uint32_t*)player->buffer[0],
player->header.num_channels * 1024);
if (player->header.num_channels == 2) {
// 立体声,也启动右声道
HAL_TIM_PWM_Start_DMA(player->htim, player->channel_right,
(uint32_t*)player->buffer[0] + 1,
player->header.num_channels * 1024);
}
player->state = AUDIO_PLAYING;
return 1;
}
3、填充音频缓冲区
c
// 填充音频缓冲区
void Fill_Audio_Buffer(Audio_Player_t *player, uint8_t buffer_idx)
{
UINT bytes_read;
uint16_t *buffer = player->buffer[buffer_idx];
uint32_t samples_to_read;
// 计算需要读取的样本数
samples_to_read = 1024; // 缓冲区大小
if (player->bytes_remaining < samples_to_read * player->header.num_channels * 2) {
samples_to_read = player->bytes_remaining / (player->header.num_channels * 2);
}
if (samples_to_read == 0) {
// 文件结束,填充静音
memset(buffer, 0, 1024 * sizeof(uint16_t) * player->header.num_channels);
return;
}
// 读取音频数据
f_read(&player->file, buffer,
samples_to_read * player->header.num_channels * 2, &bytes_read);
// 更新位置和剩余字节
player->position += bytes_read;
player->bytes_remaining -= bytes_read;
// 音量调节
Apply_Volume(buffer, samples_to_read * player->header.num_channels, player->volume);
// 如果读取的样本不足,剩余部分填0
uint32_t total_samples = samples_to_read * player->header.num_channels;
if (total_samples < 1024 * player->header.num_channels) {
memset(&buffer[total_samples], 0,
(1024 * player->header.num_channels - total_samples) * sizeof(uint16_t));
}
}
4、音量控制
c
// 应用音量控制
void Apply_Volume(uint16_t *buffer, uint32_t sample_count, float volume)
{
if (volume == 1.0f) return; // 最大音量,不需要处理
for (uint32_t i = 0; i < sample_count; i++) {
// 16位有符号转换
int16_t sample = (int16_t)buffer[i];
sample = (int16_t)(sample * volume);
buffer[i] = (uint16_t)sample;
}
}
七、DMA中断处理
c
// stm32f4xx_it.c
#include "stm32f4xx_it.h"
#include "audio_player.h"
extern Audio_Player_t audio_player;
// DMA传输完成中断
void DMA1_Stream5_IRQHandler(void)
{
// 半传输完成
if (__HAL_DMA_GET_FLAG(DMA1_Stream5, DMA_FLAG_HTIF1_5)) {
__HAL_DMA_CLEAR_FLAG(DMA1_Stream5, DMA_FLAG_HTIF1_5);
// 填充刚刚传输完的缓冲区
uint8_t buffer_to_fill = audio_player.current_buffer;
Fill_Audio_Buffer(&audio_player, buffer_to_fill);
}
// 全传输完成
if (__HAL_DMA_GET_FLAG(DMA1_Stream5, DMA_FLAG_TCIF1_5)) {
__HAL_DMA_CLEAR_FLAG(DMA1_Stream5, DMA_FLAG_TCIF1_5);
// 切换缓冲区
audio_player.current_buffer = !audio_player.current_buffer;
// 填充另一个缓冲区
uint8_t buffer_to_fill = audio_player.current_buffer;
Fill_Audio_Buffer(&audio_player, buffer_to_fill);
// 检查是否播放结束
if (audio_player.bytes_remaining == 0) {
Audio_Stop(&audio_player);
}
}
}
八、主程序
c
// main.c
#include "main.h"
#include "audio_player.h"
#include "fatfs.h"
#include <stdio.h>
FATFS fs; // FatFs工作区
FIL file; // 文件对象
DIR dir; // 目录对象
FILINFO fno; // 文件信息
Audio_Player_t player;
int main(void)
{
HAL_Init();
SystemClock_Config();
// 1. 初始化SD卡
MX_SDIO_SD_Init();
MX_FATFS_Init();
// 2. 挂载文件系统
f_mount(&fs, "", 0);
// 3. 初始化音频播放器
Audio_Init(&player);
printf("Audio Player Ready\r\n");
// 4. 播放音频文件
if (Audio_Play_File(&player, "test.wav")) {
printf("Playing: test.wav\r\n");
} else {
printf("Failed to play file\r\n");
}
while (1)
{
// 主循环(音频播放由DMA中断处理)
// 可以添加控制功能
// 如:播放/暂停/停止/音量控制
// 示例:按键控制
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
Audio_Pause(&player);
}
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) {
Audio_Resume(&player);
}
HAL_Delay(10);
}
}
参考代码 STM32读取SD卡中的音频数据,PWM输出 www.youwenfan.com/contentcsu/70038.html
九、播放控制函数
c
// 停止播放
void Audio_Stop(Audio_Player_t *player)
{
if (player->state != AUDIO_PLAYING) return;
// 停止DMA
HAL_TIM_PWM_Stop_DMA(player->htim, player->channel_left);
if (player->header.num_channels == 2) {
HAL_TIM_PWM_Stop_DMA(player->htim, player->channel_right);
}
// 停止定时器
HAL_TIM_PWM_Stop(player->htim, player->channel_left);
HAL_TIM_PWM_Stop(player->htim, player->channel_right);
// 关闭文件
f_close(&player->file);
player->state = AUDIO_STOP;
}
// 暂停播放
void Audio_Pause(Audio_Player_t *player)
{
if (player->state != AUDIO_PLAYING) return;
// 停止DMA传输
HAL_TIM_PWM_Stop_DMA(player->htim, player->channel_left);
if (player->header.num_channels == 2) {
HAL_TIM_PWM_Stop_DMA(player->htim, player->channel_right);
}
player->state = AUDIO_PAUSED;
}
// 恢复播放
void Audio_Resume(Audio_Player_t *player)
{
if (player->state != AUDIO_PAUSED) return;
// 重新启动DMA
HAL_TIM_PWM_Start_DMA(player->htim, player->channel_left,
(uint32_t*)player->buffer[player->current_buffer],
player->header.num_channels * 1024);
if (player->header.num_channels == 2) {
HAL_TIM_PWM_Start_DMA(player->htim, player->channel_right,
(uint32_t*)player->buffer[player->current_buffer] + 1,
player->header.num_channels * 1024);
}
player->state = AUDIO_PLAYING;
}
// 设置音量
void Audio_Set_Volume(Audio_Player_t *player, float volume)
{
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
player->volume = volume;
}
十、硬件滤波电路
1、一阶RC低通滤波
PWM输出 → 1kΩ电阻 → 输出 → 1uF电容 → GND
↓
音频功放
- 截止频率:fc = 1/(2πRC) ≈ 160Hz
2、二阶有源低通滤波
PWM → 10kΩ → 运放反相端
↓
10kΩ → 输出
↓
0.1uF电容 → GND
↓
0.1uF电容 → 反馈
- 使用NE5532/OPA2134运放
- 截止频率:~20kHz
十一、常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 爆音/噪音 | PWM频率不够高 | 提高定时器频率 |
| 声音失真 | 滤波器截止频率不对 | 调整RC值 |
| 播放卡顿 | SD卡读取慢 | 使用DMA双缓冲 |
| 无声音 | 音量过小/静音 | 检查音量设置 |