【STM32+MAX98357】MP3转PCM存入Flash并控制音频输出

【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」 ,获取完整代码!

相关推荐
沐欣工作室_lvyiyi1 小时前
基于STM32的老人跌倒与防走失报警系统设计与实现(论文+源码)
stm32·单片机·嵌入式硬件
yangmu32031 小时前
万字长文拆解:AI视频生成(SVD/Sora类)的底层技术原理与实现路径
人工智能·音视频
我叫洋洋1 小时前
Allegro 画布“垃圾显示“清理指南:把板子调成干净工作台(实战总结)
c语言·c++·单片机·嵌入式硬件·eda·嘉立创
xiangw@GZ1 小时前
SPI Flash 接口模式与 XIP 技术详解
单片机·嵌入式硬件
沐欣工作室_lvyiyi1 小时前
基于STM32的人体健康检测设计(论文+源码)
stm32·单片机·嵌入式硬件
艾络迅Acceleronix2 小时前
气象监测:STM32+EG800Z AT + 艾络迅平台实战|气压/光照/雨量采集全流程
stm32·单片机·嵌入式硬件·mcu·物联网·1024程序员节
深圳老胡2 小时前
FDC2212 + STM32G030 液面探测:IIC 读数跳动导致下探深度不一致的排查与解决
笔记·stm32·单片机·代码规范·1024程序员节
硅基造物主2 小时前
STM32 从 0 到 1 做智能家居(一):先想清楚要做什么
stm32·mqtt·嵌入式·智能家居·hal库·esp-12f
xiangw@GZ2 小时前
两线 JTAG(cJTAG / IEEE 1149.7)技术专题
单片机·嵌入式硬件