Shared pins on the PIC32MZ's ADC

Cautionary note about shared analog inputs when choosing which analog inputs to use

Last time I briefly went over how to use the alternative analog inputs on ADCs 0 to 4. It went like this:

  • set SH0ALT to 0 / 1 to use AN0 / AN45 on ADC0
  • set SH1ALT to 0 / 1 to use AN1 / AN46 on ADC1,
  • set SH2ALT to 0 / 1 to use AN2 / AN47 on ADC2,
  • set SH3ALT to 0 / 1 to use AN3 / AN48 on ADC3 or
  • set SH4ALT to 0 / 1 to use AN4 / AN49 on ADC4 respectively.

At the time I didn't realise the full implications of that. I was working on a project yesterday and saw this:

PIC32MZ ADC MPLAB X Code Complete!

I was trying to use AN45, but there didn't seem to be a flag to check to see if the ADC conversion had completed, so I went to check the datasheet. Sure enough, I found this:

PIC32MZ ADC ADCDATA!

What I was expecting was ADCDATA45, ADCDATA46, ADCDATA47, ADCDATA48 and ADCDATA49, but it ended at ADCDATA44. Then suddenly I remember the pins could be used as alternative inputs and went back to look at the ADC block diagram again:

PIC32MZ ADC - Alternative Inputs Block Diagram!

That confirmed it for me. You can use AN45 as an input OR you can use AN0 as an input, and they will both work on ADC0. However, you can not use them at the same time!

TECHNICALLY
Yes, I'm sure you could keeping setting and clearing SH0ALT to use them both but that is not what I'm referring to. I'm referring to a situation where you look at the datasheet and think you can attach signal A to AN0 and signal B to AN45 and read them in at the same time.
/TECHNICALLY

So how to use AN45 then? Well, you set SH0ALT to 1 and then write the code as if you are actually using AN0. That is, you still use all the ADC0 bit settings and flags (like ADCDATA0, ARDY0, TRGSRC0, etc.) as if you were using AN0 but the PIC32MZ will know that you are actually using AN45 instead of AN0 because you've set SH0ALT to 1.

TL;DR ADC inputs AN45, AN46, AN47, AN48 and AN49 are shared with ADC inputs AN0, AN1, AN2, AN3 and AN4 respectively. You can use, for example, either AN0 or AN45 but you can not use both at the same time for different signals.

And yes, it seems obvious now but I think this is a potentially very dangerous design trap that you might fall into if you don't know about it so I'm uploading it in the hopes that someone who needs this will find it.

Tags: code, ADC

ADC on the PIC32MZ

So you want to use the ADC

If you've come from using the PIC32MX series, you'll remember that using the ADC was a simple affair. You could set it up in a few lines and then BAM it worked. And wow, the specs of the PIC32MX were merely:

  • Fast and Accurate 16 channel 10-bit ADC
  • Max 1 Mega sample per second at +/- 1LSB, conversion available during SLEEP & IDLE

But now with the MZ, take a look at this:

  • 12-bit ADC Module
  • 18 Msps rate with six Sample and Hold (S&H) circuits (five dedicated and one shared)
  • Up to 48 analog inputs
  • Multiple trigger sources
  • Temperature sensor with ±2°C accuracy

Whooooooooooaaaaaa! 18 times the speed, 3 times the channels and even a temperature sensor. That is so cool! So you undoubtedly thought that, like the PIC32MX. the PIC32MZ would be simple too, maybe even use the same code. But, like everything else with the MZ, you're met with the cold, hard reality that

Microchip
hates
you

and doesn't want you to use one of their fastest PIC chips. You see, apart from the minor issue of the temperature sensor not working, at all, on any device, there's the matter of the insane way they've gone about implementing these 48 channels. To illustrate, here's what the map of the PIC32MX ADC hardware looks like:

PIC32MX - ADC Block Diagram

OK, so it's your typical Microchip block diagram. A bit confusing in places (and what's happening with AN1 there?) but it's not too bad. Now let's take a look at the new map for the PIC32MZ:

PIC32MZ ADC block diagram!

Ha. Ha. Ha. What a cruel joke. And this is the point where you run back to the safety of Harmony, only to realise multiple releases of Harmony also have bugs with the ADC code. Not even Microchip can get it working. But it's not just that it looks complicated, it actually is more complicated to use. But not impossible. In theory, this gives us more flexibility but the trade-off is that what used to take 3 lines to set up now takes a good deal more.

Secret Knowledge 1 - The ADC inputs are divided into classes

What you absolutely cannot tell from looking at that diagram is the on the PIC32MZ the ADC inputs are divided into the following classes:

  • Class 1 - These inputs each have their own dedicated sample and holding hardware and can be triggered individually, are always sampling and will be converted as soon as you trigger them, independent of each other.
  • Class 2 - These share ONE sample and hold unit, specificially ADC7, and can also be triggered individually but will only start sampling once you trigger them and even then, they have to take turns using ADC7 to do so.
  • Class 3 - These also share ADC7

Furthermore, the inputs are divided as follows (for the PIC32MZ EF series):

  • Class 1 = AN0 to AN4
  • Class 2 = AN5 to AN31
  • Class 3 = AN32 to AN44

Furthermore, the 18Msps speed is a bit fat bit of marketing speak as shown here:

PIC32MZ real ADC speeds

Even in their own datasheet they can't get 18msps, the closest they get is 16.67msps and that's only is you trigger 4 class 1 inputs at once, and even then only if they're in 6-bit mode. Ugh. Marketing sucks. But it's not all bad news!

How to use class 1 inputs on the PIC32MZ ADC

Today I'm going to just cover how to use class 1 inputs. Shockingly, the example in the ADC datasheet (DS60001344B) actually mostly works (apart from some key things that need to be set up first). Well, the class 1 one does. The class 2 one has typos in some of the register names. Because of course it does :)

In said example, AN0, AN1 and AN2 are set up and read into an array of integers called result[]. Let's take a look at said code from the datasheet:

int main(int argc, char** argv) 
{
    int result[3];
    /* initialize ADC calibration setting */
    ADC0CFG = DEVADC0;
    ADC1CFG = DEVADC1;
    ADC2CFG = DEVADC2;
    ADC3CFG = DEVADC3;
    ADC4CFG = DEVADC4;
    ADC7CFG = DEVADC7;
    /* Configure ADCCON1 */
    ADCCON1 = 0; // No ADCCON1 features are enabled including: Stop-in-Idle, turbo,
    // CVD mode, Fractional mode and scan trigger source.
    /* Configure ADCCON2 */
    ADCCON2 = 0; // Since, we are using only the Class 1 inputs, no setting is
    // required for ADCDIV
    /* Initialize warm up time register */
    ADCANCON = 0;
    ADCANCONbits.WKUPCLKCNT = 5; // Wakeup exponent = 32 * TADx
    /* Clock setting */
    ADCCON3 = 0;
    ADCCON3bits.ADCSEL = 0; // Select input clock source
    ADCCON3bits.CONCLKDIV = 1; // Control clock frequency is half of input clock
    ADCCON3bits.VREFSEL = 0; // Select AVDD and AVSS as reference source
    /* Select ADC sample time and conversion clock */
    ADC0TIMEbits.ADCDIV = 1; // ADC0 clock frequency is half of control clock = TAD0
    ADC0TIMEbits.SAMC = 5; // ADC0 sampling time = 5 * TAD0
    ADC0TIMEbits.SELRES = 3; // ADC0 resolution is 12 bits
    ADC1TIMEbits.ADCDIV = 1; // ADC1 clock frequency is half of control clock = TAD1
    ADC1TIMEbits.SAMC = 5; // ADC1 sampling time = 5 * TAD1
    ADC1TIMEbits.SELRES = 3; // ADC1 resolution is 12 bits
    ADC2TIMEbits.ADCDIV = 1; // ADC2 clock frequency is half of control clock = TAD2
    ADC2TIMEbits.SAMC = 5; // ADC2 sampling time = 5 * TAD2
    ADC2TIMEbits.SELRES = 3; // ADC2 resolution is 12 bits
    /* Select analog input for ADC modules, no presync trigger, not sync sampling */
    ADCTRGMODEbits.SH0ALT = 0; // ADC0 = AN0
    ADCTRGMODEbits.SH1ALT = 0; // ADC1 = AN1
    ADCTRGMODEbits.SH2ALT = 0; // ADC2 = AN2
    /* Select ADC input mode */
    ADCIMCON1bits.SIGN0 = 0; // unsigned data format
    ADCIMCON1bits.DIFF0 = 0; // Single ended mode
    ADCIMCON1bits.SIGN1 = 0; // unsigned data format
    ADCIMCON1bits.DIFF1 = 0; // Single ended mode
    ADCIMCON1bits.SIGN2 = 0; // unsigned data format
    ADCIMCON1bits.DIFF2 = 0; // Single ended mode
    /* Configure ADCGIRQENx */
    ADCGIRQEN1 = 0; // No interrupts are used
    ADCGIRQEN2 = 0;
    /* Configure ADCCSSx */
    ADCCSS1 = 0; // No scanning is used
    ADCCSS2 = 0;
    /* Configure ADCCMPCONx */
    ADCCMPCON1 = 0; // No digital comparators are used. Setting the ADCCMPCONx
    ADCCMPCON2 = 0; // register to '0' ensures that the comparator is disabled.
    ADCCMPCON3 = 0; // Other registers are “don't care”.
    ADCCMPCON4 = 0;
    ADCCMPCON5 = 0;
    ADCCMPCON6 = 0;
    /* Configure ADCFLTRx */
    ADCFLTR1 = 0; // No oversampling filters are used.
    ADCFLTR2 = 0;
    ADCFLTR3 = 0;
    ADCFLTR4 = 0;
    ADCFLTR5 = 0;
    ADCFLTR6 = 0;
    /* Set up the trigger sources */
    ADCTRGSNSbits.LVL0 = 0; // Edge trigger
    ADCTRGSNSbits.LVL1 = 0; // Edge trigger
    ADCTRGSNSbits.LVL2 = 0; // Edge trigger
    ADCTRG1bits.TRGSRC0 = 1; // Set AN0 to trigger from software.
    ADCTRG1bits.TRGSRC1 = 1; // Set AN1 to trigger from software.
    ADCTRG1bits.TRGSRC2 = 1; // Set AN2 to trigger from software.
    /* Early interrupt */
    ADCEIEN1 = 0; // No early interrupt
    ADCEIEN2 = 0;
    /* Turn the ADC on */
    ADCCON1bits.ON = 1;
    /* Wait for voltage reference to be stable */
    while(!ADCCON2bits.BGVRRDY); // Wait until the reference voltage is ready
    while(ADCCON2bits.REFFLT); // Wait if there is a fault with the reference voltage
    /* Enable clock to analog circuit */
    ADCANCONbits.ANEN0 = 1; // Enable the clock to analog bias
    ADCANCONbits.ANEN1 = 1; // Enable the clock to analog bias
    ADCANCONbits.ANEN2 = 1; // Enable the clock to analog bias
    /* Wait for ADC to be ready */
    while(!ADCANCONbits.WKRDY0); // Wait until ADC0 is ready
    while(!ADCANCONbits.WKRDY1); // Wait until ADC1 is ready
    while(!ADCANCONbits.WKRDY2); // Wait until ADC2 is ready
    /* Enable the ADC module */
    ADCCON3bits.DIGEN0 = 1; // Enable ADC0
    ADCCON3bits.DIGEN1 = 1; // Enable ADC1
    ADCCON3bits.DIGEN2 = 1; // Enable ADC2
    while (1) 
    {
        /* Trigger a conversion */
        ADCCON3bits.GSWTRG = 1;
        /* Wait the conversions to complete */
        while (ADCDSTAT1bits.ARDY0 == 0);
        /* fetch the result */
        result[0] = ADCDATA0;
        while (ADCDSTAT1bits.ARDY1 == 0);
        /* fetch the result */
        result[1] = ADCDATA1;
        while (ADCDSTAT1bits.ARDY2 == 0);
        /* fetch the result */
        result[2] = ADCDATA2;
        /*
        * Process results here
        *
        * Note: Loop time determines the sampling time since all inputs are Class 1.
        * If the loop time is small and the next trigger happens before the completion
        * of set sample time, the conversion will happen only after the sample time
        * has elapsed.
        *
        */
    }
    return (1);
}

OK...
...

70 lines of code to set up ADC0, ADC1 and ADC2. These are some big trade-offs but fine, fine, we'll take a look at it piece by piece. Important to note is that for some of this code the order of execution is very important so I recommend not changing it unless you know what you are doing. Also, on the EF devices there is no such thing as ADC5 and ADC6 so I've removed references to them from my code. With that, let's start:

/* initialize ADC calibration setting */
ADC0CFG = DEVADC0;
ADC1CFG = DEVADC1;
ADC2CFG = DEVADC2;
ADC3CFG = DEVADC3;
ADC4CFG = DEVADC4;
ADC7CFG = DEVADC7;

This sets up the 6 ADC units, ADC0 through ADC4 and ADC7, with their default values. Why you wouldn't do this automatically I don't know because on inspection of the datasheet ADCxCFG looks like this:

PIC32MZ ADC default config registers

So while yes, you can change these there seems to be no reason to. Just classic Microchip design crashing into reality again.

/* Configure ADCCON1 */
ADCCON1 = 0; // No ADCCON1 features are enabled including: Stop-in-Idle, turbo,
// CVD mode, Fractional mode and scan trigger source.
/* Configure ADCCON2 */
ADCCON2 = 0; // Since, we are using only the Class 1 inputs, no setting is
// required for ADCDIV

Something very important to note in the set up of ADCCON1 and ADCCON2. We are only using class 1 inputs so no clock division stuff needs to be set upbut if you are using class 2 or 3 you will have to set up these as they control various aspects of ADC7.

/* Initialize warm up time register */
ADCANCON = 0;
ADCANCONbits.WKUPCLKCNT = 5; // Wakeup exponent = 32 * TADx
/* Clock setting */
ADCCON3 = 0;
ADCCON3bits.ADCSEL = 0; // Select input clock source
ADCCON3bits.CONCLKDIV = 1; // Control clock frequency is half of input clock
ADCCON3bits.VREFSEL = 0; // Select AVDD and AVSS as reference source

ADCANCON controls how long the ADC modules need to "warm up" before they can be used for conversion. Thankfully, this number is common to all ADCs on the PIC32MZ devices and is always set to 5 (which = 32TADx). As we will see later, it also contains the "enable" bits for ADCs 0 through 7 and status bits for whether they are ready or not.
For us and our class 1 example, ADCCON3 controls what to use as a voltage reference, what the clock frequency is and whether the digital outputs for each ADC module, again 0 through 7, are enabled or not.

/* Select ADC sample time and conversion clock */
ADC0TIMEbits.ADCDIV = 1; // ADC0 clock frequency is half of control clock = TAD0
ADC0TIMEbits.SAMC = 5; // ADC0 sampling time = 5 * TAD0
ADC0TIMEbits.SELRES = 3; // ADC0 resolution is 12 bits
ADC1TIMEbits.ADCDIV = 1; // ADC1 clock frequency is half of control clock = TAD1
ADC1TIMEbits.SAMC = 5; // ADC1 sampling time = 5 * TAD1
ADC1TIMEbits.SELRES = 3; // ADC1 resolution is 12 bits
ADC2TIMEbits.ADCDIV = 1; // ADC2 clock frequency is half of control clock = TAD2
ADC2TIMEbits.SAMC = 5; // ADC2 sampling time = 5 * TAD2
ADC2TIMEbits.SELRES = 3; // ADC2 resolution is 12 bits

As we're using AN0, AN1 and AN2, we need to set up timing information and resolution information for ADCs 0 through 2. Nothing complicated here.

/* Select analog input for ADC modules, no presync trigger, not sync sampling */
ADCTRGMODEbits.SH0ALT = 0; // ADC0 = AN0
ADCTRGMODEbits.SH1ALT = 0; // ADC1 = AN1
ADCTRGMODEbits.SH2ALT = 0; // ADC2 = AN2

Hmmm... what's this? There is some limited flexibility with ADCs 0 through 4. While, by default, AN0 is set up to be connected to ADC0, you can change SH0ALT to 1 to make ADC0 use pin AN45 instead. Similarly, you can:

  • set SH1ALT to 0 / 1 to use AN1 / AN46,
  • set SH2ALT to 0 / 1 to use AN2 / AN47,
  • set SH3ALT to 0 / 1 to use AN3 / AN48 or
  • set SH4ALT to 0 / 1 to use AN4 / AN49 respectively.
/* Select ADC input mode */
ADCIMCON1bits.SIGN0 = 0; // unsigned data format
ADCIMCON1bits.DIFF0 = 0; // Single ended mode
ADCIMCON1bits.SIGN1 = 0; // unsigned data format
ADCIMCON1bits.DIFF1 = 0; // Single ended mode
ADCIMCON1bits.SIGN2 = 0; // unsigned data format
ADCIMCON1bits.DIFF2 = 0; // Single ended mode

The FRACT and SIGN bits together make up the format in which the ADC will return data to you, according to this table:

PIC32MZ ADC result format

For our purpose, we just want a 12-bit integer, so we want them both set to 0 (FRACT is contained within ADCCON1 which we previously set to 0).
DIFF, as the name suggests, allows us to select between single-ended or differential inputs, as follows:

PIC32MZ ADC single-ended or differential mode

For this example, we're just wanting to stick a potentiometer on AN0, AN1 and AN2 and measure between 3.3V and 0V so I'll be using single-ended inputs only.

/* Configure ADCGIRQENx */
ADCGIRQEN1 = 0; // No interrupts are used
ADCGIRQEN2 = 0;
/* Configure ADCCSSx */
ADCCSS1 = 0; // No scanning is used
ADCCSS2 = 0;

We are not using interrupts and we are not using scanning. That is, we will be manually triggering the ADCs every time we want to read values from them.

/* Configure ADCCMPCONx */
ADCCMPCON1 = 0; // No digital comparators are used. Setting the ADCCMPCONx
ADCCMPCON2 = 0; // register to '0' ensures that the comparator is disabled.
ADCCMPCON3 = 0; // Other registers are “don't care”.
ADCCMPCON4 = 0;
ADCCMPCON5 = 0;
ADCCMPCON6 = 0;
/* Configure ADCFLTRx */
ADCFLTR1 = 0; // No oversampling filters are used.
ADCFLTR2 = 0;
ADCFLTR3 = 0;
ADCFLTR4 = 0;
ADCFLTR5 = 0;
ADCFLTR6 = 0;

We are not wanting any of the fancy digital comparator functionality and we are not wanting to use any of the oversampling filters. Just let us use the ADC already!

/* Set up the trigger sources */
ADCTRGSNSbits.LVL0 = 0; // Edge trigger
ADCTRGSNSbits.LVL1 = 0; // Edge trigger
ADCTRGSNSbits.LVL2 = 0; // Edge trigger
ADCTRG1bits.TRGSRC0 = 1; // Set AN0 to trigger from software.
ADCTRG1bits.TRGSRC1 = 1; // Set AN1 to trigger from software.
ADCTRG1bits.TRGSRC2 = 1; // Set AN2 to trigger from software.

From what I can tell, ADCTRGSNS (ADC Trigger Sensitivity) is set to 0 to trigger on a positive edge (a transition from 0 to 1) or set to 1 to constantly trigger as long as the trigger is set to 1.
For ADCTRG1 through 8 (ADC Trigger) we need to set up how the ADC will be triggered. As we will be triggering it manually from software, we need to set AN0, AN1 and AN2's triggers to 1 to tell the PIC32MZ we will be triggering it from software.

/* Early interrupt */
ADCEIEN1 = 0; // No early interrupt
ADCEIEN2 = 0;

No interrupts. Not today. No.

/* Turn the ADC on */
ADCCON1bits.ON = 1;
/* Wait for voltage reference to be stable */
while(!ADCCON2bits.BGVRRDY); // Wait until the reference voltage is ready
while(ADCCON2bits.REFFLT); // Wait if there is a fault with the reference voltage

Having finally finished the ADC setup, we can turn it on. Once it's on, we need to wait for voltages to stabilise, by checking the BGVRRDY and REFFLT flags) before we can actually enable the inputs and use it.

/* Enable clock to analog circuit */
ADCANCONbits.ANEN0 = 1; // Enable the clock to analog bias
ADCANCONbits.ANEN1 = 1; // Enable the clock to analog bias
ADCANCONbits.ANEN2 = 1; // Enable the clock to analog bias
/* Wait for ADC to be ready */
while(!ADCANCONbits.WKRDY0); // Wait until ADC0 is ready
while(!ADCANCONbits.WKRDY1); // Wait until ADC1 is ready
while(!ADCANCONbits.WKRDY2); // Wait until ADC2 is ready

We are so close now! We enable the clock bias circuitry for each of the ADCs (ANEN0 is ADC0, ANEN1 is ADC1 and ANEN2 is ADC2) and then wait for the ADCs to be ready. And then, finally:

/* Enable the ADC module */
ADCCON3bits.DIGEN0 = 1; // Enable ADC0
ADCCON3bits.DIGEN1 = 1; // Enable ADC1
ADCCON3bits.DIGEN2 = 1; // Enable ADC2

The final step in the setup is to set the Digital Enable DIGEN to 1 . From this point on, ADC0, ADC1 and ADC2 are always sampling from AN0, AN1 and AN2 and are waiting for us to trigger them before they convert the sampled values into the 12-bit format we told them to. This is accomplished by:

/* Trigger a conversion */
ADCCON3bits.GSWTRG = 1;
/* Wait the conversions to complete */
while (ADCDSTAT1bits.ARDY0 == 0);
/* fetch the result */
result[0] = ADCDATA0;
while (ADCDSTAT1bits.ARDY1 == 0);
/* fetch the result */
result[1] = ADCDATA1;
while (ADCDSTAT1bits.ARDY2 == 0);
/* fetch the result */
result[2] = ADCDATA2;

Basically, once you trigger the conversion by setting GSWTRG to 1, ADC0, ADC1 and ADC2 will simultaneously start converting their sampled values to something we can use. Once they're done converting, we can read the values from the appropriate register (ADCDATA0 etc).

I tried the example, it doesn't work, you suck

Indeed. If you try the example as above, it may not work. Why? Because for each of those analog inputs, AN0 to AN2, you have to both set the TRIS value for that port to 1 and set the ANSEL bit for each pin to 1 too. Thankfully, these are the defaults so there's no huge problem there but it is something to be aware of. For reference, this can be done like this:

TRISBbits.TRISB2 = 1; // AN2 = RB2, set it as an input
ANSELBbits.ANSB2 = 1; // Set AN2 to analog mode

My example code, available below, adds the fuses for using FRC at 200Mhz and sets up the wait states and cache. Other than that, it's the same as the example straight from the datasheet.

Here's the code.

Tags: code, ADC

Powerbeats Pro Rant / Review

If you're looking for PIC32 information today, I'm sorry to say there's none. I haven't posted anything in a while and I'm on vacation but I've become so frustrated with the lack of information on these earphones that I had to say something.

If anyone is reading this, I'll be back from vacation in two weeks. I'll try to keep answering PIC32MZ questions in the comments until then.

Using the Powerbeats Pro with the Galaxy Watch

TL;DR version: If you want to buy the Powerbeats Pro and use them with the Galaxy Watch, don't! At the very least, know what you're getting yourself into.

A short bit of background

I enjoy running outside and have been doing so for many years. I can't stand the constant pace the treadmill forces me into and I find my legs hurt. Now despite running for almost 20 years I still run like a drunk hippo with a limp. You can feel me coming. I like running with music because it blocks the wheezing sounds and allows me to imagine I look super cool. I've used the Jaybird Bluebuds X, X2, X3 and X4 and enjoyed them. I found the cable that gets stuck on my neck a massive pain and the horrendous proprietary charging clip to be another large concern. So now that I'm on vacation I bought myself the Powerbeats Pro, my first truly wireless earphones. They look so good, I'm sure they'll be excellent for running and will go perfectly with the Galaxy Watch I bought 10 months ago, also for running. Right?

The Powerbeats Pro according to Internet reviewers

Wow! These things are amazing! Completely coincidentally every reviewer has the exact same list of pros and cons. They fit well, have a long battery life, a good seal and pause when you take them out. Good! But the charging case is large, much larger than the airpods and airpods 2. Bad! Apart from that, the concensus is that gee golly they're the best exercise earphones ever!

Enter the Galaxy Watch

They do work great, with my phone. The problem is the average phone size and price has increased over time and I find I no longer want to strap my $1000 phone to my arm when I go for a run or a walk just to listen to music. So I use my Galaxy Watch which, again, got super awesome reviews when it came out.

But it seems that nobody ever tried the Powerbeats Pro with the Galaxy Watch. Presumably because they're 10 months old they no longer exist. So every review I saw tested them with the very latest Android phones, iPhones and Apple Watches. The story is very different with my Galaxy Watch, and with old Android phones too.

Nightmare 1: MAXIMUM VOLUME

Whenever you pause a song or have any dead air for about a second or more (and this seems to include gaps between playback of songs), the powerbeats adjust their volume to the volume of the Watch's media player. So if you've cranked that up to 15/15, welcome to eardrum busting hell. Strangely enough, if you press a volume key the Powerbeats immediately revert to the volume they were supposed to be at. Needless to say, the eardum busting is undesirable at best. Why would I have the volume so high? Because I like to vary the volume during my run at fast and slow sections and I want to be able to control the volume with my earphones, not have to go back to the music app on the watch and adjust it there every single time.

Nightmare 2: Your earphones won't turn off

The Powerbeats Pro have this awesome (?) feature that all reviewers make sure to mention - if you take one of them out, the music pauses. Great! Oh you didn't put them straight into their case and close the case? Well, welcome to them randomly turning themselves on in your pocket, or even in your hand if you go into a slightly dark room and trick the built-in light sensor into thinking they're in your ear. So what, just disconnect them from your watch, right? Sure, and then they'll reconnect themselves when they wake up. Because they don't have a power button they cannot be turned off. They "just work", except when they don't. The only way I've found to shut them up is to turn off the bluetooth on my watch or put them in their charging case. Maybe this is why every single reviewer made such a big deal about the charging case size but I doubt it.

Oh and when they restart, thanks to the volume bug they restart at MAXIMUM VOLUME so be prepared for giggles as your embarrassing running playlist starts blasting out of your pocket at all sorts of hilariously inappropriate locations.

Nightmare 3: Poor connection and the volume bug

The Galaxy Watch's Bluetooth connection sucks. When I'm running and my arms are raised the connection seems fine but if I'm going on a walk it's a whole 'nother story. My arms are fairly long, clearly too long for the signal to reach the Powerbeats Pro. So they start to stutter, maybe even have different audio in each earbud. If I raise my arm, the connection is fine again but who wants to walk with their arms up by their side like a T-Rex? So I pause the music, let the watch catch up and then resume. MAXIMUM VOLUME again! Rock on! It's an unbelievably poor experience.

In summation

Who are tech reviews written for? Do I have to own all the latest tech in order to make use of them? Why does nobody mention these problems which, as I've said, I've encountered old Android phones (a few years old) too? Anyway, I hope this rant helps someone out there avoid an unnecessary purchase.

Tags: review, rant, moaning, audio

SQI on the PIC32MZ

Refresher on SPI

A while back, I covered using the Serial Peripheral Interface (SPI) and how it worked. If you recall, we had four signals:

  • Chip Select (aka Slave Select) - To choose which slave we are talking to
  • Clock - Generated by the master and provided to all the slaves
  • Master Out Slave In (MOSI) - For the master to send data to the slave, and
  • Master In Slave Out (MISO) - For the slave to send data back to the master

PIC32MZ - Typical SPI connection

The communication was full-duplex, meaning data could be sent and received at the same time. However, the transactions had to be quite carefully controlled. If, for example, we wanted to send the command 0x9F to the slave and get a byte of data back, we'd have to do this:

PIC32MZ - SPI in a jarring GIF

<TL;DR>
As can hopefully be seen in the thrilling animation above, the master starts off sending a the byte 0x9F to the slave. At the same time, the slave is also sending data to the master. Until the slave has received the whole 8 bits, it does not know what the master is sending to it. So it has to wait until it receives the 8 bits, formulate a response and then send it out to the master the next time. Assuming it generates the response instantly, it still has no way to send the data back to the master because only the master can generate clock signals. This is why the master is seen to again be sending 0xFF (a dummy value) to the slave, purely to provide it with the clock signals it needs to send the data back to the master. 0xFF is typically used to avoid confusion with actual commands.
</TL;DR>

If we look at the code, we'd have to do this:

// Send the 0x9F byte
SPI1BUF = 0x9F;
// Wait until the reply has been received
while (SPI1STATbits.SPIRBE);
// Very important: read the reply from the buffer to clear the buffer
reply = SPI1BUF;
// Now we need to read the reply
// Send a dummy byte 0xFF
SPI1BUF = 0xFF;
// Wait until the reply has been received
while (SPI1STATbits.SPIRBE);
// Read the actual reply we want
reply = SPI1BUF;

So as you can see, it's fairly involved and there's a lot of hand-holding required. The speed is also not fantastic. We have a maximum speed of 50MHz, though some peripherals only work up to 20Mhz or even less. We can also send only one bit at a time, so even at 50MHz, we're only getting a maximum transfer rate of 6.25MB/s. This is where Serial Quad Interface (SQI) comes in.

So what is SQI?

SQI, as the name implies, can transfer up to 4 bits at once. While SPI uses 4 lines, SQI uses 6. They are:

  • Clock - The same as SPI, generated by the master
  • Chip select - Again, the same as SPI
  • D0 - The first data bit
  • D1 - The second data bit
  • D2 - The third data bit
  • D3 - The fourth data bit

<TL;DR>
"Up to"? Yes, SQI can be configured to send either 1 bit, 2 bits or 4 bits at a time. Many external devices that support both SPI and SQI start up in SPI mode and need to be sent a special command in order to switch to SQI mode. The PIC32MZ SQI peripheral fully supports this, thankfully, so if you really wanted you could use the SQI peripheral as a regular SPI peripheral.
</TL;DR>

From this, we can see the major difference between SPI and SQI. With SPI, we had one line for sending data to the slave and one line for receiving data and they were both in use at the same time. That is to say that communication was full duplex. With SQI, communication can only occur in one direction at a time, it is half duplex.

Let's take a look at a typical SQI connection:

PIC32MZ - Typical SQI connection

OK, not that hard so far. The hard part comes next.

The PIC32MZ's SQI peripheral

One of the most important things to remember with SQI on the PIC32MZ is that the communication can only happen in one direction at a time. This means we need some way of telling the PIC32MZ whether we want to send or receive data. This also means that, unlike the SPI peripheral, we cannot just write data to the SQI buffer and hope it'll get sent because it won't.

It turns out there are six registers we need to concern ourselves with:

  • SQI1THR - SQI Threshhold Control Register
  • SQI1INTTHR - SQI Interrupt Threshold Register
  • SQI1CMDTHR - SQI Command Threshhold Register
  • SQI1CON - SQI Control Register
  • SQI1TXDATA - SQI Transfer Data Buffer Register
  • SQI1RXDATA - SQI Receive Data Buffer Register

OK, that's a lot of registers, but why? Well, with SQI Microchip decided you know what, why don't we make this as fancy as we can? And to their credit, it is very fancy, but it can be very confusing too. Whereas with SPI we had to handle the transactions one by one, that is no longer the case with the SQI peripheral. Everything now works with buffers, meaning I can load up a whole list of transactions and it'll do them one by one. I repeat, writing to any of these registers will add whatever you've written to a buffer (or a queue).

This means that for every transaction with SQI, I need to tell it how many bytes I want to send or receive, how many lanes (1, 2 or 4) I will use and whether or not it's a send or receive transaction and then write the data to SQI1TXDATA (or read it from SQI1RXDATA). It can be confusing, so I'll go into it more in a moment.

If you've been following along with the PIC32MZ datasheet, you may have experienced cases where the datasheet contradicts itself multiple times. The SQI is one such case and why it's taken so long for me to get this code working at an acceptable level! As such, some of these registers remain a bit of a mystery to me but I know they have to be set in order for stuff to work :) Let's take a look at them one by one, very briefly.

SQI1THR

I think this controls how many transactions we can queue up in the SQI Control buffer. I just set it to 0x100 when I initialise the SQI peripheral and never touch it again.

SQI1INTTHR

I think this is used to set how many bytes transferred or received will trigger a transfer or receive interrupt. Again, I just set it to 0x100 when I initialise the SQI peripheral and never touch it again.

SQI1CMDTHR

I think this is used to set how many bytes need to be in the send or receive buffer before the SQI periperhal will start doing anything. I again set this before each transaction and I set it to be the same as the number of bytes I'm about to transmit/receive. The lower 6 bits are receive bytes and bits 8 to 13 are for transmit bytes.

SQI1CON

Finally, one I understand. This is used to tell the SQI peripheral how many bytes we are going to write to SQI1TXDATA (or read from SQI1RXDATA), how many lanes (so 1-lane (like SPI), 2-lane or 4-lane) and what kind of transaction it is, be it send or receive. There are other things it can do but I'm limiting it to this much today. It bears looking at how it's defined in the datasheet:

PIC32MZ - SQI1CON register

Note: There are two Chip Select pins (SQICS0 and SQICS1) so I presume that's where device 0 and 1 come from. I don't use either, I use a different pin as chip select and have only been able to get it to work using device 1. No, I don't know why.

Note 2: TXRXCOUNT is an ambitious 16 bits wide but the actual buffer itself is much smaller at 32 bytes, so this number should never exceed 32.

SQ1ITXDATA

Writing to this register will cause whatever data we write to be pushed into the transmit buffer. Note, however, that it defaults to writing 32-bits of data to the SQI transmit buffer!. This means that this:

unsigned char tmp = 128;
SQI1TXDATA = tmp

Will actually write 4 bytes of data to the transmit buffer (that is, 0x00 0x00 0x00 0x80)! If you want to write only 8 bits, you need to perform this trick:

unsigned char *TXDATA = (unsigned char *)&SQI1TXDATA;   // Address to write to for 8-bit data
*TXDATA = 128;

This is unlike SPI and is something to be wary of!

SQI1RXDATA

Reading from this register will pop whatever is on the top of the receive buffer off. Like with SQI1TXDATA, to access an 8-bit value, you have to do the following:

unsigned char *RXDATA = (unsigned char *)&SQI1RXDATA;   // Address to read from for 8-bit data
unsigned char result;
result = *RXDATA;

OK, so how do we use this in code?

First up, we have to initialise the SQI peripheral. Bear in mind it is connected to Reference Clock 2, so that is some we first have to set up. The initialisation is done like this:

void SQI_init()
{
    CFGCONbits.TROEN = 0; // Disable trace outputs because SQI share them

    // Set up Reference Clock 2
    if (!REFO2CONbits.ACTIVE)
    {
        REFO2CONbits.RODIV = 1;
        REFO2CONbits.ROSEL = 1;
        REFO2CONbits.ON = 1;
        while (REFO2CONbits.DIVSWEN);
        REFO2CONbits.OE = 1;
    }

    // Turn *off* clock division according to the errata
    SQI1CLKCONbits.CLKDIV = 0;
    SQI1CLKCONbits.EN = 1;
    // Wait until the SQI clock reports it is stable
    while (!SQI1CLKCONbits.STABLE);

   // Tell the SQI peripheral to reset
    SQI1CFGbits.RESET = 1;    
    SQI1CFGbits.CPOL = 0;
    SQI1CFGbits.CPHA = 0;
    // Set the mode to 1, which is PIO mode (where we control it directly)
    SQI1CFGbits.MODE = 1;
    // Enable burst mode, again as datasheet says
    SQI1CFGbits.BURSTEN = 1;
    // Enable the SQI peripheral
    SQI1CFGbits.SQIEN = 1;
    // Enable data lines SQID0, SQID1, SQID2 and SQID3
    SQI1CFGbits.DATAEN = 0b10;

    // Set up buffers to trigger as soon as 1 byte is present
    SQI1THR = 0x100;
    SQI1INTTHR = 0x100;
}

OK, now it's set up, how on earth do we use the thing?

My first example today is very basic. I am communicating with an 8MB PSRAM. The PSRAM starts up in SPI mode. So initially, what I want to do is send it the command 0x9F, which will cause it to send me it's EID information. Again, this is what I want to do:

  • Send the 8-bit command 0x9F
  • Send 3 dummy bytes for address (as the PSRAM datasheet says to)
  • Read 8 bytes of data as a response

In code, I'd do this:

void SRAM_get_EID(unsigned int *buf)
{    
    // Pull CS low, select the PSRAM
    SRAM_select(0);

    // Set up for 4 bytes initially
    SQI1THR = 0x100;
    SQI1INTTHR = 0x100;

    // Sending 4 bytes, 0x9F "Get EID" command and 3 empty address bytes
    SQI1CMDTHR = 0x100;
    // Deassert chip select when done, using device 1, using single lane mode, transmit command, 4 bytes
    SQI1CON = 0x00510004;
    // Remember, this next line actually sends 0x00 0x00 0x00 0x9F!
    SQI1TXDATA = 0x9F;

    // Wait until the transmit buffer is empty i.e. the data has been fully transmitted
    while (SQI1STAT1bits.TXBUFFREE < 32);

    // Trigger on each byte received
    SQI1CMDTHR = 0x01; // Receive 8 bytes
    // Deassert chip select when done, using device 1, using single lane mode, receive command, 8 bytes
    SQI1CON = 0x00520008;

    // Wait until the lower 8 bits of SQI1STAT1 (which are received bytes count) = 8
    while ((SQI1STAT1 & 0xFF) != 0x08);

    // Read the first 4 bytes and store them in the array pointer
    *buf = SQI1RXDATA;
    // Move array pointer to next element
    buf++;
    // Read the final 4 bytes and store them in the array pointer
    *buf = SQI1RXDATA;

    // Set CS high again, deselect the PSRAM
    SRAM_select(1);
}

This same PSRAM then has a command to set it into quad lane mode (command 0x35, but this time with no dummy bytes). I'd do this as follows:

void SRAM_go_SQI()
{
    // We want to write an 8-bit value to the SQI transmit buffer, so set that up
    unsigned char *TXDATA;
    TXDATA = (unsigned char *)&SQI1TXDATA;

    // Pull CS low, select the PSRAM
    SRAM_select(0);

    SQI1THR = 0x100;
    SQI1INTTHR = 0x100;
    // Trigger on 4 bytes, though this works just fine
    SQI1CMDTHR = 0x100;
    // Deassert chip select when done, using device 1, using single lane mode, transmit command, 1 byte
    SQI1CON = 0x00510001;

    // Write the 8-bit value 0x35 to the transmit buffer
    *TXDATA = 0x35;

    // Wait until the transmit buffer is empty i.e. the data has been fully transmitted
    while (SQI1STAT1bits.TXBUFFREE < 32);

    // Set CS high again, deselect the PSRAM
    SRAM_select(1);
}

Now that we're in quad lane mode, let's try writing some data to that PSRAM. First, we need to tell the PSRAM we are going to be writing to it. The quad-lane write command is 0x38 and it needs to be followed by a 3-byte address. We'll do it like this:

void SRAM_start_write_quad(int address)
{
    // We're about to have some endian fun
    unsigned int data;
    unsigned int endian[3];

    // Pull CS low, select the PSRAM
    SRAM_select(0);

    // Trigger on 4 bytes
    SQI1CMDTHR = 0x00000400;
    // Deassert chip select when done, using device 1, using quad lane mode now, transmit command, 4 bytes
    SQI1CON = 0x00590004;

    // Now, if our address is 0x123456, we are going to have to switch that around to 0x563412 because the PIC32 is a little-endian device. Yay.  
    endian[0] = address >> 16;
    endian[1] = (address & 0x00FF00) >> 8;
    endian[2] = (address & 0xFF);

    address = (endian[2] << 16) | (endian[1] << 8) |(endian[0]);
    data = (address << 8) | 0x38;

    // The actual order of bytes *sent* will be 0x38, address[16:23], address[8:15], address[0:7].
    // I've done it in a bit of a round-about way to hopefully make it clearer.

    // Send the 4 bytes of data to the transfer queue
    SQI1TXDATA = data;

    // Wait until the transmit buffer is empty i.e. the data has been fully transmitted
    while (SQI1STAT1bits.TXBUFFREE < 32);

    // NOTE: HERE I DO NOT SET THE CHIP SELECT LINE HIGH. That would indicate to the PSRAM chip that the transaction was over!
}

OK, so I've told it to get ready for data, now let's write that data in super fast quad lane mode, one byte at a time:

    for (cnt = 0; cnt < num_bytes; cnt++)
    {
        // Set SQI TX command threshold to 1 byte in a slightly different way why not
        SQI1CMDTHRbits.TXCMDTHR = 1; 
        // Deassert chip select when done, using device 1, using quad lane mode now, transmit command, 1 byte
        SQI1CON = 0x00590001;

        // Send an 8-bit value to the buffer
        *TXDATA = buffer[cnt];

        // Wait until the transmit buffer is empty i.e. the data has been fully transmitted
        while (SQI1STAT1bits.TXBUFFREE < 32);
    }

    // Now I can set Chip Select high again and thus deselect the PSRAM because I'm done writing
    SRAM_select(1);

Would it be faster if I didn't send one byte at a time and then nanny over the transmit buffer? Surely it would, yes. However, as this is an intro to getting SQI to work let's keep it as safe as we can for now :)

Phew, and now we've written data to the PSRAM. In the example code below I've included code for reading and writing in both single and quad-lane mode.

Please bear in mind that on my development board the PSRAM's Chip Select is connected to Port RJ1

Here's the code. Good luck!

Tags: code, SQI