Nine Pico PIO Wats with MicroPython (Part 1)

Raspberry Pi programmable IO pitfalls illustrated with a musical examplePico PIO Surprises — Source: https://openai.com/dall-e-2/. All other figures from the author.In JavaScript and other languages, we call a surprising or inconsistent behavior a “Wat!” [that is, a “What!?”]. For example, in JavaScript, an empty array plus an empty array produces an empty string, [] + [] === "". Wat!Python, by comparison, is more consistent and predictable. However, one corner of MicroPython on the Raspberry Pi Pico microcontroller offers similar surprises. Specifically, the Pico’s Programmable Input/Output (PIO) subsystem, while incredibly powerful and versatile, comes with peculiarities.PIO programming matters because it provides an ingenious solution to the challenge of precise, low-level hardware control. It is incredibly fast — even when programmed from MicroPython, PIO performs just as efficiently as it does from Rust or C/C++. Its flexibility is unparalleled: rather than relying on special-purpose hardware for the countless peripherals you might want to control, PIO allows you to define custom behaviors in software, seamlessly adapting to your needs without adding hardware complexity.Consider this simple example: a $15 theremin-like musical instrument. By waving their hand in the air, the musician changes the pitch of (admittedly annoying) tones. Using PIO provides a simple way to program this device that ensures it reacts instantly to movement.https://medium.com/media/dc482ceb88d9def2e9c46e2dbd60fe35/hrefSo, all is wonderful, except — to paraphrase Spider-Man:With great power comes… nine Wats!?We’ll explore and illustrate those nine PIO Wats through the creation of this theremin.Who Is This Article For?All Programmers: Microcontrollers like the Pico cost under $7 and support high-level languages like Python, Rust, and C/C++. This article will show how microcontrollers let your programs interact with the physical world and introduce you to programming the Pico’s low-level, high-performance PIO hardware.MicroPython Pico Programmers: Curious about the Pico’s hidden potential? Beyond its two main cores, it has eight tiny “state machines” dedicated to PIO programming. These state machines take over time-critical tasks, freeing up the main processors for other work and enabling surprising parallelism.Rust and C/C++ Pico Programmers: While this article uses MicroPython, PIO programming is — for good and bad — nearly identical across all languages. If you understand it here, you’ll be well-equipped to apply it in Rust or C/C++.PIO Programmers: The journey through nine Wats may not be as entertaining as JavaScript’s quirks (thankfully), but it will shed light on the peculiarities of PIO programming. If you’ve ever found PIO programming confusing, this article should reassure you that the problem isn’t (necessarily) you — it’s partly PIO itself. Most importantly, understanding these Wats will make writing PIO code simpler and more effective.Finally, this article isn’t about “fixing” PIO programming. PIO excels at its primary purpose: efficiently and flexibly handling custom peripheral interfaces. Its design is purposeful and well-suited to its goals. Instead, this article focuses on understanding PIO programming and its quirks — starting with a bonus Wat.Bonus Wat 0: “State Machines” Are Not State MachinesDespite their name, the eight “PIO state machines” in the Raspberry Pi Pico are not state machines in the formal computer science sense. Instead, they are tiny programmable processors with their own assembly-like instruction set, capable of looping, branching, and conditional operations. In reality, they — like most practical computers — are von Neumann machines.Each state machine processes one instruction per clock cycle. The $4 Pico 1 runs at 125 million cycles per second, while the $5 Pico 2 offers a faster 150 million cycles per second. Each instruction performs a simple operation, such as “move a value” or “jump to a label”.With that bonus Wat out of the way, let’s move to our first main Wat.Wat 1: The Register Hunger GamesIn PIO programming, a register is a small, fast storage location that acts like a variable for the state machine. You might dream of an abundance of variables to hold your counters, delays, and temporary values, but the reality is brutal: you only get two general-purpose registers, x and y. It's like The Hunger Games, where no matter how many tributes enter the arena, only Katniss and Peeta emerge as victors. You’re forced to winnow down your needs to fit within these two registers, ruthlessly deciding what to prioritize and what to sacrifice. Also, like the Hunger Games, we can sometimes bend the rules.Let’s start with a challenge: create a backup beeper — 1000 Hz for ½ second, silence for ½ second, repeat. The result? “Beep Beep Beep…”We would like five variables:half_period: The number of clock cycles to hold the voltage high and then low to create a 1000 Hz tone. This is 150,000,000 / 1000 / 2 =

Jan 23, 2025 - 13:54
 0
Nine Pico PIO Wats with MicroPython (Part 1)

Raspberry Pi programmable IO pitfalls illustrated with a musical example

Pico PIO Surprises — Source: https://openai.com/dall-e-2/. All other figures from the author.

In JavaScript and other languages, we call a surprising or inconsistent behavior a “Wat!” [that is, a “What!?”]. For example, in JavaScript, an empty array plus an empty array produces an empty string, [] + [] === "". Wat!

Python, by comparison, is more consistent and predictable. However, one corner of MicroPython on the Raspberry Pi Pico microcontroller offers similar surprises. Specifically, the Pico’s Programmable Input/Output (PIO) subsystem, while incredibly powerful and versatile, comes with peculiarities.

PIO programming matters because it provides an ingenious solution to the challenge of precise, low-level hardware control. It is incredibly fast — even when programmed from MicroPython, PIO performs just as efficiently as it does from Rust or C/C++. Its flexibility is unparalleled: rather than relying on special-purpose hardware for the countless peripherals you might want to control, PIO allows you to define custom behaviors in software, seamlessly adapting to your needs without adding hardware complexity.

Consider this simple example: a $15 theremin-like musical instrument. By waving their hand in the air, the musician changes the pitch of (admittedly annoying) tones. Using PIO provides a simple way to program this device that ensures it reacts instantly to movement.https://medium.com/media/dc482ceb88d9def2e9c46e2dbd60fe35/href

So, all is wonderful, except — to paraphrase Spider-Man:

With great power comes… nine Wats!?

We’ll explore and illustrate those nine PIO Wats through the creation of this theremin.

Who Is This Article For?

  • All Programmers: Microcontrollers like the Pico cost under $7 and support high-level languages like Python, Rust, and C/C++. This article will show how microcontrollers let your programs interact with the physical world and introduce you to programming the Pico’s low-level, high-performance PIO hardware.
  • MicroPython Pico Programmers: Curious about the Pico’s hidden potential? Beyond its two main cores, it has eight tiny “state machines” dedicated to PIO programming. These state machines take over time-critical tasks, freeing up the main processors for other work and enabling surprising parallelism.
  • Rust and C/C++ Pico Programmers: While this article uses MicroPython, PIO programming is — for good and bad — nearly identical across all languages. If you understand it here, you’ll be well-equipped to apply it in Rust or C/C++.
  • PIO Programmers: The journey through nine Wats may not be as entertaining as JavaScript’s quirks (thankfully), but it will shed light on the peculiarities of PIO programming. If you’ve ever found PIO programming confusing, this article should reassure you that the problem isn’t (necessarily) you — it’s partly PIO itself. Most importantly, understanding these Wats will make writing PIO code simpler and more effective.

Finally, this article isn’t about “fixing” PIO programming. PIO excels at its primary purpose: efficiently and flexibly handling custom peripheral interfaces. Its design is purposeful and well-suited to its goals. Instead, this article focuses on understanding PIO programming and its quirks — starting with a bonus Wat.

Bonus Wat 0: “State Machines” Are Not State Machines

Despite their name, the eight “PIO state machines” in the Raspberry Pi Pico are not state machines in the formal computer science sense. Instead, they are tiny programmable processors with their own assembly-like instruction set, capable of looping, branching, and conditional operations. In reality, they — like most practical computers — are von Neumann machines.

Each state machine processes one instruction per clock cycle. The $4 Pico 1 runs at 125 million cycles per second, while the $5 Pico 2 offers a faster 150 million cycles per second. Each instruction performs a simple operation, such as “move a value” or “jump to a label”.

With that bonus Wat out of the way, let’s move to our first main Wat.

Wat 1: The Register Hunger Games

In PIO programming, a register is a small, fast storage location that acts like a variable for the state machine. You might dream of an abundance of variables to hold your counters, delays, and temporary values, but the reality is brutal: you only get two general-purpose registers, x and y. It's like The Hunger Games, where no matter how many tributes enter the arena, only Katniss and Peeta emerge as victors. You’re forced to winnow down your needs to fit within these two registers, ruthlessly deciding what to prioritize and what to sacrifice. Also, like the Hunger Games, we can sometimes bend the rules.

Let’s start with a challenge: create a backup beeper — 1000 Hz for ½ second, silence for ½ second, repeat. The result? “Beep Beep Beep…”

We would like five variables:

  • half_period: The number of clock cycles to hold the voltage high and then low to create a 1000 Hz tone. This is 150,000,000 / 1000 / 2 = 75,000 cycles high and 75,000 cycles low.
Voltage and timing (millisecond and clock cycles) to generate a 1000 Hz square tone.
  • y: Loop counter from 0 to half_period to create a delay.
  • period_count: The number of repeated periods needed to fill ½ second of time. 150,000,000 × 0.5 / (75,000 × 2) = 500.
  • x: Loop counter from 0 to period_count to fill ½ second of time.
  • silence_cycles: The number of clock cycles for ½ second of silence. 150,000,000 × 0.5 = 75,000,000.

We want five registers but can only have two, so let the games begin! May the odds be ever in your favor.

First, we can eliminate silence_cycles because it can be derived as half_period × period_count × 2. While PIO doesn’t support multiplication, it does support loops. By nesting two loops—where the inner loop delays for 2 clock cycles—we can create a delay of 75,000,000 clock cycles.

One variable down, but how can we eliminate two more? Fortunately, we don’t have to. While PIO only provides two general-purpose registers, x and y, it also includes two special-purpose registers: osr (output shift register) and isr (input shift register).

The PIO code that we’ll see in a moment implements the backup beeper. Here’s how it works:

Initialization:

  • The pull(block) instruction reads the half period of the tone (75,000 clock cycles) into osr.
  • The value is then copied to isr for later use.
  • The second pull(block) reads the period count (500 repeats), leaving the value in osr.

Beep Loops:

  • The mov(x, osr) instruction copies the period count into the x register, which serves as the outer loop counter.
  • For the inner loops, mov(y, isr) repeatedly copies the half period into y to create delays for the high and low states of the tone.

Silence Loops:

  • The silence loops mirror the structure of the beep loops but don’t set any pins, so they act solely as a delay.

Wrap and Continuous Execution:

  • The wrap_target() and wrap() instructions define the main loop of the state machine.
  • After finishing both the beep and silence loops, the state machine jumps back near the start of the program, repeating the sequence indefinitely.

With this outline in mind, here’s the PIO assembly code for generating the backup beeper signal.

import rp2

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def back_up():
pull(block) # Read the half period of the beep sound.
mov(isr, osr) # Store the half period in ISR.
pull(block) # Read the period_count.

wrap_target() # Start of the main loop.

# Generate the beep sound.
mov(x, osr) # Load period_count into X.
label("beep_loop")

set(pins, 1) # Set the buzzer to high voltage (start the tone).
mov(y, isr) # Load the half period into Y.
label("beep_high_delay")
jmp(y_dec, "beep_high_delay") # Delay for the half period.
set(pins, 0) # Set the buzzer to low voltage (end the tone).
mov(y, isr) # Load the half period into Y.
label("beep_low_delay")
jmp(y_dec, "beep_low_delay") # Delay for the low duration.

jmp(x_dec, "beep_loop") # Repeat the beep loop.

# Silence between beeps.
mov(x, osr) # Load the period count into X for outer loop.
label("silence_loop")

mov(y, isr) # Load the half period into Y for inner loop.
label("silence_delay")
jmp(y_dec, "silence_delay")[1] # Delay for two clock cycles (jmp + 1 extra)

jmp(x_dec, "silence_loop") # Repeat the silence loop.

wrap() # End of the main loop, jumps back to wrap_target for continuous execution.

And here’s the MicroPython code to configure and run the PIO program for the backup beeper. This script initializes the state machine, calculates timing values (half_period and period_count), and sends them to the PIO. It plays the beeping sequence for 5 seconds and stops. If connected to a desktop machine via USB, you can stop it early with Ctrl-C.

import machine
from machine import Pin
import time

BUZZER_PIN = 15

def demo_back_up():
print("Hello, back_up!")
pio0 = rp2.PIO(0)
pio0.remove_program()
state_machine_frequency = machine.freq()
back_up_state_machine = rp2.StateMachine(0, back_up, set_base=Pin(BUZZER_PIN))

try:
back_up_state_machine.active(1)
half_period = int(state_machine_frequency / 1000 / 2)
period_count = int(state_machine_frequency * 0.5 / (half_period * 2))
print(f"half_period: {half_period}, period_count: {period_count}")
back_up_state_machine.put(half_period)
back_up_state_machine.put(period_count)
time.sleep_ms(5_000)
except KeyboardInterrupt:
print("back_up demo stopped.")
finally:
back_up_state_machine.active(0)


demo_back_up()

Here’s what happens when you run the program:https://medium.com/media/0f1833eca8d143a358314b2158c3017d/href

Aside: Running this yourself
The most popular Integrated Development Environment (IDE) for programming the Raspberry Pi Pico with MicroPython is Thonny. Personally, I use the PyMakr extension for VS Code, though the MicroPico extension is another popular choice.
To hear sound, I connected a passive buzzer, a resistor, and a transistor to the Pico. For detailed wiring diagrams and a parts list, check out the passive buzzer instructions in the SunFounder’s Kepler Kit.

Alternative Endings to the Register Hunger Games

We used four registers — two general and two special — to resolve the challenge. If this solution feels less than satisfying, here are alternative approaches to consider:

Use Constants: Why make half_period, period_count, and silence_cycles variables at all? Hardcoding the constants "75,000," "500," and "75,000,000" could simplify the design. However, PIO constants have limitations, which we’ll explore in Wat 5.

Pack Bits: Registers hold 32 bits. Do we really need two registers (2×32=64 bits) to store half_period and period_count? No. Storing 75,000 only requires 17 bits, and 500 requires 9 bits. We could pack these into a single register and use the out instruction to shift values into x and y. This approach would free up either osr or isr for other tasks, but only one at a time—the other register must hold the packed value.

Slow Motion: In MicroPython, you can configure a PIO state machine to run at a slower frequency by simply specifying your desired clock speed. MicroPython takes care of the details for you, allowing the state machine to run as slow as ~2290 Hz. Running the state machine at a slower speed means that values like half_period can be smaller, potentially as small as 2. Small values are easier to hardcode as constants and more compactly bit-packed into registers.

A Happy Ending to the Register Hunger Games

The Register Hunger Games demanded strategic sacrifices and creative workarounds, but we emerged victorious by leveraging PIO’s special registers and clever looping structures. If the stakes had been higher, alternative techniques could have helped us adapt and survive.

But victory in one arena doesn’t mean the challenges are over. In the next Wat, we face a new trial: PIO’s strict 32-instruction limit.

Wat 2: The 32-Instruction Carry-On Suitcase

Congratulations! You’ve purchased a trip around the world for just $4. The catch? All your belongings must fit into a tiny carry-on suitcase. Likewise, PIO programs allow you to create incredible functionality, but every PIO program is limited to just 32 instructions.

Wat! Only 32 instructions? That’s not much space to pack everything you need! But with clever planning, you can usually make it work.

The Rules

  • No PIO program can be longer than 32 instructions.
  • The wrap_target, label, and wrap instructions do not count.
  • A Pico 1 includes eight state machines, organized into two blocks of four. A Pico 2 includes twelve state machines, organized into three blocks of four. Each block shares 32 instruction slots. So, because all four state machines in a block draw from the same 32-instruction pool, if one machine’s program uses all 32 slots, there’s no space left for the other three.

Avoiding Turbulence

For a smooth flight, use MicroPython code to clean out any previous programs from the block before loading new ones. Here’s how to do it:

pio0 = rp2.PIO(0) # Block 0, containing state machines 0,1,2,3
pio0.remove_program() # Remove all programs from block 0
back_up_state_machine = rp2.StateMachine(0, back_up, set_base=Pin(BUZZER_PIN))

This ensures your instruction memory is fresh and ready for takeoff. Clearing the blocks is especially important when using the PyMakr extension’s “development mode.”

When Your Suitcase Won’t Close

If your idea doesn’t fit in the PIO instruction slots, these packing tricks may help. (Disclaimer: I haven’t tried all of these myself.)

  • Swap PIO Programs on the Fly:
    Instead of trying to cram everything into one program, consider swapping out programs mid-flight. Load only what you need, when you need it.
  • Share Programs Across State Machines:
    Multiple state machines can run the same program at the same time. Each state machine can make the shared program behave differently based on an input value.
  • Use MicroPython’s exec Command:
    Save space by offloading instructions to MicroPython. For example, you can execute initialization steps directly from a string:
back_up_state_machine.exec("pull(block)")
  • Use PIO’s exec commands:
    Inside your state machine, you can execute instruction values stored in osr with out(exec) or use mov(exec, x) or mov(exec, y) for registers.
  • Offload to the Main Processors:
    If all else fails, move more of your program to the Pico’s larger dual processors — think of this as shipping your extra baggage to your destination separately. The Pico SDK (section 3.1.4) calls this “bit banging”.

With your bags now packed, let’s travel to the scene of a mystery.

Wat 3: The pull(noblock) Mystery

In Wat 1, we programmed our audio hardware as a backup beeper. But that’s not what we need for our musical instrument. Instead, we want a PIO program that plays a given tone indefinitely — until it’s told to play a new one. The program should also wait silently when given a special “rest” tone.

Resting until a new tone is provided is easy to program with pull(block)—we’ll explore the details below. Playing a tone at a specific frequency is also straightforward, building on the work we did in Wat 1.

But how can we check for a new tone while continuing to play the current one? The answer lies in using “noblock” instead of “block” in pull(noblock). Now, if there’s a new value, it will be loaded into osr, allowing the program to update seamlessly.

Here’s where the mystery begins: what happens to osr if pull(noblock) is called and there’s no new value?

I assumed it would keep its previous value. Wrong! Maybe it gets reset to 0? Wrong again! The surprising truth: it gets the value of x. Why? (No, not y — x.) Because the Pico SDK says so. Specifically, section 3.4.9.2 explains:

A nonblocking PULL on an empty FIFO has the same effect as MOV OSR, X.

Knowing how pull(noblock) works is important, but there’s a bigger lesson here. Treat the Pico SDK documentation like the back of a mystery novel. Don’t try to solve everything on your own—cheat! Skip to the “who done it” section, and in section 3.4, read the fine details for each command you use. Reading just a few paragraphs can save you hours of confusion.

With this in mind, let’s look at a practical example. Below is the PIO program for playing tones and rests continuously. It uses pull(block) to wait for input during a rest and pull(noblock) to check for updates while playing a tone.

import rp2

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def sound():
# Rest until a new tone is received.
label("resting")
pull(block) # Wait for a new delay value, keep it in osr.
mov(x, osr) # Copy the delay into X.
jmp(not_x, "resting") # If new delay is zero, keep resting.

# Play the tone until a new delay is received.
wrap_target() # Start of the main loop.

set(pins, 1) # Set the buzzer to high voltage.
label("high_voltage_loop")
jmp(x_dec, "high_voltage_loop") # Delay

set(pins, 0) # Set the buzzer to low voltage.
mov(x, osr) # Load the half period into X.
label("low_voltage_loop")
jmp(x_dec, "low_voltage_loop") # Delay

# Read any new delay value. If none, keep the current delay.
mov(x, osr) # set x, the default value for "pull(noblock)"
pull(noblock) # Read a new delay value or use the default.

# If the new delay is zero, rest. Otherwise, continue playing the tone.
mov(x, osr) # Copy the delay into X.
jmp(not_x, "resting") # If X is zero, rest.
wrap() # Continue playing the sound.

We’ll eventually use this PIO program in our theremin-like musical instrument. For now, let’s see the PIO program in action by playing a familiar melody. This demo uses “Twinkle, Twinkle, Little Star” to show how you can control a melody by feeding frequencies and durations to the state machine. With just a few lines of code, you can make the Pico sing!

import rp2
import machine
from machine import Pin
import time

from sound_pio import sound

BUZZER_PIN = 15

twinkle_twinkle = [
# 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
]


def demo_sound():
print("Hello, sound!")
pio0 = rp2.PIO(0)
pio0.remove_program()
state_machine_frequency = machine.freq()
sound_state_machine = rp2.StateMachine(0, sound, set_base=Pin(BUZZER_PIN))

try:
sound_state_machine.active(1)
for frequency, ms, lyrics in twinkle_twinkle:
if frequency > 0:
half_period = int(state_machine_frequency / frequency / 2)
print(f"'{lyrics}' -- Frequency: {frequency}")
# Send the half period to the PIO state machine
sound_state_machine.put(half_period)
time.sleep_ms(ms) # Wait as the tone plays
sound_state_machine.put(0) # Stop the tone
time.sleep_ms(50) # Give a short pause between notes
else:
sound_state_machine.put(0) # Play a silent rest
time.sleep_ms(ms + 50) # Wait for the rest duration + a short pause
except KeyboardInterrupt:
print("Sound demo stopped.")
finally:
sound_state_machine.active(0)


demo_sound()

Here’s what happens when you run the program:https://medium.com/media/42b1ddeeddfee5c255b2fe18a9a3f776/href

We’ve solved one mystery, but there’s always another challenge lurking around the corner. In Wat 4, we’ll explore what happens when your smart hardware comes with a catch — it’s also very cheap.

Wat 4: Smart, Cheap Hardware: An Emotional Roller Coaster

With sound working, we turn next to measuring the distance to the musician’s hand using the HC-SR04+ ultrasonic range finder. This small but powerful device is available for less than two dollars.

HC-SR04+ Range Finder (Pen added for scale.)

This little peripheral took me on an emotional roller coaster of “Wats!?”:

  • Up: Amazingly, this $2 range finder includes its own microcontroller, making it smarter and easier to use.
  • Down: Frustratingly, that same “smart” behavior is unintuitive.
  • Up: Conveniently, the Pico can supply peripherals with either 3.3V or 5V power.
  • Down: Unpredictably, many range finders are unreliable — or fail outright — at 3.3V, and they can damage your Pico at 5V.
  • Up: Thankfully, both damaged range finders and Picos are inexpensive to replace, and a dual-voltage version of the range finder solved my problems.

Details

I initially assumed the range finder would set the Echo pin high when the echo returned. I was wrong.

Instead, the range finder emits a pattern of 8 ultrasonic pulses at 40 kHz (think of it as a backup beeper for dogs). Immediately after, it sets Echo high. The Pico should then start measuring the time until Echo goes low, which signals that the sensor detected the pattern — or that it timed out.

As for voltage, the documentation specifies the range finder operates at 5V. It seemed to work at 3.3V — until it didn’t. Around the same time, my Pico stopped connecting to MicroPython IDEs, which rely on a special USB protocol.

So, at this point both the Pico and the range finder were damaged.

After experimenting with various cables, USB drivers, programming languages, and even an older 5V-only range finder, I finally resolved the issue with:

  • A new Pico microcontroller that I already had on hand. (It was a Pico 2, but I don’t think the model matters.)
  • A new dual-voltage 3.3/5V range finder, still just $2 per piece.

Wat 4: Lessons Learned

As the roller coaster return to the station, I learned two key lessons. First, thanks to microcontrollers, even simple hardware can behave in non-intuitive ways that require careful reading of the documentation. Second, while this hardware is clever, it’s also inexpensive — and that means it is prone to failure. When it fails, take a deep breath, remember it’s only a few dollars, and replace it.

Hardware quirks, however, are only part of the story. In Wat 5, in Part 2, we’ll shift our focus back to software: the PIO programming language itself. We’ll uncover a behavior so unexpected, it might leave you questioning everything you thought you knew about constants.

Those are the first four Wats from programming the Pico PIO with MicroPython. You can find the code for the project on GitHub.

In Part 2 (expected next week), we’ll explore Wats 5 through 9. These will cover inconstant constants, conditions through the looking glass, overshooting jumps, too many pins, and kludgy debugging. We’ll also unveil the code for the finished musical instrument. Follow me on Medium to get notified about this and future articles.

I write on scientific programming in Rust and Python, machine learning, and statistics. I typically post one article a month.


Nine Pico PIO Wats with MicroPython (Part 1) was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow