How to Port a Board for RIOT


Introduction

It is already a few weeks back, but this year marked a debut for me: Though I am a long-term contributor to the RIOT project—my oldest contribution goes back to June 2011—I never actually ported a board for RIOT support. This changed at the beginning of this year.

RIOT is an operating system for the Internet of Things (IoT), so an operating system for hardware platforms that do not typical PC operating systems fit like Linux, Mac OSX, or Windows or even phone operating systems like Android or iOS. When talking of IoT devices, the RIOT community usually thinks in terms of kilobytes of memory. Not gigabytes, not megabytes, kilobytes! As in, a few 1000 bytes. The premise behind this thinking is, that IoT devices can not follow Moore's Law—which states that every two years the number of transistors in a given piece of computer hardware approximately doubles. In fact, the efforts to save cost and energy for IoT hardware are directly opposed to Moore's Law: every additional transistor, be it for calculation or storage, costs more money and leads to more energy usage. Given, that for the IoT a multiple of 10 of devices per person is expected, meaning at least 100 billion devices deployed, low cost and low energy usage is vital.

The main code base of RIOT is written in the C programming language, but bindings for C++, Rust, Python, JavaScript, and others exist as well.

For RIOT, a board is defined as a module that defines a microcontroller and configuration parameters how this microcontroller can interact with other devices on this platform and the outside world. Colloquially speaking, a board is somewhat like a computer system (e.g. a PC), and the microcontroller the central processor (CPU). So the process of porting a board is providing these configuration parameters.

Personally, I am more involved with the networking architecture of RIOT—I make sure RIOT can talk to the outside world. So my closest contact to hardware in that area is mostly the interaction with the network device. Thus, there was just never the need for me to port a board for RIOT. So why did I do it now? Mainly, because I wanted to try and learn and because I had this piece of hardware lying around, for which I knew, that the port would be relatively easy, considering my skill set.

The board I ported is the Adafruit Feather nRF52840. I bought this board earlier this year mainly to save some shipping costs, but also because I like the form factor of Adafruit's Feather line and wanted to see more support for it in RIOT. It then became incidentally part of the most current RIOT release 2020.01.

I am intentionally leaving the process of creating a pull request out as I want to focus this article mainly on how to provide a board port, not how to get it upstream.

Creating the board port

As I said, a board in RIOT is mostly just configuration, both for the build system and the actual code. But first I needed a place to put this configuration. For simplicity I cut out the “Adafruit” out of the board's identifying name—and because the Adafruit Feather M0 already was called feather-m0. Hence, for RIOT's purpuses the board is now just called feather-nrf52840. Under this name I created a directory for the board configuration to reside in: boards/feather-nrf52840.

There I first created a Makefile to tell the build system make that the directory is indeed describing a board. This file is called boards/feather-nrf52840/Makefile:

MODULE = board

include $(RIOTBASE)/Makefile.base

The first line tells the build system, that this is a module called board (all boards are called like this) and the third includes all the stuff the build system developers of RIOT already provided for a module to be built.

However, the build system needs more information. What features (i.e. interfaces and devices) does the board support? This is done with the file boards/feather-nrf52840/Makefile.features:

CPU_MODEL = nrf52840xxaa

# Put defined MCU peripherals here (in alphabetical order)
FEATURES_PROVIDED += periph_i2c
FEATURES_PROVIDED += periph_spi
FEATURES_PROVIDED += periph_uart
FEATURES_PROVIDED += periph_usbdev

# Various other features (if any)
FEATURES_PROVIDED += radio_nrf802154

include $(RIOTBOARD)/common/nrf52/Makefile.features

The first line tells the build system, the CPU (or more accurate microcontoller) the board is using in a naming scheme that is understand by the build system. We basically tell it, "this board is using an nRF52840 microcontroller". Lines 4-9 tell the build system which features are provided by the board, naming communication interfaces such as I²C, SPI, UART, and USB, but also that the radio of this board can speak the IEEE 802.15.4 protocol—imagine something like low-power WiFi—via its nRF microcontroller. The last line declares to include further features provided by the nRF52 family of microcontrollers in general.

Lastly, I tell the build system with the file boards/feather-nrf52840/Makefile.include, what else needs to be included for this board to be programmable, among them the most important thing: the programmer itself. In case of the Feather nRF52840 this is the Segger JLink:

# HACK: replicate dependency resolution in Makefile.dep, only works
# if `USEMODULE` or `DEFAULT_MODULE` is set by the command line or in the
# application Makefile.
ifeq (,$(filter stdio_%,$(DISABLE_MODULE) $(USEMODULE)))
  RIOT_TERMINAL ?= jlink
endif

include $(RIOTMAKE)/tools/serial.inc.mk

include $(RIOTBOARD)/common/nrf52/Makefile.include

Now we finally leave the build system and tell the microcontroller where to put all the peripheral interfaces to the outside world and devices on the board in the boards/feather-nrf52840/include/periph_config.h. Namely, I configured one UART, one I²C and one SPI interface according to the pin labeling on the board. I strictly followed the schematics provided by Adafruit for this and looked at the periph_conf.h of other nRF52-based boards for reference.

/*
 * Copyright (C) 2020 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     boards_feather-nrf52840
 * @{
 *
 * @file
 * @brief       Peripheral configuration for the Adafruit Feather nRF52840
 *              Express
 *
 * @author      Martine S. Lenders <m.lenders@fu-berlin.de>
 *
 */

#ifndef PERIPH_CONF_H
#define PERIPH_CONF_H

#include "periph_cpu.h"
#include "cfg_clock_32_1.h"
#include "cfg_rtt_default.h"
#include "cfg_timer_default.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @name    UART configuration
 * @{
 */
static const uart_conf_t uart_config[] = {
    {
        .dev        = NRF_UARTE0,
        .rx_pin     = GPIO_PIN(0,24),
        .tx_pin     = GPIO_PIN(0,25),
        .rts_pin    = (uint8_t)GPIO_UNDEF,
        .cts_pin    = (uint8_t)GPIO_UNDEF,
        .irqn       = UARTE0_UART0_IRQn,
    },
};

#define UART_0_ISR          (isr_uart0)

#define UART_NUMOF          ARRAY_SIZE(uart_config)
/** @} */

/**
 * @name    SPI configuration
 * @{
 */
static const spi_conf_t spi_config[] = {
    {
        .dev  = NRF_SPI0,
        .sclk = 14,
        .mosi = 13,
        .miso = 15
    }
};

#define SPI_NUMOF           ARRAY_SIZE(spi_config)
/** @} */

/**
 * @name    I2C configuration
 * @{
 */
static const i2c_conf_t i2c_config[] = {
    {
        .dev = NRF_TWIM1,
        .scl = 11,
        .sda = 12,
        .speed = I2C_SPEED_NORMAL
    }
};
#define I2C_NUMOF           ARRAY_SIZE(i2c_config)
/** @} */

#ifdef __cplusplus
}
#endif

#endif /* PERIPH_CONF_H */

We also need to tell the sensor/actuator abstraction layer of RIOT, SAUL, where to find all the buttons and LEDs provided on the board, so it can react to button push events and control the LEDs. This we do in boards/feather-nrf52840/include/gpio_params.h. There are two LEDs and one switch and I again just followed the schematics:

/*
 * Copyright (C) 2020 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     boards_feather-nrf52840
 * @{
 *
 * @file
 * @brief       Configuration of SAUL mapped GPIO pins
 *
 * @author      Martine S. Lenders <m.lenders@fu-berlin.de>
 */

#ifndef GPIO_PARAMS_H
#define GPIO_PARAMS_H

#include "board.h"
#include "saul/periph.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief    LED configuration
 */
static const  saul_gpio_params_t saul_gpio_params[] =
{
    {
        .name  = "LED Red (D3)",
        .pin   = LED0_PIN,
        .mode  = GPIO_OUT,
        .flags = (SAUL_GPIO_INIT_CLEAR),
    },
    {
        .name  = "LED Blue (Conn)",
        .pin   = LED1_PIN,
        .mode  = GPIO_OUT,
        .flags = (SAUL_GPIO_INIT_CLEAR),
    },
    {
        .name  = "UserSw",
        .pin   = BTN0_PIN,
        .mode  = BTN0_MODE,
        .flags = SAUL_GPIO_INVERTED,
    },
};

#ifdef __cplusplus
}
#endif

#endif /* GPIO_PARAMS_H */
/** @} */

Lastly, the board needs to define some low-level debugging LED and button GPIO pins and needs to initialize those pins. We do this in the in boards/feather-nrf52840/include/board.h and boards/feather-nrf52840/board.c. Rumors tell, this might not be necessary in the future, so watch out for this later if you read this when you follow my instructions ;-).

/*
 * Copyright (C) 2020 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @{
 *
 * @file
 * @author  Martine Lenders <m.lenders@fu-berlin.de>
 */

#include "cpu.h"
#include "board.h"

#include "periph/gpio.h"

void board_init(void)
{
    /* initialize the boards LEDs */
    gpio_init(LED0_PIN, GPIO_OUT);
    gpio_clear(LED0_PIN);
    gpio_init(LED1_PIN, GPIO_OUT);
    gpio_clear(LED1_PIN);

    /* initialize the CPU */
    cpu_init();
}

/** @} */
/*
 * Copyright (C) 2020 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @{
 *
 * @file
 * @author  Martine Lenders <m.lenders@fu-berlin.de>
 */

#include "cpu.h"
#include "board.h"

#include "periph/gpio.h"

void board_init(void)
{
    /* initialize the boards LEDs */
    gpio_init(LED0_PIN, GPIO_OUT);
    gpio_clear(LED0_PIN);
    gpio_init(LED1_PIN, GPIO_OUT);
    gpio_clear(LED1_PIN);

    /* initialize the CPU */
    cpu_init();
}

/** @} */

After that I tested if everything worked as configured, provided some documentation how to do that, and then called it done.

Conclusion

Given that you already worked with C and make and that the microcontroller for the board you want to port is already provided, porting a board is quite straight-forward. Basically, all that is needed are 7 very concise files. All you need beyond that is to look up what other people did and maybe read a bit up on the boards doc. Being able to read hardware schematics also helps, but I found myself quickly into the graphic notation for that, though I barely had contact with this kind of notation before.