文章目录
1 设计思路
在一些需要播报报警音的场景中,当然可以搭载一个文件系统,然后将音频文件以文件形式存储并读取,这样做不仅麻烦而且特别占用系统资源。
因此,不如将生成好的音乐WAV文件存储到flash,然后通过stm32将flash中的音频数据读入解析并通过DMA搬运到DAC转化为模拟信号输出,本次示例采用DAC1、DAC2两个通道实现双声道的输出。
flash型号W25Q128FV
芯片:stm32f407ZGT
大概步骤如下
-
音频数据存储: 我们首先将经过处理的音频数据保存在外部Flash中,以确保音频内容能够被安全地存储下来。这些数据可以是预先生成的报警音或其他需要播放的声音。
-
Flash读取: 在报警事件触发时,STM32F407微控制器会通过SPI(串行外设接口)与外部Flash通信,以读取存储的音频数据。通过逐字节或分块读取的方式,我们能够有效地将音频数据从Flash读取到微控制器内部。
-
DAC转换: 一旦音频数据被读取,它将被送入数字模拟转换器(DAC)的输入通道。这些DAC通道,比如DAC1和DAC2,将数字音频数据转化为模拟信号,这个信号可以在扬声器或者其他音频输出设备中得到放大并播放出来。
-
双声道输出: 通过同时使用DAC1和DAC2两个通道,我们能够实现双声道的模拟输出。这意味着我们可以分别控制左声道和右声道的音频数据,从而在播放报警音时创造更加逼真和震撼的声音效果。
2 DAC介绍
DAC(Digital-to-Analog Converter,数模转换器),用于将数字信号转换为模拟信号。它将数字数据转换为相应的模拟电压或电流输出,以便在模拟电路中使用。DAC在许多领域中都有广泛的应用,如音频处理、通信系统、控制系统等。
DAC工作框图如下图所示
引脚说明
模拟输出信号则是我们音频输出口
STM32F407微控制器具有2个DAC通道(我们使用的是stm32f407ZGT6),每个通道都有一个输出引脚。这两个DAC通道分别是DAC1和DAC2,它们各自具有一个输出引脚。
其中:
DAC1的输出引脚为PA4
DAC2的输出引脚为PA5
这些引脚允许您通过模拟电压输出来生成相应的模拟信号,从而驱动扬声器播放音乐。
3 实现代码
实现过程:
1.配置了一个定时器 TIM6,用于触发DAC的转换。通过调整定时器的配置,可以实现特定的采样率。
2.实现8位/16位双通道音频数据发送到DAC进行输出。它使用DMA来实现数据的循环播放。
3.实现WAV文件的解析
注:flash读写不再此展示,可以直接看我上传的源代码。
3.1 定时器配置
配置定时器可以定期生成触发信号,该信号用于指示何时应该进行音频采样和转换。通过精确的定时器配置,可以实现固定的采样率,确保音频数据以一致的速率传输。
/**
* @brief TIM6 Configuration
* @param 采用频率
* @retval None
*/
static void TIM6_Config(uint32_t sample_rate)
{
/* 重置定时器为其默认状态 */
TIM_DeInit(TIM6);
/*
设置定时器的触发频率与wav采样率一致
Twav=(1/84Mhz)*(84M/fwav)=1/fwav
*/
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Period = 84000000 / sample_rate;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
/* 选择 TIM6 输出触发源 */
TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update);
TIM_Cmd(TIM6, ENABLE);
}
3.2 DAC配置
3.2.1 DAC 双通道 8-bit 循环播放配置
/**
* @brief DAC 双通道 8-bit 循环播放配置
* @param buffer 无符号 8-bit 双通道音频数据, 采样率 24000Hz
* @param size 数据长度 (总字节数)
* @retval None
*/
static void DAC_Dual8bitCircular_Config(const void *buffer, uint32_t size)
{
/* DAC 配置 */
DAC_DeInit();
DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Init(DAC_Channel_2, &DAC_InitStructure);
DAC_ClearFlag(DAC_Channel_1, DAC_FLAG_DMAUDR);
DAC_ClearFlag(DAC_Channel_2, DAC_FLAG_DMAUDR);
DAC_ITConfig(DAC_Channel_1, DAC_IT_DMAUDR, ENABLE);
DAC_ITConfig(DAC_Channel_2, DAC_IT_DMAUDR, ENABLE);
/* DMA1_Stream5 channel7 **************************************/
/* 我们使用双通道 DAC 模式, 仅需接收一个 DAC 通道的 Request, 因此只配置 DAC channel1 的 DMA*/
DMA_DeInit(DMA1_Stream5);
DMA_InitStructure.DMA_Channel = DMA_Channel_7;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR8RD;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
DMA_InitStructure.DMA_BufferSize = size >> 1;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream5, &DMA_InitStructure);
/* Enable DMA1_Stream5 */
DMA_Cmd(DMA1_Stream5, ENABLE);
/* Enable DAC Channel */
DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_Cmd(DAC_Channel_2, ENABLE);
/* Enable DMA for DAC Channel1 */
DAC_DMACmd(DAC_Channel_1, ENABLE);
}
3.2.2 DAC 双通道 8-bit 循环播放配置
/**
* @brief DAC 双通道 16-bit (高 12 bits) 循环播放配置
* @param buffer 无符号 16-bit (高 12 bits) 双通道音频数据, 采样率 24000Hz
* @param size 数据长度 (总字节数)
* @retval None
*/
static void DAC_Dual16bitCircular_Config(const void *buffer, uint32_t size)
{
/* DAC 配置 */
DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Init(DAC_Channel_2, &DAC_InitStructure);
/* DMA1_Stream5 channel7 **************************************/
/* 我们使用双通道 DAC 模式, 仅需接收一个 DAC 通道的 Request, 因此只配置 DAC channel1 的 DMA*/
DMA_DeInit(DMA1_Stream5);
DMA_InitStructure.DMA_Channel = DMA_Channel_7;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12LD;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
DMA_InitStructure.DMA_BufferSize = size >> 2;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream5, &DMA_InitStructure);
/* Enable DMA1_Stream5 */
DMA_Cmd(DMA1_Stream5, ENABLE);
/* Enable DAC Channel */
DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_Cmd(DAC_Channel_2, ENABLE);
/* Enable DMA for DAC Channel1 */
DAC_DMACmd(DAC_Channel_1, ENABLE);
}
3.2.3 双缓存区实现播放音频
/**
* @breif 启动双通道 DAC 输出
* @note
* @param buffer_0 缓冲区0
* @param buffer_1 缓冲区1
* @param sample_rate 采样率//频率
* @param buffer_size 单个缓冲区长度
* @param bits_per_sample 采样深度 8 或 16
*/
void DAC_Stereo_DualBuffering_Start(const uint8_t *buffer_0, const uint8_t *buffer_1, uint32_t sample_rate,
uint32_t buffer_size, int bits_per_sample)
{
TIM6_Config(sample_rate);//根据采样频率配置时钟
/* DAC channel Configuration */
DAC_DMACmd(DAC_Channel_1, DISABLE);
DAC_DeInit();//重启DAC(当播放完一个音频再播放另一个音频,需要重置)
DMA_DeInit(DMA1_Stream5);
DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;//不用DAC自己产生的波
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Init(DAC_Channel_2, &DAC_InitStructure);
/* DMA1_Stream5 channel7 configuration **************************************/
/* 我们使用双通道 DAC 模式, 仅需接收一个 DAC 通道的 Request, 因此只配置 DAC channel1 的 DMA*/
DMA_InitStructure.DMA_Channel = DMA_Channel_7;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer_0;
if (bits_per_sample == 8)
{
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR8RD;
DMA_InitStructure.DMA_BufferSize = buffer_size >> 1; // 8bit 双通道, 一个采样2字节
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
} else {
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12LD;
DMA_InitStructure.DMA_BufferSize = buffer_size >> 2; // 16bit 双通道, 一个采样4字节
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
}
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
// 双缓冲配置
DMA_DoubleBufferModeConfig(DMA1_Stream5, (uint32_t)buffer_1, DMA_Memory_0);
DMA_DoubleBufferModeCmd(DMA1_Stream5, ENABLE);
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);//清除传输完成中断标志
DMA_ITConfig(DMA1_Stream5, DMA_IT_TC, ENABLE);//DMA转移完成行程中断,为了实现双缓冲区
DMA_Init(DMA1_Stream5, &DMA_InitStructure);
/* Enable DMA1_Stream5 */
DMA_Cmd(DMA1_Stream5, ENABLE);
/* Enable DAC Channel */
DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_Cmd(DAC_Channel_2, ENABLE);
/* Enable DMA for DAC Channel1 */
DAC_DMACmd(DAC_Channel_1, ENABLE);
}
这个函数是用于双缓冲播放音频的函数。它接受两个缓冲区的指针(buffer_0和buffer_1),以及其他参数如采样率、缓冲区大小和采样深度。
3.2.4 DMA中断配置
void DMA1_Stream5_IRQHandler()
{
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5))
{
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
if (DAC_DMA_TransmitComplete_Callback)
{
DAC_DMA_TransmitComplete_Callback(DMA1_Stream5);//读新数据
}
}
if(DMA_GetFlagStatus(DMA1_Stream5,DMA_FLAG_TCIF5)!=RESET)//等待DMA传输完成
{
DMA_Cmd(DMA1_Stream5, DISABLE); //关闭DMA,防止处理期间有数据
DMA_ClearFlag(DMA2_Stream5,DMA_FLAG_TCIF5 | DMA_FLAG_FEIF5 | DMA_FLAG_DMEIF5 | DMA_FLAG_TEIF5 | DMA_FLAG_HTIF5);//清除DMA2_Steam7传输完成标志
DMA_SetCurrDataCounter(DMA2_Stream5, 4);
DMA_Cmd(DMA2_Stream5, ENABLE); //打开DMA
}
}
3.3 WAV文件解析
先看下wav在flash中的存储
如上图所示,所有音频文件收尾相连,只需要记录文件的首地址即可,示例代码如下图所示:
#define WAV_KEYPADTONE 0x00000000
#define WAV_TRUMPET 0x00004E4C
#define WAV_CH_STARTUP 0x000070DC
#define WAV_CH_SHUTDOWN 0x00027FC0
#define WAV_CH_JOYSTICK_MIDDLE 0x0002CE82
#define WAV_CH_JOYSTICK_BROKEN 0x00040EAC
#define WAV_CH_BATTERY_LOW 0x00052622
#define WAV_CH_BATTERY_DEAD 0x00068E82
#define WAV_CH_OUT_OF_AERA 0x000900EC
#define WAV_CH_EXCEED_INCLINATION_ANGLE 0x000A4B16
#define WAV_CH_VEHICLE_LOCKED 0x000C9BC0
#define WAV_CH_VEHICLE_LOCK 0x000F042A
#define WAV_CH_VEHICLE_UNLOCK 0x000F8B56
#define WAV_EN_STARTUP 0x00102002
#define WAV_EN_SHUTDOWN 0x0011A1FC
#define WAV_EN_JOYSTICK_MIDDLE 0x001242CA
#define WAV_EN_JOYSTICK_BROKEN 0x0014C76C
#define WAV_EN_BATTERY_LOW 0x00166DBE
#define WAV_EN_BATTERY_DEAD 0x00195620
#define WAV_EN_OUT_OF_AERA 0x001DEB48
#define WAV_EN_EXCEED_INCLINATION_ANGLE 0x00205B10
#define WAV_EN_VEHICLE_LOCKED 0x00238098
#define WAV_EN_VEHICLE_LOCK 0x00262372
#define WAV_EN_VEHICLE_UNLOCK 0x0026C11E
#define WAV_CN_APP2 0x002767CA
#define WAV_CN_APP1 0x00280E76
wav文件头说明如下图所示
文件头(File Header): WAV文件以一个固定长度的文件头开始,用于描述整个文件的基本信息。这个文件头通常占据文件的前44个字节,其中包括以下关键信息:
单元块 | 说明 |
---|---|
文件标识符(RIFF标志) | 4个字节,表示文件类型,通常为 “RIFF”。 |
文件大小(File Size) | 4个字节,表示整个文件的大小(包括文件头和音频数据)。 |
文件格式(Format) | 4个字节,表示文件格式,通常为 “WAVE”。 |
数据块标识符("fmt "标志) | 4个字节,表示格式块的开始。 |
格式块大小(Format Chunk Size) | 4个字节,表示格式块的大小。 |
音频格式(Audio Format) | 2个字节,表示音频数据的格式,常见的是PCM(Pulse Code Modulation)格式。 |
声道数(Number of Channels) | 2个字节,表示声道数,如单声道(Mono)或立体声(Stereo)。 |
采样率(Sample Rate) | 4个字节,表示每秒采样的样本数。 |
数据传输速率(Byte Rate) | 4个字节,表示每秒传输的字节数。 |
数据块对齐(Block Align) | 2个字节,表示每个采样帧的字节数。 |
量化位数(Bits per Sample) | 2个字节,表示每个样本的位数,常见的有8位、16位等。 |
数据块标识符("data"标志) | 4个字节,表示音频数据块的开始。 |
音频数据大小(Data Size) | 4个字节,表示音频数据块的大小。 |
需要将,每个wav文件数据按上述格式解析,根据文件头,我就可以知道音频数据有多大。通过wav在flash中的存储方式,我们已知文件首地址,通过首地址可以读取文件头,通过解析文件头则可以指定音频c长度。
音频加载函数如下:
//从flash读取音频数据到缓冲区
static void wave_flash_to_buffer(uint8_t *buffer)
{
int length_to_read;//需要从flash读取的长度
// 确定应该从 flash 读多少字节
if (player_status.segments_remaining == 0)
return;
else if (player_status.segments_remaining == 1)
// 如果是最后一个段, 则读取剩余音频长度
length_to_read = player_status.data_size - player_status.player_offset;//需要读取的长度=音频数据长度-已经读取到的位置
else
length_to_read = player_status.read_buffer_size;
// 从 flash 读音频数据
sFLASH_ReadBuffer(buffer, player_status.data_base + player_status.player_offset, length_to_read);
player_status.player_offset += length_to_read;
player_status.segments_remaining--;
// 音频格式是 unsigned 8-bit/signed 16-bit PCM, 需要转换成 unsigned
if (player_status.bits_per_sample == 16)
{
/**
* int16_t 数据平移至 uint16_t, 只需 += 0x8000, 等效于 ^= 0x8000
* 因为数据是小端序, 所以只需在每 2 bytes 的高位地址 ^= 0x80
*/
for (int i = 1; i < length_to_read; i += 2)
{
buffer[i] ^= 0x80;
}
}
if (player_status.num_channels == 1)
{
// 音频格式是单声道, 需要复制成双声道
if (player_status.bits_per_sample == 16)
{
for (int i = player_status.read_buffer_size - 2; i >= 0; i -= 2)
{
buffer[i * 2] = buffer[i * 2 + 2] = buffer[i];
buffer[(i + 1) * 2 - 1] = buffer[(i + 1) * 2 + 1] = buffer[i + 1];
}
}
else
{//8位深度单通道复制为双通道
for (int i = player_status.read_buffer_size - 1; i >= 0; --i)
{
buffer[i << 1] = buffer[(i << 1) | 1] = buffer[i];
}
}
// 将缓冲区没有数据的部分填充成 0x80, 比较接近中位
memset(buffer + length_to_read * 2, 0x80, WAVE_BUFFER_SIZE - length_to_read * 2);//转双通道后,缓冲剩余长度=总长-读入(单通道)数据字节数*2
}
else
{
// 将缓冲区没有数据的部分填充成 0x80
memset(buffer + length_to_read, 0x80, WAVE_BUFFER_SIZE - length_to_read);//如果读的是双通道数据,缓冲剩余长度=总长-读入(双通道)数据字节数
}
}
音频播放函数如下:
/**
* @brief 播放 flash 中的音频
* @note 音频格式为单声道/双声道 wav 格式, 采样深度为 unsigned 8-bit / signed 16-bit
* @param wav_addr 声音在 flash 中的地址
*/
void wave_play(uint32_t wave_addr)
{
int header_length;
// 设置 DAC 中 DMA 的中断回调函数
DAC_DMA_TransmitComplete_Callback = WAVE_DMA_TransmitComplete_Callback;
// init player_status
player_status.player_offset = 0;
player_status.ready_to_stop = false;
player_status.wav_base = wave_addr;
player_status.memory_index_for_new_data = -1;
// 读取并解析 wav 头
sFLASH_ReadBuffer(wave_header, wave_addr, WAVE_HEADER_SIZE);
header_length = WAVE_Parsing(&wave_format, wave_header);
if (header_length < 0 || wave_format.DataSize == 0)
{//判断解析 wav 格式是否出错<0即出错,>0为头长
while (1)
;
}
//音频数据的起始地址 = wav起始地址+头长度
player_status.data_base = wave_addr + header_length;
player_status.sample_rate = wave_format.SampleRate;
player_status.data_size = wave_format.DataSize;
player_status.num_channels = wave_format.NumChannels;
player_status.block_align = wave_format.BlockAlign;
player_status.bits_per_sample = wave_format.BitsPerSample;
/*
WAVE_BUFFER_SIZE:内存中开辟的缓冲区大小
当为单声道数据时,因为输出时双声道,因此在输出时会将读取的数据再复制一遍。所以最大只能从flash读取缓冲区一半大小的数据即WAVE_BUFFER_SIZE >> 1
*/
player_status.read_buffer_size = (player_status.num_channels == 2) ? (WAVE_BUFFER_SIZE) : (WAVE_BUFFER_SIZE >> 1);
player_status.segments_remaining = (player_status.data_size - 1u) / player_status.read_buffer_size + 1;//整数除法向上取整
// 将前两段数据放入缓冲区中
if (player_status.segments_remaining == 1)
{
wave_flash_to_buffer(wave_buffer[0]);
player_status.ready_to_stop = true;
}
else
{
wave_flash_to_buffer(wave_buffer[0]);
wave_flash_to_buffer(wave_buffer[1]);
}
player_status.playing = true;
// 启动 DAC
DAC_Stereo_DualBuffering_Start(wave_buffer[0], wave_buffer[1], player_status.sample_rate, WAVE_BUFFER_SIZE,
player_status.bits_per_sample);
}
4. 音频文件生成方法
音频文件一个一个烧进flash太麻烦了,可以将所有音频文件拼接起来。
并记录首地址即可,我这里提供一个自己开发的小插件。见代码附件。
5.音频烧录到flash的方法
**方法1:**使用STM32CubeProgrammer软件烧录
该软件没有提供W25Q128FV型号FLASH的固件支持,可以自己编写一个(相关代码见附件)
。
将自己制作的固件放到该软件STM32Cube\STM32CubeProgrammer\bin\ExternalLoader目录下
然后选用该固件即可。
固件及源码见附件。
**方法2:**使用正点原子的P100烧录。