#pio — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #pio, aggregated by home.social.
-
FreeRTOS WS2812 (Neopixel) RGB LED (part 2)
Adding FreeRTOS to driving Neopixel WS2812 addressable RGB LEDs using a RP2350. We will be adding FreeRTOS into the mix but keep the PIO state machine and DMA code from the previous project.
#RP2350 #GettingStarted #Tutorial #WS2812 #Neopixel #RPPicoWorld #DMA #PIO #FreeRTOs
-
Simplified Pico VGA – Part 2
Having spent far too long chewing over how the Raspberry Pi Pico Scanvideo library works over in Simplified Pico VGA, this post actually starts to do something with it.
One constraint I have for myself, is that I want to keep using the Arduino IDE for the Pico, so the first step is to see if I can get the Scanvideo library over to an Arduino sketch. This means grabbing the files from the following locations:
- https://github.com/raspberrypi/pico-extras/tree/master/src/common/pico_scanvideo
- https://github.com/raspberrypi/pico-extras/tree/master/src/rp2_common/pico_scanvideo_dpi
- https://github.com/raspberrypi/pico-extras/tree/master/src/common/pico_util_buffer
- https://github.com/raspberrypi/pico-playground/tree/master/scanvideo
The pico_util_buffer code is used for buffer management, so that is required too. The last one houses some examples, I’m just using the https://github.com/raspberrypi/pico-playground/tree/master/scanvideo/test_pattern as a basis for my own example.
As with my other PIO experiments, I’m using the unofficial Arduino RP2040 core.
Starting the Arduino Sketch
In order to get things started, I’m just taking all the files from the above projects and dropping them directly into the Arduino Sketch folder, with no further hierarchy. My file list looks as follows:
25/07/2026 19:26 392 buffer.c
25/07/2026 19:25 2,265 buffer.h
25/07/2026 19:20 1,425 composable_scanline.h
25/07/2026 19:27 1,675 PicoScanVideo.ino
25/07/2026 19:49 6,293 scanvideo-pio.h
25/07/2026 19:26 83,925 scanvideo.c
25/07/2026 19:24 1,949 scanvideo.h
25/07/2026 19:16 13,613 scanvideo_base.h
25/07/2026 19:49 2,488 timing-pio.h
25/07/2026 19:20 17,057 vga_modes.cEach one of these files must be edited to change the include paths to drop down to a single directory.
There are three files here that are not in the original libraries. The .ino I’ll come to shortly, but the two *-pio.h files are the assembled files from the two original .pio files. These are created using https://wokwi.com/tools/pioasm and the output is pasted into these two new h files.
Initial Test Code
I’ve created an example based on https://github.com/raspberrypi/pico-playground/tree/master/scanvideo/test_pattern. But I’ve removed the multicore and keyboard input. This will (hopefully) just display the “test card” and nothing else.
#include "scanvideo.h"
#include "composable_scanline.h"
#define vga_mode vga_mode_320x240_60
static bool invert = false;
void draw_color_bar(scanvideo_scanline_buffer_t *buffer) {
// figure out 1/32 of the color value
uint line_num = scanvideo_scanline_number(buffer->scanline_id);
uint32_t primary_color = 1u + (line_num * 7 / vga_mode.height);
uint32_t color_mask = PICO_SCANVIDEO_PIXEL_FROM_RGB5(
0x1f * (primary_color & 1u),
0x1f * ((primary_color >> 1u) & 1u),
0x1f * ((primary_color >> 2u) & 1u));
uint bar_width = vga_mode.width / 32;
uint16_t *p = (uint16_t *) buffer->data;
uint32_t invert_bits = invert ? PICO_SCANVIDEO_PIXEL_FROM_RGB5(0x1f,0x1f,0x1f) : 0;
for (uint bar = 0; bar < 32; bar++) {
*p++ = COMPOSABLE_COLOR_RUN;
uint32_t color = PICO_SCANVIDEO_PIXEL_FROM_RGB5(bar, bar, bar);
*p++ = (color & color_mask) ^ invert_bits;
*p++ = bar_width - 3;
}
// 32 * 3, so we should be word aligned
assert(!(3u & (uintptr_t) p));
// black pixel to end line
*p++ = COMPOSABLE_RAW_1P;
*p++ = 0;
// end of line with alignment padding
*p++ = COMPOSABLE_EOL_SKIP_ALIGN;
*p++ = 0;
buffer->data_used = ((uint32_t *) p) - buffer->data;
assert(buffer->data_used < buffer->data_max);
buffer->status = SCANLINE_OK;
}
void setup() {
// initialize video and interrupts on core 1
scanvideo_setup(&vga_mode);
scanvideo_timing_enable(true);
}
void loop() {
scanvideo_scanline_buffer_t *scanline_buffer =
scanvideo_begin_scanline_generation(true);
draw_color_bar(scanline_buffer);
scanvideo_end_scanline_generation(scanline_buffer);
}This it the same as the provided example apart from the fact that the core1 function has been split into the initialisation code, placed in setup(), and the infinite loop code, now placed in loop().
This requires the circuit from the pico_playground here: https://github.com/raspberrypi/pico-playground/tree/master/scanvideo, to provide a 5-bit (RGB555) VGA interface.
VGA Breakout
As can be seen in the above photo I’m using a breakout board between the Pico and the VGA display. I designed this to let me experiment with VGA and the Pico.
Bill of Materials:
- VGA socket (see photos and PCB for footprint).
- Pin headers.
- Range of resistors and diodes depending on the DAC design.
The photo below shows two configurations. On the left is a RGB555 as detailed in the RP2040 Hardware Design Guide and the pico_playground repository. This uses a 5-resistor DAC comprising: 8K, 4K, 2K, 1K, 500R resistors. Although I only had 4K7 resistors, so as you can see have used two 2K resistors in series. It uses two 47R resistors for HSYNC and VSYNC.
On the right is the 330R/100R and diode configuration mentioned in Part 1 for a RGBY1111 configuration. This uses two 100R resistors for HSYNC and VSYNC (I don’t know why they are different to the RGB555 case).
There are suggested resistor values for different options printed on the board, although there is one mistake. The RGBY1111 suggests 300R+100R in the text, whereas 330R+100R against the components. 330R is the recommended value.
To wire up the board for the default RGB555 configuration with a Pico uses the following resistor to GPIO mapping:
GP08KRed MSBitGP14KRedGP22KRedGP31KRedGP4500RRed LSBitGP5N/CN/CGP68KGreen MSBitGP74KGreenGP82KGreenGP91KGreenGP10500RGreen LSBitGP118KBlue MSBitGP124KBlueGP132KBlueGP141KBlueGP15500RBlue LSBitGP1647RHSYNCGP1747RVSYNCVGA to CGA
The reason for doing all this is to get to that simpler CGA-like mode that mirrors the ZX Spectrum video capability. To do this I need to get the scanvideo library implementing RGBY1111 as mentioned in Part 1.
The scanvideo library assumes the use of compiler directives to set the video modes, but these can’t just be set prior to including the appropriate header files in my .ino file as they need to actually be compiled into the library C files too.
To do this, I created a vgamode.h file which can be included once in scanvideo.h which is included everywhere within the library.
vgamode.h:
#define VGA_RGBY111
#define PICO_SCANVIDEO_COLOR_PIN_COUNT 4u
#define PICO_SCANVIDEO_DPI_PIXEL_RCOUNT 2u
#define PICO_SCANVIDEO_DPI_PIXEL_GCOUNT 1u
#define PICO_SCANVIDEO_DPI_PIXEL_BCOUNT 1u
#define PICO_SCANVIDEO_DPI_PIXEL_RSHIFT 2u
#define PICO_SCANVIDEO_DPI_PIXEL_GSHIFT 1u
#define PICO_SCANVIDEO_DPI_PIXEL_BSHIFT 0u
#define PICO_SCANVIDEO_COLOR_PIN_BASE 12uAs described in Part 1 this sets the hardware interface up for 4 GPIO pins for RGB and Y (as a second R) and sets the GPIO base pin to 12. The SYNC pins are automatically set elsewhere to be:
#define PICO_SCANVIDEO_SYNC_PIN_BASE (PICO_SCANVIDEO_COLOR_PIN_BASE + PICO_SCANVIDEO_COLOR_PIN_COUNT)
This creates the following GPIO mapping and resistor usage:
GP12330R+100RBlueGP13330R+100RGreenGP14330R+100RRedGP15Diodes + 330R x 3Second Red (Y)GP16100RHSYNCGP17100RVSYNCThis also now requires an alternative scanline generator, so I’ve implemented the following:
void draw_zxcolor_bar(scanvideo_scanline_buffer_t *buffer) {
uint32_t line_num =
scanvideo_scanline_number(buffer->scanline_id);
uint32_t col = (line_num * 16ul) / vga_mode.height;
uint16_t num_pxls = vga_mode.width;
uint16_t *p = (uint16_t *) buffer->data;
// | jmp color_run | color | count-3 |
*p++ = COMPOSABLE_COLOR_RUN ;
*p++ = zxd_colour_words[col];
*p++ = num_pxls - 3;
*p++ = COMPOSABLE_EOL_ALIGN;
buffer->data_used = ((uint32_t *) p) - buffer->data;
assert(buffer->data_used < buffer->data_max);
buffer->status = SCANLINE_OK;
}This creates a “COLOR_RUN” line of a single colour using the mapping in zxd_colour_words[] which has been taken from the pico_zxspectrum code, but rather than providing 32-bit (duplicated) values (as described last time) I’m just using direct, single, 16-bit values:
#define VGA_RGBY_1111(r,g,b,y) ((y<<3)|(r<<2)|(g<<1)|b)
static uint16_t zxd_colour_words[16] = {
VGA_RGBY_1111(0,0,0,0), // Black
VGA_RGBY_1111(0,0,1,0), // Blue
VGA_RGBY_1111(1,0,0,0), // Red
VGA_RGBY_1111(1,0,1,0), // Magenta
VGA_RGBY_1111(0,1,0,0), // Green
VGA_RGBY_1111(0,1,1,0), // Cyan
VGA_RGBY_1111(1,1,0,0), // Yellow
VGA_RGBY_1111(1,1,1,0), // White
VGA_RGBY_1111(0,0,0,0), // Bright Black
VGA_RGBY_1111(0,0,1,1), // Bright Blue
VGA_RGBY_1111(1,0,0,1), // Bright Red
VGA_RGBY_1111(1,0,1,1), // Bright Magenta
VGA_RGBY_1111(0,1,0,1), // Bright Green
VGA_RGBY_1111(0,1,1,1), // Bright Cyan
VGA_RGBY_1111(1,1,0,1), // Bright Yellow
VGA_RGBY_1111(1,1,1,1) // Bright White
};This seems to work pretty well, but all the colours are a bit too dark. The bottom bar should be “bright white” for example, and it is a pretty dirty grey really.
I’m also not sure what is going on at the top – it looks like there is a bit of bleed-through of the last colour at the start. I don’t know if this is a sync/coordination issue with scanline number and the processing code or if this is something else.
Looking more closely at what is produced between the two modes, the only difference I could see was that I was creating a single scanline entry for the whole line of pixels, whereas the testcard pattern was splitting each line up in to 32 “bars”.
Rewriting the code to do the same, albeit with the same colour in each “bar” gives me:
void draw_zxcolor_bar(scanvideo_scanline_buffer_t *buffer) {
uint32_t line_num =
scanvideo_scanline_number(buffer->scanline_id);
uint32_t col = (line_num * 8ul) / vga_mode.height;
uint bar_width = vga_mode.width / 32;
uint16_t *p = (uint16_t *) buffer->data;
// | jmp color_run | color | count-3 |
for (uint bar = 0; bar < 32; bar++) {
*p++ = COMPOSABLE_COLOR_RUN ;
if (bar < 16) {
*p++ = zxd_colour_words[col];
} else {
*p++ = zxd_colour_words[col+8];
}
*p++ = bar_width - 3;
}
// 32 * 3, so we should be word aligned
assert(!(3u & (uintptr_t) p));
// black pixel to end line
*p++ = COMPOSABLE_RAW_1P;
*p++ = 0;
// end of line with alignment padding
*p++ = COMPOSABLE_EOL_SKIP_ALIGN;
*p++ = 0;
buffer->data_used = ((uint32_t *) p) - buffer->data;
assert(buffer->data_used < buffer->data_max);
buffer->status = SCANLINE_OK;
}This is so much better! I’m getting proper colours now.
In the first version (as shown above) it isn’t obvious if the brightness is working or not (it is – I used cut-and-paste on the photo to compare the top and the bottom!), so I created a version (given above) to show the bright and non-bright colours side by side. Now we can see the full 15 colours properly.
I have no idea why this makes a difference, but for some reason it does. My initial working theory is that having too long a COLOR_RUN causes too much time to be taken up in the PIO program, as COLOR_RUN is basically implemented as “set the colour values; then wait around for the number of pixels time before getting the next video instruction”, so a large number of pixels means the PIO is stuck in a loop without consuming data from DMA.
I don’t know what effect this will have, but with each scan line split into 32 chunks, there is more data to DMA to the PIO, but the PIO also consumes it quicker.
I don’t know why this would affect the video signal, but maybe it means there are gaps in the GPIO output video signal meaning the voltage gets averaged to a lower value somehow (a bit like how PWM would work)? If that is the case I should be able to see that on a scope…
In the following traces, yellow is the B GPIO pin and blue is the HSYNC. On the left is the correctly functioning 32-block version and on the right is the “draw a whole line in one go” version.
Zooming out a little…
Ok, so now I’ve even less idea what is going on. For the failing “write a whole line in one go” case the B GPIO never seems to let up – it is constantly HIGH. For the working case, it drops back to zero synchronised with each scan line.
Then I realised what is causing that – there is an additional black pixel added for the working case – the 32 blocks is probably irrelevant! So third try:
void draw_zxcolor_bar(scanvideo_scanline_buffer_t *buffer) {
uint32_t line_num = scanvideo_scanline_number(buffer->scanline_id);
uint32_t col = (line_num * 16ul) / vga_mode.height;
uint16_t numpxls = vga_mode.width;
uint16_t *p = (uint16_t *) buffer->data;
// | jmp color_run | color | count-3 |
*p++ = COMPOSABLE_COLOR_RUN ;
*p++ = zxd_colour_words[col];
*p++ = numpxls - 3;
*p++ = COMPOSABLE_RAW_1P;
*p++ = 0;
*p++ = COMPOSABLE_EOL_ALIGN;
buffer->data_used = ((uint32_t *) p) - buffer->data;
assert(buffer->data_used < buffer->data_max);
buffer->status = SCANLINE_OK;
}And this works!! This gives me the nice full-coloured patterns with brightness.
A bit of searching turns up this note on the scanvideo readme that I’d missed:
“Important; You MUST end the scanline with one or more black pixels of your own (otherwise your color will bleed into the blanking!!!).”
Well that explains the problem, but I still don’t really understand what “bleed into the blanking” means and why that results in a much lower intensity colour on the screen. Searching some more has not enlightened me, so if you know why this is the case, do let me know.
Update: Someone sent me a link to this, which explains it: https://www.righto.com/2018/04/#fn:blanking
“When I forgot to blank the pixels outside the valid screen area, the monitor still managed to display an image, but it was very dim because the monitor got confused about what voltage represented “dark”. Just a tip in case you find your display mysteriously darkened.”
So it is something to do with ensuring the monitor can detect what “off” looks like in the signal, which would explain why everything went screwy when it what it thought was “off” was still set to a voltage.
Anyway. It all now works. I have a working, ZX Spectrum compatible video mode implemented on a Raspberry Pi Pico using just 6 GPIO in the end (as I still have both SYNCs).
Kevin
#cga #pio #raspberryPiPico #scanvideo #vga #zxSpectrum -
What does software development, a pendulum, and the genie have to do with each other? You wouldn't think much, but in fact, just like the motion of a pendulum is dependent on it's size, weight, and how hard it's poked, software development works the same way. And the genie is poking at it. Hard.
-
#pio&amedeo
mio pensiero mattutino:
pio e amedeo sono uno dei simboli del degrado culturale italiano -
PIO on the Raspberry Pi Pico – Part 2
Having got all the theory out of the way in PIO on the Raspberry Pi Pico now is the time to actually start programming. Whilst I have the option of using the C/C++ SDK or one of the Python variants, I’m particularly interested in getting it going from within the Arduino environment, just because that is where I do pretty much all of my other microcontroller messing about.
I’m not using the official Arduino for Pico support though, I’m using Earl Philhower’s version from here: https://github.com/earlephilhower/arduino-pico
Pico Arduino Getting Started
Before getting too far into PIO land, there are a few things to note about using the unofficial Arduino Pico core with the Raspberry Pi Pico.
On first boot, hold down the BOOT switch and the Pico will be detected as a “UF2 Board”. This will allow the first upload to take place (more here). I’ve selected “Raspberry Pi Pico” or “Raspberry Pi Pico 2” as appropriate for the board.
Prior to the first download, the configuration should set the Debug Port to Serial. Then once the first sketch is downloaded the board can be redetected via a serial link which will allow both Serial.print() and automatic reset on download of new sketches.
Aside: there are three serial ports (more here):
- Serial – the USB serial port – the one used here
- Serial1 – UART0
- Serial2 – UART1
Here is a simple starter program to make sure everything is working:
void setup() {
Serial.begin(9600);
pinMode (LED_BUILTIN, OUTPUT);
}
unsigned counter;
void loop() {
Serial.println(counter);
counter++;
delay(1000);
digitalWrite (LED_BUILTIN, (counter & 1));
}Assuming everything is working, every second the LED will flash on or off and the counter value will be printed to the serial monitor.
Hello PIO
I’m starting off with a simple pulse on a GPIO pin and will be using the online PIO assembler from https://wokwi.com/tools/pioasm to build it.
My PIO Source:
.program pulse
.wrap_target
set pins, 1 [3] // 4 cycles
set pins, 0 [11] // 12 cycles
.wrap
% c-sdk {
static inline void pulse_program_init(PIO pio, uint sm, uint offset, uint pin) {
pio_sm_config c = pulse_program_get_default_config(offset);
// set_base=pin, count=1
sm_config_set_set_pins(&c, pin, 1);
pio_gpio_init(pio, pin);
// pins_base=pin, pin_count=1, is_out=true
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// 440 Hz pulse over 16 cycles
float div = (float)clock_get_hz(clk_sys) / (440.0 * 16.0);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}The online assembler turns the above into the following, which is pasted into a pulse_pio.h file within an Arduino sketch.
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// ----- //
// pulse //
// ----- //
#define pulse_wrap_target 0
#define pulse_wrap 1
static const uint16_t pulse_program_instructions[] = {
// .wrap_target
0xe301, // 0: set pins, 1 [3]
0xeb00, // 1: set pins, 0 [11]
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program pulse_program = {
.instructions = pulse_program_instructions,
.length = 2,
.origin = -1,
};
static inline pio_sm_config pulse_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + pulse_wrap_target, offset + pulse_wrap);
return c;
}
static inline void pulse_program_init(PIO pio, uint sm, uint offset, uint pin) {
pio_sm_config c = pulse_program_get_default_config(offset);
// set_base=pin, count=1
sm_config_set_set_pins(&c, pin, 1);
pio_gpio_init(pio, pin);
// pins_base=pin, pin_count=1, is_out=true
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// 440 Hz pulse over 16 cycles
float div = (float)clock_get_hz(clk_sys) / (440.0 * 16.0);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
#endifAdding the appropriate additional PIO initialisation code to my previous test sketch now gives me the following complete code:
#include <PIOProgram.h>
#include "pulse_pio.h"
#define PULSE_PIN 2
void setup() {
Serial.begin(9600);
pinMode (LED_BUILTIN, OUTPUT);
PIO pio;
uint sm, offset;
if (!pio_claim_free_sm_and_add_program(&pulse_program, &pio, &sm, &offset)) {
for (;;) {
Serial.print("No PIO or SM");
delay(10000);
}
}
pulse_program_init(pio, sm, offset, PULSE_PIN);
}
unsigned counter;
void loop() {
Serial.println(counter);
counter++;
delay(1000);
digitalWrite (LED_BUILTIN, (counter & 1));
}Notes:
- As I’m using “set” in the pio program I need to use the “set” group of pins in the various API calls – hence the use of sm_config_set_set_pins() which configures which pins to use with the set command. In this case, just one pin determined by the “pin” parameter.
- I’m using the wait [] instructions to put the pulse HIGH for 4 cycles and LOW for 12 cycles, giving 16 cycles in total.
- The waiting cycles have to account for the single cycle of the actual executed instruction, hence using [3] and [11].
- When setting the clock divisor, I’m dividing the system frequency by my required frequency * 16 as there are 16 cycles in the complete program.
- When I had a single cycle HIGH and 3 cycles LOW and then used the value 440.0 * 4.0 I wasn’t getting an accurate frequency (I was getting ~1.9K rather than 440). I’m guessing (I haven’t done the maths) this was overflowing the integer part of the divisor maybe.
The PIO and state machine used are allocated dynamically by the system using pio_claim_free_sm_and_add_program(). The first version had hard-coded PIO 0, state machine 0:
PIO pio = pio0;
int sm = 0;
uint offset = pio_add_program(pio, &pulse_program);The final result can be seen on the oscilloscope trace below.
Conclusion
I’ve now been through the theory and a real, albeit simple, application and am feeling like I understand a lot more what is going on now. I still am somewhat bewildered by the huge array of API calls and do feel like they could be grouped together somehow to make them more accessible to people who haven’t swallowed the entire chip datasheet and SDK guidebooks…
But yes, I’m slowly starting to feel like I’m getting to grips with PIO a bit more now. I want to do something that now grabs some input from the GPIO and sticks it into memory, ideally using the DMA system, so that is probably where I’ll go next.
Kevin
#pio #raspberryPiPico #rp2040 #rp2350 -
PIO on the Raspberry Pi Pico
Every time I’ve started to approach the use of the programmable IO (PIO) subsystem on the RP2040 or RP2350 (as used on the Raspberry Pi Pico), I’ve found myself essentially starting from scratch again and the examples quite opaque to me.
So this time as I’ve worked through it yet again, I’ve decided to write it all down 🙂
Here are some existing tutorials and projects that talk about getting going with the PIO:
- RP2350 Datasheet – “Chapter 11: PIO”
- Raspberry Pi Pico-series C/C++ SDK – “Chapter 3: Using Programmable I/O (PIO)”.
- Hackspace Magazine Issue 39 – Raspberry Pi Pico Programmable I/O (page 40).
- Stephen Smith’s “I/O Co-processing on the Raspberry Pi Pico”
- PIO Examples: https://github.com/raspberrypi/pico-examples?tab=readme-ov-file#pio
Assembling PIO Code
The PIO has its own bespoke micro-instruction set that is very similar to many types of assembly language and it requires its own pio assembler to process it. The basic sequence is as follows:
- PIO -> pioasm -> C header file -> include in C/C++ project and build
There are options for writing PIO in both Micropython and Circuitpython, which I have done in the past, but I’m sticking with the C route here. This requires the pioasm to take PIO code and produce a C header file that can then be included in a C project.
To use the RP2040/2350 Arduino environment, it is necessary to process PIO independently and then add the C file to the Arduino project. The Raspberry Pi C/C++ SDK can process PIO files directly as part of the main build.
There is also an option to use hardware SDK functions for dynamic creation of PIO code at runtime. The functions are a series of pio_encode_XX() functions representing the different PIO instructions as listed here: https://www.raspberrypi.com/documentation/pico-sdk/hardware.html#group_pio_instructions
There are two other novel approaches I found so far too:
- Wokwi online PIO assembler: https://wokwi.com/tools/pioasm
- Piersrocks’ runtime PIO assembler: https://github.com/piersfinlayson/apio
The first is an online editing environment that creates the required processed PIO related code for the C/C++ SDK or Python which can then be included in your build environment as required.
The second is an alternative run-time approach that uses a range of C macros to allow the “assembling” of PIO code as part of the run-time execution. It does this by directly creating the HEX equivalents of PIO instructions, thereby effectively assembling in the fly. This means that the PIO code can be customised to the specific run-time situation.
At this stage I’m not sure what it gives over using the pio_encode_ SDK functions directly. I do note however there is an equivalent PIO emulator which means this approach will run equally well on real hardware or in emulation. I’ve bookmarked this to come back to at some point.
Running PIO Code
Regardless of how the PIO instructions become code, to use them requires setting up and configuring the PIO state machines at run time as part of a project. A common approach is to include an initialisation function within the PIO code itself that is destined for passing straight through to the C/C++ SDK. This will have access to all definitions used within the PIO code and also allows the appropriate configuration information to remain encapsulated with the code.
But I have to admit I find there is an awful lot of assumed “magic” going on when configuring and getting running PIO programs and state machines. And whilst there are plenty of examples to study, I don’t find that they are written so as to teach. Consequently, I’ve noted the following as “reminders to self” on how to read some of the examples. It doesn’t help that the SDK function list is very long and there are several ways to achieve the same things.
Taking the PIO PWM code from the pico_examples as a starting point (https://github.com/raspberrypi/pico-examples/tree/master/pio/pwm), I’ve added in some comments containing the full function prototypes for some of the calls to make them a bit easier to walk through.
pwm.pio:
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;
; Side-set pin 0 is used for PWM output
.pio_version 0 // only requires PIO version 0
.program pwm
.side_set 1 opt
pull noblock side 0 ; Pull from FIFO to OSR if available, else copy X to OSR.
mov x, osr ; Copy most-recently-pulled value back to scratch X
mov y, isr ; ISR contains PWM period. Y used as counter.
countloop:
jmp x!=y noset ; Set pin high if X == Y, keep the two paths length matched
jmp skip side 1
noset:
nop ; Single dummy cycle to keep the two paths the same length
skip:
jmp y-- countloop ; Loop until Y hits 0, then pull a fresh PWM value from FIFO
% c-sdk {
static inline void pwm_program_init(PIO pio, uint sm, uint offset, uint pin) {
// static void pio_gpio_init (PIO pio, uint pin)
pio_gpio_init(pio, pin);
// int pio_sm_set_consecutive_pindirs (PIO pio, uint sm, uint pins_base, uint pin_count, bool is_out)
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// A piece of pioasm "magic" based on .program pwm (see following notes)
pio_sm_config c = pwm_program_get_default_config(offset);
// static void sm_config_set_sideset_pins (pio_sm_config *c, uint sideset_base)
sm_config_set_sideset_pins(&c, pin);
// int pio_sm_init (PIO pio, uint sm, uint initial_pc, const pio_sm_config *config)
pio_sm_init(pio, sm, offset, &c);
}
%}And its associated C code pwm.c:
/**
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "pwm.pio.h"
// Write `period` to the input shift register
void pio_pwm_set_period(PIO pio, uint sm, uint32_t period) {
pio_sm_set_enabled(pio, sm, false);
pio_sm_put_blocking(pio, sm, period);
pio_sm_exec(pio, sm, pio_encode_pull(false, false));
pio_sm_exec(pio, sm, pio_encode_out(pio_isr, 32));
pio_sm_set_enabled(pio, sm, true);
}
// Write `level` to TX FIFO. State machine will copy this into X.
void pio_pwm_set_level(PIO pio, uint sm, uint32_t level) {
pio_sm_put_blocking(pio, sm, level);
}
int main() {
stdio_init_all();
#ifndef PICO_DEFAULT_LED_PIN
#warning pio/pwm example requires a board with a regular LED
puts("Default LED pin was not defined");
#else
// todo get free sm
PIO pio = pio0;
int sm = 0;
uint offset = pio_add_program(pio, &pwm_program);
printf("Loaded program at %d\n", offset);
pwm_program_init(pio, sm, offset, PICO_DEFAULT_LED_PIN);
pio_pwm_set_period(pio, sm, (1u << 16) - 1);
int level = 0;
while (true) {
printf("Level = %d\n", level);
pio_pwm_set_level(pio, sm, level * level);
level = (level + 1) % 256;
sleep_ms(10);
}
#endif
}There are a few key things to remember to make sense of these examples:
- The offset that is talked about is (I believe) the location within the shared 32 instruction program area and is used to refer back to the installed PIO program. It is returned from pio_add_program().
- A PIO .program directive becomes a default C like directive on processing by pioasm. This results in two obscure bits of “magic” coding going on meaning in this case that “.program pwm” in the PIO file becomes “pwm_program” in C/C++:
- pio_program_t pwm_program is a C structure which can then be referenced from the C code as shown in the line pio_add_program(pio, &pwm_program).
- static inline pio_sm_config pwm_program_get_default_config(uint offset) is a C function based on pio_get_default_sm_config() that returns the PIO configuration for the specific PIO program in question – in this case of course the pwm program.
- The use of .side_step opt means that not every PIO instruction has to have a side step instruction too.
- The PIO refers to an abstract group of pins, but it is the configuration which is part of the C/C++ SDK that determines which pins are used.
- The %c-sdk { … %} pairing signifies that this part of the PIO code will be passed straight onto the C/C++ SDK.
- There are multiple ways of initialising GPIO pins and directions. In this example it doesn’t use pindirs in the PIO code but uses pio_sm_set_consecutive_pindirs() in the C code.
- This example uses hardcoded references to PIO 0 and SM 0, but in many cases the PIO and SM would be chosen dynamically using API calls such as the following:
- pio_claim_free_sm_and_add_program ()
- pio_claim_free_sm_and_add_program_for_gpio_range()
- pio_claim_unused_sm()
- Each PIO program has a default configuration associated with it which can be updated. A typical pattern is shown here where the default configuration is grabbed using (in this case) pwm_program_get_default_config() and then updated by passing into following SDK calls.
- The state machine is finally set running using pio_sm_init();
There is one additional mix of techniques that is worth pulling out here. In the C code the function pio_pwm_set_period() is used to update the PWM period which it has to do by passing it into the SM via the FIFO. It is using some SM manipulation routines and then some inline, run-time PIO code, to achieve this.
void pio_pwm_set_period(PIO pio, uint sm, uint32_t period) {
pio_sm_set_enabled(pio, sm, false);
pio_sm_put_blocking(pio, sm, period);
pio_sm_exec(pio, sm, pio_encode_pull(false, false));
pio_sm_exec(pio, sm, pio_encode_out(pio_isr, 32));
pio_sm_set_enabled(pio, sm, true);
}Again some pretty confusing API calls, especially giving this is meant to be an example, but essentially what is going on (I think) is:
Disable the statemachine by using pio_sm_set_enabled(... false).
Push the period value into the TX FIFO, blocking if full to wait for it to be empty.
Execute two direct PIO instructions using pio_sm_exec():
This uses pio_encode_pull and pio_encode_out to run the following PIO code:
pull noblock ; non-blocking pull
out isr, 32 ; out 32 bits to the interrupt shift register
Re-enable he state machine using pio_sm_set_enabled(... true).By default anything sent to the FIFO is written to the X register and used to set the duty cycle of the PWM. But this code creates some temporary PIO code to receive the contents of the FIFO and put it into ISR instead. Of course it has to temporarily suspend the execution of the stored PIO code in order to do this.
I really dislike the nomenclature of “set enabled (false)” as an API approach. I’d much prefer to see something like pio_sm_enable() and pio_sm_disable() myself. I suppose they haven’t done this due to the large increase in API functions it creates.
I guess this is personal preference, but I do find that it adds to the opaqueness of much of the example code when it doesn’t read naturally.
So To Recap…
Writing PIO code can be done at build time (from Python or C/C++ using pioasm or an online assembler) or run time (using pio_encode_ functions or maybe APIO).
pioasm bridges the gap between PIO code and C/C++ including creating two magic C/C++ constructs: pwm_program for the code and pwm_program_get_default_config() to return the created PIO configuration.
PIO and SMs can be allocated by the system using a range of “claim” functions. There are 2 PIOs on the RP2040 and 3 on the RP2350, each with its own 32 instruction program memory and each with four state machines.
It can be useful to include an initialisation routine, that configures and starts the PIO program, within the PIO code for use from the C/C++ code using % c-sdk { … %}.
The PIO program is added into the system and given an offset in instruction memory using pio_add_program.
PIO code is very dense and often the functionality cannot be seen from the PIO code itself as it is defined by the PIO configuration – e.g. pins to use, frequency of execution, direction of shifts and so on.
I’ve not touched on it here, but the use of PIO and DMA (direct memory access) often go hand in hand to create completely CPU-free means of getting data in and out of a RP2040/RP2350 system. A really good example of this is Piers Rocks’ OneROM (see this video for a brilliant summary of how this works: https://www.youtube.com/watch?v=Y8RODQZM2HY).
Finally I need to remember that ISR stands for Input Shift Register and not Interrupt Service Routine…
Kevin
#pio #raspberryPiPico #rp2040 #rp2350 -
One ROM 40: A 16-bit Kickstart Replacement With a Browser Logic Analyzer
#OneROM #Amiga #Kickstart #RetroComputing #RP2350 #PIO #DMA #LogicAnalyzer #WebAssembly #OpenSourceHardware #HardwareHacking
https://theoasisbbs.com/one-rom-40-a-16-bit-kickstart-replacement-with-a-browser-logic-analyzer/?fsp_sid=2751 -
OneROM RAM Mode Turns ROMs Into RAM
#OneROM #VIC20 #RetroComputing #Commodore #6502 #HardwareHacking #OpenSourceHardware #PIO #DMA #RP2350
https://theoasisbbs.com/onerom-ram-mode-turns-roms-into-ram/?fsp_sid=1534 -
A Deep Dive into Using PIO and DMA on the RP2350 https://hackaday.com/2025/11/30/a-deep-dive-into-using-pio-and-dma-on-the-rp2350/ #Microcontrollers #ProgrammedI/O #OneROMFire #rp2350 #dma #PIO
-
Разбираемся с композитным видеосигналом NTSC, и стоит ли изучать его в 2025 году. Часть 2
В предыдущей статье я рассказал об основах композитного видеосигнала NTSC. Эта статья должна быть интереснее, так как она посвящена программной генерации такого видеосигнала. Тема интересна тем, что помимо самого видеосигнала вы ещё получаете множество практических навыков применения современных микроконтроллеров. Сигнал CVBS можно получить, используя и FPGA-решения, но стоимость их выше, чем у микроконтроллеров, таких как Raspberry Pi Pico или ESP32. Я использовал платы разработчика на базе микроконтроллера RP2040. На рынке существует несколько таких плат. Классика — это Raspberry Pi Pico, но есть несколько китайских аналогов, например, YD-2040. Отдельно хочется выделить RP2040 Zero от Waveshare — очень компактное решение, правда у него отсутствует порт для отладки, но можно обойтись и без порта. Важный момент — СVBS-сигнал является аналоговый, поэтому стабильность напряжение на выходе играет важную роль и для приемлемого качества сигнала китайские клоны Raspberry Pi Pico могут не подойти, так как они страдают нестабильным напряжением на выходах. Мой совет — используйте или оригинальный Raspberry Pi Pico или RP2040 Zero от Waveshare. Желающих продолжить чтение приглашаю под кат.
https://habr.com/ru/companies/ruvds/articles/961086/
#ntsc #raspberrypi #raspberry_pi_pico #cvbs #композитный_видеосигнал #r2r_dac #DMA #PIO #прерывания #ruvds_статьи
-
Arduino and SP0256A-AL2 – Part 3
Following on from using an Arduino as a variable clock in Arduino and SP0256A-AL2 – Part 2, I have some ideas for a few options, but this post looks in detail at using a Raspberry Pi Pico as the clock source.
Spoilers: it kind of works, but isn’t quite the answer I need yet…
- Part 1 – Basic introduction and getting started
- Part 2 – Arduino programmable clock
- Part 3 – Using a Raspberry Pi Pico as a programmable clock
- Part 4 – Using a HC4046 PLL as the clock
- Part 5 – Using an I2C SI5351 programmable clock
- Part 6 – Adding MIDI
https://makertube.net/w/bxBYCqHrZvQLwLuwYa5Z9r
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to microcontrollers, see the Getting Started pages.
Using a RPi Pico
The RP2040 can be overclocked quite a bit, so generating a variable square wave up in the few MHz range should presumably be relatively straight forward. Using the built-in PIO state machines for a square wave is fairly simple and it can be done from Circuitpython or Micropython too.
This is a complete square wave generator for GP2 that steps down from 4MHz to 2MHz in steps of 100kHz. It can optionally overclock the RPi to 250 MHz too if required.
import time
import microcontroller
import board
import rp2pio
import adafruit_pioasm
square = adafruit_pioasm.assemble ("""
.program square
set pins, 1
set pins, 0
""")
RP2040Freq = 125_000_000
#RP2040Freq = 250_000_000
print ("RP2040 Frequency = ", microcontroller.cpu.frequency)
microcontroller.cpu.frequency = RP2040Freq
time.sleep(1)
print ("New RP2040 Frequency = ", microcontroller.cpu.frequency)
while True:
for freq in range (4000000, 2000000, -100000):
print("\nReqd frequency = ", freq*2)
print("Sq frequency = ", freq)
sm = rp2pio.StateMachine(
square,
frequency=freq*2,
first_set_pin=board.GP2,
)
print("Actual freq = {:d}".format(sm.frequency))
print("Actual sq freq = {:d}".format(int(sm.frequency/2)))
time.sleep(5)
sm.deinit()The PIO program itself has two instruction steps, so takes two cycles to complete, so the running frequency has to be twice the desired frequency of the square wave. It automatically keeps looping, so no additional instructions are required there.
The state machine will run at the system speed with a 16.8 fixed point fractional clock divider. Full details can be found in section 3.5.5 “Clock Dividers” in the RP2040 datasheet.
For certain values there might be some jitter:
If the system clock is faster though, the amount of jitter will be less I suspect, so it is advantageous to overclock the Pico for more accurate frequencies.
The problem with this approach is that whilst I get a nice accurate clock source with quite a good resolution across its range, every time the state machine is recreated to change the frequency, there is a “blip” in the audio from the SP0256A-AL2 whilst its clock temporarily disappears!
An alternative approach is to use a fixed state machine frequency but include a counter in the PIO program to allow for a configurable number of steps per scan of the PIO without having to stop and restart the clock.
The problem with this is that I am limited to a unit of the instruction time for the PIO state machine which gives a fixed overhead, in terms of the instructions required for a minimal loop, and a flexible overhead, in terms of the counter I can pass in.
The upshot of this is that I’m tied to a certain resolution of frequency change.
I have the following PIO code:
.program blink
.side_set 1
.wrap_target
pull noblock
mov x, osr
mov y, x
set pins, 1
lp1:
jmp y-- lp1
nop
nop
mov y, x
set pins, 0
lp2:
jmp y-- lp2
.wrapThe “pull” will update the output shift register (OSR) either with any new value written to the state machine or the last value of the X register. This value gets copied to Y to use as a counter. This happens twice, once for when the pin is set at 1 and once for when the pin is set at 0.
There are two nops whilst the pin is set at 1 to balance for the original pull and mov instructions at the end of the pin set to 0 cycle.
As the Y counter value is used twice, the flexible overhead of the timing is essentially proportional to count * 2. It counts for the pin being HIGH and then for the pin being LOW.
The fixed overhead is the cost of the original pull, two moves, the pin sets, and a single jmp per pin state – so by using the two nops to ensure the HIGH and LOW times are the same, that is 10 instruction cycles.
I was hoping to use the “side” instruction to eliminate the two set instructions, but so far I’ve not managed to get that to work. I still don’t understand PIO…
So for now the timing of the PIO routine is = 10 + 2 * count and the unit is the time for a single instruction, which is 1 / frequency of the PIO execution, up to a maximum frequency of the Pico’s system clock frequency.
Using an overclocked Pico at 250MHz, the frequency range would start at the following:
- Execution freq = Pico Sys Clock / (10 + 2 * count)
- So when count = 0; execution freq = 250MHZ / 10 = 25 MHz
That is far too fast for the SP0256A-AL5. In fact, I’ve found that anything over around 5MHz causes the chip problems.
For this reason, I’m using a minimum count of 20:
- Max execution freq = 250MHz / (10 + 2 * 20) = 5 MHz
Plotting execution frequency per “count” value (starting from 20) gives the following:
We can see the limits of the resolution at the top-end, and in fact, the first few equivalent frequencies in that range are as follows:
CountEquivalent Frequency205,000,000214,807,692224,629,629234,464,285244,310,344That is giving me something like a 150-200kHz jump each time, which isn’t great, but is probably the best I can do. I would be larger if I wasn’t overclocking the Pico. It does get smaller as the count increases, but it is only really worth going down to a count value of around 120, which is around 1MHz for the resulting clock. Anything lower than that and the SP0256A-AL2 isn’t particularly useful.
Here is the full Circuitpython code which attaches a pot to GP26 to control the frequency in the range of around 900kHz up to 5MHz. Note the scaling of the pot value (0 to 65535) by 600 prior to its use to add to the count.
import array
import time
import board
import rp2pio
import microcontroller
import adafruit_pioasm
from analogio import AnalogIn
algin = AnalogIn(board.GP26) # ADC0
blink = adafruit_pioasm.assemble(
"""
.program blink
.side_set 1
.wrap_target
pull noblock
mov x, osr
mov y, x
set pins, 1
lp1:
jmp y-- lp1
nop
nop
mov y, x
set pins, 0
lp2:
jmp y-- lp2
.wrap
"""
)
RP2040Freq = 250_000_000
microcontroller.cpu.frequency = RP2040Freq
time.sleep(1)
oldalgval = 0
sm = rp2pio.StateMachine(
blink,
frequency=RP2040Freq,
first_set_pin=board.GP2
)
sm.write(bytes(16))
while True:
algval = algin.value
if (algval != oldalgval):
oldalgval = algval
count = 20 + int(algval / 600)
freq = int (RP2040Freq / (10 + count*2))
data = array.array("I", [count])
sm.write(data)
time.sleep(0.2)One problem will be the 3V3 operating levels of the Pico. The SP0256A-AL2 datasheet states the following:
So whilst a “high logic” value for the oscillator has a minimum level of 2.5V, it also states that a minimum of 3.9V is required if driven from an external source.
If required, something like a 74HCT14, powered by 5V, can be used to level shift the 3V3 output of the Pico to a 5V signal for use with the SP0256A-AL2.
But in practice, I was finding the Pico worked fine as is. It is important to ensure both the Pico, Arduino and SP0256A-AL2 all have their grounds connected.
A this point I’m just using the Pico as a programmable clock, but if I was to go this route, then it would make sense to have the Pico drive the SP0256A-AL2 too and forgo the Arduino.
Closing Thoughts
So I have two choices if I want to use a Raspberry Pi Pico:
- Go for smooth changes of frequency, but with less resolution, especially at the higher frequencies.
- Go for more accurate resolution across the range but accept there will be blips when the clock changes which will be heard in the audio.
Neither is a perfect solution, but it shows the principles are valid. Also, using two microcontrollers is a bit over the top, so if I was to move to using a Pico, I’d probably want to find a way to drive the SP0256A from the Pico directly too and skip using an Arduino.
One benefit of that would be that I can time the frequency changes to coincide with silence in the speaking should I wish to, avoiding the possibility of major audio blips.
But I also have a few other options to try, which I’ll come back to in a future post.
Kevin
-
Aviation weather for Captain Renán Elías Olivera International airport in Pisco area (Peru) is “SPSO 131900Z 30006KT 270V330 3000 BR OVC017 19/16 Q1013 RMK BIRD HAZARD RWY 22/04 PP000” : See what it means on https://www.bigorre.org/aero/meteo/spso/en #pisco #peru #captainrenaneliasoliverainternationalairport #spso #pio #metar #aviation #aviationweather #avgeek #airport vl
-
Read Motor Speed Better By Making The RP2040 PIO Do It https://hackaday.com/2025/04/29/read-motor-speed-better-by-making-the-rp2040-pio-do-it/ #quadratureencoder #Microcontrollers #motorspeed #rp2040 #PIO
-
Read Motor Speed Better By Making The RP2040 PIO Do It - A quadrature encoder provides a way to let hardware read movement (and direction) ... - https://hackaday.com/2025/04/29/read-motor-speed-better-by-making-the-rp2040-pio-do-it/ #quadratureencoder #microcontrollers #motorspeed #rp2040 #pio
-
Riddle:
Executing a WAIT instruction with 24 delay cycles should take 25 cycles in total
Except?
nop 24 waits 14 cycles to and including out pins
nop 26 waits 15 cycles to and including out pins
nop 25 waits 15 cycles to and including out pins
nop 14 waits 9 cycles to and including out pins
so 10 nops is 5 cycles? WTF!?What am I missing or overlooking here?
#raspberrypio #pico #pio #rp2040 #rp2350
(4/4) -
Erasing meaning is the whole point of making it an acronym if you're a #GOP #PIO
Like saying #BLM instead of #BlackLivesMatter
Like talking about a #COLA instead of a #CostOfLiving IncreaseThey only say " #CriticalRaceTheory " bc they know those are the three scariest words their base can hear (after "taxes")
-
Создаём эмулятор легендарной игры «Ну, Погоди» на базе Raspberry Pi Pico
Многие из тех, кому сейчас за 30, и рождённых в СССР или на постсоветском пространстве, помнят электронную игру «Ну, погоди!». Во времена, когда не было ни интернета, ни ноутбуков, ни мобильных телефонов, а из общедоступных электронных развлечений были только аттракционы в парках культуры и видеосалоны, обладание бытовым компьютером, электронными наручными часами Montana или электронной игрой «Ну, погоди!» было мечтой многих детей. Были ещё и другие электронные игры, но именно «Ну, погоди!» считается классикой. Игре посвящено много ностальгических статей и видео. На различных торговых площадках можно купить её в различном состоянии от убитого до «с хранения» и даже новодел. Лет 10 назад и я купил её в идеальном состоянии, поигрался, вспомнил детство и положил в ящик. Но несколько месяцев назад с разочарованием увидел, что «потекла» нижняя часть экрана. Можно было или отремонтировать, или купить другой экземпляр игры, но я сначала попробовал узнать, как её отремонтировать, а потом решил воссоздать игру на современных компонентах. Я не был одинок в своём желании воссоздать игру, этой теме посвящено также немало статей, но в них обычно создавали симуляторы, а не эмуляторы игры. Симулятор у меня ассоциируется с фразой: «Я художник, я так вижу», эмулятор — это более точное воспроизведение устройства. Формат статьи не позволяет выразить все те ощущения, которые я испытал при путешествии от зарождения идеи до реально работающей игры, практически ничем не отличающейся от оригинала. Много из того, что я узнал в этом путешествии, не поместилось в статью или поместилось в очень сжатом виде. Эмулятор максимально приближен к оригиналу, если не считать экран (он не сегментный, как в оригинале) и корпус (я пока реализовал на беспаечной макетной плате). Если вам интересно, как за несколько вечеров воссоздать у себя эмулятор «Ну, погоди!» на современном микроконтроллере или просто поностальгировать, добро пожаловать под кат.
https://habr.com/ru/companies/ruvds/articles/889414/
#ruvds_статьи #raspberry_pi_pico #pio #dma #эмулятор #ну_погоди #fritzing
-
Anyone know if the Circuitpython Neopixel implementation uses PIO on the RP2040? The existence of neopixel_write in the HAL for CP implies it does...
But if so, what if you have an application that also wants to use PIO? How are conflicts managed?
-
Supercon 2023 – Going into Deep Logic Waters With The Pico’s PIO And The Pi’s SMI https://hackaday.com/2024/09/30/supercon-2023-going-into-deep-logic-waters-with-the-picos-pio-and-the-pis-smi/ #2023HackadaySuperconference #HackadayColumns #RaspberryPiPico #RaspberryPi #raspberrypi #pipico #rp2040 #cons #PIO #SMI