USD

Raspberry Pi Pico and RP2040 - C/C++ Part 3: How to Use PIO

By ShawnHymel

The Raspberry Pi Pico has a fascinating peripheral known as the “Programmable Input/Output” (PIO). This device allows us to write very simple assembly programs to emulate a number of different peripherals and communication protocols. This guide will show you how to get started creating PIO programs.

You will need to set up the Pico C/C++ SDK and build system on your computer as shown in the first tutorial. While optional, we also recommend setting up step-through debugging in VS Code as per the second tutorial.

Video

You can find the steps performed in this tutorial in video form here:

 

What is PIO?

PIO is a special, on-chip peripheral uniquely developed for the Raspberry Pi RP2040. Each RP2040 has 2 PIO instances, and each instance is capable of executing instructions, much like a small, very limited microcontroller.

Raspberry Pi RP2040 block diagram

Chapter 3 of the RP2040 Datasheet gives the best overview of the internal workings of the PIO.

Each PIO instance has 4 “state machine.” A state machine (in this case) is similar to a tiny processor capable of executing assembly instructions stored in shared PIO memory. State machines also have access to FIFOs (to transfer data to/from main system memory/CPU), all GPIOs on the RP2040, and some interrupts that can be used to synchronize the execution of the other state machines or main CPU.

We can use the PIO instances to emulate advanced peripherals that are not on the chip, such as SD card interface, CAN Bus, WS2812b driver, and so on. The state machines within the PIOs operate independently from the main CPU, so these emulated peripherals can communicate with external devices concurrently with the main program.

Hardware Hookup

You will need only a single Raspberry Pi Pico for this demo. Note that you are welcome to connect it to a second Raspberry Pi Pico running picoprobe for uploading and debugging, if you wish. However, picoprobe cannot perform step-through debugging of PIO programs (as they operate independently of the main program).

PIO Program

Create a new folder in your Pico project space (e.g. named “blink pio”). Open that folder in VS Code. Create a new file in that folder named blink.pio. This is where we will keep our PIO assembly program and helper C function.

Enter the following code into that file:

Copy Code
.program blink

; Turn on LED for 100 cycles and off for 100 cycles.
; At 2 kHz, this will toggle pin at 10 Hz (200 cycles / 2000 Hz = 0.1 sec)

.wrap_target
set pins, 1 [19] ; Turn LED on and wait another 19 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
set pins, 0 [19] ; Turn LED off and wait another 19 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
nop [19] ; Wait 20 cycles
.wrap

% c-sdk {

// Helper function (for use in C program) to initialize this PIO program
void blink_program_init(PIO pio, uint sm, uint offset, uint pin, float div) {

// Sets up state machine and wrap target. This function is automatically
// generated in blink.pio.h.
pio_sm_config c = blink_program_get_default_config(offset);

// Allow PIO to control GPIO pin (as output)
pio_gpio_init(pio, pin);

// Connect pin to SET pin (control with 'set' instruction)
sm_config_set_set_pins(&c, pin, 1);

// Set the pin direction to output (in PIO)
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

// Set the clock divider for the state machine
sm_config_set_clkdiv(&c, div);

// Load configuration and jump to start of the program
pio_sm_init(pio, sm, offset, &c);
}

%}

Save the file.

The PIO assembly program is at the top. We name the program “blink” with the “.program” directive. We can set 1 free “jump” command (using .wrap and .wrap_target) per program. The state machine will automatically wrap to the .wrap_target directive when it reaches .wrap, and it does not cost an instruction cycle.

The assembly instructions are defined in chapter 3 of the RP2040 datasheet, so I recommend reading through that section to learn what they do. We call “set pins, 1” to turn our pin (defined later) high and “set pins, 0” to set the pin low. There is no “nop” instruction, but the assembler translates this to “mov y, y” (which accomplishes nothing).

Each instruction takes 1 cycle of the state machine clock (which is divided from the main system clock). We can delay the clock beyond that by adding a number in brackets (e.g. [19]) between 1 and 31 after the instruction.

This program will turn the given pin (which we will define later) on for 100 cycles and off for 100 cycles.

The bottom of the file contains a C helper function, which we will call in our main program to configure the PIO program.

By setting the clock divider, we can run the PIO program at or slower than the main system clock. Note that with a system clock of 133 MHz, the lowest we can set the divided state machine clock is about 2 kHz.

Main Code

Create a new file named main.c. Copy in the following code:

Copy Code
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "blink.pio.h"

int main() {

static const uint led_pin = 25;
static const float pio_freq = 2000;

// Choose PIO instance (0 or 1)
PIO pio = pio0;

// Get first free state machine in PIO 0
uint sm = pio_claim_unused_sm(pio, true);

// Add PIO program to PIO instruction memory. SDK will find location and
// return with the memory offset of the program.
uint offset = pio_add_program(pio, &blink_program);

// Calculate the PIO clock divider
float div = (float)clock_get_hz(clk_sys) / pio_freq;

// Initialize the program using the helper function in our .pio file
blink_program_init(pio, sm, offset, led_pin, div);

// Start running our PIO program in the state machine
pio_sm_set_enabled(pio, sm, true);

// Do nothing
while (true) {
sleep_ms(1000);
}
}

Save the file.

In this program, we configure the PIO program, upload it to PIO memory, and start running the state machine.

CMake Configuration File

Create another file named CMakeLists.txt. Enter the following:

Copy Code
# Set minimum required version of CMake
cmake_minimum_required(VERSION 3.12)

# Include build functions from Pico SDK
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
include($ENV{PICO_SDK_PATH}/tools/CMakeLists.txt)

# Set name of project (as PROJECT_NAME) and C/C++ standards
project(blink_pio C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()

# Tell CMake where to find the executable source file
add_executable(${PROJECT_NAME}
main.c
)

# Create C header file with the name <pio program>.pio.h
pico_generate_pio_header(${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/blink.pio
)

# Create map/bin/hex/uf2 files
pico_add_extra_outputs(${PROJECT_NAME})

# Link to pico_stdlib (gpio, time, etc. functions)
target_link_libraries(${PROJECT_NAME}
pico_stdlib
hardware_pio
)

# Enable usb output, disable uart output
pico_enable_stdio_usb(${PROJECT_NAME} 0)
pico_enable_stdio_uart(${PROJECT_NAME} 1)

Save the file.

This includes a few extra functions to the CMakeLists.txt file from the first tutorial, as we need to include the PIO tools and have it generate the blink.pio.h file that helps us configure the state machine.

(Optional) .vscode Settings

If you wish to use step-through debugging in VS Code, you will want to add the following files to your project.

Create a folder named .vscode. Create a file named launch.json in that folder and add the following:

Copy Code
{
"version": "0.2.0",
"configurations": [
{
"name": "Pico Debug",
"cwd": "${workspaceRoot}",
"executable": "${command:cmake.launchTargetPath}",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
// This may need to be arm-none-eabi-gdb depending on your system
"gdbPath" : "arm-none-eabi-gdb",
"device": "RP2040",
"configFiles": [
"interface/picoprobe.cfg",
"target/rp2040.cfg"
],
"svdFile": "${env:PICO_SDK_PATH}/src/rp2040/hardware_regs/rp2040.svd",
"runToMain": true,
// Work around for stopping at main on restart
"postRestartCommands": [
"break main",
"continue"
],
"searchDir": ["C:/VSARM/sdk/pico/openocd/tcl"],
}
]
}

Save the file.

Create another file named settings.json in that folder and add the following:

Copy Code
{
// These settings tweaks to the cmake plugin will ensure
// that you debug using cortex-debug instead of trying to launch
// a Pico binary on the host
"cmake.statusbar.advanced": {
"debug": {
"visibility": "hidden"
},
"launch": {
"visibility": "hidden"
},
"build": {
"visibility": "default"
},
"buildTarget": {
"visibility": "hidden"
}
},
"cmake.buildBeforeRun": true,
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
"cortex-debug.openocdPath": "C:/VSARM/sdk/pico/openocd/src/openocd.exe",
"files.associations": {
"blink.pio.h": "c",
"clocks.h": "c"
}
}

Save the file.

At this point, you should have all of the files necessary to build and run your blink PIO program

Creating PIO program in VS Code

Build and Run

Click the Cmake: [Debug]: Ready button on the status bar to configure the project. Make sure you have GCC for arm-none-eabi selected as your kit. Click the Build button.

If you are not using the picoprobe debugger, put your Pico into bootloader mode by holding the BOOTSEL button while plugging in the USB cable. Copy the blink_pio.uf2 file (found in the build folder) to the enumerated RPI-RP2 drive.

If you are using the picoprobe debugger, press the Debug button on the left of the VS Code window. Press the Start Debugging button to build the project, upload it to the target Pico, and start the debugger. Press the Continue button to start running the program.

Debugging a Raspberry Pi Pico program

Your Pico should begin blinking its LED at a rapid pace (10 Hz).

Raspberry Pi Pico blinking LED

Going Further

I hope this guide has helped you get started creating PIO programs on your RP2040. If you would like to dive deeper into the Pico C/C++ SDK, I recommend checking out the following documentation:

Key Parts and Components

Add all Digi-Key Parts to Cart
  • 2648-SC0915CT-ND