4.4 Configuration System
4.4.1 Board Configuration
One of the downsides of the otherwise useful alternate function system for the STM32F4’s GPIO pins is that one does not always know which pins, for example, the I2C1 core will be connected to. This precludes a driver automatically mapping GPIO pins correctly. Additionally, depending on the board, a peripheral may be connected to a different I2C core to make board routing easier.
As such, a system for describing the connections between low-level and high-level drivers, as well as configuring both of them, was desired. Several options were considered. Some were compile-time options which would update header files or provide preprocessor definitions to cor-rectly configure pins and devices. This would likely have been implemented as a pre-preprocessor, written in Python, which would create the apporopirate header files based on an XML or JSON-formatted configuration file before compilation. A basic prototype of this system was written, but was scrapped due to complexity.
Further research into prior implementations of board-specific configurations led to the device tree format used by the Linux Kernel [60]. First developed by IBM for running Linux on the Power architecture [61], it has also been adopted by Linux ports for ARM processors as well [60]. The device tree format is a simple text-based tree format, which can be compiled into a binary device tree blob format. This is ideal, as it allows writing a human-readable text file to configure the board, which is then “compiled” into a binary format suitable with a small memory footprint. This binary format is designed to be easily “walked” and have configuration data extracted at run-time.
A simple device tree parsing/walking library, libfdt, is freely available [62]. This allows work-ing with a binary Flattened Device Tree (FDT) easily. The code is fairly small as well (TODO find binary size). Libfdt is compiled separately and linked as a static library. This allows independent updating of the library. The FDT is stored as a static array which is loaded into the executable image at compile-time; a script is provided which takes a binary FDT and converts it to a C array representation.
Device Tree Format
As mentioned, the device tree is a simple text format. A minimal working example is shown in Source Code 4.2. Each node has a name and is delimited by “{” and “};”. The “root” node has the special name “/”. All other nodes can be named arbitrarily, but are typically named “de-vice@address”; additionally, the name must be unique among other subnodes at the same level.
Note that the device tree format was intended to be used for memory mapped peripherals. This leads to some potential confusion as the implementation here does not use memory addresses, but instead uses 0-based indexing for instance and subinstance IDs. Most references to “addresses”
that follow are actually various parts of the Driver GUID that will be constructed for each device.
Each node can have subnodes, as well as properties. Properties are defined by the format as being either 32-bit unsigned numbers, or C-style strings. There are several “special” properties which must be present for all subnodes. The “#address-cells” and “#size-cells” properties which tell how big the address space for subnodes is. A node that has subnodes must have both of these properties. For this implementation, all nodes have “#address-cells” as “1” and “#size-cells” as 0; this means that the “address” of the subnodes has one address entry and 0 size entries. The subnode address is stored in the special “reg” property. Additionally, each subnode must contain a “compatible” property of the form “vendor,driver”. The “driver” element of this property will be used to determine the Driver ID and configuration function for a given device node. Additional properties prefixed with “stm32,” will be passed to the driver code in an array, in the order they are presented in the node list. To configure each driver, a configure function is called which gives each driver a parent GUID, its driver GUID, and those configuration parameters. This prototype is seen in Source Code 4.1.
void configure_fn(uint32_t parent_guid, uint32_t parent_guid, uint32_t *config, uint32_t length);
Source Code 4.1: Device Configuration Function Form
/dts-v1/;
The minimal example in Source Code 4.2 demonstrates most of these properties. At a high level, this tree specifies an MPU9150 device, which is connected to an I2C device. This I2C device is on the APB1 bus; however, this information is currently unused by the configuration. The I2C node is named “I2C1@0”, indicating it is the I2C1 device, and has instance ID 0. Using the Driver ID derived from its “compatible” property and the Instance ID derived from the “reg” property, a Driver GUID will be constructed. The Parent GUID will be the address of the APB1 bus, but it is unused. In addition, there will be four configuration options passed to the I2C configuration. As the MPU is a submode of I2C1, it will be passed the Driver GUID of I2C1 as the parent GUID.
Its Driver GUID will be derived in the same manner as the I2C driver GUID was. It also has two
properties which will be provided to the configuration function.
Device Tree Parsing
At boot, the device tree is parsed and all peripherals are configured. This is done by “walking”
the tree, starting with the root node and parsing all subnodes in depth-first order. As the tree is parsed, Driver GUIDs are built up from the driver type and driver address of each node, and each driver is configured appropriately. Both low-level and high-level drivers are configured by the device tree parser. An invalid device tree will cause the device to enter a hard fault state, as a board without valid configuration will not perform correctly.