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:
void initDAC()
{
ACLKCONbits.APSTSCLR = 0b111; ACLKCONbits.SELACLK = 0;
DAC1STATbits.LOEN = 1; DAC1STATbits.ROEN = 1;
DAC1STATbits.LITYPE = 0; DAC1STATbits.RITYPE = 0;
DAC1CONbits.AMPON = 0;
DAC1DFLT = 0x00;
IFS4bits.DAC1LIF = 0; IFS4bits.DAC1RIF = 0;
IEC4bits.DAC1LIE = 0; IEC4bits.DAC1RIE = 0;
DAC1CONbits.DACEN = 1; }
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:
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:
147456000 Hz / 72 = 2048000 Hz
which is the required DAC
clock for an input frequency of:
2048000 Hz / 256 = 8000 Hz
The following code shows how to set the DACFDIV
value for a specific input frequency:
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):
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:
DAC1CONbits.FORM = 0;
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.
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:
void __attribute__((interrupt, no_auto_psv))_DAC1LInterrupt(void)
{
IFS4bits.DAC1LIF = 0;
}
void __attribute__((interrupt, no_auto_psv))_DAC1RInterrupt(void)
{
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.