Developing in C on the RP2040: Exploring Blink

8 minute read

Where I develop multiple versions of blink to better understand the timing API and bit-setting hardware API in the Pico C SDK in learning how to program the Pico in C.

Sources

Introduction

The Pi Pico family of microcontroller boards have an incredible price/performance ratio. Ranging in price from $4 to $10, the Pico can provide a low-cost, high-performance dual-core Cortex M0-based board with or without wireless or installed headers (for easy breadboarding).

There are mulitple methods of programming the Pico, MicroPython/CircuitPython, Arduino framework and the C Language. In the series “Developing in C on the RP2040:”, I’m using C and the Pico C SDK. This is easily the most complex approach and one that stretches my abilities, however, that is the point of this blog. :)

Using the program blink is the standard practice to demonstrate how to program a microcontroller. The Pi Pico documentation is no different. What I will do in this entry is explore different methods of blinking the LED (or LEDs) to develop a better understanding of how to program the Pico.

Repository

All code (along with other examples) are this repository.

The initial instance of blink is quite simple and pretty much the same as the more famous Arduino version:

#include "pico/stdlib.h"

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    const uint interval = 50;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
        gpio_put(LED_PIN, 1);
        sleep_ms(interval);
        gpio_put(LED_PIN, 0);
        sleep_ms(interval);
    }
}

Works great and as I’ve said before, its always valuable to have to test your total system.

I then changed the program by adding another LED and moving away from the built-in LEDs. More importantly I explored what was needed to add another example in the pico-examples folder. Once I was able to easily add more examples, I went back to changing blink.

Process for a new program in pico-examples

  1. Duplicate existing folder, such as blink to blink_multi
  2. Change the name of the .c file from blink to blink_multi
  3. In the local CMAKELists.txt file change all blink references to blink_multi
  4. In CMAKELists at root, duplicate the single line for blink and change to blink_multi:
# Add blink example
add_subdirectory(blink)

# Change blink references to blink_multi
add_subdirectory(blink_multi)
  1. Follow original instructions:
# assuming in folder pico/pico-examples/build/blink
cd ..
cmake ..
cd blink_multi
make -j4
picotool load blink_multi.uf2
  1. Once cmake has been run, you may simply use the following two commands in the local build directory. For example, blink_multi would be (once in the build/blink_multi directory):
# assuming in folder pico/pico-examples/build/blink_multi
make -j4
picotool load blink_multi.uf2

This example is very simple, not much more to add:

// blink_multi - blink multiple pins
#include "pico/stdlib.h"

int main() {
    const uint LED_0 = PICO_DEFAULT_LED_PIN;
    const uint INTERVAL_0 = 500;
    gpio_init(LED_0);
    gpio_set_dir(LED_0, GPIO_OUT);
    const uint LED_1 = 15;
    const uint INTERVAL_1 = 500;
    gpio_init(LED_1);
    gpio_set_dir(LED_1, GPIO_OUT);
    const uint LED_2 = 14;
    const uint INTERVAL_2 = 500;
    gpio_init(LED_2);
    gpio_set_dir(LED_2, GPIO_OUT);
    while (true) {
        gpio_put(LED_0, 1);
        sleep_ms(INTERVAL_0);
        gpio_put(LED_0, 0);
        sleep_ms(INTERVAL_0);
        gpio_put(LED_1, 1);
        sleep_ms(INTERVAL_1);
        gpio_put(LED_1, 0);
        sleep_ms(INTERVAL_1);
        gpio_put(LED_2, 1);
        sleep_ms(INTERVAL_2);
        gpio_put(LED_2, 0);
        sleep_ms(INTERVAL_2);
    }
}

This version is an improvement on the blink_multi as it moves from straight-line code to using an array and looping to accomplish blinking multiple LEDs. This would be the preferred method than blink_multi, as it would be easier to change the LED pins and the blink intervals.

Notice I now initialize the two arrays, the LED[] array with pin numbers and the INTERVAL[] array with delay times. One could easily add a third array to have a different off time versus the on time. With the array initialization easily accessed in one location, changing the times or pins is quite easy.

// blink_array - blink multiple pins
#include "pico/stdlib.h"

int main() {
    const uint LED[] = {PICO_DEFAULT_LED_PIN, 15, 14, 13, 12};
    const uint INTERVAL[] = {50, 100, 200, 400, 800};
    for (uint8_t i = 0; i<5;i++)
    {    
            gpio_init(LED[i]);
            gpio_set_dir(LED[i], GPIO_OUT);
    }

    while (true) {
        for (uint8_t i = 0; i<5;i++)
        {    
            gpio_put(LED[i], 1);
            sleep_ms(INTERVAL[i]);
        }
        for (uint8_t i = 0; i<5;i++)
        {    
                gpio_put(LED[i], 0);
                sleep_ms(INTERVAL[i]);
        }
    }
}

This version becomes the version, I usually use. It incorporates a struct array for easier initialization and it is non-blocking. I like this version a lot and use it as a starting point in many Arduino simple control programs. For more information on this approach, here is how I use it on the Uno and in detail.

// blink_struct - blink multiple pins non-blocking, using a struct
// Uses the Arduino method of non-blocking by tracking time expired
// uses static uint32_t time_us_32 (void) instead of millis()
#include "pico/stdlib.h"

#define N_LEDS 4
struct blinker
{
  uint8_t ledPin;           // the number of the LED pin
  bool state;          // the state (HIGH/LOW) the LED pin
  uint32_t interval;      // interval at which to blink (milliseconds)
  uint32_t previous_micros;   // will store last time LED was updated
} ;

struct blinker LEDS[N_LEDS];

void init_blink(uint8_t index, uint8_t ledPin, uint8_t state, 
    uint32_t interval, uint32_t previous_micros)
{
    LEDS[index].ledPin = ledPin;
    LEDS[index].state = state;
    LEDS[index].interval = interval;
    LEDS[index].previous_micros = previous_micros;

    gpio_init(LEDS[index].ledPin);
    gpio_set_dir(LEDS[index].ledPin, GPIO_OUT);

}

int main (void)
{
    init_blink(0, 15, false, 100000, 0);
    init_blink(1, 14, false, 200000, 0);
    init_blink(2, 13, false, 400000, 0);
    init_blink(3, 12, false, 800000, 0);

    while(1)
    {
        uint32_t current_micros = time_us_32();
        for (uint8_t i = 0; i < N_LEDS; i++)
        {
            if(current_micros - LEDS[i].previous_micros > LEDS[i].interval) 
            {
                LEDS[i].previous_micros = current_micros;   
                LEDS[i].state = !LEDS[i].state;
                gpio_put(LEDS[i].ledPin, LEDS[i].state);
            }
        }
    }
}

For this version, I wanted to move away from the gpio_put() or “digitalWrite()"-style approach, which is similar to how its used in the Arduino. I wanted to use what I use in my AVR_C, where I use port bit-manipulation. I prefer this method, as it is more powerful and higher performance (if required).

It follows the same approach as the original blink, except it uses the lower level hardware_gpio API. I find this approach more interesting and I used a similar approach in my RP2040 Forth examples.

Most importantly, this approach set me up to easily use the preferred version of timing, a repeating timer.

gpio API Overview

Its best to have the Pico pinout on your screen when you are using the hardware API. As you will want to know the GPIO number of the pin you are referencing. The GPIO pins on the Pico can be quite complex, however, in this context we will be using them in the same fashion as the digital pins D0-D13 on the Arduino Uno. This functionality is called SIO, Function 5 or GPIO_FUNC_SIO.

There three actions you need to take with a GPIO pin in this state:

  1. Set the GPIO as SIO functionality, i.e. gpio_init()
  2. Set the direction, INPUT or OUTPUT i.e. gpio_set_dir()
  3. Set the value of the pin, HIGH, LOW, or TOGGLE i.e. gpio_set()

There are two ways to designate pins in the above steps, either by GPIO number or via a mask, which uses a binary representation of the 29 ports (0-28) in a UINT32_T number as in 0b0000 0000 0000 0000 0000 0000 0000 00011 would set the lowest two bits. For ease, it will assume all bits not referenced as 0, so you could set the lowest 2 as 0b0011 or 0x3 in hexadecimal.

So taking the same three actions using a mask:

# Use a mask to identify pins 12-15
#define PIN_ALL_MASK    0b1111000000000000

# initialize the pins to SIO
gpio_init_mask(PIN_ALL_MASK);

# set the direction, OUTPUT
gpio_set_dir_out_masked(PIN_ALL_MASK);

# set the value of the pins (in this case CLR sets low)
gpio_clr_mask(PIN_ALL_MASK);

Using the commands in a program, I also broke out the blink action into a function, so I could add other functions which might blink different pins or perform a different action.

// blink_bit - uses bit operations to blink pins

#include "pico/stdlib.h"
#define PIN_ALL_MASK    0b1111000000000000

const uint interval = 1000;

void blink_all(void)
{
    for (int i = 0; i< 8; i++)
    {
        gpio_xor_mask(PIN_ALL_MASK);
        sleep_ms(interval);    
    }    
}

int main() {
    gpio_init_mask(PIN_ALL_MASK);
    gpio_set_dir_masked(PIN_ALL_MASK, PIN_ALL_MASK);

    blink_all();
    return 0;
}

To develop this version, I needed to spend some time understanding the program “hello_timer” in the pico-examples folder. Its a bit like setting up a PWM, in the sense, the microcontroller will take over the timing once it is properly defined. From the add_repeating_timer_us API :

- static bool add_repeating_timer_us (int64_t delay_us, repeating_timer_callback_t callback, void *user_data, repeating_timer_t *out)
 Add a repeating timer that is called repeatedly at the specified interval in microseconds. 

Programmatically, you need to do the following:

  • write the call back function, this is what you want to happen when the timer fires
  • set aside memory for each specific instance
  • assign a value to user_data, if required by the call back function
  • determine the interval and the timing approach used by the timer, for this point follow this guideline: the repeat delay in microseconds; if >0 then this is the delay between one callback ending and the next starting; if <0 then this is the negative of the time between the starts of the callbacks.

The program uses the bit manipulation API, to simply toggle the bit, the bit is passed using user_data.

// blink_timer - uses the repeating timer function for timing
// passes the pin bit number using user_data
#include <stdio.h>
#include "pico/stdlib.h"

#define PIN_ALL_MASK    0b1111000000000000
#define PIN_12_MASK     0b0001000000000000
#define PIN_13_MASK     0b0010000000000000
#define PIN_14_MASK     0b0100000000000000
#define PIN_15_MASK     0b1000000000000000

bool toggle_led(struct repeating_timer *t) 
{
    gpio_xor_mask(*(int*) t->user_data);
    printf("Repeat at %lld\n", time_us_64());
    return true;
}

int main() {
    stdio_init_all();
    printf("Hello Timer!\n");
    gpio_init_mask(PIN_ALL_MASK);
    gpio_set_dir_out_masked(PIN_ALL_MASK);

    // Create a repeating timer that calls toggle_led.
    uint PIN0 = PIN_12_MASK;
    uint PIN1 = PIN_13_MASK;
    uint PIN2 = PIN_14_MASK;
    uint PIN3 = PIN_15_MASK;
    struct repeating_timer timer0;
    struct repeating_timer timer1;
    struct repeating_timer timer2;
    struct repeating_timer timer3;

    const uint32_t PRIME_INTERVAL = 400000;

    add_repeating_timer_us(PRIME_INTERVAL*8, toggle_led, &PIN0, &timer0);
    add_repeating_timer_us(PRIME_INTERVAL*4, toggle_led, &PIN1, &timer1);
    add_repeating_timer_us(PRIME_INTERVAL*2, toggle_led, &PIN2, &timer2);
    add_repeating_timer_us(PRIME_INTERVAL, toggle_led, &PIN3, &timer3);
    busy_wait_us_32(PRIME_INTERVAL*16);
    gpio_clr_mask(PIN_ALL_MASK);
    return 0;
}

Similar to the PWM functionality, this approach has its advantages. Its light in execution cycles and memory, however, it can be more complicated to setup specific timings.

GPIO API

As seen above, the GPIO hardware API’s are being used to control the pins as compared to the gpio_put() used in the blink example. This approach coupled with the repeating_timer functionality, provides tremendous flexibility.

You could setup 3 repeating timers, one each for toggle, set high and set low, respectively. Then change the times as desired to create fairly complicated square waves.

Given the price of a Pico ($4) and the number of GPIO pins (29), you could make a pretty sophisticated persistence-of-vision (POV) device!

Comments powered by Talkyard.