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

Playing MP3s using PWM audio

Credits and disclaimers:

  • Naturally, I didn't write the Helix MP3 decoder. It can be found on the Helix website.
  • I made extensive use of the information provided by Microchip.
  • The Helix MP3 code is based heavily on code I found by user derkling on GitHub.
  • The song is Beat your competition by Vibe tracks, found in the YouTube Audio Library.

If any of the above turn out to be copyrighted, or the owner doesn't want them online anymore, please contact me and I'll change/remove them as necessary.

Update 2018-11-21: For some reason it doesn't like the way I'm converting 16-bit signed numbers to 11-bit unsigned. For now I've modified the type of playback_buffer to int because that fixes it. I've adjusted the playback buffer size to half of what it was to compensate for the extra RAM usage. It doesn't cause any exceptions so I'm not sure what's wrong. If anyone has any ideas please do let me know :)

Playing MP3s on the PIC32MZ

Ever since I started writing this site I've been wanting to write this post. In fact, almost every post leading up to this point had this in mind. For years I wanted to be able to play MP3s from my microcontroller and have seen countless solutions for Arduino and PIC but most of them use an external decoder like the VS1053b. I've also been aware of the PIC32 Helix MP3 decoder for some time but I never wanted to sit down and look at the source code because ain't nobody got time for that.

A few months ago I eventually had enough willpower to sit down and make everything work. To my surprise, I was able to get MP3s working after a few (stressful) days of tinkering. At this point, my modifications can even run on a PIC32MX170F or PIC32MX270F chip and they only run at 40MHz! The main requirements for the MP3 player are as follows:

  • 28000 bytes of RAM (heap) for the decoder
  • 1940 bytes of RAM for the encoded MP3 data buffer
  • 4608 bytes of RAM for the decoded MP3 data
  • 4608 bytes of RAM for the playback buffer

This totals 39,156 bytes of RAM needed, no problem for the PIC32MZ. Some of those sizes can be tweaked but those are the numbers I am using in my implementation. It should be noted that compiler optimisation is necessary. You need to change to O1 at least, possibly even O3 depending on your device. I found that the PIC32MX series needed O3 to work.

<TL;DR>
Compiler optimisation? If you go to the Production menu -> Set Project Configuration -> xc32-gcc -> Option categories -> Customise you can see five options for Optimisation level:

  • 0 - This is the default. It's also the only option where debugging works nicely. However, it generates horribly inefficient code that almost seems intentionally bad.
  • 1 - This is the level you should use if you have any timing specific code.
  • 2 - Levels 0 and 1 are free but levels 2 and up require a license. Level 2 is better than level 1 but I don't use it for my little projects.
  • 3 - The best optimisation level. This is required to use the MP3 decoder on the 40Mhz PIC32MX parts.
  • s - This level optimises for smaller code while also trying to be as efficient as it can.

For my purposes, levels 0 and 1 are fine. I'm not writing code for devices I'm just making projects in my spare time :)

OK so then what is heap size? Heap is a term that means memory is allocated dynamically (so while the program is running) and not in the code (such as an array). The MP3 decoder uses malloc() to allocate memory for itself and thus cannot work without a heap. This has to be manually set up in your project. Go to Production menu -> Set Project Configuration -> xc32-ld -> Heap size (bytes) and type in 28000. That should be enough but if you run into random errors with some songs maybe try increasing it.
</TL;DR>

When you open the attached ZIP file, you will see two folders:
* PIC32MZ_11.X - The project itself, with all of its many files.
* SD card - This folder contains beat.mp3, which must be copied to the root directory of your SD card in order for this program to work.

The flow of the program

PIC32MZ - MP3 player flow chart

Let's take a brief look at those individually.

Configuration and initialisation of peripherals

There is one new thing here, related to SD card setup:

// Enabling internal pull-up bits to counter potential problems
CNPUBbits.CNPUB3 = 1;
CNPUBbits.CNPUB5 = 1;

I've enabled the internal, weak pull-ups on SDI2 and SDO2 to counter any potential problems others may have. I usually put 10k pull-up resistors on my physical dev boards to counter these issues and that's why I didn't get why my code was working last time. Hope this helps!

Apart from that, there's really nothing out of the ordinary here. Please bear in mind that my code uses the following settings:

  • SD card is set up to use SPI2, with CS on RA0, MISO (SDI2) on RB3, MOSI (SDO2) on RB5 and Clock (SCLK2) on RG6 (SCLK2 is hard-wired and cannot be changed).
  • PWM outputs are set up as follows: OC2 is connected to RC2 and OC6 is connected to RC1.

If you need to adapt this code, please bear all of those in mind. SD card changes can be made in mmcpic32.c as discussed before, but changes to the PWM outputs need to be made both in the PPS setup section in main(), that is:

// PPS for PWM outputs
RPC1R = 0b1100;         // RC1 = OC6
RPC2R = 0b1011;         // RC2 = OC2

As well as in the PWM_init() function:

OC2CON = 0;             // Turn off Output Compare module 6
OC2CONbits.OCTSEL = 1;  // Interrupt source for this module is Timer 3
OC2CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
OC2RS = 0;              // Keep the signal low for the entire duration

OC6CON = 0;             // Turn off Output Compare module 6
OC6CONbits.OCTSEL = 1;  // Interrupt source for this module is Timer 3
OC6CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
OC6RS = 0;              // Keep the signal low for the entire duration
OC2CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)
OC6CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)    

Opening the file and finding the so-called MP3 Sync Word

Nothing out of the ordinary here either. The program attempts to open a file named "beat.mp3" in the root directory of the SD card. On success, it'll attempt to intialise the MP3 decoder like this:

// Initialise the MP3 decoder
mp3Decoder = MP3InitDecoder();

if(mp3Decoder == 0)
{
}

If the MP3 decoder could not be initialised, mp3Decoder will be equal to 0 (null) and the program will not work. If this happens, the first thing you should attempt is to increase the heap size as I mentioned earlier in the post.

From there, chunks are read from the file until the MP3 Sync Word is found. This is a special marker that marks the start of a valid frame. After that, it attempts to read the data from the frame. If the frame is valid, the first phase of MP3 reading is complete and we now have the song's frequency, number of channels and bit depth.

PLEASE NOTE: This example program will only work with 16-bit stereo MP3 files. It should be fairly easy to modify it to work with mono or other bit depths.

Streaming the remaining frames from SD card and playing them back via two PWM channels

From here, the program simply reads data from the MP3 file on the SD card into a buffer and attempts to decode the MP3 frames as they are read. It outputs the data to a playback buffer that the Timer 3 interrupt will use to output the audio data.

My program uses something called a "circular buffer" for the playback data. All this is, is an array that works as follows:

  • The interrupt routine keeps track of the position it is reading from in pb_readpos.
  • The main streaming loop keeps track of where it is writing in pb_writepos.
  • Writing to the array is very fast on the PIC32MZ, much faster than the comparatively slow Timer 3 interrupt that operates at 1/44100s or every 22 microseconds.
  • Writing to array continues until the reading position = the writing position, at which point the writing waits until the reading position has moved on and then writes some more.
  • Once the writing part runs out of data to write, the program decodes another MP3 frame and starts writing again. This continues until the entire MP3 file has been played.

I prefer this system to other ones like double-buffering because there's only one array. It also writes data as soon as it can, pretty much ensuring there will be no stutters or hitches in the playback. You can easily do other things in between as the playback buffer is fairly big and can easily be made even bigger. The size is defined at the top of the code:

#define PLAYBACK_BUFFER_SIZE 2304 * 4

This provides 9,216 words of data which is four times the MP3 decoder's output buffer size. At 22 microseconds per word, is about 203 milliseconds. If your program is experiencing underrun errors and the audio is stuttering, increase this value. If you need more RAM, decrease it.

Clean-up

Once all the frames are played (or an error occurs), the very first thing the program does it call PWM_stop(). If you don't do this, the program will keep playing the last 203 milliseconds of data over and over again and nobody wants that.
After that, it closes the MP3 file handle with f_close() and frees the memory used by the MP3 decoder.

That's it for general code overview, let's take a look at some more in-depth things. This entire next section can be considered <TL;DR> :)

Things to be aware of when playing MP3s back via PWM

Converting signed 16-bit numbers to unsigned 11-bit

Most MP3s I've seen are encoded at 44.1kHz or 48kHz signed 16-bit stereo. Let's think about what that means in terms of playback.

The frequency is not a problem, we already did 44.1kHz last time. The real problem is the 16-bit signed audio. Let's take a look at our timer calculation again:

PR3 = SYS_FREQ / 2 / frequency;  

Plugging in some numbers for 44.1kHz, we can see that we get:

PR3 = 200,000,000 / 2 / 44,100 = 2,267

OK, but so what? Well, 16-bit numbers can hold up to 2^16 - 1, or 65,535. Further complicating matters is that this audio is signed, meaning is uses the first bit as a +/- sign and the rest of the 15 bits as data.
However, the most resolution we can get at 44.1kHz is 11-bit (as 2^11 - 1 = 2,047). Thankfully, this still sounds pretty good. So our first order of business will be to convert that 16-bit sign data into the 11-bit unsigned data our PWM channels expect. We can do it as follows:

output_data = ((input_data + 32,768) >> 5)

<TL;DR>
Signed 16-bit audio data is represented as a wave with audio centered around 0. Unsigned 16-bit audio data is represented as a wave centered around 32,768. So effectively, all we need to do is shift the entire waveform up by 32,768 positions to shift the center-point. That's why we add 32,768.

Because this is the tl;dr section, let's refresh how signed binary numbers work again. Let's take 3,000 and -3,000 as examples and see how they look in binary.
3,000 = 0000 1011 1011 1000b -3,000 = 1111 0100 0100 1000b

Hmmmm... what? Negative binary numbers are generally represented using something called Two's complement, which is itself a modification of One's complement. It works as follows:

  • Write down the number in binary
  • Toggle all the bits, changing 0's to 1's and 1's to 0's
  • Add 1 to the answer

So for our example, we have 3,000:

  • 3,000 = 0000 1011 1011 1000b
  • Toggle : 1111 0100 0100 0111b
  • Add 1 : 1111 0100 0100 1000b

Which was our answer to start with. For more information, Google Two's complement, it really isn't the focus of today's post.

Finally, we shift right by 6 bits because we want to discard the lower 6 bits and only keep the more important upper 10 bits.
</TL;DR>

Stereo audio from PWM

Thankfully, this is an easy one. You just need to set up two Output Compare modules. In my example, I've used OC2 and OC6. If you remember the first post on PWM, there is one thing you need to be aware of:

PIC32MZ - Output Compare Timer Select

If, for whatever reason, you have set the OCACLK bit in CFGCON to 1, the Output Compare modules use different timers for their operation. As I don't do that, I don't have such an issue.

OK, good luck with that. If anybody reads this and tries it, I'll be happy to help in the comments. I realise the number of PIC32MZ hobbyists like me out there probably isn't very high but I hope it helps.

Here's the code

Tags: code, PWM, audio, MP3