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 Device
1 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.
- defines the
- one or more client modules, eg.
UARTstr.mod
andUARTkstr.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 theDevice
/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
andDisable
: usually means to set, or clear, the corresponding enable bits in the control registers in the hardware.
Remarks:
- Only the
Init
procedure is mandatory. SomeDevice
don’t require hardware configuration or explicit enabling. - The
Configure
procedure can take aDeviceCfg
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
andIn.mod
Terminals.mod
UARTdev.mod
andUARTstr.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.
-
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. ↩︎