Understanding the C/C++ SDK architecture for the Raspberry Pi Pico

V. Hunter Adams (vha3@cornell.edu)

All of the information in this webpage comes from the Raspberry Pi Pico C/C++ SDK. Many parts of this webpage come directly from this document, slightly rewritten or re-ordered as an exercise in understanding the content and arranging it in a way that is most useful to me.


INTERFACE libraries

All libraries within the SDK (with the exception of the C/C++ standard libraries provided by the compiler) are INTERFACE libraries. A CMake INTERFACE library is a collection of:

  • Source files
  • Include paths
  • Compiler definitions (visible to code as #defines)
  • Compile and link options
  • Dependencies on other INTERFACE libraries

All of these INTERFACE libraries form a tree of dependencies, each contributing source files, include paths, compiler definitions, and compile/link options to the build. Collection of all of these dependencies is done recursively. They are collected based on the libraries you have listed in your CMakeLists.txt file, and by the libraries depended on by those libraries, and so on.

A single project may contain many executables, as is the case for the pico-examples project. All of the code for each executable, including the SDK libraries, is (re)compiled for each executable from source. This allows you to specify customised settings for those libraries on a per-application basis, at compile-time.


SDK library structure

High-level API's

The SDK libraries are arranged heirarchically. There are high-level libraries (pico_xxxx) that generally enable the user to do things that have cross-cutting concerns between various pieces of hardware. For example, the sleep_ functions in pico_time must be aware of both the RP2040's timer hardware and with the way that the RP2040 enters and exits low-power states.

Generally speaking, these libraries are build upon one or more lower-level hardware_ libraries, and often depend on one another. Section 4.2 of the SDK guide lists all these high-level libraries. This list is copied below.

  • pico_multicore - Adds support for running code on the second processor core (core1)
    • fifo - Functions for inter-core FIFO
  • pico_stdlib - Aggregation of a core subset of Raspberry Pi Pico SDK libraries used by most executables along with some additional utility methods. Including pico_stdlib gives you everything you need to get a basic program running which prints to stdout or flashes a LED.
  • pico_sync - synchronizations primitives and mutual exclusion
    • critical_section - Critical Section API for short-lived mutual exclusion safe for IRQ and multi-core.
    • mutex - mutex API for non IRQ mutual exclusion events between cores
    • sem - semaphore API for restricting access to a resource
  • pico_time - API for accurate timestamps, sleeping, and time based callbacks.
    • timestamp - Timestamp functions relating to points in time (including the current time)
    • sleep - Sleep functions for delaying execution in a lower power state.
    • alarm - Alarm functions for scheduling future execution.
    • repeating_timer - Repeating Timer functions for simple scheduling of repeated execution.
  • pico_unique_id - Unique device ID access API.
  • pico_util - Useful data structures and utility functions.
    • datetime - Date/Time formatting.
    • pheap - Pairing Heap Implementation.
    • queue - Multi-core and IRQ safe queue implementation.

Section 4.2 contains some example code and the API description for these high-level functions.

Runtime support libraries

Section 4.4 of the SDK guide provides a description of all runtime libraries that bundle functionality which is common to most RP2040-based applications. Each of these API's is described thoroughly in the SDK guide, but they are listed here for reference.

  • boot_stage2 - Second stage boot loaders responsible for setting up external flash.
  • pico_base - Core types and macros for the Raspberry Pi Pico SDK. This header is intended to be included by all source code.
  • pico_bit_ops - Optimized bit manipulation functions. Additionally provides replacement implementations of the compiler built-ins builtin_popcount, builtinclz and \_bulitin_ctz.
  • pico_bootrom - Access to functions and data in the RP2040 bootrom.
  • pico_cxx_options - non-code library controlling C++ related compile options
  • pico_divider - Optimized 32 and 64 bit division functions accelerated by the RP2040 hardware divider. Additionally provides integration with the C / and % operators.
  • pico_double - Optimized double-precision floating point functions.
  • pico_float - Optimized single-precision floating point functions.
  • pico_int64_ops - Optimized replacement implementations of the compiler built-in 64 bit multiplication.
  • pico_malloc - Multi-core safety for malloc, calloc and free.
  • pico_mem_ops - Provides optimized replacement implementations of the compiler built-in memcpy, memset and related functions:
  • pico_platform - Compiler definitions for the selected PICO_PLATFORM.
  • pico_printf - Compact replacement for printf by Marco Paland (info@paland.com)
  • pico_runtime - Aggregate runtime support including pico_bit_ops, pico_divider, pico_double, pico_int64_ops, pico_float, pico_malloc, pico_mem_ops and pico_standard_link.
  • pico_stdio - Customized stdio support allowing for input and output from UART, USB, semi-hosting etc.
    • pico_stdio_semihosting - Experimental support for stdout using RAM semihosting.
    • pico_stdio_uart - Support for stdin/stdout using UART.
    • pico_stdio_usb - Support for stdin/stdout over USB serial (CDC)
  • pico_standard_link - Standard link step providing the basics for creating a runnable binary.

As an example of the heirarchical nature of this SDK, both pico_runtime and pico_standard_link are included with pico_stdlib. And, furthermore, pico_runtime itself includes pico_bit_ops, pico_divider, pico_double, pico_int64_ops, pico_float, pico_malloc, pico_mem_ops and pico_standard_link.

Hardware support libraries

See section 4.1. These are individual libraries (hardware_xxx) that provide actual API's for interacting with each piece of physical hardware/peripheral. They are lightweight and provide only thin abstractions. They generally provide functions for configuring or interacting with the peripheral hardware at a functional level, rather than accessing registers directly. For example:

pio_sm_set_wrap(pio, sm, bottom, top) ;

instead of

pio->sm[sm].execctrl =
$\hspace{1cm}$(pio->sm[sm].execctrl & ~(PIO_SM0_EXECCTRL_WRAP_TOP_BITS | PIO_SM0_EXECCTRL_WRAP_BOTTOM_BITS)) |
(bottom << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB) | (top << PIO_SM0_EXECCTRL_WRAP_TOP_LSB);

These libraries are intended to have very minimal runtime cost. They generally do not require any or much RAM, and do not rely on other runtime infrastructure. In general, their onlly dpendencies are the hardware_structs and hardware_regs libraries that contain definitions of memory-mapped register layout on the RP2040. Many of them are implemented as static inline functions, the idea being that you sacrifice no performance by using these functions as compared with using preprocessor macros with the hardware_regs definitions.

  • hardware_adc - Analog to Digital Converter (ADC) API.
  • hardware_base - Low-level types and (atomic) accessors for memory-mapped hardware registers.
  • hardware_claim - Lightweight hardware resource management.
  • hardware_clocks - Clock Management API.
  • hardware_divider - Low-level hardware-divider access.
  • hardware_dma - DMA Controller API.
    • channel_config - DMA channel configuration.
  • hardware_flash - Low level flash programming and erase API.
  • hardware_gpio - General Purpose Input/Output (GPIO) API.
  • hardware_i2c - I2C Controller API.
  • hardware_interp - Hardware Interpolator API.
    • interp_config - Interpolator configuration.
  • hardware_irq - Hardware interrupt handling.
  • hardware_pio - Programmable I/O (PIO) API.
    • sm_config - PIO state machine configuration.
  • hardware_pll - Phase Locked Loop control APIs.
  • hardware_pwm - Hardware Pulse Width Modulation (PWM) API.
  • hardware_resets - Hardware Reset API.
  • hardware_rtc - Hardware Real Time Clock API.
  • hardware_spi - Hardware SPI API.
  • hardware_sync - Low level hardware spin-lock, barrier and processor event API.
  • hardware_timer - Low-level hardware timer API.
  • hardware_uart - Hardware UART API.
  • hardware_vreg - Voltage Regulation API.
  • hardware_watchdog - Hardware Watchdog Timer API.
  • hardware_xosc - Crystal Oscillator (XOSC) API.

Hardware structs library

The hardware_structs library provides a set of C structures which represent the memory mapped layout of the RP2040 registers in the system address space. This allows the user to replace code that looks like this (written in C with defines from the lower-level hardware_regs, described in the next section):

*(volatile uint32_t *)(PIO0_BASE + PIO_SM1_SHIFTCTRL_OFFSET) |= PIO_SM1_SHIFTCTRL_AUTOPULL_BITS ;

with code that looks like this:

pio0->sm[1].shiftctrl |= PIO_SM1_SHIFTCTRL_AUTOPULL_BITS ;

The struct headers are named consistently with both the hardware libraries and the hardware_regs register headers. So, for example, if you access the hardware_pio library's functionality through hardware/pio.h, the hardware_structs library (a dependee of hardware_pio) contains a header you can include as hardware/structs/pio.h if you need to access a register directly, and this itself pulls in hardware/regs/pio.h for register field definitions.

Just as an example, here is a snippet from src/rp2040/hardware_structs/structs/pll.h:

typedef struct {
    io_rw_32 cs ;
    io_rw_32 pwr ;
    io_rw_32 fbdiv_int ;
    io_rw_32 prim ;
} pll_tw_t ;

#define pll_sys_hw ((pll_hw_t *const)PLL_SYS_BASE)
#define pll_usb_hw ((pll_hw_t *const)PLL_USB_BASE)

Hardware registers library

These are the lowest level libraries. The hardware_regs library is a complete set of include files for all RP2040 registers, autogenerated from the hardware itself. These are heavily commented, and they define the offset of every register and the layout of the fields in those registers, as well as the access type of the field (e.g. read-only). Note that these contain only comments and #define statements, so they can be included from both assembly files and C files.


Adding SDK libraries to your project

The build system

The Pico SDK uses CMake to manage builds. The project files titled CMakeLists.txt specify how your application or project should be built. To quote the SDK guide, "CMake is fundamental to the way the SDK is structured, and how applications are configured and built."

Let us introduce some of the ideas and syntax for CMake through a simple example: the CMakeLists.txt file used for the blink example in pico-examples. This code is provided below.

add_executable(blink blink.c)

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(blink pico_stdlib)

#create map/bin/hex file etc.
pico_add_extra_outputs(blink)

The add_executable function in this file declares that a program called blink should be built from the single C source file blink.c. This will also be the target name used to build the program, allowing the user to say something like make blink in the build directory to build this particular application.

target_link_libraries is pulling in the SDK functionality that the program needs. If you don't ask for a library, it doesn't appear in your program binary.

pico_add_extra_outputs creates UF2 files for loading onto the Pico via USB. If we didn't include this, the system would build an ELF file (executable linkable format) that could be loaded onto the Pico through the Serial Wire Debug port, with a debugger setup like gdb and openocd. This also creates .hex, .bin, .map, and .dis files.

Dependency trees

Consider the example CMakeLists.txt file for the blink project above. We declare a dependency on the INTERFACE library pico_stdlib. This library itself depends on other INTERFACE libraries, includeing pico_runtime, hardware_gpio, hardware_uart, and others. The linker will garbage collect any of the functions in these libraries which we don't call, so that they don't bloat our binary. But, what does it actually mean for the pico_stdlib library to depend on (for example) the hardware_gpio libary? Well, let's look at the directory structure of the hardware_gpio libarary, illustrated below:

hardware_gpio
|--- CMakeLists.txt
|--- gpio.c
|--- include
$\hspace{.75cm}$|---hardware
$\hspace{1.5cm}$|---gpio.h

The dependency on the hardware_gpio INTERFACE library (thru the pico_stdlib library) causes your application to compile and link gpio.c, and adds the include directory within the hardware_gpio directory to be added to your search path. This means that, in your own project, when you #include "hardware/gpio.h", the correct header will be pulled into the code.

These INTERFACE libraries tidily aggregate a bunch of functionality into readily consumable chunks like pico_stdlib. Many of these libraries don't directly contribute any code, but they depend on a handful of lower-level libraries that do contribute code. This lets you pull in a group of libraries related to a particular goal without listing them all by name. This adds readability, with the potential cost of obfuscating exactly what's being included, at least until you've memorized the code that's included in each INTERFACE library.

Accessing libraries

In the CMakeLists.txt example file above, target_link_libraries is pulling in the SDK functionality that the program needs. If you don't ask for a library, it doesn't appear in your program binary. If we needed access to other headers not pulled in by pico_stdlib (e.g. hardware_dma), we would list those libraries in that line. For example, we might list hardware_dma before or after pico_stdlib.

We would then be able to add #include "hardware/dmah.h" to our source code. Trying to include that header file in our source code without listing hardware_dma as a dependency will fail. And by the way, the naming convention illustrated for hardware_dma is true for all toplevel SDK library headers. The library is called foo_bar and the associated header is foo/bar.h. Recall that the top-level bundle API's include a number of these harware support libraries.


Multi-core support

This is copy-pasted from the SDK guide . . .

Multi-core support should be familiar to those used to programming with threads in other environments. The second core is just treated as a second thread within your application; initially the second core (core1 as it is usually referred to; the main application thread runs on core0) is halted, however you can start it executing some function in parallel from your main application thread.

Core 1 (the second core) is started by calling multicore_launch_core1(some_function_pointer); on core 0, which wakes the core from its low-power sleep state and provides it with its entry point — some function you have provided which hopefully with a descriptive name like void core1_main() { }. This function, as well as others such as pushing and popping data through the inter-core mailbox FIFOs, is listed under pico_multicore.

Care should be taken with calling C library functions from both cores simultaneously as they are generally not designed to be thread safe. You can use the mutex_ API provided by the SDK in the pico_sync library ( https://github.com/raspberrypi/pico-sdk/tree/master/src/common/pico_sync/include/pico/mutex.h) from within your own code.

missing
Example multicore C-code from the SDK guide