PIC32 DMA+SPI+DAC : Analog output synthesis with zero CPU overhead

I have previously shown how to use the PIC32 SPI module to use the 12-bit DAC MCP4822: http://electel.blogspot.com/2016/10/pic32-spi-using-mcp4822-12-bit-serial.html. While that does allow you to generate analog outputs as desired, it requires you to use CPU cycles to process the timer interrupt and accordingly drive the SPI module.

Since the PIC32 contains DMA channels, the process can be completely offloaded from the CPU. For an idea of the PIC32 DMA module, refer to my previously written article: http://electel.blogspot.com/2016/05/simple-pic32-dma-example.html

Fig. 1 - The generated sine wave at fpwm = 400kHz and 32 elements in the sine table

So, the simple way of offloading the SPI update to the DMA module would be to let the DMA channel transfer data to the SPI buffer. The SPI module is configured for 16-bit data transfer (since the MCP4822 write command requires 2 bytes). This means that the cell size for the DMA channel has been set to 2 (2 bytes to transfer once triggered). The DMA transfer is triggered by a timer interrupt. Unlike the previous example where I used Timer 1 and its interrupt for the SPI transfer, here I use Timer 2. The reason for this is that, if I want the entire process to be offloaded from the CPU, the SS (Slave Select) or CS (Chip Select) also has to be done entirely in hardware. For that I used the Output Compare 1 module. To use the OC1 module, either Timer 2 or Timer 3 (or the combined 32-bit timer) has to be used. So, I just went with Timer 2.

Configuring the DMA module for transferring data to the SPI buffer was the simple part. I found the configuration of the SS/CS pin the more challenging part. The idea here was to use the OC module in "dual compare mode continuous output pulses" mode. The OC module in this mode generates continuous pulses - the output pin OC1 - which I used as CS/SS - is set high one PBCLK (peripheral bus clock) after the Timer value reaches OC1R; the OC1 pin is cleared one PBCLK after the Timer value reaches OC1RS. Since SS/CS is active low, I set ( (period register) - 4) to be OC1RS and a variable CSlength to be OC1R. CSlength was chosen to be 80% of the period register. What this meant was that. One PBCLK after the Timer reached the (period register - 4), the OC1 pin went low (CS/SS went low) "selecting" the DAC. After about 0.80*(period register) from there, the OC1 pin went high. This means that the CS pin is low for 80% of the period - it goes low a small time right before the DMA transfer happens and is raised high late enough, after the DMA transfer occurs. I determined that 80% the period was enough time since that is higher than the time required to shift out 16 bits of data at 20MHz SPI clock. See Fig. 2 below for an illustration of the operation of the output compare module in the "dual compare mode continuous output pulses" mode:

 Fig. 2 - Output Compare module in Dual Compare Mode: Continuous Output Pulse mode (taken from PIC32 reference manual, figure 16-16)

Beyond that, the idea is simple. There is a sine table that is the DMA source. The key thing to remember here is that the 12-bit data must be OR-ed with 0x3000 since that is required for the DAC (to set gain=1, shutdown = 0 and channel = A):

void generateTables(void){
    uint8_t i;
    for (i = 0; i<TABLE_SIZE; i++){
        sineTable[i] = (short) (2047.0 * sin(6.2832*((float)i)/(float)TABLE_SIZE));
        sineTable[i] = sineTable[i] + 2047;

        sineTable[i] = 0x3000 | sineTable[i];
    }
}

Since the entries in the sine table are of "short" data type (each element occupies two bytes), the source size (in bytes) is twice the number of elements. The destination source size is two bytes since I'm transferring two bytes to the SPI buffer register. The cell size is two bytes since the DMA channel has to transfer two bytes (16 bits) at a time to the SPI buffer register. The DMA configuration is:

    DmaChnOpen(0, 3, DMA_OPEN_AUTO);
    // (ch, ch priority, mode)
    DmaChnSetTxfer(0, sineTable, &SPI1BUF, TABLE_SIZE*2, 2, 2);
    // (ch, start virtual addr, dest virtual addr, source size, dest size, cell size)
    DmaChnSetEventControl(0, DMA_EV_START_IRQ(_TIMER_2_IRQ));
    // (ch, trigger irq)
    DmaChnEnable(0);

A point of note is that, the DmaChnSetTxfer( ) takes in the virtual addresses of the source and destination, not the physical addresses. The virtual-to-physical address conversion is done by the function itself as opposed to it being required manually when doing register level operations (as done in my previous DMA example article).

To demonstrate that the DAC control is done with no CPU overhead, in the main( ) function, I have an infinite loop that is just toggling RA0. The output is checked on an oscilloscope:

Fig. 3 - RA0 toggle being demonstrated

Observe that the frequency of the square wave is 4.0MHz - the same that would be observed if all the PIC was doing was the pin toggling.

This also gave better performance than my previous experiment where I did not use DMA. The output sine wave has a higher frequency than observed before and this matches exactly as expected: 400kHz/32 = 12.5kHz as seen (see Fig. 1).

The rest should be very easy to understand and self-explanatory. If you have any questions or suggestions, feel free to comment!

Here are the source and header files:
config.h: https://drive.google.com/file/d/0B4SoPFPRNziHcmlxMmg2Si0zWjQ/view?usp=sharing
main.c: https://drive.google.com/file/d/0B4SoPFPRNziHcmlxMmg2Si0zWjQ/view?usp=sharing

Comments

Popular posts from this blog

Using the TLP250 Isolated MOSFET Driver - Explanation and Example Circuits

N-Channel MOSFET High-Side Drive: When, Why and How?

Using the high-low side driver IR2110 - explanation and plenty of example circuits