A better PIO program for PWM decode

One of the things that the BLDC controller needs to do is to decode a PWM signal from an AS5048A hall effect sensor. I had been using two RP2040 PIO programs to measure both the high time and the interval, but I was finding it a little awkward to interleave reads from two FIFOs (and it was using all the PIO resources with 4 motors).

After a bit of head scratching, I came up with this program that reads both the high time of the PWM and the interval and sends them in one 32-bit “struct” on the FIFO. The main reason that I wanted to use two programs before was to avoid the possibility of getting out of sync if I alternated high/interval on the same FIFO.

What if the CPU was reading the interval but thought it was the high time or vice versa? Packing them into one 32-bit value neatly solves that problem. The trick is to use the “IN” instruction, which is normally used to read from pins, but it supports reading from another register instead. By using IN to fetch 16 bits from the counter register (X), we can shift 16 bits of the counter into the ISR register, ready to be sent. By using the “autopush” feature on 32 bits the PIO automatically pushes the packed value after the second “IN” instruction. The only gotcha with this approach is that the counters are limited to 16 bits, which is enough to handle roughly 1.5ms intervals. The AS5048A uses a 1ms interval so that’s just about right for my application.

.wrap_target
  mov x, !NULL             ; x = 0xffffffff

loop_high:                 ; do {
  jmp x-- cont_loop_high   ;   x--
cont_loop_high:            ;
  nop                      ;   nop to match number of cycles below
  jmp pin loop_high        ; } while pin is high

  in x, 16                 ; Copy 16 bits of counter into ISR

loop_low:                  ; do {
  jmp x-- cont_loop_low   ;   x--
cont_loop_low:             ;
  jmp pin exit_loop_low    ;   if pin is high: break
  jmp loop_low             ; } while 1

exit_loop_low:
  in x, 16                 ; Copy 16 bits of counter into ISR
.wrap

The interval and count can then be decoded as follows:

uint32_t high = 0xffff - (raw_pio_output>>16);
uint32_t invl = 0xffff - (raw_pio_output & 0xffff);

The PIO assembler file is here on Github.

BLDC controller phase 4: moving to C

While I had got a motor moving in MicroPython, I found I hit a wall with the garbage collector causing multi-millisecond pauses.

Green signal high showing a multi-millisecond pause. Multiple PWM pulses go by on the yellow signal while Python is snoozing!

A BLDC controller needs to be updating its model at at least 1KHz (and preferably more) so a multi-ms pause at random times is out of the question. I tried a few approaches to avoid GC but, even using the “compile-to-machine code” options in MicroPython, I couldn’t seem to control it. Beyond that, you’re only one small mistake from reintroducing GC by allocating something on the heap accidentally…. If I’m going to be one small mistake away from blowing my foot off, I’d rather work in C, where that’s perfectly normal 🙂

Porting the code to C went very smoothly:

  • The Pico C SDK is wonderful, with excellent documentation. Probably the smoothest C SDK bring-up that I’ve ever experienced. Along with copious examples, there’s even a GUI tool to generate a skeleton project that has a hello-world for each peripheral that you intend on using.
  • I translated the PIO programs from Python syntax to native PIO assembler and the C SDK made it easy to build that into the project.
  • Translating the driver code itself, I did a fairly straight port for the initial version, but I opted to roll my own fixed point arithmetic instead of defaulting to floating point. For example, rather than converting a raw PWM value (which ranges from 0-212-1) I stuck with 212 as my base for angles. That means that clamping to 0-360 degrees can be handled by bitwise AND with 212-1 rather than an expensive modulo operation.

And what was the end result? The C code was significantly faster. So fast that my trick of toggling a pin while the code was actively running didn’t seem to be working at first. I had to zoom in on the oscilloscope to catch it!

Equivalent C code, blink and you’ll miss it!

Once I started work on the C code, I created a Github repo, the initial commit of the C code is here. The initial commit was still a prototype, it

  • Measured the angle of the motor.
  • Multiplied up to get the angle relative to the magnetic poles in the motor.
  • Added a offset, controlled by a pair of pushbuttons.
  • Drove the motor PWMs with a sine wave at the offset phase.

Still, it was enough to prove that C was fast and that the motor could be driven efficiently by correctly controlling that phase angle.