STM32CubeIDE Audio播放音频,DAC + TIM + DMA https://blog.csdn.net/sudaroot/article/details/106337717
STM32CubeIDE Audio播放音频,PWM + TIM https://blog.csdn.net/sudaroot/article/details/106373388
STM32CbueIDE Audio播放音频 WM8978 + I2S https://blog.csdn.net/sudaroot/article/details/106528371
STM32CubeIDE Audio播放音频 WM8978 + I2S + DMA双缓存模式 https://blog.csdn.net/sudaroot/article/details/106660351
STM32CubeIDE USB Audio声卡 WM8978 + I2S https://blog.csdn.net/sudaroot/article/details/106834893
本来没想写这篇DMA双缓存文章的,不过想想还是记录一下吧,毕竟自己是健忘患者。
实现DMA双缓存的方式有两种,其一是下面文章这种;还有就是利用单DMA传输 + DMA半传输完成回调函数XferHalfCpltCallback更新数据(这种方式就不提及了,挺简单的)。
这篇《STM32CbueIDE Audio播放音频 WM8978 + I2S》:https://blog.csdn.net/sudaroot/article/details/106528371
小结:
1、首先STM32F4系列芯片的DMA1 和 DMA2是支持双DMA缓存区模式的,通过将 DMA_SxCR 寄存器中的 DBM 位置 1,即可使能双缓冲区模式。将自动使能循环模式(DMA_SxCR 中的 CIRC 位位的状态是“无 关”),即DMA发送模式设置无关。
2、由于是双缓存区只有一个缓存区在工作,故可以在一个缓存区工作的时候填充另一个缓存区数据。
3、在缓存区在工作的时候,不支持切换缓存区基地址,否则硬件会使TEIF = 1,中止数据传输。比如DMA_SxM0AR = buf0正在工作,此时你让DMA_SxM0AR = buf1,即出错。
4、可以通过当 DMA_SxCR 寄存器中的 CT 位的值判读哪个缓存区正在工作。如果是做串口DMA接收数据空闲中断,则必须用到(题外话)。
5、双缓存模式的源和目标不支持内存到内存模式。
但是官方HAL库的HAL_DMA_Init()函数不支持双缓冲区模式,却给出了HAL_DMAEx_MultiBufferStart()和HAL_DMAEx_MultiBufferStart_IT()这两个设置双缓存区模式的函数。在轮询模式下使用HAL_DMA_MultiBufferStart()函数或在中断模式下使用HAL_DMA_MultiBufferStart_IT()启动多缓冲区传输。
但是官方又没有给出这两个函数的具体用法,故只能靠自己作了。
故如果要使用DMA双缓存区功能就要自己仿写HAL实现双DMA缓存区功能,由于这里我用的是I2S,即实现HAL_I2S_Transmit_DMA自定义版本,这个版本内部调用的HAL_DMAEx_MultiBufferStart_IT()而非HAL_DMA_Start_IT(),并且还要实现XferCpltCallback()、XferM1CpltCallback()或者XferErrorCallback()这三个回调函数,最后还要DMA_HandleTypeDef指向这三个回调函数。
配置I2C控制WM8978寄存器。
I2S配置。使用的音频还是双声道8KHz_16bit数据,文件还是存储在MCU的内部flash。DMA数据模式我还是配置为Normal,反正和它无关。DMA的数据宽度为什么是Half Word,因为我选的音频数据是16bits和16bit为一帧。
生成代码即可。
读取内部flash的音频文件。datas.h文件存放在音频文件。
WAV_FileInit()初始化文件长度和数据地址。
WAV_FileRead()读取size大小数据,返回值为0表示文件已经读完了。
#include
#include
#include
#include "datas.h"
#define BUFFER_SIZE 1024
static uint32_t DataLength = 0;
static uint8_t *DataAddress = NULL;
uint16_t I2S_Buf0[BUFFER_SIZE] = { 0 };
uint16_t I2S_Buf1[BUFFER_SIZE] = { 0 };
void WAV_FileInit(void)
{
DataLength = sizeof(data) - 0x2c;
DataAddress = (uint8_t*) (data + 0x2c);
}
uint32_t WAV_FileRead(uint8_t *buf, uint32_t size)
{
uint32_t Playing_End = 0;
if (DataLength >= size)
{
memcpy(buf, DataAddress, size);
DataLength -= size;
DataAddress += size;
Playing_End = 1;
}
else
{
memcpy(buf, DataAddress, DataLength);
Playing_End = 0;
}
return Playing_End;
}
首先先看一下HAL_DMAEx_MultiBufferStart_IT()这个函数源码。红框处发现如果用户没实现下面三个回调函数的话,这个函数使用出错。
那么先实现这三个回调函数。
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
这个回调函数是第1个缓存区Buffer0发送完成后中断会调用回调函数,此时可以去填充该函数下一轮的数据。
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
这个回调函数是第2个缓存区Buffer1发送完成后中断会调用回调函数,此时可以去填充该函数下一轮的数据。
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
这个回调函数是传输数据出错的回调函数。出错原因上面有小结,认真看,谢谢~@。
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA 第1个缓存区半传输完成回调
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA 第2个缓存区半传输完成回调
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); // DMA 传输中止回调
其余的回调函数我用不上,让DMA_HandleTypeDef hdma_spi2_tx 对应的函数指针 = NULL。
static void DMAEx_XferCpltCallback(struct __DMA_HandleTypeDef *hdma)
{
if (WAV_FileRead((uint8_t*) I2S_Buf0, sizeof(I2S_Buf0)) == 0)
{
Audio_Player_Stop();
}
}
static void DMAEx_XferM1CpltCallback(struct __DMA_HandleTypeDef *hdma)
{
if (WAV_FileRead((uint8_t*) I2S_Buf1, sizeof(I2S_Buf1)) == 0)
{
Audio_Player_Stop();
}
}
static void DMAEx_XferErrorCallback(struct __DMA_HandleTypeDef *hdma)
{
}
先复制一份HAL库的HAL_I2S_Transmit_DMA()源码,函数修改为HAL_I2S_Transmit_DMAEx(),如下:
HAL_StatusTypeDef HAL_I2S_Transmit_DMAEx(I2S_HandleTypeDef *hi2s, uint16_t *FirstBuffer, uint16_t *SecondBuffer, uint16_t Size)
{
uint32_t tmpreg_cfgr;
if ((FirstBuffer == NULL) || (SecondBuffer == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(hi2s);
if (hi2s->State != HAL_I2S_STATE_READY)
{
__HAL_UNLOCK(hi2s);
return HAL_BUSY;
}
/* Set state and reset error code */
hi2s->State = HAL_I2S_STATE_BUSY_TX;
hi2s->ErrorCode = HAL_I2S_ERROR_NONE;
hi2s->pTxBuffPtr = FirstBuffer;
tmpreg_cfgr = hi2s->Instance->I2SCFGR
& (SPI_I2SCFGR_DATLEN | SPI_I2SCFGR_CHLEN);
if ((tmpreg_cfgr == I2S_DATAFORMAT_24B)
|| (tmpreg_cfgr == I2S_DATAFORMAT_32B))
{
hi2s->TxXferSize = (Size << 1U);
hi2s->TxXferCount = (Size << 1U);
}
else
{
hi2s->TxXferSize = Size;
hi2s->TxXferCount = Size;
}
/* Set the I2S Tx DMA Half transfer complete callback */
hi2s->hdmatx->XferHalfCpltCallback = NULL;
hi2s->hdmatx->XferM1HalfCpltCallback = NULL;
/* Set the I2S Tx DMA transfer complete callback */
hi2s->hdmatx->XferCpltCallback = DMAEx_XferCpltCallback;
hi2s->hdmatx->XferM1CpltCallback = DMAEx_XferM1CpltCallback;
/* Set the DMA error callback */
hi2s->hdmatx->XferErrorCallback = DMAEx_XferErrorCallback;
/* Set the DMA abort callback */
hi2s->hdmatx->XferAbortCallback = NULL;
/* Enable the Tx DMA Stream/Channel */
if (HAL_OK != HAL_DMAEx_MultiBufferStart_IT(hi2s->hdmatx, (uint32_t) FirstBuffer, (uint32_t) &hi2s->Instance->DR, (uint32_t) SecondBuffer, hi2s->TxXferSize))
{
/* Update SPI error code */
SET_BIT(hi2s->ErrorCode, HAL_I2S_ERROR_DMA);
hi2s->State = HAL_I2S_STATE_READY;
__HAL_UNLOCK(hi2s);
return HAL_ERROR;
}
/* Check if the I2S is already enabled */
if (HAL_IS_BIT_CLR(hi2s->Instance->I2SCFGR, SPI_I2SCFGR_I2SE))
{
/* Enable I2S peripheral */
__HAL_I2S_ENABLE(hi2s);
}
/* Check if the I2S Tx request is already enabled */
if (HAL_IS_BIT_CLR(hi2s->Instance->CR2, SPI_CR2_TXDMAEN))
{
/* Enable Tx DMA Request */
SET_BIT(hi2s->Instance->CR2, SPI_CR2_TXDMAEN);
}
__HAL_UNLOCK(hi2s);
return HAL_OK;
}
剩下就是初始化WM8978芯片 和 功能逻辑代码。
#define WM8978_ADDRESS 0x1A
#define WM8978_WIRTE_ADDRESS (WM8978_ADDRESS << 1 | 0)
extern I2C_HandleTypeDef hi2c1;
extern I2S_HandleTypeDef hi2s2;
extern DMA_HandleTypeDef hdma_spi2_tx;
HAL_StatusTypeDef WM8978_Register_Wirter(uint8_t reg_addr, uint16_t data)
{
uint8_t pData[10] = { 0 };
pData[0] = (reg_addr << 1) | ((data >> 8) & 0x01);
pData[1] = data & 0xFF;
return HAL_I2C_Master_Transmit(&hi2c1, WM8978_WIRTE_ADDRESS, pData, 2, 1000);
}
void Audio_Player_Init(void)
{
WM8978_Register_Wirter(0, 0); // 软复位
WM8978_Register_Wirter(1, 0x0F); // 模拟放大器使能, 使能输出输入缓存区
WM8978_Register_Wirter(3, 0x7F); // 使能左右声道和L\ROUT2
WM8978_Register_Wirter(4, 0x10); // I2S 16bit
WM8978_Register_Wirter(6,0); // MCU提供时钟
WM8978_Register_Wirter(10, 0x08); // 输出音质最好
WM8978_Register_Wirter(43, 0x10); // ROUT2反相
WM8978_Register_Wirter(54,30); // 设置LOUT2左声道音量
WM8978_Register_Wirter(55,30|(1<<8)); // 设置ROUT2右声道音量, 更新左右声道音量
}
void Audio_Player_Start(void)
{
WAV_FileInit();
WAV_FileRead((uint8_t*) I2S_Buf0, sizeof(I2S_Buf0));
WAV_FileRead((uint8_t*) I2S_Buf1, sizeof(I2S_Buf1));
HAL_I2S_Transmit_DMAEx(&hi2s2, I2S_Buf0, I2S_Buf1, BUFFER_SIZE);
}
void Audio_Player_Pause(void)
{
HAL_I2S_DMAPause(&hi2s2);
}
void Audio_Player_Resume(void)
{
HAL_I2S_DMAResume(&hi2s2);
}
void Audio_Player_Stop(void)
{
WAV_FileInit();
HAL_I2S_DMAStop(&hi2s2);
}
主函数:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_I2C1_Init();
MX_I2S2_Init();
MX_USART1_UART_Init();
printf("Sudaroot\r\n");
HAL_Delay(1000);
Audio_Player_Init();
while (1)
{
printf("开始播放\r\n");
Audio_Player_Start();
HAL_Delay(5000);
printf("暂停播放\r\n");
Audio_Player_Pause();
HAL_Delay(3000);
printf("继续播放\r\n");
Audio_Player_Resume();
HAL_Delay(10000);
printf("停止播放\r\n");
Audio_Player_Stop();
HAL_Delay(5000);
}
}
现象:播放5s音频,暂停3s后继续播放。10s后停止播放。循环。
源码:https://download.csdn.net/download/sudaroot/12510712
全篇完。
本人是一个嵌入式未入门小白,博客仅仅代表我个人主观见解方便记录成长笔记。 若有与大神大大见解有冲突,我坚信大神大大见解是对的,我的是错的。 若无法下载源码,可私聊私发。 感谢~!