Using pointer types
In C, an object pointer such as
uint8_t* ptr is the equivalent concept to use for hardware addresses. A compiler makes this pointer type large enough to contain all addresses in the microcontroller's default address space.
It is valid to convert from an integer to a pointer and back, by means of an explicit cast 1). So if you wish to have a pointer corresponding to address 0x1234 (addresses are by tradition always written in hex), you can cast the integer containing this absolute address to a pointer type:
type must correspond to what's acctually stored at that location - if it is a 8 bit register, then you should use
We may now de-reference the pointer and access the pointed-at data in the register. This is done with the unary indirection operator
*, so we may write
*(uint8_t*)0x1234, which thanks to C's operator precedence/associativity is equivalent to
The result of the
* operator is guaranteed to be a so-called "lvalue", which roughly means an accessible memory location of an object. Therefore
uint8_t x = *(uint8_t*)0x1234; reads from the register and
*(uint8_t*)0x1234 = x; writes to the register.
1) ISO 9899:2018 126.96.36.199 §5 and §6.
All access to hardware registers must be volatile-qualified
CPU registers can by their nature get updated by hardware, as well as by the programmer. Unlike regular variables that can only be updated by the programmer. To reflect that the register may be updated by external factors, we must use the
volatile keyword, which is actually pretty self-explanatory: the data is volatile and may change at any point.
If we don't use
volatile, then the compiler may do incorrect assumptions during optimization. For example if we have this code:
int x = 1;
if(x == 1)
The compiler may assume that since 'x' isn't changed from the point where it was first written to, until the point where it is checked, the code can be optimized into behaving like this:
int x = 1;
If we add
volatile however, the compiler is not allowed to make such an assumption. Therefore we must always use
volatile not only when accessing hardware registers, but when accessing any variable that may be changed by external factors, such as variables shared with an ISR or a DMA buffer.
The correct placement of the
volatile qualifier is anywhere left of the * in the pointer type:
volatile uint8_t*. When the qualifier is placed to the left of *, it means that the pointed-at data is volatile.
So the complete and correct way to access a hardware register is:
Declaring a hardware register macro
Code such as
*(volatile uint8_t*)0x1234 = data; is burnensome to read, and would mean that we have to type out the hardcoded address each time we access the register - which is bad style known as "using magic numbers". Instead we can hide the address and the access behind a macro:
#define REGISTER (*(volatile uint8_t*)0x1234)
where REGISTER should be a meaningful name corresponding to the MCU datasheet/manual's name of the register. They are almost always written in upper case by tradition.
REGISTER may now be used just as if it was a declared variable:
REGISTER = x; or
x = REGISTER;.
In the above macro, an outer parenthesis was added for safe macro practice - macros containing expressions should always be surrounded by parenthesis, to avoid accidental bugs on the caller side. Suppose for example if there is an array of registers and we declare the macro as
#define REG_ARRAY (volatile uint8_t*)0x1234. If the caller attempts to access this through
REG_ARRAY[i], there will be a mysterious compiler error because when the macro is expanded, operator precedence gives
(volatile uint8_t*) (0x1234[i]) rather than
((volatile uint8_t*)0x1234)[i] which was the intention. Therefore, always surround macros by an outer parenthesis.
There are some other subtle issues with the integer constant 0x1234 itself. This is an integer constant, and all integer constants in C have a type of their own. Normally it is
int, which is a signed type. But in case the constant can't fit inside an
int, the compiler will give it a larger type. In case of plain decimal numbers, it would have picked a
long instead. But in case of hex numbers specifically, it will first try if it can fit in an
unsigned int, then a
long 2). For a 16 bit system, this means that 0x1234 is of type
int, but 0x8000 is actually of type
Generally, we do not want to use signed numbers in embedded systems. They can give overflows and come with poorly-defined behavior when the various bit-wise operators are applied to them. For example does the code
1 << 31 invokes undefined behavior bugs on almost any computer, because the integer constant
1 is a signed type.
To get rid of the ambiguity and ensure that we always use unsigned types, we should make a habit of always adding a
u suffix after the integer constant (or
U, it is 100% equivalent).
Therefore we should polish our macro from above further by always adding a
u suffix to the address:
#define REGISTER (*(volatile uint8_t*)0x1234u)
And since I want to avoid signed numbers, this also explains why I used
uint8_t throughout these examples. You should pretty much never use
int when coding embedded systems, because it is signed. Same thing with
char. The few times when you actually need signed variables, go with
stdint.h instead. Similarly, using unsigned int/unsigned short etc is unfortunate, since that means that we get a non-portable variable size. Whereas
uint8_t is always 8 bits, portably. This is especially important for registers, where bit-wise access is extremely likely to happen.
2) Detailed rules for types picked for integer constants can be found in ISO 9899:2018 188.8.131.52.
Defining bit flags and masks
All hardware registers comes with named flags/fields found in the MCU manual. You should access a register using such names, rather than to hard-code the mask with the dreaded "magic numbers". Suppose we are to declare a SPI control register, named SPICR by the MCU manual. It is 8 bits large and found at address 0x1000 according to the manual, so we do this:
#define SPICR (*(volatile uint8_t*)0x1000u)
The manual names three bit flags for this register: bit 7 named "SPIE", to enable the whole SPI hardware, bit 4 named "CPOL", for clock polarity and bit 3 named "CPHA" for clock phase settings. We should declare masks corresponding to these bit names. When doing so, we want to clarify which register they belong to. So such masks may be declared as:
#define SPICR_SPIE (1u << 7)
#define SPICR_CPOL (1u << 4)
#define SPICR_CPHA (1u << 3)
To set the register by enabling SPIE, setting clock polarity to 0 but CPHA to 1, we would write this in the SPI driver:
SPICR = SPICR_SPIE | SPICR_CPHA;
If we wish to be overly clear about which bits that are set and which ones that are clear, we can be even more explicit and name all fields but comment out those not set:
SPICR = SPICR_SPIE |
/* SPICR_CPOL */
This is where lots of different styles and versions exist. Yet another option:
#define SPICR_SPIE(val) ((val) << 7)
#define SPICR_CPOL(val) ((val) << 4)
#define SPICR_CPHA(val) ((val) << 3)
SPICR = SPICR_SPIE(1) | SPICR_CPOL(0) | SPICR_CPHA(1) ;
Non-standard language extensions
Specific compilers and tool chains often have a non-standard way of declaring a variable at a fixed address. It may look like one of these forms:
volatile uint8_t REGISTER @ 0x1234;
volatile uint8_t REGISTER __attribute__((section(".name")));
volatile uint8_t REGISTER;
None of these are portable or standardized, unlike the macro discussed previously. They do however come with the advantage that the register gets appended to the linker output file just like any variable, so that a debugger may use the watch feature on it. This isn't possible with registers declared through macros.
Compiler-specific register maps
Almost every embedded system compiler with target support for a specific MCU comes with a pre-defined register map found in a header file. These are made either by the compiler vendor or the silicon vendor. It's quite a lot of work to define some thousands registers manually, so generally we want to use these pre-made register maps.
By doing so, we should however be aware that the register map is most likely non-portable. By using it we limit ourselves to a certain compiler or tool chain. Because these register maps are unfortunately always certain to rely on numerous non-standard compiler extensions. Not only the ones mentioned above, but also commonly bit-fields and questionable type conversions, making them 100% non-portable and theoretically potential dangers. Such register maps are non-compliant to the C language standard and to quality standards like MISRA-C.
Should we use it still? Well... probably. If you don't have high quality or compiler portability requirements, I'd use it. Writing the entire register map manually ourselves with #defines is a pain. One can copy/paste the register map as text from the pdf manual, then write a little program that converts that into C code, but that's also extra work. I've done all of these variants in the past, so I'd say it's project-specific.
For smaller projects with high quality standards, you can chose to only
#define the registers you are actually using, from within the driver for that peripheral. You shouldn't be accessing those registers from outside the driver anyway, so it is a sensible compromise.