Initialising the USB peripheral

Initialising the USB peripheral and what registers actually matter

Updated 2020-12-04 with some stuff I wasn't sure of yesterday.

Welcome to another torturously long post. This one is only for people who want to get a very basic understanding of USB in general and how to initialise the USB peripheral and some endpoints.

OK so you want to use the PIC32MZ USB peripheral. Good call! It is high speed (not full speed or low speed like most STM32 chips that need an external high speed PHY connected to them) and actually does most of the work of getting USB to work for you. It's a genuinely good peripheral in my opinion. However, USB itself is a tricky thing to get into and understand. Today I want to cover the very, very basics of how to get the USB peripheral working and how to get your first packet from the computer.

How to set up the USB peripheral and put it into High Speed mode

First off, the good old USB naming conventions w.r.t. speed need to be cleared up:

  • Low Speed (LS) - 1.5 Megabits/s = 187.5kB/s max (awfully slow)
  • Full Speed (FS) - 12 Megabits/s = 1.5MB/s max (fine for HID devices, etc)
  • High Speed (HS) - 480 Megabits/s = 60MB/s max (what I want for my MSD [Mass Storage Device] application)
  • SuperSpeed (SS) - 5 Gigabits/s = 625MB/s max (this is USB 3, not available on any PIC device I know of)

The PIC32MZ series supports High Speed, whereas the older PIC32MX series only supported up to Full Speed. So naturally, as a PIC32MZ user I'm going to be focusing purely on High Speed operation. I am also going to be focusing on making a USB device for now (i.e. a device controlled by a host like a computer). At some point I will go into USB host mode (like reading from and writing to USB flash drives) but that's a story for another time.
If you just take any old PIC32MZ board and plug it into the computer via a USB cable, nothing will happen because the USB peripheral is off by default. To get it to start communicating with the computer we need to enable it, like this:

void USB_init(void *receive_callback)
{
    USB_RECEIVE_CALLBACK = receive_callback;

    USBCSR0bits.SOFTCONN = 0;   // Initially disable it while we go into setup

    USB_address = 0;            // We haven't been given an address yet    
    USBCSR0bits.FUNC = 0;       // Initially set the USB address to 0 until later when the host assigns us an address

    USBCSR2bits.RESETIE = 1;    // Enable the reset interrupt
    IEC4bits.USBIE = 1;         // Enable the USB interrupt
    USBCRCONbits.USBIE = 1;     // Enable USB module interrupt
    IFS4bits.USBIF = 0;         // Clear the USB interrupt flag.
    IPC33bits.USBIP = 7;        // USB Interrupt Priority 7
    IPC33bits.USBIS = 1;        // USB Interrupt Sub-Priority 1

    USBCSR0bits.HSEN = 1;       // Enable High Speed (480Mbps) USB mode
}

Line by line, as with all peripherals we turn the USB peripheral off while we configure it. This is controlled by the SOFTCONN bit field.
Secondly, initialise the USB address as 0. This I store both in an internal variable called USB_address and also in the hardware bit field USBCSR2.FUNC. Later on the computer will give us an address. Again, we are making a USB device, something that is controlled by a host. In this case, we can't assign ourselves an address. The host device, usually a computer, can have up to 127 USB devices attached to it and will handle assigning each of them an address by itself. More on this later.
Next, we set up the USB interrupt-related stuff. The USB peripheral communicates your our program via interrupts, and anything that happens will come into one giant interrupt handler. For now, I've set it to interrupt priority 7, sub-priority 1 but these numbers don't really matter. I've also enabled the USB interrupts in IEC4 and set the USB peripheral to generate interrupts as well as told it to generate an interrupt when a "USB reset" happens. More on this too, in a bit :)
Finally, turn on High Speed mode by setting USBCSR0bits.HSEN to 1. Setting it to 0 means we are going to be in low speed mode.

You may have notice that we didn't turn the USB peripheral on again. That's because after we've initialised it, we're supposed to wait until the VBUS pin is high, meaning we're connected to USB. If you're making a board, connect the VBUS pin on the PIC32MZ to the USB connector's VBUS pin (it's 5V tolerant so no worries there). In the project I uploaded yesterday, I didn't bother to check if we're connected, I assumed we were connected and powered by USB so if it didn't work that may be why.
OK, that's not actually too bad right? Pretty simple for a PIC32MZ peripheral really. Let's turn the USB peripheral on and get to the real fun.

void USB_connect()
{
    USBCSR0bits.SOFTCONN = 1;
}

From this moment on, the PIC32MZ and the computer will start communicating. From what I can see, the first thing the computer will do is send a "Reset" instruction to the device and it is at this point that you actually set up your device and get ready to roll. Inside your ISR (interrupt service routine), that'll look like this:

void __attribute__((vector(_USB_VECTOR), interrupt(ipl7srs), nomips16)) USB_handler()
{
    if(USBCSR2bits.RESETIF)
    {
        // So when this is set, the USB bus has been reset and we actually need to set up all the USB endpoints now

        USB_init_endpoints();

        USBCSR2bits.RESETIF = 0;      

    }
}

(Side note: PLEASE make sure you add SRS (not SOFT) after the ipl (interrrupt priority level) in your ISR declarations. AUTO works as well, usually, but set it to SRS to be sure. Setting it to SOFT means the PIC32MZ has to save the registers each time the ISR is called and it wastes tons of time. Do be aware you need to set the PRISS register for this to work, which I covered in an earlier post, but setting PRISS = 0x76543210 works in a pinch).
OK, so when a reset is called for, USBCSR2bits.RESETIF is set, so we catch it and then we actually set up our endpoints. But what are endpoints?

USB Endpoints

When getting into USB on the PIC32MZ, half of the battle was trying to understand what I was reading on websites. Terms like "endpoint" and "setup packet" were thrown around and it was pretty hard to understand. Perhaps I'm just not bright enough :) Let's look at the Microchip Developer site for an image

Endpoint Table

Long story short, endpoints are communications channels between your device and the host (computer or whatever that may be). An endpoint that sends data from the Host to the Device is called an OUT Endpoint. One that reads in data to the host from the device is called an IN Endpoint. This IN/OUT stuff is a bit confusing at first, but you always need to think of it from the point of view of the host, not from your PIC32MZ device. Furthermore, for High Speed and under, these endpoints are one way only (i.e. not full duplex but half duplex), though their direction can be controlled in software. So, for example, if you wanted to read a message from the host, you'd have to set your endpoint to be an OUT endpoint and then read the data. If you then wanted to send a reply, you'd have to set your endpoint to be an IN endpoint and then send the message.

There are 7 endpoints available on the PIC32MZ for us to use as we wish, numbered 1 to 7. Initially, when the device connects to the computer all communication will be done on Endpoint 0 because the computer doesn't yet known which endpoints we've chosen to use. Endpoint 0 is special and is used for what they call Control Transfers. It sends instructions from the host to the device and replies back from the device to the host.
So what happens when we plug our device in is that the host asks our device what type of device it is, what endpoints it uses and a whole bunch of other stuff. This is actually fairly involved and I won't cover it today. What I will end on today is the setup of our chosen endpoints.

Additionally, each endpoint has a mode it operates in. The valid modes are:

  • Control transfers
  • Interrupt transfers
  • Isochronous transfers
  • Bulk transfers

OK. Let's type some more! To put it fairly (perhaps too) simply:

  • Control transfers are used to send messages back and forth between the host and device about the setup/configuration/status of the device.
  • Interrupt transfers - Intended for small, infrequents bursts of data. In this mode the host is periodically checking to see if an interrupt has been raised so it guarantees delivery of data within a certain set time period.
  • Isochronous transfers - Used for time-dependent information like audio and video. It is periodic like an Interrupt transfer but it is also continuous. However, due to the need to guarantee of time and speed of transfer, it does not retry to send on error.
  • Bulk transfers - Used to transfer large blocks of non-time-dependent data in bursts. Guarantees delivery of data but not there are no time guarantees. Can use all currently available bandwidth to send data as fast as possible.

For a better explanation, please see this site.

Setting up our chosen endpoints

For the next few posts, I'm going to be covering how to write a program that'll give us a Communications Device Class (aka CDC) device. Looking at the spec for a CDC device, we see we need:

  • One interrupt IN endpoint for notifications to the USB Host
  • One bulk IN and one bulk OUT endpoint for data transfer

As we need two different kinds of endpoints, we need at least two endpoints. We could use three in total but I'm going to just use two. Why? Well, for a Virtual COM port it's highly unlikely we'll be looking at simultaneous two-way communication and I also want to cover how to change the endpoint modes from IN to OUT and back again :)

OK, so to carry on from the code earlier, a USB interrupt has been raised and USBCSR2bits.RESETIF was set, meaning we now need to actually do our endpoint setup. This mostly likely didn't need to be done earlier but I really struggled getting USB to work initially and it led to me being paranoid and putting it into the USB_init() function to. Anyway, onto the code:

void USB_init_endpoints()
{
    USBE1CSR0bits.MODE = 0;     // EP1 is OUT (OUT from host = in to device = RX)
    USBE2CSR0bits.MODE = 0;     // EP2 is OUT (for now, will be IN at times)

    // Clear all interrupt flags
    USBCSR0bits.EP0IF = 0;
    USBCSR0bits.EP1TXIF = 0;
    USBCSR0bits.EP2TXIF = 0;
    USBCSR1bits.EP1RXIF = 0;
    USBCSR1bits.EP2RXIF = 0;

    // Set the maximum transfer length for each endpoint
    // Configure endpoint 0 first.
    USBE0CSR0bits.TXMAXP = 16; // Set endpoint 0 buffer size to 16 bytes (can be a maximum of 64 for EP0)

    // And next my custom endpoints
    USBE1CSR0bits.TXMAXP = 16;   // Endpoint 1 - Maximum TX payload / transaction (can be a maximum of 64 for CDC "Interrupt IN endpoint"
    USBE2CSR0bits.TXMAXP = 512;   // Endpoint 2 - Maximum TX payload / transaction (512 is the maximum, set to 512)
    USBE2CSR1bits.RXMAXP = 512;   // Endpoint 2 - Maximum RX payload / transaction (512 is the maximum, set to 512)

    // Specify which kinds of endpoint we will be using
    USBE1CSR2bits.PROTOCOL = 3; // Endpoint 1 - Interrupt mode
    USBE2CSR2bits.PROTOCOL = 2; // Endpoint 2 TX - Bulk mode
    USBE2CSR3bits.PROTOCOL = 2; // Endpoint 2 RX - Bulk mode

    // Enable DISNYET
    USBE1CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies
    USBE2CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies

    // Set up buffer location for endpoint 1
    USBCSR3bits.ENDPOINT = 1;
    USBFIFOA = 0x08;
    USBIENCSR0bits.CLRDT = 1;

    // Set up buffer locations for endpoint 2
    USBCSR3bits.ENDPOINT = 2;
    USBFIFOA = 0x000A004A;
    USBIENCSR0bits.CLRDT = 1;
    USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
    USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

    // Set maximum size for each packet before splitting occurs
    USBOTGbits.RXFIFOSZ = 0b0110; // 0b0110 = 512 bytes
    USBOTGbits.TXFIFOSZ = 0b0110; // 0b0110 = 512 bytes

    // Set receive endpoint 2 to High Speed
    USBE2CSR3bits.SPEED = 1;

    // Disable Isochronous mode for endpoints 1 and 2
    USBE1CSR0bits.ISO = 0;      // Isochronous TX Endpoint Disable bit (Device mode).
    USBE2CSR0bits.ISO = 0;      // Isochronous TX Endpoint Disable bit (Device mode).

    // Set up endpoint interrupts
    // Initially clear all the interrupt enables (IE)
    USBCSR1 = 0;
    USBCSR2 = 0;

    USBCSR1bits.EP0IE = 1;      // Endpoint 0 interrupt enable
    USBCSR1bits.EP1TXIE = 1;    // Endpoint 1 TX interrupt enable
    USBCSR2bits.EP1RXIE = 1;    // Endpoint 1 RX interrupt enable
    USBCSR1bits.EP2TXIE = 1;    // Endpoint 2 TX interrupt enable
    USBCSR2bits.EP2RXIE = 1;    // Endpoint 2 RX interrupt enable
    USBCSR2bits.RESETIE = 1;
    USBCSR2bits.RESUMEIE = 1;
    USBCSR2bits.SOFIE = 1;

    // Set current endpoint to EP2
    USBCSR3bits.ENDPOINT = 2;    
}

OK that's incredibly involved. Let's take a brief overview of what we've done.

USBE1CSR0bits.MODE = 0;     // EP1 is OUT (OUT from host = in to device = RX)
USBE2CSR0bits.MODE = 0;     // EP2 is OUT (for now, will be IN at times)

Not much to see here. Just setting the current operation mode of the EPs to be OUT (receive from host). This is entirely unnecessary in this function, we only need to set them when we get an interrupt, but I'm covering here how to change the mode.

// Clear all interrupt flags
USBCSR0bits.EP0IF = 0;
USBCSR0bits.EP1TXIF = 0;
USBCSR0bits.EP2TXIF = 0;
USBCSR1bits.EP1RXIF = 0;
USBCSR1bits.EP2RXIF = 0;

Self-explanatory, clear all the flags.

// Set the maximum transfer length for each endpoint
// Configure endpoint 0 first.
USBE0CSR0bits.TXMAXP = 16; // Set endpoint 0 buffer size to 16 bytes (can be a maximum of 64 for EP0)

// And next my custom endpoints
USBE1CSR0bits.TXMAXP = 16;   // Endpoint 1 - Maximum TX payload / transaction (can be a maximum of 64 for CDC "Interrupt IN endpoint"
USBE2CSR0bits.TXMAXP = 512;   // Endpoint 2 - Maximum TX payload / transaction (512 is the maximum, set to 512)
USBE2CSR1bits.RXMAXP = 512;   // Endpoint 2 - Maximum RX payload / transaction (512 is the maximum, set to 512)

OK, here we are getting to something more interesting. This is how many bytes can be sent in a single transaction between the host and the device.

// Specify which kinds of endpoint we will be using
USBE1CSR2bits.PROTOCOL = 3; // Endpoint 1 - Interrupt mode
USBE2CSR2bits.PROTOCOL = 2; // Endpoint 2 TX - Bulk mode
USBE2CSR3bits.PROTOCOL = 2; // Endpoint 2 RX - Bulk mode

The protocol/transfer type of each endpoint needs to be set up. As discussed earlier, we need one interrupt, one bulk IN and one bulk OUT endpoint.

BE WARNED

For setting the transfer type of IN endpoints (from device to host), you change bits in USBEnCSR2 (where n = endpoint number, in my case 2), but for OUT endpoints (from host to device) you change bits in USBEnCSR3!
A further hilarious note is that in the datasheet it's called USBIEnCSR2 not USBEnCSR2. They point to the same register but XC.h seems to name them differently from the datasheet for whatever reason. I guess because they want you to use Harmony and that means you'll never need to know it? Who knows.

// Enable DISNYET
USBE1CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies
USBE2CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies

PIDERR? DISNYET? In USB host mode, this bit field is called PIDERR and in device mode it's called DISNYET (Disable NYET). It's the same field that fills two roles. The NYET (not Russian but short for No response YET) is a special kind of handshake that the device uses to tell the host that the device isn't ready for the next part of the transfer to happen. This can happen if the FIFO buffer is full or for any numbers of reasons. I now set it to 0 to enable the NYET packets.

// Set up buffer location for endpoint 1
USBCSR3bits.ENDPOINT = 1;
USBFIFOA = 0x08;
USBIENCSR0bits.CLRDT = 1;

// Set up buffer locations for endpoint 2
USBCSR3bits.ENDPOINT = 2;
USBFIFOA = 0x000A004A;
USBIENCSR0bits.CLRDT = 1;
USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

OK, we're getting to some of the real involved stuff now. Notice that we've set USBCSR3bits.ENDPOINT = 1;? That's because for some registers, like USBFIFOA, any value you put into them or read from them is valid only for the value of USBCSR3bits.ENDPOINT. So if you change ENDPOINT to 2, you'll get/set endpoint 2's information. Something to be wary of for sure!
Set the FIFO address to... 8? Not actually, no. The 8 there means 64. Due to the way the PIC32MZ's USB FIFO buffer works, the actual starting address is whatever you put in there times 8. So, in other words, start endpoint 1 TX FIFO buffer at address 64.

USBIENCSR0bits.CLRDT = 1;

According to the datasheet this "Resets the endpoint data toggle to 0". Upon further research, I've found that USB packets use DATA0 and DATA1 PIDs (Packet Identifiers) (and USB High Speed adds DATA2). In low speed and full speed transactions, when transmitting packets they toggle DATA0 and DATA1 as a form of simple error checking to know if the packet they're receiving is the one they're supposed to receive. For example, if a device receives two packets in a row with a PID of DATA0 it'll know something went wrong. DATA2 (and another one called MDATA) seems to have been added to High Speed for use with isochronous transfers and are thus beyond what I'll cover today. Thankfully this is all handled for us by the PIC32MZ's USB peripheral so we never have to worry about it.
Please see this site for more details.

USBFIFOA = 0x000A004A;

OK that looks different to endpoint 1. The first four digits are for the receive buffer's start address, which is 000A = 10 * 8 = 80. 80 because we started endpoint 1 at 64 and it's 16 bytes long, so 16 + 64 = 80. The next four digits are the transfer buffer start point. 4A, which is 74. 74 * 8 = 592 and 592 - 80 is 512, which is the length of the EP2 receive buffer. So start EP2's transfer buffer at position 592.

USBIENCSR0bits.CLRDT = 1;
USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

TXFIFOSZ and RXFIFOSZ come from the datasheet. They look like this:

TXFIFOSZ RXFIFOSZ

Very, very annoying how they only show some values while saving minimal space. If you work down, you'll see that 0x9 (0b1001) works out to 512 bytes maximum send and receive buffer size.

// Set maximum size for each packet before splitting occurs
USBOTGbits.RXFIFOSZ = 0b0110; // 0b0110 = 512 bytes
USBOTGbits.TXFIFOSZ = 0b0110; // 0b0110 = 512 bytes

Missing out these two lines caused my program to not work at all for a few days. If I sent more than, iirc, 8 characters to my code it'd crash the windows app and cause me much grief. Here we have a different table to work from, and 0110 means 512 bytes.

// Set up endpoint interrupts
// Initially clear all the interrupt enables (IE)
USBCSR1 = 0;
USBCSR2 = 0;

USBCSR1bits.EP0IE = 1;      // Endpoint 0 interrupt enable
USBCSR1bits.EP1TXIE = 1;    // Endpoint 1 TX interrupt enable
USBCSR2bits.EP1RXIE = 1;    // Endpoint 1 RX interrupt enable
USBCSR1bits.EP2TXIE = 1;    // Endpoint 2 TX interrupt enable
USBCSR2bits.EP2RXIE = 1;    // Endpoint 2 RX interrupt enable
USBCSR2bits.RESETIE = 1;
USBCSR2bits.RESUMEIE = 1;
USBCSR2bits.SOFIE = 1;

// Set current endpoint to EP2
USBCSR3bits.ENDPOINT = 2;    

Not much to explain here. Just setting interrupt enables so that when these interrupts occurs I get notified of them in my ISR.

Phew, what an incredibly long post. Next time, CDC.

Tags: code, tl;dr

USB

USB on the PIC32MZ

UPDATE: ERROR IN CODE FIXED

In the USB_device_description, change the line called /* bDeviceClass */ from 0x00 to 0x02 otherwise enumeration won't complete

Hello again all, he said to no one. Great news! I've finally gotten the USB port on the PIC32MZ working without Harmony. It's been a multi-year on-again-off-again project but this time I managed to get it done. Well, it works on all my development boards anyway.
What I've gotten to work so far is USB CDC (virtual COM port), which means I no longer have to use a USB-UART chip on my PIC32MZ development boards. From here I want to move on to getting HID working and then the ultimate goal of MSD. But for now, I'm uploading the CDC code and hopefully if anyone reads this they can see if it works on their boards :)

The documentation is severely lacking on USB-related stuff and to give credit where credit is due, here are all the sites I referenced when making my code:
* Microchip's own OTG datasheet
* Microchip's AN1166 on USB CDC
* The massively long USB.org CDC spec 1.2
* MARKDINGST.BLOGSPOT.COM that helped me understand the get/set line coding better
* WWW.XMOS.AI that helped me understand what the various endpoints were
* AIJU.DE that really helped explain some of the intricacies of the flags I needed to set
* WWW.BEYONDLOGIC.ORG for their simple explanataion of the USB setup packets
* WWW.SILABS.COM for a better explanation of USB-CDC

And then the big ones. Actual examples of USB-CDC for the PIC32MZ. I couldn't get either of these to work on my dev board but I used them as skeletons to base my own code off of (with a fair bit of copy and pasting!). Thanks to both of these people without whom I'd never have had any hope of getting this done:

What you will notice missing there is MPLAB Harmony, because it continues to be a bloated, slow, useless mess that is so cryptic that it is useless and provides no hints of how to do anything yourself. The Harmony approach of one library for all devices may be great for corporations who don't care about how anything works but it is atrocious for a hobbyist like me trying to understand how to use the PIC32MZ series.

Anyway, I will go into a brief explanation of USB and CDC in a future post but for now I just wanted to share the code.

Please note that while I have started splitting up the code into a few different files the work isn't done yet. I also have a lot more to learn about USB so some of the naming or things I'm doing may well be wrong :) Point is, it works on my board and I think having the code out there is better than not having it out there. As always, YMMV!

Here's the code. I hope it works on your boards and, if not, leave a comment and I'll try and help out.

Tags: code

A quick update!

An update on my site

Hi there everyone and no one. I haven't updated this site for quite some time (10 months or so). Certainly the COVID-19 pandemic has changed the way we do work and changed the world but apart from that I really just started this site to share the knowledge of PIC32MZ that I had gained through years of trial and error. I was sick of seeing useless "read the manual" and "work it out for yourself" posts by arrogant and selfish people on the microchip forums and annoyed by the tons of errors I had found in the datasheets. But now that I've written about all the stuff I wanted to share I really don't know what more to write about :)

I'm keeping the site up and I do hope it can provide a useful reference to the few people that need it. I'll also be uploading some of the schematics of stuff I've been working on in the next week or two.

Works and life have kept me busy and so I've been putting off replying to posts ("I'll get to that tomorrow") but I'll get to them today and hopefully stop being so lazy :)

Apologies if I miss out on replying to anyone's older questions the notification system here sucks!

Tags: rambling

Choosing which pins to use on a PIC32MZ project

How I choose which pins to use on a PIC32MZ project

I got an interesting question in my comments yesterday asking how I go about choosing which pins to use when I do a PIC32MZ project. My answer is a bit long and I think it might be useful to other noobs like me so I'm releasing it as a post today. Please note: This isn't based on anything but my own experience with PIC32MZ projects, it is certainly not the only way to to things. It's just the way I do it myself.

Basically, I go through the following steps:

  • List all peripherals I will need for the project
  • Check which hidden secrets the peripherals have
  • Check errata to make sure they all work
  • Check which pins each peripheral needs
  • Actually choose the pins

List all peripherals I will need for the project

In the example I was given, it was 2 x I2S (using an internally generated reference clock), 3 x UART, 2 x I2C and 2 x SPI on a 144-pin PIC32MZ EFG chip. Furthermore, while I2C and SPI peripherals can both be connected to multiple devices, in the interests of speed (and for the explanation) I'm going to connect each device to a separate SPI and I2C peripheral. The aim will be simple: to map these peripherals to GPIO pins in a way that makes layout easy.

Check which hidden secrets the peripherals have

All the peripherals have certain things that must be kept in mind when they are used. They're not really secrets at all but they may not be immediately obvious when you first set out to use them. For the example, the "hidden secrets" are as follows:

SECRET 1
I2C peripherals are fixed and cannot be moved. Furthermore, I2C SDA and SCL pins require a 4.7k pullup resistor in order for them to meet the I2C standard and work reliably.

SECRET 2
I2S either works off of PBCLK2 or off of REFCLKO1 because REFCLKO1 controls all SPI (which I2S is a subset of). It cannot just use any reference clock. For audio and the frequencies it uses, REFCLKO1 is much more useful. Furthermore, REFCLKO1 is set up by PPS, which further limits out options to the following:

PIC32MZ - REFOCLK1 pins

SECRET 3
Although most pins for SPI peripherals can be moved around at will, the clock pins (SCK1 ~ SCK6) cannot be moved. Luckily, if we control the Chip Select (CS) pin of the device ourselves, we can gain a bit more flexibility.

SECRET 4
As I detailed in a post in the past, in order to extract 50Mhz from the SPI port you can only use SPI2 or SPI3 and even then only on specific pins, as follows:

PIC32MZ - SPI speed trick

Because of this, I'm going to be putting the SD card on SPI2 (but you could put the LCD there for 50Mhz too if you wish).

Check errata to make sure they all work

Unlike the 32MZ EC series, which was a horrible mess full of bugs, the EF series has relatively few errata. There is, however, one that looks concerning. It's listed as "The I2C module does not function reliably under certain conditions."
It turns out that this problem occurs when we use I2C at a rate of more than 100kHz and have constant I2C transfers of more than 500 bytes. The solution from Microchip, as it has been since the PIC32MX days, is to "bit bang". Which means to implement I2C by yourself and not use their peripheral. That's pretty poor.
In practice I haven't encountered this yet but it is something to bear in mind.

Amusingly, the UART has the following errata: "The UART automatic baud rate feature is intended to set the baud rate during run-time based on external data input. However, this feature does not function." Great work, Microchip. Great work.

Check which pins each peripheral needs

Straightforward stuff here. I'll list the ones relevant to my example.

Each SPI needs:

  • MISO (SDI)
  • MOSI (SDO)
  • CS
  • CLK

Each I2C needs:

  • SDA
  • SCL

Each UART needs:

  • RX
  • TX

Each I2S needs:

  • MCLK (REFCLKO1)
  • MOSI (SDO)
  • SCK
  • LRCK (for which I use Slave Select (SS))

Actually choose the pins

OK, now the part that we actually wanted to do. I like to keep the peripherals separated if possible to make layout easier as I routinely use double-sided boards. For this step I look at two parts of the datasheet, namely the pinout of the device and the PPS Input and Output sections. Bearing that in mind, for this example I'd use something like this:

First, on the one "side" of the PIC32MZ EF 144-pin devices, we have the RH and RK ports, which are totally useless from a PPS perspective. There are also no I2C peripherals on that particular side. However, there is a potential output for REFCLKO1 and three SPI peripherals, so I've put the two I2S ports there like this:

SPI3 (I2S):

  • MOSI3 = RD14
  • SCK3 = RB14
  • SS3 = RF12 (used as LRCK)
  • REFOCLK1 = RD15

SPI5 (I2S):

  • MOSI5 = RB9
  • SCK5 = RF13
  • SS5 = RB8 (used as LRCK)
  • REFOCLK1 = RD15

As noted earlier, I need two SPI peripherals and one of them must be on SPI2, using RB3 and RB5. There is one I2C peripheral on this same side. I've laid it out like this:

SPI2 (50Mhz):

  • SCK2 = RG6
  • MISO2 = RB3
  • MOSI2 = RB5
  • SS2 = RG9 (Chip Select)

I2C4:

  • SDA4 = RG7
  • SCL4 = RG8

We still need one more I2C and one more SPI peripheral, which I've done as follows:

I2C2:

  • SDA2 = RA3
  • SCL2 = RA2

SPI4:

  • SCK4 = RD10
  • MISO4 = RD11
  • MOSI4 = RD0
  • SS4 = RH12 [See below]

I've chosen RH12 as SS4 (or Chip Select) but it cannot be used as SS4 in the PPS setting. I've done this because I plan to use this for the LCD, which needs me to manually control the Chip Select (CS) line and there are also no convenient PPS mappings for SS4 nearby.

OK, mostly done now. We now need 3 UARTs, which I've done as follows:

UART3:

  • U3TX = RD6
  • U3RX = RD7

UART4:

  • U4TX = RD4
  • U4RX = RD5

UART5:

  • U5TX = RF0
  • U5RX = RF1

In the end, it looks like this in Eagle (click to zoom):

PIC32MZ - Choosing pins PIC32MZ - Choosing pins

Hope this will be of use to someone :)

Tags: hardware, planning, circuits