One of the fundamental issues in driver design is deciding how to map to the peripheral registers. Over the years there have been many different methods that have been used such as setting up structures to define bit maps or simply writing the desired value to the register.
However, my all-time favorite method is to create an array of pointers that map to the peripheral registers. This method offers an elegant way to group peripheral registers into logical channels and provide a simple method to not only initialize the peripheral but also access its data.
The pointer array method is easily ported and can be used to create standard API’s and application code that can work across different hardware platforms, allowing for application code to be shared. If properly written, it also creates code that is far easier to read and understand which makes software maintenance easier.
The concepts of pointer arrays are a relatively straight forward method for mapping to a peripheral. The idea is to create an array where each index of an array is a pointer to a peripheral register of a particular type. For example, for a microcontroller with multiple GPIO ports a pointer array would be setup to access the direction registers of each of the available ports (Listing 1). Another pointer array would be setup to access the input and output registers. Each register type would be associated with its own pointer array.
Listing 1: Pointer Array for GPIO
It is important to take note of the way in which the pointer array is declared. The pointer array portsddr is a constant pointer to a volatile uint16. Notice that the declaration is defined from right to left. The pointer to the register is a constant pointer but declaring it as a volatile uint16 notifies the compiler that the value that is being pointed to may change on its own without interaction from the software.
There are many advantages to using this approach to memory mapping. First, it allows registers of the same function to be logically grouped together. This allows the software engineer to view each peripheral as a separate channel of the MCU. For example, timer 1 and timer 2 could be looked at as being two different timer channels.
To setup the period register of each timer would only require a simple write to the proper channel index of the period pointer array. The index of the pointer array then becomes a channel access index. For instance, pointer array index 0 would be associated with Timer 1; pointer array index 1 would be associated with Timer 2.
Next, when the peripherals start to look like channels, it becomes easy to create an abstract method of not only initializing but also of accessing each of the peripheral data. This allows a simple loop to be used to initialize each peripheral (Listing 2). It allows the data of the peripheral to be accessed by simply using the correct channel index. This results in a driver framework that is not only easy to understand and reuse but a framework that abstracts the device registers.
Listing 2: Timer Initialization Loop
Finally, it allows the developer to create configuration tables for each peripheral. Instead of always writing custom initialization code, the developer can create a reusable driver that takes the configuration table as a parameter. The initialization function then loops through the table one channel at a time and initializes the peripheral registers through the pointer array. This allows the driver to become a library module that is tested time and time again resulting in proven code that can accelerate the next project.
This article was published previously on Embedded.com’s sister publication, EDN Magazine