Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I put RMT Encoder in IRAM? #280

Open
faern opened this issue Jul 24, 2023 · 7 comments
Open

How do I put RMT Encoder in IRAM? #280

faern opened this issue Jul 24, 2023 · 7 comments

Comments

@faern
Copy link
Contributor

faern commented Jul 24, 2023

In the official documentation for RMT, it is highly recommended to put the encoding function into IRAM:

The encoding function is running in the ISR context. To speed up the encoding session, it’s high recommend to put the encoding function into IRAM. This can also avoid the cache miss during encoding. -- https://docs.espressif.com/projects/esp-idf/en/v5.1/esp32/api-reference/peripherals/rmt.html

But I don't understand how to do this. Neither the esp_idf_hal::rmt module docs, nor anything in esp_idf_sys or the Rust on ESP Book mentions IRAM at all.

How can I move arbitrary Rust functions to IRAM? And will this be possible with the iterator approach that TxRmtDriver is taking?

@dacut
Copy link
Contributor

dacut commented Jul 27, 2023

I looked at this for I2S. The answer I came to is it's not terribly easy to do today.

The main problem I ran into was determining the size of a function at runtime. LLVM does emit this into the debug symbols. For example, this Rust code:

pub fn add_it(a: i32, b: i32) -> i32 {
    a.wrapping_add(b)
}

Becomes this RISCV32 assembly (demangled):

        .globl _add_it
        .type   _add_it, @function
_add_it:
        add     a0, a0, a1
        ret
.Lfunc_end0:
        .size   _add_it, .Lfunc_end0-_add_it

Note the .size directive at the end. That .Lfunc_end0-_add_it calculation is what we need to determine the size of the function, but the .size directive only stores it into the symbol table.

Ideally, we'd get another symbol that does end up in the resulting object, like:

        .globl _add_it_size
_add_it_size:
        .dc.l  .Lfunc_end0-_add_it
        .size   _add_it_size, 4

This requires hooking into LLVM somehow, though. Would likely require a patch to LLVM since this intermediate assembly is never actually emitted in the normal workflow, just the MIR.

Barring this, the main alternative I can think of is to define an external symbol that gets resolved later, scrape this out of the debug info using readelf, then set that external symbol in a higher level crate or post-build step.

@MabezDev
Copy link
Member

So the CONFIG_RMT_ISR_IRAM_SAFE config option will handle the C side of putting the required methods in IRAM, so we then just have to worry about the rust side when we detect this option. This isn't trivial at all tbh, it's quite difficult to ensure that all the code/subroutine calls inside a function are all in IRAM too. In my experience its dependent on a number of things, including optimization levels as to whether the functions actually end up in IRAM.

@dacut
Copy link
Contributor

dacut commented Jul 27, 2023

Interesting... it looks like if you can get the function into a section whose name is in the form .iram1.#, there's some magic that copies that function into IRAM.

If I'm reading this correctly, CONFIG_RMT_ISR_IRAM_SAFE only allocates data structures in IRAM:

Actually putting a function into IRAM is done by decorating it with IRAM_ATTR, e.g.:
static void IRAM_ATTR rmt_tx_mark_eof(rmt_tx_channel_t *tx_chan). IRAM_ATTR is defined in esp_common/include/esp_attr.h as:

#define IRAM_ATTR _SECTION_ATTR_IMPL(".iram1", __COUNTER__)
#define _SECTION_ATTR_IMPL(SECTION, COUNTER) __attribute__((section(SECTION "." _COUNTER_STRINGIFY(COUNTER))))
#define _COUNTER_STRINGIFY(COUNTER) #COUNTER

I can't find the magic that does the copying, though. Maybe it's in the ROM? @MabezDev, do you know where this magic happens? (grep didn't turn up anything other than linker scripts for me.)

Both C and Rust have the issue of calling from IRAM into non-IRAM; C is just a bit more predictable in its code generation here. The C code does take care not to accept callback parameters it can't verify as being IRAM-safe, e.g. in rmt_tx_register_event_callbacks.

@MabezDev
Copy link
Member

The copying code will be buried deep in the startup code for esp-idf, but provided we also put our Rust functions there, i.e by using #[link_section = ".iram1"] then it should work, provided the code within that function is also in IRAM.

@faern
Copy link
Contributor Author

faern commented Jul 28, 2023

Great research both of you. I will experiment a bit with this. Do you know how to verify what functions are placed in IRAM for a given compiled firmware/app?

@MabezDev
Copy link
Member

Do you know how to verify what functions are placed in IRAM for a given compiled firmware/app?

Yes, you can use nm to find function names and their address, I'm not sure what chip you're using but you'll need to ensure that the address of the function is in the IRAM address space.

@dacut
Copy link
Contributor

dacut commented Aug 6, 2023

Adding on to this, as a note for myself and future spelunkers:
The code that is responsible for moving the function into IRAM is in the second stage bootloader, in process_segment, whose signature is:

process_segment(int index, uint32_t flash_addr, esp_image_segment_header_t *header, bool silent, bool do_load, bootloader_sha256_handle_t sha_handle, uint32_t *checksum)

esp_image_segment_header_t just has a load address and a length:

typedef struct {
    uint32_t load_addr;     /*!< Address of segment */
    uint32_t data_len;      /*!< Length of data */
} esp_image_segment_header_t;

This calls process_segment_data, which bootloader_mmaps the flash data, memcpys it into the destination, then bootloader_munmaps it.

The segment headers are created by the flash utility (e.g. espflash's elf.rs for translating the ELF binary into the firmware format).

I'm a bit surprised with the amount of code around memory mapping into the flash. It handles virtual to physical address translation cases, which I didn't think were a thing on any ESP32 chips; I thought addresses were directly mapped, and the flash accessible by the SPI bus (with cache possibly mediating accesses).

If you're using ESP32-C3/-C6 direct boot (no second-stage bootloader, just ROM into your app code) or have a custom second-stage bootloader, you're responsible for doing all of this yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Todo
Development

No branches or pull requests

3 participants