Framework Structure
Overview
While the MCUs supported share the same cores, Cortex-M33 and Cortex-M0+, including the functionality as defined by ARM in their respective architecture reference manuals, such as the NVIC, or the system tick, they are very different with respect to their architectures and functionalities as system-on-a-chip. A basic example is the clock subsystem for CPU, bus, and peripheral devices.
The RPs are remarkably similar to each other, but vastly different from what is used with the NXPs and the STMs. The latter two again are very different from each other, and even within products of one vendor, say NXP, there can be found different designs for the same purpose, say, clocking. The RPs are also very neatly designed and structured when looking as just one of the MCU instances, while the other two tend to be a bit messier. I suppose this is not least the result of offering way more MCU product lines, the re-use of certain common building blocks on the chip across many different products, and the corresponding (design) history.
For a framework such as Oberon RTK, the question arises how to deal with said differences, while maintaining a reasonable compatibility.
Approach
We can divide the MCU’s subsystems (hardware modules) which are not part of the ARM architecture definitions into three categories:
- required for the basic MCU operation, such as clocks or resets;
- quasi-standard modules, such as UART or SPI;
- chip and application specific functional subsystems, such as encryption engines, neural net processors, or operational amplifiers.
None of these subsystems are compatible on the hardware level across different MCU vendors and products; within vendors and products, we can find some degree of, or even full, compatibility. Peripheral devices such as UART or SPI provide compatibility on the application layer, but even here we find more or less extensive additional and diverging functionality. For example, UART devices than can act as SPI masters.
The approach chosen for the Oberon RTK modules:
- hardware-specific driver modules that provide an API to control and manage the hardware, but omit higher level functionality required for control programs, with as many as possible common names* for data types and procedures; these modules also export any constants, such as clock sources, that are required for the parameters of their client modules (next point);
- modules that make use of these driver modules to provide program level functionality, eg. for UARTs (
PutString), or to configure the run-time parameters for the MCU as required for a specific program, eg. for the clocks; these modules do not directly access the hardware;1 - program modules make use of the second layer if it exists, or directly of the basic hardware driver modules if not; program modules usually do not use the hardware directly, unless there is a benefit regarding performance.
For the hardware-specific driver modules, even if the names of data types and procedures are the same, they usually take different parameters for MCUs of different vendors. For example, a PLL as found in the RPs has a different set of configuration parameters than PLLs provided by NXP or ST. But the configuration procedure is always ConfigPLL, which takes a record type PLLcfg as parameter. Hence, we can achieve at least a conceptual consistency, with the same design patterns applicable for different MCU types.
Another example would be a module for UART or SPI devices, which always export a Device type, and provide procedures Init, Configure, Enable and so on, with Configure taking a DeviceCfg parameter.
Nothing new or earth-shattering here.
Required Subsystems for Basic MCU Operation
This category includes all MCU subsystems that are not part of the CPU architecture specification, but are required to get even off the ground. Each vendor has very different names for their respective subsystems, so Oberon RTK uses the same names for each MCU type and vendor.
Currently Oberon RTK uses in lib/v3.0, for all MCUs:
CLK.mod: clocks configuration and control: oscillators, PLLs, global dividers;RST.mod: reset control: peripheral devices;PWR.mod: power control: eg. core voltage controllers for different core and bus clock frequencies, power saving states;FLASH.mod: flash memory configuration: eg. wait states, configuration data in flash;RAM.mod: RAM configuration: eg. wait states, clock gating.
Module Clocks then uses module CLK to configure and run the required clocks for a program, implementing Clocks.Configure, which is called in module Main, as design pattern used for all MCUs.2 Clocks also uses modules PWR, FLASH, and RAM as needed for the required clock frequencies.
Quasi-standard Subsystems
Oberon Module UART defines the peripheral device in software as type, plus the procedures to set it up, as outlined above. Module UARTstr uses the thusly defined peripheral device to print strings to a serial terminal.
Summary
Hardware-specific driver modules operate the MCU hardware, omitting higher level abstractions and design decisions. Across MCU vendors and types, a common naming scheme for data types and procedures provide the basis for common design patterns, even if the details of parameters are different for each hardware platform.
A second layer of modules make use of these lower level driver modules to provide an API (types, procedures) that is as much as possible unified across different MCUs. Differences in the capabilities and the functionality of the hardware subsystems can result in diverging APIs, though, unless we want to limit ourselves to the common functional denominator of all MCUs.
The separation between low level hardware driver modules, and modules that make use of these driver modules also should make it easier to design and implement modules that are specific for a program and its scope and purpose.
Experience
At the time of this writing, the basic driver modules as listed above have been implemented for all currently supported MCUs, following the design pattern as outlined. The example and test programs implemented for lib/v3.0 did not require any major changes as a consequence, once Clocks.Configure and Main had been implemented or amended accordingly.
(this page to be extended)
-
Exceptions can be made for modules where we want to limit its dependence on other modules, or where hardware access is required for the module’s core functionality, such as run-time error handling. ↩︎
-
Module
Clocksis located in directoryconfig, so it can be implemented specifically for a program or application. ↩︎