Let´s play some audio

Now we use the I2S transmitter and the AXI-Stream interface to output a media file from an SD card over a speaker.

In the last part of the I2S tutorial, the created I2S transmitter was equipped with an AXI stream interface in order to be able to connect it to the processing system. In this part of the tutorial we want to use this interface to read wave files from an SD card using the processing system and output the music using a CS4344 D/A converter via a connected speaker.

The following IP cores are required for the project:

  • The I2S transmitter with the AXI-Stream Interface

  • A Processing System to read the data from the SD card and write them into a FIFO

  • An AXI-Stream FIFO

  • A Clocking Wizard to generate the audio clock

The Clocking Wizard generates the clock, which is then used as the master clock for the CS4344. The output clock can be adapted to the sampling rate of the audio file via the AXI-Lite interface. The Clocking Wizard is initialized with a 12.288 MHz clock for 48 kHz audio signals.

The AXI-Stream FIFO serves as a link between the processing system and the I2S transmitter. The processing system writes data to the FIFO via the AXI-Lite (or AXI) interface and this then streams the data to the I2S transmitter.

A bitstream is created from the design and after that, the software can be developed.

The xilffs FAT library from Xilinx is required to read the SD card, which must be integrated into the Board Support Package of the Vitis project:

Xilinx's FAT library is based on Elm Chan's FAT library, which I have already used for my implementation of an SD card for the AVR.

In the first step, the software uses the AudioPlayer_Init function to initialize the audio player and thus the FIFO, the GIC, and the interrupt handler, as well as the clocking wizard and the SD card.

u32 AudioPlayer_Init(void)
{
	xil_printf("[INFO] Looking for FIFO configuration...\r\n");
	_Fifo_ConfigPtr = XLlFfio_LookupConfig(XPAR_FIFO_DEVICE_ID);
	if(_Fifo_ConfigPtr == NULL)
	{
		xil_printf("[ERROR] Invalid FIFO configuration!\r\n");
		return XST_FAILURE;
	}

	xil_printf("[INFO] Initialize FIFO...\r\n");
	if(XLlFifo_CfgInitialize(&_Fifo, _Fifo_ConfigPtr, _Fifo_ConfigPtr->BaseAddress) != XST_SUCCESS)
	{
		xil_printf("[ERROR] FIFO initialization failed!\n\r");
		return XST_FAILURE;
	}

	xil_printf("[INFO] Looking for GIC configuration...\r\n");
	_GIC_ConfigPtr = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID);
	if(_GIC_ConfigPtr == NULL)
	{
		xil_printf("[ERROR] Invalid GIC configuration!\n\r");
		return XST_FAILURE;
	}

	xil_printf("[INFO] Initialize GIC...\r\n");
	if(XScuGic_CfgInitialize(&_GIC, _GIC_ConfigPtr, _GIC_ConfigPtr->CpuBaseAddress) != XST_SUCCESS)
	{
		xil_printf("[ERROR] GIC initialization failed!\n\r");
		return XST_FAILURE;
	}

	xil_printf("[INFO] Setup interrupt handler...\r\n");
	XScuGic_SetPriorityTriggerType(&_GIC, XPAR_FABRIC_FIFO_INTERRUPT_INTR, 0xA0, 0x03);
	if(XScuGic_Connect(&_GIC, XPAR_FABRIC_FIFO_INTERRUPT_INTR, (Xil_ExceptionHandler)AudioPlayer_FifoHandler, &_Fifo) != XST_SUCCESS)
	{
		xil_printf("[ERROR] Can not connect interrupt handler!\n\r");
		return XST_FAILURE;
	}
	XScuGic_Enable(&_GIC, XPAR_FABRIC_FIFO_INTERRUPT_INTR);

	xil_printf("[INFO] Enable exceptions...\r\n");
	Xil_ExceptionInit();
	Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &_GIC);
	Xil_ExceptionEnable();

	xil_printf("[INFO] Enable FIFO interrupts...\r\n");
	XLlFifo_IntClear(&_Fifo, XLLF_INT_ALL_MASK);

	xil_printf("[INFO] Initialize Clocking Wizard...\r\n");
	if((ClockingWizard_Init(&_ClkWiz, XPAR_CLOCKINGWIZARD_BASEADDR) || ClockingWizard_GetOutput(&_ClkWiz, &_AudioClock))!= XST_SUCCESS)
	{
		xil_printf("[ERROR] Clocking Wizard initialization failed!\n\r");
		return XST_FAILURE;
	}
	
	xil_printf("[INFO] Mount SD card...\r\n");
	if(SD_Init())
	{
		xil_printf("[ERROR] Can not initialize SD card!\n\r");
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

As soon as the initialization is completed, the function AudioPlayer_LoadFile is called to load the file Audio.wav from the SD card.

if(AudioPlayer_Init() != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize audio player. Abort...\n\r");
	return XST_FAILURE;
}

if(AudioPlayer_LoadFile("Audio.wav"))
{
	xil_printf("[ERROR] Can not open Audio file!\n\r");
	return XST_FAILURE;
}

u32 AudioPlayer_LoadFile(char* File)
{
	if(SD_LoadFileFromCard(File, &_File))
	{
		xil_printf("[ERROR] Can not open Audio file!\n\r");
		return XST_FAILURE;
	}

	xil_printf("	File size: %lu bytes\n\r", _File.Header.ChunkSize + 8);
	xil_printf("	File format: %lu\n\r", _File.Format.AudioFormat);
	xil_printf("	Channels: %lu\n\r", _File.Format.NumChannels);
	xil_printf("	Sample rate: %lu Hz\n\r", _File.Format.SampleRate);
	xil_printf("	Bits per sample: %lu bits\n\r", _File.Format.BitsPerSample);
	xil_printf("	Block align: %lu bytes\n\r", _File.Format.BlockAlign);
	xil_printf("	Data bytes per channel: %lu bytes\n\r", _File.Header.ChunkSize / _File.Format.NumChannels);
	xil_printf("	Samples: %lu\n\r", 8 * _File.Header.ChunkSize / _File.Format.NumChannels / _File.Format.BitsPerSample);
	AudioPlayer_ChangeFreq(_File.Format.SampleRate);

	if(( _File.Format.BitsPerSample != 16) || (_File.Format.NumChannels > 2))
	{
		xil_printf("[ERROR] Invalid file format!\n\r");
		return XST_FAILURE;
	}

	XLlFifo_TxReset(&_Fifo);
	XLlFifo_IntEnable(&_Fifo, XLLF_INT_ALL_MASK);
	SD_CopyDataIntoBuffer(_FifoBuffer, 256);
	AudioPlayer_CopyBuffer();

	return XST_SUCCESS;
}

The function AudioPlayer_LoadFile calls the function SD_LoadFileFromCard to load the wave file from the SD card.

u32 SD_LoadFileFromCard(const char* FileName, Wave_t* File)
{
	xil_printf("[INFO] Opening file: %s...\n\r", FileName);

	if(f_open(&_FileHandle, FileName, FA_READ))
	{
		xil_printf("[ERROR] Can not open audio file!\n\r");
		return XST_FAILURE;
	}

	if(f_read(&_FileHandle, &File->RIFF, sizeof(Wave_RIFF_t), &_BytesRead) || f_read(&_FileHandle, &File->Format, sizeof(Wave_Format_t), &_BytesRead))
	{
		xil_printf("[ERROR] Can not read SD card!\n\r");
		return XST_FAILURE;
	}

	Wave_Header_t Header;
	uint32_t Offset = sizeof(Wave_RIFF_t) + sizeof(Wave_Format_t);
	if(f_read(&_FileHandle, Header.ChunkID, sizeof(Wave_Header_t), &_BytesRead) || f_lseek(&_FileHandle, Offset))
	{
		xil_printf("[ERROR] Can not read SD card!\n\r");
		return XST_FAILURE;
	}
	
	if(strncmp("LIST", Header.ChunkID, 4) == 0)
	{
		Offset += Header.ChunkSize + sizeof(Wave_Header_t);
		if(f_read(&_FileHandle, &File->ListHeader, sizeof(Wave_Header_t), &_BytesRead) || f_lseek(&_FileHandle, Offset))
		{
			xil_printf("[ERROR] Can not place SD card pointer!\n\r");
			return XST_FAILURE;
		}
	}

	if(f_read(&_FileHandle, &File->DataHeader, sizeof(Wave_Header_t), &_BytesRead))
	{
		xil_printf("[ERROR] Can not read SD card!\n\r");
		return XST_FAILURE;
	}

	if(File->Format.AudioFormat != WAVE_FORMAT_PCM)
	{
		xil_printf("[ERROR] Audio format not supported! Keep sure that the file use the PCM format!\n\r");
		return XST_FAILURE;
	}

	_RemainingBytes = File->DataHeader.ChunkSize;

	_IsBusy = true;

	return XST_SUCCESS;
}

In the next step, the output frequency of the clocking wizard is set from the wave file according to the sampling frequency used:

static void AudioPlayer_ChangeFreq(const u32 SampleRate)
{
	if(SampleRate == 44100)
	{
		xil_printf("	Use clock setting 1...\n\r");
		_ClkWiz.DIVCLK_DIVIDE = 5;
		_ClkWiz.CLKFBOUT_MULT = 42;
		_ClkWiz.CLKFBOUT_Frac_Multiply = 0;
		_AudioClock.DIVIDE = 93;
		_AudioClock.FRAC_Divide = 0;
	}
	else if(SampleRate == 48000)
	{
		xil_printf("	Use clock setting 2...\n\r");
		_ClkWiz.DIVCLK_DIVIDE = 3;
		_ClkWiz.CLKFBOUT_MULT = 23;
		_ClkWiz.CLKFBOUT_Frac_Multiply = 0;
		_AudioClock.DIVIDE = 78;
		_AudioClock.FRAC_Divide = 0;
	}
	else if(SampleRate == 96000)
	{
		xil_printf("	Use clock setting 3...\n\r");
		_ClkWiz.DIVCLK_DIVIDE = 3;
		_ClkWiz.CLKFBOUT_MULT = 23;
		_ClkWiz.CLKFBOUT_Frac_Multiply = 0;
		_AudioClock.DIVIDE = 39;
		_AudioClock.FRAC_Divide = 0;
	}

	ClockingWizard_SetClockBuffer(&_ClkWiz);
	ClockingWizard_SetOutput(&_ClkWiz, &_AudioClock);
}

When the audio file has been loaded and the clocking wizard's output frequency has been adjusted, the first block of data is read from the wave file and copied to the FIFO:

u32 SD_CopyDataIntoBuffer(u8* Buffer, const u32 Length)
{
	if(_RemainingBytes >= Length)
	{
		if(f_read(&_FileHandle, Buffer, Length, &_BytesRead))
		{
			return XST_FAILURE;
		}

		_RemainingBytes -= _BytesRead;
	}
	else
	{
		if(f_read(&_FileHandle, Buffer, _RemainingBytes, &_BytesRead))
		{
			return XST_FAILURE;
		}

		if(f_close(&_FileHandle))
		{
			xil_printf("[ERROR] Can not close audio file!\n\r");
			return XST_FAILURE;
		}

		_IsBusy = false;
	}

	return XST_SUCCESS;
}

static void AudioPlayer_CopyBuffer(void)
{
    u32 Bytes = 0x00;
    for(u32 i = 0x00; i < AUDIOPLAYER_FIFO_BUFFER_SIZE; i += _File.Format.BlockAlign)
    {
        if(XLlFifo_iTxVacancy(&_Fifo))
        {
            u32 Word = 0x00;
            for(u8 Byte = 0x00; Byte < _File.Format.BlockAlign; Byte++)
            {
                Word |= _FifoBuffer[i + Byte];
                Word <<= 0x08;
                Bytes++;
            }

            Bytes += sizeof(u32);
            XLlFifo_TxPutWord(&_Fifo, Word);
        }
    }
    XLlFifo_iTxSetLen(&_Fifo, Bytes);
}

The rest of the program flow then takes place in the callback of the FIFO:

static void FifoHandler(void* CallbackRef)
{
	XLlFifo* InstancePtr = (XLlFifo*)CallbackRef;
	u32 Pending = XLlFifo_IntPending(InstancePtr);
	while(Pending)
	{
		if(Pending & XLLF_INT_TC_MASK)
		{
			SD_CopyDataIntoBuffer(FifoBuffer, FIFO_BUFFER_SIZE);
			XLlFifo_IntClear(InstancePtr, XLLF_INT_TC_MASK);
		}
		else if(Pending & XLLF_INT_TFPE_MASK)
		{
			AudioPlayer_CopyBuffer();

			if(!SD_IsBusy())
			{
				XLlFifo_IntDisable(&_Fifo, XLLF_INT_ALL_MASK);
			}
			
			XLlFifo_IntClear(InstancePtr, XLLF_INT_TFPE_MASK);
		}
		else if(Pending & XLLF_INT_ERROR_MASK)
		{
			xil_printf("	Error: %lu!\n\r", Pending);
			XLlFifo_IntClear(InstancePtr, XLLF_INT_ERROR_MASK);
		}
		else
		{
			XLlFifo_IntClear(InstancePtr, Pending);
		}
		Pending = XLlFifo_IntPending(InstancePtr);
	}
}

As soon as the FIFO triggers a TFPE interrupt (Transmit FIFO Programmable Empty), the FIFO is filled with new data from the internal buffer. When the transfer from the processing system to the FIFO is complete, a TC interrupt (transmit complete) is triggered and the next block of data is read from the SD card. This will repeat until the file is completely played.

static void AudioPlayer_CopyBuffer(void)
{
	u32 Bytes = 0x00;
	for(u32 i = 0x00; i < 256; i += _File.Format.BlockAlign)
	{
		u32 Word = 0x00;
		for(u8 Byte = 0x00; Byte < _File.Format.BlockAlign; Byte++)
		{
			Word |= _FifoBuffer[i + Byte];
			Word <<= 0x08;
		}

		if(XLlFifo_iTxVacancy(&_Fifo))
		{
			XLlFifo_TxPutWord(&_Fifo, Word);
			Bytes += sizeof(u32);
		}
	}

	XLlFifo_iTxSetLen(&_Fifo, Bytes);
}

Now a wave file is needed. Simple test signals are available in the repository or can e.g. be generated at wavtones.com.

The respective file then only has to be copied to the SD card under the name Audio.wav and you're ready to go.

-----------I2S Audio player-----------

[INFO] Looking for FIFO configuration...
[INFO] Initialize FIFO...
[INFO] Looking for GIC configuration...
[INFO] Initialize GIC...
[INFO] Setup interrupt handler...
[INFO] Enable exceptions...
[INFO] Enable FIFO interrupts...
[INFO] Initialize Clocking Wizard...
[INFO] Mount SD card...
[INFO] Opening file: Single.wav...
        File size: 264610 bytes
        File format: 1
        Channels: 1
        Sample rate: 48000 Hz
        Bits per sample: 16 bits
        Data bytes: 264602 bytes
        Samples: 132301
        Use clock setting 2...
[INFO] Finished!

Or with stereo audio:

-----------I2S Audio player-----------

[INFO] Looking for FIFO configuration...
[INFO] Initialize FIFO...
[INFO] Looking for GIC configuration...
[INFO] Initialize GIC...
[INFO] Setup interrupt handler...
[INFO] Enable exceptions...
[INFO] Enable FIFO interrupts...
[INFO] Initialize Clocking Wizard...
[INFO] Mount SD card...
[INFO] Opening file: Dual.wav...
        File size: 529208 bytes
        File format: 1
        Channels: 2
        Sample rate: 44100 Hz
        Bits per sample: 16 bits
        Block align: 4 bytes
        Data bytes: 264600 bytes
        Samples: 132300
        Use clock setting 1...
[INFO] Finished!

Last updated