SPI Encoder read times extended dramatically with second encoder

I have a Raspberry Pi Pico board reading 2 AS5048A position encoders via SPI.

If my loop reads either encoder on its own, the read completes in around 100uS.
If I read both sensors, the read time isn’t the 200us I am expecting, its 7x slower at 1400 us!

I think this is why the control loop becomes unstable as soon as the second encoder is added to the main loop. Any thoughts on what would cause this?

I am measuring the read time with this section of the main loop, and comment out one of the update() calls to test the single encoder case.

  unsigned long t1 = micros();
  sensor0.update();    
  sensor1.update();     
  unsigned long t2 = micros();
  Serial.println(t2-t1);

Both sensors appear to be working and return sensible values.

Below is the full sketch

`#include "Arduino.h"
#include "SPI.h"
#include "SimpleFOC.h"
#include "SimpleFOCDrivers.h"
#include "encoders/as5048a/MagneticSensorAS5048A.h"


#define SPI_0_MISO  4
#define SPI_0_MOSI  3
#define SPI_0_SCK   2
#define SENSOR0_CS  5 


#define SPI_1_MISO  12
#define SPI_1_MOSI  15
#define SPI_1_SCK   14
#define SENSOR1_CS  13 

MagneticSensorAS5048A sensor0(SENSOR0_CS, true);
MbedSPI spi_i0(SPI_0_MISO, SPI_0_MOSI, SPI_0_SCK);

MagneticSensorAS5048A sensor1(SENSOR1_CS, true);
MbedSPI spi_i1(SPI_1_MISO, SPI_1_MOSI, SPI_1_SCK);


void setup() {

  spi_i0.begin();
  sensor0.init(&spi_i0);

  spi_i1.begin();
  sensor1.init(&spi_i1);
}

void loop() {
 // update the sensor (only needed if using the sensor without a motor)

  unsigned long t1 = micros();
  sensor0.update();    
  sensor1.update();     
  unsigned long t2 = micros();
  Serial.println(t2-t1);

  // get the angle, in radians, including full rotations
  float a1 = sensor1.getAngle();


  if (sensor1.isErrorFlag()) {
    AS5048Error error = sensor1.clearErrorFlag();
    Serial.println("Cleaning up sensor 1");

  }

  // get the angle, in radians, including full rotations
  float a0 = sensor0.getAngle();
  
  Serial.println(String(a0)+','+String(a1));

  if (sensor0.isErrorFlag()) {
    AS5048Error error = sensor0.clearErrorFlag();
    Serial.println("Cleaning up sensor 0");
  }


  delay(500);
}`

I don’t see obvious issues in your code, but first I’d make sure my timing procedure is correct:

  1. I would check that my sketch is the only task running (it seems you’re using a RTOS);
  2. I would measure a lot of values and average them, instead of performing a one-time measurement.

Next I would probably but the two encoders on the same SPI bus to see what happens.

The sketch is the only task loaded. I don’t normally use Arduino but my limited understanding is your sketch is all that runs, there is no OS getting in the way.

The main loop measures the time to read the two encoders every 0.5 seconds and the readings are very consistent. Always long when reading 2 encoders, always short when reading 1.

We can see the slow down in the cs timing. Here are the cs pulses with 2 encoders being used. Red is the cs for one channel, blue the cs for the other.

And with a single encoder there is a much shorter active period for the cs

With 2 encoders in use the cs signals go low, then nothing happens for a long time, then finally the read is made and the cs goes high again. Here cs is red, clock is blue.

With a single encoder that delay doesn’t happen. cs goes low and the reads follow soon after. Again cs is red, clock is blue.

So the question becomes what causes the large delay between asserting the cs and the reads commencing.

Timing function calls down the calling hierarchy I have got to this file

Arduino15/packages/arduino/hardware/mbed_rp2040/4.1.3/libraries/SPI/SPI.cpp

In that file we find this function

uint16_t arduino::MbedSPI::transfer16(uint16_t data) {
union { uint16_t val; struct { uint8_t lsb; uint8_t msb; }; } t;
t.val = data;

if (settings.getBitOrder() == LSBFIRST) {
    t.lsb = transfer(t.lsb);
    t.msb = transfer(t.msb);
} else {
    t.msb = transfer(t.msb);	**DELAY OCCURS HERE**
    t.lsb = transfer(t.lsb);
}
unsigned long t4 = micros();

return t.val;

}

The above line calls

uint8_t arduino::MbedSPI::transfer(uint8_t data) {
uint8_t ret;
dev->obj->write((const char*)&data, 1, (char*)&ret, 1);
return ret;
}

But here I draw a blank, not sure where to look for the definition of this write function.
The dev->obj pointer appears to be an instance of mbed::SPI

I find MbedSPI in

Arduino15/packages/arduino/hardware/mbed_rp2040/4.1.3/libraries/SPI/SPI.h

Its defined as

class MbedSPI : public SPIClass

and does not contain a member function called write

SPIClass is defined in

Arduino15/packages/arduino/hardware/avr/1.8.6/libraries/SPI/src/SPI.h

also does not have a function called write, and doesn’t inherit from anything. Will come back to this another day.

That transfer function is internal to the framework…

Very interesting bug report.

Since you’re using 2 SPI ports, you could try to run the code for each sensor on separate cores, and see if the problem still occurs.

It looks like some kind of glitch in the RP2040 SPI implementation - I wonder which core is handling the SPI interrupts internally, and if there is some kind of issue there once you add the second sensor.

Hi Runger, Thank you for your assistance. The two core idea is interesting! I might be able to run one control loop on each core and double the speed.

The problem is I cannot get concurrency to happen. Here is my trivial test case.

#include "SimpleFOC.h"
#include "SimpleFOCDrivers.h"

void setup() {
}

void loop() {
  Serial.println("Core 0 says hi");
  delay(2000);
}

void setup1() {
}

void loop1() {
  Serial.println("Core 1 says hi");
  delay(3000);
}

Using the Arduino Mbed OS RP2040 Boards by Arduino board library I don’t see any concurrency, only the Core 0 message is shown.

Using the Raspberry Pi Pico/RP2040 by Earle F. Philhower, III the concurrency happens, but the SimpleFOC headers wont compile. I saw your fix you made for the pin numbering on github

and applied that. But that just exposed another set of errors.

/Users/jeremy/Documents/Arduino/libraries/SimpleFOCDrivers/src/settings/rp2040/RP2040FlashSettingsStorage.cpp: In member function ‘virtual RegisterIO& RP2040FlashSettingsStorage::operator<<(uint8_t)’:

/Users/jeremy/Documents/Arduino/libraries/SimpleFOCDrivers/src/settings/rp2040/RP2040FlashSettingsStorage.cpp:85:1: error: no return statement in function returning non-void [-Werror=return-type]

And its got a valid point, line 83 on I see

RegisterIO& RP2040FlashSettingsStorage::operator<<(uint8_t value) {

};

RegisterIO& RP2040FlashSettingsStorage::operator<<(float value) {

};

RegisterIO& RP2040FlashSettingsStorage::operator<<(uint32_t value) {

};

Guilty as charged.

Jeremy

Progress finally!

The code below reads two sensors each on a different SPI port, each reader running on a different core on a Raspberry Pi Pico with good performance, almost always less than 40uS per read.

To get this working I did as follows:
1
Went with the Raspberry Pi Pico/RP2040 board profile from Earle F. Philhower, III.
2
Commented out the lines in Arduino/libraries/SimpleFOCDrivers/src/settings/rp2040/RP2040FlashSettingsStorage.cpp
causing syntax errors mentioned in the post above. Specifically lines from 82 to just prior to the final #endif in that file.
3
Applied Runger’s patch from github, also mentioned above. Nothing sophisticated using git pulls, I just typed in the change by hand.
4
Changed the way SPI busses and their pinouts are selected to be compatible with the way Philhower does SPI.

And with that done, this simple test case happily reads 2 off AS5048A magnetic encoders in parallel.

#include "Arduino.h"
#include "SPI.h"
#include "SimpleFOC.h"
#include "SimpleFOCDrivers.h"
#include "encoders/as5048a/MagneticSensorAS5048A.h"

#define SPI_0_MISO  4
#define SPI_0_MOSI  3
#define SPI_0_SCK   2
#define SENSOR0_CS  5 


#define SPI_1_MISO  12
#define SPI_1_MOSI  15
#define SPI_1_SCK   14
#define SENSOR1_CS  13 

MagneticSensorAS5048A sensor0(SENSOR0_CS, true);
SPIClassRP2040 SPI_i0(spi0, SPI_0_MISO, SENSOR0_CS, SPI_0_SCK, SPI_0_MOSI);

MagneticSensorAS5048A sensor1(SENSOR1_CS, true);
SPIClassRP2040 SPI_i1(spi1, SPI_1_MISO, SENSOR1_CS, SPI_1_SCK, SPI_1_MOSI);

float a1;
int tCore1;

void setup() {
  SPI_i0.begin();
  sensor0.init(&SPI_i0);
}

void setup1() {
  SPI_i1.begin();
  sensor1.init(&SPI_i1);
}

void loop() {
  unsigned long t1 = micros();
  sensor0.update();    
  float a0 = sensor0.getAngle();
  unsigned long t2 = micros();


  Serial.println("Angles: A0="+String(a0)+", A1="+String(a1)+" timings Core 0 "+String(t2-t1)+"us, Core 1 "+String(tCore1)+"us");

  if (sensor0.isErrorFlag()) {
    AS5048Error error = sensor0.clearErrorFlag();
    Serial.println("Cleaning up sensor 0");
  }

  delay(500);

}


void loop1() {
  static long loopCounter = 0;

  unsigned long t1 = micros();
  sensor1.update();
  a1 = sensor1.getAngle();
  unsigned long t2 = micros();

  tCore1 = t2-t1;

  if (sensor1.isErrorFlag()) {
    AS5048Error error = sensor1.clearErrorFlag();
    Serial.println("Cleaning up sensor 1");
  }

  delay(500);
}

Now I can get back to the SimpleFOC motor control.

2 Likes

Having been forced from mbed to Earles implementation of the Raspberry Pico board integration to get multi core support, it occurred to me the SPI driver code got swapped out too. Perhaps the original SPI performance issue disappeared along with that.

So tried merging the 2 threads back into one, and find it does indeed still perform well.

I am seeing 30us to read the first encoder, and another 20us on the second. Of course with a single thread, these add up, so 50us for both encoders. But thats still a massive 28 times faster than the 1400us I was getting with mbed reading both encoders, and even 4x faster than mbed reading a single encoder!

So in summary, stay clear of mbed if you are working with SPI on the Raspberry Pi Pico and care about performance.

1 Like

Thanks a lot for this very informative test! That’s really good to know.
I’ve definitely observed the difference in performance between the two cores, but didn’t know what the reason was. pinpointing it to the SPI is very useful indeed.

I wonder if we should just recommend the use of philearlehowers core in the first place?

It’s also hard to understand why Arduino does this to its users? Building on Mbed, which they’ve done on a number of their boards, would seem to benefit no one but themselves, and then only if we assume their aim is to kick new framework versions out the door ASAP, with no regards to size or performance… :frowning:

Hi Runger. I now have two SimpleFOC motor controllers running and its working well. It’s a tidy solution, each motor controller has its own dedicated core and SPI interface.

I support recommending using Earles code for Pi Pico users starting from the moment SimpleFOC’s code will compile without modification against it. That means the fix you already have in github being released as well as a fix for the empty functions with non void return values.