Browse over 10,000 Electronics Projects

PIC32 SPI: Using the MCP4822 12-bit serial dual DAC

PIC32 SPI: Using the MCP4822 12-bit serial dual DAC

The MCP4822 comes with 2 12-bit DACs, which you can easily configure over SPI. I found them to be neat little devices. In a small 8-pin PDIP package. Here is the datasheet.

This was a great opportunity to get a simple PIC32 SPI application going. I worked on this today to see how fast I can get the DAC output going.

Here’s the pinout of the MCP4822:

Fig. 1 – MCP4822 pinout (taken from datasheet)

The MCP4822 can be supplied a voltage in the range of 2.7V to 5.5V. Since I use 3.3V for my PIC32, I used the same 3.3V supply for the VDD for the MCP4822. Pin 5 is the active low signal LDAC that is used to synchronize the two DAC channels. When this pin is brought low, the data in the DAC’s input register is copied to the output and both outputs are updated at the same time. I just had this tied to ground. VoutA and VoutB are the two output pins. The other pins are the regular pins for SPI communication – CS (active low) chip select, SCK serial clk, SDI serial data in.

The datasheet provides a nice diagram explaining the timing and pin functions very nicely:

Fig. 2 – Write command for MCP4822 (taken from datasheet)

The write command to the MCP4822 is a 16-bit wide command sent over SPI. Bit 15 (A/B) selects which channel you’re sending data to: 1 signifies channel B, 0 signifies channel A. Bit 13 (GA) selects the gain. The MCP4822 has an internal 2.048V reference it uses for the DAC. So, that means that with a gain of 1, the maximum possible output voltage is 2.04975V (4095/4096 * Vref). If a higher output is required, the gain can be increased. The MCP4822 has a configurable gain of 1 or 2. When GA = 1, gain = 1; when GA = 0, gain = 2. Bit 12 SHDN is the shutdown signal. When SHDN is low, the output of the DAC is shut down. The 12 data bits from there on: D11 to D0 ([D11:D0], [bit11:bit0]) are the 12 data bits for the digital to analog conversion.

The transfer function is the very-simple, (almost) typical-for-DACs:

Vout = (D_12 / 4096) * 2.048V * Gain

D_12 is the digital value given by D11 to D0 in the write command, 4096 = 2^12 (12-bit resolution), 2.048V = internal voltage reference, gain = 1 or 2 depending on bit GA in write command.

While I typically use the registers and configure them manually, I decided to use the Microchip plib (peripheral library) here for the SPI module. The peripheral library consists of a lot of low-level functions and macros that essentially require understanding the peripheral but just make the code nicer and easier to read.

I decided to clock the MCP4822 at 20MHz (max for MCP4822) to get quick data transfers.

The SPI module can be initialized using one of 2 options:
1) Using the OpenSPIx(config1, config2) and SpiChnSetBrg(spi_channel, spi_brg) functions
2) Using the SpiChnOpen(spi_channel, config, spi_divider) function

These are both described in the plib documentation.

The DAC initialization routine I wrote is shown below:
// SS = PORTA4
#define SS      LATAbits.LATA4
#define dirSS   TRISAbits.TRISA4
void initDAC(void){
/* Steps:
 *    1. Setup SS as digital output.
 *    2. Map SDO to physical pin.
 *    3. Configure SPI control and clock with either of a or b:
 *        a. OpenSPIx(config1, config2) and SpiChnSetBrg(spi_channel, spi_brg)
 *        b. SpiChnOpen(spi_channel, config, spi_divider)

    dirSS = 0;                    // make SS an output
    SS = 1;                        // set SS = 1 to deselect slave
    PPSOutput(2, RPB5, SDO2);    // map SDO2 to RB5

     *    ENABLE_SDO_PIN        -> SPI Output pin enabled
     *    SPI_MODE16_ON        -> 16-bit SPI mode
     *    SPI_SMP_OFF            -> Sample at middle of data output time
     *    SPI_CKE_ON            -> Output data changes on transition from active clock
     *                            to idle clock state
     *    SLAVE_ENABLE_OFF    -> Manual SW control of SS
     *    MASTER_ENABLE_ON    -> Master mode enable
#define config2 SPI_ENABLE
    /*    SPI_ENABLE    -> Enable SPI module
//    OpenSPI2(config1, config2);
    // see pg 193 in plib reference

#define spi_channel    2
    // Use channel 2 since channel 1 is used by TFT display

#define spi_brg    0
    // Divider = 2 * (spi_brg + 1)
    // Divide by 2 to get SPI clock of FPBDIV/2 -> max SPI clock

//    SpiChnSetBrg(spi_channel, spi_brg);
    // see pg 203 in plib reference



#define spi_divider 2
/* Unlike OpenSPIx(), config for SpiChnOpen describes the non-default
 * settings. eg for OpenSPI2(), use SPI_SMP_OFF (default) to sample
 * at the middle of the data output, use SPI_SMP_ON to sample at end. For
 * SpiChnOpen, using SPICON_SMP as a parameter will use the non-default
 * SPI_SMP_ON setting.
    /*    SPI_OPEN_MSTEN        -> Master mode enable
     *    SPI_OPEN_MODE16        -> 16-bit SPI mode
     *    SPI_OPEN_DISSDI        -> Disable SDI pin since PIC32 to DAC is a
     *                            master-to-slave    only communication
     *    SPI_OPEN_CKE_REV    -> Output data changes on transition from active
     *                            clock to idle clock state
//    SpiChnOpen(spi_channel, config, spi_divider);


The code block above has code written using both the initialization functions (one is commented out). Essentially, the configuration is that SPI channel 2 is enabled as a master, it is configured for 16-bit data transmission, the SDI pin is disabled, it is configured for serial output change from active high (1) to active low (0) as required by the MCP4822 (see Fig. 2) and the clock divisor is set to 2 so that SPI clock = 20MHz.

The SCK pins for the PIC32 are mapped physically to the RB15 (SCK2) and RB14 (SCK1) pins (check pinout on page 4 in datasheet). The SDO and SDI pins are remappable pins and thus require you to map them to physical pins. The SDI pin here is unused since the PIC32-to-MCP4822 is a one way communication with the PIC32 as the master and the MCP4822 as the slave.

The datasheet provides the table for all the PPS (peripheral pin select) mappings. The relevant section for the SDO is:

Table 1 – PPS configuration for SDO

I decided to use RPB5:

PPSOutput(2, RPB5, SDO2);    // map SDO2 to RB5

The connections I used for the MCP4822 are:

Fig. 3 – MCP4822 connections

The chip select / slave select line (CS as shown on the MCP4822 pinout, see Fig. 1) has to be controlled manually. (For a hardware-based processor-free control scheme, see PIC32 DMA+SPI+DAC : Analog output synthesis with zero CPU overhead.) This is an active low chip select signal. When data is being sent to the MCP4822, while the SPI transaction is occurring, the CS pin must be kept low. Initially I checked to see if there was a way the hardware would take care of that. I looked into the framed SPI mode. However, this did not serve my purpose. From what I’ve read, the framed SPI mode is an SPI mode where the clock is always output (instead of only during transactions as traditionally done). However, the hardware generates a low sync signal (by pulling CS low) before the data is transmitted from the PIC32. The problem here was that the CS pin was pulled low and kept low for one SPI clock period before raising CS high as data is transmitted out from SDO. This isn’t going to work for the MCP4822 since the chip requires that CS be held low during the entirety of data transmission (see Fig. 2). So I just decided to use RA4 as the pin for CS. Before starting any transmission, I pull CS low in software and raise it high at the end.

Here’s the code for writing to the DAC:

inline void writeDAC(uint16_t data){
    SS = 0; // select slave device: MCP4822 DAC
    while (TxBufFullSPI2()); // ensure buffer is free before writing
    WriteSPI2(data);   // send the data through SPI
    while (SPI2STATbits.SPIBUSY); // blocking wait for end of transaction
    SS = 1; // deselect slave device, transmission complete

Since I called this function from the ISR in my code, I decided to make it an inline function to eliminate function call overhead.

In my test code, I just wanted to see how fast an output I can get. So I clocked the MCP4822 at 20MHz and tried to maximize the frequency of the output signal I generate: I used channel A to generate a sine wave output and channel B to generate a triangle wave output. I used a timer (Timer 1) to control the timing and in the ISR, I just called the writeDAC inline function to generate outputs as required from two pre-generated tables.

Here are the different waveforms and outputs I generated:

Fig. 4 – Generating the outputs with timer period = 400kHz, sine table entries = 64


Fig. 5 – Generating the outputs with timer period = 550kHz, sine table entries = 32


Fig. 6 – Generating the outputs with timer period = 550kHz, sine table entries = 64


Fig. 7 – SPI clock and data transmission
You can clearly see the SPI clock during transmission here. From the measurement panel on the right you can see that the SPI clock is 20MHz. There are also 16 pulses you can count – since a write command to the DAC consists of 16 bit transfers. CH2, on the bottom, shows the data being transmitted. Since the first signal (leftmost) is a 1, and since this corresponds to bit 15, this is a snapshot of data being transmitted to DAC channel B.


Fig. 8 – DAC steps – zoomed in


Fig. 9 – DAC steps – zoomed out
Fig. 10 – Hardware test setup
For the PIC32 proto board, I used my own custom proto board:

Here are the header and source files for my test program:


While this was a quick test I did to test the PIC32 SPI and the MCP4822, this is in no means a complete evaluation of the MCP4822. For example, I updated the DAC more quickly than I should have, to push for speed, as this didn’t give enough of a settling time. However, I was satisfied with the results and so, stuck with that. I will do some further testing on this later.

For more details of the PIC32MX250F128B SPI module, refer to the datasheet and reference manual. For more details on the MCP4822, refer to its datasheet.

If you have any questions, feel free to ask. Let me know what you think, in the comments section below!

Original Article by Tahmid