Raspberry Pi programmable IO pitfalls illustrated with a musical instance
Additionally obtainable: A MicroPython model of this text
In JavaScript and different languages, we name a shocking or inconsistent habits a “Wat!” [that is, a “What!?”]. For instance, in JavaScript, an empty array plus an empty array produces an empty string, [] + [] === ""
. Wat!
Rust, by comparability, is constant and predictable. Nevertheless, one nook of Rust on the Raspberry Pi Pico microcontroller gives related surprises. Particularly, the Pico’s Programmable Enter/Output (PIO) subsystem, whereas extremely highly effective and versatile, comes with peculiarities.
PIO programming issues as a result of it gives an ingenious resolution to the problem of exact, low-level {hardware} management. It’s extremely quick and versatile: slightly than counting on special-purpose {hardware} for the numerous peripherals you would possibly wish to management, PIO means that you can outline customized behaviors in software program, seamlessly adapting to your wants with out including {hardware} complexity.
Think about this straightforward instance: a $15 theremin-like musical instrument. By waving their hand within the air, the musician adjustments the pitch of (admittedly annoying) tones. Utilizing PIO gives a easy approach to program this machine that ensures it reacts immediately to motion.
So, all is fantastic, besides — to paraphrase Spider-Man:
With nice energy comes… 9 Wats!?
We’ll discover and illustrate these 9 PIO Wats by means of the creation of this theremin.
Who Is This Article For?
- All Programmers: Microcontrollers just like the Pico value underneath $7 and assist high-level languages like Python, Rust, and C/C++. This text will present how microcontrollers let your packages work together with the bodily world and introduce you to programming the Pico’s low-level, high-performance PIO {hardware}.
- Rust Pico Programmers: Curious concerning the Pico’s hidden potential? Past its two primary cores, it has eight tiny “state machines” devoted to PIO programming. These state machines take over time-critical duties, releasing up the principle processors for different work and enabling shocking parallelism.
- C/C++ Pico Programmers: Whereas this text makes use of Rust, PIO programming is — for good and dangerous — almost similar throughout all languages. When you perceive it right here, you’ll be well-equipped to use it in C/C++.
- MicroPython Pico Programmers: Chances are you’ll want to learn the MicroPython model of this text.
- PIO Programmers: The journey by means of 9 Wats might not be as entertaining as JavaScript’s quirks (fortunately), however it can make clear the peculiarities of PIO programming. When you’ve ever discovered PIO programming complicated, this text ought to reassure you that the issue isn’t (essentially) you — it’s partly PIO itself. Most significantly, understanding these Wats will make writing PIO code easier and simpler.
Lastly, this text isn’t about “fixing” PIO programming. PIO excels at its main goal: effectively and flexibly dealing with customized peripheral interfaces. Its design is purposeful and well-suited to its objectives. As a substitute, this text focuses on understanding PIO programming and its quirks — beginning with a bonus Wat.
Regardless of their identify, the eight “PIO state machines” within the Raspberry Pi Pico are usually not state machines within the formal laptop science sense. As a substitute, they’re tiny programmable processors with their very own assembly-like instruction set, able to looping, branching, and conditional operations. In actuality, they’re nearer to Harvard structure machines or, like most sensible computer systems, von Neumann machines.
Every state machine processes one instruction per clock cycle. The $4 Pico 1 runs at 125 million cycles per second, whereas the $5 Pico 2 gives a sooner 150 million cycles per second. Every instruction performs a easy operation, equivalent to “transfer a worth” or “leap to a label”.
With that bonus Wat out of the best way, let’s transfer to our first primary Wat.
In PIO programming, a register is a small, quick storage location that acts like a variable for the state machine. You would possibly dream of an abundance of variables to carry your counters, delays, and non permanent values, however the actuality is brutal: you solely get two general-purpose registers, x
and y
. It is like The Starvation Video games, the place irrespective of what number of tributes enter the sector, solely Katniss and Peeta emerge as victors. You’re compelled to winnow down your wants to suit inside these two registers, ruthlessly deciding what to prioritize and what to sacrifice. Additionally, just like the Starvation Video games, we are able to typically bend the principles.
Let’s begin with a problem: create a backup beeper — 1000 Hz for ½ second, silence for ½ second, repeat. The outcome? “Beep Beep Beep…”
We wish 5 variables:
half_period:
The variety of clock cycles to carry the voltage excessive after which low to create a 1000 Hz tone. That is 125,000,000 / 1000 / 2 = 62,500 cycles excessive and 62, 500 cycles low.
y
: Loop counter from 0 tohalf_period
to create a delay.period_count
: The variety of repeated intervals wanted to fill ½ second of time. 125,000,000 × 0.5 / (62,500 × 2) = 500.x
: Loop counter from 0 toperiod_count
to fill ½ second of time.silence_cycles
: The variety of clock cycles for ½ second of silence. 125,000,000 × 0.5 = 62,500,000.
We would like 5 registers however can solely have two, so let the video games start! Might the chances be ever in your favor.
First, we are able to remove silence_cycles
as a result of it may be derived as half_period × period_count × 2
. Whereas PIO doesn’t assist multiplication, it does assist loops. By nesting two loops—the place the interior loop delays for two clock cycles—we are able to create a delay of 62,500,000 clock cycles.
One variable down, however how can we remove two extra? Thankfully, we don’t must. Whereas PIO solely gives two general-purpose registers, x
and y
, it additionally contains two special-purpose registers: osr
(output shift register) and isr
(enter shift register).
The PIO code that we’ll see in a second implements the backup beeper. Right here’s the way it works:
Initialization:
- The
pull block
instruction reads the half interval of the tone (62,500 clock cycles) from a buffer and locations the worth intoosr
. - The worth is then copied to
isr
for later use. - The second
pull block
reads the interval depend (500 repeats) from the buffer and locations the worth inosr
, the place we go away it.
Beep Loops:
- The
mov x, osr
instruction copies the interval depend into thex
register, which serves because the outer loop counter. - For the interior loops,
mov y, isr
repeatedly copies the half interval intoy
to create delays for the excessive and low states of the tone.
Silence Loops:
- The silence loops mirror the construction of the beep loops however don’t set any pins, so that they act solely as a delay.
Wrap and Steady Execution:
- The
.wrap_target
and.wrap
directives outline the principle loop of the state machine. - After ending each the beep and silence loops, the state machine jumps again close to the beginning of this system, repeating the sequence indefinitely.
With this define in thoughts, right here’s the PIO meeting code for producing the backup beeper sign.
.program backup; Learn preliminary configuration
pull block ; Learn the half interval of the beep sound
mov isr, osr ; Retailer the half interval in ISR
pull block ; Learn the period_count
.wrap_target ; Begin of the principle loop
; Generate the beep sound
mov x, osr ; Load period_count into X
beep_loop:
set pins, 1 ; Set the buzzer to excessive voltage (begin the tone)
mov y, isr ; Load the half interval into Y
beep_high_delay:
jmp y--, beep_high_delay ; Delay for the half interval
set pins, 0 ; Set the buzzer to low voltage (finish the tone)
mov y, isr ; Load the half interval into Y
beep_low_delay:
jmp y--, beep_low_delay ; Delay for the low length
jmp x--, beep_loop ; Repeat the beep loop
; Silence between beeps
mov x, osr ; Load the interval depend into X for outer loop
silence_loop:
mov y, isr ; Load the half interval into Y for interior loop
silence_delay:
jmp y--, silence_delay [1] ; Delay for 2 clock cycles (jmp + 1 additional)
jmp x--, silence_loop ; Repeat the silence loop
.wrap ; Finish of the principle loop, jumps again to wrap_target
Right here’s the core Rust code to configure and run the PIO program for the backup beeper. It makes use of the Embassy framework for embedded purposes. The perform initializes the state machine, calculates the timing values (half_period
and period_count
), and sends them to the PIO. It then performs the beeping sequence for five seconds earlier than coming into an countless loop. The complete supply file and undertaking can be found on GitHub.
async fn inner_main(_spawner: Spawner) -> End result<By no means> {
data!("Howdy, back_up!");
let {hardware}: {Hardware}<'_> = {Hardware}::default();
let mut pio0 = {hardware}.pio0;
let state_machine_frequency = embassy_rp::clocks::clk_sys_freq();
let mut back_up_state_machine = pio0.sm0;
let buzzer_pio = pio0.widespread.make_pio_pin({hardware}.buzzer);
back_up_state_machine.set_pin_dirs(Path::Out, &[&buzzer_pio]);
back_up_state_machine.set_config(&{
let mut config = Config::default();
config.set_set_pins(&[&buzzer_pio]); // For set instruction
let program_with_defines = pio_file!("examples/backup.pio");
let program = pio0.widespread.load_program(&program_with_defines.program);
config.use_program(&program, &[]);
config
});back_up_state_machine.set_enable(true);
let half_period = state_machine_frequency / 1000 / 2;
let period_count = state_machine_frequency / (half_period * 2) / 2;
data!(
"Half interval: {}, Interval depend: {}",
half_period, period_count
);
back_up_state_machine.tx().wait_push(half_period).await;
back_up_state_machine.tx().wait_push(period_count).await;
Timer::after(Length::from_millis(5000)).await;
data!("Disabling back_up_state_machine");
back_up_state_machine.set_enable(false);
// run perpetually
loop {
Timer::after(Length::from_secs(3_153_600_000)).await; // 100 years
}
}
Right here’s what occurs if you run this system:
Apart 1: Working this your self
The only — however usually irritating — approach to run Rust code on the Pico is to cross-compile it in your desktop and manually copy over the ensuing information. A significantly better method is to spend money on a $12 Raspberry Pi Debug Probe and arrange probe-rs in your desktop. With this setup, you need to usecargo run
to routinely compile in your desktop, copy to your Pico, after which begin your code operating. Even higher, your Pico code can usedata!
statements to ship messages again to your desktop, and you may carry out interactive breakpoint debugging. For setup directions, go to the probe-rs web site.To listen to sound, I linked a passive buzzer, a resistor, and a transistor to the Pico. For detailed wiring diagrams and a elements checklist, try the passive buzzer directions within the SunFounder’s Kepler Package.
Apart 2: In case your solely purpose is to generate tones with the Pico, PIO isn’t crucial. MicroPython is quick sufficient to toggle pins straight, or you need to use the Pico’s built-in pulse width modulation (PWM) characteristic.
Various Endings to the Register Starvation Video games
We used 4 registers — two basic and two particular — to resolve the problem. If this resolution feels lower than satisfying, listed here are different approaches to think about:
Use Constants: Why make half_period
, period_count
, and silence_cycles
variables in any respect? Hardcoding the constants “62,500,” “500,” and “62,500,000” might simplify the design. Nevertheless, PIO constants have limitations, which we’ll discover in Wat 5.
Pack Bits: Registers maintain 32 bits. Do we actually want two registers (2×32=64 bits) to retailer half_period
and period_count
? No. Storing 62,500 solely requires 16 bits, and 500 requires 9 bits. We might pack these right into a single register and use the out
instruction to shift values into x
and y
. This method would release both osr
or isr
for different duties, however solely one after the other—the opposite register should maintain the packed worth.
Sluggish Movement: In Rust with the Embassy framework, you’ll be able to configure a PIO state machine to run at a slower frequency by setting its clock_divider
. This enables the state machine to run as sluggish as ~1907 Hz. Working the state machine at a slower pace signifies that values like half_period
may be smaller, doubtlessly as small as 2
. Small values are simpler to hardcode as constants and extra compactly bit-packed into registers.
A Joyful Ending to the Register Starvation Video games
The Register Starvation Video games demanded strategic sacrifices and inventive workarounds, however we emerged victorious by leveraging PIO’s particular registers and intelligent looping constructions. If the stakes had been increased, different strategies might have helped us adapt and survive.
However victory in a single enviornment doesn’t imply the challenges are over. Within the subsequent Wat, we face a brand new trial: PIO’s strict 32-instruction restrict.
Congratulations! You’ve bought a visit all over the world for simply $4. The catch? All of your belongings should match right into a tiny carry-on suitcase. Likewise, PIO packages permit you to create unbelievable performance, however each PIO program is proscribed to only 32 directions.
Wat! Solely 32 directions? That’s not a lot house to pack every little thing you want! However with intelligent planning, you’ll be able to normally make it work.
- No PIO program may be longer than 32 directions.
- The
wrap_target
andwrap
directives don’t depend. - Labels don’t depend.
- A Pico 1 contains eight state machines, organized into two blocks of 4. A Pico 2 contains twelve state machines, organized into three blocks of 4. Every block shares 32 instruction slots. So, as a result of all 4 state machines in a block draw from the identical 32-instruction pool, if one machine’s program makes use of all 32 slots, there’s no house left for the opposite three.
In case your concept doesn’t match within the PIO instruction slots, these packing tips might assist. (Disclaimer: I haven’t tried all of those myself.)
- Swap PIO Packages on the Fly:
As a substitute of making an attempt to cram every little thing into one program, contemplate swapping out packages mid-flight. Load solely what you want, if you want it. - Share Packages Throughout State Machines:
A number of state machines can run the identical program on the similar time. Every state machine could make the shared program behave in another way primarily based on an enter worth. - Use Rust/Embassy’s
exec_instr
Command:
Save house by offloading directions to Rust. For instance, you’ll be able to execute initialization steps earlier than enabling the state machine:
let half_period = state_machine_frequency / 1000 / 2;
back_up_state_machine.tx().push(half_period); // Utilizing non-blocking push since FIFO is empty
let pull_block = pio_asm!("pull block").program.code[0];
unsafe {
back_up_state_machine.exec_instr(pull_block);
}
- Use PIO’s
exec
instructions:
Inside your state machine, you’ll be able to dynamically execute directions utilizing PIO’sexec
mechanism. For instance, you’ll be able to execute an instruction worth saved inosr
without exec
. Alternatively, you need to usemov exec, x
ormov exec, y
to execute directions straight from these registers. - Offload to the Important Processors:
If all else fails, transfer extra of your program to the Pico’s bigger twin processors — consider this as delivery your additional baggage to your vacation spot individually. The Pico SDK (part 3.1.4) calls this “bit banging”.
Together with your baggage now packed, let’s be a part of Dr. Dolittle’s seek for a fabled creature.
Two readers identified an necessary PIO Wat that I missed — so right here’s a bonus! When programming PIO, you’ll discover one thing peculiar:
- The PIO
pull
instruction receives values from TX FIFO (transmit buffer) and inputs them into the output shift register (osr
). So, it inputs into output and transmits from obtain. - Likewise, the PIO
push
instruction outputs values from the enter shift register (isr
) and transmits them to the RX FIFO (obtain buffer). So, it outputs from enter and receives from transmit.
Wat!? Just like the two-headed Pushmi-Pullyu from the Dr. Dolittle tales, one thing appears backwards. However it begins to make sense if you notice PIO names most issues from the host’s perspective (MicroPython, Rust, C/C++), not the standpoint of the PIO program.
This desk summarizes the directions, registers, and buffer names. (“FIFO” stands for first-in-first-out.)
With the Pushmi-Pullyu in hand, we subsequent transfer to the scene of a thriller.
In Wat 1, we programmed our audio {hardware} as a backup beeper. However that’s not what we’d like for our musical instrument. As a substitute, we would like a PIO program that performs a given tone indefinitely — till it’s instructed to play a brand new one. This system must also wait silently when given a particular “relaxation” tone.
Resting till a brand new tone is offered is simple to program with pull block
—we’ll discover the small print beneath. Taking part in a tone at a selected frequency can also be simple, constructing on the work we did in Wat 1.
However how can we test for a brand new tone whereas persevering with to play the present one? The reply lies in utilizing “noblock” as an alternative of “block” in pull noblock
. Now, if there’s a brand new worth, it is going to be loaded into osr
, permitting this system to replace seamlessly.
Right here’s the place the thriller begins: what occurs to osr
if pull noblock
is known as and there’s no new worth?
I assumed it could maintain its earlier worth. Flawed! Possibly it will get reset to 0? Flawed once more! The shocking fact: it will get the worth of x
. Why? (No, not y
— x
.) As a result of the Pico SDK says so. Particularly, part 3.4.9.2 explains:
A nonblocking PULL on an empty FIFO has the identical impact as MOV OSR, X.
Realizing how pull noblock
works is necessary, however there’s an even bigger lesson right here. Deal with the Pico SDK documentation just like the again of a thriller novel. Don’t attempt to remedy every little thing by yourself—cheat! Skip to the “who performed it” part, and in part 3.4, learn the advantageous particulars for every command you utilize. Studying just some paragraphs can prevent hours of confusion.
Apart: When even the SDK documentation feels unclear, flip to the RP2040 (Pico 1) and RP2350 (Pico 2) datasheets. These encyclopedias — 600 and 1,300 pages respectively — are like all-powerful narrators: they supply the bottom fact.
With this in thoughts, let’s take a look at a sensible instance. Beneath is the PIO program for enjoying tones and rests constantly. It makes use of pull block
to attend for enter throughout a relaxation and pull noblock
to test for updates whereas enjoying a tone.
.program sound; Relaxation till a brand new tone is acquired.
resting:
pull block ; Look ahead to a brand new delay worth
mov x, osr ; Copy delay into X
jmp !x resting ; If delay is zero, maintain resting
; Play the tone till a brand new delay is acquired.
.wrap_target ; Begin of the principle loop
set pins, 1 ; Set the buzzer excessive voltage.
high_voltage_loop:
jmp x-- high_voltage_loop ; Delay
set pins, 0 ; Set the buzzer low voltage.
mov x, osr ; Load the half interval into X.
low_voltage_loop:
jmp x-- low_voltage_loop ; Delay
; Learn any new delay worth. If none, maintain the present delay.
mov x, osr ; set x, the default worth for "pull(noblock)"
pull noblock ; Learn a brand new delay worth or use the default.
; If the brand new delay is zero, relaxation. In any other case, proceed enjoying the tone.
mov x, osr ; Copy the delay into X.
jmp !x resting ; If X is zero, relaxation.
.wrap ; Proceed enjoying the sound.
We’ll ultimately use this PIO program in our theremin-like musical instrument. For now, let’s see the PIO program in motion by enjoying a well-recognized melody. This demo makes use of “Twinkle, Twinkle, Little Star” to indicate how one can management a melody by feeding frequencies and durations to the state machine. With simply this code (full file and undertaking), you may make the Pico sing!
const TWINKLE_TWINKLE: [(u32, u64, &str); 16] = [
// Bar 1
(262, 400, "Twin-"), // C
(262, 400, "-kle"), // C
(392, 400, "twin-"), // G
(392, 400, "-kle"), // G
(440, 400, "lit-"), // A
(440, 400, "-tle"), // A
(392, 800, "star"), // G
(0, 400, ""), // rest
// Bar 2
(349, 400, "How"), // F
(349, 400, "I"), // F
(330, 400, "won-"), // E
(330, 400, "-der"), // E
(294, 400, "what"), // D
(294, 400, "you"), // D
(262, 800, "are"), // C
(0, 400, ""), // rest
];async fn inner_main(_spawner: Spawner) -> End result<By no means> {
data!("Howdy, sound!");
let {hardware}: {Hardware}<'_> = {Hardware}::default();
let mut pio0 = {hardware}.pio0;
let state_machine_frequency = embassy_rp::clocks::clk_sys_freq();
let mut sound_state_machine = pio0.sm0;
let buzzer_pio = pio0.widespread.make_pio_pin({hardware}.buzzer);
sound_state_machine.set_pin_dirs(Path::Out, &[&buzzer_pio]);
sound_state_machine.set_config(&{
let mut config = Config::default();
config.set_set_pins(&[&buzzer_pio]); // For set instruction
let program_with_defines = pio_file!("examples/sound.pio");
let program = pio0.widespread.load_program(&program_with_defines.program);
config.use_program(&program, &[]);
config
});
sound_state_machine.set_enable(true);
for (frequency, ms, lyrics) in TWINKLE_TWINKLE.iter() {
if *frequency > 0 {
let half_period = state_machine_frequency / frequency / 2;
data!("{} -- Frequency: {}", lyrics, frequency);
// Ship the half interval to the PIO state machine
sound_state_machine.tx().wait_push(half_period).await;
Timer::after(Length::from_millis(*ms)).await; // Wait because the tone performs
sound_state_machine.tx().wait_push(0).await; // Cease the tone
Timer::after(Length::from_millis(50)).await; // Give a brief pause between notes
} else {
sound_state_machine.tx().wait_push(0).await; // Play a silent rust
Timer::after(Length::from_millis(*ms + 50)).await; // Look ahead to the remainder length + a brief pause
}
}
data!("Disabling sound_state_machine");
sound_state_machine.set_enable(false);
// run perpetually
loop {
Timer::after(Length::from_secs(3_153_600_000)).await; // 100 years
}
}
Right here’s what occurs if you run this system:
We’ve solved one thriller, however there’s all the time one other problem lurking across the nook. In Wat 4, we’ll discover what occurs when your sensible {hardware} comes with a catch — it’s additionally very low-cost.
With sound working, we flip subsequent to measuring the space to the musician’s hand utilizing the HC-SR04+ ultrasonic vary finder. This small however highly effective machine is accessible for lower than two {dollars}.
HC-SR04+ Vary Finder (Pen added for scale.)
This little peripheral took me on an emotional curler coaster of “Wats!?”:
- Up: Amazingly, this $2 vary finder contains its personal microcontroller, making it smarter and simpler to make use of.
- Down: Frustratingly, that very same “sensible” habits is unintuitive.
- Up: Conveniently, the Pico can provide peripherals with both 3.3V or 5V energy.
- Down: Unpredictably, many vary finders are unreliable — or fail outright — at 3.3V, they usually can injury your Pico at 5V.
- Up: Fortunately, each broken vary finders and Picos are cheap to interchange, and a dual-voltage model of the vary finder solved my issues.
Particulars
I initially assumed the vary finder would set the Echo pin excessive when the echo returned. I used to be improper.
As a substitute, the vary finder emits a sample of 8 ultrasonic pulses at 40 kHz (consider it as a backup beeper for canine). Instantly after, it units Echo excessive. The Pico ought to then begin measuring the time till Echo goes low, which alerts that the sensor detected the sample — or that it timed out.
As for voltage, the documentation specifies the vary finder operates at 5V. It appeared to work at 3.3V — till it didn’t. Across the similar time, whereas my Pico stored working with Rust (by way of the Debug Probe and probe-rs), it stopped working with any of the MicroPython IDEs, which depend on a particular USB protocol.
So, at this level each the Pico and the vary finder have been broken.
After experimenting with numerous cables, USB drivers, programming languages, and even an older 5V-only vary finder, I lastly resolved the difficulty by:
Wat 4: Classes Discovered
Because the curler coaster return to the station, I realized two key classes. First, because of microcontrollers, even easy {hardware} can behave in non-intuitive ways in which require cautious studying of the documentation. Second, whereas this {hardware} is intelligent, it’s additionally cheap — and meaning it’s liable to failure. When it fails, take a deep breath, bear in mind it’s just a few {dollars}, and substitute it.
{Hardware} quirks, nonetheless, are solely a part of the story. In Wat 5, in Half 2, we’ll shift our focus again to software program: the PIO programming language itself. We’ll uncover a habits so surprising, it would go away you questioning every little thing you thought you knew about constants.