Programmable IO on the Pi Pico

I’m working on a project with a Raspberry Pi Pico to control some devices over IR (infrared). Many IR controlled devices pulse the IR LED at a frequency of about 38 kilohertz so that it can be differentiated from other stray IR light sources.  What is a good way to turn a pin on and off 38,000 times per second? As a starting point, I used one of the PICO examples that generates a square wave.  

The most obvious way would be to write code in a cycle that activates a pin, waits for a moment, and then deactivates a pin.  That code would look similar the following.

gpio_put(LED_PIN, true);
sleep_us(12);
gpio_put(LED_PIN, false);
sleep_us(12);

There are 1,000,000 us (microseconds) in a second. The total of the two waits together is 24 us. 1,000,000/24 is 38,461. There will be additional time consumed on the calls to set the pins, making the actual number of times that this code can run in a loop per second to be slightly lower than 38,461. But it is close enough to be effective.

There is a lot of room for improvement in approach. A significant problem with this code is that it consumes one of the execution cores of the processor to be in wait states. This is a waste of a processor core; there’s other work that it could be doing in that time. Let’s take a step towards a better approach. While there are several elements that would be part of a better solution, I want to focus on one.

In addition to the primary cores, the Pi Pico also has processors that are made specifically for operations on a few of the GPIOs.  These make up the Programmable IO (PIO) system. This processor is simple.  There are two blocks of 4 processors (8 total). There are only 9 instructions that the processor can execute. But its execution of these instructions is deterministic, taking 1 clock cycle per instruction.  We can also set an instruction to wait up to 31 additional cycles before going to the next instruction.

These execution units give a developer the following hardware to work with.

  • Two general purpose registers, labeled X and Y
  • An Input and Output shift register
  • A Clock Divider for modifying the execution speed of the PIO unit
  • Access to the Pico’s IRQ registers
  • Mapped and direct access to the GPIO pins

Because the execution units support mapped IO, the same program could run on multiple PIO units and be assigned to different GPIOs

PIO is programmed with PIO Assembler (pioasm). Each PIO unit has two general purpose records, labeled X and Y. There are only 9 instructions, each of which is encoded as a 16-bit structure of the instructions and the operands. We don’t need all the instructions for the task I’m trying to accomplish here. I’ll list all nine of them.

  • IN – shift up to 32 bits from a GPIO or register to the input shift register
  • OUT – shift up to 32 bits from the Output Shift Register to a pin or register
  • PULL – move the contents of the Tx FIFO to the output shift register
  • PUSH – move the contents of the input shift register to the Rx FIF and clear the ISR.
  • MOV – Copy data from a register or pin to some other register or pin.
  • IRQ – Sets or clears an IRQ flag
  • SET – Write an immediate value to a pin or register
  • JMP – Jumps to an absolute address within the PIO instruction memory
  • WAIT – stall execution until a specific pin or IRQ flag is set or unset

Since all I am trying to do is set a pin to alternating states, the only instruction I need for this program is the SET instruction.  One call to SET will activate a pin. Another call will deactivate it. The part where more attention must be given to detail is to ensure that this happens about 38,000 times per second. There will be more code in this posting about setting PIO attributes than in the PIO code itself. Let’s address the easier part, the PIO program.

The PIO program itself is only 7 lines. Most of these lines are not executable code.  The first line lets the software tools know what version of the pio spec is being used. The second line sets the name of the program. This name will propagate to other auto-generated elements in code. It isn’t only notational.  In the third line, I specify that the pins that are assigned to the program should be set to output pins. There will only be one pin assigned to the program.

The first line of executable code is the call to “set pins , 1 [1]”. This sets the assigned pin to high. The [1] next to the instruction causes the execution unit to stall for a clock cycle. This line of code takes 2 clock cycles to execute. The next line sets the pin to the low state.  

.pio_version 0
.program squarewave
    set pindirs, 1  ; Set pin to output
loop:
    set pins, 1  [1]
    set pins, 0 
    .wrap

The last line of the program, .wrap, marks the end of the executable code. While .wrap isn’t itself an instruction, implicitly there is a JMP instruction hat gets executed when this line is reached. The program will either jump to the beginning of the code (if no jump target is specified) or it will jump to a line with .wrap_target (if such a line is entered).  The code that gets executed could be written as follows.

loop:
set pins, 1 [1] ; Set pin (1-cycle) + delay (1-cycle) = 2-cycles
set pine, 0 ; 1-cycle
 jmp loop ; 1-cycle

You might have the question of why I have a delay. I want the output to have a 50% duty cycle. If I wrote that code without any delays, then the pin would be high for 1/3 or the cycle and low for 2/3, since the pin would remain low while the jump instruction was executing.

When the code is compiled, a C++ header file is emitted. The C++ header contains the program as an array of numerical data. It also defines some additional functions that provide support and initialization for the program. If we want additionally C/C++ code that is associated with our PIO program, we can embed C/C++ code in our PIO file. This ensures that if the PIO is distributed, the C/C++ code will always be distributed with it. We just need to ensure that it is embedded between “% c-sdk {“ and “%}”.

For my program, I have added a function named “squarewave_program_init” that performs a few tasks. It performs the some initialization steps for my PIO program, including applying a clock divider to lower the frequency at which the program runs.

.pio_version 0
.program squarewave
    set pindirs, 1  ; Set pin to output
loop:
    set pins, 1  [1]
    set pins, 0 
    .wrap

% c-sdk {
    static inline void squarewave_program_init(PIO pio, uint sm, uint offset, uint pin, float div)
    {
        pio_sm_config c = squarewave_program_get_default_config(offset);
        sm_config_set_out_pins(&c, pin, 1);
        pio_gpio_init(pio, pin);
        pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

        sm_config_set_clkdiv(&c, div);
        sm_config_set_set_pins(&c, pin, 1);
        pio_sm_init(pio, sm, offset, &c);
        pio_sm_set_enabled(pio, sm, true);
    }
%}

We still need to calculate a divider frequency. The Raspberry Pi Pico can run up to 133 MHz. They will generally be clocked between 125 MHz and 133 MHz. To get the frequency at which the Pico is running, we can use the function clock_get_hz().  Each loop of my PIO program needs 4 instructions. To run at 38KHz, I need for the PIO program to run with a clock rate of 38,000 x 4 times per second. The PIO clock rate needs to be at 152 KHz. The divider amount needs to be the result of the clock frequency divided by 152,000.

static const float pio_freq = 38000*4;
float div = (float)clock_get_hz(clk_sys) / pio_freq;

The last couple of things that must be done is that I need to grab an available PIO unit and assign my program to it. Then I need to enable my program to run.

bool success = pio_claim_free_sm_and_add_program_for_gpio_range(&squarewave_program, &pio, &sm, &offset, CARRIER_PIN, 1, true);
 hard_assert(success);
squarewave_program_init(pio, sm, offset, CARRIER_PIN, div);

After that last line of code runs, the PIO will be active and running the program. It will stay active until I deactivate it (or the Pico loses power). If I needed to stop the PIO program and deallocate the use of resources, I can perform that with a call to pio_remove_program_and_unclaim_sm();

The Pico that I am using is connected to a break-out board that shows the status of each one of the GPIOs. (See A Pi Pico Breakout Board – j2i.net).While 38KHz is too fast to observe with the naked eye, when I run the program, the first indication that it is operating as expected is that the light on the target pin appears to be illuminated with a slightly lower intensity than the other pins. This is expected, since the status light is unpowered 50% of the time.

To know it is working, we can use an oscilloscope. Connecting the scope to the pin, I see a square wave.

Checking the frequency on the scope, I see a reading of 38.0 KHz.

A closeup of the Oscilloscope showing the frequency

This gives me a carrier for IR signalling. With that accomplished, I now need to turn this output on and off in a sequence to communicate an IR message. If you’d like to see the code used for making this post in the form it was in at the time this post was published, you can find it on GitHub at this URL.

https://github.com/j2inet/irdetect/tree/addingGpio


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Raspberry Pi Pico
Pi Pico Breakout Board

flexDOCK (Icy Dock)

I’ve got a machine that I’ll be repuposing and decided to add additional drives to it. I’ve got plenty of 2.5 inch drives on shelves and through they would be good candidates for the machine. Often times, the limits on how many drives I place in a machine is from how many bays there are to place them; the machines are often capable of connecting to more drives. There is just no place to put them.

The Icy Dock (or flexDOCK) is a solution for this. I’m using a SATA verion. There is a version for M.2 drives also. The Icy Dock distributes power to up to 4 drives (only one power cable is needed to the Dock) and provides 4 slots for holding hot-swappable drives. The device installs into a 5.25 inch bay. Horizontally in line on the back of the dock are 4 SATA connectors; one connector is available for each drive. There is also a fan on the back of the unit for circulating air over the drives. Speed adjustment for the fan is on the front of the dock. There’s a jumper on the back for disabling the fan alltogether.

Provided that the computer’s operating system and firmware supports it, these drives are hot-swappable. If one wants to experiement with different operating systems on the same computer, this is a great option for being able to swap out drives without breaking out a screwdrive or removing drive bays. Each one of the slots for a drive has a power button that can be used to disrupt power from the drive, and an eject button.

One criticism I have is that the eject buttons sometimes require a lot of force to eject the drive. But it is still much easier and more convinient than opening up the drive.

You can find the Icy Dock on Amazon here (affiliate link).


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Pi Pico Cases

I picked up a few Pi Pico cases. Both provide different protection for the units. I also find them to be aesthetically pleasing additions to have on the board. They both protect the top and underside of the board. The most significant difference is whether they also protect the pins that may be soldered to the board.

The Minimalistic Case

One of the cases is minimalistic. It sandwiches two pieces of acrylic around the board. There are spaces so that the acrylic on the top side isn’t resting on the board and has enough space to hold an extension so that the BOOTSEL button is still accessible. But this case was clearly made for the Picos that don’t have the WiFi chip. The debug header pins are in different places on the Picos with Wi-Fi and without. If you don’t use the debug header pins, this won’t be an issue. The lower acrylic is just wide enough to cover the bottom of the board between the header pins. This case protects the board itself, but not the pins that are connected to it. I use this on a Pico that is connected to a breakout board. That it doesn’t cover the pins gives me enough clearance to easily plug it in.

C4Labs Case

C4Labs Case

The other case, from C4Labs, is also made of acrylic pieces. Though it is many more pieces sandwiched together to completely envelope the Pico circuit board, the pins, and the debug header. This case was made to universally fit the Picos with and without Wi-Fi. There are cutouts for either position of the debug header. Since the pins are completely enveloped, there are restrictions on how one might connect something to it. Jumper wires will connect to the pins without trouble.

Underside of C4 Labs Case

I cannot use this case with the breakout board that I have though. Parts of the case conflict with other connectors that I have on my breakout board. However, the area in the case in which the pins would extend could potentially be used to hold a small amount of other electronics. I’m working on an IR control project, and I might place an IR Emitter and detector within this space.

These cases are available on Amazon. The minimalistic case is available by itself or with a Pi Pico. You can purchase them through the following links. Note that these are affiliate links. I make a small commission if you purchase through these links.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Setting Up for Pi Pico Development (2025)

In a previous post, I mentioned that I was re-introducing myself to development for the Pi Pico. The Pico is a microcontroller, often compared to an Arduino, that can be programmed from a Linux, Mac, or Windows machine. The Pico is based on the RP2040 chip. This is an ARM based Cortex-M0 dual core processor, generally running between 125 and 133 MHz. It has 264 KB of SRAM, 2 MB of flash memory, 26 general purpose IO pins, some of which support additional functionality. The other functionality overlaid on these pins includes

  • 2 UART pins
  • 2 SPI controllers
  • 2 I2C controllers
  • 16 PWM channels

There are several development boards that use the RP2040. Collectively, I generically refer to all of these as Pico. It is a bit easier to say then “RP2040 based board.”

A smaller RP2040 based board by WaveShare

I already had a few machines setup for development for the Raspberry Pi Pico. While that procedure still works, as do those development machines, I was recently reintroducing myself to Pico development. I started with a clean installation and went to the currently published instructions for setup. The more recent instructions are a lot easier to follow; there are less dependencies on manually setting paths and downloading files. The easier process is made possible through a Visual Studio Code plugin. This extension, which is still labeled as a zero version at the time that I am making this post (0.17.3) adds project generation and sample code along with scripts and automations for common tasks. To get started, just Install the Raspberry Pi Pico Visual Studio Code Extension. Once it is installed, you’ll have a new icon on the left pane of VS Code for Pico related tasks.

The first time you do anything with this icon, expect it to be slow. It installs the other build tools that it needs on-demand. I prefer to use the C++ build tools. Most of what I write here will be focused on that. I’ll start with creating a new C++ project. Double-clicking on “New C/C++ Project” from the Pico tools panel gets the process started.

This will only be a “Hello World” program. We will have the Pico print a message to a serial port in a loop. The new project window lets us specify our target hardware, including which hardware features that we plan to use. Selecting a feature will result in the build file for the project linking to necessary libraries for that feature and adding a small code sample that access that feature. Select a folder in which the project folder will be created, enter a project name, and check the box labeled “Console over USB.” After selecting these options, click on the “Create” button.

This is the part that takes a while the first time. A notification will show in VS Code stating that it is installing the SDK and generating the project. The wait is only a few minutes. While this is executing, it is a good time to grab a cup of coffee.

When you get back, you’ll see VS Code welcome you with a new project. The default new project prints “Hello, world!\n” in a loop with a 1 second delay. Grab your USB cable and a Pico. We can immediately start running this program to see if the build chain works. On the Pico, there’s a button. Connect your USB cable to your computer, then connect the Pico, making sure you are holding down this button as you connect it. The Pico will show up on your computer as a writable drive. After you’ve done this, take note of which serial ports show up on your computer. In my case, I’m using Windows, which shows that Com1 is the only serial port. In VS Code, you now have several tasks for your project that you can execute. Double-click on Run Project (USB). The code will compile, deploy to the Pico, and the Pico will reboot and start running the code.

Check to see what serial ports exist on your computer now. For me, there is a new port named Com4. Using PuTTY, I open Com4 at a baud rate of 115,200. The printed text starts to show there.

Using the USB UART for output is generally convenient, but at time you may want to use the USB for other features. The USB output is enabled or disabled in part through a couple of lines in the CMakeList.txt file.

pico_enable_stdio_uart(HelloWorldSample 0)
pico_enable_stdio_usb(HelloWorldSample 1)

The 1 and 0 can be interpreted as meaning enable and disable. Swap these values and run the project again by disconnecting the Pico, reattach while pressing the button, and then selecting the Run Project (USB) option from VS Code. When you run the code this time, the output is being transmitted over GPIO pins 0 and 1. But how do we read this?

FTDI USB

FTDI is the name of an integrated circuit manufacturer. For microcontroller interfacing, you might often see people refer to “FTDI USB” cables. These are USB devices that have 3 or 4 pins for connecting to other serial devices. These are generally cheaply available. The pins that we care about will be labeled GND (Ground), TX (Transmit), and RX (Receive). The transmit pin on one end of a serial exchange connects to the receive end on the other, and vice versa. On the Pico, the default pins used for uart0 (the name of our serial port) are GP0 for TX and GP1 for RX. When connecting an FTDI device, connect the FTDI’s RX to the Pico’s TX on GPO, then the FTDI’s TX to the Pico’s RX (on GP1), and finally the FTDI’s ground to the Pico’s ground.

GPIO – Setting a Pin

Many, Pico’s have a LED attached to one of the pins that is immediately available for test programs. While many do, not all do. On the Pi Pico and Pi Pico 2, GPIO 25 is connected to a LED. On the Pi Pico W, the LED is connected to the WiFi radio and not the RP2040 directly. For uniformity, I’ll drive an external LED. I’ve taken a LED and have it connected in series with a resistor. 220Ω should be a sufficient value for the resistor. I’m connecting the longer wire of the LED to GP5 and the shorter pin to ground.

In the code, the pin number is assigned to a #define. This is common, as it makes the code more flexible for others that may be using a different pin assignment. Before we can start writing to the pin, we need to gall an initialize function for the pin number named gpio_init(). After the initialization, we need to set the pin to be either in input or output mode. Since we are going to be controlling a LED, this needs to be output mode. This is done with a call to gpio_set_dir() (meaning “set direction”) passing the pin number as the first argument, and the direct (GPIO_IN or GPIO_OUT) as the second argument. For writing, we use GPIO_OUT. With the pin set to output, we can drive the pin to a high or low state by calling gpio_put(). The pin number is passed in the first argument, and a value indicating whether it should be in a high or low state in the second argument. A zero value is considered low, while a non-zero value is considered high. To make it apparent that the LED is being driven by our control of the pin (and not that we just happened to wire the LED to a pin that is always high) we will turn the light on and off once per second. In a loop, we will turn the light on, wait half a second, turn the light off, and wait again.

#include <stdio.h>
#include "pico/stdlib.h"

#define LED_PIN 5
int main()
{
    stdio_init_all();
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    while (true) {
        gpio_put(LED_PIN, 1);   
        sleep_ms(500);
        gpio_put(LED_PIN, 0);
        sleep_ms(500);
    }
}

When we run the code now, we should see the light blink.

Up Next: Programmable IO – The Processor within the Processor

While the GPIO system can be manipulated by the main processor core, there are also smaller processors on the silicon that exist just for controlling the GPIO. These processors have a much smaller reduced set but are great for writing deterministic code that controls the pins. This system of sub-processors and the pins that they control are known as “Programmable IO.” They are programmed using assembler. There’s much to say about PIO. In the next post that I make on the Pico, I’ll walk you through an introduction to the PIO system.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net