Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

16-bit Stereo Audio DAC on dsPIC33FJ128GP802

0.00/5 (No votes)
4 Apr 2023CPOL5 min read 3K  
16-bit Stereo Audio DAC on dsPIC33FJ128GP802
This post is about my experiment with the dsPIC’s DAC module.

The dsPIC33FJ128GP802 is among the few devices in the dsPIC33F family that have an integrated 16-bit stereo DAC module. The DAC can be useful to play WAV audio files stored on SD card. I got my free samples (in DIP packages of course) from Microchip and spent a few hours experimenting with the dsPIC’s DAC module.

According to the datasheet, the DAC module of this PIC supports 16-bit resolution (with 14-bit accuracy) with an input frequency up to 45kHz. The DAC can be managed via the DACXXXX registers, described in section 22.0 of the datasheet. There is also a DACDFLT register which specifies the default output value which will be used should the DAC buffer become empty. Interestingly, although the DACDFLT description says that the default value is useful for industrial control applications where the DAC output controls an important processor or machinery, the same page of the sheet indicates that the DAC module is designed specifically for audio applications and is not recommended for control type applications! This kind of contradicting information can be misleading to many users. During my experiments, the maximum voltage on the DAC output pins is around 1.6V. Also, it looks as if the output waveform has been passed through some kinds of low-pass filter and does not vary linearly with the input value. Hence, the DAC module on this PIC should strictly be used for audio only, as the datasheet has stated.

The following code initializes the DAC using the PLL output (FVCO) as the clock source:

C++
void initDAC()
{
    ACLKCONbits.APSTSCLR = 0b111; 	// Auxiliary Clock Output Divider (divide by 1)
    ACLKCONbits.SELACLK = 0;    	// 1 = Auxiliary Oscillators provides the 
                                    // source clock for Auxiliary Clock Divider
				                    // 0 = PLL output (Fvco) provides the source clock 
                                    // for the Auxiliary Clock Divider for the DAC clock

    DAC1STATbits.LOEN = 1; 	        // Left Channel DAC Output Enabled
    DAC1STATbits.ROEN = 1; 	        // Right Channel DAC Output Enabled

    DAC1STATbits.LITYPE = 0;        // Left Channel Interrupt Type 
                                    // (1 = Interrupt if FIFO is empty, 
                                    //  0 = Interrupt if FIFO is not full)
    DAC1STATbits.RITYPE = 0; 	    // Right Channel Interrupt Type

    DAC1CONbits.AMPON = 0; 	// Amplifier Disabled During Sleep and Idle Modes

    DAC1DFLT = 0x00; 		// Default value When DAC buffer is empty

    IFS4bits.DAC1LIF = 0; 	// Clear Left Channel Interrupt Flag
    IFS4bits.DAC1RIF = 0; 	// Clear Right Channel Interrupt Flag

    IEC4bits.DAC1LIE = 0; 	// Left Channel Interrupt   (0 = Disabled, 1 = Enabled)
    IEC4bits.DAC1RIE = 0; 	// Right Channel Interrupt 

    DAC1CONbits.DACEN = 1; 	// DAC1 Module Enabled
}

The next step is to set the DAC clock, which must be equal to the sampling rate times 256. DACCLK is generated by dividing the high-speed oscillator (auxiliary or system clock) by a specified value. The divisor ratio is specified by clock divider bits (DACFDIV<6:0>) in the DAC Control (DACxCON register<6:0>). The resulting DACCLK must not exceed 25.6 MHz. The dsPIC DSC PLL can be configured to provide a system clock, which is an integral multiple of the DAC clock rate. The system clock source to the DAC module is designated FVCO and is the output of the PLL before the post-PLL divider (PLLPOST or N2). Assuming the PIC is using the internal oscillator with M (which is PLLFBD + 2) set to 40 and N1 (which is PLLPRE) set to 2, we have:

C++
FVCO = (7.3728 * 10^6 * 40) / 2 = 147456000 Hz

Setting the SELACLK (ACLKCON<13>) bit to ‘0’ and setting the DACFDIV register value to divide the FVCO clock by 72 we have a DAC clock of:

C++
147456000 Hz / 72 = 2048000 Hz 

which is the required DAC clock for an input frequency of:

C++
2048000 Hz / 256 = 8000 Hz 

The following code shows how to set the DACFDIV value for a specific input frequency:

C++
unsigned int div = (FINT * (MCONST + 2) / freq / N1 / 256) - 1;
DAC1CONbits.DACFDIV = div; 

where FINT is the PIC oscillator frequency (7372800 for the internal oscillator), MCONST is the value assigned to PLLFBD and N1 is the value assigned to PLLPRE during clock setup. For more information, refer to part 33.4 of Microchip’s DS70211, Audio Digital-to-Analog Converter (DAC) and also section “Oscillator Configuration” of the datasheet.

Take note that DACFDIV is a 16-bit integer so there might be some inaccuracies for certain frequencies such as 11025Hz, 22050Hz or 44100Hz, depending on the clock source. For all intents and purposes, these inaccuracies are negligible and should not affect the output audio quality.

There is a ridiculous typo in Microchip’s DS70211. The formula for FVCO is shown as below (emphasis mine):

Microchip DS70211 wrong FVCO formula

Well, if you are ever going to get the DAC to work, then the above typo should be obvious. 147.456 * 106 MHz is faster than the fastest computer as of today. It is ironic that such a basic error can occur in a Microchip document. In any case, I asked a friend of mine who is a professor in EE with a PhD degree in information processing, and she failed to spot the error. So that may tell you something …

To send audio samples to the DAC, simply set the value of DAC1LDAT for the left channel and DAC1RDAT for the right channel. If your input samples are 8-bit, you will need to convert to 16-bit, taking into account whether the samples are signed or unsigned. By convention, 8-bit audio samples are unsigned whereas 16-bit samples are signed. Use the following code to configure the DAC to work in signed or unsigned mode:

C++
DAC1CONbits.FORM = 0;    // Data Format (0 = Unsigned, 1 = Signed)

The following code demonstrates how the DAC can be used to play 8-bit unsigned audio samples retrieved from a 8kHz WAV file using FatFs. The file header (which contains just 44 bytes) is not taking into account yet.

C++
FSFILE * pointer;
unsigned int i;

#define BUFFER_LENGTH 64
unsigned char buffer[BUFFER_LENGTH];

pointer = FSfopen ("AUDIO.WAV", FS_READ);
if (pointer == NULL)
{
	SendUARTStr("Error opening WAV");
}
else {
	SendUARTStr("Playing WAV");

	unsigned int bytesRead = 0;
	do
	{
		bytesRead = FSfread(buffer, 1, BUFFER_LENGTH, pointer);
		for (i=0; i<bytesRead;i++)
		{
			DAC1LDAT = (unsigned int)(buffer[i]) << 8;
			delay_us(125);
		}
	}
	while (bytesRead > 0);

	FSfclose(pointer);
	SendUARTStr("Finished");
} 

Here, a delay of 125µs is added after each sample for an input frequency of 8kHz. A better implementation would be to have a small PCM playback buffer, configure a timer interrupt running at 8kHz that retrieves data from the buffer and puts them into the DAC, and program the main routine to dump audio data into this buffer instead. This implementation would allow your main routine to do other things while waiting for playback to complete. I will leave this as an exercise for the reader.

It might be useful to set DACDFLT to the last played sample and reduce the effects of buffer underflow should your code be unable to fill the DAC buffer in time. Also, by setting DAC1LIE/DAC1RIE to 1 and configure LITYPE/RITYPE, you can then use the following interrupt routines to signal when the DAC FIFO buffer is either empty or full and handle the situation accordingly:

C++
void __attribute__((interrupt, no_auto_psv))_DAC1LInterrupt(void)
{
    // Clear Left Channel Interrupt Flag
   IFS4bits.DAC1LIF = 0;
}

void __attribute__((interrupt, no_auto_psv))_DAC1RInterrupt(void)
{
    // Clear Right Channel Interrupt Flag
    IFS4bits.DAC1RIF = 0;
} 

To calculate the current DAC clock frequency on an oscilloscope, you may want to toggle a pin (e.g., LATBbits.LATB5 = !LATBbits.LATB5) within the DAC interrupt routine and measure the output waveform on that pin. You might also want to set DAC1LDAT/DAC1RDAT before leaving the interrupt routine or the interrupt will not be called again until the DAC buffer has received another value. The datasheet says that the DAC has a 4-byte deep FIFO buffer, which must be taken into account when calculating the DAC frequency via this method. In my projects, using the DAC interrupt is not necessary as audio playback is done from a timer interrupt configured to run at the frequency of the input audio.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)