Working on DMA

What I've been doing and progress on DMA

These last few weeks I've been working on a project for work which is basically putting the DAC-based MP3 player onto a PIC32MX250F256B chip, attaching some serial flash memory and having it attach to the PC via USB in vendor mode, which meant I had to write an app to do that too. Harmony works fine for this and you can still open up their examples and modify them to get them to work. I'm really not a fan of how Harmony splits up everything into its own little file and then needs the Harmony library too, it makes working on the project on multiple machines - which I do - a pain. Personal preference I guess.

Anyway, I'm back to working on trying to get SD card reads to work in DMA mode. With my previous code, the maximum speed I saw was about 3302kB/s, even with huge sustained block reads of 64kB. I'm happy to report that I'm currently getting a bit over 5000kB/s with 64kB sustained block reads. Please note: I am not currently working on DMA mode to get background reads working, I am doing this solely for the speed increase it provides.

I know the SD card spec is for 25MHz but everything seems to work fine at 50MHz. I'm not putting my code into hospital equipment, it's all hobbyist / personal stuff so I just want speed. There are some reliability issues that I'm working on but I hope to upload the code in a few days. Even if it doesn't work, I'll upload it and someone smarter than me can hopefully show me where I've gone wrong. Until then, good luck with the PIC32!

Update (2019-01-07): Here are some results from my tests, comparing the original SD SPI code that I got 8 years ago, my newer 32-bit enhanced buffers code and my new 8-bit enhanced buffers DMA code. The results are fantastic at bigger block sizes!

PIC32MZ - SPI SD transfer speed comparison

HERE BE DRAGONS

There may well still be issues with this code!

I'm uploading this code for anyone who cares to test but be warned. You have to make the following changes to your code:

  • Add this line PRISS = 0x76543210; after you've set up TRIS registers, to enable the SRS registers I use in the DMA interrupt
  • Set SCK as an input (in my case, SPI2 needs TRISG6 set to 1). I have no idea why this is but it refuses to work without it and caused untold hours of debugging!
  • Change your read buffer declaration from unsigned char read_buffer[8192] to __attribute__((coherent)) unsigned char read_buffer[8192];
  • Pray

This code is provided as is and you should fully expect it to have issues. I'm providing it because it works for me, on my own board and in my own programs and I thought someone might like to get their hands on it while I slowly write the post on DMA. Good luck! Now to write that post on DMA...

Here's the code

Tags: blah, DMA

I2C on the PIC32MZ

What is I2C?

I2C is short for Inter-Intergrated Circuit, and people pronounce it I-squared-C or I2C. It's a way of connecting multiple ICs using just two wires, called the Serial Data Line (SDA) and the Serial Clock Line (SCL). Before I even begin, there is something very important to be aware of. The lines are open-drain (aka open-collector). This means that devices on the I2C bus can only pull the lines low or leave them open/floating.

This means that both SDA and SCL need a pullup resistor or they will not be able to set the lines high.

To further drive this home, this is what I mean:

PIC32MZ - I2C pullups

I've personally seen the value of these resistors be anywhere from 1.8k to 47k with many people recommending 4.7k or 10k. The important thing is that they are there.

I repeat, I2C will not work at all without these pullup resistors. Got it? Good, because that really wasted a lot of my time the first time I tried to use I2C. OK, on with the show.

An overview of the I2C protocol

Both SDA and SCL are bidirectional communication signals, they can be changed by either master or slave. Compare this to SPI which needed 3 (MISO, MOSI and CLK) just for communication and then an additional Chip Select for each device and you can see why this can be benficial in systems with multiple devices. Of course, using only two wires means to talk to a lot of devices means that I2C is going to be both more complex and slower than SPI but it's very useful nonetheless.

Today we are going to take a look at the most common situation us hobbyists find ourselves in, namely with one master device and with multiple slave devices. The master is responsible for initiating communication and providing the clock signals. For the purposes of today's article, the PIC32MZ is going to be the master device.

<TL;DR>
What if the clock signal is too fast for the slave device to handle, or it needs extra time to process data? In the I2C protocol there is a way for slave devices to force the master to wait and this is called clock stretching. In master mode, the PIC32 can detect and handle this automatically and we don't need to worry about it. If we want to implement clock stretching, we can do so by setting the STR_EN bit in I2CxCON.
</TL;DR>

So how does it all work? Well, all devices are connected to the same SDA and SCL wires, forming what is known as a bus. Each device on the bus has its own unique address. Slave devices are constantly listening out for this address to be broadcast.

Once a special signal called a start signal is seen on the bus, slave devices wake up and listen. The first data sent is always the address of the slave device we want to talk to. Once a slave device sees its address on the bus, it replies to the master and communication begins. The rest of the devices then ignore the following transfers. Today let's assume that everything is set up nicely and no devices have the same addresses or anything strange like that. I will also be assuming that the devices used have 7-bit addresses. I have read that I2C supports 10-bit devices but I've never seen one myself and I've been through tons of eBay and Aliexpress modules.

So when we write an I2C slave's address to the bus, how does it know whether we want to write to it or read from it? Well, assuming we're using 7-bit addresses it looks like this:

PIC32MZ - I2C Address

So there you can see the 7-bits of the address and two other bits. The first, labelled RW, is the Read/Write bit. This tells the device we're talking to whether we're wanting to read from it (RW set to 1) or write to it (RW set to 0). The second, labelled ACK, is the Acknowledge bit. This is used to tell the device we received data successfully from it and are ready to continue. If we do not send the ACK bit the device sending the data will assume we are not ready to receive more data.

Something to bear in mind, therefore, is that it takes 9 SCL clock pulses to send an 8-bit byte of data because the Acknowledge bit needs to be sent/received too and the master is responsible for generating that clock pulse. Also note that for data bytes there is no Read/Write bit, this is all set up initially when sending out the address. This means that if you want to write some data and then immediately read some data again you will need to send another start signal and another address byte with the read/write bit set.

There are several kinds of signals that you need to be aware of when using I2C:

  • Start - Tells the slave devices on the bus to start listening.
  • Stop - Ends communication, telling slave devices they can go back into idle mode. To restart communication a new start signal must be sent.
  • Acknowledge (ACK) - Used by devices receiving data to acknowledge they have received the data and are ready for more data.
  • Not Acknowledge (NACK) - Not really a signal of its own, just a way to skip the acknowledge signal. This could be because either no more data is wanted or the device is not ready to receive more data.
  • Write - Writes a byte to the I2C bus. For this to be a write, the Read/Write bit must be set to low.
  • Read - Read a byte from the I2C bus. For this to work, the Read/Write bit must be set to high.

There is one more called the repeated start. This is used when we want to continue sending data and don't want to have to stop and start again with the same slave device.

<TL;DR>

Although the PIC32MZ hardware handles the signals electrically, let's take a look at what those signals mean in terms of setting SDA and SCL high and low. Bear in mind that, due to the pullup resistors that you did not forget about, the default values of SCL and SDA are high.

  • Start - SCL remains high and SDA changes from high to low.
  • Stop - SCL remains high but SDA changes from low to high.
  • Repeated start - Electrically the same as start
  • Acknowledge - SDA is set low while SCL sends a clock pulse
  • Not acknowledge - SDA is set high while SCL sends a clock pulse

</TL;DR>

Order of operations

Let's take a look at two example. In the first, a byte (8-bits) of data is being written to a slave that has an I2C address of 0x68. The slave's internal register number is 0x56 and we want to set it to a value of 0x23.
The order of operations would be:

  • Send start signal
  • Write slave address with Read/Write bit set to 0 (Send 0x68 << 1 = 0xD0)
  • Receive ACK
  • Write register address (0x56)
  • Receive ACK
  • Write data value (0x23)
  • Send stop signal

That second line is going to be confusing. Take a look again at how the 9-bit number is made up above. We are actually only sending 8 bits, the ACK is automatic so let's look at the first 8 bits. The upper 7 bits contain the slave's address and the least significant bit is the R/W bit. This means that 7-bit I2C addresses need to be shifted left by 1 when writing them to the I2C bus.

For the second example, 8 bits of data will be read from the same slave, register number 0x73.
The order of operations would be:

  • Send start signal
  • Write slave address with Read/Write bit set to 1 (Send 0x68 << 1 | 1 = 0xD1)
  • Receive ACK
  • Write register address (0x75)
  • Receive ACK
  • Receive data byte (8-bits)
  • Send NACK
  • Send stop signal

Again, that second line. If we want to set the least significant bit, we can either add 1 to the address or OR it by 1, which I prefer because it looks more confusing.

Also, why am I sending a NACK instead of an ACK? This is because I only want to receive one 8-bit number and end the transaction. If I send an ACK, the slave device will start sending me more data.

The PIC32MZ I2C hardware and registers

The PIC32MZ I2C hardware is generally very easy to use. There is no Peripheral Pin Select (PPS) stuff to worry about, the pins are hardwired. The 144-pin device I uses has five I2C peripherals. Today I will be looking at using the first one, namely I2C1. This means the two physical pins I will connect to are SDA1 (on port RA15) and SCL1 (on port RA14).

Although I've made I2C seem incredibly difficult, it's very easy to use on the PIC32MZ. The following registers are used:

  • I2C1BRG - I2C1 Baud Rate Generator Register - Used for setting the speed of the I2C peripheral
  • I2C1CON - I2C1 Control Register - Used to set up I2C
  • I2C1TRN - I2C1 Transmit Data Register - Contains data we want to send onto the I2C bus
  • I2C1RCV - I2C1 Receive Data Register - Contains data received from the I2C bus
  • I2C1STAT - I2C1 Status Register - Contains the status of the I2C peripheral

Setting the speed of the I2C peripheral

OK, before we can jump straight into the code, let's take a look at that I2C1BRG register. It can run from 100kHz to 1Mhz, according to the product page for the PIC32MZ. I often run it at 400kHz in fact. However, as with all things PIC32MZ there are surprises contained in documents. In this case, the "PIC32MZ Embedded Connectivity with Floating Point Unit (EF) Family Silicon Errata and Data Sheet Clarification".

PIC32MZ - I2C pullups

In short, Microchip thinks that "software" solutions for their terrible I2C peripheral are acceptable. By software, they mean implementing the entire I2C protocol yourself on a port you want. Thankfully, 100kHz seems to work fine on I2C1. Please note, I2C3 straight up doesn't work according to the datasheet. I repeat, I2C3 does not work at all, do not use it!

So today, we're going to run at 100kHz because nobody has the time to debug impossible to find errors later on. Here's the formula for setting I2C1BRG to 100kHz:

PIC32MZ - I2C - BRG Formula

This formula is a bit harder than the SPI formula, but not too much so:

  • TPGD is a propagation delay, defined in the PIC32MZ EF datasheet as 104ns.
  • FSCK is the speed we want, so 100kHz for my example.
  • PBCLK is the speed of the peripheral bus clock. I2C uses Peripheral Bus Clock 2 (PBCLK2), which I've set to 100MHz.

So let's take a look at the code to do that:

// I2C_init() initialises I2C1 at at frequency of [frequency]Hz  
void I2C_init(double frequency)
{
    double BRG;

    I2C1CON = 0;            // Turn off I2C1 module
    I2C1CONbits.DISSLW = 1; // Disable slew rate for 100kHz

    BRG = (1 / (2 * frequency)) - 0.000000104;
    BRG *= (SYS_FREQ / 2) - 2;    

    I2C1BRG = (int)BRG;     // Set baud rate
    I2C1CONbits.ON = 1;     // Turn on I2C1 module
}

For stuff like this I always use double precision floating point numbers instead of setting registers directly. This makes debugging easier when stuff doesn't work later on :)

<TL;DR>

Slew rate? For bus speeds of 400kHz, the I2C specification requires that we have slew rate (or rate of change of output) control. For 100kHz, this should be disabled by setting DISSLW to 1.

</TL;DR>

Start, stop, restart, ACK and NACK

All of these are contained in one register, I2C1CON:

  • I2C1CONbits.SEN - Start Condition Enable bit
  • I2C1CONbits.PEN - Stop Condition Enable bit
  • I2C1CONbits.RSEN - Restart (Repeated start) Condition Enable bit
  • I2C1CONbits.ACKDT - Acknowledge Data bit. Set to 0 to ACK and 1 for NACK.
  • I2C1CONbits.ACKEN - Acknowledge Sequence Enable bit

There's a bunch of code here but it's fairly straightforward. I've separated them all into their own functions for readability:

// I2C_wait_for_idle() waits until the I2C peripheral is no longer doing anything  
void I2C_wait_for_idle(void)
{
    while(I2C1CON & 0x1F); // Acknowledge sequence not in progress
                                // Receive sequence not in progress
                                // Stop condition not in progress
                                // Repeated Start condition not in progress
                                // Start condition not in progress
    while(I2C1STATbits.TRSTAT); // Bit = 0 ? Master transmit is not in progress
}

// I2C_start() sends a start condition  
void I2C_start()
{
    I2C_wait_for_idle();
    I2C1CONbits.SEN = 1;
    while (I2C1CONbits.SEN == 1);
}

// I2C_stop() sends a stop condition  
void I2C_stop()
{
    I2C_wait_for_idle();
    I2C1CONbits.PEN = 1;
}

// I2C_restart() sends a repeated start/restart condition
void I2C_restart()
{
    I2C_wait_for_idle();
    I2C1CONbits.RSEN = 1;
    while (I2C1CONbits.RSEN == 1);
}

// I2C_ack() sends an ACK condition
void I2C_ack(void)
{
    I2C_wait_for_idle();
    I2C1CONbits.ACKDT = 0; // Set hardware to send ACK bit
    I2C1CONbits.ACKEN = 1; // Send ACK bit, will be automatically cleared by hardware when sent  
    while(I2C1CONbits.ACKEN); // Wait until ACKEN bit is cleared, meaning ACK bit has been sent
}

// I2C_nack() sends a NACK condition
void I2C_nack(void) // Acknowledge Data bit
{
    I2C_wait_for_idle();
    I2C1CONbits.ACKDT = 1; // Set hardware to send NACK bit
    I2C1CONbits.ACKEN = 1; // Send NACK bit, will be automatically cleared by hardware when sent  
    while(I2C1CONbits.ACKEN); // Wait until ACKEN bit is cleared, meaning NACK bit has been sent
}

Writing data to the I2C bus

This is simply a matter of writing to the I2C1TRN register, waiting for the Transmit Buffer to be empty by checking Transmit Buffer Full (TBF) flag and then waiting for the slave device to Acknowledge receipt.

// address is I2C slave address, set wait_ack to 1 to wait for ACK bit or anything else to skip ACK checking  
void I2C_write(unsigned char address, char wait_ack)
{
    I2C1TRN = address | 0;              // Send slave address with Read/Write bit cleared
    while (I2C1STATbits.TBF == 1);      // Wait until transmit buffer is empty
    I2C_wait_for_idle();                // Wait until I2C bus is idle
    if (wait_ack) while (I2C1STATbits.ACKSTAT == 1); // Wait until ACK is received  
}

Reading data from the I2C bus

For reading, we tell the I2C module to receive data, wait for it to clear the flag and until the receive buffer is full by checking Receive Buffer Full (RBF) flag and then send either an ACK or a NACK.

// value is the value of the data we want to send, set ack_nack to 0 to send an ACK or anything else to send a NACK  
void I2C_read(unsigned char *value, char ack_nack)
{
    I2C1CONbits.RCEN = 1;               // Receive enable
    while (I2C1CONbits.RCEN);           // Wait until RCEN is cleared (automatic)  
    while (!I2C1STATbits.RBF);          // Wait until Receive Buffer is Full (RBF flag)  
    *value = I2C1RCV;                   // Retrieve value from I2C1RCV

    if (!ack_nack)                      // Do we need to send an ACK or a NACK?  
        I2C_ack();                      // Send ACK  
    else
        I2C_nack();                     // Send NACK  
}

Actually using all this stuff in the real world

Theory is fun and all, but let's look at an example of using this in an actual slave device. I have a MPU-9250 module (9 degrees of freedom) module. It's a very complex module and I'm not going into it deeply today. However, as a test of whether or not I2C is working it'll do. Let's look in the datasheet for the MPU-9250 to find the address:

PIC32MZ - I2C - MPU9250 AD0

OK good, so I connect AD0 to ground and then the address will be 0x68. Excellent. Let's see what registers I can read from to make sure I2C is working:

PIC32MZ - I2C - MPU9250 WHOAMI

Excellent again. Register 117 (0x75) should return 0x68. Or 0x71...? Or in my case, 0x73. I think it must be a revision number or clones use something different. Point is, it's consistent and I've tested it with multiple modules to make sure it works and isn't random :) Let's see what the MPU9250's datasheet says about reading and writing:

PIC32MZ - I2C - MPU9250 Read
PIC32MZ - I2C - MPU9250 Write
PIC32MZ - I2C - MPU9250 Legend

OK, nothing too hard there. Let's see how the code for that looks:

#define MPU9250_ADDRESS 0x68            // The address of MPU9250 when the AD0 pin is connected to ground
#define MPU9250_WHOAMI  0x75            // Will return a set value based on device, in my case 0x73

// Write byte value to register at reg_address
void MPU9250_write(unsigned char reg_address, unsigned char value)
{
    I2C_start();                        /* Send start condition */  
    I2C_write(MPU9250_ADDRESS << 1, 1); /* Send MPU9250's address, read/write bit not set (AD + R) */  
    I2C_write(reg_address, 1);          /* Send the register address (RA) */  
    I2C_write(value, 1);                /* Send the value to set it to */  
    I2C_stop();                         /* Send stop condition */  
}

// Read a byte from register at reg_address and return in *value
void MPU9250_read(unsigned char reg_address, unsigned char *value)
{
    I2C_start();                        /* Send start condition */  
    I2C_write(MPU9250_ADDRESS << 1, 1); /* Send MPU9250's address, read/write bit not set (AD + R) */  
    I2C_write(reg_address, 1);          /* Send the register address (RA) */  
    I2C_restart();                      /* Send repeated start condition */  
    I2C_write(MPU9250_ADDRESS << 1 | 1, 1); /* Send MPU9250's address, read/write bit set (AD + W) */  
    I2C_read(value, 1);                 /* Read value from the I2C bus */  
    I2C_stop();                         /* Send stop condition */  
}

unsigned char main()
{
    unsigned char value;

    // Set performance to ultra rad
    set_performance_mode();

    // Moved all the ANSEL, TRIS and LAT settings to their own function
    setup_ports();        

    // Enable multi-vectored interrupts mode
    INTCONbits.MVEC = 1;

    // No need to set up PPS, I2C hardware is fixed to certain pins. SCL1 = RA14, SDA1 = RA15

    // Initialise I2C1 at 100kHz
    I2C_init(100000);

    while (1)
    {
        /* Read the value at register 0x75, the MPU9250's WHOAMI register. Should return 0x68, 0x71 or 0x73 depending on version. */  
        MPU9250_read(MPU9250_WHOAMI, &value);

        /* Wait 10ms before trying again so as not to overwhelm the MPU9250 or the PIC32MZ's I2C peripheral */  
        delay_ms(10);
    }
}

So I ran that, and here's what I got on my oscilloscope:

PIC32MZ - I2C - Full wave

Cyan is SDA and magenta is SCL. Just looking at that provides no real understanding of what's happening so I created another Photoshop monsterpiece for my dear nonexistent readers. Click on it to see it in its full glory:

PIC32MZ - I2C - Wave with captions

Yep, looks like the code. Phew. With that, another long-winded post is over. Good luck!

Here's the code

Tags: code, I2C

I2S and Reference Clocks on the PIC32MZ

What is I2S?

I2S is short for Inter-IC sound, and people call it various things like I-I-S, I-squared-S, I2S etc. but what's important is that it behaves a lot like SPI, which I covered a few posts back. To think of it very basically, it is SPI plus a few other clock signals and it's used for sending audio data to chips like Digital to Analog Converters (DACs). In today's example I will be using the CS4334 DAC, a 5V DAC that I have used for some time. I'm going to modify the existing MP3 player app to use this DAC instead of PWM.

What signals does the CS4334 need from the PIC32MZ?

Let's take a look at the CS4334's datasheet:

PIC32MZ - CS4334 Pinout

It needs SDATA, DEM/SCLK. LRCK and MCLK. Let's run over what those pins do:

  • SDATA is the actual data line, in the SPI examples before it would have been SDI/SDO.
  • DEM/SCLK I will use as SCLK, the SPI data clock, and not as DEM or de-emphasis clock.
  • MCLK is the master clock, which I will discuss below. I will set this to 256x the SCLK frequency.
  • LRCK controls whether the data being input into SDATA is for the left or right channel. It's low for left channel data and high for right channel data.

In the description for MCLK, we can see that the clock speeds needs to be 256x the sample rate of the audio. For a 44.1kHz signal, this means the MCLK clock frequency needs to be 256 x 44,100 = 11,289,600Hz.

BRM? HRM? The CS4334 has two modes and, thankfully, it auto-detects which mode we are using based on the frequency of MCLK. The modes are Base Rate Mode (BRM) and High Rate Mode (HRM). According to the datasheet, High Rate Mode allows for input frequencies up to 100kHz but we have no need for that for playing MP3s. I'm going to stick to using 44.1kHz and 48kHz MP3s and set my MCLK to 256x the MP3's frequency.

Before we look at how to set this all up, let's go over what each of those signals requires:

  • MCLK, as discussed above, needs to be 256 x 44,100Hz or 11,289,600Hz.
  • SCLK needs to send 44,100 16-bit words of data per channel each second.
  • LRCK needs to switch between high and low 2 x 44,100 = 88,200 times per second.

That SCLK calculation can be a bit tricky to understand. To ease our understanding of it, let's say that two 16-bit channels together gives us 32-bits of data for each "audio frame" and we need to send 44,100 audio frames per second.

LRCK only changes when we have sent some data, and it changes like this:

  • It starts out low, and then we send data for the left channel.
  • When the data is sent, it automatically goes high and then we send data for the right channel.
  • When the data is sent, it automatically goes low again, and this repeats until we tell it not to.

Generating all those different clock signals

As I stated before, the I2S peripheral works very similarly to how the SPI peripheral does. For the purposes of this post, one of the main differences is that the Slave Select (SS) pin can be used to automatically generate the LRCK signal for us, which is great. So that takes care of that clock signal at least, but what about that massive MCLK signal? You might think you can get away with using a timer but let's see what that'd entail:

  • If SYSCLK is 200Mhz then PBCLK3 is a max of 100MHz.
  • We need a 11.2896MHz clock signal at 44.1kHz, or even 12.288MHz at 48kHz
  • Timer period calculation gives us 100,000,000 / 11,289,600 = 8.85.

The first apparent problem is that the period needs to be a whole number, so we'll either have to round up or round down. Let's see what that gives us:

100,000,000 / 9 = 11.111MHz which we then need to divide by 256 to get our playback frequency, so 43,402Hz. Not great. Well then, how about dividing by 8?

100,000,000 / 8 = 12.5MHz / 256 = 48,828Hz. That's even worse.

Further, with a period as low as 8.85, the PIC32 is going to have to call an interrupt every 8 clock ticks and even if that's possible it's going to leave almost zero free time to run any other code. Thankfully, there's a bit of hardware that can help us and it's called the Reference Clock Output.

Using the Reference Clock Output (REFCLKO)

There are two things to know about using the Reference Clock Output as it pertains to today's post. The first is found in the datasheet under the SPIxCON register:

PIC32MZ - SPIxCON MCLKSEL selection

As that shows, we can only use Reference Clock 1 for SPI/I2S. This can also be found in the Clock Distribution section of the same datasheet:

PIC32MZ - Clock Distribution

The second is the formula for calculating the Reference Clock Output frequency. Of course, this isn't in the main datasheet but in the separate DS60001250. This shows us the formula:

PIC32MZ - Reference Clock Frequency Formula

That's perhaps not as easy to understand. Let's see how it works. We have two variables:

  • RODIV - Reference Clock Divider, which is a 15-bit number so can have a value from 0 to 32,767 and
  • ROTRIM - Reference Clock Trim, which is a 9-bit number that can have a value from 0 to 511.

Whereas normal clocks just have a Divider, the Reference Clocks have a Trim variable to help provide fractional clock speeds. This means we can get much closer to our desired frequency than we can with timers. FREFIN refers to the frequency of the clock source we are using for the Reference Clock. This is defined by the ROSEL bits in REFOxCON:

PIC32MZ - ROSEL bits

Phew. Lots of pictures today. Basically, when we're using the Reference Clock I'm going to be use PBCLK1 as the source and in all my example PBCLK1 is running at 100MHz.

To understand how setting the frequency works, let's look at how it works. For this example, we want an output frequency of 256 * 44,100Hz, or 11,289,600Hz:

  • FREFOUT = FREFIN / [2 * (RODIV + ROTRIM/512)] can be rearranged to give us RODIV + ROTRIM/512 = FREFIN / (2 * FREFOUT).
  • For us, that gives RODIV + ROTRIM/512 = 100,000,000 / (2 * 11,289,600), which is 4.4289.
  • I will set RODIV to 4 and ROTRIM to (0.4289 * 512), which is 219.5968, so I'll round that up to 220.

Let's plug 4 and 220 back into the formula and see what we get out:

FREFOUT = FREFIN / [2 * (RODIV + ROTRIM/512)] so FREFOUT = 100,000,000 / [2 * (4 + 220/512)] = 100,000,000 / 8.859375 = 11,287,477Hz. Let's divide that to see what sample rate this translates to for our MP3 playback:

11,287,477 / 256 = 44,091Hz. That's only 9Hz off what we want, that's great. The Reference Clock Outputs all have a bit of error called jitter so they're not perfect but they're more than good enough for what we want to do.

BEWARE Something that tricked me when I first started working with the Reference Clock was that my frequency was frequently (har har) wrong. It turned out that this was the culprit:

PIC32MZ - REFOxTRIM warning

ONLY THE UPPER 9 BITS OF REFOxTRIM ARE USED. This means that when we set the Trim value, we must shift the value left by 23, like such:

REFO1TRIM = Trim << 23;

There is no separate REFOxDIV register, so this problem is avoided by using the named bits, like such:

REFO1CONbits.RODIV = div;

OK, back to regularly scheduled programming.

There's only one clock signal that we have yet to set up, and that's the SPI Clock.

Setting up the SPI Clock (SPICLK) to use the reference clock

If you'll recall, we're wanting to send stereo 44,100Hz 16-bit data to the DAC via I2S. Our Reference Clock Output (REFCLKO) is now outputting a signal that is 256 * 44,100. How fast does our signal need to be?

We want to send 16 bits (one 16-bit sample per left and right channel) 44,100 * 2 times a second because of stereo. So we need to be running at 88,200 * 16 = 1,411,200Hz. Thankfully, we don't have to set up yet another Reference Clock Output we can just use the SPI peripheral's built-in divider called the SPI Baud Rate Generator, or SPIBRG.

Our REFCLKO output is 256 x the sample frequency of 44,100 and we want our SPI signal to be (16+16) * 2, or 64 times 44,100. So let's do some easy maths for once: 256 / 64 = 4. The SPIBRG formula was:

PIC32MZ - SPI BRG formula

As we're dividing by 4, we want the bottom half of that fraction to be 4 so we will need to set SPIBRG to 1.

Setting up SPI audio mode

In my example, the pins will be connected as follows:


PIC32MZ - I2S DAC Connections


Note: The communication with this DAC is one way, we just send it data and it processes it automatically. So we don't need to use SDI3, we can disable it.
Let's take a look at how to initialise I2S on the PIC32MZ. It's actually laid out in the datasheet DS60001106 for us like this:

PIC32MZ - Enable Audio Mode



Let's see how that looks in C:

void I2S_init(int frequency)
{    
    unsigned long int ref_freq; // The desired output frequency
    int div, trim;              // RODIV and ROTRIM
    float calc;             // Fractional values
    unsigned short dummy; // Used only to clear the data from SPI3BUF

    /* 1. If using interrupts, disable the SPI interrupts in the respective IECx register. */  
    IEC4bits.SPI3TXIE = 0;

    /* 2. Stop and reset the SPI module by clearing the ON bit (SPIxCON<15>). */  
    SPI3CONbits.ON = 0;
    /* 3. Reset the SPI audio configuration register, SPIxCON2. */  
    SPI3CON2 = 0;
    /* 4. Reset the baud rate register, SPIxBRG. */  
    SPI3BRG = 0;

    /* 5. Clear the receive buffer. */  
    dummy = SPI3BUF;

    /* 6. Clear the ENHBUF bit (SPIxCON<16>) if using Standard Buffer mode or set the bit if using Enhanced Buffer mode */  
    SPI3CONbits.ENHBUF = 1;

    /* 7. If using interrupts, perform these additional steps:
        a) Clear the SPIx interrupt flags/events in the respective IFSx register. */  
    IFS4bits.SPI3TXIF = 0;

    /*  b) Write the SPIx interrupt priority and subpriority bits in the respective IPCx register. */  
    IPC39bits.SPI3TXIP = 3;
    IPC39bits.SPI3TXIS = 1;    

    /*  c) Set the SPIx interrupt enable bits in the respective IECx register. */  
    IEC4bits.SPI3TXIE = 1;

    /* 8. Clear the SPIROV bit (SPIxSTAT<6>). */  
    SPI3STATbits.SPIROV = 0;

    /* 9. Write the desired settings to the SPIxCON2 register. The AUDMOD<1:0> bits (SPIxCON2<1:0>) must be set to ?00? for I2S mode and the AUDEN bit (SPIxCON2<7>) must be set to ?1? to enable the audio protocol. */  
    SPI3CON2bits.AUDEN = 1;     // Enable Audio mode
    SPI3CON2bits.AUDMONO = 0;   // Enable Stereo mode
    SPI3CON2bits.AUDMOD = 0;    // Type of audio mode is I2S
    SPI3CON2bits.IGNROV = 1;    // Ignore receive overflows
    SPI3CON2bits.IGNTUR = 1;    // Ignore transfer underruns

    /* 10. Set the SPIxBRG baud rate register */
    SPI3BRG = 1;                // Yields 64x the sample rate, for 16-bit stereo playback

    /* 11. Write the desired settings to the SPIxCON register:
        a) MSTEN (SPIxCON<5>) = 1. */  
    SPI3CONbits.MSTEN = 1;
    /* b) CKP (SPIxCON<6>) = 1. */  
    SPI3CONbits.CKP = 1;
    /* c) MODE<32,16> (SPIxCON<11:10>) = 0 for 16-bit audio channel data. */  
    SPI3CONbits.MODE16 = 0;
    SPI3CONbits.MODE32 = 0;

    /* Additional settings needed for using the reference clock */  
    SPI3CONbits.DISSDI = 1;     // Disable SDI pin  
    SPI3CONbits.FRMPOL = 0;     // Frame pulse is active-low  
    SPI3CONbits.CKE = 0;        // Serial output data changes on transition from idle clock state to active clock state
    SPI3CONbits.SMP = 1;        // Input data sampled at end of data output time
    SPI3CONbits.MCLKSEL = 1;    // PBCLK is used by the Baud Rate Generator

    /* Before turning on SPI3, let's set up the Reference Clock 1 (REFO1CLK) */  

    // Set up REFO1CLK to be 256 x MIXER_FREQ and then times 2 again due to the formula
    calc = frequency * 256 * 2;

    // Calculate the values for RODIV and ROTRIM
    calc = (SYS_FREQ / 2 / calc); // At 44100Hz, this gives us 4.4288

    // Store the integer part in div (at 44100Hz, 4)
    div = (int)calc;
    // Subtract the integer part (at 44100Hz, 0.4288)
    calc -= div;
    // Multiply it by 512 to get it as a fraction of 512
    calc *=  512;
    // Store the integer part in trim
    trim = (int)calc;

    REFO1CON = 0;               // Reset the Reference Clock 1 configuration
    REFO1CONbits.RODIV = div;   // Set divider
    REFO1CONbits.ROSEL = 1;     // Source is PBCLK1
    REFO1TRIM = trim << 23;     // Shift the bits 23 places to the left

    // Enable the output of Reference Clock 1
    REFO1CONbits.OE = 1;
    // Turn on Reference Clock 1
    REFO1CONbits.ON = 1;

    // Turn on the SPI3 peripheral
    SPI3CONbits.ON = 1;
}

Boy, that is a lot of code. Something to be copy and pasted if ever such a thing existed :)
OK, now we've got SPI3 set up in I2S mode and it's being clocked by Reference Clock 1. Next, we need to write the interrupt handler:

void __attribute__((vector(_SPI3_TX_VECTOR), interrupt(ipl3soft), nomips16)) SPI3TX_handler()
{    
    // Clear the SPI3 Transfer Interrupt Flag
    IFS4bits.SPI3TXIF = 0;

    SPI3BUF = playback_buffer[pb_readpos];

    // Increment the read position in the playback buffer
    pb_readpos++;

    // Check if the read position has exceeded the length of the playback buffer. If so, restart it at 0.
     if (pb_readpos >= PLAYBACK_BUFFER_SIZE) 
         pb_readpos = 0;
}

That's very similar to the interrupt handler from last time so there's nothing much to discuss.

Final setup

The final thing to do is set all the PPS settings needed by this program:

// PPS for I2S outputs
RPD15R = 0b1111; // D15 = REFCLKO1
RPB10R = 0b0111; // B10 = SDO3
RPB15R = 0b0111; // B15 = SS3

Another difference is that we don't need to convert the 16-bit audio to 11-bit anymore, so we can just do this directly:

playback_buffer[pb_writepos] = MP3_OUTPUT_BUFFER[count];

The post before this one shows the benefits of using a proper, even if fairly cheap, DAC versus using PWM. Good luck and as always post any questions in the comments.

Here's the code

Tags: code, I2S, audio, MP3, interrupts

The difference between PWM and an I2S DAC

Pulse Width Modulation (PWM) vs an Inter-IC Sound Digital to Analog Converter (I2S DAC)

I spent a long time today capturing images from my scope in preparation for a post about using I2S to output audio. I thought it'd be interesting to see the differences between audio playback using PWM with no RC filter, the two-order RC filter from yesterday and then a cheap I2S DAC (the CS4334). The results were interesting and I'll post something about them in my next post on getting I2S to work. Until then, enjoy this 80's-style monstrosity I whipped up in Photoshop!

PIC32MZ - PWM vs I2S DAC

Tags: code, PWM, I2S, audio