一、概述
MAX98357是一个性能非常好的声卡功放芯片,但是由于该芯片是使用I²S串行总线来驱动的,而STM32只有F4/F7/H7等高端系列才有I²S外设, 所以目前网上STM32驱动MAX98357的资料还是比较少的,基本上都是ESP32的。
再者就是我们要把I²S 和 I²C 的区别开来,它们是为完全不同的目的而设计 的两种通信协议 。I²S 是用于传输音频数据的,I²C 是一般用于控制芯片的。
详细解释:
I²S - 音频传输专家
I²S 的唯一目标就是无损地传输数字音频。它的设计非常简单直接,有3根总线::
-
SD: 传输实际的音频数据(比如左声道的16位样本,然后是右声道的16位样本)。
-
SCK: 为每个数据位提供一个时钟脉冲。
-
WS: 告诉接收端当前传输的是左声道数据还是右声道数据。当WS为低电平时通常表示左声道,高电平表示右声道。
I²C - 控制总线管家
I²C 是为系统中各种芯片之间的短距离、低速通信而设计的。它就像一个系统管家,负责询问和配置各个部件。I²C有2根总线:
-
SDA: 用于传输和接收数据。这根线是双向的。
-
SCL: 由主设备产生,同步数据传输。
二、硬件连接
经过查询STM32资料,我们可以知道STM32的I²S是基于SPI 实现的,那我就拿STM32F429来举个例子,通过查询STM32F429 的数据手册和参考手册:
STM32F429的SPI和I²S的引脚定义总结表:
| I2S 外设 | 信号 | 引脚选项 | 复用 AF | 备注 |
|---|---|---|---|---|
| I2S1 (SPI1) | WS (LRCK) | PA4 / PB12 | AF5 | 帧同步信号 |
| CK (SCK) | PB3 / PA5 | AF5 | 时钟 | |
| SD (SDI/SDO) | PA7 / PB5 | AF5 | 数据线 | |
| MCK | PC4 | AF5 | 主时钟输出(可选) | |
| I2S2 (SPI2) | WS | PB9 / PB12 | AF5 | 帧同步信号 |
| CK | PB10 / PB13 | AF5 | 时钟 | |
| SD | PB15 / PC3 | AF5 | 数据线 | |
| MCK | PC6 | AF5 | 主时钟输出 | |
| I2S3 (SPI3) | WS | PA4 / PA15 | AF6 | 帧同步信号 |
| CK | PB3 | AF6 | 时钟 | |
| SD | PB5 | AF6 | 数据线 | |
| MCK | PC7 | AF6 | 主时钟输出 | |
| I2S2ext / I2S3ext | SD_EXT(全双工 Rx) | PC2(I2S2ext) / PC11(I2S3ext) | AF6 | 专用于接收通道 |
STM32F429的DMA1和DMA2的 请求映射表:


下面就是STM32F429的复用功能映射表:

例如: 我们采用STM32F429 的I2S2 来驱动我们的MAX98357,那么通过查询上面的表格,我们可以清楚的总结出来以下关键的软硬件参数配置 :
GPIO引脚和复用功能使用:
|-----------------|-----|-------------|-----|-------|
| I2S2 (SPI2) | WS | PB9 / PB12 | AF5 | 帧同步信号 |
| | CK | PB10 / PB13 | AF5 | 时钟 |
| | SD | PB15 / PC3 | AF5 | 数据线 |
| | MCK | PC6 | AF5 | 主时钟输出 |
DMA通道和数据流使用: DMA1 通道0 数据流4
三、I2S驱动代码编写
1.定义I2S和DMA句柄
I2S_HandleTypeDef hi2s2;
DMA_HandleTypeDef hdma_spi2_tx;
2.配置和初始化I2S
// I2S2初始化函数
void MX_I2S2_Init(void)
{
__HAL_RCC_SPI2_CLK_ENABLE();
hi2s2.Instance = SPI2;
hi2s2.Init.Mode = I2S_MODE_MASTER_TX;
hi2s2.Init.Standard = I2S_STANDARD_PHILIPS;
hi2s2.Init.DataFormat = I2S_DATAFORMAT_16B;
hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_22K;
hi2s2.Init.CPOL = I2S_CPOL_LOW;
hi2s2.Init.ClockSource = I2S_CLOCK_PLL;
hi2s2.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
if (HAL_I2S_Init(&hi2s2) != HAL_OK)
{
printf("HAL_I2S_Init Error !\r\n");
while(1);
}
}
3.编写I2S的初始化的MSP回调函数
void HAL_I2S_MspInit(I2S_HandleTypeDef* hi2s)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hi2s->Instance == SPI2)
{
/* 使能外设时钟 */
__HAL_RCC_SPI2_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 2. 配置I2S3引脚 (WS, SCK, SD, MCK) */
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* 3. 配置DMA */
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_spi2_tx.Instance = DMA1_Stream4;
hdma_spi2_tx.Init.Channel = DMA_CHANNEL_0;
hdma_spi2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi2_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_spi2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_spi2_tx.Init.Mode = DMA_NORMAL;
// hdma_spi2_tx.Init.Mode = DMA_CIRCULAR;
hdma_spi2_tx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_spi2_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi2_tx) != HAL_OK)
{
printf("HAL_DMA_Init Error !\r\n");
while(1);
}
__HAL_LINKDMA(hi2s, hdmatx, hdma_spi2_tx);
/* 配置中断优先级并使能 */
HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn);
}
}
4.编写DMA中断服务函数
/* DMA IRQ Handler(放在 stm32f4xx_it.c 或同文件) */
void DMA1_Stream4_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_spi2_tx);
}
5.编写I2S传输回调函数
void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
/* 半完成回调(双缓冲/分段填充时使用)*/
(void)hi2s;
}
/* 可选:I2S/ DMA 回调(当需要在回调中填充下一个缓冲区时使用) */
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
/* 完成回调(如果需要)*/
(void)hi2s;
}
6.编写音频生成代码(正弦波声音)
/* 生成正弦波(立体声交织):
* 参数:
* buffer: 指向 int16_t 数组,长度 = frames*2
* frames: 要生成的帧数(每帧含 L 和 R)
* amplitude: 0.0 - 1.0
* frequency: 正弦频率(Hz)
* sample_rate: 采样率(Hz)
*/
void generate_sine_wave(int16_t* buffer, uint32_t frames, float amplitude, float frequency, uint32_t sample_rate)
{
for (uint32_t n = 0; n < frames; n++)
{
float t = (float)n / (float)sample_rate;
float sample_f = amplitude * sinf(2.0f * PI * frequency * t);
/* 限幅 */
if (sample_f > 1.0f) sample_f = 1.0f;
if (sample_f < -1.0f) sample_f = -1.0f;
int16_t sample = (int16_t)(sample_f * 32767.0f);
/* 交织到左右声道,这里左右相同(单声道复制为立体声) */
buffer[2 * n] = sample; /* Left */
buffer[2 * n + 1] = sample; /* Right */
}
}
当然,如果你的MCU的RAM空间足够那你也可以测试下生成个《欢乐颂》音频:
// 生成单个音符的函数
uint32_t generate_note(int16_t* buffer, float frequency, uint32_t duration, float amplitude, uint32_t sample_rate)
{
// 添加淡入淡出效果,避免爆音
uint32_t fade_duration = duration / 10; // 淡入淡出时间为音符时长的10%
if (fade_duration < 50) fade_duration = 50; // 最小淡入淡出时间
for (uint32_t i = 0; i < duration; i++) {
// 计算正弦波样本
float sample = amplitude * sin(2 * PI * frequency * i / sample_rate);
// 应用淡入淡出效果
float fade_factor = 1.0f;
if (i < fade_duration) {
// 淡入
fade_factor = (float)i / fade_duration;
} else if (i > duration - fade_duration) {
// 淡出
fade_factor = (float)(duration - i) / fade_duration;
}
buffer[i] = (int16_t)(sample * fade_factor * 32767);
// printf("%x ",buffer[i]);
}
return duration;
}
// 生成《欢乐颂》旋律的函数
void generate_ode_to_joy(int16_t* buffer, uint32_t frames, float amplitude, uint32_t sample_rate)
{
uint32_t position = 0;
// 第一小节: E E F G
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_F4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_G4, QUARTER_NOTE, amplitude, sample_rate);
// 第二小节: G F E D
position += generate_note(buffer + position, NOTE_G4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_F4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_D4, QUARTER_NOTE, amplitude, sample_rate);
// 第三小节: C C D E
position += generate_note(buffer + position, NOTE_C4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_C4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_D4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
// 第四小节: E D D (延长)
position += generate_note(buffer + position, NOTE_E4, HALF_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_D4, HALF_NOTE, amplitude, sample_rate);
// 第五小节: E E F G
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_F4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_G4, QUARTER_NOTE, amplitude, sample_rate);
// 第六小节: G F E D
position += generate_note(buffer + position, NOTE_G4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_F4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_D4, QUARTER_NOTE, amplitude, sample_rate);
// 第七小节: C C D E
position += generate_note(buffer + position, NOTE_C4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_C4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_D4, QUARTER_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_E4, QUARTER_NOTE, amplitude, sample_rate);
// 第八小节: D C C (延长)
position += generate_note(buffer + position, NOTE_D4, HALF_NOTE, amplitude, sample_rate);
position += generate_note(buffer + position, NOTE_C4, HALF_NOTE, amplitude, sample_rate);
// 填充剩余部分为静音
while (position < frames) {
buffer[position++] = 0;
}
}
7.编写测试用例
void MAX98357_Test(void)
{
static uint8_t PLAY = 1;
uint32_t ret = 0;
uint32_t CNT = 0;
int16_t *p = NULL;
uint16_t buffSize = 0;
MX_I2S2_Init();
audio_buffer = (int16_t *)mymalloc(SRAMEX,AUDIO_BUFFER_SIZE*2);
if(audio_buffer == NULL)
{
printf("audio_buffer = NULL\r\n");
while(1);
}
// generate_sine_wave((int16_t *)audio_buffer, AUDIO_BUFFER_SIZE,AUDIO_AMPLITUDE, 1000.0f, 22050); //生成 1kHz 正弦,采样率 44100,振幅 AUDIO_AMPLITUDE
generate_ode_to_joy(audio_buffer,AUDIO_BUFFER_SIZE,AUDIO_AMPLITUDE,22050); // 生成《欢乐颂》旋律
p = audio_buffer;
CNT = AUDIO_BUFFER_SIZE;
printf("audio_buffer is OK !(size=%d)\r\n",AUDIO_BUFFER_SIZE);
while(1)
{
if(KEY0_ON == Key_Scan(0)) PLAY = 1;
if(PLAY)
{
printf("Play music!\r\n");
for(uint32_t i=0;i<CNT;)
{
// 启动I2S DMA传输
if(CNT - i > I2S_DMA_MAX_TX_SIZE)
{
buffSize = I2S_DMA_MAX_TX_SIZE;
}
else
{
buffSize = CNT - i;
}
HAL_I2S_Transmit_DMA(&hi2s2, (uint16_t*)p, buffSize);
//HAL_I2S_Transmit(&hi2s2, (uint16_t*)p, buffSize,HAL_MAX_DELAY);
//printf("i=%d buffSize=%d %p\r\n",i,buffSize,p);
while(HAL_I2S_STATE_READY != HAL_I2S_GetState(&hi2s2) && PLAY)
{
ret++;
if(KEY0_ON == Key_Scan(0)) PLAY = 0;
}
if(PLAY == 0)
{
HAL_I2S_DMAStop(&hi2s2);
printf("Stop music!ret=%d\r\n",ret);
break;
}
p += buffSize;
i += buffSize;
ret = 0;
}
p = audio_buffer;
PLAY = 0;
}
LED0_TOGGLE();
delay_ms(20);
}
}
测试效果视频:
MAX98357测试视频
测试的STM32F429_MAX98357驱动代码下载:
【免费】STM32F429-MAX98357驱动代码资源-CSDN下载
https://download.csdn.net/download/qq_34885669/92260957
使用逻辑分析仪查看I2S输出44K采样率的波形:

四、DMA环形缓冲区实现
本文前几个章节讲解了 如何在F429上面使用I2S和DMA驱动MAX98357这个功放芯片,但是在现实中实际的应用场景肯定是没有说像我上面的测试用例是先生成完成一整段的音频,然后再交给DMA去发送那么简单了,往往在实际的场景都是音频和视频都是是实时生成并且是同步的,一边生产音频数据一边DMA要负责搬运发送数据到MAX98357,这里就会涉及到一个问题就是:DMA的搬运音频速度和CPU的生成音频速度一般是不一样的 ,那就会造成音频输出不平滑、卡顿、杂音。 为了解决两者速率不同步问题。我们就需要引入一个环形缓冲区,及时在环形缓冲区里面填充数据,或者适当丢弃一些不同步的数据。
一开始我是想到利用I2S的2个传输完成回调函数HAL_I2S_TxHalfCpltCallback和HAL_I2S_TxCpltCallback来实现生产和消耗的同步,无奈一顿操作下来还是没能做到音频的流畅同步。因为实际情况是很难说你这边DMA刚好传输完成一半数据,同时CPU刚好也生成好一半的数据的。直到我了解到有个函数接口叫**__HAL_DMA_GET_COUNTER()**,问题才得到有效的解决,该接口作用就是查询 DMA 实时传输位置。
利用这个函数接口我们就可以间接计算出来环形缓冲区里面还有多少空间可以写入新的音频数据:
/*
获取当前DMA已经发送完成的音频数据量
*/
static inline uint32_t Audio_GetDmaPlayPos(void)
{
// 使用 HAL 宏或直接访问寄存器
uint32_t ndtr = __HAL_DMA_GET_COUNTER(&audioDMA_Handler); // 获取当前 DMA 流中待传输的剩余数据数量(剩余待发送的音频采样点数据)
uint32_t played = AUDIO_SAMPLES - ndtr; // 计算DMA已经传输完的数据数量(已经完成发送的音频采样点数据)
if (played >= AUDIO_SAMPLES) played %= AUDIO_SAMPLES;
return played;
}
/*
获取当前DMA环形缓冲区可以被写入新音频数据的空间
*/
inline uint32_t Audio_GetWritable(void)
{
uint32_t play = Audio_GetDmaPlayPos();
uint32_t w = audioWritePos; // 记录当前音频数据已经写入到环形缓冲区的位置(有效的可用于发送的音频数据量)
if (w >= play) return AUDIO_SAMPLES - (w - play) - 1; // 计算出环形缓冲区内还有多少空间可以被写入音频数据。
else return (play - w) - 1;
}
那既然知道了实时的环形缓冲区的可支配写入新数据的空间,那在音频数据生产端,我们只需要每次在写缓冲区前先查询下剩余空间,来灵活控制写环形缓冲区,例如:
/* Sound Output 5 Waves - 2 Pulse, 1 Triangle, 1 Noise, 1 DPCM */
void InfoNES_SoundOutput(int samples, BYTE *wave1, BYTE *wave2, BYTE *wave3, BYTE *wave4, BYTE *wave5)
{
uint32_t need = (uint32_t)samples * AUDIO_CHANNELS; // 估算需写的 int16 单元数: samples * channels
uint32_t writable = Audio_GetWritable(); // 如果可写空间不足,选择策略:直接丢弃整帧(简单、不会阻塞)
if (writable < need)
{
samples = writable / AUDIO_CHANNELS; // 可换策略:写入部分样本或降低音量并写入
//return; // 丢弃本帧音频(避免阻塞)
}
for (int i = 0; i < samples; ++i)
{
//输入是8位无符号音频(0-255范围),通过 -128 转换为有符号(-128到+127),避免削波失真
int32_t mix = ( ((int)wave1[i]-128) + ((int)wave2[i]-128) + ((int)wave3[i]-128) + ((int)wave4[i]-128) + ((int)wave5[i]-128) );
mix *= audioVolume ; // 调整音量,避免溢出,我自定义audioVolume范围是0-50,因为超过50我的喇叭有点承受不了了。
if(mix > 32767) mix = 32767;
if(mix < -32768)mix = -32767;
int16_t pcm = (int16_t)mix;
// 写每个声道的数据到音频环形缓冲区
audioDMABuffer[audioWritePos++] = pcm;
if (audioWritePos >= AUDIO_SAMPLES) audioWritePos = 0;
if(AUDIO_CHANNELS == 2)
{
audioDMABuffer[audioWritePos++] = pcm;
if (audioWritePos >= AUDIO_SAMPLES) audioWritePos = 0;
}
}
}
在DMA初始化配置里面我们还需注意下DMA模式要使用循环模式:

注意:环形缓冲区大小需要根据不同实际情况调整大小,太大太小都不行,必须适当,太小可能导致卡顿,太大又会导致播放滞后,建议100~1000ms之间。 生产速度、消费速度、实时性要求找到那个"平衡点"。
STM32H743实现MAX98357驱动代码(I2S+DMA+环形缓冲区):
