Select Page

Modern low-level driver programming

CMSIS, MCAL and Co. – Off-the-shelf low-level drivers

Embedded systems are found in many areas today. They are often a crucial factor for convenience, safety, sustainability, and innovation. The proportion of software in embedded systems continues to increase. And the hardware, whether microprocessors with external peripherals or microcontrollers, is becoming ever more complex. Multicore systems are already a reality, and more and more manufacturers are bringing new multicore derivatives to market. Knowing—and programming—this complex hardware down to the last bit is no longer feasible within the available timeframe. This makes hardware abstraction indispensable.

For 8-bit and 16-bit microcontrollers, component and tool manufacturers typically provided the header files containing symbol definitions for all control/status and working registers of the peripheral modules. These files were often several thousand lines long for 16-bit microcontrollers, as the following example with a total of 12,000 lines demonstrates.

Excerpt from the file XE16xREGS.H

Fig. 1 Excerpt from the file XE16xREGS.H [1]

Complex 32-bit (single-core/multi-core) microcontrollers contain peripheral modules, where a single module can provide several hundred control/status and working registers. Often, several identical peripheral modules are integrated into a single chip.

Different component variants often contain a different number of the respective peripheral modules of the same type. This makes the number of registers and the necessary symbol definitions increasingly complex.

Hardware abstraction

Instead of defining individual symbols for each peripheral register, the complete set of control/status and working registers of a peripheral module can be represented using a C structure. The registers of a peripheral module are arranged sequentially in a defined address space – possibly with gaps between them. The structure represents these registers in the correct order and with the gaps.

Structure for the I2C module of an STM32 chip

Fig. 2 Structure for the I2C module of an STM32 device

In order to reuse this image of the peripheral registers for multiple modules of the same type in the component, the starting address of the respective register block is also required.

Base addresses of various peripheral modules of an STM32 chip

Fig. 3 Base addresses of various peripheral modules of an STM32 chip

A pointer to the corresponding structure type is set to the base address of the register block. If there are two or more modules of the same type, there are also two or more pointers of that type. These pointers allow access to all elements of the previously defined structure (registers of the peripheral module).

Pointer definition and access to the registers of type I2C_TypeDef

Fig. 4 Pointer definition and access to the registers of type I2C_TypeDef

In this example, access to the peripheral registers is performed directly within the application. The disadvantage of this approach is obvious: any modification or extension, and especially the reusability of the application in other systems, is problematic, as every line of code throughout the entire application must be checked and potentially adjusted. Each line of code can contain a reference to peripheral registers and therefore must be modified or removed for other components or other tasks.

The application programmer must also know what to do with the peripheral registers. Every read or write access to one of the registers requires detailed hardware knowledge. Furthermore, the testing effort is very high because the entire application directly accesses the module's registers. Who uses what, when, and how must be completely retested with every minor change.

Software layer model

A clean separation between application code and low-level driver code, on the other hand, has many advantages. It creates independent software layers (software subsystems) that communicate via interfaces, and these subsystems can be developed and tested separately.

The upper layer can access the layer below it via a predefined interface. In the C programming language, this interface is simply a header file. However, the lower layer must not also access the upper layer via an interface, as this would lead to bidirectional dependencies. Access from the lower to the upper layer can be implemented using callbacks. In the C programming language, function pointers are used for this purpose.

A change in one of the subsystems has no effect on the other subsystem, provided the interface is not changed.

Software layer model: 2-layer model

Fig. 5 Software layer model: 2-layer model

From the application's perspective, simply replacing the header file and the underlying low-level driver layer might be enough to continue working with the same application after a component change without much effort. However, it's usually not that simple. The names and parameter interfaces of the functions in the low-level driver layer are often defined completely differently. Therefore, simply replacing them is insufficient in most cases. Old function names must be replaced in the application, and the parameter list for each call must be checked and adjusted.

If another component doesn't have a specific peripheral module directly available (e.g., an I2C module), it must be emulated by another module (e.g., an SPI module). This requires completely different functions for initializing and using this module to be integrated into the application, which in turn means additional time for implementation and testing.

3-layer model with low-level driver abstraction

And this is where the 3-layer model comes into play. A low-level driver abstraction layer is inserted between the low-level driver layer and the application layer. This layer hides the actual hardware from the user (the application) above – for example, whether a true I2C interface is available in the hardware or whether it has to be emulated via another serial interface.

The application provides the parameters for the correct settings, and the underlying abstraction layer passes these on to the appropriate low-level driver component. Naturally, the abstraction layer must be adjusted accordingly when a component is changed.

Software layer model: 3-layer model

Fig. 6 Software layer model: 3-layer model

Should I write my own driver or use a pre-made one?

But regardless of whether it's a 2-layer or 3-layer model, the low-level driver is required in any case. And the crucial question now is: write it yourself or use a ready-made driver from the component manufacturer?

Writing it yourself means that there must be a "hardware expert" who knows exactly how the peripheral modules, the registers, the bit fields and bits in the registers work and translates the initialization and use into the programming language.

Since there are no universally valid rules (naming rules, structure of the parameter interface, etc.), the implementation can look completely different for each component or even for each peripheral module, and the abstraction layer must be adapted accordingly.

Low-level driver function with any name, no parameters

Fig. 7 Low-level driver function with arbitrary name, no parameters

Utilizing the advantages of "off-the-shelf" drivers

If no parameters are passed to low-level driver initialization functions, predefined values must be used for configuration within the function. This severely limits reusability because different functions are required for each type of use.

To minimize the effort involved, there must be rules governing how many and which parameters are passed to these functions, and in what order. Furthermore, it makes sense to always structure the function names according to a fixed, predefined scheme. And that, in turn, is one of the advantages of using "off-the-shelf drivers.".

Specifications and standards facilitate portability and reusability.

Component manufacturers and software providers are collaborating to define interfaces for accessing peripheral modules, real-time operating systems, and middleware components. If a different component type from the same series is then used (for example, a Cortex derivative), the application can remain unchanged, and the abstraction only requires minor component-specific adjustments.

CMSIS layer model

Fig. 8 CMSIS layer model

Standardization therefore facilitates portability and reusability.

Low-level drivers simplify resource usage

The pre-implemented low-level drivers from the component manufacturers simplify the use of component resources. Of course, the user still needs to know what can be achieved with each component. However, the details of this implementation—which register, bit field, or bit needs to be written with which value—are hidden within the pre-implemented driver.

Parameters to be set are supplied to the initialization function via an initialization structure.

Initialization structure for an I2C module

Fig. 9 Initialization structure for an I2C module

The application populates the initialization structure with the values to be set and passes the base address of the module to be initialized and the address of the initialization structure to the lower layer. The call to the low-level driver function is again directly in the application code in the following example.

Initialization structure and call to the initialization function

Fig. 10 Initialization structure and call to the initialization function

Hardware Abstraction Layer helps with the initialization and use of peripheral modules.

The next level of abstraction would again be the separation of application code and low-level driver access. The Hardware Abstraction Layer (HAL) provides the application layer with a structure containing everything needed for initializing and using a peripheral module. This means that, in addition to the initialization structure, further structures, one of which contains function pointers, are entered into the lower layer to represent the actual functions required by the low-level driver. The application is then called via the function pointer, eliminating the need to know the underlying driver.

Application with Hardware Abstraction Layer (HAL) calls

Fig. 11 Application with Hardware Abstraction Layer /HAL calls

Three different structures are defined in the HAL. They are available for initialization (Configuration – cfg), runtime control (Control – ctrl), and the various function calls (Application Programmers Interface – API).

HAL structure with function pointers

Fig. 12 HAL structure with function pointers

The various structures responsible for the use of a module are grouped together in a surrounding structure.

HAL structure with elements of type structure

Fig. 13 HAL structure with elements of type structure

A fully functional system in a short time

The detailed know-how is therefore contained in the low-level driver. The Hardware Abstraction Layer hides the accesses to this lower software layer in structures, and the application only needs to define how the peripheral modules are used.

While this will reduce efficiency (runtime, storage space requirements), reusability, modifiability, and portability will be significantly improved. This is less of a concern, especially for complex systems. The primary goal is to develop a working system as quickly as possible, and ready-made low-level drivers certainly contribute to this.

Summary

By cleanly separating the software into subsystems or layers, the reusability and interchangeability of the hardware or application is significantly improved. Using pre-implemented low-level drivers simplifies the handling of complex components. This saves time both during the development phase and in the preparation phase when learning the functionality of the component being used.

One disadvantage can be the complexity of the resulting software system. Storage requirements and runtimes change, therefore the analysis phase must clarify what is more important for the system – efficiency or reusability, interchangeability, adaptability.

More information

MicroConsult Training & Coaching on Embedded and Real-Time Programming

MicroConsult Expertise in Embedded and Real-Time Software Development

List of abbreviations

API – Application Programmers Interface
CMSIS – Cortex Microcontroller System Interface Standard
CMSIS-SVD – CMSIS System View Description
DAP – Debug Access Port
DSP – Digital Signal Processing
HAL – Hardware Abstraction Layer
LLD – Low Level Driver
MCAL – Microcontroller Abstraction Layer
RTOS – Real-Time Operating System

Bibliography and list of sources

[1] Infineon XE16x Register Definition File, July 28, 2008
[2] ARM Embedded Software Development (CMSIS)

MicroConsult Newsletter

With the MicroConsult newsletter, you'll stay on the pulse of the embedded world. Look forward to proven practical knowledge, real professional tips, and current events – directly from our experts for your project success.

Subscribe now!

Published by

Renate Schultes

Renate Schultes