What are the alternatives, really?
Looking at the possible ways to do it (including example code, so one has something real to base an opinion on) might be more useful.
Even when structures are used to describe the register contents, they can be accessed in different ways.
Consider PORTA on SAMC21. You can access it either through the IOBUS at addresses 0x60000000–0x6000005F, or through the AHB-APB bridge at addresses 0x41000000–0x4100005F. Let's assume you have defined
struct samc21_port to correspond to the SAMC21 PORT registers (Section 28.7. Register Summary in the
datasheet), and you want to expose the port via
PORTA and
PORTA_BRIDGE; the former used for CPU access (single cycle operations, highest priority), and the latter for DMA and such (slower, lower priority accesses).
There are three main ways you can declare the two variables.
- Use a linker script to assign the exact address for the symbols:
extern volatile struct samc21_port PORTA;
extern volatile struct samc21_port PORTA_BRIDGE;
- Declare the "variables" as macros referring to a specific memory address:
#define PORTA ((volatile struct samc21_port *)0x60000000)
#define PORTA_BRIDGE ((volatile struct samc21_port *)0x41000000)
- Declare variables as pointers to the structures:
volatile struct samc21_port *const PORTA = 0x60000000;
volatile struct samc21_port *const PORTA_BRIDGE = (volatile struct samc21_port *const)0x41000000;
- Declare variables as compile-time constant pointers to the structures: (C++)
volatile struct samc21_port *constexpr PORTA = 0x60000000;
volatile struct samc21_port *constexpr PORTA_BRIDGE = 0x41000000;
These each have their upsides and downsides.
In the first case, the compiler cannot make any compile-time assumptions about the address, and may generate silly code because of that. For example, instead of accessing a nearby memory address by subtracting or adding the difference to the pointer, it will have to load the full 32-bit address.
This is the only one that uses e.g.
PORTA.OUTTGL instead of
PORTA->OUTTGL.
(If you rename the symbol as say
PORTA_struct, and define a macro
#define PORTA (&PORTA_struct), you can use
PORTA->OUTTGL in this case as well.)
In the second case,
PORTA and
PORTA_BRIDGE are not variables at all, they are just expressions (expanded from preprocessor macros).
(If you were to define e.g.
PORTA as
(*(volatile struct samc21_port *)0x60000000), noting the dereference at the beginning, you would use
PORTA.OUTTGL in this case too.)
In the third case, each access may incur an extra memory load or dereference, because while the variable is marked
const, the compiler may decide to generate code that loads the address from some address in Flash, instead of constructing it then and there. However, this seems to be the most common construction.
The fourth case is similar to the second case, but uses the C++
constexpr to denote that the address of the pointer is a compile-time constant.
In all cases, one will need to write some test code to be used with any new compiler, to see that it constructs the structure as desired (since things like bitfield fill order is up to the compiler), and that the chosen use pattern generates acceptable code. If it doesn't, well, there isn't much you can do except test if the code generated by another compatible definition (that requires no code changes) yields better results... but that is the risk here.