Initializing Hardware on Raspberry Pi PICO

Published 2026-03-02

In the Simple Real-Time Scheduling article I showed a main() function:

#include "main.h"

main ()
{
    initialize_hw ();
    for (;;) {
        do_work ();
    }
}

and explained how to set up scheduling of your tasks. In this article, we'll look at the initialize_hw () function and some common conventions that I use.

First off, I like to set up a hardware map in my main.h file with the following empty template:

/*
 *  main.h
*/

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

#define KILO            (1000)
#define MEGA            (KILO * KILO)
#define GIGA            (KILO * MEGA)

#define NELEMENTS(_x) ((sizeof(_x)/sizeof((_x)[0])))

/*
 *  GPIO Map
 *
 *  Pin     GPIO    UART    I2C     SPI     ADC         Function
 *  ---     ----    ----    -----   ------  ------      ------------------------------------------
 *   1       0      TX 0    SDA 0   RX 0                ** AVAILABLE **
 *   2       1      RX 0    SCL 0   CSn 0               ** AVAILABLE **
 *   4       2              SDA 1   SCK 0               ** AVAILABLE **
 *   5       3              SCL 1   TX 0                ** AVAILABLE **
 *   6       4      TX 1    SDA 0   RX 0                ** AVAILABLE **
 *   7       5      RX 1    SCL 0   CSn 0               ** AVAILABLE **
 *   9       6              SDA 1   SCK 0               ** AVAILABLE **
 *  10       7              SCL 1   TX 0                ** AVAILABLE **
 *  11       8      TX 1    SDA 0   RX 1                ** AVAILABLE **
 *  12       9      RX 1    SCL 0   CSn 1               ** AVAILABLE **
 *  14       10             SDA 1   SCK 1               ** AVAILABLE **
 *  15       11             SCL 1   TX 1                ** AVAILABLE **
 *  16       12     TX 0    SDA 0   RX 1                ** AVAILABLE **
 *  17       13     RX 0    SCL 0   Csn 1               ** AVAILABLE **
 *  19       14             SDA !   SCK 1               ** AVAILABLE **
 *  20       15             SCL 1   TX 1                ** AVAILABLE **
 *  21       16                                         ** AVAILABLE **
 *  22       17     RX 0    SCL 0   CSn 0               ** AVAILABLE **
 *  24       18             SDA 1   SCK 0               ** AVAILABLE **
 *  25       19             SCL 1   TX 0                ** AVAILABLE **
 *  26       20             SDA 0                       ** AVAILABLE **
 *  27       21             SCL 0                       ** AVAILABLE **
 *  29       22                                         ** AVAILABLE **
 *  31       26             SDA 1           ADC 0       ** AVAILABLE **
 *  32       27             SCL 1           ADC 1       ** AVAILABLE **
 *  34       28                             ADC 2       ** AVAILABLE **
*/

This is a handy reminder of which GPIOs, pins, and functions are available to the project. As I allocate I/Os, I fill in the table and assign manifest constants (#defines). For example, in the IBM PC/AT Keyboard Interface project, I have the following:

#define SERIAL_ENABLE    0
#define LOCAL_ENABLE     1
#define SERIAL_TX        4
#define SERIAL_RX        5
#define PCAT_DATA_IN    13
#define PCAT_DATA_OUT   14
#define PCAT_CLOCK_OUT  15
#define PCAT_CLOCK_IN   16
#define GPIO_BASE       PCAT_DATA_IN

/*
 *  GPIO Map
 *
 *  Pin     GPIO    UART    I2C     SPI     ADC         Function
 *  ---     ----    ----    -----   ------  ------      ------------------------------------------
 *   1       0                                          SERIAL_ENABLE
 *   2       1                                          LOCAL_ENABLE (Rev B)
 *   4       2              SDA 1   SCK 0               ** AVAILABLE **
 *   5       3              SCL 1   TX 0                ** AVAILABLE **
 *   6       4      TX 1                                Serial transmit
 *   7       5      RX 1                                Serial receive
 *   9       6              SDA 1   SCK 0               ** AVAILABLE **
 *  10       7              SCL 1   TX 0                ** AVAILABLE **
 *  11       8      TX 1    SDA 0   RX 1                ** AVAILABLE **
 *  12       9      RX 1    SCL 0   CSn 1               ** AVAILABLE **
 *  14       10             SDA 1   SCK 1               ** AVAILABLE **
 *  15       11             SCL 1   TX 1                ** AVAILABLE **
 *  16       12     TX 0    SDA 0   RX 1                ** AVAILABLE **
 *  17       13                                         PCAT_DATA_IN
 *  19       14                                         PCAT_DATA_OUT
 *  20       15                                         PCAT_CLOCK_OUT
 *  21       16                                         PCAT_CLOCK_IN
 *  22       17     RX 0    SCL 0   CSn 0               ** AVAILABLE **
 *  24       18             SDA 1   SCK 0               ** AVAILABLE **
 *  25       19             SCL 1   TX 0                ** AVAILABLE **
 *  26       20             SDA 0                       ** AVAILABLE **
 *  27       21             SCL 0                       ** AVAILABLE **
 *  29       22                                         ** AVAILABLE **
 *  31       26             SDA 1           ADC 0       ** AVAILABLE **
 *  32       27             SCL 1           ADC 1       ** AVAILABLE **
 *  34       28                             ADC 2       ** AVAILABLE **
*/

Notice that I eliminate the alternate pin mappings as I allocate, so for example, pin 1 (#define SERIAL_ENABLE 0) started as AVAILABLE in my template and could have been a GPIO, UART TX 0, I2C SDA 0 or SPI RX 0, but ended up being just a plain GPIO pin:

 *  Pin     GPIO    UART    I2C     SPI     ADC         Function
 *  ---     ----    ----    -----   ------  ------      ------------------------------------------
 *   1       0      TX 0    SDA 0   RX 0                ** AVAILABLE **
 *   1       0                                          SERIAL_ENABLE

For complicated projects with lots of input and output GPIOs, I prefer to put the inputs and outputs into an array and initialize them en masse:

static unsigned switches  [] = {SW_A, SW_B, SW_C, SW_D};
static unsigned addresses [] = {A0, A1, A2, A3};        // 74154 decoder
static unsigned enables   [] = {ENABLE};                // Enable (active low)
static unsigned cathodes  [] = {IN_A, IN_B, IN_C, IN_D, IN_E, IN_F, IN_G, IN_DP };

static void
init_ios (unsigned *gpios, unsigned n, int dir)
{
    for (unsigned i = 0; i < n; i++) {
        gpio_init (gpios [i]);
        gpio_set_dir (gpios [i], dir);
        if (dir == GPIO_IN) {
            gpio_pull_down (gpios [i]);
        }
    }
}

void
initialize_hw (void)
{
    // map all the hardware points to the appropriate types
    init_ios (switches,  NELEMENTS (switches),  GPIO_IN);
    init_ios (addresses, NELEMENTS (addresses), GPIO_OUT);
    init_ios (enables,   NELEMENTS (enables),   GPIO_OUT);
    init_ios (cathodes,  NELEMENTS (cathodes),  GPIO_OUT);
}

Where I'm scanning lots of I/O points (as above), I can use the static unsigned arrays as both an initialization target as well as a convenient ordered reference.