STM32上使用HAL库完美实现驱动MAX98357声卡模块(I2S+DMA+音频环形缓冲区)
一、概述
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 Gposition += 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 Dposition += 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 Eposition += 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 Gposition += 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 Dposition += 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 Eposition += 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_AMPLITUDEgenerate_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环形缓冲区实现
马上更新。。。。

