Hardware registers

Hardware registers #

As Hardware Abstraction Layer (HAL), JeeH needs to provide access to a wide range of built-in hardware peripherals. On ARM Cortex, there are specific addresses in the 4 GB address space which map to the registers of these peripherals. These are documented in Reference Manuals 1.

Direct access from C++ #

To simplify use, JeeH creates a set of IoReg definitions which allow reading and writing the registers, as well as individual bits and bit ranges in them. This example shows how to send a character to a serial port on an STM32F7xx chip, using polled mode:

#include <jee.h>
using namespace jeeh;

void putch (char c)  {
    enum { ISR=0x1C, TDR=0x28 }; // USART register offsets
    while (!USART1[ISR](7)) {}   // wait until TXE is clear
    USART1[TDR] = c;             // send the byte out
}

The above code is highly µC-specific, although much of this is often the same across several different STM32 µC families. One key trick in JeeH is how it defines USART1:

constexpr IoReg<0x4001'1000> USART1;

This is automatically generated as part of the build process, from a System View Definition (SVD) file for the specific µC currently used as build target. These definitions are generated in the file jee-svd.h and included by jee.h.

Taking a closer look #

The IoReg type is a C++ template. It defines an object which can be treated as a subscripted array, with each element a hardware register. Thus, USART1[ISR] refers to the 32-bit register at address 0x4001101C (i.e. 0x40011000 + 0x1C). This register can then be read or written as 32-bit word as if it were a normal (volatile) C++ variable. In this case we only check bit 7, using the notation USART1[ISR](7). Again, this bit can be read or written as if it were a C++ variable.

Once the USART TX buffer is available, USART1[TDR] = c; then stores a new character in it.

There is no middleman #

Unlike other HAL libraries, JeeH does not add wrapper functions to access hardware registers. Only the base of each hardware peripheral is defined as a (constexpr) C++ variable. It is up to the driver code to specify any offsets it needs and to introduce symbolic values for them.

Since the Reference Manuals are the ultimate source of truth for each hardware peripheral in each µC, and since they need to be perused anyway, there’s no reason to wrap things up in yet another API, which would require its own documentation (and introduce its own bugs …).

The result of this choice of C++ syntax, is that direct access to all the hardware tends to require much less source code compared to other HAL designs. Which in turn makes it easier to read.

The way to use JeeH for direct access to built-in hardware, is to open up the Reference Manual!

Configure and use GPIO pins #

There is a Pin type to easily define and configure the General Purpose I/O (GPIO) pins. Here is what must surely be JeeH’s simplest blink example:

#include <jee.h>
using namespace jeeh;

int main () {
    Pin led ("C13");   // the LED is tied to GPIO pin PC13
    led.mode("P");     // set this pin in "push-pull" output mode
    while (true) {
        led.toggle();  // toggle the LED
        msIdle(500);   // wait 500 ms
    }
}

There’s also a quick way to configure multiple pins, for example:

Pin::config("B0:A,B1,C13:P,G10:H7"):

This sets PB0 and PB1 to analog (in), PC13 to pull-up (out), and PG10 to alt mode 7 with a high-speed pin drive. The following variant also defines the pin objects for use in subsequent code:

Pin pins [4];
Pin::config("B0:A,B1,C13:P,G10:H7", pins):
...
pins[2] = 1;  // set PC13 high

Lastly, this code defines the same pin objects, but without configuring them:

Pin pins [4];
Pin::config("B0,B1,C13,G10", pins):
...
pins[2].mode("P");  // configure PC13 as push-pull output

The following configuration modes are available for pins on STM32 µCs:

  • A = analog (in)
  • F = floating (in)
  • O = open drain (out)
  • P = push-pull (out)

Optionally followed by these (not every combination will be meaningful):

  • D = enable pull-down
  • U = enable pull-up
  • L = low speed drive
  • N = normal speed drive (default)
  • H = high speed drive
  • V = very high speed drive
  • <N> = alternate mode N

All pin configurations, reads, and writes use the direct access mechanism described earlier.

C++17 code efficiency #

The gcc / g++ compilers are surprisingly adept at optimising the generated code these days. One example is with pins: the definition Pin led ("C13"); is in fact simply a compile-time way of defining a variable, which encodes the GPIO port and bit location in a single byte (when the argument is a string known at compile time). It could also have been written as:

constexpr Pin led ("C13");

This means that all the code based on top of this is essentially using a small constant, which lets the compiler perform constant folding and constant propagation in most cases. The same goes for hardware register definitions such as USART1: it’s simply a compile-time constant.

Turning variables into constant values helps the compiler generate smaller and faster code.


  1. Such as the 1700+ page RM0385 PDF for STM32F74x / STM32F75x µCs on STM’s site. ↩︎