WAV player using STM32F0 Discovery board interfaced to a SD card and using onchip DAC.
The STM32F0 Discovery board has STM32F051R8 microcontroller which has an onchip 1Ms/s 12bit DAC. I thought I could use this for music playback.
Music is stored on an SD card connected to STM32F0 board on SPI in the form of WAV files.
You can read more about SD card interfacing to MCU and FAT filesystem here: FAT filesystem on SD card.
By reading and examining the WAV file header we know the music data format stored in the wav file.
A nice description of the wav file header can be found here: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
The wav header is 44B long and mapped to this structure:
Wav header structure
typedef struct
{
char chunk_id[4]; /* Contains the letters "RIFF" in ASCII: 0x52494646 big-endian */
u32 chunk_size; /* 36 + SubChunk2Size */
char format[4]; /* Contains the letters "WAVE" in ASCII: 0x57415645 big-endian */
char subchunk1_id[4]; /* Contains the letters "fmt " in ASCII: 0x666d7420 big-endian */
u32 subchunk1_size; /* 16 for PCM. This is the size of the rest of the Subchunk which follows this number */
u16 audio_format; /* PCM = 1, values other than 1 indicate some form of compression */
u16 num_channels; /* Mono = 1, Stereo = 2, etc */
u32 sample_rate; /* 8000, 16000, 32000, 44100, etc */
u32 byte_rate; /* = sample_rate * num_channels * bits_per_sample/8 */
u16 block_align; /* = num_channels * bits_per_sample/8 - the number of bytes for one sample for all channels */
u16 bits_per_sample; /* 8 bits = 8, 16 bits = 16, etc */
char subchunk2_id[4]; /* Contains the letters "data" in ASCII: 0x64617461 big-endian */
u32 subchunk2_size; /* = NumSamples * num_channels * bits_per_sample/8 */
}wav_header_t;
For the playback of the wave file I used a 1KB RAM buffer and activated DMA to move data from this buffer in circular mode to the output register of the DAC.
TIM6 is configured to trigger the DMA transactions. TIM6 is configured to overflow as many times per second as the wav sample rate. DMA was configured to trigger an IRQ at Half Transfer and one at Transfer Complete.
In the Half Transfer DMA interrupt service routine I update the first half(512B) of the 1KB play buffer with new data from the wav file and on the Transfer Complete interrupt service routine I update the second half (512B) of the 1KB play buffer with new data from the wav file.
DMA Half Transfer and Transfer Complete handlers
void DMA1_Channel2_3_IRQHandler()
{
u16 bw;
FRESULT res1;
if(DMA_GetITStatus(DMA1_IT_HT3))
{
if(flag_stopDMA) TIM_Cmd(TIM6, DISABLE);
if(buff_chunks)
{
res1 = f_read(&fil, (u8*)wav_buff_start, 512, (UINT*)&bw);
buff_chunks--;
}
else flag_stopDMA = TRUE;
DMA_ClearITPendingBit(DMA1_IT_HT3);
}
else if(DMA_GetITStatus(DMA1_IT_TC3))
{
if(flag_stopDMA) TIM_Cmd(TIM6, DISABLE);
if(buff_chunks)
{
res1 = f_read(&fil, (u8*)wav_buff_mid, 512, (UINT*)&bw);
buff_chunks--;
}
else flag_stopDMA = TRUE;
DMA_ClearITPendingBit(DMA1_IT_TC3);
}
}
The play buffer is updated like this for the entire length of the wav file. When I reach wav end-of-file I stop TIM6 which will not trigger anymore DMA transactions to the DAC.
DAC and DMA configuration
void Config_DAC_DMA()
{
GPIO_InitTypeDef GPIO_InitStructure;
DAC_InitTypeDef DAC_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//PA4 DAC Output
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1 , ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //DAC Output
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO;
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
/* DMA1 Channel1 Config */
DMA_DeInit(DMA1_Channel3);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(DAC->DHR8R1);
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)wav_buff_start;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 1024;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
/* Enable the DMA channel 1 global Interrupt */
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* Enable Transfer Complete IT and Half Transfer notification for DMA1 Channel 1 */
DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE);
DMA_ITConfig(DMA1_Channel3, DMA_IT_HT, ENABLE);
/* DMA1 Channel1 enable */
DMA_Cmd(DMA1_Channel3, ENABLE);
DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_DMACmd(DAC_Channel_1, ENABLE);
}