9 Pico PIO Wats with MicroPython (Half 2) | by Carl M. Kadie | Jan, 2025

Raspberry Pi programmable IO pitfalls illustrated with a musical instance

Pico PIO Surprises, Half 2 — Supply: https://openai.com/dall-e-2/. All different figures from the writer.

That is Half 2 of an exploration into the sudden quirks of programming the Raspberry Pi Pico PIO with MicroPython. In the event you missed Half 1, we uncovered 4 Wats that problem assumptions about register depend, instruction slots, the conduct of pull(noblock), and sensible but low-cost {hardware}.

Now, we proceed our journey towards crafting a theremin-like musical instrument — a mission that reveals among the quirks and perplexities of PIO programming. Put together to problem your understanding of constants in a means that brings to thoughts a Shakespearean tragedy.

On the planet of PIO programming, constants must be dependable, steadfast, and, properly, fixed. However what in the event that they’re not? This brings us to a puzzling Wat about how the set instruction in PIO works—or does not—when dealing with bigger constants.

Very similar to Juliet doubting Romeo’s fidelity, you would possibly end up questioning if PIO constants will, as she says, “show likewise variable.”

The Downside: Constants Are Not as Massive as They Appear

Think about you’re programming an ultrasonic vary finder and must depend down from 500 whereas ready for the Echo sign to drop from excessive to low. To arrange this wait time in PIO, you would possibly naively attempt to load the fixed worth immediately utilizing set:

set(y, 500)                   # Load max echo wait into Y
label("measure_echo_loop")
jmp(pin, "echo_active") # if echo voltage is excessive proceed depend down
jmp("measurement_complete") # if echo voltage is low, measurement is full
label("echo_active")
jmp(y_dec, "measure_echo_loop") # Proceed counting down until timeout

Apart: Don’t attempt to perceive the loopy jmp operations right here. We’ll talk about these subsequent in Wat 6.

However right here’s the tragic twist: the set instruction in PIO is proscribed to constants between 0 and 31. Furthermore, MicroPython’s star-crossed set instruction does not report an error. As an alternative, it silently corrupts your entire PIO instruction. (PIO from Rust reveals an identical downside.) This produces a nonsense consequence.

Workarounds for Inconstant Constants

To handle this limitation, think about the next approaches:

  • Learn Values and Retailer Them in a Register: We noticed this method in Wat 1. You possibly can load your fixed within the osr register, then switch it to y. For instance:
# Learn the max echo wait into OSR.
pull() # similar as pull(block)
mov(y, osr) # Load max echo wait into Y
  • Shift and Mix Smaller Values: Utilizing the isr register and the in_ instruction, you possibly can construct up a continuing of any dimension. This, nevertheless, consumes time and operations out of your 32-operation price range (see Half 1, Wat 2).
# Initialize Y to 500
set(y, 15) # Load higher 5 bits (0b01111)
mov(isr, y) # Switch to ISR (clears ISR)
set(y, 20) # Load decrease 5 bits (0b10100)
in_(y, 5) # Shift in decrease bits to type 500 in ISR
mov(y, isr) # Transfer ultimate worth (500) from ISR to y
  • Gradual Down the Timing: Scale back the frequency of the state machine to stretch delays over extra system clock cycles. For instance, reducing the state machine velocity from 150 MHz to 343 kHz reduces the timeout fixed 218,659 to 500.
  • Use Additional Delays and (Nested) Loops: All directions help an elective delay, permitting you so as to add as much as 31 additional cycles. (To generate even longer delays, use loops — and even nested loops.)
# Generate 10μs set off pulse (4 cycles at 343_000Hz)
set(pins, 1)[3] # Set set off pin to excessive, add delay of three
set(pins, 0) # Set set off pin to low voltage
  • Use the “Subtraction Trick” to Generate the Most 32-bit Integer: In Wat 7, we’ll discover a option to generate 4,294,967,295 (the utmost unsigned 32-bit integer) through subtraction.

Very similar to Juliet cautioning towards swearing by the inconstant moon, we’ve found that PIO constants will not be at all times as steadfast as they appear. But, simply as their story takes sudden turns, so too does ours, transferring from the inconstancy of constants to the uneven nature of situations. Within the subsequent Wat, we’ll discover how PIO’s dealing with of conditional jumps can depart you questioning its loyalty to logic.

In most programming environments, logical situations really feel balanced: you possibly can check if a pin is excessive or low, or examine registers for equality or inequality. In PIO, this symmetry breaks down. You possibly can leap on pin excessive, however not pin low, and on x_not_y, however not x_eq_y. The principles are whimsical — like Humpty Dumpty in By way of the Trying-Glass: “After I provide a situation, it means simply what I select it to imply — neither extra nor much less.”

These quirks power us to rewrite our code to suit the lopsided logic, making a gulf between how we want the code could possibly be written and the way we should write it.

The Downside: Lopsided Circumstances in Motion

Take into account a easy situation: utilizing a spread finder, you need to depend down from a most wait time (y) till the ultrasonic echo pin goes low. Intuitively, you would possibly write the logic like this:

label("measure_echo_loop")
jmp(not_pin, "measurement_complete") # If echo voltage is low, measurement is full
jmp(y_dec, "measure_echo_loop") # Proceed counting down until timeout

And when processing the measurement, if we solely want to output values that differ from the earlier worth, we’d write:

label("measurement_complete")
jmp(x_eq_y, "cooldown") # If measurement is similar, skip to chill down
mov(isr, y) # Retailer measurement in ISR
push() # Output ISR
mov(x, y) # Save the measurement in X

Sadly, PIO doesn’t allow you to check not_pin or x_eq_y immediately. It’s essential to restructure your logic to accommodate the obtainable situations, equivalent to pin and x_not_y.

The Answer: The Method It Should Be

Given PIO’s limitations, we adapt our logic with a two-step method that ensures the specified conduct regardless of the lacking situations:

  • Bounce on the other situation to skip two directions ahead.
  • Subsequent use an unconditional leap to succeed in the specified goal.

This workaround provides one additional leap (affecting the instruction restrict), however the extra label is cost-free.

Right here is the rewritten code for counting down till the pin goes low:

label("measure_echo_loop")
jmp(pin, "echo_active") # If echo voltage is excessive, proceed countdown
jmp("measurement_complete") # If echo voltage is low, measurement is full
label("echo_active")
jmp(y_dec, "measure_echo_loop") # Proceed counting down until timeout

And right here is the code for processing the measurement such that it’ll solely output differing values:

label("measurement_complete")
jmp(x_not_y, "send_result") # If measurement is completely different, ship it
jmp("cooldown") # If measurement is similar, skip sending
label("send_result")
mov(isr, y) # Retailer measurement in ISR
push() # Output ISR
mov(x, y) # Save the measurement in X

Classes from Humpty Dumpty’s Circumstances

In By way of the Trying-Glass, Alice learns to navigate Humpty Dumpty’s peculiar world — simply as you’ll be taught to navigate PIO’s Wonderland of lopsided situations.

However as quickly as you grasp one quirk, one other reveals itself. Within the subsequent Wat, we’ll uncover a stunning conduct of jmp that, if it had been an athlete, would shatter world data.

In Half 1’s Wat 1 and Wat 3, we noticed how jmp x_dec or jmp y_dec is usually used to loop a hard and fast variety of occasions by decrementing a register till it reaches 0. Easy sufficient, proper? However what occurs when y is 0 and we run the next instruction?

jmp(y_dec, "measure_echo_loop")

In the event you guessed that it does not leap to measure_echo_loop and as an alternative falls by means of to the subsequent instruction, you are completely appropriate. However for full credit score, reply this: What worth does y have after the instruction?

The reply: 4,294,967,295. Why? As a result of y is decremented after it’s examined for zero. Wat!?

This worth, 4,294,967,295, is the utmost for a 32-bit unsigned integer. It’s as if a track-and-field lengthy jumper launches off the takeoff board however, as an alternative of touchdown within the sandpit, overshoots and finally ends up on one other continent.

Apart: As foreshadowed in Wat 5, we will use this conduct deliberately to set a register to the worth 4,294,967,295.

Now that we’ve discovered how one can stick the touchdown with jmp, let’s see if we will keep away from getting caught by the pins that PIO reads and units.

In Dr. Seuss’s Too Many Daves, Mrs. McCave had 23 sons, all named Dave, resulting in infinite confusion at any time when she referred to as out their title. In PIO programming, pin and pins can consult with fully completely different ranges of pins relying on the context. It’s arduous to know which Dave or Daves you are speaking to.

The Downside: Pin Ranges and Bases

In PIO, each pin and pins rely on base pins outlined exterior this system. Every instruction interacts with a selected base pin, and a few directions additionally function on a spread of pins ranging from that base. To make clear PIO’s conduct, I created this desk:

Desk exhibiting how PIO interprets ‘pin’ and ‘pins’ in numerous directions, with their related contexts and configurations.

Instance: Distance Program for the Vary Finder

Right here’s a PIO program for measuring the gap to an object utilizing Set off and Echo pins. The important thing options of this program are:

  • Steady Operation: The vary finder runs in a loop as quick as doable.
  • Most Vary Restrict: Measurements are capped at a given distance, with a return worth of 4,294,967,295 if no object is detected.
  • Filtered Outputs: Solely measurements that differ from their speedy predecessor are despatched, lowering the output fee.

Look over this system and spot that though it’s working with two pins — Set off and Echo — all through this system we solely see pin and pins.

import rp2

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def distance():
# X is the final worth despatched. Initialize it to
# u32::MAX which implies 'echo timeout'
# (Set X to u32::MAX by subtracting 1 from 0)
set(x, 0)
label("subtraction_trick")
jmp(x_dec, "subtraction_trick")

# Learn the max echo wait into OSR.
pull() # similar as pull(block)

# Essential loop
wrap_target()

# Generate 10μs set off pulse (4 cycles at 343_000Hz)
set(pins, 0b1)[3] # Set set off pin to excessive, add delay of three
set(pins, 0b0) # Set set off pin to low voltage

# When the set off goes excessive, begin counting down till it goes low
wait(1, pin, 0) # Look forward to echo pin to be excessive voltage
mov(y, osr) # Load max echo wait into Y

label("measure_echo_loop")
jmp(pin, "echo_active") # if echo voltage is excessive proceed depend down
jmp("measurement_complete") # if echo voltage is low, measurement is full
label("echo_active")
jmp(y_dec, "measure_echo_loop") # Proceed counting down until timeout

# Y tells the place the echo countdown stopped. It
# might be u32::MAX if the echo timed out.
label("measurement_complete")
jmp(x_not_y, "send_result") # if measurement is completely different, then despatched it.
jmp("cooldown") # If measurement is similar, do not ship.
# Ship the measurement
label("send_result")
mov(isr, y) # Retailer measurement in ISR
push() # Output ISR
mov(x, y) # Save the measurement in X

# Quiet down interval earlier than subsequent measurement
label("cooldown")
wait(0, pin, 0) # Look forward to echo pin to be low
wrap() # Restart the measurement loop

Configuring Pins

To make sure the PIO program behaves as supposed:

  • set(pins, 0b1) ought to management the Set off pin.
  • wait(1, pin, 0) ought to monitor the Echo pin.
  • jmp(pin, "echo_active") must also monitor the Echo pin.

Right here’s how one can configure this in MicroPython:

ECHO_PIN = 16
TRIGGER_PIN = 17

echo = Pin(ECHO_PIN, Pin.IN)
distance_state_machine = rp2.StateMachine(
4, # PIO Block 1, State machine 4
distance, # PIO program
freq=state_machine_frequency,
in_base=echo,
set_base=Pin(TRIGGER_PIN, Pin.OUT),
jmp_pin=echo,
)

The important thing right here is the elective in_base, set_base, and jmp_pin inputs to the StateMachine constructor:

  • in_base: Specifies the beginning pin for enter operations, equivalent to wait(1, pin, ...).
  • set_base: Configures the beginning pin for set operations, like set(pins, 1).
  • jmp_pin: Defines the pin utilized in conditional jumps, equivalent to jmp(pin, ...).

As described within the desk, different elective inputs embody:

  • out_base: Units the beginning pin for output operations, equivalent to out(pins, ...).
  • sideset_base: Configures the beginning pin for sideset operations, which permit simultaneous pin toggling throughout different directions.

Configuring A number of Pins

Though not required for this program, you possibly can configure a spread of pins in PIO utilizing a tuple that specifies the preliminary states for every pin. In contrast to what you would possibly anticipate, the vary isn’t outlined by specifying a base pin and a depend (or finish). As an alternative, the tuple determines the pins’ preliminary values and implicitly units the vary, ranging from the set_base pin.

For instance, the next PIO decorator configures two pins with preliminary states of OUT_LOW:

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW, rp2.PIO.OUT_LOW))

If set_base is ready to pin 17, this tuple designates pin 17 and the subsequent consecutive pin (pin 18) as “set pins.” A single instruction can then management each pins:

set(pins, 0b11)[3]  # Units each set off pins (17, 18) excessive, provides delay
set(pins, 0b00) # Units each set off pins low

This method permits you to effectively apply bit patterns to a number of pins concurrently, streamlining management for functions involving a number of outputs.

Apart: The Phrase “Set” in Programming
In programming, the phrase “set” is notoriously overloaded with a number of meanings. Within the context of PIO, “set” refers to one thing to which you’ll assign a worth — equivalent to a pin’s state. It does not imply a group of issues, because it usually does in different programming contexts. When PIO refers to a group, it often makes use of the time period “vary” as an alternative. This distinction is essential for avoiding confusion as you’re employed with PIO.

Classes from Mrs. McCave

In Too Many Daves, Mrs. McCave lamented not giving her 23 Daves extra distinct names. You possibly can keep away from her mistake by clearly documenting your pins with significant names — like Set off and Echo — in your feedback.

However in case you assume dealing with these pin ranges is difficult, debugging a PIO program provides a completely new layer of problem. Within the subsequent Wat, we’ll dive into the kludgy debugging strategies obtainable. Let’s see simply how far we will push them.

I wish to debug with interactive breakpoints in VS Code. MicroPython doesn’t help that.

The fallback is print debugging, the place you insert non permanent print statements to see what the code is doing and the values of variables. MicroPython helps this, however PIO doesn’t.

The fallback to the fallback is push-to-print debugging. In PIO, you briefly output integer values of curiosity. Then, in MicroPython, you print these values for inspection.

For instance, within the following PIO program, we briefly add directions to push the worth of x for debugging. We additionally embody set and out to push a continuing worth, equivalent to 7, which should be between 0 and 31 inclusive.

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def distance():
# X is the final worth despatched. Initialize it to
# u32::MAX which implies 'echo timeout'
# (Set X to u32::MAX by subtracting 1 from 0)
set(x, 0)
label("subtraction_trick")
jmp(x_dec, "subtraction_trick")

# DEBUG: See the worth of X
mov(isr, x)
push()

# Learn the max echo wait into OSR.
pull() # similar as pull(block)

# DEBUG: Ship fixed worth
set(y, 7) # Push '7' so we all know we have reached this level
mov(isr, y)
push()
# ...

Again in MicroPython, you possibly can learn and print these values to assist perceive what’s taking place within the PIO code:

import rp2
from machine import Pin

from distance_debug_pio import distance

def demo_debug():
print("Hi there, debug!")
pio1 = rp2.PIO(1)
pio1.remove_program()
echo = Pin(16, Pin.IN)
distance_state_machine = rp2.StateMachine(
4, distance, freq=343_000, in_base=echo, set_base=Pin(17, Pin.OUT), jmp_pin=echo
)
attempt:
distance_state_machine.energetic(1) # Begin the PIO state machine
distance_state_machine.put(500)
whereas True:
end_loops = distance_state_machine.get()
print(end_loops)
besides KeyboardInterrupt:
print("distance demo stopped.")
lastly:
distance_state_machine.energetic(0)

demo_debug()

Outputs:

Hi there, debug!
4294967295
7

When push-to-print debugging isn’t sufficient, you possibly can flip to {hardware} instruments. I purchased my first oscilloscope (a FNIRSI DSO152, for $37). With it, I used to be capable of affirm the Echo sign was working. The Set off sign, nevertheless, was too quick for this cheap oscilloscope to seize clearly.

Utilizing these strategies — particularly push-to-print debugging — you possibly can hint the circulate of your PIO program, even with no conventional debugger.

Apart: In C/C++ (and probably Rust), you will get nearer to a full debugging expertise for PIO, for instance, through the use of the piodebug mission.

That concludes the 9 Wats, however let’s carry every little thing collectively in a bonus Wat.

Now that every one the elements are prepared, it’s time to mix them right into a working theremin-like musical instrument. We’d like a MicroPython monitor program. This program begins each PIO state machines — one for measuring distance and the opposite for producing tones. It then waits for a brand new distance measurement, maps that distance to a tone, and sends the corresponding tone frequency to the tone-playing state machine. If the gap is out of vary, it stops the tone.

MicroPython’s Place: On the coronary heart of this method is a perform that maps distances (from 0 to 50 cm) to tones (roughly B2 to F5). This perform is easy to put in writing in MicroPython, leveraging Python’s floating-point math and exponential operations. Implementing this in PIO can be just about unimaginable attributable to its restricted instruction set and lack of floating-point help.

Right here’s the monitor program to run the theremin:

import math

import machine
import rp2
from machine import Pin

from distance_pio import distance
from sound_pio import sound

BUZZER_PIN = 15
ECHO_PIN = 16
TRIGGER_PIN = 17

CM_MAX = 50
CM_PRECISION = 0.1
LOWEST_TONE_FREQUENCY = 123.47 # B2
OCTAVE_COUNT = 2.5 # to F5

def theremin():
print("Hi there, theremin!")

pio0 = rp2.PIO(0)
pio0.remove_program()
sound_state_machine_frequency = machine.freq()
sound_state_machine = rp2.StateMachine(0, sound, set_base=Pin(BUZZER_PIN))

pio1 = rp2.PIO(1)
pio1.remove_program()
echo = Pin(ECHO_PIN, Pin.IN)
distance_state_machine_frequency = int(2 * 34300.0 / CM_PRECISION / 2.0)
distance_state_machine = rp2.StateMachine(
4,
distance,
freq=distance_state_machine_frequency,
set_base=Pin(TRIGGER_PIN, Pin.OUT),
in_base=echo,
jmp_pin=echo,
)
max_loops = int(CM_MAX / CM_PRECISION)

attempt:
sound_state_machine.energetic(1)
distance_state_machine.energetic(1)
distance_state_machine.put(max_loops)

whereas True:
end_loops = distance_state_machine.get()
distance_cm = loop_difference_to_distance_cm(max_loops, end_loops)
if distance_cm is None:
sound_state_machine.put(0)
else:
tone_frequency = distance_to_tone_frequency(distance_cm)
print(f"Distance: {distance_cm} cm, tone: {tone_frequency} Hz")
half_period = int(sound_state_machine_frequency / (2 * tone_frequency))
sound_state_machine.put(half_period)
besides KeyboardInterrupt:
print("theremin stopped.")
lastly:
sound_state_machine.energetic(0)
distance_state_machine.energetic(0)

def loop_difference_to_distance_cm(max_loops, end_loops):
if end_loops == 0xFFFFFFFF:
return None
distance_cm = (max_loops - end_loops) * CM_PRECISION
return distance_cm

def distance_to_tone_frequency(distance):
return LOWEST_TONE_FREQUENCY * 2.0 ** ((distance / CM_MAX) * OCTAVE_COUNT)

theremin()

Discover how utilizing two PIO state machines and a MicroPython monitor program lets us run three applications directly. This method combines simplicity with responsiveness, reaching a degree of efficiency that will in any other case be tough to appreciate in MicroPython alone.

Now that we’ve assembled all of the elements, let’s watch the video once more of me “enjoying” the musical instrument. On the monitor display, you possibly can see the debugging prints displaying the gap measurements and the corresponding tones. This visible connection highlights how the system responds in actual time.

PIO programming on the Raspberry Pi Pico is a fascinating mix of simplicity and complexity, providing unparalleled {hardware} management whereas demanding a shift in mindset for builders accustomed to higher-level programming. By way of the 9 Wats we’ve explored, PIO has each shocked us with its limitations and impressed us with its uncooked effectivity.

Whereas we’ve lined vital floor — managing state machines, pin assignments, timing intricacies, and debugging — there’s nonetheless far more you possibly can be taught as wanted: DMA, IRQ, side-set pins, variations between PIO on the Pico 1 and Pico 2, autopush and autopull, FIFO be part of, and extra.

Really useful Assets

At its core, PIO’s quirks replicate a design philosophy that prioritizes low-level {hardware} management with minimal overhead. By embracing these traits, PIO is not going to solely meet your mission’s calls for but in addition open doorways to new potentialities in embedded techniques programming.

Please comply with Carl on Medium. I write on scientific programming in Rust and Python, machine studying, and statistics. I have a tendency to put in writing about one article monthly.