https://gcc.gnu.org/bugzilla/show_bug.cgi?id=121915
Bug ID: 121915
Summary: Feature request: bare-metal: attribute address (like
in AVRGCC) or equivalent for declaring MMIO objects at
fixed addresses
Product: gcc
Version: unknown
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c
Assignee: unassigned at gcc dot gnu.org
Reporter: antto at mail dot bg
Target Milestone: ---
Background -- what MMIO is, roughly:
• Memory-mapped I/O (MMIO) maps peripheral/special registers into the
processor's memory space so they look like ordinary variables.
• MMIO registers are part of the MCU hardware, often have special semantics,
and exist independently of program startup/teardown. The program should not
generate constructors/destructors/init code for them.
• They are typically accessed via volatile integer types in high-level code.
• Peripherals often have multiple registers grouped into structs. There may be
multiple instances of a peripheral on a chip.
Different package/configuration variants may include fewer
instances (e.g., 144-pin device: 10 UARTs; 32-pin variant: 4 UARTs).
Typical vendor C header example (this is a very long file):
// structure for a hypothetical UART peripheral
struct [[gnu::packed]] UART_t
{
volatile uint8_t STATUS;
volatile uint8_t INTFLAGS;
volatile uint8_t _reserved[2]; // padding
volatile uint32_t BAUD;
volatile uint32_t CTRLA; // often unions+bitfields instead of raw int
volatile uint32_t CTRLB;
/* more registers */
};
// the actual "declaration" of peripheral instances in memory:
#define UART0 *(UART_t*)(0x40003000)
#define UART1 *(UART_t*)(0x40003400)
#define UART2 *(UART_t*)(0x40003800)
// bitmasks for the registers typically provided as #defines or enums (not
shown)
Typical User code:
void main()
{
// some code ...
UART0.BAUD = 333;
UART0.CTRLA = UART_CTRLA_ENABLE;
while ((UART0.STATUS & UART_STATUS_READY) == 0);
// more code ...
}
This scheme works (of course), it should work in any C/C++ compiler, all of
the information is contained in the source code (in the header, technically).
The generated code with this AFAIK is "as good as as you can get", but:
What's the problem:
• In C++, the macro-style "declaration" (effectively a
*reinterpret_cast<UART_t*>(0x40003000)) doesn't work with templates and
constexpr.
• There's no actual symbol declared under the `UART0` name, i think this makes
debugging a bit.. awkward? I think you'll also never see `UART0` in a
compiler error/warning when you write something wrong.
• A better approach is to declare a symbol that refers to the peripheral at its
fixed address without emitting object code (a "hollow reference"), so it's
usable in templates/constexpr and generates no constructors/destructors.
• Existing user code like "UART0.BAUD = 333;" would still work after the
#define'd reinterpret_cast<> is replaced with a "hollow reference"
declaration, thus the potential changes to the vendor C headers are minimal.
Existing ways to do it:
• extern + linker symbol
extern UART_t UART0;
...and the address is provided via linker script (not shown here),
or via linker flags (-Wl,--defsym,UART0=0x40003000).
AFAIK this is currently the only "proper" way to do it elegantly.
Pros: creates a "hollow reference", works with templates/constexpr, the
generated code seems equal to the vendor C code results.
Cons: requires either long linker scripts for each MCU, or long piles of
linker flags (--defsym), in order to set the addresses.
The linker scripts typically come from the Vendor. The MCU i'm using
right now has 61 peripherals, much bigger ones exist!
• extern + inline asm
extern UART_t UART0;
asm
(
".global UART0\n"
".type UART0, %object\n"
".set UART0, 0x40003000"\n"
);
...and the address is provided with an inline asm statement in the
global scope which i don't fully understand, but it seems to work so
far.
I found this scheme after a much deeper websearch, this is what i use for
now (wrapped in a macro), but it's fragile: putting something like this:
"0x40003000+3*sizeof(UART_t)" in the address gets stringified into the asm
statement and there are no warnings and i think it can misbehave silently.
Pros: no linker involved, everything is in the source code.
Cons: the asm chunk perhaps doesn't look too wonderful?
I haven't found any "official" source saying that this is a
legitimate "proper" way to do it.
• Section + linker
Declare with attribute section and position the section via the linker,
i think some extra care has to be taken to prevent construction/
destruction/init.
Pros: this is probably a "proper" way to do it (or close enough).
Cons: requires linker involvement again.
• AVR GCC "address" attribute
UART_t [[gnu::address(0x40003000)]] UART0;
AVRGCC has the [[gnu::address(addr)]] variable attribute which declares a
"hollow reference".
Documentation:
https://gcc.gnu.org/onlinedocs/gcc/AVR-Variable-Attributes.html#index-address-variable-attribute_002c-AVR
Pros: it works, it works with templates/constexpr, visible in source,
no linker gymnastics, no sketchy inline asm.
Cons: not available in other bare-metal GCCs, e.g. arm-none-eabi.
• Other Compiler-specific
Some other compilers support special syntaxes or schemes like:
UART_t UART0 __at(0x40003000); // Microchip XC8,
XC16
UART_t UART0 __attribute__((address(0x40003000))) // Microchip XC32
__xdata __at (0x40003000) UART_t UART0; // SDCC
UART_t UART0 @ 0x40003000; // i've seen something like this, but i
can't find it now
#pragma location // IAR, but i'm not fully sure about this
one
Feature request:
• Add a GCC variable attribute to place a variable/struct at an absolute
address for relevant bare-metal targets (e.g., arm-none-eabi, RISC-V,
Extensa), similar to AVRGCC's address attribute.
• Desired properties (minimum):
• Declares a symbol that refers to an existing hardware object at a fixed
absolute address without generating global constructors/destructors or
object storage.
• Works with templates, constexpr, and compile-time expressions.
• Does not introduce overhead compared to the current
reinterpret_cast<> way.
• Makes the debugger aware of an actual Named symbol with a Type.
• Should be available maybe only on bare-metal targets where MMIO-as-memory
is valid.
• Should probably also be available in "C mode" (the AVRGCC attribute at
least works in both C/C++).
• Extra:
• Allow also declaring an array of known count.
• Allow the address to use compile-time computations (sizeof, arithmetic,
constexpr functions).
• Allow putting these "hollow references" in namespaces (e.g.
peripherals::UART0).
• It's like a reference, not a pointer. The address is known at compile
time, so if possible - allow getting the address from it as a constant
expression uintptr_t maybe?
• Notes:
• The attribute's name doesn't have to be exactly "address", it could be
anything that makes more sense: mmio_address, mmio, place_at, at, ...
• The syntax "order" doesn't have to be exactly "T attr NAME;", it could be
whatever makes more sense: "attr T NAME;" or "T NAME attr;" or ...
• I'm not sure if "static" and/or "extern" has to be added to the
declaration, whatever makes sense!
• If for some reason this can only be available in `-std=gnu*`, or if it
has to be enabled explicitly with a compile flag - that's fine by me.
• If it can't be an attribute, then a #pragma or something else?
Possible declaration syntax:
UART_t [[gnu::address(0x40003000)]] UART0;
UART_t __attribute__((address(0x40003400))) UART1;
// extra:
UART_t
[[gnu::address(PERIPHERAL_BASE_ADDR+0x3000+2*sizeof(UART_t))]] UART2;
UART_t [[gnu::address(&UART0
+3*sizeof(UART_t))]] UART3;
// alternatively, an array:
UART_t [[gnu::address(0x40003000)]] UART[10];