基于STM32的音频播放系统,实现SD卡读取音频文件PWM输出播放

基于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双缓冲
无声音 音量过小/静音 检查音量设置
相关推荐
Deitymoon6 小时前
STM32——软件IIC显示字符
stm32·单片机·嵌入式硬件
百万老师6 小时前
自然语言编程时代,如何零基础学习掌握嵌入式编程
c语言·单片机·嵌入式硬件·学习·ai全流程闭环开发
Soari7 小时前
告别商业收费与审核枷锁:深度拆解 Open-Generative-AI,构建 MIT 开源、零过滤的私有化视频生成工作站
人工智能·开源·音视频·私有化部署·sora·ai视频生成·generative-ai
efangfd7 小时前
TXS0104 和 TXB0104 的 IO 驱动电流对比
单片机·嵌入式硬件
leon_teacher7 小时前
HarmonyOS 6 实战:基于 Ads Kit 的插屏广告(视频 + 图片)架构与实现全解析
架构·音视频·harmonyos
小婷资料库7 小时前
新高考日语历年真题、听力音频mp3及答案解析(1998-2025年)
音视频·高考
jushi89997 小时前
抖音APP抖音助手增强版 内置逗音小手 支持无水印下载/音频提取/去广告等功能
android·智能手机·音视频
网管NO.18 小时前
视频核心技术 07:音视频同步与延迟优化 —— 为什么直播会卡顿 / 不同步?怎么解决?
音视频
gihigo19988 小时前
STM32F407 Modbus RTU主站例程
stm32·单片机·嵌入式硬件