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

Reply via email to