How to initialising an inmutable static with pre_init when specifying link_section

Hello

The documentation for the cortex-m-rt crate explains how one could place data in other sections than RAM: cortex_m_rt - Rust, but points out:

However, note that these sections are not initialised by cortex-m-rt, and so must be used either with MaybeUninit types or you must otherwise arrange for them to be initialised yourself, such as in pre_init.

The MaybeUninit route fails when trying to use its write() method, and I would prefer to be able to use the static without any level of indirection. I thought the pre_init route (being a special function and all) would allow me to initialize the struct, after all, that is what I understand the cortex-m-rt crate is doing for the "RAM" section. Failing, I tried and searched, to no avail. Online resources (including this forum) always pointing back to some indirection (Mutex, RWLock, Cells...).

Therefore I would like to ask: Can I initialize an immutable static with no indirection, just one initial write?

Here is the situation, with the comments in the before_main function being the compiler errors for my increasingly stubborn attempts.

// Creating static data, not initialised due to the link section.  
#[unsafe(link_section=".dtcm0.BUFFERS")]
static DATA_0000: aligned::Aligned<aligned::A32, [u8; 32]> =
    aligned::Aligned([0; 32]);

use cortex_m_rt::pre_init;
#[pre_init] // Must only exist once in the dependency tree.
unsafe fn before_main() {
    // "cannot assign to immutable static item `DATA_0000`"
    DATA_0000 = aligned::Aligned::<aligned::A32, [u8; 32]>([0; 32]);

    // "expected identifier, found keyword `mut`"
    // "expected type, found keyword `mut`"
    // "expected one of: `for`, parentheses, `fn`, `unsafe`, `extern`,
    //  identifier, `::`, `<`, `dyn`, square brackets, `*`, `&`, `!`, `impl`,
    //  `_`, lifetime"
    DATA_0000 as mut aligned::Aligned::<aligned::A32, [u8; 32]> =
        aligned::Aligned::<aligned::A32, [u8; 32]>([0; 32]);

    // "invalid left-hand side of assignment"
    // "non-primitive cast: `aligned::Aligned<A32, [u8; 32]>` as 
    // `&mut aligned::Aligned<A32, [u8; 32]>`"
    *&mut DATA_0000 as &mut aligned::Aligned::<aligned::A32, [u8; 32]> =
        aligned::Aligned::<aligned::A32, [u8; 32]>([0; 32]);
}

My (hopefully sane) plan is to write to the immutable static with a DMA on the wide, but slower AXI bus of a Cortex-M7 MCU, and defining DTMC0/1 memory sections as RAM, so that the stack and other static data reside close (and fast) to the CPU.
I believe this has 3 advantages:

  1. Less worry about the Data Cache, since the CPU is never allowed to write to the buffers. That allows me to focus on invalidating parts of the Dcache and no worry about the apparently very expensive clean calls.
  2. The code becomes simpler. Effectively, this would make the buffers writable only by a single "thread": the DMA. No unsafe blocks, no &raw mut .... pointers.
  3. Fewer bus collisions. The CPU should rarely need to access AXIRAM when the DMA does too. To further improve this, I could split the "AXIRAM" into its distinct parts "AXIRAM1", "AXIRAM2",... and taking advantage of that.

Given the DMA would be the one filling the buffers with anything useful, I contemplated whether an initialization would be necessary. My motivation for asking is the curiosity whether this is possible (and maybe a sanity check overall). Scrolling through the cortex-m-rt source (lib.rs - source) I see a lot of assembly :confused:. My hope that this can be done in plain rust, and quite quickly, is waning.

Here is an image from the SMT32H7R/S reference manual for some context:

Did you change static to static mut after the first attempt? Because it sure doesn’t look like an immutable static anymore, so the first error doesn’t seem right.

Anyway, you can’t write to an immutable static, except to parts of the static which are wrapped in UnsafeCell (which permits “shared read-write” access, though it’s then your responsibility to prevent data races and aliasing violations).

(Also, this does require unsafe. It sort of has to. The compiler doesn’t know that there’s no risk of data races due to platform-specific details.)

Apologies, the mut should not be there, I have edited the post to prevent further confusion.

Thank you for mentioning UnsafeCell. Something like this might be the only option, other than keeping the buffers on the stack and the stack in AXIRAM (or not initializing and letting the DMA do the work).

which also means you are not allowed to "initialize* the buffer.

in rust, an immutable static is truly immutable (except for the part wrapped in UnsafeCell). it can only be "initialized" with compile time constant, it does not mean "intialized just once at runtime and then read only for the rest of the time".

the DMA is out of the scope of the rust abstract machine, it does access the buffer concurrently along the CPU, but it should not be treated like a "thread".

thus the buffer should not be declared as "static" storage, because it isn't. what it actually is is memory-mapped io, and that's how you should access it from rust: using raw pointers and volatile reads. I believe using safe references for volatile memory will cause UB.

unsafe is unavoidable because of the nature of raw pointers, but it can be encapsulated and kept as an implementation detail, and it should be possible to make the api completely safe with careful design.

bus collisions should be coordinated by the arbiter and the stalls caused by them should be transparent to software (unless you have an unusual hardwares).

no, it's not necessary. in fact, it should not.

only static variables need to be initialized, not volatile buffers.

yes, it can be done in rust, but unsafe is required, at least for the driver implementation.

You wouldn't know, by any chance, how cortex-m-rt ends up having immutable statics initialized for the RAM section, i.e. when not specifying a different section such as #[unsafe(link_section=".dtcm0.BUFFERS")]?

I lean heavily on DMA - The Embedonomicon to hopefully catch all the pitfalls when using a DMA, relying on cache invalidation, compiler fences and leveraging the borrow system. You might still be right, as I currently do not posses the knowledge to check if I got everything right.

Something like this?:

    let buffer: *const [u8; 32] = 0x20000000 as *const [u8; 32];
    defmt::println!("buffer: {} -> {}", buffer, 
        unsafe { core::ptr::read_volatile(buffer) });

It works, and would allow exquisite flexibility. From the read_volatile in core::ptr - Rust documentation I gather that a core::ptr::read_volatile() will not be cached:

This implies that the operation will actually access memory and not e.g. be lowered to reusing data from a previous read.

No caching, no cache invalidation or cleanup. With the access not being reordered either, I do not have to worry about compiler fences and memory barriers.

In addition:

  • pointer - Rust allows for a plethora of information gathering of and access of the underlying type. I was not aware of this until now.
  • Messing with memory.x is reduced to specifying the right RAM location.

For my use case, I think this is the way forward. Walking the DMA - The Embedonomicon path seems currently out of my reach, with 'static lifetime for buffers being the one i still could not figure out.

it copies the initial values of the .data section from its LMA (in flash) to VMA (in ram). you can think of it as some form of minimal ELF loader.

however, this is not initialization, there's no code being run to initialize the content of the section: it is already fully initialized, at compile time. it's just the section has different VMA and LMA so it must be copied over, and since there's no OS and ELF loader on such embedded platforms, the architecture runtime crates (cortex-m-rt in this case, but same for other runtime crates e.g. riscv-rt) need to implement it themselves.

in fact, for truly immutable statics, they usually end up in the rodata section, which is typically placed into ROM (not RAM) by the linker script. the data section mainly contains mutable statics (or immutables but with UnsafeCell), but still, the initial values are computed at compile time, the runtime crate simply copies from LMA to VMA.

to be clear, the term "cache" has two (loosely related, but distinguishable) meanings:

what a volatile read guarantees is that the compiler will not "remember and reuse" the loaded value, in contrast to a safe reference where the compiler is allowed to assume the value does not change through the entire borrowing lifetime, thus it only need to load once and "cache" the value for later uses.

as for the processor's cache flushing and invalidation, volatile operations has absolute nothing to say about it. it is up to how the hardware is designed and configured. for example, if you have an MMU, the caching behavior is usually configured in a page-by-page basis, e.g. with some bits in the page table entries, and/or some special registers.

also, if the platform supports atomics, the memory ordering of atomic operations may also affect caching behavior.


for rust, volatile simply means the value may change over time, even if there's absolute no writes the address happend in rust code, this is exactly how peripheral registers and memory-mapped IOs are modeled in rust.

if you are interested in the details, here's a discussion on the topic of rust memory model and device io, and why volatile memory must be accessed explicitly through raw pointers, not through safe references (a.k.a. borrows):

Thank you for the explanation, incredibly interesting.

I always wondered where all the smart guys on the internet hang out. Now I know... I might come back to the link and try understanding a bit more.

Am I wrong with my assumption: A volatile read may, simply by how the hardware works, place the read data in the data cache. Performing the exact same volatile read again right after, the data is again read from memory, not the cache. I.e. whether data is cached or not has no effect on the outcome of the volatile read?

ptr::read or ptr::read_volatile only affect what instructions are emitted by the compiler, not what the hardware does with them.

See for example this godbolt example:

#![no_std]

#[unsafe(no_mangle)]
pub unsafe fn two_reads_volatile(addr: usize) -> u32 {
    let ptr: *const u32 = core::ptr::with_exposed_provenance(addr);
    // first read, discard it
    let _ = unsafe { ptr.read_volatile() };
    // second read, return it
    unsafe { ptr.read_volatile() }
}

#[unsafe(no_mangle)]
pub unsafe fn two_reads(addr: usize) -> u32 {
    let ptr: *const u32 = core::ptr::with_exposed_provenance(addr);
    // first read, discard it
    let _ = unsafe { ptr.read() };
    // second read, return it
    unsafe { ptr.read() }
}
two_reads:
        ldr     r0, [r0]
        bx      lr

two_reads_volatile:
        ldr     r1, [r0]
        ldr     r0, [r0]
        bx      lr

You see that both read and read_volatile emit the same ldr instruction. In the case of read, the compiler sees that the first read is not used, so that read is elided and only one instruction is emitted. For read_volatile, however, the compiler cannot optimize the first read away as it may have side effects, so both reads emit ldr instructions. Only the result of the second read is returned.


As far as the hardware is concerned, there's no difference between an ldr emitted from a read and one emitted from a read_volatile. Whether the value is read from memory or from cache depends on the cache policy set up by the memory management unit (MMU) / memory protection unit (MPU), and the current state of the cache.

Thank you very much for the clear and thoughtful explanation of several concepts at once, and the introduction to godbolt.org, very appreciated.

I will stay mindful of the DCache, disable or keep buffers 32 byte aligned and long by that multiple, invalidate and clean. Now that I was kindly taught how to properly create buffers in a separate section of memory (and memory mapped IO in general), I shall also have a look into the (on the MCU i use) MPU.

Thanks everybody for the generous help.