I2S for PIC32MX/MZ – Direct Memory Access (DMA)

Skill level: Advanced, with C and 32-bit experience.

This tutorial requires an oscilloscope to confirm the resulting output!

Required: Cytron SK1632, PIC32MX150F128B, MPLAB X IDE, MPLAB XC32 1.40, MPLAB Harmony v1.07 and above.

In the previous tutorial, you have learned to generate a sine wave using DDS (Digital Direct Synthesis) method and transfer the sample data into the audio DAC through I2S bus.

Remember about the SPI transfer complete interrupts in the other earlier tutorials? Everytime an audio sample is done being sent to the DAC, the interrupt triggers and the next sample is being placed into the SPI buffer. With the small DDS algorithm in the interrupt area, you get the sine wave output from the DAC’s output.

However, there is one issue: the sampling rate specified in the microcontroller is 32kHz, and the SPI transfer complete interrupt happens below every 31.25μs. (Two audio channels: 31.25μs, and then for one is 15.625μs)

The PIC32MX/MZ microcontrollers are fast but still it could not catch up with the interrupts if more processing of the samples have to be done. Sure, the small DDS code in the interrupt vector in the earlier tutorial works well, but when you have to further process the samples (like filtering), another method must be employed.

Here it is, these 32-bit microcontrollers are equipped with a DMA (Direct Memory Access) module. What is a DMA? It allows the microcontroller to transfer data between memory to peripherals (or peripherals to other peripherals) without the intervention of the CPU inside. In short, it frees the CPU from sending the samples one-by-one, allowing you to do other tasks while the system is pushing the samples into the DAC.

As usual, you have to set up the DMA modules and its related interrupts. Here is the initialization code to enable and configure the DMA modules. Note that we have more than one DMA channel, so we are using two here, and attach the DMA channel sources to the buffers:

DMACONCLR = 0x8000; // disable entire DMA.
IEC1bits.DMA0IE = 1;
IFS1bits.DMA0IF = 0;
IPC10bits.DMA0IP = 7; // Setting DMA0 at highest priority.
IPC10bits.DMA0IS = 3; // Setting DMA0 at highest sub-priority.
DMACONSET = 0x8000; // enable DMA.
DCH0CON = 0x0000;
DCRCCON = 0x00; // 
DCH0INTCLR = 0xff00ff; // clear DMA0 interrupts register.
DCH0INTbits.CHSDIE = 1; // DMA0 Interrupts when source done enabled.
DCH0ECON = 0x00;
DCH0SSA = KVA_TO_PA(&buffer_a[0]); // DMA0 source address.
DCH0DSA = KVA_TO_PA(&SPI1BUF); // DMA0 destination address.
DCH0SSIZ = BUFFER_LENGTH*2; // DMA0 Source size (default).
DCH0DSIZ = 2; // DMA0 destination size.
DCH0CSIZ = 2; // DMA0 cell size.
DCH0ECONbits.CHSIRQ = _SPI1_TX_IRQ; // DMA0 transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)
DCH0ECONbits.AIRQEN = 0; // do not enable DMA0 transfer abort interrupt.
DCH0ECONbits.SIRQEN = 1; // enable DMA0 transfer start interrupt.
DCH0CONbits.CHAEN = 0; // DMA Channel 0 is always disabled right after the transfer.
DCH0CONbits.CHEN = 1; // DMA Channel 0 is enabled. 
IEC1bits.DMA1IE = 1;
IFS1bits.DMA1IF = 0;
IPC10bits.DMA1IP = 7; // Setting DMA1 at highest priority.
IPC10bits.DMA1IS = 3; // Setting DMA1 at highest sub-priority.
DCH1CON = 0x0000;
DCH1INTCLR = 0xff00ff; // clear DMA1 interrupts register.
DCH1INTbits.CHSDIE = 1; // DMA1 Interrupts when source done enabled.
DCH1ECON = 0x00;
DCH1SSA = KVA_TO_PA(&buffer_b[0]); // DMA1 source address.
DCH1DSA = KVA_TO_PA(&SPI1BUF); // DMA1 destination address.
DCH1SSIZ = BUFFER_LENGTH*2; // DMA1 Source size (default).
DCH1DSIZ = 2; // DMA1 destination size.
DCH1CSIZ = 2; // DMA1 cell size.
DCH1ECONbits.CHSIRQ = _SPI1_TX_IRQ; // DMA1 transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)
DCH1ECONbits.AIRQEN = 0; // do not enable DMA1 transfer abort interrupt.
DCH1ECONbits.SIRQEN = 1; // enable DMA1 transfer start interrupt.
DCH1CONbits.CHAEN = 0; // DMA Channel 1 is always disabled right after the transfer.
DCH1CONbits.CHEN = 0; // DMA Channel 1 is enabled.

And also, we need to declare an array for the DMA module, because you need to place the sound samples into the data memory (the global area) first before these are transferred automatically to the DAC:

short int buffer_a[BUFFER_LENGTH];

That is simple. BUFFER_LENGTH is 64 in this tutorial. is However, if you are dealing with PIC32MZ, additional keywords must be used:

short int __attribute__((coherent)) buffer_a[BUFFER_LENGTH] ;

Why that “coherent” keyword? The PIC32MZ has an L1 (level 1) cache inside to speed up operations by allocating, queueing and managing the data into a special area (which is the L1 cache) before it is processed by the CPU. [In this microcontroller with that architecture, the variables are allocated/declared into the cacheable area by default, unless the “coherent” keyword is specified.]

In that case, this buffer is declared at the non-cacheable area in the PIC32MZ. It means the data in the buffer do not go into the cache during transfer. Therefore, all the contents in the buffer are safely (and directly) transferred to the peripheral without the interference of the contents in the L1 cache during the process.

And, don’t forget this too:

DCH0SSA = KVA_TO_PA(&buffer_a); // DMA source address.
DCH1SSA = KVA_TO_PA(&buffer_b); // DMA source address.

You give the DMA module the address of your buffer to allow the module to transfer the data out into the SPI module.

  • DCH0SSA simply means “DMA channel 0 source address”.
  • Similarly, DCH1SSA simply means “DMA channel 1 source address”.
  • On PIC32MX/MZ, the “KVA_TO_PA” means  “convert virtual address to physical address”. The DMA module only accepts physical address and you need to convert them first before it can do anything meaningful.

Coming back to the DMA, instead of you generating the audio samples per SPI transfer complete interrupt, you do all the activities in the main loop instead.

The activities mentioned can be generating the samples of the sine wave and put a part of them into the buffer. The DMA issues an interrupt if all the contents in the buffer are being transferred.

This interrupt is very less frequent than the continuous SPI transfer complete interrupts too.  Look at the “IFS1bits.DMA0IF” and the “DCH0INTbits.CHDDIF” (CHDDIF means Channel Destination Done Interrupt Flag) in the following interrupt code. So when the DMA is done transferring the data, these interrupt flags go up, and you clear them inside the handler. Besides that, you can do some other things when the DMA has finished doing the job – it’ll be discussed afterwards.

The interrupts cleared are:

  • IFS1bits.DMA0IF         -> The DMA0 channel interrupt.
  • DCH0INTbits.CHDDIF -> The DMA0 channel destination done complete interrupt.

In short, the system is configured to assert an interrupt if the DMA channel 0  or DMA channel 1 which have fully completed the transfer (when the DMA channel reads to the end of the array). It is possible to configure the system to assert an interrupt when the transfer is half-complete too but in the tutorial, the interrupt happens when the transfer is entirely complete.

// source: http://chipkit.net/forum/viewtopic.php?t=3137
void __ISR(_DMA0_VECTOR, ipl7AUTO) _IntHandlerSysDmaCh0(void)
{ 
bufferAFull = 0;  
DCH1CONSET = 0x0000080;
DCH0INTCLR = 0x0000ff;
IFS1CLR    = (1<<28);
//SYS_DMA_TasksISR(sysObj.sysDma, DMA_CHANNEL_0);
}
void __ISR(_DMA1_VECTOR, ipl7AUTO) _IntHandlerSysDmaCh1(void)
{        
bufferBFull = 0;
DCH0CONSET = 0x00000080;
DCH1INTCLR = 0x0000ff;
IFS1CLR    = (1<<29);
//SYS_DMA_TasksISR(sysObj.sysDma, DMA_CHANNEL_1);
}

Double Buffering

Apart from that, when the system is busy transferring the contents of the buffer to the SPI module, modifying the stuff inside could cause the audio to crackle and pop. Well, with the ample data memory in PIC32s, you can declare another buffer which stores 512 samples (it must be identical to the other buffer). Let’s call the buffers buffer_a and buffer_b:

1.) The DMA channel 0 starts first by pushing the contents of the buffer_a to the SPI module.

2.) Some time passes. Once the interrupt flag for DMA channel 0 has been up, the transfer begins for buffer_b by switching on the DMA channel 1, and buffer_a can be accessed by the user. Buffer_b is now occupied by DMA channel 1 as the contents inside are being transferred to the SPI module.

3.) At the time DMA channel 1 interrupt is up, the transfer begins for buffer_a by switching on the DMA channel 0, and buffer_b can be accessed by the user. Buffer_a is now occupied by DMA channel 1 as the contents inside are being transferred to the SPI module.

This process (2 to 3) repeats over and over again, and it is called “double buffering method” or “ping-pong buffers“.

But first and foremost, the DMA and the SPI module must be initialized. The code is as follows:

void i2s_init_DMA(void) {
DMACONCLR = 0x8000; // disable entire DMA.
IEC1bits.DMA0IE = 1;
IFS1bits.DMA0IF = 0;
IPC10bits.DMA0IP = 7; // Setting DMA0 at highest priority.
IPC10bits.DMA0IS = 3; // Setting DMA0 at highest sub-priority.
DMACONSET = 0x8000; // enable DMA.
DCH0CON = 0x0000;
DCRCCON = 0x00; // 
DCH0INTCLR = 0xff00ff; // clear DMA0 interrupts register.
DCH0INTbits.CHSDIE = 1; // DMA0 Interrupts when source done enabled.
DCH0ECON = 0x00;
DCH0SSA = KVA_TO_PA(&buffer_a[0]); // DMA0 source address.
DCH0DSA = KVA_TO_PA(&SPI1BUF); // DMA0 destination address.
DCH0SSIZ = BUFFER_LENGTH*2; // DMA0 Source size (default).
DCH0DSIZ = 2; // DMA0 destination size.
DCH0CSIZ = 2; // DMA0 cell size.
DCH0ECONbits.CHSIRQ = _SPI1_TX_IRQ; // DMA0 transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)
DCH0ECONbits.AIRQEN = 0; // do not enable DMA0 transfer abort interrupt.
DCH0ECONbits.SIRQEN = 1; // enable DMA0 transfer start interrupt.
DCH0CONbits.CHAEN = 0; // DMA Channel 0 is always disabled right after the transfer.
DCH0CONbits.CHEN = 1; // DMA Channel 0 is enabled. 
IEC1bits.DMA1IE = 1;
IFS1bits.DMA1IF = 0;
IPC10bits.DMA1IP = 7; // Setting DMA1 at highest priority.
IPC10bits.DMA1IS = 3; // Setting DMA1 at highest sub-priority.
DCH1CON = 0x0000;
DCH1INTCLR = 0xff00ff; // clear DMA1 interrupts register.
DCH1INTbits.CHSDIE = 1; // DMA1 Interrupts when source done enabled.
DCH1ECON = 0x00;
DCH1SSA = KVA_TO_PA(&buffer_b[0]); // DMA1 source address.
DCH1DSA = KVA_TO_PA(&SPI1BUF); // DMA1 destination address.
DCH1SSIZ = BUFFER_LENGTH*2; // DMA1 Source size (default).
DCH1DSIZ = 2; // DMA1 destination size.
DCH1CSIZ = 2; // DMA1 cell size.
DCH1ECONbits.CHSIRQ = _SPI1_TX_IRQ; // DMA1 transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)
DCH1ECONbits.AIRQEN = 0; // do not enable DMA1 transfer abort interrupt.
DCH1ECONbits.SIRQEN = 1; // enable DMA1 transfer start interrupt.
DCH1CONbits.CHAEN = 0; // DMA Channel 1 is always disabled right after the transfer.
DCH1CONbits.CHEN = 0; // DMA Channel 1 is enabled. 
}
void init_i2s1() {
// http://chipkit.net/forum/viewtopic.php?f=6&t=3137&start=10
/* The following code example will initialize the SPI1 Module in I2S Master mode. */
/* It assumes that none of the SPI1 input pins are shared with an analog input. */
unsigned int rData;
IEC0CLR = 0x03800000; // disable all interrupts
IFS1bits.SPI1TXIF = 0;
SPI1CON = 0; // Stops and resets the SPI1.
SPI1CON2 = 0; // Reset audio settings
SPI1BRG = 0; // Reset Baud rate register
rData = SPI1BUF; // clears the receive buffer
SPI1STATCLR = 0x40; // clear the Overflow
SPI1CON2 = 0x00000080; // I2S Mode, AUDEN = 1, AUDMON = 0
SPI1CON2bits.IGNROV = 1; // Ignore Receive Overflow bit (for Audio Data Transmissions)
SPI1CON2bits.IGNTUR = 1; //  Ignore Transmit Underrun bit (for Audio Data Transmissions) 1 = A TUR is not a critical error and zeros are transmitted until thSPIxTXB is not empty 0 = A TUR is a critical error which stop SPI operation
SPI1CONbits.ENHBUF = 1; // 1 = Enhanced Buffer mode is enabled
SPI1BRG = 9;
SPI1CON = 0x00000060; // Master mode, SPI ON, CKP = 1, 16-bit audio channel
SPI1CONbits.STXISEL = 0b11;
SPI1CONbits.DISSDI = 1; // 0 = Disable SDI bit
SPI1CONSET = 0x00008000;
IFS1CLR = 0x000000f0;
IPC7CLR = 0x1F000000;
IPC7SET = 0x1C000000;
IEC1bits.SPI1TXIE = 0;
// data, 32 bits per frame
// from here, the device is ready to receive and transmit data
/* Note: A few of bits related to frame settings are not required to be set in the SPI1CON */
/* register during audio mode operation. Please refer to the notes in the SPIxCON2 register.*/
}

After starting all the DMA and SPI module, the program does its job by switching the buffers every time the contents of the buffer are fully transferred.

The simplified program flow is as follows:

At least for now you can have more time for the samples to be processed! There is a lot of things you can do with the samples in that main loop. You will wonder what kind of thing you can actually do on that – check out the next tutorial where you can play a nice tune by combining the I2S and DMA!

The sample code is in the Github – please check it out!

https://github.com/uncle-yong/sk1632-i2s-dma-intro

References:

1.) Lucio di Jasio, Programming 32-bit Microcontrollers in C: Exploring the PIC32

2.) PIC32 SPI Reference Manual: ww1.microchip.com/downloads/en/DeviceDoc/61106G.pdf

3.) PIC32 DMA Reference Manual: ww1.microchip.com/downloads/en/DeviceDoc/60001117H.pdf

4.) PIC32MZ Cache Coherency: microchipdeveloper.com/faq:83

5.) PIC32MZ Cache Coherency: http://microchipdeveloper.com/32bit:mz-cache-policy

6.) Double buffering using two DMA channels, courtesy of Majenko: http://chipkit.net/forum/viewtopic.php?t=3137

7.) Ping-pong buffers: http://embedded.fm/blog/2017/3/21/ping-pong-buffers

, , ,

Related Post

I2S for PIC32MX/MZ – Application: Music Box

I2S for PIC32MX/MZ – Sine wave generation using DDS

I2S for PIC32MX/MZ – Introduction

Driving RainbowBits with Cytron’s sk1632!

Leave a Reply

Your email address will not be published. Required fields are marked *