Sign Up Sign In

How to access a hardware register from firmware?


When writing firmware in C for a microcontroller, how can I directly access a memory location such as a hardware peripheral register, given its absolute address?

Is there a way to do this safely and portably in standard C without using the pre-made register map delivered together with the compiler libraries?

Why should this post be closed?


1 answer


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:


Where type must correspond to what's acctually stored at that location - if it is a 8 bit register, then you should use (uint8_t*)0x1234.

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 *((uint8_t*)0x1234).

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 §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:

*(volatile uint8_t*)0x1234

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.

Integer constants

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 unsigned int.

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 intn_t from 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

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:


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_CPOL */
           SPICR_CPHA ;

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)

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.


Nice writeup with lots of detail, +1. However, all hardware registers don't need to be declared volatile, only the ones the hardware can change. For example, a UART baud rate divider register would not need to be volatile, since you write the value and the hardware never changes it. Olin Lathrop about 2 months ago

@Olin Then you would still have the possibility that the compiler may assume "Oh, that UARTBR=0x42; line is there, but the variable isn't used anywhere. Lets optimize away that line." I wouldn't gamble but keep volatile consistent across all registers. Lundin about 2 months ago

@Lundin: The compiler can't make that assumption simply due to the variable being global. Olin Lathrop about 2 months ago

@Olin Sure it can, the linkage & storage duration doesn't really matter. It's highly tool chain-specific what gets optimized away and what gets linked, it's nothing we can rely on cross-platform. Also note that it is very common for bare metal microcontroller programs to use a non-standard start-up, where .bss/.data initialization isn't carried out at all, despite what the C code says. Lundin about 2 months ago

Sign up to answer this question »