Device Modules

Overview

As MCU, the RP2040 comprises several what the datasheet calls peripherals to add functionality to the processor cores, such as UART or SPI communication interfaces, as well as a timer or real-time clock. The RTK framework employs modules and related data structures to represent these blocks of functionality in the controller software. The corresponding RECORD types, or rather pointers thereto, are identified as Device.

Such a type Device1 can describe:

  • a complete peripheral as described in the datasheet, ie. a functional hardware block such as a UART or SPI interface, the timer, or the watchdog, delineated by their specific registers, and, for IO-focused items, their expression as GPIO function;
  • a part of such complete block, for example one of the four alarms of the timer;
  • a “virtual peripheral” implemented in software, for example an output buffer that can be written to just like to an UART, providing the same API.

Modules

A specific peripheral device, or its representation structure Device, can be used by the software in different ways. For example, a UART can be used with blocking, busy-waiting access, or from kernel threads yielding control back to the scheduler while waiting.

Therefore, the RTK framework has

  • a device module, eg. UARTdev.mod, which
    • defines the Device data structure with the data required to operate the device,
    • implements basic functionality, such as initialising, configuring, enabling, or reading status flags.
  • one or more client modules, eg. UARTstr.mod and UARTkstr.mod, using the definitions and the functionality of the device module for different uses cases.

The two kinds of module can be unified in one, such as in Alarms.mod, but could easily be separated if need be, since the single module follows the same design approach as if separated into a Device module and its clients.

Design Approach

Device Data Structure

Usually, the Device data type is a POINTER TO DeviceDesc, with DeviceDesc being a RECORD with all the device data. The DeviceDesc RECORD must define all data items that are necessary to operate the actual peripheral device. The exported fields of that RECORD, as well as the exported procedures, is all that the client modules such as UARTstr will ever “see”. Of course we can make all fields of Device private, and provide corresponding exported procedures in the device module to access the RECORDs fields. Some device modules do. It’s a performance consideration and trade-off.

Basic Procedures

A device module such as UARTdev or SPIdev generally has four distinct procedures to set up, configure, and enable/disable a device:

  • Init: initialise the Device/DeviceDesc RECORD; this happens purely in software, no hardware is configured or operated here;
  • Configure: configure the hardware, for example GPIO pin functions, serial clock frequency or baudrate, and so on, which means to write to the hardware configuration registers;
  • Enable and Disable: usually means to set, or clear, the corresponding enable bits in the control registers in the hardware.

Remarks:

  • Only the Init procedure is mandatory. Some Device don’t require hardware configuration or explicit enabling.
  • The Configure procedure can take a DeviceCfg RECORD as parameter, which specifies the parameters to configure the corresponding device hardware.

Additional Procedures

The Device module can provide additional procedures that are the same and useful for all types of module clients, such as reading status flags, enabling output-to-input loopback, configuring interrupts, and others.

GPIO Pad Configuration

The pad configuration must be done by the program.

  • Most peripherals work just fine with the default reset configuration.
  • If a GPIO pad must be configured specifically, the solution that encumbers the API the least is to leave it to the program.

For example, the SPI MISO connection requires a pull-up resistor. We can configure the corresponding resistors in the pad accordingly, but maybe we have a discrete resistor on our board for that purpose. Either we define this sort of configuration options in the parameters for Configure, eg. by passing a configuration RECORD or procedure, or we simply leave it to the program, resulting in a narrower API.

General Remark on API Design

I prefer a set of API procedures to set up a peripheral device, such as the Init, Configure, and Enable idiom, each of which is focused on a specific, narrow task. In my view, this results in better “composability” of modules and their functionality. It’s always possible to combine all narrow set-up procedures into a more comprehensive one in the program, when the parameters are defined by the use case, and can be provided for the benefit of the programmer, say, as CONST.

See, for example, the hierarchy, from top to bottom:

  • Main.mod
  • Out.mod and In.mod
  • Terminals.mod
  • UARTdev.mod and UARTstr.mod

Module Main composes the whole text output chain from Out to the UART using the different components of the “lower” modules. Example program StringBufOut demonstrates how this chain can easily be recomposed, using additional modules and their compatible data types and procedures.


  1. The type identifier Device is loosely derived from Unix, where hardware is similarly described and referred to by device files, and devices can be software-only items as well. ↩︎