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.