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];