[{"data":1,"prerenderedAt":3889},["ShallowReactive",2],{"page-/blog/embedded/esp32/run_rust_on_app_core":3},{"id":4,"title":5,"body":6,"date":3882,"description":17,"extension":3883,"meta":3884,"navigation":245,"path":3885,"seo":3886,"stem":3887,"__hash__":3888},"content/embedded/ESP32/run_rust_on_app_core.md","Running Bare-Metal Rust Alongside ESP-IDF on the ESP32-S3's Second Core",{"type":7,"value":8,"toc":3862},"minimark",[9,13,18,27,30,33,53,59,70,77,80,83,88,95,178,189,196,203,205,209,212,216,224,231,272,275,279,282,294,300,307,778,782,785,795,802,807,942,946,953,958,1057,1062,1106,1112,1116,1122,1132,1139,1424,1428,1446,1449,1454,1671,1675,1678,1741,1748,1750,1754,1757,1760,1767,1770,1774,1780,1790,1794,1914,1921,1925,2037,2042,2053,2067,2072,2501,2505,2518,2692,2696,2699,2706,2882,2886,2889,2893,2900,2905,2932,2949,2953,2956,2966,3212,3216,3226,3348,3352,3358,3461,3465,3479,3606,3610,3613,3618,3652,3657,3770,3777,3781,3788,3796,3799,3843,3845,3849,3852,3855,3858],[10,11,5],"h1",{"id":12},"running-bare-metal-rust-alongside-esp-idf-on-the-esp32-s3s-second-core",[14,15,17],"h3",{"id":16},"building-a-hot-swappable-dual-paradigm-environment-on-espressif-silicon","Building a Hot-Swappable, Dual-Paradigm Environment on Espressif Silicon",[19,20,21,22,26],"p",{},"I've been working with the RP2350 and ",[23,24,25],"code",{},"no_std"," Rust for a while now, and I've really come to appreciate how Rust is designed — safe yet surprisingly straightforward. But my latest project needs Wi-Fi and BLE, and the RP2350 doesn't have wireless hardware built in. That meant switching to the ESP32-S3.",[19,28,29],{},"The ESP32-S3 is a great chip, but here's the catch: most Wi-Fi and Bluetooth functionality lives inside Espressif's ESP-IDF framework, which is a C-based SDK built on top of FreeRTOS. There are community Rust wrappers for parts of ESP-IDF, and Espressif themselves offer some Rust support, but both are a moving target — documentation is sparse compared to the mature C API, and there's always one or two critical features missing.",[19,31,32],{},"So I was stuck choosing between two imperfect options:",[34,35,36,47],"ul",{},[37,38,39,43,44,46],"li",{},[40,41,42],"strong",{},"Go all-in on Rust."," I'd get the language features and crates I love, but the ",[23,45,25],{}," ecosystem on ESP32-S3 is still young. In a shipping product, I didn't want to risk hitting undefined behavior in an immature HAL at 2 AM.",[37,48,49,52],{},[40,50,51],{},"Go all-in on ESP-IDF (C)."," I'd get battle-tested Wi-Fi and BLE stacks, but I'd be writing C for everything — including the business logic, audio processing, and data handling where Rust really shines.",[19,54,55,56],{},"Then I remembered something: ",[40,57,58],{},"the ESP32-S3 has two CPU cores.",[19,60,61,62,65,66,69],{},"There's an option buried in ESP-IDF's ",[23,63,64],{},"Kconfig"," called ",[23,67,68],{},"CONFIG_FREERTOS_UNICORE",". When you enable it, FreeRTOS only runs on Core 0. Core 1 just... sits there, stalled, doing nothing. That got me thinking: what if I let ESP-IDF own Core 0 for all the Wi-Fi, BLE, and system tasks, and then wake up Core 1 to run my own bare-metal Rust code — completely outside the RTOS?",[19,71,72,73,76],{},"Both cores share the same memory space, so passing data between them should be straightforward (though it does require some ",[23,74,75],{},"unsafe"," Rust). And since Core 1 wouldn't be managed by FreeRTOS, there'd be no scheduler preempting my time-critical audio processing loop.",[19,78,79],{},"After convincing myself this wasn't completely insane, I got to work. Here's how it all fits together.",[81,82],"hr",{},[84,85,87],"h2",{"id":86},"background-why-not-just-pin-a-freertos-task","Background: Why Not Just Pin a FreeRTOS Task?",[19,89,90,91,94],{},"Before diving in, it's worth addressing the obvious question: ESP-IDF already provides ",[23,92,93],{},"xTaskCreatePinnedToCore",", which can pin a task to a specific core:",[96,97,102],"pre",{"className":98,"code":99,"language":100,"meta":101,"style":101},"language-c shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// FreeRTOS provides this function to create a task on a specific core.\n// You could pin a Rust function to Core 1 this way — but FreeRTOS\n// would still manage the scheduler on that core.\nBaseType_t xTaskCreatePinnedToCore(\n    TaskFunction_t pvTaskCode,       // Function that implements the task\n    const char * const pcName,       // Human-readable name for debugging\n    const uint32_t usStackDepth,     // Stack size in words (not bytes)\n    void * const pvParameters,       // Arbitrary pointer passed to the task\n    UBaseType_t uxPriority,          // Priority (higher = more CPU time)\n    TaskHandle_t * const pvCreatedTask, // Output: handle to the created task\n    const BaseType_t xCoreID         // 0 = PRO core, 1 = APP core\n);\n","c","",[23,103,104,112,118,124,130,136,142,148,154,160,166,172],{"__ignoreMap":101},[105,106,109],"span",{"class":107,"line":108},"line",1,[105,110,111],{},"// FreeRTOS provides this function to create a task on a specific core.\n",[105,113,115],{"class":107,"line":114},2,[105,116,117],{},"// You could pin a Rust function to Core 1 this way — but FreeRTOS\n",[105,119,121],{"class":107,"line":120},3,[105,122,123],{},"// would still manage the scheduler on that core.\n",[105,125,127],{"class":107,"line":126},4,[105,128,129],{},"BaseType_t xTaskCreatePinnedToCore(\n",[105,131,133],{"class":107,"line":132},5,[105,134,135],{},"    TaskFunction_t pvTaskCode,       // Function that implements the task\n",[105,137,139],{"class":107,"line":138},6,[105,140,141],{},"    const char * const pcName,       // Human-readable name for debugging\n",[105,143,145],{"class":107,"line":144},7,[105,146,147],{},"    const uint32_t usStackDepth,     // Stack size in words (not bytes)\n",[105,149,151],{"class":107,"line":150},8,[105,152,153],{},"    void * const pvParameters,       // Arbitrary pointer passed to the task\n",[105,155,157],{"class":107,"line":156},9,[105,158,159],{},"    UBaseType_t uxPriority,          // Priority (higher = more CPU time)\n",[105,161,163],{"class":107,"line":162},10,[105,164,165],{},"    TaskHandle_t * const pvCreatedTask, // Output: handle to the created task\n",[105,167,169],{"class":107,"line":168},11,[105,170,171],{},"    const BaseType_t xCoreID         // 0 = PRO core, 1 = APP core\n",[105,173,175],{"class":107,"line":174},12,[105,176,177],{},");\n",[19,179,180,181,184,185,188],{},"You could absolutely compile your Rust code as a static library, export a ",[23,182,183],{},"pub extern \"C\" fn",", and have FreeRTOS run it on Core 1 via this API. The ESP-IDF build system would statically link your Rust ",[23,186,187],{},".a"," file into the firmware.",[19,190,191,192,195],{},"The problem is that ",[40,193,194],{},"FreeRTOS's scheduler is still running on Core 1."," Your task can be preempted at any time by higher-priority tasks or system ticks. For a high-performance audio processing loop where every microsecond of jitter matters, that's a non-starter. I needed a guarantee that nothing would interrupt my code once it started running.",[19,197,198,199,202],{},"By disabling FreeRTOS on Core 1 entirely (via ",[23,200,201],{},"CONFIG_FREERTOS_UNICORE=y","), we get an empty CPU that we can control directly at the hardware level — no scheduler, no context switching, no surprises.",[81,204],{},[84,206,208],{"id":207},"part-0-statically-linked-rust-on-a-bare-core","Part 0: Statically Linked Rust on a Bare Core",[19,210,211],{},"Let's start with the simpler approach: building Rust as a static library, linking it into the ESP-IDF firmware at compile time, and manually booting Core 1 to run it. This is the foundation everything else builds on.",[14,213,215],{"id":214},"step-1-reserve-memory-for-the-bare-metal-core-c-side","Step 1: Reserve Memory for the Bare-Metal Core (C Side)",[19,217,218,219,223],{},"When Core 1 wakes up outside of FreeRTOS, it doesn't get a dynamically allocated stack from the OS — because there ",[220,221,222],"em",{},"is"," no OS on that core. We need to manually set aside a chunk of RAM that ESP-IDF's heap allocator won't touch.",[19,225,226,227,230],{},"ESP-IDF provides the ",[23,228,229],{},"SOC_RESERVE_MEMORY_REGION"," macro for exactly this. It tells the bootloader and memory allocator to treat a specific address range as off-limits:",[96,232,234],{"className":98,"code":233,"language":100,"meta":101,"style":101},"#include \"heap_memory_layout.h\"\n\n// Reserve 128KB of internal SRAM for Core 1's stack and data.\n// The two hex values define the start and end addresses of the reserved region.\n// 0x3FCE9710 - 0x3FCC9710 = 0x20000 = 131072 bytes = 128KB.\n// \"rust_app\" is just a label for debugging — it shows up in boot logs.\nSOC_RESERVE_MEMORY_REGION(0x3FCC9710, 0x3FCE9710, rust_app);\n",[23,235,236,241,247,252,257,262,267],{"__ignoreMap":101},[105,237,238],{"class":107,"line":108},[105,239,240],{},"#include \"heap_memory_layout.h\"\n",[105,242,243],{"class":107,"line":114},[105,244,246],{"emptyLinePlaceholder":245},true,"\n",[105,248,249],{"class":107,"line":120},[105,250,251],{},"// Reserve 128KB of internal SRAM for Core 1's stack and data.\n",[105,253,254],{"class":107,"line":126},[105,255,256],{},"// The two hex values define the start and end addresses of the reserved region.\n",[105,258,259],{"class":107,"line":132},[105,260,261],{},"// 0x3FCE9710 - 0x3FCC9710 = 0x20000 = 131072 bytes = 128KB.\n",[105,263,264],{"class":107,"line":138},[105,265,266],{},"// \"rust_app\" is just a label for debugging — it shows up in boot logs.\n",[105,268,269],{"class":107,"line":144},[105,270,271],{},"SOC_RESERVE_MEMORY_REGION(0x3FCC9710, 0x3FCE9710, rust_app);\n",[19,273,274],{},"Why 128KB? It's a reasonable default for an embedded stack plus some working memory. You can adjust this range depending on how much RAM your Rust code needs — just make sure the addresses fall within the ESP32-S3's internal SRAM region and don't overlap with anything ESP-IDF is using.",[14,276,278],{"id":277},"step-2-wake-up-core-1-from-the-c-side","Step 2: Wake Up Core 1 from the C Side",[19,280,281],{},"This is the main ESP-IDF application running on Core 0. Its job is to:",[283,284,285,288,291],"ol",{},[37,286,287],{},"Set up the system (Wi-Fi, peripherals, etc. — or in our test case, just boot).",[37,289,290],{},"Wake up Core 1 and point it at our Rust code.",[37,292,293],{},"Go about its normal FreeRTOS business.",[19,295,296,297,299],{},"Instead of using ",[23,298,93],{},", we're talking directly to the ESP32-S3's hardware registers to boot Core 1. We set a boot address, enable the clock, release the stall, and pulse the reset line. Core 1 wakes up completely independent of FreeRTOS.",[19,301,302,303,306],{},"To verify that everything is working, Core 0 will read a shared counter variable (",[23,304,305],{},"RUST_CORE1_COUNTER",") that the Rust code on Core 1 increments in a loop.",[96,308,310],{"className":98,"code":309,"language":100,"meta":101,"style":101},"#include \u003Cstdio.h>\n#include \u003Cstdint.h>\n#include \"esp_log.h\"\n#include \"esp_cpu.h\"\n#include \"heap_memory_layout.h\"\n#include \"freertos/FreeRTOS.h\"\n#include \"freertos/task.h\"\n#include \"soc/system_reg.h\"\n#include \"soc/soc.h\"\n\nstatic const char *TAG = \"rust_app_core\";\n\n// Reserve memory so ESP-IDF's heap allocator doesn't use it.\n// (Same macro from Step 1 — it must appear in a compiled C file.)\nSOC_RESERVE_MEMORY_REGION(0x3FCC9710, 0x3FCE9710, rust_app);\n\n// ---- External symbols ----\n// These are defined in other files and resolved at link time:\n//   rust_app_core_entry  — the Rust function (from our .a library)\n//   app_core_trampoline  — tiny assembly stub that sets the stack pointer\n//   _rust_stack_top      — address from our linker script (top of reserved 128KB)\n//   ets_set_appcpu_boot_addr — ROM function that tells Core 1 where to start\nextern void rust_app_core_entry(void);\nextern void ets_set_appcpu_boot_addr(uint32_t);\nextern uint32_t _rust_stack_top;\nextern void app_core_trampoline(void);\n\n/*\n * Boot Core 1 by directly manipulating ESP32-S3 hardware registers.\n * This bypasses FreeRTOS entirely — Core 1 will run our code with\n * no scheduler, no interrupts (unless we set them up), and no OS.\n */\nstatic void start_rust_on_app_core(void)\n{\n    ESP_LOGI(TAG, \"Starting Rust on Core 1...\");\n    ESP_LOGI(TAG, \"  Stack: 0x3FCC9710 - 0x3FCE9710 (128K)\");\n\n    /* 1. Tell Core 1 where to begin executing after it resets.\n     *    This ROM function writes the address into a register that the\n     *    CPU reads on boot. We point it at our assembly trampoline. */\n    ets_set_appcpu_boot_addr((uint32_t)app_core_trampoline);\n\n    /* 2. Hardware-level wake-up sequence for Core 1.\n     *    These register writes control the clock, stall, and reset\n     *    signals for the second CPU core. */\n\n    // Enable the clock gate — Core 1 can't run without a clock signal.\n    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                      SYSTEM_CONTROL_CORE_1_CLKGATE_EN);\n\n    // Clear the RUNSTALL bit. While stalled, the core is frozen mid-instruction.\n    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                        SYSTEM_CONTROL_CORE_1_RUNSTALL);\n\n    // Pulse the reset line: assert it, then immediately de-assert.\n    // This causes Core 1 to reboot and jump to the address we set above.\n    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                      SYSTEM_CONTROL_CORE_1_RESETING);\n    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                        SYSTEM_CONTROL_CORE_1_RESETING);\n\n    ESP_LOGI(TAG, \"Core 1 released\");\n}\n\n// This counter lives in the Rust code. Because it's an AtomicU32 with\n// #[no_mangle], the C linker can find it by this exact name.\nextern volatile uint32_t RUST_CORE1_COUNTER;\n\nvoid app_main(void)\n{\n    ESP_LOGI(TAG, \"Core 0: Starting IDF app\");\n\n    // Wake up Core 1 and start the Rust code\n    start_rust_on_app_core();\n\n    // Core 0 continues running FreeRTOS as normal.\n    // Here we just monitor the shared counter to prove both cores are alive.\n    while (1)\n    {\n        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)RUST_CORE1_COUNTER);\n        vTaskDelay(pdMS_TO_TICKS(1000)); // Print once per second\n    }\n}\n",[23,311,312,317,322,327,332,336,341,346,351,356,360,365,369,375,381,386,391,397,403,409,415,421,427,433,439,445,451,456,462,468,474,480,486,492,498,504,510,515,521,527,533,539,544,550,556,562,567,573,579,585,590,596,602,608,613,619,625,630,636,641,647,652,658,664,669,675,681,687,692,698,703,709,714,720,726,731,737,743,749,755,761,767,773],{"__ignoreMap":101},[105,313,314],{"class":107,"line":108},[105,315,316],{},"#include \u003Cstdio.h>\n",[105,318,319],{"class":107,"line":114},[105,320,321],{},"#include \u003Cstdint.h>\n",[105,323,324],{"class":107,"line":120},[105,325,326],{},"#include \"esp_log.h\"\n",[105,328,329],{"class":107,"line":126},[105,330,331],{},"#include \"esp_cpu.h\"\n",[105,333,334],{"class":107,"line":132},[105,335,240],{},[105,337,338],{"class":107,"line":138},[105,339,340],{},"#include \"freertos/FreeRTOS.h\"\n",[105,342,343],{"class":107,"line":144},[105,344,345],{},"#include \"freertos/task.h\"\n",[105,347,348],{"class":107,"line":150},[105,349,350],{},"#include \"soc/system_reg.h\"\n",[105,352,353],{"class":107,"line":156},[105,354,355],{},"#include \"soc/soc.h\"\n",[105,357,358],{"class":107,"line":162},[105,359,246],{"emptyLinePlaceholder":245},[105,361,362],{"class":107,"line":168},[105,363,364],{},"static const char *TAG = \"rust_app_core\";\n",[105,366,367],{"class":107,"line":174},[105,368,246],{"emptyLinePlaceholder":245},[105,370,372],{"class":107,"line":371},13,[105,373,374],{},"// Reserve memory so ESP-IDF's heap allocator doesn't use it.\n",[105,376,378],{"class":107,"line":377},14,[105,379,380],{},"// (Same macro from Step 1 — it must appear in a compiled C file.)\n",[105,382,384],{"class":107,"line":383},15,[105,385,271],{},[105,387,389],{"class":107,"line":388},16,[105,390,246],{"emptyLinePlaceholder":245},[105,392,394],{"class":107,"line":393},17,[105,395,396],{},"// ---- External symbols ----\n",[105,398,400],{"class":107,"line":399},18,[105,401,402],{},"// These are defined in other files and resolved at link time:\n",[105,404,406],{"class":107,"line":405},19,[105,407,408],{},"//   rust_app_core_entry  — the Rust function (from our .a library)\n",[105,410,412],{"class":107,"line":411},20,[105,413,414],{},"//   app_core_trampoline  — tiny assembly stub that sets the stack pointer\n",[105,416,418],{"class":107,"line":417},21,[105,419,420],{},"//   _rust_stack_top      — address from our linker script (top of reserved 128KB)\n",[105,422,424],{"class":107,"line":423},22,[105,425,426],{},"//   ets_set_appcpu_boot_addr — ROM function that tells Core 1 where to start\n",[105,428,430],{"class":107,"line":429},23,[105,431,432],{},"extern void rust_app_core_entry(void);\n",[105,434,436],{"class":107,"line":435},24,[105,437,438],{},"extern void ets_set_appcpu_boot_addr(uint32_t);\n",[105,440,442],{"class":107,"line":441},25,[105,443,444],{},"extern uint32_t _rust_stack_top;\n",[105,446,448],{"class":107,"line":447},26,[105,449,450],{},"extern void app_core_trampoline(void);\n",[105,452,454],{"class":107,"line":453},27,[105,455,246],{"emptyLinePlaceholder":245},[105,457,459],{"class":107,"line":458},28,[105,460,461],{},"/*\n",[105,463,465],{"class":107,"line":464},29,[105,466,467],{}," * Boot Core 1 by directly manipulating ESP32-S3 hardware registers.\n",[105,469,471],{"class":107,"line":470},30,[105,472,473],{}," * This bypasses FreeRTOS entirely — Core 1 will run our code with\n",[105,475,477],{"class":107,"line":476},31,[105,478,479],{}," * no scheduler, no interrupts (unless we set them up), and no OS.\n",[105,481,483],{"class":107,"line":482},32,[105,484,485],{}," */\n",[105,487,489],{"class":107,"line":488},33,[105,490,491],{},"static void start_rust_on_app_core(void)\n",[105,493,495],{"class":107,"line":494},34,[105,496,497],{},"{\n",[105,499,501],{"class":107,"line":500},35,[105,502,503],{},"    ESP_LOGI(TAG, \"Starting Rust on Core 1...\");\n",[105,505,507],{"class":107,"line":506},36,[105,508,509],{},"    ESP_LOGI(TAG, \"  Stack: 0x3FCC9710 - 0x3FCE9710 (128K)\");\n",[105,511,513],{"class":107,"line":512},37,[105,514,246],{"emptyLinePlaceholder":245},[105,516,518],{"class":107,"line":517},38,[105,519,520],{},"    /* 1. Tell Core 1 where to begin executing after it resets.\n",[105,522,524],{"class":107,"line":523},39,[105,525,526],{},"     *    This ROM function writes the address into a register that the\n",[105,528,530],{"class":107,"line":529},40,[105,531,532],{},"     *    CPU reads on boot. We point it at our assembly trampoline. */\n",[105,534,536],{"class":107,"line":535},41,[105,537,538],{},"    ets_set_appcpu_boot_addr((uint32_t)app_core_trampoline);\n",[105,540,542],{"class":107,"line":541},42,[105,543,246],{"emptyLinePlaceholder":245},[105,545,547],{"class":107,"line":546},43,[105,548,549],{},"    /* 2. Hardware-level wake-up sequence for Core 1.\n",[105,551,553],{"class":107,"line":552},44,[105,554,555],{},"     *    These register writes control the clock, stall, and reset\n",[105,557,559],{"class":107,"line":558},45,[105,560,561],{},"     *    signals for the second CPU core. */\n",[105,563,565],{"class":107,"line":564},46,[105,566,246],{"emptyLinePlaceholder":245},[105,568,570],{"class":107,"line":569},47,[105,571,572],{},"    // Enable the clock gate — Core 1 can't run without a clock signal.\n",[105,574,576],{"class":107,"line":575},48,[105,577,578],{},"    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n",[105,580,582],{"class":107,"line":581},49,[105,583,584],{},"                      SYSTEM_CONTROL_CORE_1_CLKGATE_EN);\n",[105,586,588],{"class":107,"line":587},50,[105,589,246],{"emptyLinePlaceholder":245},[105,591,593],{"class":107,"line":592},51,[105,594,595],{},"    // Clear the RUNSTALL bit. While stalled, the core is frozen mid-instruction.\n",[105,597,599],{"class":107,"line":598},52,[105,600,601],{},"    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n",[105,603,605],{"class":107,"line":604},53,[105,606,607],{},"                        SYSTEM_CONTROL_CORE_1_RUNSTALL);\n",[105,609,611],{"class":107,"line":610},54,[105,612,246],{"emptyLinePlaceholder":245},[105,614,616],{"class":107,"line":615},55,[105,617,618],{},"    // Pulse the reset line: assert it, then immediately de-assert.\n",[105,620,622],{"class":107,"line":621},56,[105,623,624],{},"    // This causes Core 1 to reboot and jump to the address we set above.\n",[105,626,628],{"class":107,"line":627},57,[105,629,578],{},[105,631,633],{"class":107,"line":632},58,[105,634,635],{},"                      SYSTEM_CONTROL_CORE_1_RESETING);\n",[105,637,639],{"class":107,"line":638},59,[105,640,601],{},[105,642,644],{"class":107,"line":643},60,[105,645,646],{},"                        SYSTEM_CONTROL_CORE_1_RESETING);\n",[105,648,650],{"class":107,"line":649},61,[105,651,246],{"emptyLinePlaceholder":245},[105,653,655],{"class":107,"line":654},62,[105,656,657],{},"    ESP_LOGI(TAG, \"Core 1 released\");\n",[105,659,661],{"class":107,"line":660},63,[105,662,663],{},"}\n",[105,665,667],{"class":107,"line":666},64,[105,668,246],{"emptyLinePlaceholder":245},[105,670,672],{"class":107,"line":671},65,[105,673,674],{},"// This counter lives in the Rust code. Because it's an AtomicU32 with\n",[105,676,678],{"class":107,"line":677},66,[105,679,680],{},"// #[no_mangle], the C linker can find it by this exact name.\n",[105,682,684],{"class":107,"line":683},67,[105,685,686],{},"extern volatile uint32_t RUST_CORE1_COUNTER;\n",[105,688,690],{"class":107,"line":689},68,[105,691,246],{"emptyLinePlaceholder":245},[105,693,695],{"class":107,"line":694},69,[105,696,697],{},"void app_main(void)\n",[105,699,701],{"class":107,"line":700},70,[105,702,497],{},[105,704,706],{"class":107,"line":705},71,[105,707,708],{},"    ESP_LOGI(TAG, \"Core 0: Starting IDF app\");\n",[105,710,712],{"class":107,"line":711},72,[105,713,246],{"emptyLinePlaceholder":245},[105,715,717],{"class":107,"line":716},73,[105,718,719],{},"    // Wake up Core 1 and start the Rust code\n",[105,721,723],{"class":107,"line":722},74,[105,724,725],{},"    start_rust_on_app_core();\n",[105,727,729],{"class":107,"line":728},75,[105,730,246],{"emptyLinePlaceholder":245},[105,732,734],{"class":107,"line":733},76,[105,735,736],{},"    // Core 0 continues running FreeRTOS as normal.\n",[105,738,740],{"class":107,"line":739},77,[105,741,742],{},"    // Here we just monitor the shared counter to prove both cores are alive.\n",[105,744,746],{"class":107,"line":745},78,[105,747,748],{},"    while (1)\n",[105,750,752],{"class":107,"line":751},79,[105,753,754],{},"    {\n",[105,756,758],{"class":107,"line":757},80,[105,759,760],{},"        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)RUST_CORE1_COUNTER);\n",[105,762,764],{"class":107,"line":763},81,[105,765,766],{},"        vTaskDelay(pdMS_TO_TICKS(1000)); // Print once per second\n",[105,768,770],{"class":107,"line":769},82,[105,771,772],{},"    }\n",[105,774,776],{"class":107,"line":775},83,[105,777,663],{},[14,779,781],{"id":780},"step-3-the-assembly-trampoline","Step 3: The Assembly Trampoline",[19,783,784],{},"When a CPU core wakes up from reset, it doesn't have a stack yet. And without a stack, it can't call any C or Rust functions — function calls need somewhere to store return addresses and local variables.",[19,786,787,788,791,792,794],{},"The ESP32-S3 uses the Xtensa instruction set architecture, where register ",[23,789,790],{},"a1"," serves as the stack pointer. Our tiny assembly stub loads the address of our reserved memory into ",[23,793,790],{},", then jumps into Rust. That's all it does — just two instructions.",[19,796,797,798,801],{},"We place this code in the ",[23,799,800],{},".iram1"," section, which maps to Internal RAM. This is important because when a core first boots, it may not have flash caching set up yet. Code in IRAM is always accessible.",[19,803,804],{},[40,805,806],{},"app_core_trampoline.S",[96,808,812],{"className":809,"code":810,"language":811,"meta":101,"style":101},"language-s shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","/*\n * app_core_trampoline.S\n *\n * Minimal startup code for Core 1. Sets the stack pointer to our\n * reserved memory region, then jumps to the Rust entry point.\n *\n * Placed in IRAM (.iram1) so it's available immediately after core\n * reset, before flash cache is configured.\n */\n\n    .section .iram1, \"ax\"       /* \"ax\" = allocatable + executable */\n    .global  app_core_trampoline\n    .type    app_core_trampoline, @function\n    .align   4                  /* Xtensa requires 4-byte alignment */\n\napp_core_trampoline:\n    /* Load the top of our 128KB reserved stack into register a1.\n     * Stacks grow downward on Xtensa, so \"top\" means the highest\n     * address — the stack will grow toward lower addresses from here. */\n    movi  a1, _rust_stack_top\n\n    /* Jump to the Rust entry function. call0 is a \"windowless\" call\n     * (no register window rotation), suitable for bare-metal startup.\n     * This function never returns — it contains an infinite loop. */\n    call0 rust_app_core_entry\n\n    .size app_core_trampoline, . - app_core_trampoline\n","s",[23,813,814,818,823,828,833,838,842,847,852,856,860,865,870,875,880,884,889,894,899,904,909,913,918,923,928,933,937],{"__ignoreMap":101},[105,815,816],{"class":107,"line":108},[105,817,461],{},[105,819,820],{"class":107,"line":114},[105,821,822],{}," * app_core_trampoline.S\n",[105,824,825],{"class":107,"line":120},[105,826,827],{}," *\n",[105,829,830],{"class":107,"line":126},[105,831,832],{}," * Minimal startup code for Core 1. Sets the stack pointer to our\n",[105,834,835],{"class":107,"line":132},[105,836,837],{}," * reserved memory region, then jumps to the Rust entry point.\n",[105,839,840],{"class":107,"line":138},[105,841,827],{},[105,843,844],{"class":107,"line":144},[105,845,846],{}," * Placed in IRAM (.iram1) so it's available immediately after core\n",[105,848,849],{"class":107,"line":150},[105,850,851],{}," * reset, before flash cache is configured.\n",[105,853,854],{"class":107,"line":156},[105,855,485],{},[105,857,858],{"class":107,"line":162},[105,859,246],{"emptyLinePlaceholder":245},[105,861,862],{"class":107,"line":168},[105,863,864],{},"    .section .iram1, \"ax\"       /* \"ax\" = allocatable + executable */\n",[105,866,867],{"class":107,"line":174},[105,868,869],{},"    .global  app_core_trampoline\n",[105,871,872],{"class":107,"line":371},[105,873,874],{},"    .type    app_core_trampoline, @function\n",[105,876,877],{"class":107,"line":377},[105,878,879],{},"    .align   4                  /* Xtensa requires 4-byte alignment */\n",[105,881,882],{"class":107,"line":383},[105,883,246],{"emptyLinePlaceholder":245},[105,885,886],{"class":107,"line":388},[105,887,888],{},"app_core_trampoline:\n",[105,890,891],{"class":107,"line":393},[105,892,893],{},"    /* Load the top of our 128KB reserved stack into register a1.\n",[105,895,896],{"class":107,"line":399},[105,897,898],{},"     * Stacks grow downward on Xtensa, so \"top\" means the highest\n",[105,900,901],{"class":107,"line":405},[105,902,903],{},"     * address — the stack will grow toward lower addresses from here. */\n",[105,905,906],{"class":107,"line":411},[105,907,908],{},"    movi  a1, _rust_stack_top\n",[105,910,911],{"class":107,"line":417},[105,912,246],{"emptyLinePlaceholder":245},[105,914,915],{"class":107,"line":423},[105,916,917],{},"    /* Jump to the Rust entry function. call0 is a \"windowless\" call\n",[105,919,920],{"class":107,"line":429},[105,921,922],{},"     * (no register window rotation), suitable for bare-metal startup.\n",[105,924,925],{"class":107,"line":435},[105,926,927],{},"     * This function never returns — it contains an infinite loop. */\n",[105,929,930],{"class":107,"line":441},[105,931,932],{},"    call0 rust_app_core_entry\n",[105,934,935],{"class":107,"line":447},[105,936,246],{"emptyLinePlaceholder":245},[105,938,939],{"class":107,"line":453},[105,940,941],{},"    .size app_core_trampoline, . - app_core_trampoline\n",[14,943,945],{"id":944},"step-4-gluing-it-together-with-cmake-and-a-linker-script","Step 4: Gluing It Together with CMake and a Linker Script",[19,947,948,949,952],{},"ESP-IDF uses CMake as its build system. We need to tell it about three extra things: our assembly file, our pre-compiled Rust library, and a custom linker script that defines where ",[23,950,951],{},"_rust_stack_top"," lives.",[19,954,955],{},[40,956,957],{},"CMakeLists.txt",[96,959,963],{"className":960,"code":961,"language":962,"meta":101,"style":101},"language-cmake shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Register our C source and the assembly trampoline as component sources.\n# ESP-IDF builds each directory under \"main/\" as a \"component.\"\nidf_component_register(\n    SRCS \"main.c\" \"app_core_trampoline.S\"\n    INCLUDE_DIRS \".\"\n)\n\n# Tell the linker about our pre-compiled Rust static library.\n# This .a file is produced by `cargo build` and copied into main/lib/.\nadd_prebuilt_library(rust_app \"${CMAKE_CURRENT_SOURCE_DIR}/lib/libesp_rust_app.a\")\n\n# Link the Rust library into our component. INTERFACE means anything\n# that depends on this component also gets the Rust symbols.\ntarget_link_libraries(${COMPONENT_LIB} INTERFACE rust_app)\n\n# Inject our custom linker script. This is how the assembly trampoline\n# knows the numeric value of _rust_stack_top.\ntarget_link_options(${COMPONENT_LIB}\n    INTERFACE \"-T${CMAKE_CURRENT_SOURCE_DIR}/rust_stack.ld\")\n","cmake",[23,964,965,970,975,980,985,990,995,999,1004,1009,1014,1018,1023,1028,1033,1037,1042,1047,1052],{"__ignoreMap":101},[105,966,967],{"class":107,"line":108},[105,968,969],{},"# Register our C source and the assembly trampoline as component sources.\n",[105,971,972],{"class":107,"line":114},[105,973,974],{},"# ESP-IDF builds each directory under \"main/\" as a \"component.\"\n",[105,976,977],{"class":107,"line":120},[105,978,979],{},"idf_component_register(\n",[105,981,982],{"class":107,"line":126},[105,983,984],{},"    SRCS \"main.c\" \"app_core_trampoline.S\"\n",[105,986,987],{"class":107,"line":132},[105,988,989],{},"    INCLUDE_DIRS \".\"\n",[105,991,992],{"class":107,"line":138},[105,993,994],{},")\n",[105,996,997],{"class":107,"line":144},[105,998,246],{"emptyLinePlaceholder":245},[105,1000,1001],{"class":107,"line":150},[105,1002,1003],{},"# Tell the linker about our pre-compiled Rust static library.\n",[105,1005,1006],{"class":107,"line":156},[105,1007,1008],{},"# This .a file is produced by `cargo build` and copied into main/lib/.\n",[105,1010,1011],{"class":107,"line":162},[105,1012,1013],{},"add_prebuilt_library(rust_app \"${CMAKE_CURRENT_SOURCE_DIR}/lib/libesp_rust_app.a\")\n",[105,1015,1016],{"class":107,"line":168},[105,1017,246],{"emptyLinePlaceholder":245},[105,1019,1020],{"class":107,"line":174},[105,1021,1022],{},"# Link the Rust library into our component. INTERFACE means anything\n",[105,1024,1025],{"class":107,"line":371},[105,1026,1027],{},"# that depends on this component also gets the Rust symbols.\n",[105,1029,1030],{"class":107,"line":377},[105,1031,1032],{},"target_link_libraries(${COMPONENT_LIB} INTERFACE rust_app)\n",[105,1034,1035],{"class":107,"line":383},[105,1036,246],{"emptyLinePlaceholder":245},[105,1038,1039],{"class":107,"line":388},[105,1040,1041],{},"# Inject our custom linker script. This is how the assembly trampoline\n",[105,1043,1044],{"class":107,"line":393},[105,1045,1046],{},"# knows the numeric value of _rust_stack_top.\n",[105,1048,1049],{"class":107,"line":399},[105,1050,1051],{},"target_link_options(${COMPONENT_LIB}\n",[105,1053,1054],{"class":107,"line":405},[105,1055,1056],{},"    INTERFACE \"-T${CMAKE_CURRENT_SOURCE_DIR}/rust_stack.ld\")\n",[19,1058,1059],{},[40,1060,1061],{},"rust_stack.ld",[96,1063,1067],{"className":1064,"code":1065,"language":1066,"meta":101,"style":101},"language-ld shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","/*\n * Custom linker script fragment.\n *\n * Defines _rust_stack_top as the END of our reserved 128KB block.\n * Stacks grow downward, so the \"top\" is the highest address.\n * The assembly trampoline loads this value into register a1.\n */\n_rust_stack_top = 0x3FCE9710;\n","ld",[23,1068,1069,1073,1078,1082,1087,1092,1097,1101],{"__ignoreMap":101},[105,1070,1071],{"class":107,"line":108},[105,1072,461],{},[105,1074,1075],{"class":107,"line":114},[105,1076,1077],{}," * Custom linker script fragment.\n",[105,1079,1080],{"class":107,"line":120},[105,1081,827],{},[105,1083,1084],{"class":107,"line":126},[105,1085,1086],{}," * Defines _rust_stack_top as the END of our reserved 128KB block.\n",[105,1088,1089],{"class":107,"line":132},[105,1090,1091],{}," * Stacks grow downward, so the \"top\" is the highest address.\n",[105,1093,1094],{"class":107,"line":138},[105,1095,1096],{}," * The assembly trampoline loads this value into register a1.\n",[105,1098,1099],{"class":107,"line":144},[105,1100,485],{},[105,1102,1103],{"class":107,"line":150},[105,1104,1105],{},"_rust_stack_top = 0x3FCE9710;\n",[19,1107,1108,1109,1111],{},"The connection here is: the linker script provides a symbol (",[23,1110,951],{},") → the assembly trampoline references that symbol to set the stack pointer → the C code triggers the hardware boot sequence that starts Core 1 at the trampoline.",[14,1113,1115],{"id":1114},"step-5-the-bare-metal-rust-application","Step 5: The Bare-Metal Rust Application",[19,1117,1118,1119,1121],{},"Finally, here's the code that actually runs on Core 1. It's entirely ",[23,1120,25],{}," — there's no operating system, no allocator, no standard library. Just raw hardware access.",[19,1123,1124,1125,1128,1129,1131],{},"The key technique here is ",[23,1126,1127],{},"AtomicU32",". Atomics are special CPU instructions that read and write memory in a way that's safe even when two cores access the same address simultaneously. By using ",[23,1130,1127],{}," for our shared counter, we avoid race conditions without needing a mutex (which wouldn't work easily across the OS/bare-metal boundary anyway).",[19,1133,1134,1135,1138],{},"The ",[23,1136,1137],{},"spin_loop"," hint tells the CPU \"I'm intentionally busy-waiting\" — on some architectures this reduces power consumption or yields resources to other hardware threads. Here it also serves as a simple delay so the counter doesn't overflow instantly.",[96,1140,1144],{"className":1141,"code":1142,"language":1143,"meta":101,"style":101},"language-rust shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// no_std: we're running without the Rust standard library.\n// There's no OS below us — no heap, no threads, no println!.\n#![no_std]\n\n// no_main: we don't use Rust's normal main() entry point.\n// Instead, Core 1 enters via rust_app_core_entry(), called from assembly.\n#![no_main]\n\nuse core::panic::PanicInfo;\nuse core::sync::atomic::{AtomicU32, Ordering};\n\n// Every no_std binary needs a panic handler. When something goes wrong\n// (array out of bounds, unwrap on None, etc.), this function is called.\n// On a bare-metal core with no debugger attached, there's not much we\n// can do — so we just loop forever. A production system might toggle\n// an LED or write to a shared error flag that Core 0 can read.\n#[panic_handler]\nfn panic(_info: &PanicInfo) -> ! {\n    loop {}\n}\n\n// The shared counter. Both cores can see this variable because it lives\n// in the same memory space.\n//\n// #[unsafe(no_mangle)] prevents Rust from renaming this symbol during\n// compilation. Without it, Rust would generate something like\n// \"_ZN12esp_rust_app18RUST_CORE1_COUNTER17h...\" — and the C code\n// wouldn't be able to find it by name.\n//\n// AtomicU32 ensures that reads and writes are atomic at the CPU level,\n// so Core 0 will never see a \"torn\" (half-written) value.\n#[unsafe(no_mangle)]\npub static RUST_CORE1_COUNTER: AtomicU32 = AtomicU32::new(0);\n\n// The entry point called by the assembly trampoline after it sets\n// up the stack pointer. The `-> !` return type means \"this function\n// never returns\" — it runs an infinite loop.\n//\n// `extern \"C\"` uses the C calling convention so the assembly code\n// (and the C linker) can call this function correctly.\n#[unsafe(no_mangle)]\npub extern \"C\" fn rust_app_core_entry() -> ! {\n    loop {\n        // Atomically increment the counter by 1.\n        // Ordering::Relaxed means we don't need any memory ordering\n        // guarantees beyond the atomicity of this single operation.\n        // (For a simple counter, Relaxed is sufficient.)\n        RUST_CORE1_COUNTER.fetch_add(1, Ordering::Relaxed);\n\n        // Busy-wait loop as a simple delay. spin_loop() is a CPU hint\n        // that says \"I'm spinning, not doing real work\" — on some\n        // architectures this saves power or avoids starving other\n        // hardware threads.\n        for _ in 0..1_000_000 {\n            core::hint::spin_loop();\n        }\n    }\n}\n","rust",[23,1145,1146,1151,1156,1161,1165,1170,1175,1180,1184,1189,1194,1198,1203,1208,1213,1218,1223,1228,1233,1238,1242,1246,1251,1256,1261,1266,1271,1276,1281,1285,1290,1295,1300,1305,1309,1314,1319,1324,1328,1333,1338,1342,1347,1352,1357,1362,1367,1372,1377,1381,1386,1391,1396,1401,1406,1411,1416,1420],{"__ignoreMap":101},[105,1147,1148],{"class":107,"line":108},[105,1149,1150],{},"// no_std: we're running without the Rust standard library.\n",[105,1152,1153],{"class":107,"line":114},[105,1154,1155],{},"// There's no OS below us — no heap, no threads, no println!.\n",[105,1157,1158],{"class":107,"line":120},[105,1159,1160],{},"#![no_std]\n",[105,1162,1163],{"class":107,"line":126},[105,1164,246],{"emptyLinePlaceholder":245},[105,1166,1167],{"class":107,"line":132},[105,1168,1169],{},"// no_main: we don't use Rust's normal main() entry point.\n",[105,1171,1172],{"class":107,"line":138},[105,1173,1174],{},"// Instead, Core 1 enters via rust_app_core_entry(), called from assembly.\n",[105,1176,1177],{"class":107,"line":144},[105,1178,1179],{},"#![no_main]\n",[105,1181,1182],{"class":107,"line":150},[105,1183,246],{"emptyLinePlaceholder":245},[105,1185,1186],{"class":107,"line":156},[105,1187,1188],{},"use core::panic::PanicInfo;\n",[105,1190,1191],{"class":107,"line":162},[105,1192,1193],{},"use core::sync::atomic::{AtomicU32, Ordering};\n",[105,1195,1196],{"class":107,"line":168},[105,1197,246],{"emptyLinePlaceholder":245},[105,1199,1200],{"class":107,"line":174},[105,1201,1202],{},"// Every no_std binary needs a panic handler. When something goes wrong\n",[105,1204,1205],{"class":107,"line":371},[105,1206,1207],{},"// (array out of bounds, unwrap on None, etc.), this function is called.\n",[105,1209,1210],{"class":107,"line":377},[105,1211,1212],{},"// On a bare-metal core with no debugger attached, there's not much we\n",[105,1214,1215],{"class":107,"line":383},[105,1216,1217],{},"// can do — so we just loop forever. A production system might toggle\n",[105,1219,1220],{"class":107,"line":388},[105,1221,1222],{},"// an LED or write to a shared error flag that Core 0 can read.\n",[105,1224,1225],{"class":107,"line":393},[105,1226,1227],{},"#[panic_handler]\n",[105,1229,1230],{"class":107,"line":399},[105,1231,1232],{},"fn panic(_info: &PanicInfo) -> ! {\n",[105,1234,1235],{"class":107,"line":405},[105,1236,1237],{},"    loop {}\n",[105,1239,1240],{"class":107,"line":411},[105,1241,663],{},[105,1243,1244],{"class":107,"line":417},[105,1245,246],{"emptyLinePlaceholder":245},[105,1247,1248],{"class":107,"line":423},[105,1249,1250],{},"// The shared counter. Both cores can see this variable because it lives\n",[105,1252,1253],{"class":107,"line":429},[105,1254,1255],{},"// in the same memory space.\n",[105,1257,1258],{"class":107,"line":435},[105,1259,1260],{},"//\n",[105,1262,1263],{"class":107,"line":441},[105,1264,1265],{},"// #[unsafe(no_mangle)] prevents Rust from renaming this symbol during\n",[105,1267,1268],{"class":107,"line":447},[105,1269,1270],{},"// compilation. Without it, Rust would generate something like\n",[105,1272,1273],{"class":107,"line":453},[105,1274,1275],{},"// \"_ZN12esp_rust_app18RUST_CORE1_COUNTER17h...\" — and the C code\n",[105,1277,1278],{"class":107,"line":458},[105,1279,1280],{},"// wouldn't be able to find it by name.\n",[105,1282,1283],{"class":107,"line":464},[105,1284,1260],{},[105,1286,1287],{"class":107,"line":470},[105,1288,1289],{},"// AtomicU32 ensures that reads and writes are atomic at the CPU level,\n",[105,1291,1292],{"class":107,"line":476},[105,1293,1294],{},"// so Core 0 will never see a \"torn\" (half-written) value.\n",[105,1296,1297],{"class":107,"line":482},[105,1298,1299],{},"#[unsafe(no_mangle)]\n",[105,1301,1302],{"class":107,"line":488},[105,1303,1304],{},"pub static RUST_CORE1_COUNTER: AtomicU32 = AtomicU32::new(0);\n",[105,1306,1307],{"class":107,"line":494},[105,1308,246],{"emptyLinePlaceholder":245},[105,1310,1311],{"class":107,"line":500},[105,1312,1313],{},"// The entry point called by the assembly trampoline after it sets\n",[105,1315,1316],{"class":107,"line":506},[105,1317,1318],{},"// up the stack pointer. The `-> !` return type means \"this function\n",[105,1320,1321],{"class":107,"line":512},[105,1322,1323],{},"// never returns\" — it runs an infinite loop.\n",[105,1325,1326],{"class":107,"line":517},[105,1327,1260],{},[105,1329,1330],{"class":107,"line":523},[105,1331,1332],{},"// `extern \"C\"` uses the C calling convention so the assembly code\n",[105,1334,1335],{"class":107,"line":529},[105,1336,1337],{},"// (and the C linker) can call this function correctly.\n",[105,1339,1340],{"class":107,"line":535},[105,1341,1299],{},[105,1343,1344],{"class":107,"line":541},[105,1345,1346],{},"pub extern \"C\" fn rust_app_core_entry() -> ! {\n",[105,1348,1349],{"class":107,"line":546},[105,1350,1351],{},"    loop {\n",[105,1353,1354],{"class":107,"line":552},[105,1355,1356],{},"        // Atomically increment the counter by 1.\n",[105,1358,1359],{"class":107,"line":558},[105,1360,1361],{},"        // Ordering::Relaxed means we don't need any memory ordering\n",[105,1363,1364],{"class":107,"line":564},[105,1365,1366],{},"        // guarantees beyond the atomicity of this single operation.\n",[105,1368,1369],{"class":107,"line":569},[105,1370,1371],{},"        // (For a simple counter, Relaxed is sufficient.)\n",[105,1373,1374],{"class":107,"line":575},[105,1375,1376],{},"        RUST_CORE1_COUNTER.fetch_add(1, Ordering::Relaxed);\n",[105,1378,1379],{"class":107,"line":581},[105,1380,246],{"emptyLinePlaceholder":245},[105,1382,1383],{"class":107,"line":587},[105,1384,1385],{},"        // Busy-wait loop as a simple delay. spin_loop() is a CPU hint\n",[105,1387,1388],{"class":107,"line":592},[105,1389,1390],{},"        // that says \"I'm spinning, not doing real work\" — on some\n",[105,1392,1393],{"class":107,"line":598},[105,1394,1395],{},"        // architectures this saves power or avoids starving other\n",[105,1397,1398],{"class":107,"line":604},[105,1399,1400],{},"        // hardware threads.\n",[105,1402,1403],{"class":107,"line":610},[105,1404,1405],{},"        for _ in 0..1_000_000 {\n",[105,1407,1408],{"class":107,"line":615},[105,1409,1410],{},"            core::hint::spin_loop();\n",[105,1412,1413],{"class":107,"line":621},[105,1414,1415],{},"        }\n",[105,1417,1418],{"class":107,"line":627},[105,1419,772],{},[105,1421,1422],{"class":107,"line":632},[105,1423,663],{},[14,1425,1427],{"id":1426},"step-6-configuring-the-rust-build-cargotoml","Step 6: Configuring the Rust Build (Cargo.toml)",[19,1429,1430,1431,1433,1434,1437,1438,1441,1442,1445],{},"ESP-IDF's build system expects a standard C-compatible static archive (",[23,1432,187],{}," file). By default, ",[23,1435,1436],{},"cargo build"," produces Rust-specific ",[23,1439,1440],{},".rlib"," files that only the Rust toolchain understands. We need to tell Cargo to output a ",[23,1443,1444],{},"staticlib"," instead.",[19,1447,1448],{},"We also apply aggressive size optimizations — on a microcontroller with limited flash, every kilobyte matters.",[19,1450,1451],{},[40,1452,1453],{},"Cargo.toml",[96,1455,1459],{"className":1456,"code":1457,"language":1458,"meta":101,"style":101},"language-toml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","[package]\nedition      = \"2024\"\nname         = \"esp_rust_app\"\nrust-version = \"1.88\"\nversion      = \"0.1.0\"\n\n# Output a C-compatible static library (.a file).\n# This is what lets us link Rust code into an ESP-IDF project\n# the same way you'd link any C library.\n[lib]\ncrate-type = [\"staticlib\"]\n\n[dependencies]\n# esp-hal provides low-level hardware access for the ESP32-S3.\n# Even though we're not using most of its features yet, it sets up\n# the critical-section implementation we need for atomics.\nesp-hal = { version = \"~1.0\", features = [\"esp32s3\"] }\n# Provides the critical-section implementation needed for safe\n# interrupt handling in no_std environments.\ncritical-section = \"1.2.0\"\n\n[profile.dev]\n# Rust's default debug builds are unoptimized and produce huge binaries.\n# On embedded, even dev builds should use \"s\" (optimize for size) to\n# keep things manageable. Without this, you might overflow flash.\nopt-level = \"s\"\n\n[profile.release]\n# Force the compiler to use a single codegen unit. This is slower to\n# compile, but allows LLVM to see the entire crate at once and perform\n# better cross-function optimizations (inlining, dead code elimination).\ncodegen-units    = 1\ndebug            = 2     # Keep debug symbols (useful for GDB on-device)\ndebug-assertions = false # Disable assert!() checks in release\nincremental      = false # Disable incremental compilation for cleaner builds\n\n# \"fat\" Link-Time Optimization. The linker analyzes ALL code (including\n# dependencies) as a single unit, aggressively removing unused functions\n# and inlining across crate boundaries. This can dramatically reduce\n# binary size — often 30-50% smaller than without LTO.\nlto              = 'fat'\nopt-level        = 's'   # Optimize for size over speed\noverflow-checks  = false # Disable integer overflow checks in release\n","toml",[23,1460,1461,1466,1471,1476,1481,1486,1490,1495,1500,1505,1510,1515,1519,1524,1529,1534,1539,1544,1549,1554,1559,1563,1568,1573,1578,1583,1588,1592,1597,1602,1607,1612,1617,1622,1627,1632,1636,1641,1646,1651,1656,1661,1666],{"__ignoreMap":101},[105,1462,1463],{"class":107,"line":108},[105,1464,1465],{},"[package]\n",[105,1467,1468],{"class":107,"line":114},[105,1469,1470],{},"edition      = \"2024\"\n",[105,1472,1473],{"class":107,"line":120},[105,1474,1475],{},"name         = \"esp_rust_app\"\n",[105,1477,1478],{"class":107,"line":126},[105,1479,1480],{},"rust-version = \"1.88\"\n",[105,1482,1483],{"class":107,"line":132},[105,1484,1485],{},"version      = \"0.1.0\"\n",[105,1487,1488],{"class":107,"line":138},[105,1489,246],{"emptyLinePlaceholder":245},[105,1491,1492],{"class":107,"line":144},[105,1493,1494],{},"# Output a C-compatible static library (.a file).\n",[105,1496,1497],{"class":107,"line":150},[105,1498,1499],{},"# This is what lets us link Rust code into an ESP-IDF project\n",[105,1501,1502],{"class":107,"line":156},[105,1503,1504],{},"# the same way you'd link any C library.\n",[105,1506,1507],{"class":107,"line":162},[105,1508,1509],{},"[lib]\n",[105,1511,1512],{"class":107,"line":168},[105,1513,1514],{},"crate-type = [\"staticlib\"]\n",[105,1516,1517],{"class":107,"line":174},[105,1518,246],{"emptyLinePlaceholder":245},[105,1520,1521],{"class":107,"line":371},[105,1522,1523],{},"[dependencies]\n",[105,1525,1526],{"class":107,"line":377},[105,1527,1528],{},"# esp-hal provides low-level hardware access for the ESP32-S3.\n",[105,1530,1531],{"class":107,"line":383},[105,1532,1533],{},"# Even though we're not using most of its features yet, it sets up\n",[105,1535,1536],{"class":107,"line":388},[105,1537,1538],{},"# the critical-section implementation we need for atomics.\n",[105,1540,1541],{"class":107,"line":393},[105,1542,1543],{},"esp-hal = { version = \"~1.0\", features = [\"esp32s3\"] }\n",[105,1545,1546],{"class":107,"line":399},[105,1547,1548],{},"# Provides the critical-section implementation needed for safe\n",[105,1550,1551],{"class":107,"line":405},[105,1552,1553],{},"# interrupt handling in no_std environments.\n",[105,1555,1556],{"class":107,"line":411},[105,1557,1558],{},"critical-section = \"1.2.0\"\n",[105,1560,1561],{"class":107,"line":417},[105,1562,246],{"emptyLinePlaceholder":245},[105,1564,1565],{"class":107,"line":423},[105,1566,1567],{},"[profile.dev]\n",[105,1569,1570],{"class":107,"line":429},[105,1571,1572],{},"# Rust's default debug builds are unoptimized and produce huge binaries.\n",[105,1574,1575],{"class":107,"line":435},[105,1576,1577],{},"# On embedded, even dev builds should use \"s\" (optimize for size) to\n",[105,1579,1580],{"class":107,"line":441},[105,1581,1582],{},"# keep things manageable. Without this, you might overflow flash.\n",[105,1584,1585],{"class":107,"line":447},[105,1586,1587],{},"opt-level = \"s\"\n",[105,1589,1590],{"class":107,"line":453},[105,1591,246],{"emptyLinePlaceholder":245},[105,1593,1594],{"class":107,"line":458},[105,1595,1596],{},"[profile.release]\n",[105,1598,1599],{"class":107,"line":464},[105,1600,1601],{},"# Force the compiler to use a single codegen unit. This is slower to\n",[105,1603,1604],{"class":107,"line":470},[105,1605,1606],{},"# compile, but allows LLVM to see the entire crate at once and perform\n",[105,1608,1609],{"class":107,"line":476},[105,1610,1611],{},"# better cross-function optimizations (inlining, dead code elimination).\n",[105,1613,1614],{"class":107,"line":482},[105,1615,1616],{},"codegen-units    = 1\n",[105,1618,1619],{"class":107,"line":488},[105,1620,1621],{},"debug            = 2     # Keep debug symbols (useful for GDB on-device)\n",[105,1623,1624],{"class":107,"line":494},[105,1625,1626],{},"debug-assertions = false # Disable assert!() checks in release\n",[105,1628,1629],{"class":107,"line":500},[105,1630,1631],{},"incremental      = false # Disable incremental compilation for cleaner builds\n",[105,1633,1634],{"class":107,"line":506},[105,1635,246],{"emptyLinePlaceholder":245},[105,1637,1638],{"class":107,"line":512},[105,1639,1640],{},"# \"fat\" Link-Time Optimization. The linker analyzes ALL code (including\n",[105,1642,1643],{"class":107,"line":517},[105,1644,1645],{},"# dependencies) as a single unit, aggressively removing unused functions\n",[105,1647,1648],{"class":107,"line":523},[105,1649,1650],{},"# and inlining across crate boundaries. This can dramatically reduce\n",[105,1652,1653],{"class":107,"line":529},[105,1654,1655],{},"# binary size — often 30-50% smaller than without LTO.\n",[105,1657,1658],{"class":107,"line":535},[105,1659,1660],{},"lto              = 'fat'\n",[105,1662,1663],{"class":107,"line":541},[105,1664,1665],{},"opt-level        = 's'   # Optimize for size over speed\n",[105,1667,1668],{"class":107,"line":546},[105,1669,1670],{},"overflow-checks  = false # Disable integer overflow checks in release\n",[14,1672,1674],{"id":1673},"building-and-testing","Building and Testing",[19,1676,1677],{},"Build the Rust library, then copy it into the ESP-IDF project:",[96,1679,1683],{"className":1680,"code":1681,"language":1682,"meta":101,"style":101},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Build the Rust code targeting the ESP32-S3's Xtensa CPU.\n# This produces a .a file in target/xtensa-esp32s3-none-elf/release/\ncargo build --release --target xtensa-esp32s3-none-elf\n\n# Copy the compiled library to where our CMakeLists.txt expects it.\ncp target/xtensa-esp32s3-none-elf/release/libesp_rust_app.a \\\n   /path/to/idf-project/main/lib/\n","bash",[23,1684,1685,1691,1696,1715,1719,1724,1736],{"__ignoreMap":101},[105,1686,1687],{"class":107,"line":108},[105,1688,1690],{"class":1689},"sHwdD","# Build the Rust code targeting the ESP32-S3's Xtensa CPU.\n",[105,1692,1693],{"class":107,"line":114},[105,1694,1695],{"class":1689},"# This produces a .a file in target/xtensa-esp32s3-none-elf/release/\n",[105,1697,1698,1702,1706,1709,1712],{"class":107,"line":120},[105,1699,1701],{"class":1700},"sBMFI","cargo",[105,1703,1705],{"class":1704},"sfazB"," build",[105,1707,1708],{"class":1704}," --release",[105,1710,1711],{"class":1704}," --target",[105,1713,1714],{"class":1704}," xtensa-esp32s3-none-elf\n",[105,1716,1717],{"class":107,"line":126},[105,1718,246],{"emptyLinePlaceholder":245},[105,1720,1721],{"class":107,"line":132},[105,1722,1723],{"class":1689},"# Copy the compiled library to where our CMakeLists.txt expects it.\n",[105,1725,1726,1729,1732],{"class":107,"line":138},[105,1727,1728],{"class":1700},"cp",[105,1730,1731],{"class":1704}," target/xtensa-esp32s3-none-elf/release/libesp_rust_app.a",[105,1733,1735],{"class":1734},"sTEyZ"," \\\n",[105,1737,1738],{"class":107,"line":144},[105,1739,1740],{"class":1704},"   /path/to/idf-project/main/lib/\n",[19,1742,1743,1744,1747],{},"Then build and flash the ESP-IDF project as usual (",[23,1745,1746],{},"idf.py build flash monitor","). You should see the counter incrementing on your serial monitor — proof that Core 1 is running your Rust code independently of FreeRTOS.",[81,1749],{},[84,1751,1753],{"id":1752},"part-1-loading-rust-at-runtime-hot-swappable-programs","Part 1: Loading Rust at Runtime (Hot-Swappable Programs)",[19,1755,1756],{},"The static linking approach from Part 0 works well, but it has a limitation: the Rust code is baked into the firmware at compile time. Every time you change the Rust program, you have to rebuild the entire ESP-IDF project, re-link everything, and reflash the whole firmware.",[19,1758,1759],{},"What if the Rust program could be swapped at runtime? Imagine this: the ESP-IDF firmware acts like a bootloader, setting up the hardware environment (Wi-Fi, BLE, peripherals). The Rust program lives in its own flash partition and can be updated independently. Core 0 could even write a new Rust program to flash and reset Core 1 to run it — no full firmware rebuild required.",[19,1761,1762,1763,1766],{},"This is especially useful if the Rust code is ",[220,1764,1765],{},"user-provided content"," — for example, a customizable audio processing pipeline that end users can update.",[19,1768,1769],{},"To make this work, we need to change several things.",[14,1771,1773],{"id":1772},"step-1-build-rust-as-a-standalone-binary","Step 1: Build Rust as a Standalone Binary",[19,1775,1776,1777,1779],{},"In Part 0, Cargo built a static library (",[23,1778,187],{}," file) that got linked into the ESP-IDF binary. Now we need Cargo to produce a standalone executable binary with its own entry point — something that can be loaded and jumped to at a specific memory address.",[19,1781,1782,1783,1786,1787,1789],{},"First, remove the ",[23,1784,1785],{},"[lib]"," section from ",[23,1788,1453],{}," so Cargo builds a binary instead of a library:",[19,1791,1792],{},[40,1793,1453],{},[96,1795,1797],{"className":1456,"code":1796,"language":1458,"meta":101,"style":101},"[package]\nedition      = \"2024\"\nname         = \"esp_rust_app\"\nrust-version = \"1.88\"\nversion      = \"0.1.0\"\n\n# No [lib] section — we want a standalone binary, not a library.\n# Cargo will look for src/main.rs as the entry point.\n\n[dependencies]\nesp-hal = { version = \"~1.0\", features = [\"esp32s3\"] }\ncritical-section = \"1.2.0\"\n\n[profile.dev]\n# Even dev builds need size optimization on embedded — unoptimized Rust\n# produces enormous binaries that won't fit in flash.\nopt-level = \"s\"\n\n[profile.release]\ncodegen-units    = 1     # Single codegen unit for best LLVM optimization\ndebug            = 2\ndebug-assertions = false\nincremental      = false\nlto              = 'fat' # Full link-time optimization across all crates\nopt-level        = 's'   # Optimize for size\noverflow-checks  = false\n",[23,1798,1799,1803,1807,1811,1815,1819,1823,1828,1833,1837,1841,1845,1849,1853,1857,1862,1867,1871,1875,1879,1884,1889,1894,1899,1904,1909],{"__ignoreMap":101},[105,1800,1801],{"class":107,"line":108},[105,1802,1465],{},[105,1804,1805],{"class":107,"line":114},[105,1806,1470],{},[105,1808,1809],{"class":107,"line":120},[105,1810,1475],{},[105,1812,1813],{"class":107,"line":126},[105,1814,1480],{},[105,1816,1817],{"class":107,"line":132},[105,1818,1485],{},[105,1820,1821],{"class":107,"line":138},[105,1822,246],{"emptyLinePlaceholder":245},[105,1824,1825],{"class":107,"line":144},[105,1826,1827],{},"# No [lib] section — we want a standalone binary, not a library.\n",[105,1829,1830],{"class":107,"line":150},[105,1831,1832],{},"# Cargo will look for src/main.rs as the entry point.\n",[105,1834,1835],{"class":107,"line":156},[105,1836,246],{"emptyLinePlaceholder":245},[105,1838,1839],{"class":107,"line":162},[105,1840,1523],{},[105,1842,1843],{"class":107,"line":168},[105,1844,1543],{},[105,1846,1847],{"class":107,"line":174},[105,1848,1558],{},[105,1850,1851],{"class":107,"line":371},[105,1852,246],{"emptyLinePlaceholder":245},[105,1854,1855],{"class":107,"line":377},[105,1856,1567],{},[105,1858,1859],{"class":107,"line":383},[105,1860,1861],{},"# Even dev builds need size optimization on embedded — unoptimized Rust\n",[105,1863,1864],{"class":107,"line":388},[105,1865,1866],{},"# produces enormous binaries that won't fit in flash.\n",[105,1868,1869],{"class":107,"line":393},[105,1870,1587],{},[105,1872,1873],{"class":107,"line":399},[105,1874,246],{"emptyLinePlaceholder":245},[105,1876,1877],{"class":107,"line":405},[105,1878,1596],{},[105,1880,1881],{"class":107,"line":411},[105,1882,1883],{},"codegen-units    = 1     # Single codegen unit for best LLVM optimization\n",[105,1885,1886],{"class":107,"line":417},[105,1887,1888],{},"debug            = 2\n",[105,1890,1891],{"class":107,"line":423},[105,1892,1893],{},"debug-assertions = false\n",[105,1895,1896],{"class":107,"line":429},[105,1897,1898],{},"incremental      = false\n",[105,1900,1901],{"class":107,"line":435},[105,1902,1903],{},"lto              = 'fat' # Full link-time optimization across all crates\n",[105,1905,1906],{"class":107,"line":441},[105,1907,1908],{},"opt-level        = 's'   # Optimize for size\n",[105,1910,1911],{"class":107,"line":447},[105,1912,1913],{},"overflow-checks  = false\n",[19,1915,1916,1917,1920],{},"Next, we need a ",[23,1918,1919],{},".cargo/config.toml"," to tell the Rust toolchain how to link our binary. Since we're not linking into ESP-IDF anymore, we need to supply our own linker script and disable the standard startup code:",[19,1922,1923],{},[40,1924,1919],{},[96,1926,1928],{"className":1456,"code":1927,"language":1458,"meta":101,"style":101},"[target.xtensa-esp32s3-none-elf]\nrustflags = [\n    \"-Clink-arg=-Tlink.x\",             # Use our custom linker script\n    \"-Clink-arg=-nostdlib\",             # Don't link the C standard library\n    \"-Clink-arg=-nostartfiles\",         # Don't include default startup code\n    \"-Clink-arg=-Wl,--no-gc-sections\", # Keep all sections (don't garbage-collect)\n    \"-Clink-arg=-Wl,--no-check-sections\", # Skip section overlap checks\n    \"-Clink-arg=-mtext-section-literals\",  # Xtensa-specific: inline literal pools\n    \"-Clink-arg=-Wl,--entry=rust_app_core_entry\", # Set the ELF entry point\n]\n\n[env]\n\n[build]\n# Default build target — no need to pass --target every time\ntarget = \"xtensa-esp32s3-none-elf\"\n\n[unstable]\n# Build the `core` library from source for our target.\n# The Xtensa target doesn't ship prebuilt standard libraries,\n# so Cargo needs to compile `core` itself.\nbuild-std = [\"core\"]\n",[23,1929,1930,1935,1940,1945,1950,1955,1960,1965,1970,1975,1980,1984,1989,1993,1998,2003,2008,2012,2017,2022,2027,2032],{"__ignoreMap":101},[105,1931,1932],{"class":107,"line":108},[105,1933,1934],{},"[target.xtensa-esp32s3-none-elf]\n",[105,1936,1937],{"class":107,"line":114},[105,1938,1939],{},"rustflags = [\n",[105,1941,1942],{"class":107,"line":120},[105,1943,1944],{},"    \"-Clink-arg=-Tlink.x\",             # Use our custom linker script\n",[105,1946,1947],{"class":107,"line":126},[105,1948,1949],{},"    \"-Clink-arg=-nostdlib\",             # Don't link the C standard library\n",[105,1951,1952],{"class":107,"line":132},[105,1953,1954],{},"    \"-Clink-arg=-nostartfiles\",         # Don't include default startup code\n",[105,1956,1957],{"class":107,"line":138},[105,1958,1959],{},"    \"-Clink-arg=-Wl,--no-gc-sections\", # Keep all sections (don't garbage-collect)\n",[105,1961,1962],{"class":107,"line":144},[105,1963,1964],{},"    \"-Clink-arg=-Wl,--no-check-sections\", # Skip section overlap checks\n",[105,1966,1967],{"class":107,"line":150},[105,1968,1969],{},"    \"-Clink-arg=-mtext-section-literals\",  # Xtensa-specific: inline literal pools\n",[105,1971,1972],{"class":107,"line":156},[105,1973,1974],{},"    \"-Clink-arg=-Wl,--entry=rust_app_core_entry\", # Set the ELF entry point\n",[105,1976,1977],{"class":107,"line":162},[105,1978,1979],{},"]\n",[105,1981,1982],{"class":107,"line":168},[105,1983,246],{"emptyLinePlaceholder":245},[105,1985,1986],{"class":107,"line":174},[105,1987,1988],{},"[env]\n",[105,1990,1991],{"class":107,"line":371},[105,1992,246],{"emptyLinePlaceholder":245},[105,1994,1995],{"class":107,"line":377},[105,1996,1997],{},"[build]\n",[105,1999,2000],{"class":107,"line":383},[105,2001,2002],{},"# Default build target — no need to pass --target every time\n",[105,2004,2005],{"class":107,"line":388},[105,2006,2007],{},"target = \"xtensa-esp32s3-none-elf\"\n",[105,2009,2010],{"class":107,"line":393},[105,2011,246],{"emptyLinePlaceholder":245},[105,2013,2014],{"class":107,"line":399},[105,2015,2016],{},"[unstable]\n",[105,2018,2019],{"class":107,"line":405},[105,2020,2021],{},"# Build the `core` library from source for our target.\n",[105,2023,2024],{"class":107,"line":411},[105,2025,2026],{},"# The Xtensa target doesn't ship prebuilt standard libraries,\n",[105,2028,2029],{"class":107,"line":417},[105,2030,2031],{},"# so Cargo needs to compile `core` itself.\n",[105,2033,2034],{"class":107,"line":423},[105,2035,2036],{},"build-std = [\"core\"]\n",[2038,2039,2041],"h4",{"id":2040},"the-linker-script","The Linker Script",[19,2043,2044,2045,2048,2049,2052],{},"In Part 0, the ",[23,2046,2047],{},".bss"," (uninitialized global variables) and ",[23,2050,2051],{},".data"," (initialized global variables) sections from our Rust code were handled by the ESP-IDF linker — they became part of the main firmware's memory layout. But now that we're building a standalone binary, we need our own linker script to tell the toolchain where everything goes.",[19,2054,2055,2056,2059,2060,2063,2064,2066],{},"This is a critical piece of the puzzle. The linker script defines two memory regions: ",[23,2057,2058],{},"FLASH_TEXT"," (where our code lives in flash, mapped to a virtual address via the MMU) and ",[23,2061,2062],{},"DRAM"," (our reserved 128KB of RAM from the ",[23,2065,229],{}," macro).",[19,2068,2069],{},[40,2070,2071],{},"link.x",[96,2073,2075],{"className":1064,"code":2074,"language":1066,"meta":101,"style":101},"/* Declare our Rust entry function as the ELF entry point */\nENTRY(rust_app_core_entry)\n\nMEMORY\n{\n    /*\n     * FLASH_TEXT: Where our code will be mapped in the address space.\n     * 0x42400000 is a virtual address — the MMU will map our flash\n     * partition to this region at runtime (we'll set that up in C).\n     * 512K should be plenty for most Rust programs.\n     */\n    FLASH_TEXT (rx)  : ORIGIN = 0x42400000, LENGTH = 512K\n\n    /*\n     * DRAM: The 128KB block we reserved with SOC_RESERVE_MEMORY_REGION.\n     * This is physical SRAM that both cores can access directly.\n     * Our stack, .data, and .bss all live here.\n     */\n    DRAM       (rw)  : ORIGIN = 0x3FCC9710, LENGTH = 128K\n}\n\nSECTIONS\n{\n    /*\n     * 4-byte header at offset 0 of the binary.\n     * This is a simple convention: the first 4 bytes of our binary\n     * contain the address of rust_app_core_entry. The C bootloader\n     * reads this to know where to jump.\n     */\n    .header : {\n        LONG(rust_app_core_entry)\n    } > FLASH_TEXT\n\n    /*\n     * Xtensa puts function literal pools (constants used by instructions)\n     * in .literal sections. We place the entry function's literals and\n     * code first to ensure they're near the beginning of the binary.\n     */\n    .entry_lit : {\n        KEEP(*(.literal.rust_app_core_entry))\n    } > FLASH_TEXT\n\n    .entry : {\n        KEEP(*(.text.rust_app_core_entry))\n    } > FLASH_TEXT\n\n    /* All remaining code and read-only data goes into flash */\n    .text : {\n        *(.literal .literal.*)    /* Xtensa literal pools */\n        *(.text .text.*)          /* Executable code */\n        *(.rodata .rodata.*)      /* Read-only data (strings, constants) */\n    } > FLASH_TEXT\n\n    /*\n     * .data: Initialized global/static variables.\n     * These live in DRAM at runtime (VMA), but their initial values\n     * are stored in flash (LMA). Our Rust startup code must copy\n     * them from flash to RAM before using them.\n     *\n     * The \"AT> FLASH_TEXT\" part means: \"put the content in flash,\n     * but assign addresses as if it's in DRAM.\"\n     */\n    .data : {\n        _data_start = .;\n        *(.data .data.*)\n        _data_end = .;\n    } > DRAM AT> FLASH_TEXT\n    _data_load = LOADADDR(.data);  /* Flash address where .data content lives */\n\n    /*\n     * .bss: Uninitialized global/static variables.\n     * NOLOAD means the linker doesn't store anything in the binary for\n     * this section — our startup code just zeroes the region at boot.\n     */\n    .bss (NOLOAD) : {\n        _bss_start = .;\n        *(.bss .bss.* COMMON)\n        _bss_end = .;\n    } > DRAM\n\n    /* Discard sections we don't need — saves space in the binary */\n    /DISCARD/ : {\n        *(.eh_frame)         /* Exception handling frames (unused in no_std) */\n        *(.eh_frame_hdr)\n        *(.stack)\n        *(.xtensa.info)      /* Xtensa toolchain metadata */\n        *(.comment)          /* Compiler version strings */\n    }\n}\n",[23,2076,2077,2082,2087,2091,2096,2100,2105,2110,2115,2120,2125,2130,2135,2139,2143,2148,2153,2158,2162,2167,2171,2175,2180,2184,2188,2193,2198,2203,2208,2212,2217,2222,2227,2231,2235,2240,2245,2250,2254,2259,2264,2268,2272,2277,2282,2286,2290,2295,2300,2305,2310,2315,2319,2323,2327,2332,2337,2342,2347,2352,2357,2362,2366,2371,2376,2381,2386,2391,2396,2400,2404,2409,2414,2419,2423,2428,2433,2438,2443,2448,2452,2457,2462,2467,2473,2479,2485,2491,2496],{"__ignoreMap":101},[105,2078,2079],{"class":107,"line":108},[105,2080,2081],{},"/* Declare our Rust entry function as the ELF entry point */\n",[105,2083,2084],{"class":107,"line":114},[105,2085,2086],{},"ENTRY(rust_app_core_entry)\n",[105,2088,2089],{"class":107,"line":120},[105,2090,246],{"emptyLinePlaceholder":245},[105,2092,2093],{"class":107,"line":126},[105,2094,2095],{},"MEMORY\n",[105,2097,2098],{"class":107,"line":132},[105,2099,497],{},[105,2101,2102],{"class":107,"line":138},[105,2103,2104],{},"    /*\n",[105,2106,2107],{"class":107,"line":144},[105,2108,2109],{},"     * FLASH_TEXT: Where our code will be mapped in the address space.\n",[105,2111,2112],{"class":107,"line":150},[105,2113,2114],{},"     * 0x42400000 is a virtual address — the MMU will map our flash\n",[105,2116,2117],{"class":107,"line":156},[105,2118,2119],{},"     * partition to this region at runtime (we'll set that up in C).\n",[105,2121,2122],{"class":107,"line":162},[105,2123,2124],{},"     * 512K should be plenty for most Rust programs.\n",[105,2126,2127],{"class":107,"line":168},[105,2128,2129],{},"     */\n",[105,2131,2132],{"class":107,"line":174},[105,2133,2134],{},"    FLASH_TEXT (rx)  : ORIGIN = 0x42400000, LENGTH = 512K\n",[105,2136,2137],{"class":107,"line":371},[105,2138,246],{"emptyLinePlaceholder":245},[105,2140,2141],{"class":107,"line":377},[105,2142,2104],{},[105,2144,2145],{"class":107,"line":383},[105,2146,2147],{},"     * DRAM: The 128KB block we reserved with SOC_RESERVE_MEMORY_REGION.\n",[105,2149,2150],{"class":107,"line":388},[105,2151,2152],{},"     * This is physical SRAM that both cores can access directly.\n",[105,2154,2155],{"class":107,"line":393},[105,2156,2157],{},"     * Our stack, .data, and .bss all live here.\n",[105,2159,2160],{"class":107,"line":399},[105,2161,2129],{},[105,2163,2164],{"class":107,"line":405},[105,2165,2166],{},"    DRAM       (rw)  : ORIGIN = 0x3FCC9710, LENGTH = 128K\n",[105,2168,2169],{"class":107,"line":411},[105,2170,663],{},[105,2172,2173],{"class":107,"line":417},[105,2174,246],{"emptyLinePlaceholder":245},[105,2176,2177],{"class":107,"line":423},[105,2178,2179],{},"SECTIONS\n",[105,2181,2182],{"class":107,"line":429},[105,2183,497],{},[105,2185,2186],{"class":107,"line":435},[105,2187,2104],{},[105,2189,2190],{"class":107,"line":441},[105,2191,2192],{},"     * 4-byte header at offset 0 of the binary.\n",[105,2194,2195],{"class":107,"line":447},[105,2196,2197],{},"     * This is a simple convention: the first 4 bytes of our binary\n",[105,2199,2200],{"class":107,"line":453},[105,2201,2202],{},"     * contain the address of rust_app_core_entry. The C bootloader\n",[105,2204,2205],{"class":107,"line":458},[105,2206,2207],{},"     * reads this to know where to jump.\n",[105,2209,2210],{"class":107,"line":464},[105,2211,2129],{},[105,2213,2214],{"class":107,"line":470},[105,2215,2216],{},"    .header : {\n",[105,2218,2219],{"class":107,"line":476},[105,2220,2221],{},"        LONG(rust_app_core_entry)\n",[105,2223,2224],{"class":107,"line":482},[105,2225,2226],{},"    } > FLASH_TEXT\n",[105,2228,2229],{"class":107,"line":488},[105,2230,246],{"emptyLinePlaceholder":245},[105,2232,2233],{"class":107,"line":494},[105,2234,2104],{},[105,2236,2237],{"class":107,"line":500},[105,2238,2239],{},"     * Xtensa puts function literal pools (constants used by instructions)\n",[105,2241,2242],{"class":107,"line":506},[105,2243,2244],{},"     * in .literal sections. We place the entry function's literals and\n",[105,2246,2247],{"class":107,"line":512},[105,2248,2249],{},"     * code first to ensure they're near the beginning of the binary.\n",[105,2251,2252],{"class":107,"line":517},[105,2253,2129],{},[105,2255,2256],{"class":107,"line":523},[105,2257,2258],{},"    .entry_lit : {\n",[105,2260,2261],{"class":107,"line":529},[105,2262,2263],{},"        KEEP(*(.literal.rust_app_core_entry))\n",[105,2265,2266],{"class":107,"line":535},[105,2267,2226],{},[105,2269,2270],{"class":107,"line":541},[105,2271,246],{"emptyLinePlaceholder":245},[105,2273,2274],{"class":107,"line":546},[105,2275,2276],{},"    .entry : {\n",[105,2278,2279],{"class":107,"line":552},[105,2280,2281],{},"        KEEP(*(.text.rust_app_core_entry))\n",[105,2283,2284],{"class":107,"line":558},[105,2285,2226],{},[105,2287,2288],{"class":107,"line":564},[105,2289,246],{"emptyLinePlaceholder":245},[105,2291,2292],{"class":107,"line":569},[105,2293,2294],{},"    /* All remaining code and read-only data goes into flash */\n",[105,2296,2297],{"class":107,"line":575},[105,2298,2299],{},"    .text : {\n",[105,2301,2302],{"class":107,"line":581},[105,2303,2304],{},"        *(.literal .literal.*)    /* Xtensa literal pools */\n",[105,2306,2307],{"class":107,"line":587},[105,2308,2309],{},"        *(.text .text.*)          /* Executable code */\n",[105,2311,2312],{"class":107,"line":592},[105,2313,2314],{},"        *(.rodata .rodata.*)      /* Read-only data (strings, constants) */\n",[105,2316,2317],{"class":107,"line":598},[105,2318,2226],{},[105,2320,2321],{"class":107,"line":604},[105,2322,246],{"emptyLinePlaceholder":245},[105,2324,2325],{"class":107,"line":610},[105,2326,2104],{},[105,2328,2329],{"class":107,"line":615},[105,2330,2331],{},"     * .data: Initialized global/static variables.\n",[105,2333,2334],{"class":107,"line":621},[105,2335,2336],{},"     * These live in DRAM at runtime (VMA), but their initial values\n",[105,2338,2339],{"class":107,"line":627},[105,2340,2341],{},"     * are stored in flash (LMA). Our Rust startup code must copy\n",[105,2343,2344],{"class":107,"line":632},[105,2345,2346],{},"     * them from flash to RAM before using them.\n",[105,2348,2349],{"class":107,"line":638},[105,2350,2351],{},"     *\n",[105,2353,2354],{"class":107,"line":643},[105,2355,2356],{},"     * The \"AT> FLASH_TEXT\" part means: \"put the content in flash,\n",[105,2358,2359],{"class":107,"line":649},[105,2360,2361],{},"     * but assign addresses as if it's in DRAM.\"\n",[105,2363,2364],{"class":107,"line":654},[105,2365,2129],{},[105,2367,2368],{"class":107,"line":660},[105,2369,2370],{},"    .data : {\n",[105,2372,2373],{"class":107,"line":666},[105,2374,2375],{},"        _data_start = .;\n",[105,2377,2378],{"class":107,"line":671},[105,2379,2380],{},"        *(.data .data.*)\n",[105,2382,2383],{"class":107,"line":677},[105,2384,2385],{},"        _data_end = .;\n",[105,2387,2388],{"class":107,"line":683},[105,2389,2390],{},"    } > DRAM AT> FLASH_TEXT\n",[105,2392,2393],{"class":107,"line":689},[105,2394,2395],{},"    _data_load = LOADADDR(.data);  /* Flash address where .data content lives */\n",[105,2397,2398],{"class":107,"line":694},[105,2399,246],{"emptyLinePlaceholder":245},[105,2401,2402],{"class":107,"line":700},[105,2403,2104],{},[105,2405,2406],{"class":107,"line":705},[105,2407,2408],{},"     * .bss: Uninitialized global/static variables.\n",[105,2410,2411],{"class":107,"line":711},[105,2412,2413],{},"     * NOLOAD means the linker doesn't store anything in the binary for\n",[105,2415,2416],{"class":107,"line":716},[105,2417,2418],{},"     * this section — our startup code just zeroes the region at boot.\n",[105,2420,2421],{"class":107,"line":722},[105,2422,2129],{},[105,2424,2425],{"class":107,"line":728},[105,2426,2427],{},"    .bss (NOLOAD) : {\n",[105,2429,2430],{"class":107,"line":733},[105,2431,2432],{},"        _bss_start = .;\n",[105,2434,2435],{"class":107,"line":739},[105,2436,2437],{},"        *(.bss .bss.* COMMON)\n",[105,2439,2440],{"class":107,"line":745},[105,2441,2442],{},"        _bss_end = .;\n",[105,2444,2445],{"class":107,"line":751},[105,2446,2447],{},"    } > DRAM\n",[105,2449,2450],{"class":107,"line":757},[105,2451,246],{"emptyLinePlaceholder":245},[105,2453,2454],{"class":107,"line":763},[105,2455,2456],{},"    /* Discard sections we don't need — saves space in the binary */\n",[105,2458,2459],{"class":107,"line":769},[105,2460,2461],{},"    /DISCARD/ : {\n",[105,2463,2464],{"class":107,"line":775},[105,2465,2466],{},"        *(.eh_frame)         /* Exception handling frames (unused in no_std) */\n",[105,2468,2470],{"class":107,"line":2469},84,[105,2471,2472],{},"        *(.eh_frame_hdr)\n",[105,2474,2476],{"class":107,"line":2475},85,[105,2477,2478],{},"        *(.stack)\n",[105,2480,2482],{"class":107,"line":2481},86,[105,2483,2484],{},"        *(.xtensa.info)      /* Xtensa toolchain metadata */\n",[105,2486,2488],{"class":107,"line":2487},87,[105,2489,2490],{},"        *(.comment)          /* Compiler version strings */\n",[105,2492,2494],{"class":107,"line":2493},88,[105,2495,772],{},[105,2497,2499],{"class":107,"line":2498},89,[105,2500,663],{},[2038,2502,2504],{"id":2503},"initializing-data-and-bss-from-rust","Initializing .data and .bss from Rust",[19,2506,2507,2508,2510,2511,2513,2514,2517],{},"When our Rust code was a library linked into ESP-IDF, the IDF startup code handled copying ",[23,2509,2051],{}," from flash to RAM and zeroing ",[23,2512,2047],{},". Now that we're standalone, we have to do it ourselves. This must happen ",[220,2515,2516],{},"before"," any static or global variables are accessed, or we'll read garbage.",[96,2519,2521],{"className":1141,"code":2520,"language":1143,"meta":101,"style":101},"// These symbols are defined by our linker script (link.x).\n// They don't contain data — their *addresses* ARE the data.\n// For example, &_data_start gives us the RAM address where .data begins.\nunsafe extern \"C\" {\n    static _data_start: u8;  // Start of .data in RAM\n    static _data_end: u8;    // End of .data in RAM\n    static _data_load: u8;   // Start of .data's initial values in flash\n    static _bss_start: u8;   // Start of .bss in RAM\n    static _bss_end: u8;     // End of .bss in RAM\n}\n\n/// Copy .data initial values from flash to RAM, and zero .bss.\n/// MUST be called before accessing any static/global variables.\nunsafe fn init_sections() {\n    // Calculate how many bytes the .data section occupies\n    let data_size = &raw const _data_end as usize - &raw const _data_start as usize;\n    if data_size > 0 {\n        // Copy initial values from flash (where the linker stored them)\n        // to RAM (where the program expects them at runtime).\n        core::ptr::copy_nonoverlapping(\n            &raw const _data_load,          // Source: flash\n            &raw const _data_start as *mut u8, // Destination: RAM\n            data_size,\n        );\n    }\n\n    // Calculate how many bytes the .bss section occupies\n    let bss_size = &raw const _bss_end as usize - &raw const _bss_start as usize;\n    if bss_size > 0 {\n        // Zero out .bss. C and Rust both assume uninitialized globals\n        // start as zero. Without this, they'd contain whatever was\n        // previously in RAM — likely garbage from the bootloader.\n        core::ptr::write_bytes(&raw const _bss_start as *mut u8, 0, bss_size);\n    }\n}\n",[23,2522,2523,2528,2533,2538,2543,2548,2553,2558,2563,2568,2572,2576,2581,2586,2591,2596,2601,2606,2611,2616,2621,2626,2631,2636,2641,2645,2649,2654,2659,2664,2669,2674,2679,2684,2688],{"__ignoreMap":101},[105,2524,2525],{"class":107,"line":108},[105,2526,2527],{},"// These symbols are defined by our linker script (link.x).\n",[105,2529,2530],{"class":107,"line":114},[105,2531,2532],{},"// They don't contain data — their *addresses* ARE the data.\n",[105,2534,2535],{"class":107,"line":120},[105,2536,2537],{},"// For example, &_data_start gives us the RAM address where .data begins.\n",[105,2539,2540],{"class":107,"line":126},[105,2541,2542],{},"unsafe extern \"C\" {\n",[105,2544,2545],{"class":107,"line":132},[105,2546,2547],{},"    static _data_start: u8;  // Start of .data in RAM\n",[105,2549,2550],{"class":107,"line":138},[105,2551,2552],{},"    static _data_end: u8;    // End of .data in RAM\n",[105,2554,2555],{"class":107,"line":144},[105,2556,2557],{},"    static _data_load: u8;   // Start of .data's initial values in flash\n",[105,2559,2560],{"class":107,"line":150},[105,2561,2562],{},"    static _bss_start: u8;   // Start of .bss in RAM\n",[105,2564,2565],{"class":107,"line":156},[105,2566,2567],{},"    static _bss_end: u8;     // End of .bss in RAM\n",[105,2569,2570],{"class":107,"line":162},[105,2571,663],{},[105,2573,2574],{"class":107,"line":168},[105,2575,246],{"emptyLinePlaceholder":245},[105,2577,2578],{"class":107,"line":174},[105,2579,2580],{},"/// Copy .data initial values from flash to RAM, and zero .bss.\n",[105,2582,2583],{"class":107,"line":371},[105,2584,2585],{},"/// MUST be called before accessing any static/global variables.\n",[105,2587,2588],{"class":107,"line":377},[105,2589,2590],{},"unsafe fn init_sections() {\n",[105,2592,2593],{"class":107,"line":383},[105,2594,2595],{},"    // Calculate how many bytes the .data section occupies\n",[105,2597,2598],{"class":107,"line":388},[105,2599,2600],{},"    let data_size = &raw const _data_end as usize - &raw const _data_start as usize;\n",[105,2602,2603],{"class":107,"line":393},[105,2604,2605],{},"    if data_size > 0 {\n",[105,2607,2608],{"class":107,"line":399},[105,2609,2610],{},"        // Copy initial values from flash (where the linker stored them)\n",[105,2612,2613],{"class":107,"line":405},[105,2614,2615],{},"        // to RAM (where the program expects them at runtime).\n",[105,2617,2618],{"class":107,"line":411},[105,2619,2620],{},"        core::ptr::copy_nonoverlapping(\n",[105,2622,2623],{"class":107,"line":417},[105,2624,2625],{},"            &raw const _data_load,          // Source: flash\n",[105,2627,2628],{"class":107,"line":423},[105,2629,2630],{},"            &raw const _data_start as *mut u8, // Destination: RAM\n",[105,2632,2633],{"class":107,"line":429},[105,2634,2635],{},"            data_size,\n",[105,2637,2638],{"class":107,"line":435},[105,2639,2640],{},"        );\n",[105,2642,2643],{"class":107,"line":441},[105,2644,772],{},[105,2646,2647],{"class":107,"line":447},[105,2648,246],{"emptyLinePlaceholder":245},[105,2650,2651],{"class":107,"line":453},[105,2652,2653],{},"    // Calculate how many bytes the .bss section occupies\n",[105,2655,2656],{"class":107,"line":458},[105,2657,2658],{},"    let bss_size = &raw const _bss_end as usize - &raw const _bss_start as usize;\n",[105,2660,2661],{"class":107,"line":464},[105,2662,2663],{},"    if bss_size > 0 {\n",[105,2665,2666],{"class":107,"line":470},[105,2667,2668],{},"        // Zero out .bss. C and Rust both assume uninitialized globals\n",[105,2670,2671],{"class":107,"line":476},[105,2672,2673],{},"        // start as zero. Without this, they'd contain whatever was\n",[105,2675,2676],{"class":107,"line":482},[105,2677,2678],{},"        // previously in RAM — likely garbage from the bootloader.\n",[105,2680,2681],{"class":107,"line":488},[105,2682,2683],{},"        core::ptr::write_bytes(&raw const _bss_start as *mut u8, 0, bss_size);\n",[105,2685,2686],{"class":107,"line":494},[105,2687,772],{},[105,2689,2690],{"class":107,"line":500},[105,2691,663],{},[2038,2693,2695],{"id":2694},"the-updated-rust-entry-point","The Updated Rust Entry Point",[19,2697,2698],{},"Since our Rust binary is no longer linked into the ESP-IDF project, we can't share global variables by name across the C/Rust boundary (there's no shared linker pass). Instead, both sides agree on a fixed memory address for the shared counter. The C side reads from that address; the Rust side writes to it.",[19,2700,2701,2702,2705],{},"For this demo, I'm using the start of our reserved memory region (",[23,2703,2704],{},"0x3FCC9710",") as the counter address. In a real system, you'd want a more structured approach — perhaps a shared header at a fixed address that defines the layout of all shared data.",[96,2707,2709],{"className":1141,"code":2708,"language":1143,"meta":101,"style":101},"// Fixed memory address for the shared counter.\n// Both the C side and Rust side must agree on this address.\n// We're using the very start of our reserved DRAM region.\nconst COUNTER_ADDR: usize = 0x3FCC9710;\n\n// #[unsafe(link_section = \".text.rust_app_core_entry\")] places this\n// function in a specific linker section making it easy to find.\n#[unsafe(no_mangle)]\n#[unsafe(link_section = \".text.rust_app_core_entry\")]\npub extern \"C\" fn rust_app_core_entry() -> ! {\n    // FIRST THING: initialize .data and .bss before touching any statics.\n    // If we skip this, any global variable could contain garbage.\n    unsafe {\n        init_sections();\n    }\n\n    // Create an atomic reference to our shared counter.\n    // We cast the raw memory address to an AtomicU32 pointer.\n    // This is unsafe because we're asserting that this address is:\n    //   1. Valid and aligned\n    //   2. Not being used for anything else\n    //   3. Accessible by both cores\n    let counter = unsafe { &*(COUNTER_ADDR as *const AtomicU32) };\n\n    // Initialize the counter to zero (in case there was leftover data)\n    counter.store(0, Ordering::Relaxed);\n\n    loop {\n        // Increment the shared counter atomically\n        counter.fetch_add(1, Ordering::Relaxed);\n\n        // Busy-wait delay (same as before)\n        for _ in 0..1_000_000 {\n            core::hint::spin_loop();\n        }\n    }\n}\n",[23,2710,2711,2716,2721,2726,2731,2735,2740,2745,2749,2754,2758,2763,2768,2773,2778,2782,2786,2791,2796,2801,2806,2811,2816,2821,2825,2830,2835,2839,2843,2848,2853,2857,2862,2866,2870,2874,2878],{"__ignoreMap":101},[105,2712,2713],{"class":107,"line":108},[105,2714,2715],{},"// Fixed memory address for the shared counter.\n",[105,2717,2718],{"class":107,"line":114},[105,2719,2720],{},"// Both the C side and Rust side must agree on this address.\n",[105,2722,2723],{"class":107,"line":120},[105,2724,2725],{},"// We're using the very start of our reserved DRAM region.\n",[105,2727,2728],{"class":107,"line":126},[105,2729,2730],{},"const COUNTER_ADDR: usize = 0x3FCC9710;\n",[105,2732,2733],{"class":107,"line":132},[105,2734,246],{"emptyLinePlaceholder":245},[105,2736,2737],{"class":107,"line":138},[105,2738,2739],{},"// #[unsafe(link_section = \".text.rust_app_core_entry\")] places this\n",[105,2741,2742],{"class":107,"line":144},[105,2743,2744],{},"// function in a specific linker section making it easy to find.\n",[105,2746,2747],{"class":107,"line":150},[105,2748,1299],{},[105,2750,2751],{"class":107,"line":156},[105,2752,2753],{},"#[unsafe(link_section = \".text.rust_app_core_entry\")]\n",[105,2755,2756],{"class":107,"line":162},[105,2757,1346],{},[105,2759,2760],{"class":107,"line":168},[105,2761,2762],{},"    // FIRST THING: initialize .data and .bss before touching any statics.\n",[105,2764,2765],{"class":107,"line":174},[105,2766,2767],{},"    // If we skip this, any global variable could contain garbage.\n",[105,2769,2770],{"class":107,"line":371},[105,2771,2772],{},"    unsafe {\n",[105,2774,2775],{"class":107,"line":377},[105,2776,2777],{},"        init_sections();\n",[105,2779,2780],{"class":107,"line":383},[105,2781,772],{},[105,2783,2784],{"class":107,"line":388},[105,2785,246],{"emptyLinePlaceholder":245},[105,2787,2788],{"class":107,"line":393},[105,2789,2790],{},"    // Create an atomic reference to our shared counter.\n",[105,2792,2793],{"class":107,"line":399},[105,2794,2795],{},"    // We cast the raw memory address to an AtomicU32 pointer.\n",[105,2797,2798],{"class":107,"line":405},[105,2799,2800],{},"    // This is unsafe because we're asserting that this address is:\n",[105,2802,2803],{"class":107,"line":411},[105,2804,2805],{},"    //   1. Valid and aligned\n",[105,2807,2808],{"class":107,"line":417},[105,2809,2810],{},"    //   2. Not being used for anything else\n",[105,2812,2813],{"class":107,"line":423},[105,2814,2815],{},"    //   3. Accessible by both cores\n",[105,2817,2818],{"class":107,"line":429},[105,2819,2820],{},"    let counter = unsafe { &*(COUNTER_ADDR as *const AtomicU32) };\n",[105,2822,2823],{"class":107,"line":435},[105,2824,246],{"emptyLinePlaceholder":245},[105,2826,2827],{"class":107,"line":441},[105,2828,2829],{},"    // Initialize the counter to zero (in case there was leftover data)\n",[105,2831,2832],{"class":107,"line":447},[105,2833,2834],{},"    counter.store(0, Ordering::Relaxed);\n",[105,2836,2837],{"class":107,"line":453},[105,2838,246],{"emptyLinePlaceholder":245},[105,2840,2841],{"class":107,"line":458},[105,2842,1351],{},[105,2844,2845],{"class":107,"line":464},[105,2846,2847],{},"        // Increment the shared counter atomically\n",[105,2849,2850],{"class":107,"line":470},[105,2851,2852],{},"        counter.fetch_add(1, Ordering::Relaxed);\n",[105,2854,2855],{"class":107,"line":476},[105,2856,246],{"emptyLinePlaceholder":245},[105,2858,2859],{"class":107,"line":482},[105,2860,2861],{},"        // Busy-wait delay (same as before)\n",[105,2863,2864],{"class":107,"line":488},[105,2865,1405],{},[105,2867,2868],{"class":107,"line":494},[105,2869,1410],{},[105,2871,2872],{"class":107,"line":500},[105,2873,1415],{},[105,2875,2876],{"class":107,"line":506},[105,2877,772],{},[105,2879,2880],{"class":107,"line":512},[105,2881,663],{},[14,2883,2885],{"id":2884},"step-2-update-the-esp-idf-project-to-load-the-binary-at-runtime","Step 2: Update the ESP-IDF Project to Load the Binary at Runtime",[19,2887,2888],{},"Now that our Rust code is a standalone binary instead of a linked library, the ESP-IDF side needs several changes.",[2038,2890,2892],{"id":2891},"create-a-flash-partition","Create a Flash Partition",[19,2894,2895,2896,2899],{},"The Rust binary needs its own partition in flash. We add a ",[23,2897,2898],{},"rust_app"," entry after the factory partition (where the main ESP-IDF firmware lives):",[19,2901,2902],{},[40,2903,2904],{},"partitions.csv",[96,2906,2910],{"className":2907,"code":2908,"language":2909,"meta":101,"style":101},"language-csv shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","nvs,         data, nvs,     0x9000,     0x6000,\nphy_init,    data, phy,     0xf000,     0x1000,\nfactory,     app,  factory, 0x10000,    0x1F0000,\nrust_app,    data, 0x40,    0x200000,   0x80000,\n","csv",[23,2911,2912,2917,2922,2927],{"__ignoreMap":101},[105,2913,2914],{"class":107,"line":108},[105,2915,2916],{},"nvs,         data, nvs,     0x9000,     0x6000,\n",[105,2918,2919],{"class":107,"line":114},[105,2920,2921],{},"phy_init,    data, phy,     0xf000,     0x1000,\n",[105,2923,2924],{"class":107,"line":120},[105,2925,2926],{},"factory,     app,  factory, 0x10000,    0x1F0000,\n",[105,2928,2929],{"class":107,"line":126},[105,2930,2931],{},"rust_app,    data, 0x40,    0x200000,   0x80000,\n",[19,2933,1134,2934,2936,2937,2940,2941,2944,2945,2948],{},[23,2935,2898],{}," partition starts at offset ",[23,2938,2939],{},"0x200000"," (2MB into flash) and is ",[23,2942,2943],{},"0x80000"," (512KB) in size. The subtype ",[23,2946,2947],{},"0x40"," is an arbitrary custom value — it just needs to be something ESP-IDF doesn't already use, so we can find the partition by name and type later.",[2038,2950,2952],{"id":2951},"map-the-partition-into-memory-via-the-mmu","Map the Partition into Memory via the MMU",[19,2954,2955],{},"On the ESP32-S3, code in flash isn't directly executable — it needs to be mapped into the CPU's address space via the Memory Management Unit (MMU). This is normally handled automatically by ESP-IDF for the main firmware, but for our separate Rust binary, we need to do it manually.",[19,2957,2958,2959,2961,2962,2965],{},"The function below finds our ",[23,2960,2898],{}," partition in flash and maps it page-by-page to virtual address ",[23,2963,2964],{},"0x42400000"," (the same address our linker script targets). After mapping, the CPU can execute code from this region as if it were regular memory.",[96,2967,2969],{"className":98,"code":2968,"language":100,"meta":101,"style":101},"#include \u003Cstring.h>\n#include \"esp_partition.h\"\n#include \"hal/mmu_hal.h\"\n#include \"hal/cache_hal.h\"\n\n// Virtual address where the Rust binary will be mapped.\n// This MUST match the FLASH_TEXT origin in link.x.\n#define RUST_VADDR 0x42400000\n\n// Will hold the entry point address read from the binary's header\nuint32_t rust_entry_addr = 0;\n\nstatic void load_rust_app(void)\n{\n    // Find the \"rust_app\" partition we defined in partitions.csv.\n    // We search by type (DATA) and subtype (0x40, our custom value).\n    const esp_partition_t *part =\n        esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 0x40, \"rust_app\");\n\n    if (!part)\n    {\n        ESP_LOGE(TAG, \"rust_app partition not found!\");\n        return;\n    }\n\n    // Map the partition into the CPU's address space page by page.\n    // The MMU works in pages (typically 64KB on ESP32-S3), so we\n    // calculate how many pages we need and map each one.\n    uint32_t page_size = CONFIG_MMU_PAGE_SIZE;\n    uint32_t pages = (part->size + page_size - 1) / page_size; // Round up\n    uint32_t actual_mapped_size = 0;\n\n    for (uint32_t i = 0; i \u003C pages; i++)\n    {\n        uint32_t mapped = 0;\n        // Map one page: virtual address → physical flash address\n        mmu_hal_map_region(0, MMU_TARGET_FLASH0,\n                           RUST_VADDR + (i * page_size),    // Virtual addr\n                           part->address + (i * page_size), // Flash addr\n                           page_size, &mapped);\n        actual_mapped_size += mapped;\n    }\n\n    // Invalidate the cache for this region so the CPU doesn't serve\n    // stale data from a previous mapping.\n    cache_hal_invalidate_addr(RUST_VADDR, part->size);\n\n    ESP_LOGI(TAG, \"Rust app mapped at 0x%lx (%lu bytes, flash 0x%lx)\",\n             (unsigned long)RUST_VADDR, (unsigned long)actual_mapped_size,\n             (unsigned long)part->address);\n}\n",[23,2970,2971,2976,2981,2986,2991,2995,3000,3005,3010,3014,3019,3024,3028,3033,3037,3042,3047,3052,3057,3061,3066,3070,3075,3080,3084,3088,3093,3098,3103,3108,3113,3118,3122,3127,3131,3136,3141,3146,3151,3156,3161,3166,3170,3174,3179,3184,3189,3193,3198,3203,3208],{"__ignoreMap":101},[105,2972,2973],{"class":107,"line":108},[105,2974,2975],{},"#include \u003Cstring.h>\n",[105,2977,2978],{"class":107,"line":114},[105,2979,2980],{},"#include \"esp_partition.h\"\n",[105,2982,2983],{"class":107,"line":120},[105,2984,2985],{},"#include \"hal/mmu_hal.h\"\n",[105,2987,2988],{"class":107,"line":126},[105,2989,2990],{},"#include \"hal/cache_hal.h\"\n",[105,2992,2993],{"class":107,"line":132},[105,2994,246],{"emptyLinePlaceholder":245},[105,2996,2997],{"class":107,"line":138},[105,2998,2999],{},"// Virtual address where the Rust binary will be mapped.\n",[105,3001,3002],{"class":107,"line":144},[105,3003,3004],{},"// This MUST match the FLASH_TEXT origin in link.x.\n",[105,3006,3007],{"class":107,"line":150},[105,3008,3009],{},"#define RUST_VADDR 0x42400000\n",[105,3011,3012],{"class":107,"line":156},[105,3013,246],{"emptyLinePlaceholder":245},[105,3015,3016],{"class":107,"line":162},[105,3017,3018],{},"// Will hold the entry point address read from the binary's header\n",[105,3020,3021],{"class":107,"line":168},[105,3022,3023],{},"uint32_t rust_entry_addr = 0;\n",[105,3025,3026],{"class":107,"line":174},[105,3027,246],{"emptyLinePlaceholder":245},[105,3029,3030],{"class":107,"line":371},[105,3031,3032],{},"static void load_rust_app(void)\n",[105,3034,3035],{"class":107,"line":377},[105,3036,497],{},[105,3038,3039],{"class":107,"line":383},[105,3040,3041],{},"    // Find the \"rust_app\" partition we defined in partitions.csv.\n",[105,3043,3044],{"class":107,"line":388},[105,3045,3046],{},"    // We search by type (DATA) and subtype (0x40, our custom value).\n",[105,3048,3049],{"class":107,"line":393},[105,3050,3051],{},"    const esp_partition_t *part =\n",[105,3053,3054],{"class":107,"line":399},[105,3055,3056],{},"        esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 0x40, \"rust_app\");\n",[105,3058,3059],{"class":107,"line":405},[105,3060,246],{"emptyLinePlaceholder":245},[105,3062,3063],{"class":107,"line":411},[105,3064,3065],{},"    if (!part)\n",[105,3067,3068],{"class":107,"line":417},[105,3069,754],{},[105,3071,3072],{"class":107,"line":423},[105,3073,3074],{},"        ESP_LOGE(TAG, \"rust_app partition not found!\");\n",[105,3076,3077],{"class":107,"line":429},[105,3078,3079],{},"        return;\n",[105,3081,3082],{"class":107,"line":435},[105,3083,772],{},[105,3085,3086],{"class":107,"line":441},[105,3087,246],{"emptyLinePlaceholder":245},[105,3089,3090],{"class":107,"line":447},[105,3091,3092],{},"    // Map the partition into the CPU's address space page by page.\n",[105,3094,3095],{"class":107,"line":453},[105,3096,3097],{},"    // The MMU works in pages (typically 64KB on ESP32-S3), so we\n",[105,3099,3100],{"class":107,"line":458},[105,3101,3102],{},"    // calculate how many pages we need and map each one.\n",[105,3104,3105],{"class":107,"line":464},[105,3106,3107],{},"    uint32_t page_size = CONFIG_MMU_PAGE_SIZE;\n",[105,3109,3110],{"class":107,"line":470},[105,3111,3112],{},"    uint32_t pages = (part->size + page_size - 1) / page_size; // Round up\n",[105,3114,3115],{"class":107,"line":476},[105,3116,3117],{},"    uint32_t actual_mapped_size = 0;\n",[105,3119,3120],{"class":107,"line":482},[105,3121,246],{"emptyLinePlaceholder":245},[105,3123,3124],{"class":107,"line":488},[105,3125,3126],{},"    for (uint32_t i = 0; i \u003C pages; i++)\n",[105,3128,3129],{"class":107,"line":494},[105,3130,754],{},[105,3132,3133],{"class":107,"line":500},[105,3134,3135],{},"        uint32_t mapped = 0;\n",[105,3137,3138],{"class":107,"line":506},[105,3139,3140],{},"        // Map one page: virtual address → physical flash address\n",[105,3142,3143],{"class":107,"line":512},[105,3144,3145],{},"        mmu_hal_map_region(0, MMU_TARGET_FLASH0,\n",[105,3147,3148],{"class":107,"line":517},[105,3149,3150],{},"                           RUST_VADDR + (i * page_size),    // Virtual addr\n",[105,3152,3153],{"class":107,"line":523},[105,3154,3155],{},"                           part->address + (i * page_size), // Flash addr\n",[105,3157,3158],{"class":107,"line":529},[105,3159,3160],{},"                           page_size, &mapped);\n",[105,3162,3163],{"class":107,"line":535},[105,3164,3165],{},"        actual_mapped_size += mapped;\n",[105,3167,3168],{"class":107,"line":541},[105,3169,772],{},[105,3171,3172],{"class":107,"line":546},[105,3173,246],{"emptyLinePlaceholder":245},[105,3175,3176],{"class":107,"line":552},[105,3177,3178],{},"    // Invalidate the cache for this region so the CPU doesn't serve\n",[105,3180,3181],{"class":107,"line":558},[105,3182,3183],{},"    // stale data from a previous mapping.\n",[105,3185,3186],{"class":107,"line":564},[105,3187,3188],{},"    cache_hal_invalidate_addr(RUST_VADDR, part->size);\n",[105,3190,3191],{"class":107,"line":569},[105,3192,246],{"emptyLinePlaceholder":245},[105,3194,3195],{"class":107,"line":575},[105,3196,3197],{},"    ESP_LOGI(TAG, \"Rust app mapped at 0x%lx (%lu bytes, flash 0x%lx)\",\n",[105,3199,3200],{"class":107,"line":581},[105,3201,3202],{},"             (unsigned long)RUST_VADDR, (unsigned long)actual_mapped_size,\n",[105,3204,3205],{"class":107,"line":587},[105,3206,3207],{},"             (unsigned long)part->address);\n",[105,3209,3210],{"class":107,"line":592},[105,3211,663],{},[2038,3213,3215],{"id":3214},"update-the-boot-function","Update the Boot Function",[19,3217,1134,3218,3221,3222,3225],{},[23,3219,3220],{},"start_rust_on_app_core"," function now loads the Rust binary from flash before waking Core 1. It reads the entry point address from the first 4 bytes of the binary (that's the ",[23,3223,3224],{},".header"," section from our linker script) and stores it in a global variable that the assembly trampoline will read.",[96,3227,3229],{"className":98,"code":3228,"language":100,"meta":101,"style":101},"static void start_rust_on_app_core(void)\n{\n    // Step 1: Map the Rust binary from flash into the address space\n    load_rust_app();\n\n    // Step 2: Read the entry point from the binary's 4-byte header.\n    // Our linker script placed LONG(rust_app_core_entry) at offset 0,\n    // so the first 4 bytes at RUST_VADDR contain the function's address.\n    uint32_t entry = *(volatile uint32_t *)RUST_VADDR;\n    rust_entry_addr = entry;  // Store globally for the trampoline to read\n\n    ESP_LOGI(TAG, \"Rust entry at 0x%lx\", (unsigned long)entry);\n\n    // Step 3: Same hardware boot sequence as before\n    ets_set_appcpu_boot_addr((uint32_t)app_core_trampoline);\n\n    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                      SYSTEM_CONTROL_CORE_1_CLKGATE_EN);\n    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                        SYSTEM_CONTROL_CORE_1_RUNSTALL);\n    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                      SYSTEM_CONTROL_CORE_1_RESETING);\n    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n                        SYSTEM_CONTROL_CORE_1_RESETING);\n\n    ESP_LOGI(TAG, \"Core 1 released\");\n}\n",[23,3230,3231,3235,3239,3244,3249,3253,3258,3263,3268,3273,3278,3282,3287,3291,3296,3300,3304,3308,3312,3316,3320,3324,3328,3332,3336,3340,3344],{"__ignoreMap":101},[105,3232,3233],{"class":107,"line":108},[105,3234,491],{},[105,3236,3237],{"class":107,"line":114},[105,3238,497],{},[105,3240,3241],{"class":107,"line":120},[105,3242,3243],{},"    // Step 1: Map the Rust binary from flash into the address space\n",[105,3245,3246],{"class":107,"line":126},[105,3247,3248],{},"    load_rust_app();\n",[105,3250,3251],{"class":107,"line":132},[105,3252,246],{"emptyLinePlaceholder":245},[105,3254,3255],{"class":107,"line":138},[105,3256,3257],{},"    // Step 2: Read the entry point from the binary's 4-byte header.\n",[105,3259,3260],{"class":107,"line":144},[105,3261,3262],{},"    // Our linker script placed LONG(rust_app_core_entry) at offset 0,\n",[105,3264,3265],{"class":107,"line":150},[105,3266,3267],{},"    // so the first 4 bytes at RUST_VADDR contain the function's address.\n",[105,3269,3270],{"class":107,"line":156},[105,3271,3272],{},"    uint32_t entry = *(volatile uint32_t *)RUST_VADDR;\n",[105,3274,3275],{"class":107,"line":162},[105,3276,3277],{},"    rust_entry_addr = entry;  // Store globally for the trampoline to read\n",[105,3279,3280],{"class":107,"line":168},[105,3281,246],{"emptyLinePlaceholder":245},[105,3283,3284],{"class":107,"line":174},[105,3285,3286],{},"    ESP_LOGI(TAG, \"Rust entry at 0x%lx\", (unsigned long)entry);\n",[105,3288,3289],{"class":107,"line":371},[105,3290,246],{"emptyLinePlaceholder":245},[105,3292,3293],{"class":107,"line":377},[105,3294,3295],{},"    // Step 3: Same hardware boot sequence as before\n",[105,3297,3298],{"class":107,"line":383},[105,3299,538],{},[105,3301,3302],{"class":107,"line":388},[105,3303,246],{"emptyLinePlaceholder":245},[105,3305,3306],{"class":107,"line":393},[105,3307,578],{},[105,3309,3310],{"class":107,"line":399},[105,3311,584],{},[105,3313,3314],{"class":107,"line":405},[105,3315,601],{},[105,3317,3318],{"class":107,"line":411},[105,3319,607],{},[105,3321,3322],{"class":107,"line":417},[105,3323,578],{},[105,3325,3326],{"class":107,"line":423},[105,3327,635],{},[105,3329,3330],{"class":107,"line":429},[105,3331,601],{},[105,3333,3334],{"class":107,"line":435},[105,3335,646],{},[105,3337,3338],{"class":107,"line":441},[105,3339,246],{"emptyLinePlaceholder":245},[105,3341,3342],{"class":107,"line":447},[105,3343,657],{},[105,3345,3346],{"class":107,"line":453},[105,3347,663],{},[2038,3349,3351],{"id":3350},"update-the-main-function","Update the Main Function",[19,3353,3354,3355,3357],{},"Since we can no longer reference ",[23,3356,305],{}," by name (the Rust binary isn't linked into our C project anymore), we read the counter from its known memory address directly:",[96,3359,3361],{"className":98,"code":3360,"language":100,"meta":101,"style":101},"// The Rust code writes its counter to this fixed address.\n// Both sides must agree on this — it's defined as COUNTER_ADDR in the Rust code.\n#define RUST_COUNTER_ADDR 0x3FCC9710\n\nvoid app_main(void)\n{\n    ESP_LOGI(TAG, \"Core 0: Starting IDF app\");\n\n    start_rust_on_app_core();\n\n    // Create a volatile pointer to the shared counter.\n    // \"volatile\" tells the C compiler: \"this value can change at any time\n    // (because another CPU core is writing to it), so always read from\n    // memory — don't cache it in a register.\"\n    volatile uint32_t *counter = (volatile uint32_t *)RUST_COUNTER_ADDR;\n\n    while (1)\n    {\n        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)*counter);\n        vTaskDelay(pdMS_TO_TICKS(1000));\n    }\n}\n",[23,3362,3363,3368,3373,3378,3382,3386,3390,3394,3398,3402,3406,3411,3416,3421,3426,3431,3435,3439,3443,3448,3453,3457],{"__ignoreMap":101},[105,3364,3365],{"class":107,"line":108},[105,3366,3367],{},"// The Rust code writes its counter to this fixed address.\n",[105,3369,3370],{"class":107,"line":114},[105,3371,3372],{},"// Both sides must agree on this — it's defined as COUNTER_ADDR in the Rust code.\n",[105,3374,3375],{"class":107,"line":120},[105,3376,3377],{},"#define RUST_COUNTER_ADDR 0x3FCC9710\n",[105,3379,3380],{"class":107,"line":126},[105,3381,246],{"emptyLinePlaceholder":245},[105,3383,3384],{"class":107,"line":132},[105,3385,697],{},[105,3387,3388],{"class":107,"line":138},[105,3389,497],{},[105,3391,3392],{"class":107,"line":144},[105,3393,708],{},[105,3395,3396],{"class":107,"line":150},[105,3397,246],{"emptyLinePlaceholder":245},[105,3399,3400],{"class":107,"line":156},[105,3401,725],{},[105,3403,3404],{"class":107,"line":162},[105,3405,246],{"emptyLinePlaceholder":245},[105,3407,3408],{"class":107,"line":168},[105,3409,3410],{},"    // Create a volatile pointer to the shared counter.\n",[105,3412,3413],{"class":107,"line":174},[105,3414,3415],{},"    // \"volatile\" tells the C compiler: \"this value can change at any time\n",[105,3417,3418],{"class":107,"line":371},[105,3419,3420],{},"    // (because another CPU core is writing to it), so always read from\n",[105,3422,3423],{"class":107,"line":377},[105,3424,3425],{},"    // memory — don't cache it in a register.\"\n",[105,3427,3428],{"class":107,"line":383},[105,3429,3430],{},"    volatile uint32_t *counter = (volatile uint32_t *)RUST_COUNTER_ADDR;\n",[105,3432,3433],{"class":107,"line":388},[105,3434,246],{"emptyLinePlaceholder":245},[105,3436,3437],{"class":107,"line":393},[105,3438,748],{},[105,3440,3441],{"class":107,"line":399},[105,3442,754],{},[105,3444,3445],{"class":107,"line":405},[105,3446,3447],{},"        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)*counter);\n",[105,3449,3450],{"class":107,"line":411},[105,3451,3452],{},"        vTaskDelay(pdMS_TO_TICKS(1000));\n",[105,3454,3455],{"class":107,"line":417},[105,3456,772],{},[105,3458,3459],{"class":107,"line":423},[105,3460,663],{},[2038,3462,3464],{"id":3463},"update-the-assembly-trampoline","Update the Assembly Trampoline",[19,3466,3467,3468,3471,3472,3475,3476,3478],{},"The trampoline can no longer use ",[23,3469,3470],{},"call0 rust_app_core_entry"," because that symbol doesn't exist in the C project's link stage. Instead, it reads the entry address from the ",[23,3473,3474],{},"rust_entry_addr"," global variable (which ",[23,3477,3220],{}," populated) and does an indirect jump:",[96,3480,3482],{"className":809,"code":3481,"language":811,"meta":101,"style":101},"/*\n * app_core_trampoline.S (updated for runtime loading)\n *\n * Same job as before: set the stack pointer, then jump to Rust.\n * But now the Rust entry address isn't known at link time — it's\n * stored in the rust_entry_addr global variable by the C code.\n */\n\n    .section .iram1, \"ax\"\n    .global  app_core_trampoline\n    .type    app_core_trampoline, @function\n    .align   4\n\napp_core_trampoline:\n    /* Set up the stack pointer (same as before) */\n    movi  a1, _rust_stack_top\n\n    /* Load the entry address from the global variable.\n     * movi loads the ADDRESS of rust_entry_addr into a2,\n     * then l32i loads the VALUE at that address into a0. */\n    movi  a2, rust_entry_addr\n    l32i  a0, a2, 0           /* a0 = *(rust_entry_addr) */\n\n    /* Indirect jump to the Rust entry point */\n    jx    a0\n\n    .size app_core_trampoline, . - app_core_trampoline\n",[23,3483,3484,3488,3493,3497,3502,3507,3512,3516,3520,3525,3529,3533,3538,3542,3546,3551,3555,3559,3564,3569,3574,3579,3584,3588,3593,3598,3602],{"__ignoreMap":101},[105,3485,3486],{"class":107,"line":108},[105,3487,461],{},[105,3489,3490],{"class":107,"line":114},[105,3491,3492],{}," * app_core_trampoline.S (updated for runtime loading)\n",[105,3494,3495],{"class":107,"line":120},[105,3496,827],{},[105,3498,3499],{"class":107,"line":126},[105,3500,3501],{}," * Same job as before: set the stack pointer, then jump to Rust.\n",[105,3503,3504],{"class":107,"line":132},[105,3505,3506],{}," * But now the Rust entry address isn't known at link time — it's\n",[105,3508,3509],{"class":107,"line":138},[105,3510,3511],{}," * stored in the rust_entry_addr global variable by the C code.\n",[105,3513,3514],{"class":107,"line":144},[105,3515,485],{},[105,3517,3518],{"class":107,"line":150},[105,3519,246],{"emptyLinePlaceholder":245},[105,3521,3522],{"class":107,"line":156},[105,3523,3524],{},"    .section .iram1, \"ax\"\n",[105,3526,3527],{"class":107,"line":162},[105,3528,869],{},[105,3530,3531],{"class":107,"line":168},[105,3532,874],{},[105,3534,3535],{"class":107,"line":174},[105,3536,3537],{},"    .align   4\n",[105,3539,3540],{"class":107,"line":371},[105,3541,246],{"emptyLinePlaceholder":245},[105,3543,3544],{"class":107,"line":377},[105,3545,888],{},[105,3547,3548],{"class":107,"line":383},[105,3549,3550],{},"    /* Set up the stack pointer (same as before) */\n",[105,3552,3553],{"class":107,"line":388},[105,3554,908],{},[105,3556,3557],{"class":107,"line":393},[105,3558,246],{"emptyLinePlaceholder":245},[105,3560,3561],{"class":107,"line":399},[105,3562,3563],{},"    /* Load the entry address from the global variable.\n",[105,3565,3566],{"class":107,"line":405},[105,3567,3568],{},"     * movi loads the ADDRESS of rust_entry_addr into a2,\n",[105,3570,3571],{"class":107,"line":411},[105,3572,3573],{},"     * then l32i loads the VALUE at that address into a0. */\n",[105,3575,3576],{"class":107,"line":417},[105,3577,3578],{},"    movi  a2, rust_entry_addr\n",[105,3580,3581],{"class":107,"line":423},[105,3582,3583],{},"    l32i  a0, a2, 0           /* a0 = *(rust_entry_addr) */\n",[105,3585,3586],{"class":107,"line":429},[105,3587,246],{"emptyLinePlaceholder":245},[105,3589,3590],{"class":107,"line":435},[105,3591,3592],{},"    /* Indirect jump to the Rust entry point */\n",[105,3594,3595],{"class":107,"line":441},[105,3596,3597],{},"    jx    a0\n",[105,3599,3600],{"class":107,"line":447},[105,3601,246],{"emptyLinePlaceholder":245},[105,3603,3604],{"class":107,"line":453},[105,3605,941],{},[14,3607,3609],{"id":3608},"step-3-build-and-flash","Step 3: Build and Flash",[19,3611,3612],{},"Now we have two separate build steps — one for the Rust binary, one for the ESP-IDF firmware — and two separate flash steps.",[19,3614,3615],{},[40,3616,3617],{},"Build and flash the ESP-IDF side:",[96,3619,3621],{"className":1680,"code":3620,"language":1682,"meta":101,"style":101},"# Build the ESP-IDF project (which no longer includes any Rust code)\nidf.py build\n\n# Flash the main firmware and partition table\nidf.py flash\n",[23,3622,3623,3628,3636,3640,3645],{"__ignoreMap":101},[105,3624,3625],{"class":107,"line":108},[105,3626,3627],{"class":1689},"# Build the ESP-IDF project (which no longer includes any Rust code)\n",[105,3629,3630,3633],{"class":107,"line":114},[105,3631,3632],{"class":1700},"idf.py",[105,3634,3635],{"class":1704}," build\n",[105,3637,3638],{"class":107,"line":120},[105,3639,246],{"emptyLinePlaceholder":245},[105,3641,3642],{"class":107,"line":126},[105,3643,3644],{"class":1689},"# Flash the main firmware and partition table\n",[105,3646,3647,3649],{"class":107,"line":132},[105,3648,3632],{"class":1700},[105,3650,3651],{"class":1704}," flash\n",[19,3653,3654],{},[40,3655,3656],{},"Build and flash the Rust binary:",[96,3658,3660],{"className":1680,"code":3659,"language":1682,"meta":101,"style":101},"# Build the standalone Rust binary\ncargo build --release --target xtensa-esp32s3-none-elf\n\n# Convert from ELF format to raw binary.\n# The ELF file contains metadata (section headers, debug info, etc.)\n# that we don't need — objcopy strips all of that and outputs just\n# the raw machine code that the CPU will execute.\nxtensa-esp32s3-elf-objcopy -O binary \\\n    'target/xtensa-esp32s3-none-elf/release/esp_rust_app' \\\n    rust_app.bin\n\n# Flash the raw binary to the rust_app partition.\n# 0x200000 is the offset we defined in partitions.csv.\nesptool.py --port /dev/ttyACM0 write_flash 0x200000 rust_app.bin\n",[23,3661,3662,3667,3679,3683,3688,3693,3698,3703,3716,3730,3735,3739,3744,3749],{"__ignoreMap":101},[105,3663,3664],{"class":107,"line":108},[105,3665,3666],{"class":1689},"# Build the standalone Rust binary\n",[105,3668,3669,3671,3673,3675,3677],{"class":107,"line":114},[105,3670,1701],{"class":1700},[105,3672,1705],{"class":1704},[105,3674,1708],{"class":1704},[105,3676,1711],{"class":1704},[105,3678,1714],{"class":1704},[105,3680,3681],{"class":107,"line":120},[105,3682,246],{"emptyLinePlaceholder":245},[105,3684,3685],{"class":107,"line":126},[105,3686,3687],{"class":1689},"# Convert from ELF format to raw binary.\n",[105,3689,3690],{"class":107,"line":132},[105,3691,3692],{"class":1689},"# The ELF file contains metadata (section headers, debug info, etc.)\n",[105,3694,3695],{"class":107,"line":138},[105,3696,3697],{"class":1689},"# that we don't need — objcopy strips all of that and outputs just\n",[105,3699,3700],{"class":107,"line":144},[105,3701,3702],{"class":1689},"# the raw machine code that the CPU will execute.\n",[105,3704,3705,3708,3711,3714],{"class":107,"line":150},[105,3706,3707],{"class":1700},"xtensa-esp32s3-elf-objcopy",[105,3709,3710],{"class":1704}," -O",[105,3712,3713],{"class":1704}," binary",[105,3715,1735],{"class":1734},[105,3717,3718,3722,3725,3728],{"class":107,"line":156},[105,3719,3721],{"class":3720},"sMK4o","    '",[105,3723,3724],{"class":1704},"target/xtensa-esp32s3-none-elf/release/esp_rust_app",[105,3726,3727],{"class":3720},"'",[105,3729,1735],{"class":1734},[105,3731,3732],{"class":107,"line":162},[105,3733,3734],{"class":1704},"    rust_app.bin\n",[105,3736,3737],{"class":107,"line":168},[105,3738,246],{"emptyLinePlaceholder":245},[105,3740,3741],{"class":107,"line":174},[105,3742,3743],{"class":1689},"# Flash the raw binary to the rust_app partition.\n",[105,3745,3746],{"class":107,"line":371},[105,3747,3748],{"class":1689},"# 0x200000 is the offset we defined in partitions.csv.\n",[105,3750,3751,3754,3757,3760,3763,3767],{"class":107,"line":377},[105,3752,3753],{"class":1700},"esptool.py",[105,3755,3756],{"class":1704}," --port",[105,3758,3759],{"class":1704}," /dev/ttyACM0",[105,3761,3762],{"class":1704}," write_flash",[105,3764,3766],{"class":3765},"sbssI"," 0x200000",[105,3768,3769],{"class":1704}," rust_app.bin\n",[19,3771,3772,3773,3776],{},"The two flash steps are independent. You can update the Rust binary without rebuilding or reflashing the ESP-IDF firmware — just flash the new ",[23,3774,3775],{},"rust_app.bin"," to the same partition offset.",[14,3778,3780],{"id":3779},"verifying-it-works","Verifying It Works",[19,3782,3783,3784,3787],{},"Open your serial monitor (",[23,3785,3786],{},"idf.py monitor"," or any terminal at 115200 baud) and you should see output like this:",[96,3789,3794],{"className":3790,"code":3792,"language":3793},[3791],"language-text","ESP-ROM:esp32s3-20210327\nBuild:Mar 27 2021\nrst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)\n...\nI (47) boot: Partition Table:\nI (50) boot: ## Label            Usage          Type ST Offset   Length\nI (56) boot:  0 nvs              WiFi data        01 02 00009000 00006000\nI (62) boot:  1 phy_init         RF data          01 01 0000f000 00001000\nI (69) boot:  2 factory          factory app      00 00 00010000 001f0000\nI (75) boot:  3 rust_app         Unknown data     01 40 00200000 00080000\nI (82) boot: End of partition table\n...\nI (202) heap_init: Initializing. RAM available for dynamic allocation:\nI (209) heap_init: At 3FC93BD8 len 00035B38 (214 KiB): RAM\nI (214) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM\nI (219) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM\nI (224) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM\n...\nI (279) main_task: Calling app_main()\nI (279) rust_app_core: Core 0: Starting IDF app\nI (280) rust_app_core: Rust app mapped at 0x42400000 (524288 bytes, flash 0x200000)\nI (283) rust_app_core: Rust entry at 0x42400024\nI (287) rust_app_core: Core 1 released\nI (291) rust_app_core: Rust Core 1 counter: 34538\nI (1295) rust_app_core: Rust Core 1 counter: 12369571\nI (2295) rust_app_core: Rust Core 1 counter: 24670917\nI (3295) rust_app_core: Rust Core 1 counter: 36972284\nI (4295) rust_app_core: Rust Core 1 counter: 49273651\n","text",[23,3795,3792],{"__ignoreMap":101},[19,3797,3798],{},"There are several things to confirm in this output:",[283,3800,3801,3813,3829,3837],{},[37,3802,3803,3806,3807,3809,3810,3812],{},[40,3804,3805],{},"The partition table"," shows our ",[23,3808,2898],{}," partition at offset ",[23,3811,2939],{},".",[37,3814,3815,3818,3819,3821,3822,3825,3826,3828],{},[40,3816,3817],{},"The heap_init logs"," show that our reserved 128KB region (starting at ",[23,3820,2704],{},") is ",[220,3823,3824],{},"not"," listed as available for dynamic allocation — ",[23,3827,229],{}," worked.",[37,3830,3831,3834,3835,3812],{},[40,3832,3833],{},"The MMU mapping"," succeeded — the Rust binary is mapped at ",[23,3836,2964],{},[37,3838,3839,3842],{},[40,3840,3841],{},"The counter is incrementing"," — Core 1 is alive, running Rust, and sharing data with Core 0 through the atomic counter at the agreed-upon memory address.",[81,3844],{},[84,3846,3848],{"id":3847},"whats-next","What's Next",[19,3850,3851],{},"This setup gives you the best of both worlds: ESP-IDF and FreeRTOS manage Wi-Fi, BLE, and system tasks on Core 0, while Core 1 runs your bare-metal Rust code at full speed with zero scheduler interference. Data flows between them through shared memory using atomics.",[19,3853,3854],{},"From here, there are a lot of directions you could take this: setting up interrupts on Core 1, building a proper shared memory protocol between the cores, implementing error recovery if the Rust program crashes, or even adding the ability for Core 0 to update the Rust binary over Wi-Fi and hot-restart Core 1.",[19,3856,3857],{},"The dual-core architecture of the ESP32-S3 turns out to be a surprisingly clean boundary for separating concerns — and for running two very different software paradigms side by side.",[3859,3860,3861],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":101,"searchDepth":114,"depth":114,"links":3863},[3864,3865,3866,3875,3881],{"id":16,"depth":120,"text":17},{"id":86,"depth":114,"text":87},{"id":207,"depth":114,"text":208,"children":3867},[3868,3869,3870,3871,3872,3873,3874],{"id":214,"depth":120,"text":215},{"id":277,"depth":120,"text":278},{"id":780,"depth":120,"text":781},{"id":944,"depth":120,"text":945},{"id":1114,"depth":120,"text":1115},{"id":1426,"depth":120,"text":1427},{"id":1673,"depth":120,"text":1674},{"id":1752,"depth":114,"text":1753,"children":3876},[3877,3878,3879,3880],{"id":1772,"depth":120,"text":1773},{"id":2884,"depth":120,"text":2885},{"id":3608,"depth":120,"text":3609},{"id":3779,"depth":120,"text":3780},{"id":3847,"depth":114,"text":3848},"2026-03-12","md",{},"/embedded/esp32/run_rust_on_app_core",{"title":5,"description":17},"embedded/ESP32/run_rust_on_app_core","NlFKgR5pqwUJSufyeMP6s9eNUWA9iRPOfZAISOlO_3o",1776777282624]