【STM32+MAX98357】MP3转PCM存入Flash并控制音频输出
一、前言
针对MCU内存资源受限问题,可将资源预先烧录至外部Flash,此前《LVGL从外部Flash读取并显示图片:自定义解码器方案》 已详细阐述图片资源的读取与显示方法。本文将以音频场景,介绍基于外部Flash的MP3音频播放输出控制方案。

二、系统介绍
1、项目实现的功能
(1)将不同音频格式的文件转换成PCM文件。
(2)将PCM音频数据写入外部Flash存储。
(3)读取外部音频数据并控制播放。
2、项目硬件组成
(1)主控芯片:STM32F103RCT6。
(2)音频功放模块:使用MAX98357模块,是一款I2S接口D类音频功放模块。
(3)喇叭:4欧3.2W喇叭。
(4)Flash存储器:W25Q64。
三、音频文件转换成PCM文件
1、音频格式选择
常用的音频格式有MP3、WAV等格式:
(1)MP3格式:属于有损压缩格式,存储占用空间小,但需额外解码运算,会大量消耗MCU算力与内存资源。
(2)WAV格式:完整保留原始音频音质与全量信息,但不同文件的头结构存在差异,需解析不同头信息才能读取音频数据。


PCM是音频的原始裸数据格式,各类压缩音频格式均可通过解码转换为标准PCM流。以MP3为例,其压缩编码后的音频数据,经完整解码流程后可还原为原始PCM采样序列。转换得到的PCM数据不携带任何格式头、元信息等附加字段,仅保留纯粹的音频波形采样值。
2、安装FFmpeg软件
(1)在FFmpeg官网下载软件,根据所用平台下载对应版本。如下图为使用Windows系统平台选择的版本。
官网下载网址:https://ffmpeg.org/download.html


(2)将压缩包解压到安装目录,并添加环境变量。进入高级系统设置窗口界面,按下图顺序添加安装目录下bin文件路径。

(3)打开命令提示符(CMD),输入"ffmpeg -version"命令并回车。若终端正常显示FFmpeg的版本号及编译信息,即表示安装成功。

3、使用FFmpeg命令转换
(1)命令的基本格式为:
c
ffmpeg [全局选项] {[输入选项] -i 输入源} ... {[输出选项] 输出目标} ...
*全局选项:全局参数,如 -y 覆盖输出、-n 不覆盖、-loglevel 设置日志,影响整个进程 。
*输入块:可重复多次,格式为 [输入特定选项] -i <输入源>;-i 前参数仅对该输入生效(如 -ss 跳时、-t 限时、-f 强制输入格式)。
*输出块:可重复多次,格式为 [输出特定选项] <输出路径>;-i 后、输出文件名前的参数仅对该输出生效(如 -c:v 编码、-b:v 码率、-s 分辨率)。
(2)关于音频常用的FFmpeg命令选项:
c
-i:指定输入文件路径(必须位于输出选项之前)。
-y:强制覆盖输出文件。
-n:若文件存在则不执行。
-f:强制指定封装格式。
-ss:设置起始时间点(支持 HH:MM:SS 或秒数),置于 -i 前为快速定位,置于后为精确解码。
-t:设置处理时长。
-to:设置结束时间点。
-c:a / -acodec:指定音频编码器;设为 copy 保持原音频流不变。
-b:a:设置音频码率(如 128k, 192k)。
-ar:设置音频采样率(常用 44100, 48000)。
-ac:设置音频声道数(1 单声道,2 立体声)。
-an:禁用音频流(仅保留视频)。
(3)使用命令提示符(CMD),输入PPmpeg命令将不同音频格式转换成PCM格式文件:
c
ffmpeg -i demo.mp3 -ar 44100 -ac 2 -f s16le -acodec pcm_s16le demo.pcm
ffmpeg -i demo.wav -ar 44100 -ac 2 -f s16le -acodec pcm_s16le demo.pcm
命令选项:
-ar 44100:音频采样率设为44100Hz。
-ac 2:声音设为双声道。
-f s16le:16位小端PCM格式。
-acodec pcm_s16le:指定使用PCM格式。
(还可以设置音频的比特率,如-b:a 128000)
执行命令后,若终端提示生成成功,目录下将自动出现PCM格式文件;若执行失败,请检查命令格式及参数选项是否正确。

四、PCM数据存外部Flash
关于外部 Flash 烧录算法的具体实现步骤,请参阅《解决MCU内存不足:LVGL资源外部Flash烧录教程》该文章。
在串口烧录阶段,需加载已生成的PCM文件,并将数据包大小配置为 256 字节(须与固件代码中的缓冲区定义保持一致),同时根据实际通信稳定性调整自动连续发送的延时参数。

五、读音频数据并控制播放
1、生成项目代码
STM32与外部芯片的引脚连接方式如下。MAX98357模块的SD脚用于控制开关机,上电默认为高电平,处于开机状态。


使用STM32CubeMX生成工程框架时,需依据外接芯片的数据手册进行外设参数配置。重点核对通信协议时序、电气特性及引脚复用功能,确保软件配置与硬件规格匹配。


为实现音频数据的连续流畅播放,I2S外设应配置DMA传输模式为"Circular"(循环模式),以确保缓冲区数据无缝衔接。I2S数据帧格式为16位,则DMA数据宽度设置为"Half Word"(半字,即16位),从而保证内存与外设间的数据对齐。

2、I2S音频数据控制代码
通过DMA控制器实现I2S音频数据的启动与停止传输。为确保数据对齐及传输效率,传输缓冲区的数据宽度为16位。
c
/**
* @brief 启动I2S DMA传输
* @param pData: 指向音频数据缓冲区的指针 (16位 half-word)
* @param Size: 数据长度
*/
HAL_StatusTypeDef I2S_Audio_Play_DMA(uint16_t* pData, uint16_t Size)
{
return HAL_I2S_Transmit_DMA(&hi2s3, pData, Size);
}
/**
* @brief 停止I2S DMA传输
*/
void I2S_Audio_Stop(void)
{
HAL_I2S_DMAStop(&hi2s3);
}
3、SPI读取外部Flash数据代码
本项目采用SPI通信从外部Flash存储器中读取音频数据。需要注意,SPI传输单元为 8 位。
c
//读取Flash:在指定地址开始读取指定长度的数据
//pBuffer:数据存储数组
//ReadAddr:读取的起始地址
//NumByteToRead:读取的字节数
void SPI_Flash_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead)
{
uint8_t send_buff[4] = {W25X_ReadData,0x00,0x00,0x00};
send_buff[1] = (ReadAddr >> 16) & 0xff;
send_buff[2] = (ReadAddr >> 8) & 0xff;
send_buff[3] = ReadAddr & 0xff;
SPI_Flash_Wait_Busy();
HAL_GPIO_WritePin(F_CS_GPIO_Port, F_CS_Pin, GPIO_PIN_RESET); //使能片选
HAL_SPI_Transmit_IT(&hspi1,send_buff,4); //发送读取命令 + 24bit地址
HAL_SPI_Receive_IT(&hspi1,pBuffer,NumByteToRead); //读数据
HAL_GPIO_WritePin(F_CS_GPIO_Port, F_CS_Pin, GPIO_PIN_SET); //取消片选
}
4、PCM数据处理代码
(1)由于PCM仅包含原始音频采样数据,需先定义一个关于存储在外部Flash音频信息的结构体。该结构体用于存储音频属性参数,以便MCU在读取数据前解析并正确配置音频解码与播放外设。
c
#define AUDIO_BUF_SIZE 1024
#define PLAYER_IDLE 0 //闲置
#define PLAYER_PLAYING 1 //播放中
#define PLAYER_EOF 2 //播放结束
static uint16_t AudioBuf[AUDIO_BUF_SIZE]; // 音频数据缓冲区
uint16_t bytesToRead = AUDIO_BUF_SIZE*2; // 音频数据缓冲大小:Flash中数据为8bit,I2S数据流为16bit
typedef struct {
uint32_t StartAddress; // 音频数据存储在外部Flash的起始地址
uint32_t CurrentDataOffset; // 记录地址偏移量
uint32_t DataLength; // 音频数据总长度大小
uint8_t PlayerState; // 音频播放状态
} AudioPlayer_TypeDef;
// 定义音频信息
static AudioPlayer_TypeDef Audio_MP3 ={
.StartAddress = 90336,
.CurrentDataOffset = 0,
.DataLength = 2328480,
.PlayerState = PLAYER_IDLE,
};
(2)在启动音频播放前,需预先从外部Flash读取音频数据并填充至内存缓冲区。该缓冲区作为I2S外设的数据源。
c
/**
* @brief 从Flash填充音频缓冲区
*/
static void PCM_Fill_Buffer(void)
{
if (Audio_MP3.CurrentDataOffset >= Audio_MP3.DataLength)
{
Audio_MP3.PlayerState = PLAYER_EOF;
memset(AudioBuf, 0, sizeof(AudioBuf)); // 播放静音
return;
}
if (Audio_MP3.CurrentDataOffset + bytesToRead > Audio_MP3.DataLength)
{
bytesToRead = Audio_MP3.DataLength - Audio_MP3.CurrentDataOffset;
memset(AudioBuf, 0, sizeof(AudioBuf)); // 剩余部分补零
}
// 从Flash读取数据到AudioBuf
SPI_Flash_Read((uint8_t*)AudioBuf, Audio_MP3.StartAddress + Audio_MP3.CurrentDataOffset, bytesToRead);
// 偏移量递增
Audio_MP3.CurrentDataOffset += bytesToRead;
}
在处理16位PCM数据流时,必须将连续的两个8位字节重组为16位半字(Half Word),以确保音频数据的完整性与正确解析。
(3)使用I2S通过DMA传输上一步得到的缓冲区内的数据。
c
/**
* @brief 读取PCM数据并启动音频播放
*/
void PCM_Player_Prg(void)
{
Audio_MP3.PlayerState = PLAYER_PLAYING;
// 预填充缓冲区
PCM_Fill_Buffer();
// 启动I2S DMA
I2S_Audio_Play_DMA(AudioBuf, AUDIO_BUF_SIZE);
}
(4)在DMA循环传输模式下,I2S半传输或全传输完成时会自动触发中断,调用用户自定义的回调函数。只需在该回调中从外部Flash读取数据填充音频缓冲区即可,无需重复启动I2S的DMA传输,以此保障音频流连续、低延迟输出。
c
/**
* @brief I2S接收回调函数
*/
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
if (hi2s->Instance == SPI3)
{
if (Audio_MP3.PlayerState == PLAYER_PLAYING)
{
// 填充缓冲区 (启动SPI DMA读取下一块数据)
PCM_Fill_Buffer();
}
else if(Audio_MP3.PlayerState == PLAYER_EOF)
{
I2S_Audio_Stop();
}
}
}
六、播放声音调节
1、增益选择
MAX98357仅支持硬件方式调节增益,通过在模块的GAIN引脚外接不同的硬件电路,即可实现多档位增益的配置。

2、其他音频处理方法
(1)可选用任意音频格式转换工具,将原始音频的音量调整至适配播放场景的合适大小,再将处理后的音频导出为PCM文件,最终烧录存入外部SPI Flash中。
(2)可通过软件算法对音频做数字处理实现音量调节,将PCM采样值乘以衰减系数可降低音量,搭配限幅逻辑避免增益过大引发波形削顶失真,实现灵活可控的音量调整。
七、总结
本方案针MCU内存不足问题,将MP3(或其他格式)解码得到的PCM裸音频数据预先烧录至外部SPI Flash。基于STM32CubeMX完成外设配置,通过SPI以8位为单位读取Flash音频数据,重组为16位PCM格式。采用I2S+循环模式DMA实现音频流连续传输,在传输完成回调中填充下一段音频数据,无需重复启动DMA。搭配MAX98357硬件增益调节,也可自定义代码对PCM数据做衰减、限幅处理,最终实现低延迟、流畅稳定的音频播放。
搜索关注「YouziTech」 ,获取完整代码!
