Flashing lights with MicroPython and Programmable I/O part 2

By Ben Everard. Posted

In part 1 we looked at a way of fading a LED in and out without and CPU involvement, but our options there are a little limited. Pico does have a PWM peripheral (see here for an example of how to use it: https://github.com/raspberrypi/pico-micropython-examples/blob/master/pwm/pwm_fade.py), but we're going to implement our own PWM using PIO.

PWM stands for Pulse Width Modulation and is a way of setting the brightness of an LED (amoung other things) by flicking it on or off very quickly. The larger the proportion of the time it's on, the brighter it is.

The full code for this example is at https://gist.github.com/benevpi/845e5dc432f05b6160ed56fc6f3b4032 if you want to just download it.

First we need to import some things, create a program and load it into a state machine. This should be familiar if you've already read through part 1.

from machine import Pin
from rp2 import PIO, StateMachine, asm_pio
from time import sleep

max_count = 500

def pwm_prog():
    pull(noblock) .side(0)
    mov(x, osr) 
    mov(y, isr) 
    jmp(x_not_y, "skip")
    nop()         .side(1)
    jmp(y_dec, "pwmloop")

pwm_sm = StateMachine(0, pwm_prog, freq=10000000, sideset_base=Pin(25))

We are doing things a little differently here in a few ways. Firstly, we aren't turning pins on and off using set, we're using .side(). This is known as side setting and it runs concurrently with other instructions. Note that we define the pin with sideset_base rather than set_base. This means that you can use set and sideset on different pins. There's also out, so you can use three independent groups of pins from a individual PIO program.

Let's go through this line-by-line as there are a few unfamiliar bits here.

  • pull(noblock) .side(0): The pull command takes data from the output FIFO and places it in the Output Shift Register. The noblock option means that if there is no data in the output FIFO, then it will continue running. If noblock is omitted, it will pause here and wait for data. .side(0) will turn the sideset pin (25 in this case) off. FIFOs or First In First Outs are data structures that do exactly this. The first bit of data you put in will be the first bit out the other end. They're sometimes known as queues and they perform a similar function of queueing up data before an input is ready to accept it.

  • mov(x, osr) . Move data from the Ouput Shift Register in the x (one of two scratch registers we can use -- the other being y. You can think of these a little like variables).

  • mov(y, isr) Move data from the Input Shift Register into y. Usually the ISR is used to store data read in from input pins, but in this case it's being uses a bit like a temporary variable. We'll see later how we load data into the ISR.

  • label("pwmloop"). We can place markers at certain points in our code that allows us to 'jump' to them.

  • jmp(x_not_y, "skip"). This performs a test between x and y. If they are different, then the program jumps to the label "skip" and doesn't execute any of the code in between.

  • nop() .side(1). The program will only reach this line if x is equal to y (if they are not, then the jump in the previous line will happen. nop means nothing happens in the main instruction, but the side(1) will turn the side set pin on.

  • jmp(y_dec, "pwmloop"). This jumps to the label pwmloop. As this is above this line, it will create a loop (as the label suggests). Each time it jumps, it will decrease the value of y by one, and if it gets to 0, it will not do the jump. As such, this performs a little like a for loop in Python with numbers counting down.

  • If the program gets to the end, it automatically loops back to the start.

A key to the way this program works is that we pre-load the ISR with the number of loops we want our pwm cycle to take. In our example, we've used 500, but it can be any 32-bit number.

The pull line will see if there is a new value to use, but if there isn't it will continue using the current value of the OSR (setting the PWM pin to 0 in the process), and load this into the x register. The program then loads the number of loops from the ISR into the y register. The PWM loop (between the label and the final jump) will continue to run until y reaches 0. When y reaches the value of x, it will turn on the sideset pin.

Let's take a look how to pre-load the ISR with data:

pwm_sm.exec("mov(isr, osr)")

First we use the put method to put data in the state machine's output FIFO. Then we use the exec method (which can run arbitrary commands in the state machine) to load the value into the ISR. Finally we activate the state machine so it's ready to start accepting values.

To set the brightness of the LED, then, we just have to feed a value between 0 (completely off) and 500 (almost fully on) into the output FIFO.

while True:
    for i in range(500):

Again, we use the put method to feed data into the output FIFO of the state machine.

That it for this part, we can now use PIO to set the brightness of an LED by flashing it on and off really fast.

If you want to find out more about controlling LEDs with PIO, in HackSpace magazine issue 39, we take a look at how to use Pico and the C SDK to control WS2812b (NeoPixel) LEDs to get more control over light levels than is usually possible. Download it for free or buy it online here.

You can get your very own Pico by getting a paper copy of HackSpace mag issue 39 or subscribing to HackSpace magazine from just £10. If you're in the UK head here. For delivery elsewhere in the world, take a look here.


From HackSpace magazine store


Subscribe to our newsletter