[{"data":1,"prerenderedAt":7651},["ShallowReactive",2],{"posts":3},[4,671,1488,2032,5858,7147],{"id":5,"title":6,"body":7,"date":663,"description":20,"extension":664,"meta":665,"navigation":666,"path":667,"seo":668,"stem":669,"__hash__":670},"content/cloud_notes/badwater_intro.md","I Built a WebAssembly Runtime in 5 Days Because I Was Tired of Paying for Cloud Run",{"type":8,"value":9,"toc":649},"minimark",[10,14,21,24,27,30,33,41,43,48,56,75,86,96,103,110,149,155,158,160,164,171,174,180,196,199,202,225,231,242,244,248,251,254,265,268,274,303,326,337,340,346,353,362,382,385,391,397,399,403,406,412,427,433,436,450,453,456,462,468,474,477,486,489,492,494,498,501,504,511,521,524,530,533,540,542,546,549,552,555,558,560,564,567,572,575,579,582,586,589,592,595,599,602,612,615,630,633,642,645],[11,12,6],"h1",{"id":13},"i-built-a-webassembly-runtime-in-5-days-because-i-was-tired-of-paying-for-cloud-run",[15,16,17],"p",{},[18,19,20],"em",{},"How a bootstrapped hardware startup, an $8 VPS, and five days of hacker-mode debugging became a working multi-tenant sandbox platform",[22,23],"hr",{},[15,25,26],{},"I co-founded a hardware audio startup. We needed infrastructure for firmware signing, device activation, and OTA delivery. I looked at the hyperscaler options — AWS KMS, Azure signing services — and the pricing made no sense for a bootstrapped company. Hundreds of dollars a month for workloads that consume almost nothing.",[15,28,29],{},"My background before this was embedded security. ESP32-S3 hardware secure boot, ARM TrustZone-M, HKDF key derivation on constrained hardware. The mental model from embedded work is: every single layer can fail, so you layer your defenses and verify everything. You never just trust.",[15,31,32],{},"WebAssembly seemed like the right tool. It's sandboxed by design, it compiles to a tiny binary, and WASI Preview 2 had just landed real networking support — meaning a WASM module could make HTTPS calls from inside the sandbox without me writing HTTP plumbing as a host function. That's what I wanted: run untrusted code safely, let it talk to the network, charge for compute, bill by instruction count.",[15,34,35,36,40],{},"The goal: build something like a minimal Cloudflare Workers, self-hosted, on commodity hardware. I called it ",[37,38,39],"strong",{},"Badwater",".",[22,42],{},[44,45,47],"h2",{"id":46},"day-1-the-api-doesnt-work-the-way-the-tutorial-says","Day 1 — The API Doesn't Work the Way the Tutorial Says",[15,49,50,51,55],{},"I had never used Wasmtime before. I knew Rust and I knew Linux, but the WASM component model was completely new to me. The first thing I did was copy a tutorial example that used ",[52,53,54],"code",{},"wasmtime::Func::wrap"," to register a host function. The example was for core modules. WASM components are different.",[15,57,58,59,62,63,66,67,70,71,74],{},"When you work with components, ",[52,60,61],{},"Func::wrap"," just doesn't work. You need ",[52,64,65],{},"wasmtime::component::Linker"," instead of ",[52,68,69],{},"wasmtime::Linker",", and you navigate to the right interface with ",[52,72,73],{},".instance(\"my:pkg/iface\")"," before you can wrap anything. None of the first tutorials I found made this distinction obvious — they all showed the core module API, which is shorter and cleaner.",[15,76,77,78,81,82,85],{},"Then I got an actual working guest compiled and tried to run it. The guest made an HTTPS call using ",[52,79,80],{},"ureq"," with ",[52,83,84],{},"rustls"," — pure Rust TLS, no host HTTP function. It compiled. I hit the endpoint. The host panicked:",[87,88,93],"pre",{"className":89,"code":91,"language":92},[90],"language-text","thread 'tokio-rt-worker' panicked at wasmtime-wasi-43.0.1/src/runtime.rs:108:15\nCannot start a runtime from within a runtime. This happens because a function (like block_on)\nattempted to block the current thread while the thread is being used to drive asynchronous tasks.\n","text",[52,94,91],{"__ignoreMap":95},"",[15,97,98,99,102],{},"The problem: Wasmtime's synchronous WASI implementation internally calls ",[52,100,101],{},"block_on"," to drive its async internals. If you're already inside a Tokio worker thread (which you are when an Axum handler runs), this panics because Tokio refuses to nest runtimes.",[15,104,105,106,109],{},"The fix is ",[52,107,108],{},"spawn_blocking",". Move the entire Wasmtime invocation off the async thread pool:",[87,111,115],{"className":112,"code":113,"language":114,"meta":95,"style":95},"language-rust shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","let result = tokio::task::spawn_blocking(move || {\n    // wasmtime execution here — now we're on a blocking thread\n    // can use blocking I/O, no nested runtime panic\n    ...\n}).await;\n","rust",[52,116,117,125,131,137,143],{"__ignoreMap":95},[118,119,122],"span",{"class":120,"line":121},"line",1,[118,123,124],{},"let result = tokio::task::spawn_blocking(move || {\n",[118,126,128],{"class":120,"line":127},2,[118,129,130],{},"    // wasmtime execution here — now we're on a blocking thread\n",[118,132,134],{"class":120,"line":133},3,[118,135,136],{},"    // can use blocking I/O, no nested runtime panic\n",[118,138,140],{"class":120,"line":139},4,[118,141,142],{},"    ...\n",[118,144,146],{"class":120,"line":145},5,[118,147,148],{},"}).await;\n",[15,150,151,152,154],{},"Once the WASM execution lives entirely in ",[52,153,108],{},", the blocking I/O inside it — TLS handshakes, network calls — runs on a thread pool that's designed for blocking work. The outer async runtime is unaffected.",[15,156,157],{},"By end of day 1, I had a guest function that could make an HTTPS request to Cloudflare's trace API from inside a WASM sandbox and return the result. It took the whole day.",[22,159],{},[44,161,163],{"id":162},"day-2-thinking-through-what-could-go-wrong","Day 2 — Thinking Through What Could Go Wrong",[15,165,166,167,170],{},"Working WASM runtime is one thing. A ",[18,168,169],{},"multi-tenant"," WASM runtime is something else. The question is isolation: if tenant A's code crashes, can it affect tenant B? If tenant A has a bug or is doing something malicious, what can they access?",[15,172,173],{},"I started thinking about this the way I think about embedded systems. What fails? What's the blast radius? In embedded work you always ask: if this component fails, what does it take down? The answer shapes the architecture.",[15,175,176,177,40],{},"This led to a design decision that turned out to be correct: ",[37,178,179],{},"two separate binaries",[181,182,183,190],"ul",{},[184,185,186,189],"li",{},[52,187,188],{},"badwater-dispatcher"," — the HTTP server. Handles requests, fetches WASM from storage, manages timeouts.",[184,191,192,195],{},[52,193,194],{},"badwater-runner"," — the WASM executor. Runs as a completely separate process per request, communicates over a Unix socketpair.",[15,197,198],{},"The key insight: if the runner crashes, panics, or is killed by the OS, the dispatcher is completely unaffected. They share no memory. The only interface between them is a typed binary protocol over a socket. The dispatcher sends WASM bytes and a request descriptor; the runner sends back a response. That's it.",[15,200,201],{},"But there's a bigger question: what stops the runner from accessing the host filesystem, or seeing other tenants' processes, or escalating privileges? WASM sandboxing is good, but Wasmtime has had security bugs before. You need a second layer.",[15,203,204,205,208,209,212,213,216,217,220,221,224],{},"I looked at options. Docker was too heavy — cold start overhead from the daemon alone. ",[52,206,207],{},"crun"," was interesting but had container complexity I didn't want to deal with. ",[52,210,211],{},"containerd"," was overkill. ",[52,214,215],{},"gVisor"," was too complex to self-host without an existing ops team. Then I found ",[37,218,219],{},"bubblewrap"," — the same sandbox tool that Flatpak uses for untrusted application isolation. It sets up Linux namespaces (user, pid, ipc, uts, cgroup), drops all capabilities, gives you a fresh ",[52,222,223],{},"tmpfs"," root. It's a single static binary, no daemon, auditable in an afternoon.",[87,226,229],{"className":227,"code":228,"language":92},[90],"HTTP Request\n  │\n  ▼\n[ badwater-dispatcher ] (Axum, Tokio)\n  │\n  ├─ fetch .cwasm \n  ├─ socketpair() ──────┐ \n  │                     │ (Unix socket)\n  ▼                     ▼\n[ child.kill() ]     [ bwrap sandbox (namespaces, --cap-drop ALL) ]\n (Hard timeout)         │\n                        ▼\n                     [ badwater-runner (PID 2) ]\n                        │\n                        └─ Wasmtime (Fuel metering, WASI P2)\n",[52,230,228],{"__ignoreMap":95},[15,232,233,234,237,238,241],{},"The architecture for day 2: every request spawns a fresh ",[52,235,236],{},"bwrap"," sandbox. WASM bytes stream over a socketpair file descriptor that gets ",[52,239,240],{},"dup2","'d into the child process before exec. The runner is killed if it exceeds the wall-clock timeout.",[22,243],{},[44,245,247],{"id":246},"day-3-bwrap-doesnt-like-being-told-nothing","Day 3 — bwrap Doesn't Like Being Told Nothing",[15,249,250],{},"Getting bubblewrap to actually work took most of day 3. I understood the theory. The practice was different.",[15,252,253],{},"First attempt at a sandboxed shell to verify isolation was working:",[87,255,259],{"className":256,"code":257,"language":258,"meta":95,"style":95},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","bwrap --unshare-all --ro-bind / / -- /bin/bash\n","sh",[52,260,261],{"__ignoreMap":95},[118,262,263],{"class":120,"line":121},[118,264,257],{},[15,266,267],{},"Output:",[87,269,272],{"className":270,"code":271,"language":92},[90],"bash: /dev/null: Permission denied\nbash: /dev/null: Permission denied\nbash: /dev/null: Permission denied\n... (19 more times)\n",[52,273,271],{"__ignoreMap":95},[15,275,276,277,280,281,284,285,288,289,292,293,295,296,298,299,302],{},"Twenty lines of the same error. ",[52,278,279],{},"--ro-bind / /"," bind-mounts the host root directory into the sandbox, but it's not a recursive bind. Your host's ",[52,282,283],{},"/dev"," is a separate ",[52,286,287],{},"devtmpfs"," mount that happens to be under ",[52,290,291],{},"/",". When you ",[52,294,279],{},", you get an empty ",[52,297,283],{}," directory inside the sandbox. Bash's initialization tries to redirect things to ",[52,300,301],{},"/dev/null"," and fails every time.",[15,304,305,306,309,310,312,313,315,316,318,319,318,322,325],{},"Fix: ",[52,307,308],{},"--dev /dev",". This tells bwrap to mount a minimal ",[52,311,287],{}," at ",[52,314,283],{}," inside the sandbox — ",[52,317,301],{},", ",[52,320,321],{},"/dev/zero",[52,323,324],{},"/dev/urandom",", the basic set. One flag.",[15,327,328,329,332,333,336],{},"Then: network worked in bwrap on the host, but DNS was broken. The sandbox had no ",[52,330,331],{},"/etc/resolv.conf"," because I hadn't explicitly bound it in. The sandbox can't see any host paths unless you explicitly mount them. Fix: ",[52,334,335],{},"--ro-bind /etc/resolv.conf /etc/resolv.conf",". One more flag.",[15,338,339],{},"Then I switched from Podman to Docker for building the final scratch image, and hit a completely different wall:",[87,341,344],{"className":342,"code":343,"language":92},[90],"bwrap: creating new namespace failed\n",[52,345,343],{"__ignoreMap":95},[15,347,348,349,352],{},"Root cause: Docker's default seccomp profile blocks ",[52,350,351],{},"CLONE_NEWUSER"," — the syscall bwrap needs to create a user namespace. Podman rootless mode allows it because rootless containers run with unprivileged user namespace support enabled by default. Docker runs containers as root and its default seccomp profile is more conservative.",[15,354,305,355,358,359,361],{},[52,356,357],{},"--privileged"," on the Docker container, or a custom seccomp profile. For development, ",[52,360,357],{}," is fine.",[15,363,364,365,368,369,371,372,374,375,378,379,40],{},"Then the ",[52,366,367],{},"FROM scratch"," image had no ",[52,370,331],{}," to bind-mount because ",[52,373,367],{}," contains nothing. I added a ",[52,376,377],{},"dirprep"," build stage that creates the empty directory structure before the final ",[52,380,381],{},"COPY",[15,383,384],{},"Each fix revealed the next problem. Each problem was actually bwrap working correctly — it just needed to be told explicitly about everything it needed. By end of day 3, this was in the logs:",[87,386,389],{"className":387,"code":388,"language":92},[90],"INFO badwater_runner: runner: starting (pid=2)\nINFO badwater_runner: runner: socket acquired\nINFO badwater_runner: runner: wasm completed - elapsed=555ms, fuel_consumed=182279633\n",[52,390,388],{"__ignoreMap":95},[15,392,393,396],{},[52,394,395],{},"pid=2",". Inside the new PID namespace, the runner sees itself as PID 2. That's the isolation. The socketpair fd-passing worked. The whole pipeline ran end to end.",[22,398],{},[44,400,402],{"id":401},"day-4-the-jit-issue","Day 4 — The JIT issue",[15,404,405],{},"First Cloud Run deploy. I'd been testing locally and the numbers looked fine. On Cloud Run, cold starts were showing ~2500ms. Way too slow for a platform that was supposed to be lightweight.",[15,407,408,409,411],{},"Every single request. Wasmtime was JIT-compiling the WASM component on every request from scratch. Cranelift — Wasmtime's code generator — was re-running the entire compilation pipeline each time. For a 1MB WASM binary that includes ",[52,410,84],{}," and the TLS stack, that's a long time of compilation before the code even starts running.",[15,413,414,415,418,419,422,423,426],{},"Wasmtime has a solution for this: ",[52,416,417],{},"wasmtime compile"," produces a ",[52,420,421],{},".cwasm"," file — native machine code pre-compiled for the host CPU. ",[52,424,425],{},"Component::deserialize_file"," loads it in ~20ms, bypassing JIT entirely. When I wrote a test to compare JIT and native WASM execution side by side, it showed the exact difference:",[87,428,431],{"className":429,"code":430,"language":92},[90],"test tests::compare_precompiled_vs_jit ... runner: JIT-compiling component from ./function.wasm\nrunner: wasm completed - elapsed=2081ms, fuel_consumed=182319268, fuel_remaining=4817680732, fuel_limit=5000000000, utilization=3.6%\nrunner: loading precompiled component from ./function.cwasm\nrunner: wasm completed - elapsed=117ms, fuel_consumed=182279609, fuel_remaining=4817720391, fuel_limit=5000000000, utilization=3.6%\n",[52,432,430],{"__ignoreMap":95},[15,434,435],{},"And the 2081ms dropped to 117ms. The fuel was still 182M — same work, same cost, just not recompiling on every request.",[15,437,438,439,442,443,446,447,449],{},"I built ",[52,440,441],{},"badwater-build-cwasm",", a small tool that compiles ",[52,444,445],{},".wasm"," → ",[52,448,421],{}," with the same Wasmtime config as the runner. Deployed to Cloud Run.",[15,451,452],{},"New cold start: ~100ms. 25x improvement.",[15,454,455],{},"Then it broke. Intermittently. Some requests failed with:",[87,457,460],{"className":458,"code":459,"language":92},[90],"compilation setting \"has_avx512bitalg\" is enabled, but not available on the host\n",[52,461,459],{"__ignoreMap":95},[15,463,464,465,467],{},"I had compiled the ",[52,466,421],{}," on my 7800X3D desktop. Cranelift detects the host CPU's features at compile time and emits native code using whatever extensions are available. My Ryzen supports AVX-512 variants. Cloud Run's instances do not.",[15,469,470,471,473],{},"The failure was intermittent because Cloud Run has multiple CPU generations in its fleet. The ",[52,472,421],{}," loaded fine on newer instances that happened to have AVX-512, and failed on older ones. Same image, different behavior depending on which physical machine the container landed on. This took me a while to figure out because I couldn't reproduce it locally — my desktop always had AVX-512.",[15,475,476],{},"Fix: tell Wasmtime to target a generic x86_64 baseline instead of the host CPU:",[87,478,480],{"className":112,"code":479,"language":114,"meta":95,"style":95},"config.target(\"x86_64-unknown-linux-musl\")?;\n",[52,481,482],{"__ignoreMap":95},[118,483,484],{"class":120,"line":121},[118,485,479],{},[15,487,488],{},"That one line makes Cranelift compile for the lowest common x86_64 denominator — no AVX-512, no host-specific extensions. AVX2 is still used; it's universally available on cloud hardware from the last decade.",[15,490,491],{},"After that: consistent sub-100ms server-side execution, every request, every Cloud Run instance.",[22,493],{},[44,495,497],{"id":496},"day-5-8month-live-domain","Day 5 — $8/Month, Live Domain",[15,499,500],{},"Cloud Run is elegant for development but the pricing doesn't make sense at scale. A 4-core, 8GB VPS from OVH US-West costs $7.60/month. Same region as Cloud Run us-west1. The math was obvious.",[15,502,503],{},"The VPS doesn't have GCP's Workload Identity, so the GCS authentication model breaks. (Plus, egress fees from GCP to OVH would add up fast). I needed a different storage backend. Cloudflare R2 is S3-compatible with a generous free tier and global edge distribution. The signing protocol is AWS SigV4 — an HMAC-SHA256 key derivation chain. I didn't want to pull in the AWS SDK for this.",[15,505,506,507,510],{},"The SigV4 implementation ended up being 242 lines of Rust with no external auth dependencies. The interesting part was the date math — SigV4 needs a formatted date string as part of the signing key, but I didn't want to pull in ",[52,508,509],{},"chrono"," just for that. Howard Hinnant has an algorithm for computing the calendar date from a raw Unix timestamp using pure integer arithmetic. I implemented that directly. It works with AWS S3, Cloudflare R2, MinIO, Wasabi, DigitalOcean Spaces — any S3-compatible endpoint.",[15,512,513,514,517,518,40],{},"Deployed to OVH. Put Cloudflare Tunnel in front — ",[52,515,516],{},"cloudflared"," running as a systemd service, outbound-only connection, no open ports on the VPS. Got a domain: ",[52,519,520],{},"badwater.app",[15,522,523],{},"Final numbers from the live deployment:",[87,525,528],{"className":526,"code":527,"language":92},[90],"# Health-check (WASM on disk, no storage fetch)\nWASM execution:    1ms\nServer-side total: 9ms\nEnd-to-end:        208ms (including Cloudflare TLS + proxy)\n\n# trace.cwasm (4.9MB cold fetch from R2 + outbound HTTPS inside sandbox)\nR2 fetch:          14ms\nSandbox startup:   6ms\nWASM execution:    162ms (includes full TLS 1.3 handshake + HTTP to Cloudflare)\nServer-side total: 182ms\nFuel consumed:     3.6% of limit\n",[52,529,527],{"__ignoreMap":95},[15,531,532],{},"No cache. Every request is a fresh sandbox from scratch.",[15,534,535,536,539],{},"The 9ms server-side on health-check is the floor of the model. A fresh Linux process, new user/pid/ipc namespaces, capability drop, WASM deserialization, and execution — all in under 10ms. The jump to 182ms on ",[52,537,538],{},"trace.cwasm"," is almost entirely the R2 fetch (14ms) and the guest's own outbound HTTPS call (the TLS handshake inside the sandbox). The sandbox itself is 6ms.",[22,541],{},[44,543,545],{"id":544},"what-i-ended-up-with","What I Ended Up With",[15,547,548],{},"The whole platform is 2,270 lines of Rust. No unnecessary dependencies. The image is 16MB. It runs on an $8 VPS.",[15,550,551],{},"The architecture that felt right on Day 2 shares the same general pattern that Cloudflare Workers uses — language-runtime sandbox (Wasmtime/V8) plus process-level isolation. I didn't know this when I designed it. I got there by asking \"what fails and what's the blast radius?\" That question is the embedded engineer's instinct applied to cloud infrastructure — the same thinking that ran through my ARM TrustZone-M work.",[15,553,554],{},"The JIT fix was the biggest single performance win.",[15,556,557],{},"The AVX-512 bug is obvious in retrospect. In embedded work you always specify the target CPU — you never compile for \"the current machine\" and ship to production. Somehow that gets forgotten in cloud deployments. The rule is the same: know your deployment ISA, don't assume the dev machine matches it.",[22,559],{},[44,561,563],{"id":562},"whats-next-the-multi-tenant-reality-check","What’s Next: The Multi-Tenant Reality Check",[15,565,566],{},"I built Badwater to solve my own problem, and it runs my production workloads flawlessly. But a platform isn't truly multi-tenant just because it runs in a sandbox. There are two major architectural hurdles left to solve before I’d let strangers upload arbitrary WASM to my servers.",[568,569,571],"h3",{"id":570},"_1-hiding-the-sandbox-overhead-the-warm-pool","1. Hiding the Sandbox Overhead (The Warm Pool)",[15,573,574],{},"I drove the cold-start floor down to 9ms, but 9ms on the critical path is still an eternity in high-throughput systems. Right now, every single HTTP request triggers a synchronous fork(), a bubblewrap namespace initialization, and a Wasmtime engine boot-up. To achieve the sub-millisecond overhead of commercial FaaS platforms, Badwater needs a warm pool. The dispatcher needs to maintain a queue of pre-spawned, sandboxed runners simply waiting for a payload on their socket, removing the OS-level process creation from the request lifecycle entirely.",[568,576,578],{"id":577},"_2-the-ssrf-metadata-problem","2. The SSRF Metadata Problem",[15,580,581],{},"Bubblewrap provides excellent process and filesystem isolation, but currently, the sandboxes share the host's ability to route outbound network traffic. On my OVH box, this is fine. But if someone deploys Badwater to GCP or AWS, a malicious tenant could write a WASM function that makes an HTTP GET request to 169.254.169.254—the cloud provider's internal metadata endpoint. Without strict network isolation, that tenant could easily steal the underlying virtual machine's IAM credentials. Real multi-tenancy requires isolating the network namespaces or implementing strict UID-based packet filtering at the host level to drop those internal routing requests.",[568,583,585],{"id":584},"_3-tiered-image-caching-killing-the-network-on-cold-starts","3. Tiered Image Caching (Killing the Network on Cold Starts)",[15,587,588],{},"Even with a warm pool of initialized runners, a cold start is eventually required—either because a tenant deploys new code or the pool scales up. Right now, a cold start requires pulling the .cwasm binary over the public internet from GCS or R2. In my benchmarks, fetching a 4.9MB component takes 14ms.",[15,590,591],{},"At a low request volume, 14ms is fine. At high throughput, saturating the host's network link to repeatedly download the same binary from S3 is architectural malpractice. But on an $8 VPS with only 8GB of RAM, you can't just cache every tenant's binary in memory.",[15,593,594],{},"The platform needs a multi-tier LRU (Least Recently Used) caching strategy. When the dispatcher needs a WASM image, it should check an in-memory cache first (Tier 1, sub-millisecond). If there's a cache miss, it falls back to a local SSD cache (Tier 2, ~1ms), and only if that misses does it go out to object storage (Tier 3, 14ms). Building a cache invalidation and eviction policy that respects the strict memory limits of commodity hardware is the next major infrastructure hurdle.",[44,596,598],{"id":597},"the-code","The Code",[15,600,601],{},"The runtime is open source under GPL-3.0-or-later.",[15,603,604,605],{},"Source: ",[606,607,611],"a",{"href":608,"rel":609},"https://github.com/peterw22/badwater",[610],"nofollow","github.com/peterw22/badwater",[15,613,614],{},"Prebuilt images:",[87,616,618],{"className":256,"code":617,"language":258,"meta":95,"style":95},"docker pull ghcr.io/peterw22/badwater:gcs-0.09  # Google Cloud\ndocker pull ghcr.io/peterw22/badwater:s3-0.09   # Anywhere else\n",[52,619,620,625],{"__ignoreMap":95},[118,621,622],{"class":120,"line":121},[118,623,624],{},"docker pull ghcr.io/peterw22/badwater:gcs-0.09  # Google Cloud\n",[118,626,627],{"class":120,"line":127},[118,628,629],{},"docker pull ghcr.io/peterw22/badwater:s3-0.09   # Anywhere else\n",[15,631,632],{},"Live demo:",[87,634,636],{"className":256,"code":635,"language":258,"meta":95,"style":95},"curl https://us-west.badwater.app/invoke/trace.cwasm\n",[52,637,638],{"__ignoreMap":95},[118,639,640],{"class":120,"line":121},[118,641,635],{},[15,643,644],{},"If you're an infrastructure nerd, I'd love for you to try breaking out of the sandbox, or open an issue if you see a flaw in the bubblewrap configuration.",[646,647,648],"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);}",{"title":95,"searchDepth":127,"depth":127,"links":650},[651,652,653,654,655,656,657,662],{"id":46,"depth":127,"text":47},{"id":162,"depth":127,"text":163},{"id":246,"depth":127,"text":247},{"id":401,"depth":127,"text":402},{"id":496,"depth":127,"text":497},{"id":544,"depth":127,"text":545},{"id":562,"depth":127,"text":563,"children":658},[659,660,661],{"id":570,"depth":133,"text":571},{"id":577,"depth":133,"text":578},{"id":584,"depth":133,"text":585},{"id":597,"depth":127,"text":598},"2026-04-21","md",{},true,"/cloud_notes/badwater_intro",{"title":6,"description":20},"cloud_notes/badwater_intro","On2a71OgTa8i95kpwJnPibZh-rXUf1NFCoCJnz8JZ-g",{"id":672,"title":673,"body":674,"date":1482,"description":683,"extension":664,"meta":1483,"navigation":666,"path":1484,"seo":1485,"stem":1486,"__hash__":1487},"content/cloud_notes/deploy_blog_on_azure_storage.md","Deploy Blog on Azure Storage",{"type":8,"value":675,"toc":1473},[676,679,684,689,696,700,706,709,712,715,718,735,744,759,766,774,778,785,788,793,798,809,813,820,829,840,849,853,861,897,901,904,908,915,922,926,941,944,958,964,978,998,1002,1005,1009,1025,1028,1034,1044,1050,1056,1060,1067,1079,1084,1088,1094,1417,1420,1424,1431,1438,1444,1448,1470],[11,677,673],{"id":678},"deploy-blog-on-azure-storage",[680,681,683],"h5",{"id":682},"using-github-action-to-build-mkdocs-and-deploy-to-azure","Using GitHub Action to build Mkdocs and deploy to Azure",[15,685,686],{},[37,687,688],{},"Oct 1 2024",[15,690,691],{},[692,693],"img",{"alt":694,"src":695},"An orange cat writing a website","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Gemini_Generated_Image_hz8byghz8byghz8b.jpg",[44,697,699],{"id":698},"_0-intro","0. Intro",[15,701,702],{},[606,703,705],{"href":704},"#1-setup-github-repo","Skip my complains to real deployment process",[15,707,708],{},"Several days ago, I have this new idea to setup a personal website again after abandoned my website for several years, even forgot to renew my domain. I have few experience with building my own website using Wordpress before, but consider the LAMP stack (Linux, Apache, MySQL, PHP) is a thing from ancient time, learned those back when I was still in middle school (around 10 years ago), and I just don't want to deploy another PHP website in 2024, the world don't need another internet artifact 🦕.",[15,710,711],{},"I realized that there is no need to create a dynamic content website at all, because all I wanted is a website with almost zero user interaction, even if I want to add comment function later, that could be easily done by sending an AJAX request to a serverless function run on Cloudflare Worker or Azure Functions which mean there is no need to any dynamic code on the blog site itself, also that means that I do not need to do any maintenance work on a $5 VPS.",[15,713,714],{},"On the other hand, I like that Wordpress offer a easy way to just write a blog post, the writing tool provided in Wordpress is easy to use with zero HTML gymnastics, which felt like Microsoft Word, as it should be, but I perosnally use Markdown to write a lot of documents, and Markdown can be easily rendered into HTML. So that is the logical choice for me to write blog post in.",[15,716,717],{},"So, summary all that long chat up, I need a blog stack that offer:",[181,719,720,723,726,729,732],{},[184,721,722],{},"Write everything in Markdown",[184,724,725],{},"Render Markdown content into a static HTML site",[184,727,728],{},"Some form of automation, so I don't need to upload a zip everytime I write something",[184,730,731],{},"A service that serve a static website",[184,733,734],{},"Maybe a CDN service that can cache the static content being served",[15,736,737,738,743],{},"The tool I choose to use to render Markdown into static website files is ",[606,739,742],{"href":740,"rel":741},"https://www.mkdocs.org",[610],"MkDocs",", it is quite popular and being used to host a lot of documentations of opensource projects, but there is not a lot of difference between documentation and blog 🤣.",[15,745,746,747,752,753,758],{},"I have been using ",[606,748,751],{"href":749,"rel":750},"https://developers.cloudflare.com/pages/",[610],"Cloudflare Pages"," to serve NextJS projects, and it also provide GitHub integration which provided all the necessary automation function, and there is ",[606,754,757],{"href":755,"rel":756},"https://developers.cloudflare.com/pages/framework-guides/deploy-an-mkdocs-site/",[610],"a step by step guide"," provided by Cloudflare. When this website first came online several days ago, this was my choice, and the guide is very easy to follow, with good GitHub integration, but this method come with downside too.",[15,760,761,762],{},"One of the biggest limitation is the max file size in Cloudflare Pages is 25MB.\n",[692,763],{"alt":764,"src":765},"File Size Limitation","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot1.png",[15,767,768,769,773],{},"The other thing is I do not want to be locked into one cloud provider's ecosystem.\n",[692,770],{"alt":771,"src":772},"Billing Issue","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot2.png","\nMy Cloudflare account seems to have some issue with billing, it's certainly my fault, I didn't pay the bill first, but this left a bad taste in my mouth, so I am planing to move away from the Cloudflare ecosystem and migrate to Microsoft Azure, why Azure? I don't want to use AWS and pay for Jeff Bezos' yacht 🛥️.",[44,775,777],{"id":776},"_1-setup-github-repo","1. Setup GitHub Repo",[15,779,780,781],{},"Enough of complaining, time to deploy the website. Following the Guide provided by Cloudflare, we already have a GitHub with these files.\n",[692,782],{"alt":783,"src":784},"Pre-Setup Files","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot3.png",[15,786,787],{},"If you did not setup a Cloudflare Pages site first you could follow these steps.",[15,789,790],{},[18,791,792],{},"(Copied From Cloudflare Documents)",[794,795,797],"h4",{"id":796},"_1-install-mkdocs","1. Install MkDocs:",[87,799,803],{"className":800,"code":801,"language":802,"meta":95,"style":95},"language-shell shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pip install mkdocs\n","shell",[52,804,805],{"__ignoreMap":95},[118,806,807],{"class":120,"line":121},[118,808,801],{},[794,810,812],{"id":811},"_2-create-an-mkdocs-project","2. Create an MkDocs project:",[15,814,815,816,819],{},"Use the ",[52,817,818],{},"mkdocs new"," command to create a new application:",[87,821,823],{"className":800,"code":822,"language":802,"meta":95,"style":95},"mkdocs new \u003CPROJECT_NAME>\n",[52,824,825],{"__ignoreMap":95},[118,826,827],{"class":120,"line":121},[118,828,822],{},[15,830,831,832,835,836,839],{},"Then ",[52,833,834],{},"cd"," into your project, take MkDocs and its dependencies and put them into a ",[52,837,838],{},"requirements.txt"," file:",[87,841,843],{"className":800,"code":842,"language":802,"meta":95,"style":95},"pip freeze > requirements.txt\n",[52,844,845],{"__ignoreMap":95},[118,846,847],{"class":120,"line":121},[118,848,842],{},[794,850,852],{"id":851},"_3-create-a-github-repository","3. Create a GitHub repository:",[15,854,855,860],{},[606,856,859],{"href":857,"rel":858},"https://github.com/new/",[610],"Create a new GitHub repository",". After creating a new repository, go to your newly created project directory to prepare and push your local application to GitHub by running the following commands in your terminal:",[87,862,864],{"className":800,"code":863,"language":802,"meta":95,"style":95},"git init\ngit remote add origin https://github.com/\u003Cyour-gh-username>/\u003Crepository-name>\ngit add .\ngit commit -m \"Initial commit\"\ngit branch -M main\ngit push -u origin main\n",[52,865,866,871,876,881,886,891],{"__ignoreMap":95},[118,867,868],{"class":120,"line":121},[118,869,870],{},"git init\n",[118,872,873],{"class":120,"line":127},[118,874,875],{},"git remote add origin https://github.com/\u003Cyour-gh-username>/\u003Crepository-name>\n",[118,877,878],{"class":120,"line":133},[118,879,880],{},"git add .\n",[118,882,883],{"class":120,"line":139},[118,884,885],{},"git commit -m \"Initial commit\"\n",[118,887,888],{"class":120,"line":145},[118,889,890],{},"git branch -M main\n",[118,892,894],{"class":120,"line":893},6,[118,895,896],{},"git push -u origin main\n",[44,898,900],{"id":899},"_2-setup-azure-storage-account","2. Setup Azure Storage Account",[15,902,903],{},"Next step is to set up the Azure Storage Account which is going to host the static site. Here I assume you already have a Azure account available for use.",[794,905,907],{"id":906},"_1-create-a-storage-account-in-azure-portal","1. Create a Storage Account in Azure Portal",[15,909,910,911],{},"Click here to set up a Storage Account. The name of the account could be anything.\n",[692,912],{"alt":913,"src":914},"Create Resource","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot4.png",[15,916,917,918],{},"After the resource have been deployed you should be able to view the informations about this Storage Account.\n",[692,919],{"alt":920,"src":921},"Storage Account","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot5.png",[794,923,925],{"id":924},"_2-enable-static-website-capability","2. Enable Static Website Capability",[15,927,928,929,932,933,936,937],{},"Click on the ",[37,930,931],{},"Capabilities"," tab and select ",[37,934,935],{},"static website",".\n",[692,938],{"alt":939,"src":940},"Enable Static Website","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot6.png",[15,942,943],{},"Enable the Static Website Toggle and fill in the document path:",[181,945,946,952],{},[184,947,948,949],{},"Index document name ",[52,950,951],{},"index.html",[184,953,954,955],{},"Error document path ",[52,956,957],{},"404.html",[15,959,960],{},[692,961],{"alt":962,"src":963},"Fill info","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot7.png",[15,965,966,967,970,971,974,975],{},"After that click ",[37,968,969],{},"save",", and you should be able to see the ",[37,972,973],{},"Endpoint"," created for your static website.\n",[692,976],{"alt":973,"src":977},"https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot8.png",[15,979,980,981,984,985,988,989,993,994,997],{},"And copy the ",[37,982,983],{},"Primary endpoint"," into a browser, you should be greeted by a ",[52,986,987],{},"The requested content does not exist."," error.\n",[692,990],{"alt":991,"src":992},"The requested content does not exist","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot9.png","\nThis is because we have just configured Storage account send 404.html to user when the request page cannot be found, however we haven't upload any files into the ",[52,995,996],{},"$web"," Azure Storage container, naturally there is no 404.html in the container, so we get a Azure Error page. All good, we can keep going.",[44,999,1001],{"id":1000},"_3-setup-azure-deployment-credentials","3. Setup Azure deployment credentials",[15,1003,1004],{},"Next step we want is let GitHub Action to upload Generated static files to Azure automatically whenever there is a push in the main branch in our GitHub Repo. But we need to authorize GitHub Action to access our Azure Storage Account, so we need to setup a Azure Credential for GitHub Action.",[794,1006,1008],{"id":1007},"_1-generate-azure-credentials-in-azure-cloud-shell","1. Generate Azure credentials in Azure Cloud Shell",[15,1010,1011,1012,1016,1017,1020,1021],{},"We can open Azure Cloud Shell in Azure Portal by click the console button on the top right.\n",[692,1013],{"alt":1014,"src":1015},"Open Azure Cloud Shell","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot10.png","\nThere should be a shell windows pop up at the bottom half of the screen.",[1018,1019],"br",{},"\nSelect PowerShell for shell program. This time we don't need to attach a storage account, because we are not going to save any data in the Cloud Shell.  After apply the setting we should be able to run Azure Cli commands in the Cloud Shell.\n",[692,1022],{"alt":1023,"src":1024},"Azure Cloud Shell","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot11.png",[15,1026,1027],{},"We can generate Credential by typing in the PowerShell command.",[87,1029,1032],{"className":1030,"code":1031,"language":92},[90],"az ad sp create-for-rbac --name \"GithubAction\" --role contributor --scopes /subscriptions/\u003Csubscription-id>/resourceGroups/\u003Cgroup-name> --json-auth\n",[52,1033,1031],{"__ignoreMap":95},[15,1035,1036,1039,1040],{},[37,1037,1038],{},"Replace the Subscription-ID and ResourceGroup-Name with your own",", which can be retrieved from the Storage account Overview tab.\n",[692,1041],{"alt":1042,"src":1043},"Subscription-ID and ResourceGroup-Name","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot12.png",[15,1045,1046,1047],{},"This command should give you back a JSON credential, ",[37,1048,1049],{},"keep it safe!",[87,1051,1054],{"className":1052,"code":1053,"language":92},[90],"{\n    \"clientId\": \"\u003CGUID>\",\n    \"clientSecret\": \"\u003CGUID>\",\n    \"subscriptionId\": \"\u003CGUID>\",\n    \"tenantId\": \"\u003CGUID>\",\n    (...)\n}\n",[52,1055,1053],{"__ignoreMap":95},[794,1057,1059],{"id":1058},"_2-save-azure-credentials-in-github-action-secrets","2. Save Azure credentials in GitHub Action secrets",[15,1061,1062,1063],{},"Go to the GitHub Repo, in settings tab, select secrets and variables on the left side, create a new Repository secrets.\n",[692,1064],{"alt":1065,"src":1066},"create a new Repository secrets","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot13.png",[15,1068,1069,1070,1072,1073,1076,1078],{},"Save the JSON credential create before into GitHub Repo.",[1018,1071],{},"\nName: ",[52,1074,1075],{},"AZURE_CREDENTIALS",[1018,1077],{},"\nSecert:",[87,1080,1082],{"className":1081,"code":1053,"language":92},[90],[52,1083,1053],{"__ignoreMap":95},[44,1085,1087],{"id":1086},"_4-create-a-github-action-file-in-your-project","4. Create a GitHub Action File in your Project",[15,1089,1090,1091,1093],{},"Create a file in the your Project Folder.",[1018,1092],{},"\n.github/workflows/azure-publish.yml",[87,1095,1099],{"className":1096,"code":1097,"language":1098,"meta":95,"style":95},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","name: Azure Preview Prod\non:\n  push:\n    # This Action will run when there is a push to main branch\n    branches: [ \"main\" ]\n  workflow_dispatch:\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      # checkout the project repo\n      - uses: actions/checkout@v4\n      # use python 3.12\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n          cache: 'pip' # caching pip dependencies\n      # Install Pip Requirements\n      - run: pip install -r requirements.txt\n      # MkDocs Build markdown into static files\n      - run: mkdocs build --site-dir ./site -v\n      # Login using Azure Credentials\n      - uses: Azure/login@v2.2.0\n        with:\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n      # Upload Builded file to Azure Storage Account\n      - name: Upload to blob storage\n        uses: Azure/cli@v2.1.0\n        with:\n          inlineScript: |\n            az storage blob delete-batch --account-name \u003Cstorage-account-name> -s '$web' --pattern \"*\"\n            az storage blob upload-batch --account-name \u003Cstorage-account-name> -d '$web' -s ./site\n","yaml",[52,1100,1101,1115,1124,1131,1137,1159,1166,1174,1182,1193,1201,1207,1221,1227,1239,1247,1264,1283,1289,1302,1308,1320,1326,1338,1345,1356,1362,1375,1386,1393,1405,1411],{"__ignoreMap":95},[118,1102,1103,1107,1111],{"class":120,"line":121},[118,1104,1106],{"class":1105},"swJcz","name",[118,1108,1110],{"class":1109},"sMK4o",":",[118,1112,1114],{"class":1113},"sfazB"," Azure Preview Prod\n",[118,1116,1117,1121],{"class":120,"line":127},[118,1118,1120],{"class":1119},"sfNiH","on",[118,1122,1123],{"class":1109},":\n",[118,1125,1126,1129],{"class":120,"line":133},[118,1127,1128],{"class":1105},"  push",[118,1130,1123],{"class":1109},[118,1132,1133],{"class":120,"line":139},[118,1134,1136],{"class":1135},"sHwdD","    # This Action will run when there is a push to main branch\n",[118,1138,1139,1142,1144,1147,1150,1153,1156],{"class":120,"line":145},[118,1140,1141],{"class":1105},"    branches",[118,1143,1110],{"class":1109},[118,1145,1146],{"class":1109}," [",[118,1148,1149],{"class":1109}," \"",[118,1151,1152],{"class":1113},"main",[118,1154,1155],{"class":1109},"\"",[118,1157,1158],{"class":1109}," ]\n",[118,1160,1161,1164],{"class":120,"line":893},[118,1162,1163],{"class":1105},"  workflow_dispatch",[118,1165,1123],{"class":1109},[118,1167,1169,1172],{"class":120,"line":1168},7,[118,1170,1171],{"class":1105},"jobs",[118,1173,1123],{"class":1109},[118,1175,1177,1180],{"class":120,"line":1176},8,[118,1178,1179],{"class":1105},"  build",[118,1181,1123],{"class":1109},[118,1183,1185,1188,1190],{"class":120,"line":1184},9,[118,1186,1187],{"class":1105},"    runs-on",[118,1189,1110],{"class":1109},[118,1191,1192],{"class":1113}," ubuntu-latest\n",[118,1194,1196,1199],{"class":120,"line":1195},10,[118,1197,1198],{"class":1105},"    steps",[118,1200,1123],{"class":1109},[118,1202,1204],{"class":120,"line":1203},11,[118,1205,1206],{"class":1135},"      # checkout the project repo\n",[118,1208,1210,1213,1216,1218],{"class":120,"line":1209},12,[118,1211,1212],{"class":1109},"      -",[118,1214,1215],{"class":1105}," uses",[118,1217,1110],{"class":1109},[118,1219,1220],{"class":1113}," actions/checkout@v4\n",[118,1222,1224],{"class":120,"line":1223},13,[118,1225,1226],{"class":1135},"      # use python 3.12\n",[118,1228,1230,1232,1234,1236],{"class":120,"line":1229},14,[118,1231,1212],{"class":1109},[118,1233,1215],{"class":1105},[118,1235,1110],{"class":1109},[118,1237,1238],{"class":1113}," actions/setup-python@v5\n",[118,1240,1242,1245],{"class":120,"line":1241},15,[118,1243,1244],{"class":1105},"        with",[118,1246,1123],{"class":1109},[118,1248,1250,1253,1255,1258,1261],{"class":120,"line":1249},16,[118,1251,1252],{"class":1105},"          python-version",[118,1254,1110],{"class":1109},[118,1256,1257],{"class":1109}," '",[118,1259,1260],{"class":1113},"3.12",[118,1262,1263],{"class":1109},"'\n",[118,1265,1267,1270,1272,1274,1277,1280],{"class":120,"line":1266},17,[118,1268,1269],{"class":1105},"          cache",[118,1271,1110],{"class":1109},[118,1273,1257],{"class":1109},[118,1275,1276],{"class":1113},"pip",[118,1278,1279],{"class":1109},"'",[118,1281,1282],{"class":1135}," # caching pip dependencies\n",[118,1284,1286],{"class":120,"line":1285},18,[118,1287,1288],{"class":1135},"      # Install Pip Requirements\n",[118,1290,1292,1294,1297,1299],{"class":120,"line":1291},19,[118,1293,1212],{"class":1109},[118,1295,1296],{"class":1105}," run",[118,1298,1110],{"class":1109},[118,1300,1301],{"class":1113}," pip install -r requirements.txt\n",[118,1303,1305],{"class":120,"line":1304},20,[118,1306,1307],{"class":1135},"      # MkDocs Build markdown into static files\n",[118,1309,1311,1313,1315,1317],{"class":120,"line":1310},21,[118,1312,1212],{"class":1109},[118,1314,1296],{"class":1105},[118,1316,1110],{"class":1109},[118,1318,1319],{"class":1113}," mkdocs build --site-dir ./site -v\n",[118,1321,1323],{"class":120,"line":1322},22,[118,1324,1325],{"class":1135},"      # Login using Azure Credentials\n",[118,1327,1329,1331,1333,1335],{"class":120,"line":1328},23,[118,1330,1212],{"class":1109},[118,1332,1215],{"class":1105},[118,1334,1110],{"class":1109},[118,1336,1337],{"class":1113}," Azure/login@v2.2.0\n",[118,1339,1341,1343],{"class":120,"line":1340},24,[118,1342,1244],{"class":1105},[118,1344,1123],{"class":1109},[118,1346,1348,1351,1353],{"class":120,"line":1347},25,[118,1349,1350],{"class":1105},"          creds",[118,1352,1110],{"class":1109},[118,1354,1355],{"class":1113}," ${{ secrets.AZURE_CREDENTIALS }}\n",[118,1357,1359],{"class":120,"line":1358},26,[118,1360,1361],{"class":1135},"      # Upload Builded file to Azure Storage Account\n",[118,1363,1365,1367,1370,1372],{"class":120,"line":1364},27,[118,1366,1212],{"class":1109},[118,1368,1369],{"class":1105}," name",[118,1371,1110],{"class":1109},[118,1373,1374],{"class":1113}," Upload to blob storage\n",[118,1376,1378,1381,1383],{"class":120,"line":1377},28,[118,1379,1380],{"class":1105},"        uses",[118,1382,1110],{"class":1109},[118,1384,1385],{"class":1113}," Azure/cli@v2.1.0\n",[118,1387,1389,1391],{"class":120,"line":1388},29,[118,1390,1244],{"class":1105},[118,1392,1123],{"class":1109},[118,1394,1396,1399,1401],{"class":120,"line":1395},30,[118,1397,1398],{"class":1105},"          inlineScript",[118,1400,1110],{"class":1109},[118,1402,1404],{"class":1403},"s7zQu"," |\n",[118,1406,1408],{"class":120,"line":1407},31,[118,1409,1410],{"class":1113},"            az storage blob delete-batch --account-name \u003Cstorage-account-name> -s '$web' --pattern \"*\"\n",[118,1412,1414],{"class":120,"line":1413},32,[118,1415,1416],{"class":1113},"            az storage blob upload-batch --account-name \u003Cstorage-account-name> -d '$web' -s ./site\n",[15,1418,1419],{},"This file defined a GitHub Action workflow which will trigger automatically whenever there is update pushed to the main branch.",[44,1421,1423],{"id":1422},"_5-final-step-push-your-changes-to-github","5. Final Step: Push your changes to GitHub",[15,1425,1426,1427],{},"Once your push the changes to GitHub Repo, the action we defined in the file above should be automatically be triggered and start the workflow.\n",[692,1428],{"alt":1429,"src":1430},"Triggered Workflow","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot14.png",[15,1432,1433,1434],{},"Wait around 1 mins. Open the Endpoint Url we got in Step 2, now we should be able to see the website we just deployed on Azure using GitHub Action! 🤩🥳\n",[692,1435],{"alt":1436,"src":1437},"Working Website","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Screenshot15.png",[15,1439,1440],{},[692,1441],{"alt":1442,"src":1443},"Cat with Cola","https://static-blog.tingouw.com/cloud_notes/deploy_blog_on_azure_storage/Gemini_Generated_Image_rue7g9rue7g9rue7.jpg",[44,1445,1447],{"id":1446},"references","References",[181,1449,1450,1456,1463],{},[184,1451,1452],{},[606,1453,1455],{"href":755,"rel":1454},[610],"MkDocs | Cloudflare Pages docs",[184,1457,1458],{},[606,1459,1462],{"href":1460,"rel":1461},"https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website",[610],"Static website hosting in Azure Storage",[184,1464,1465],{},[606,1466,1469],{"href":1467,"rel":1468},"https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-static-site-github-actions?tabs=userlevel",[610],"Use GitHub Actions workflow to deploy your static website in Azure Storage",[646,1471,1472],{},"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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":95,"searchDepth":127,"depth":127,"links":1474},[1475,1476,1477,1478,1479,1480,1481],{"id":698,"depth":127,"text":699},{"id":776,"depth":127,"text":777},{"id":899,"depth":127,"text":900},{"id":1000,"depth":127,"text":1001},{"id":1086,"depth":127,"text":1087},{"id":1422,"depth":127,"text":1423},{"id":1446,"depth":127,"text":1447},"2024-10-1",{},"/cloud_notes/deploy_blog_on_azure_storage",{"title":673,"description":683},"cloud_notes/deploy_blog_on_azure_storage","toiESUOVNYG9S42bK8X5HZwL6nMlCVAL7joEbaNG83c",{"id":1489,"title":1490,"body":1491,"date":2026,"description":1499,"extension":664,"meta":2027,"navigation":666,"path":2028,"seo":2029,"stem":2030,"__hash__":2031},"content/cloud_notes/interact_with_azure_vtpm.md","Interact with Azure VM vTPM (Part 0)",{"type":8,"value":1492,"toc":2020},[1493,1496,1500,1502,1505,1508,1519,1522,1531,1535,1542,1545,1585,1589,1595,1598,1607,1610,1695,1698,1702,1706,1709,1712,1721,1724,1730,1733,1736,1761,1764,1768,1771,1780,1863,1866,1870,1873,1882,1885,1894,1897,1906,1914,1917,1932,1935,1944,1947,1951,1954,1963,1966,1986,1989,1992,1995,2009,2012,2015,2018],[11,1494,1490],{"id":1495},"interact-with-azure-vm-vtpm-part-0",[680,1497,1499],{"id":1498},"how-to-prove-a-vm-is-accually-on-azure","How to prove a VM is accually on Azure",[44,1501,699],{"id":698},[15,1503,1504],{},"Recently, I have been working on Remote Attestation, and because of company need the service to be scalable, instead of the real physical Linux machine, with physical TPM, we need to deploy it on to a Cloud Provider. I start to work on how to implement remotly attestation on Azure VMs.",[15,1506,1507],{},"The goal of this series of blog is to create a system can achieve the following goal.",[181,1509,1510,1513,1516],{},[184,1511,1512],{},"Verify this is a VM running on Microsoft Azure",[184,1514,1515],{},"Remotely verify the VM's bootchain",[184,1517,1518],{},"Remotely verify system runtime through IMA",[15,1520,1521],{},"Now I have a clear goal and it is time to do some research.",[15,1523,1524,1525,1530],{},"Azure provide a custom Attestation service, called ",[606,1526,1529],{"href":1527,"rel":1528},"https://learn.microsoft.com/en-us/azure/attestation/overview",[610],"Microsoft Azure Attestation",". This service is Azure dependent, and I want to avoid using Microsoft only method to achieve this, and I dont want the attestation process envolve making RESTAPI call to Azure services.",[44,1532,1534],{"id":1533},"_1-requirement","1. Requirement",[15,1536,1537,1538,1541],{},"This verification process need the system to have a vTPM enabled. This kind of VM can be created by enable ",[52,1539,1540],{},"--enable-vtpm true"," flag.",[15,1543,1544],{},"Create a VM with vTPM and enable Secure Boot using Azure CLI",[87,1546,1548],{"className":800,"code":1547,"language":802,"meta":95,"style":95},"$ az vm create -n \"${az_vm_name}\" \\\n    -g \"${az_resource_group}\" \\\n    --image \"${image_version_id}\" \\\n    --admin-username core \\\n    --security-type TrustedLaunch \\\n    --enable-vtpm true \\\n    --enable-secure-boot true\n",[52,1549,1550,1555,1560,1565,1570,1575,1580],{"__ignoreMap":95},[118,1551,1552],{"class":120,"line":121},[118,1553,1554],{},"$ az vm create -n \"${az_vm_name}\" \\\n",[118,1556,1557],{"class":120,"line":127},[118,1558,1559],{},"    -g \"${az_resource_group}\" \\\n",[118,1561,1562],{"class":120,"line":133},[118,1563,1564],{},"    --image \"${image_version_id}\" \\\n",[118,1566,1567],{"class":120,"line":139},[118,1568,1569],{},"    --admin-username core \\\n",[118,1571,1572],{"class":120,"line":145},[118,1573,1574],{},"    --security-type TrustedLaunch \\\n",[118,1576,1577],{"class":120,"line":893},[118,1578,1579],{},"    --enable-vtpm true \\\n",[118,1581,1582],{"class":120,"line":1168},[118,1583,1584],{},"    --enable-secure-boot true\n",[44,1586,1588],{"id":1587},"_2-verify-tpm-in-the-vm","2. Verify TPM in the VM",[15,1590,1591,1592,40],{},"After the VM is spinned up, check if the TPM exist, using ",[52,1593,1594],{},"tpm2-tools",[15,1596,1597],{},"Running the following command to check if TPM is working.",[87,1599,1601],{"className":800,"code":1600,"language":802,"meta":95,"style":95},"$ sudo tpm2_getcap properties-fixed\n",[52,1602,1603],{"__ignoreMap":95},[118,1604,1605],{"class":120,"line":121},[118,1606,1600],{},[15,1608,1609],{},"And check the output value.",[87,1611,1613],{"className":800,"code":1612,"language":802,"meta":95,"style":95},"TPM2_PT_FAMILY_INDICATOR:\n  raw: 0x322E3000\n  value: \"2.0\"\nTPM2_PT_LEVEL:\n  raw: 0\nTPM2_PT_REVISION:\n  raw: 0x8A\n  value: 1.38\nTPM2_PT_DAY_OF_YEAR:\n  raw: 0x19\nTPM2_PT_YEAR:\n  raw: 0x7E1\nTPM2_PT_MANUFACTURER:\n  raw: 0x4D534654\n  value: \"MSFT\"\n...\n",[52,1614,1615,1620,1625,1630,1635,1640,1645,1650,1655,1660,1665,1670,1675,1680,1685,1690],{"__ignoreMap":95},[118,1616,1617],{"class":120,"line":121},[118,1618,1619],{},"TPM2_PT_FAMILY_INDICATOR:\n",[118,1621,1622],{"class":120,"line":127},[118,1623,1624],{},"  raw: 0x322E3000\n",[118,1626,1627],{"class":120,"line":133},[118,1628,1629],{},"  value: \"2.0\"\n",[118,1631,1632],{"class":120,"line":139},[118,1633,1634],{},"TPM2_PT_LEVEL:\n",[118,1636,1637],{"class":120,"line":145},[118,1638,1639],{},"  raw: 0\n",[118,1641,1642],{"class":120,"line":893},[118,1643,1644],{},"TPM2_PT_REVISION:\n",[118,1646,1647],{"class":120,"line":1168},[118,1648,1649],{},"  raw: 0x8A\n",[118,1651,1652],{"class":120,"line":1176},[118,1653,1654],{},"  value: 1.38\n",[118,1656,1657],{"class":120,"line":1184},[118,1658,1659],{},"TPM2_PT_DAY_OF_YEAR:\n",[118,1661,1662],{"class":120,"line":1195},[118,1663,1664],{},"  raw: 0x19\n",[118,1666,1667],{"class":120,"line":1203},[118,1668,1669],{},"TPM2_PT_YEAR:\n",[118,1671,1672],{"class":120,"line":1209},[118,1673,1674],{},"  raw: 0x7E1\n",[118,1676,1677],{"class":120,"line":1223},[118,1678,1679],{},"TPM2_PT_MANUFACTURER:\n",[118,1681,1682],{"class":120,"line":1229},[118,1683,1684],{},"  raw: 0x4D534654\n",[118,1686,1687],{"class":120,"line":1241},[118,1688,1689],{},"  value: \"MSFT\"\n",[118,1691,1692],{"class":120,"line":1249},[118,1693,1694],{},"...\n",[15,1696,1697],{},"By the output, we can see the TPM manufacturer is Microsoft.",[44,1699,1701],{"id":1700},"_3-extract-certification-from-tpm","3. Extract Certification from TPM",[794,1703,1705],{"id":1704},"_1-normal-tpm","1. Normal TPM",[15,1707,1708],{},"In a normal TPM, we can extract EKCert check if that EK keypair is originated from a real TPM, by verifying EKCert agaist Manufacture's CA public key.",[15,1710,1711],{},"We can extract EKpub from TPM using the following command.",[87,1713,1715],{"className":800,"code":1714,"language":802,"meta":95,"style":95},"$ sudo tpm2_readpublic -c 0x81010001 -o ek.pub\n",[52,1716,1717],{"__ignoreMap":95},[118,1718,1719],{"class":120,"line":121},[118,1720,1714],{},[15,1722,1723],{},"And get the following output:",[87,1725,1728],{"className":1726,"code":1727,"language":92},[90],"name: 000b75e26100d099854a0e6257a0d5d4f536771bfa3c3c6180f67305c42294051e85\nqualified name: 000b8443eb529b3fd108a6d2c13d8f65ffe1d3b37363d8d83487355c5408affb1f2f\nname-alg:\n  value: sha256\n  raw: 0xb\nattributes:\n  value: fixedtpm|fixedparent|sensitivedataorigin|adminwithpolicy|restricted|decrypt\n  raw: 0x300b2\ntype:\n  value: rsa\n  raw: 0x1\nexponent: 65537\nbits: 2048\n...\n",[52,1729,1727],{"__ignoreMap":95},[15,1731,1732],{},"We extracted the vTPM EKpub from the commandline, now we just need to generate EKCert and we can verify it just like a regular TPM.",[15,1734,1735],{},"So I run the following command.",[87,1737,1739],{"className":800,"code":1738,"language":802,"meta":95,"style":95},"$ sudo tpm2_getekcertificate -u ek.pub -o ekcert.pem\nERROR: No EK server address found for manufacturer.\nERROR: Please specify an EK server address on the command line.\nERROR: Unable to run tpm2_getekcertificate\n",[52,1740,1741,1746,1751,1756],{"__ignoreMap":95},[118,1742,1743],{"class":120,"line":121},[118,1744,1745],{},"$ sudo tpm2_getekcertificate -u ek.pub -o ekcert.pem\n",[118,1747,1748],{"class":120,"line":127},[118,1749,1750],{},"ERROR: No EK server address found for manufacturer.\n",[118,1752,1753],{"class":120,"line":133},[118,1754,1755],{},"ERROR: Please specify an EK server address on the command line.\n",[118,1757,1758],{"class":120,"line":139},[118,1759,1760],{},"ERROR: Unable to run tpm2_getekcertificate\n",[15,1762,1763],{},"It did not work, and after a quick Google search I did not find what is Azure's EK server address.",[794,1765,1767],{"id":1766},"_2-azure-tpm","2. Azure TPM",[15,1769,1770],{},"Now I am in a weird position, I cannot use EKCert to verify this tpm is from a real Azure vTPM or not.",[15,1772,1773,1774,1779],{},"After digging through some Azure documents, it is very hard to find information about TPM on Azure. After a while I finally found this page, ",[606,1775,1778],{"href":1776,"rel":1777},"https://learn.microsoft.com/en-us/azure/confidential-computing/guest-attestation-confidential-virtual-machines-design",[610],"Confidential VM Guest Attestation Design Detail",". And it introduced that instead of signing an EKCert, on Azure vTPM, Azure signed an AK and make an AKCert aviliable. And it also included the handle for the AKCert and that AK which can be used to establish as an root of trust.",[1781,1782,1783,1803],"table",{},[1784,1785,1786],"thead",{},[1787,1788,1789,1794,1797,1800],"tr",{},[1790,1791,1793],"th",{"align":1792},"left","Name",[1790,1795,1796],{"align":1792},"NV Index",[1790,1798,1799],{"align":1792},"Size (bytes)",[1790,1801,1802],{"align":1792},"Description",[1804,1805,1806,1821,1835,1849],"tbody",{},[1787,1807,1808,1812,1815,1818],{},[1809,1810,1811],"td",{"align":1792},"Attestation Report",[1809,1813,1814],{"align":1792},"0x01400001",[1809,1816,1817],{"align":1792},"2600",[1809,1819,1820],{"align":1792},"Azure-defined format with the hardware report embedded.",[1787,1822,1823,1826,1829,1832],{},[1809,1824,1825],{"align":1792},"Report Data",[1809,1827,1828],{"align":1792},"0x01400002",[1809,1830,1831],{"align":1792},"64",[1809,1833,1834],{"align":1792},"The report data to be included in the Runtime Data.",[1787,1836,1837,1840,1843,1846],{},[1809,1838,1839],{"align":1792},"vTPM AK Cert",[1809,1841,1842],{"align":1792},"0x01C101D0",[1809,1844,1845],{"align":1792},"4096",[1809,1847,1848],{"align":1792},"The certificate used to verify the TPM Quote signed by the vTPM AK.",[1787,1850,1851,1854,1857,1860],{},[1809,1852,1853],{"align":1792},"vTPM AK",[1809,1855,1856],{"align":1792},"0x81000003",[1809,1858,1859],{"align":1792},"Depending on the key type",[1809,1861,1862],{"align":1792},"The key used to sign the TPM Quote.",[15,1864,1865],{},"Now we finally have something to verify against Azure.",[794,1867,1869],{"id":1868},"_3-extract-akcert","3. Extract AKCert",[15,1871,1872],{},"We can run the following command to use tpm2-tools to extract the signed AKCert.",[87,1874,1876],{"className":800,"code":1875,"language":802,"meta":95,"style":95},"$ sudo tpm2_nvread -C o 0x01C101D0 -o ak_cert.der\n",[52,1877,1878],{"__ignoreMap":95},[118,1879,1880],{"class":120,"line":121},[118,1881,1875],{},[15,1883,1884],{},"This command extracted the AKCert from vTPM to file ak_cert.der. And we can view the information of the certificate like this.",[87,1886,1888],{"className":800,"code":1887,"language":802,"meta":95,"style":95},"$ openssl x509 -inform der -in ak_cert.der -text -noout\n",[52,1889,1890],{"__ignoreMap":95},[118,1891,1892],{"class":120,"line":121},[118,1893,1887],{},[15,1895,1896],{},"We can also convert this der format into pem, for future use",[87,1898,1900],{"className":800,"code":1899,"language":802,"meta":95,"style":95},"$ openssl x509 -inform DER -in ak_cert.der -out ak_cert.pem\n",[52,1901,1902],{"__ignoreMap":95},[118,1903,1904],{"class":120,"line":121},[118,1905,1899],{},[15,1907,1908,1909,40],{},"With the AKCert we can verify it against Microsoft's TPM CA to verify this Cert is signed by Microsoft, ",[606,1910,1913],{"href":1911,"rel":1912},"https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq?tabs=adhoccli%2Ctemplate%2Cdebianbased#certificates",[610],"Microsoft vTPM CA Cert",[15,1915,1916],{},"Download Microsoft's CA, in this case I downloaded the CA3 cer file. It's already in PEM format, we can verify AKCert against it.",[87,1918,1920],{"className":800,"code":1919,"language":802,"meta":95,"style":95},"$ openssl verify -partial_chain -CAfile azure_ca3.cer ak_cert.pem\nak_cert.pem: OK\n",[52,1921,1922,1927],{"__ignoreMap":95},[118,1923,1924],{"class":120,"line":121},[118,1925,1926],{},"$ openssl verify -partial_chain -CAfile azure_ca3.cer ak_cert.pem\n",[118,1928,1929],{"class":120,"line":127},[118,1930,1931],{},"ak_cert.pem: OK\n",[15,1933,1934],{},"Now we can extract the pubkey portion of the Microsft signed AK from this AKCert.",[87,1936,1938],{"className":800,"code":1937,"language":802,"meta":95,"style":95},"$ openssl x509 -inform der -in ak_cert.der -pubkey -noout > ak_pub_from_cert.pem\n",[52,1939,1940],{"__ignoreMap":95},[118,1941,1942],{"class":120,"line":121},[118,1943,1937],{},[15,1945,1946],{},"Now we get the AKpub from the AKCert which is signed by Microsoft.",[794,1948,1950],{"id":1949},"_4-extract-akpub-and-compare","4. Extract AKpub and compare",[15,1952,1953],{},"Run the following command to extract the public part of the AK from vTPM.",[87,1955,1957],{"className":800,"code":1956,"language":802,"meta":95,"style":95},"$ sudo tpm2_readpublic -c 0x81000003 -f pem -o ak_pub.pem\n",[52,1958,1959],{"__ignoreMap":95},[118,1960,1961],{"class":120,"line":121},[118,1962,1956],{},[15,1964,1965],{},"Now we can compare the AK is the same AK which signed by Microsoft.",[87,1967,1969],{"className":800,"code":1968,"language":802,"meta":95,"style":95},"$ cmp -s ak_pub.pem ak_pub_from_cert.pem \n$ echo $?\n0\n",[52,1970,1971,1976,1981],{"__ignoreMap":95},[118,1972,1973],{"class":120,"line":121},[118,1974,1975],{},"$ cmp -s ak_pub.pem ak_pub_from_cert.pem \n",[118,1977,1978],{"class":120,"line":127},[118,1979,1980],{},"$ echo $?\n",[118,1982,1983],{"class":120,"line":133},[118,1984,1985],{},"0\n",[15,1987,1988],{},"These two AKpubs are the same that means we can establish trust of this AK is originated from Microsoft Azure.",[15,1990,1991],{},"However, this does not prove that the VM we are currently interacting with actually possesses the corresponding private key for that public key. So this could be defeated by a classic replay attack.",[15,1993,1994],{},"A malicious actor could:",[181,1996,1997,2000,2003,2006],{},[184,1998,1999],{},"Spin up one legitimate Azure Trusted VM.",[184,2001,2002],{},"Follow this blog's steps to extract the ak_cert.der and ak_pub.pem files. These are public information.",[184,2004,2005],{},"Shut down that VM.",[184,2007,2008],{},"Spin up a malicious VM (on any cloud or on-premise) that has no vTPM at all.",[15,2010,2011],{},"When a verifier asks this malicious VM to prove its identity, it simply sends back the stolen (but valid) ak_cert.der and ak_pub.pem files.",[15,2013,2014],{},"The verifier, following only the steps in this blog post, would run openssl verify and cmp. Both checks would pass, and the verifier would falsely conclude the malicious VM is a genuine Azure VM.",[15,2016,2017],{},"We will introduce challeges to force the VM to use its private AK, that way we can stop the replay attack and we will also continue extend the trust to other key and other part of the system in the next blog post.",[646,2019,648],{},{"title":95,"searchDepth":127,"depth":127,"links":2021},[2022,2023,2024,2025],{"id":698,"depth":127,"text":699},{"id":1533,"depth":127,"text":1534},{"id":1587,"depth":127,"text":1588},{"id":1700,"depth":127,"text":1701},"2025-11-2",{},"/cloud_notes/interact_with_azure_vtpm",{"title":1490,"description":1499},"cloud_notes/interact_with_azure_vtpm","RdK8muBjCdeLd5UsbW7MzDhGHpOv1DIRUw1_JNomriY",{"id":2033,"title":2034,"body":2035,"date":5852,"description":2043,"extension":664,"meta":5853,"navigation":666,"path":5854,"seo":5855,"stem":5856,"__hash__":5857},"content/embedded/ESP32/run_rust_on_app_core.md","Running Bare-Metal Rust Alongside ESP-IDF on the ESP32-S3's Second Core",{"type":8,"value":2036,"toc":5832},[2037,2040,2044,2051,2054,2057,2074,2080,2091,2098,2101,2103,2107,2114,2181,2192,2199,2206,2208,2212,2215,2219,2226,2233,2273,2276,2280,2283,2295,2301,2308,2759,2763,2766,2776,2783,2788,2923,2927,2934,2939,3038,3043,3087,3093,3097,3103,3113,3120,3403,3407,3425,3428,3433,3650,3654,3657,3718,3725,3727,3731,3734,3737,3744,3747,3751,3757,3767,3771,3891,3898,3902,4014,4018,4029,4043,4048,4477,4481,4494,4668,4672,4675,4682,4858,4862,4865,4869,4876,4881,4908,4925,4929,4932,4942,5188,5192,5202,5324,5328,5334,5437,5441,5455,5582,5586,5589,5594,5628,5633,5744,5751,5755,5762,5768,5771,5814,5816,5820,5823,5826,5829],[11,2038,2034],{"id":2039},"running-bare-metal-rust-alongside-esp-idf-on-the-esp32-s3s-second-core",[568,2041,2043],{"id":2042},"building-a-hot-swappable-dual-paradigm-environment-on-espressif-silicon","Building a Hot-Swappable, Dual-Paradigm Environment on Espressif Silicon",[15,2045,2046,2047,2050],{},"I've been working with the RP2350 and ",[52,2048,2049],{},"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.",[15,2052,2053],{},"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.",[15,2055,2056],{},"So I was stuck choosing between two imperfect options:",[181,2058,2059,2068],{},[184,2060,2061,2064,2065,2067],{},[37,2062,2063],{},"Go all-in on Rust."," I'd get the language features and crates I love, but the ",[52,2066,2049],{}," 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.",[184,2069,2070,2073],{},[37,2071,2072],{},"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.",[15,2075,2076,2077],{},"Then I remembered something: ",[37,2078,2079],{},"the ESP32-S3 has two CPU cores.",[15,2081,2082,2083,2086,2087,2090],{},"There's an option buried in ESP-IDF's ",[52,2084,2085],{},"Kconfig"," called ",[52,2088,2089],{},"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?",[15,2092,2093,2094,2097],{},"Both cores share the same memory space, so passing data between them should be straightforward (though it does require some ",[52,2095,2096],{},"unsafe"," Rust). And since Core 1 wouldn't be managed by FreeRTOS, there'd be no scheduler preempting my time-critical audio processing loop.",[15,2099,2100],{},"After convincing myself this wasn't completely insane, I got to work. Here's how it all fits together.",[22,2102],{},[44,2104,2106],{"id":2105},"background-why-not-just-pin-a-freertos-task","Background: Why Not Just Pin a FreeRTOS Task?",[15,2108,2109,2110,2113],{},"Before diving in, it's worth addressing the obvious question: ESP-IDF already provides ",[52,2111,2112],{},"xTaskCreatePinnedToCore",", which can pin a task to a specific core:",[87,2115,2119],{"className":2116,"code":2117,"language":2118,"meta":95,"style":95},"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",[52,2120,2121,2126,2131,2136,2141,2146,2151,2156,2161,2166,2171,2176],{"__ignoreMap":95},[118,2122,2123],{"class":120,"line":121},[118,2124,2125],{},"// FreeRTOS provides this function to create a task on a specific core.\n",[118,2127,2128],{"class":120,"line":127},[118,2129,2130],{},"// You could pin a Rust function to Core 1 this way — but FreeRTOS\n",[118,2132,2133],{"class":120,"line":133},[118,2134,2135],{},"// would still manage the scheduler on that core.\n",[118,2137,2138],{"class":120,"line":139},[118,2139,2140],{},"BaseType_t xTaskCreatePinnedToCore(\n",[118,2142,2143],{"class":120,"line":145},[118,2144,2145],{},"    TaskFunction_t pvTaskCode,       // Function that implements the task\n",[118,2147,2148],{"class":120,"line":893},[118,2149,2150],{},"    const char * const pcName,       // Human-readable name for debugging\n",[118,2152,2153],{"class":120,"line":1168},[118,2154,2155],{},"    const uint32_t usStackDepth,     // Stack size in words (not bytes)\n",[118,2157,2158],{"class":120,"line":1176},[118,2159,2160],{},"    void * const pvParameters,       // Arbitrary pointer passed to the task\n",[118,2162,2163],{"class":120,"line":1184},[118,2164,2165],{},"    UBaseType_t uxPriority,          // Priority (higher = more CPU time)\n",[118,2167,2168],{"class":120,"line":1195},[118,2169,2170],{},"    TaskHandle_t * const pvCreatedTask, // Output: handle to the created task\n",[118,2172,2173],{"class":120,"line":1203},[118,2174,2175],{},"    const BaseType_t xCoreID         // 0 = PRO core, 1 = APP core\n",[118,2177,2178],{"class":120,"line":1209},[118,2179,2180],{},");\n",[15,2182,2183,2184,2187,2188,2191],{},"You could absolutely compile your Rust code as a static library, export a ",[52,2185,2186],{},"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 ",[52,2189,2190],{},".a"," file into the firmware.",[15,2193,2194,2195,2198],{},"The problem is that ",[37,2196,2197],{},"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.",[15,2200,2201,2202,2205],{},"By disabling FreeRTOS on Core 1 entirely (via ",[52,2203,2204],{},"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.",[22,2207],{},[44,2209,2211],{"id":2210},"part-0-statically-linked-rust-on-a-bare-core","Part 0: Statically Linked Rust on a Bare Core",[15,2213,2214],{},"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.",[568,2216,2218],{"id":2217},"step-1-reserve-memory-for-the-bare-metal-core-c-side","Step 1: Reserve Memory for the Bare-Metal Core (C Side)",[15,2220,2221,2222,2225],{},"When Core 1 wakes up outside of FreeRTOS, it doesn't get a dynamically allocated stack from the OS — because there ",[18,2223,2224],{},"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.",[15,2227,2228,2229,2232],{},"ESP-IDF provides the ",[52,2230,2231],{},"SOC_RESERVE_MEMORY_REGION"," macro for exactly this. It tells the bootloader and memory allocator to treat a specific address range as off-limits:",[87,2234,2236],{"className":2116,"code":2235,"language":2118,"meta":95,"style":95},"#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",[52,2237,2238,2243,2248,2253,2258,2263,2268],{"__ignoreMap":95},[118,2239,2240],{"class":120,"line":121},[118,2241,2242],{},"#include \"heap_memory_layout.h\"\n",[118,2244,2245],{"class":120,"line":127},[118,2246,2247],{"emptyLinePlaceholder":666},"\n",[118,2249,2250],{"class":120,"line":133},[118,2251,2252],{},"// Reserve 128KB of internal SRAM for Core 1's stack and data.\n",[118,2254,2255],{"class":120,"line":139},[118,2256,2257],{},"// The two hex values define the start and end addresses of the reserved region.\n",[118,2259,2260],{"class":120,"line":145},[118,2261,2262],{},"// 0x3FCE9710 - 0x3FCC9710 = 0x20000 = 131072 bytes = 128KB.\n",[118,2264,2265],{"class":120,"line":893},[118,2266,2267],{},"// \"rust_app\" is just a label for debugging — it shows up in boot logs.\n",[118,2269,2270],{"class":120,"line":1168},[118,2271,2272],{},"SOC_RESERVE_MEMORY_REGION(0x3FCC9710, 0x3FCE9710, rust_app);\n",[15,2274,2275],{},"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.",[568,2277,2279],{"id":2278},"step-2-wake-up-core-1-from-the-c-side","Step 2: Wake Up Core 1 from the C Side",[15,2281,2282],{},"This is the main ESP-IDF application running on Core 0. Its job is to:",[2284,2285,2286,2289,2292],"ol",{},[184,2287,2288],{},"Set up the system (Wi-Fi, peripherals, etc. — or in our test case, just boot).",[184,2290,2291],{},"Wake up Core 1 and point it at our Rust code.",[184,2293,2294],{},"Go about its normal FreeRTOS business.",[15,2296,2297,2298,2300],{},"Instead of using ",[52,2299,2112],{},", 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.",[15,2302,2303,2304,2307],{},"To verify that everything is working, Core 0 will read a shared counter variable (",[52,2305,2306],{},"RUST_CORE1_COUNTER",") that the Rust code on Core 1 increments in a loop.",[87,2309,2311],{"className":2116,"code":2310,"language":2118,"meta":95,"style":95},"#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",[52,2312,2313,2318,2323,2328,2333,2337,2342,2347,2352,2357,2361,2366,2370,2375,2380,2384,2388,2393,2398,2403,2408,2413,2418,2423,2428,2433,2438,2442,2447,2452,2457,2462,2467,2473,2479,2485,2491,2496,2502,2508,2514,2520,2525,2531,2537,2543,2548,2554,2560,2566,2571,2577,2583,2589,2594,2600,2606,2611,2617,2622,2628,2633,2639,2645,2650,2656,2662,2668,2673,2679,2684,2690,2695,2701,2707,2712,2718,2724,2730,2736,2742,2748,2754],{"__ignoreMap":95},[118,2314,2315],{"class":120,"line":121},[118,2316,2317],{},"#include \u003Cstdio.h>\n",[118,2319,2320],{"class":120,"line":127},[118,2321,2322],{},"#include \u003Cstdint.h>\n",[118,2324,2325],{"class":120,"line":133},[118,2326,2327],{},"#include \"esp_log.h\"\n",[118,2329,2330],{"class":120,"line":139},[118,2331,2332],{},"#include \"esp_cpu.h\"\n",[118,2334,2335],{"class":120,"line":145},[118,2336,2242],{},[118,2338,2339],{"class":120,"line":893},[118,2340,2341],{},"#include \"freertos/FreeRTOS.h\"\n",[118,2343,2344],{"class":120,"line":1168},[118,2345,2346],{},"#include \"freertos/task.h\"\n",[118,2348,2349],{"class":120,"line":1176},[118,2350,2351],{},"#include \"soc/system_reg.h\"\n",[118,2353,2354],{"class":120,"line":1184},[118,2355,2356],{},"#include \"soc/soc.h\"\n",[118,2358,2359],{"class":120,"line":1195},[118,2360,2247],{"emptyLinePlaceholder":666},[118,2362,2363],{"class":120,"line":1203},[118,2364,2365],{},"static const char *TAG = \"rust_app_core\";\n",[118,2367,2368],{"class":120,"line":1209},[118,2369,2247],{"emptyLinePlaceholder":666},[118,2371,2372],{"class":120,"line":1223},[118,2373,2374],{},"// Reserve memory so ESP-IDF's heap allocator doesn't use it.\n",[118,2376,2377],{"class":120,"line":1229},[118,2378,2379],{},"// (Same macro from Step 1 — it must appear in a compiled C file.)\n",[118,2381,2382],{"class":120,"line":1241},[118,2383,2272],{},[118,2385,2386],{"class":120,"line":1249},[118,2387,2247],{"emptyLinePlaceholder":666},[118,2389,2390],{"class":120,"line":1266},[118,2391,2392],{},"// ---- External symbols ----\n",[118,2394,2395],{"class":120,"line":1285},[118,2396,2397],{},"// These are defined in other files and resolved at link time:\n",[118,2399,2400],{"class":120,"line":1291},[118,2401,2402],{},"//   rust_app_core_entry  — the Rust function (from our .a library)\n",[118,2404,2405],{"class":120,"line":1304},[118,2406,2407],{},"//   app_core_trampoline  — tiny assembly stub that sets the stack pointer\n",[118,2409,2410],{"class":120,"line":1310},[118,2411,2412],{},"//   _rust_stack_top      — address from our linker script (top of reserved 128KB)\n",[118,2414,2415],{"class":120,"line":1322},[118,2416,2417],{},"//   ets_set_appcpu_boot_addr — ROM function that tells Core 1 where to start\n",[118,2419,2420],{"class":120,"line":1328},[118,2421,2422],{},"extern void rust_app_core_entry(void);\n",[118,2424,2425],{"class":120,"line":1340},[118,2426,2427],{},"extern void ets_set_appcpu_boot_addr(uint32_t);\n",[118,2429,2430],{"class":120,"line":1347},[118,2431,2432],{},"extern uint32_t _rust_stack_top;\n",[118,2434,2435],{"class":120,"line":1358},[118,2436,2437],{},"extern void app_core_trampoline(void);\n",[118,2439,2440],{"class":120,"line":1364},[118,2441,2247],{"emptyLinePlaceholder":666},[118,2443,2444],{"class":120,"line":1377},[118,2445,2446],{},"/*\n",[118,2448,2449],{"class":120,"line":1388},[118,2450,2451],{}," * Boot Core 1 by directly manipulating ESP32-S3 hardware registers.\n",[118,2453,2454],{"class":120,"line":1395},[118,2455,2456],{}," * This bypasses FreeRTOS entirely — Core 1 will run our code with\n",[118,2458,2459],{"class":120,"line":1407},[118,2460,2461],{}," * no scheduler, no interrupts (unless we set them up), and no OS.\n",[118,2463,2464],{"class":120,"line":1413},[118,2465,2466],{}," */\n",[118,2468,2470],{"class":120,"line":2469},33,[118,2471,2472],{},"static void start_rust_on_app_core(void)\n",[118,2474,2476],{"class":120,"line":2475},34,[118,2477,2478],{},"{\n",[118,2480,2482],{"class":120,"line":2481},35,[118,2483,2484],{},"    ESP_LOGI(TAG, \"Starting Rust on Core 1...\");\n",[118,2486,2488],{"class":120,"line":2487},36,[118,2489,2490],{},"    ESP_LOGI(TAG, \"  Stack: 0x3FCC9710 - 0x3FCE9710 (128K)\");\n",[118,2492,2494],{"class":120,"line":2493},37,[118,2495,2247],{"emptyLinePlaceholder":666},[118,2497,2499],{"class":120,"line":2498},38,[118,2500,2501],{},"    /* 1. Tell Core 1 where to begin executing after it resets.\n",[118,2503,2505],{"class":120,"line":2504},39,[118,2506,2507],{},"     *    This ROM function writes the address into a register that the\n",[118,2509,2511],{"class":120,"line":2510},40,[118,2512,2513],{},"     *    CPU reads on boot. We point it at our assembly trampoline. */\n",[118,2515,2517],{"class":120,"line":2516},41,[118,2518,2519],{},"    ets_set_appcpu_boot_addr((uint32_t)app_core_trampoline);\n",[118,2521,2523],{"class":120,"line":2522},42,[118,2524,2247],{"emptyLinePlaceholder":666},[118,2526,2528],{"class":120,"line":2527},43,[118,2529,2530],{},"    /* 2. Hardware-level wake-up sequence for Core 1.\n",[118,2532,2534],{"class":120,"line":2533},44,[118,2535,2536],{},"     *    These register writes control the clock, stall, and reset\n",[118,2538,2540],{"class":120,"line":2539},45,[118,2541,2542],{},"     *    signals for the second CPU core. */\n",[118,2544,2546],{"class":120,"line":2545},46,[118,2547,2247],{"emptyLinePlaceholder":666},[118,2549,2551],{"class":120,"line":2550},47,[118,2552,2553],{},"    // Enable the clock gate — Core 1 can't run without a clock signal.\n",[118,2555,2557],{"class":120,"line":2556},48,[118,2558,2559],{},"    SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n",[118,2561,2563],{"class":120,"line":2562},49,[118,2564,2565],{},"                      SYSTEM_CONTROL_CORE_1_CLKGATE_EN);\n",[118,2567,2569],{"class":120,"line":2568},50,[118,2570,2247],{"emptyLinePlaceholder":666},[118,2572,2574],{"class":120,"line":2573},51,[118,2575,2576],{},"    // Clear the RUNSTALL bit. While stalled, the core is frozen mid-instruction.\n",[118,2578,2580],{"class":120,"line":2579},52,[118,2581,2582],{},"    CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,\n",[118,2584,2586],{"class":120,"line":2585},53,[118,2587,2588],{},"                        SYSTEM_CONTROL_CORE_1_RUNSTALL);\n",[118,2590,2592],{"class":120,"line":2591},54,[118,2593,2247],{"emptyLinePlaceholder":666},[118,2595,2597],{"class":120,"line":2596},55,[118,2598,2599],{},"    // Pulse the reset line: assert it, then immediately de-assert.\n",[118,2601,2603],{"class":120,"line":2602},56,[118,2604,2605],{},"    // This causes Core 1 to reboot and jump to the address we set above.\n",[118,2607,2609],{"class":120,"line":2608},57,[118,2610,2559],{},[118,2612,2614],{"class":120,"line":2613},58,[118,2615,2616],{},"                      SYSTEM_CONTROL_CORE_1_RESETING);\n",[118,2618,2620],{"class":120,"line":2619},59,[118,2621,2582],{},[118,2623,2625],{"class":120,"line":2624},60,[118,2626,2627],{},"                        SYSTEM_CONTROL_CORE_1_RESETING);\n",[118,2629,2631],{"class":120,"line":2630},61,[118,2632,2247],{"emptyLinePlaceholder":666},[118,2634,2636],{"class":120,"line":2635},62,[118,2637,2638],{},"    ESP_LOGI(TAG, \"Core 1 released\");\n",[118,2640,2642],{"class":120,"line":2641},63,[118,2643,2644],{},"}\n",[118,2646,2648],{"class":120,"line":2647},64,[118,2649,2247],{"emptyLinePlaceholder":666},[118,2651,2653],{"class":120,"line":2652},65,[118,2654,2655],{},"// This counter lives in the Rust code. Because it's an AtomicU32 with\n",[118,2657,2659],{"class":120,"line":2658},66,[118,2660,2661],{},"// #[no_mangle], the C linker can find it by this exact name.\n",[118,2663,2665],{"class":120,"line":2664},67,[118,2666,2667],{},"extern volatile uint32_t RUST_CORE1_COUNTER;\n",[118,2669,2671],{"class":120,"line":2670},68,[118,2672,2247],{"emptyLinePlaceholder":666},[118,2674,2676],{"class":120,"line":2675},69,[118,2677,2678],{},"void app_main(void)\n",[118,2680,2682],{"class":120,"line":2681},70,[118,2683,2478],{},[118,2685,2687],{"class":120,"line":2686},71,[118,2688,2689],{},"    ESP_LOGI(TAG, \"Core 0: Starting IDF app\");\n",[118,2691,2693],{"class":120,"line":2692},72,[118,2694,2247],{"emptyLinePlaceholder":666},[118,2696,2698],{"class":120,"line":2697},73,[118,2699,2700],{},"    // Wake up Core 1 and start the Rust code\n",[118,2702,2704],{"class":120,"line":2703},74,[118,2705,2706],{},"    start_rust_on_app_core();\n",[118,2708,2710],{"class":120,"line":2709},75,[118,2711,2247],{"emptyLinePlaceholder":666},[118,2713,2715],{"class":120,"line":2714},76,[118,2716,2717],{},"    // Core 0 continues running FreeRTOS as normal.\n",[118,2719,2721],{"class":120,"line":2720},77,[118,2722,2723],{},"    // Here we just monitor the shared counter to prove both cores are alive.\n",[118,2725,2727],{"class":120,"line":2726},78,[118,2728,2729],{},"    while (1)\n",[118,2731,2733],{"class":120,"line":2732},79,[118,2734,2735],{},"    {\n",[118,2737,2739],{"class":120,"line":2738},80,[118,2740,2741],{},"        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)RUST_CORE1_COUNTER);\n",[118,2743,2745],{"class":120,"line":2744},81,[118,2746,2747],{},"        vTaskDelay(pdMS_TO_TICKS(1000)); // Print once per second\n",[118,2749,2751],{"class":120,"line":2750},82,[118,2752,2753],{},"    }\n",[118,2755,2757],{"class":120,"line":2756},83,[118,2758,2644],{},[568,2760,2762],{"id":2761},"step-3-the-assembly-trampoline","Step 3: The Assembly Trampoline",[15,2764,2765],{},"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.",[15,2767,2768,2769,2772,2773,2775],{},"The ESP32-S3 uses the Xtensa instruction set architecture, where register ",[52,2770,2771],{},"a1"," serves as the stack pointer. Our tiny assembly stub loads the address of our reserved memory into ",[52,2774,2771],{},", then jumps into Rust. That's all it does — just two instructions.",[15,2777,2778,2779,2782],{},"We place this code in the ",[52,2780,2781],{},".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.",[15,2784,2785],{},[37,2786,2787],{},"app_core_trampoline.S",[87,2789,2793],{"className":2790,"code":2791,"language":2792,"meta":95,"style":95},"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",[52,2794,2795,2799,2804,2809,2814,2819,2823,2828,2833,2837,2841,2846,2851,2856,2861,2865,2870,2875,2880,2885,2890,2894,2899,2904,2909,2914,2918],{"__ignoreMap":95},[118,2796,2797],{"class":120,"line":121},[118,2798,2446],{},[118,2800,2801],{"class":120,"line":127},[118,2802,2803],{}," * app_core_trampoline.S\n",[118,2805,2806],{"class":120,"line":133},[118,2807,2808],{}," *\n",[118,2810,2811],{"class":120,"line":139},[118,2812,2813],{}," * Minimal startup code for Core 1. Sets the stack pointer to our\n",[118,2815,2816],{"class":120,"line":145},[118,2817,2818],{}," * reserved memory region, then jumps to the Rust entry point.\n",[118,2820,2821],{"class":120,"line":893},[118,2822,2808],{},[118,2824,2825],{"class":120,"line":1168},[118,2826,2827],{}," * Placed in IRAM (.iram1) so it's available immediately after core\n",[118,2829,2830],{"class":120,"line":1176},[118,2831,2832],{}," * reset, before flash cache is configured.\n",[118,2834,2835],{"class":120,"line":1184},[118,2836,2466],{},[118,2838,2839],{"class":120,"line":1195},[118,2840,2247],{"emptyLinePlaceholder":666},[118,2842,2843],{"class":120,"line":1203},[118,2844,2845],{},"    .section .iram1, \"ax\"       /* \"ax\" = allocatable + executable */\n",[118,2847,2848],{"class":120,"line":1209},[118,2849,2850],{},"    .global  app_core_trampoline\n",[118,2852,2853],{"class":120,"line":1223},[118,2854,2855],{},"    .type    app_core_trampoline, @function\n",[118,2857,2858],{"class":120,"line":1229},[118,2859,2860],{},"    .align   4                  /* Xtensa requires 4-byte alignment */\n",[118,2862,2863],{"class":120,"line":1241},[118,2864,2247],{"emptyLinePlaceholder":666},[118,2866,2867],{"class":120,"line":1249},[118,2868,2869],{},"app_core_trampoline:\n",[118,2871,2872],{"class":120,"line":1266},[118,2873,2874],{},"    /* Load the top of our 128KB reserved stack into register a1.\n",[118,2876,2877],{"class":120,"line":1285},[118,2878,2879],{},"     * Stacks grow downward on Xtensa, so \"top\" means the highest\n",[118,2881,2882],{"class":120,"line":1291},[118,2883,2884],{},"     * address — the stack will grow toward lower addresses from here. */\n",[118,2886,2887],{"class":120,"line":1304},[118,2888,2889],{},"    movi  a1, _rust_stack_top\n",[118,2891,2892],{"class":120,"line":1310},[118,2893,2247],{"emptyLinePlaceholder":666},[118,2895,2896],{"class":120,"line":1322},[118,2897,2898],{},"    /* Jump to the Rust entry function. call0 is a \"windowless\" call\n",[118,2900,2901],{"class":120,"line":1328},[118,2902,2903],{},"     * (no register window rotation), suitable for bare-metal startup.\n",[118,2905,2906],{"class":120,"line":1340},[118,2907,2908],{},"     * This function never returns — it contains an infinite loop. */\n",[118,2910,2911],{"class":120,"line":1347},[118,2912,2913],{},"    call0 rust_app_core_entry\n",[118,2915,2916],{"class":120,"line":1358},[118,2917,2247],{"emptyLinePlaceholder":666},[118,2919,2920],{"class":120,"line":1364},[118,2921,2922],{},"    .size app_core_trampoline, . - app_core_trampoline\n",[568,2924,2926],{"id":2925},"step-4-gluing-it-together-with-cmake-and-a-linker-script","Step 4: Gluing It Together with CMake and a Linker Script",[15,2928,2929,2930,2933],{},"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 ",[52,2931,2932],{},"_rust_stack_top"," lives.",[15,2935,2936],{},[37,2937,2938],{},"CMakeLists.txt",[87,2940,2944],{"className":2941,"code":2942,"language":2943,"meta":95,"style":95},"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",[52,2945,2946,2951,2956,2961,2966,2971,2976,2980,2985,2990,2995,2999,3004,3009,3014,3018,3023,3028,3033],{"__ignoreMap":95},[118,2947,2948],{"class":120,"line":121},[118,2949,2950],{},"# Register our C source and the assembly trampoline as component sources.\n",[118,2952,2953],{"class":120,"line":127},[118,2954,2955],{},"# ESP-IDF builds each directory under \"main/\" as a \"component.\"\n",[118,2957,2958],{"class":120,"line":133},[118,2959,2960],{},"idf_component_register(\n",[118,2962,2963],{"class":120,"line":139},[118,2964,2965],{},"    SRCS \"main.c\" \"app_core_trampoline.S\"\n",[118,2967,2968],{"class":120,"line":145},[118,2969,2970],{},"    INCLUDE_DIRS \".\"\n",[118,2972,2973],{"class":120,"line":893},[118,2974,2975],{},")\n",[118,2977,2978],{"class":120,"line":1168},[118,2979,2247],{"emptyLinePlaceholder":666},[118,2981,2982],{"class":120,"line":1176},[118,2983,2984],{},"# Tell the linker about our pre-compiled Rust static library.\n",[118,2986,2987],{"class":120,"line":1184},[118,2988,2989],{},"# This .a file is produced by `cargo build` and copied into main/lib/.\n",[118,2991,2992],{"class":120,"line":1195},[118,2993,2994],{},"add_prebuilt_library(rust_app \"${CMAKE_CURRENT_SOURCE_DIR}/lib/libesp_rust_app.a\")\n",[118,2996,2997],{"class":120,"line":1203},[118,2998,2247],{"emptyLinePlaceholder":666},[118,3000,3001],{"class":120,"line":1209},[118,3002,3003],{},"# Link the Rust library into our component. INTERFACE means anything\n",[118,3005,3006],{"class":120,"line":1223},[118,3007,3008],{},"# that depends on this component also gets the Rust symbols.\n",[118,3010,3011],{"class":120,"line":1229},[118,3012,3013],{},"target_link_libraries(${COMPONENT_LIB} INTERFACE rust_app)\n",[118,3015,3016],{"class":120,"line":1241},[118,3017,2247],{"emptyLinePlaceholder":666},[118,3019,3020],{"class":120,"line":1249},[118,3021,3022],{},"# Inject our custom linker script. This is how the assembly trampoline\n",[118,3024,3025],{"class":120,"line":1266},[118,3026,3027],{},"# knows the numeric value of _rust_stack_top.\n",[118,3029,3030],{"class":120,"line":1285},[118,3031,3032],{},"target_link_options(${COMPONENT_LIB}\n",[118,3034,3035],{"class":120,"line":1291},[118,3036,3037],{},"    INTERFACE \"-T${CMAKE_CURRENT_SOURCE_DIR}/rust_stack.ld\")\n",[15,3039,3040],{},[37,3041,3042],{},"rust_stack.ld",[87,3044,3048],{"className":3045,"code":3046,"language":3047,"meta":95,"style":95},"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",[52,3049,3050,3054,3059,3063,3068,3073,3078,3082],{"__ignoreMap":95},[118,3051,3052],{"class":120,"line":121},[118,3053,2446],{},[118,3055,3056],{"class":120,"line":127},[118,3057,3058],{}," * Custom linker script fragment.\n",[118,3060,3061],{"class":120,"line":133},[118,3062,2808],{},[118,3064,3065],{"class":120,"line":139},[118,3066,3067],{}," * Defines _rust_stack_top as the END of our reserved 128KB block.\n",[118,3069,3070],{"class":120,"line":145},[118,3071,3072],{}," * Stacks grow downward, so the \"top\" is the highest address.\n",[118,3074,3075],{"class":120,"line":893},[118,3076,3077],{}," * The assembly trampoline loads this value into register a1.\n",[118,3079,3080],{"class":120,"line":1168},[118,3081,2466],{},[118,3083,3084],{"class":120,"line":1176},[118,3085,3086],{},"_rust_stack_top = 0x3FCE9710;\n",[15,3088,3089,3090,3092],{},"The connection here is: the linker script provides a symbol (",[52,3091,2932],{},") → 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.",[568,3094,3096],{"id":3095},"step-5-the-bare-metal-rust-application","Step 5: The Bare-Metal Rust Application",[15,3098,3099,3100,3102],{},"Finally, here's the code that actually runs on Core 1. It's entirely ",[52,3101,2049],{}," — there's no operating system, no allocator, no standard library. Just raw hardware access.",[15,3104,3105,3106,3109,3110,3112],{},"The key technique here is ",[52,3107,3108],{},"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 ",[52,3111,3108],{}," for our shared counter, we avoid race conditions without needing a mutex (which wouldn't work easily across the OS/bare-metal boundary anyway).",[15,3114,3115,3116,3119],{},"The ",[52,3117,3118],{},"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.",[87,3121,3123],{"className":112,"code":3122,"language":114,"meta":95,"style":95},"// 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",[52,3124,3125,3130,3135,3140,3144,3149,3154,3159,3163,3168,3173,3177,3182,3187,3192,3197,3202,3207,3212,3217,3221,3225,3230,3235,3240,3245,3250,3255,3260,3264,3269,3274,3279,3284,3288,3293,3298,3303,3307,3312,3317,3321,3326,3331,3336,3341,3346,3351,3356,3360,3365,3370,3375,3380,3385,3390,3395,3399],{"__ignoreMap":95},[118,3126,3127],{"class":120,"line":121},[118,3128,3129],{},"// no_std: we're running without the Rust standard library.\n",[118,3131,3132],{"class":120,"line":127},[118,3133,3134],{},"// There's no OS below us — no heap, no threads, no println!.\n",[118,3136,3137],{"class":120,"line":133},[118,3138,3139],{},"#![no_std]\n",[118,3141,3142],{"class":120,"line":139},[118,3143,2247],{"emptyLinePlaceholder":666},[118,3145,3146],{"class":120,"line":145},[118,3147,3148],{},"// no_main: we don't use Rust's normal main() entry point.\n",[118,3150,3151],{"class":120,"line":893},[118,3152,3153],{},"// Instead, Core 1 enters via rust_app_core_entry(), called from assembly.\n",[118,3155,3156],{"class":120,"line":1168},[118,3157,3158],{},"#![no_main]\n",[118,3160,3161],{"class":120,"line":1176},[118,3162,2247],{"emptyLinePlaceholder":666},[118,3164,3165],{"class":120,"line":1184},[118,3166,3167],{},"use core::panic::PanicInfo;\n",[118,3169,3170],{"class":120,"line":1195},[118,3171,3172],{},"use core::sync::atomic::{AtomicU32, Ordering};\n",[118,3174,3175],{"class":120,"line":1203},[118,3176,2247],{"emptyLinePlaceholder":666},[118,3178,3179],{"class":120,"line":1209},[118,3180,3181],{},"// Every no_std binary needs a panic handler. When something goes wrong\n",[118,3183,3184],{"class":120,"line":1223},[118,3185,3186],{},"// (array out of bounds, unwrap on None, etc.), this function is called.\n",[118,3188,3189],{"class":120,"line":1229},[118,3190,3191],{},"// On a bare-metal core with no debugger attached, there's not much we\n",[118,3193,3194],{"class":120,"line":1241},[118,3195,3196],{},"// can do — so we just loop forever. A production system might toggle\n",[118,3198,3199],{"class":120,"line":1249},[118,3200,3201],{},"// an LED or write to a shared error flag that Core 0 can read.\n",[118,3203,3204],{"class":120,"line":1266},[118,3205,3206],{},"#[panic_handler]\n",[118,3208,3209],{"class":120,"line":1285},[118,3210,3211],{},"fn panic(_info: &PanicInfo) -> ! {\n",[118,3213,3214],{"class":120,"line":1291},[118,3215,3216],{},"    loop {}\n",[118,3218,3219],{"class":120,"line":1304},[118,3220,2644],{},[118,3222,3223],{"class":120,"line":1310},[118,3224,2247],{"emptyLinePlaceholder":666},[118,3226,3227],{"class":120,"line":1322},[118,3228,3229],{},"// The shared counter. Both cores can see this variable because it lives\n",[118,3231,3232],{"class":120,"line":1328},[118,3233,3234],{},"// in the same memory space.\n",[118,3236,3237],{"class":120,"line":1340},[118,3238,3239],{},"//\n",[118,3241,3242],{"class":120,"line":1347},[118,3243,3244],{},"// #[unsafe(no_mangle)] prevents Rust from renaming this symbol during\n",[118,3246,3247],{"class":120,"line":1358},[118,3248,3249],{},"// compilation. Without it, Rust would generate something like\n",[118,3251,3252],{"class":120,"line":1364},[118,3253,3254],{},"// \"_ZN12esp_rust_app18RUST_CORE1_COUNTER17h...\" — and the C code\n",[118,3256,3257],{"class":120,"line":1377},[118,3258,3259],{},"// wouldn't be able to find it by name.\n",[118,3261,3262],{"class":120,"line":1388},[118,3263,3239],{},[118,3265,3266],{"class":120,"line":1395},[118,3267,3268],{},"// AtomicU32 ensures that reads and writes are atomic at the CPU level,\n",[118,3270,3271],{"class":120,"line":1407},[118,3272,3273],{},"// so Core 0 will never see a \"torn\" (half-written) value.\n",[118,3275,3276],{"class":120,"line":1413},[118,3277,3278],{},"#[unsafe(no_mangle)]\n",[118,3280,3281],{"class":120,"line":2469},[118,3282,3283],{},"pub static RUST_CORE1_COUNTER: AtomicU32 = AtomicU32::new(0);\n",[118,3285,3286],{"class":120,"line":2475},[118,3287,2247],{"emptyLinePlaceholder":666},[118,3289,3290],{"class":120,"line":2481},[118,3291,3292],{},"// The entry point called by the assembly trampoline after it sets\n",[118,3294,3295],{"class":120,"line":2487},[118,3296,3297],{},"// up the stack pointer. The `-> !` return type means \"this function\n",[118,3299,3300],{"class":120,"line":2493},[118,3301,3302],{},"// never returns\" — it runs an infinite loop.\n",[118,3304,3305],{"class":120,"line":2498},[118,3306,3239],{},[118,3308,3309],{"class":120,"line":2504},[118,3310,3311],{},"// `extern \"C\"` uses the C calling convention so the assembly code\n",[118,3313,3314],{"class":120,"line":2510},[118,3315,3316],{},"// (and the C linker) can call this function correctly.\n",[118,3318,3319],{"class":120,"line":2516},[118,3320,3278],{},[118,3322,3323],{"class":120,"line":2522},[118,3324,3325],{},"pub extern \"C\" fn rust_app_core_entry() -> ! {\n",[118,3327,3328],{"class":120,"line":2527},[118,3329,3330],{},"    loop {\n",[118,3332,3333],{"class":120,"line":2533},[118,3334,3335],{},"        // Atomically increment the counter by 1.\n",[118,3337,3338],{"class":120,"line":2539},[118,3339,3340],{},"        // Ordering::Relaxed means we don't need any memory ordering\n",[118,3342,3343],{"class":120,"line":2545},[118,3344,3345],{},"        // guarantees beyond the atomicity of this single operation.\n",[118,3347,3348],{"class":120,"line":2550},[118,3349,3350],{},"        // (For a simple counter, Relaxed is sufficient.)\n",[118,3352,3353],{"class":120,"line":2556},[118,3354,3355],{},"        RUST_CORE1_COUNTER.fetch_add(1, Ordering::Relaxed);\n",[118,3357,3358],{"class":120,"line":2562},[118,3359,2247],{"emptyLinePlaceholder":666},[118,3361,3362],{"class":120,"line":2568},[118,3363,3364],{},"        // Busy-wait loop as a simple delay. spin_loop() is a CPU hint\n",[118,3366,3367],{"class":120,"line":2573},[118,3368,3369],{},"        // that says \"I'm spinning, not doing real work\" — on some\n",[118,3371,3372],{"class":120,"line":2579},[118,3373,3374],{},"        // architectures this saves power or avoids starving other\n",[118,3376,3377],{"class":120,"line":2585},[118,3378,3379],{},"        // hardware threads.\n",[118,3381,3382],{"class":120,"line":2591},[118,3383,3384],{},"        for _ in 0..1_000_000 {\n",[118,3386,3387],{"class":120,"line":2596},[118,3388,3389],{},"            core::hint::spin_loop();\n",[118,3391,3392],{"class":120,"line":2602},[118,3393,3394],{},"        }\n",[118,3396,3397],{"class":120,"line":2608},[118,3398,2753],{},[118,3400,3401],{"class":120,"line":2613},[118,3402,2644],{},[568,3404,3406],{"id":3405},"step-6-configuring-the-rust-build-cargotoml","Step 6: Configuring the Rust Build (Cargo.toml)",[15,3408,3409,3410,3412,3413,3416,3417,3420,3421,3424],{},"ESP-IDF's build system expects a standard C-compatible static archive (",[52,3411,2190],{}," file). By default, ",[52,3414,3415],{},"cargo build"," produces Rust-specific ",[52,3418,3419],{},".rlib"," files that only the Rust toolchain understands. We need to tell Cargo to output a ",[52,3422,3423],{},"staticlib"," instead.",[15,3426,3427],{},"We also apply aggressive size optimizations — on a microcontroller with limited flash, every kilobyte matters.",[15,3429,3430],{},[37,3431,3432],{},"Cargo.toml",[87,3434,3438],{"className":3435,"code":3436,"language":3437,"meta":95,"style":95},"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",[52,3439,3440,3445,3450,3455,3460,3465,3469,3474,3479,3484,3489,3494,3498,3503,3508,3513,3518,3523,3528,3533,3538,3542,3547,3552,3557,3562,3567,3571,3576,3581,3586,3591,3596,3601,3606,3611,3615,3620,3625,3630,3635,3640,3645],{"__ignoreMap":95},[118,3441,3442],{"class":120,"line":121},[118,3443,3444],{},"[package]\n",[118,3446,3447],{"class":120,"line":127},[118,3448,3449],{},"edition      = \"2024\"\n",[118,3451,3452],{"class":120,"line":133},[118,3453,3454],{},"name         = \"esp_rust_app\"\n",[118,3456,3457],{"class":120,"line":139},[118,3458,3459],{},"rust-version = \"1.88\"\n",[118,3461,3462],{"class":120,"line":145},[118,3463,3464],{},"version      = \"0.1.0\"\n",[118,3466,3467],{"class":120,"line":893},[118,3468,2247],{"emptyLinePlaceholder":666},[118,3470,3471],{"class":120,"line":1168},[118,3472,3473],{},"# Output a C-compatible static library (.a file).\n",[118,3475,3476],{"class":120,"line":1176},[118,3477,3478],{},"# This is what lets us link Rust code into an ESP-IDF project\n",[118,3480,3481],{"class":120,"line":1184},[118,3482,3483],{},"# the same way you'd link any C library.\n",[118,3485,3486],{"class":120,"line":1195},[118,3487,3488],{},"[lib]\n",[118,3490,3491],{"class":120,"line":1203},[118,3492,3493],{},"crate-type = [\"staticlib\"]\n",[118,3495,3496],{"class":120,"line":1209},[118,3497,2247],{"emptyLinePlaceholder":666},[118,3499,3500],{"class":120,"line":1223},[118,3501,3502],{},"[dependencies]\n",[118,3504,3505],{"class":120,"line":1229},[118,3506,3507],{},"# esp-hal provides low-level hardware access for the ESP32-S3.\n",[118,3509,3510],{"class":120,"line":1241},[118,3511,3512],{},"# Even though we're not using most of its features yet, it sets up\n",[118,3514,3515],{"class":120,"line":1249},[118,3516,3517],{},"# the critical-section implementation we need for atomics.\n",[118,3519,3520],{"class":120,"line":1266},[118,3521,3522],{},"esp-hal = { version = \"~1.0\", features = [\"esp32s3\"] }\n",[118,3524,3525],{"class":120,"line":1285},[118,3526,3527],{},"# Provides the critical-section implementation needed for safe\n",[118,3529,3530],{"class":120,"line":1291},[118,3531,3532],{},"# interrupt handling in no_std environments.\n",[118,3534,3535],{"class":120,"line":1304},[118,3536,3537],{},"critical-section = \"1.2.0\"\n",[118,3539,3540],{"class":120,"line":1310},[118,3541,2247],{"emptyLinePlaceholder":666},[118,3543,3544],{"class":120,"line":1322},[118,3545,3546],{},"[profile.dev]\n",[118,3548,3549],{"class":120,"line":1328},[118,3550,3551],{},"# Rust's default debug builds are unoptimized and produce huge binaries.\n",[118,3553,3554],{"class":120,"line":1340},[118,3555,3556],{},"# On embedded, even dev builds should use \"s\" (optimize for size) to\n",[118,3558,3559],{"class":120,"line":1347},[118,3560,3561],{},"# keep things manageable. Without this, you might overflow flash.\n",[118,3563,3564],{"class":120,"line":1358},[118,3565,3566],{},"opt-level = \"s\"\n",[118,3568,3569],{"class":120,"line":1364},[118,3570,2247],{"emptyLinePlaceholder":666},[118,3572,3573],{"class":120,"line":1377},[118,3574,3575],{},"[profile.release]\n",[118,3577,3578],{"class":120,"line":1388},[118,3579,3580],{},"# Force the compiler to use a single codegen unit. This is slower to\n",[118,3582,3583],{"class":120,"line":1395},[118,3584,3585],{},"# compile, but allows LLVM to see the entire crate at once and perform\n",[118,3587,3588],{"class":120,"line":1407},[118,3589,3590],{},"# better cross-function optimizations (inlining, dead code elimination).\n",[118,3592,3593],{"class":120,"line":1413},[118,3594,3595],{},"codegen-units    = 1\n",[118,3597,3598],{"class":120,"line":2469},[118,3599,3600],{},"debug            = 2     # Keep debug symbols (useful for GDB on-device)\n",[118,3602,3603],{"class":120,"line":2475},[118,3604,3605],{},"debug-assertions = false # Disable assert!() checks in release\n",[118,3607,3608],{"class":120,"line":2481},[118,3609,3610],{},"incremental      = false # Disable incremental compilation for cleaner builds\n",[118,3612,3613],{"class":120,"line":2487},[118,3614,2247],{"emptyLinePlaceholder":666},[118,3616,3617],{"class":120,"line":2493},[118,3618,3619],{},"# \"fat\" Link-Time Optimization. The linker analyzes ALL code (including\n",[118,3621,3622],{"class":120,"line":2498},[118,3623,3624],{},"# dependencies) as a single unit, aggressively removing unused functions\n",[118,3626,3627],{"class":120,"line":2504},[118,3628,3629],{},"# and inlining across crate boundaries. This can dramatically reduce\n",[118,3631,3632],{"class":120,"line":2510},[118,3633,3634],{},"# binary size — often 30-50% smaller than without LTO.\n",[118,3636,3637],{"class":120,"line":2516},[118,3638,3639],{},"lto              = 'fat'\n",[118,3641,3642],{"class":120,"line":2522},[118,3643,3644],{},"opt-level        = 's'   # Optimize for size over speed\n",[118,3646,3647],{"class":120,"line":2527},[118,3648,3649],{},"overflow-checks  = false # Disable integer overflow checks in release\n",[568,3651,3653],{"id":3652},"building-and-testing","Building and Testing",[15,3655,3656],{},"Build the Rust library, then copy it into the ESP-IDF project:",[87,3658,3662],{"className":3659,"code":3660,"language":3661,"meta":95,"style":95},"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",[52,3663,3664,3669,3674,3692,3696,3701,3713],{"__ignoreMap":95},[118,3665,3666],{"class":120,"line":121},[118,3667,3668],{"class":1135},"# Build the Rust code targeting the ESP32-S3's Xtensa CPU.\n",[118,3670,3671],{"class":120,"line":127},[118,3672,3673],{"class":1135},"# This produces a .a file in target/xtensa-esp32s3-none-elf/release/\n",[118,3675,3676,3680,3683,3686,3689],{"class":120,"line":133},[118,3677,3679],{"class":3678},"sBMFI","cargo",[118,3681,3682],{"class":1113}," build",[118,3684,3685],{"class":1113}," --release",[118,3687,3688],{"class":1113}," --target",[118,3690,3691],{"class":1113}," xtensa-esp32s3-none-elf\n",[118,3693,3694],{"class":120,"line":139},[118,3695,2247],{"emptyLinePlaceholder":666},[118,3697,3698],{"class":120,"line":145},[118,3699,3700],{"class":1135},"# Copy the compiled library to where our CMakeLists.txt expects it.\n",[118,3702,3703,3706,3709],{"class":120,"line":893},[118,3704,3705],{"class":3678},"cp",[118,3707,3708],{"class":1113}," target/xtensa-esp32s3-none-elf/release/libesp_rust_app.a",[118,3710,3712],{"class":3711},"sTEyZ"," \\\n",[118,3714,3715],{"class":120,"line":1168},[118,3716,3717],{"class":1113},"   /path/to/idf-project/main/lib/\n",[15,3719,3720,3721,3724],{},"Then build and flash the ESP-IDF project as usual (",[52,3722,3723],{},"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.",[22,3726],{},[44,3728,3730],{"id":3729},"part-1-loading-rust-at-runtime-hot-swappable-programs","Part 1: Loading Rust at Runtime (Hot-Swappable Programs)",[15,3732,3733],{},"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.",[15,3735,3736],{},"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.",[15,3738,3739,3740,3743],{},"This is especially useful if the Rust code is ",[18,3741,3742],{},"user-provided content"," — for example, a customizable audio processing pipeline that end users can update.",[15,3745,3746],{},"To make this work, we need to change several things.",[568,3748,3750],{"id":3749},"step-1-build-rust-as-a-standalone-binary","Step 1: Build Rust as a Standalone Binary",[15,3752,3753,3754,3756],{},"In Part 0, Cargo built a static library (",[52,3755,2190],{}," 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.",[15,3758,3759,3760,3763,3764,3766],{},"First, remove the ",[52,3761,3762],{},"[lib]"," section from ",[52,3765,3432],{}," so Cargo builds a binary instead of a library:",[15,3768,3769],{},[37,3770,3432],{},[87,3772,3774],{"className":3435,"code":3773,"language":3437,"meta":95,"style":95},"[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",[52,3775,3776,3780,3784,3788,3792,3796,3800,3805,3810,3814,3818,3822,3826,3830,3834,3839,3844,3848,3852,3856,3861,3866,3871,3876,3881,3886],{"__ignoreMap":95},[118,3777,3778],{"class":120,"line":121},[118,3779,3444],{},[118,3781,3782],{"class":120,"line":127},[118,3783,3449],{},[118,3785,3786],{"class":120,"line":133},[118,3787,3454],{},[118,3789,3790],{"class":120,"line":139},[118,3791,3459],{},[118,3793,3794],{"class":120,"line":145},[118,3795,3464],{},[118,3797,3798],{"class":120,"line":893},[118,3799,2247],{"emptyLinePlaceholder":666},[118,3801,3802],{"class":120,"line":1168},[118,3803,3804],{},"# No [lib] section — we want a standalone binary, not a library.\n",[118,3806,3807],{"class":120,"line":1176},[118,3808,3809],{},"# Cargo will look for src/main.rs as the entry point.\n",[118,3811,3812],{"class":120,"line":1184},[118,3813,2247],{"emptyLinePlaceholder":666},[118,3815,3816],{"class":120,"line":1195},[118,3817,3502],{},[118,3819,3820],{"class":120,"line":1203},[118,3821,3522],{},[118,3823,3824],{"class":120,"line":1209},[118,3825,3537],{},[118,3827,3828],{"class":120,"line":1223},[118,3829,2247],{"emptyLinePlaceholder":666},[118,3831,3832],{"class":120,"line":1229},[118,3833,3546],{},[118,3835,3836],{"class":120,"line":1241},[118,3837,3838],{},"# Even dev builds need size optimization on embedded — unoptimized Rust\n",[118,3840,3841],{"class":120,"line":1249},[118,3842,3843],{},"# produces enormous binaries that won't fit in flash.\n",[118,3845,3846],{"class":120,"line":1266},[118,3847,3566],{},[118,3849,3850],{"class":120,"line":1285},[118,3851,2247],{"emptyLinePlaceholder":666},[118,3853,3854],{"class":120,"line":1291},[118,3855,3575],{},[118,3857,3858],{"class":120,"line":1304},[118,3859,3860],{},"codegen-units    = 1     # Single codegen unit for best LLVM optimization\n",[118,3862,3863],{"class":120,"line":1310},[118,3864,3865],{},"debug            = 2\n",[118,3867,3868],{"class":120,"line":1322},[118,3869,3870],{},"debug-assertions = false\n",[118,3872,3873],{"class":120,"line":1328},[118,3874,3875],{},"incremental      = false\n",[118,3877,3878],{"class":120,"line":1340},[118,3879,3880],{},"lto              = 'fat' # Full link-time optimization across all crates\n",[118,3882,3883],{"class":120,"line":1347},[118,3884,3885],{},"opt-level        = 's'   # Optimize for size\n",[118,3887,3888],{"class":120,"line":1358},[118,3889,3890],{},"overflow-checks  = false\n",[15,3892,3893,3894,3897],{},"Next, we need a ",[52,3895,3896],{},".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:",[15,3899,3900],{},[37,3901,3896],{},[87,3903,3905],{"className":3435,"code":3904,"language":3437,"meta":95,"style":95},"[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",[52,3906,3907,3912,3917,3922,3927,3932,3937,3942,3947,3952,3957,3961,3966,3970,3975,3980,3985,3989,3994,3999,4004,4009],{"__ignoreMap":95},[118,3908,3909],{"class":120,"line":121},[118,3910,3911],{},"[target.xtensa-esp32s3-none-elf]\n",[118,3913,3914],{"class":120,"line":127},[118,3915,3916],{},"rustflags = [\n",[118,3918,3919],{"class":120,"line":133},[118,3920,3921],{},"    \"-Clink-arg=-Tlink.x\",             # Use our custom linker script\n",[118,3923,3924],{"class":120,"line":139},[118,3925,3926],{},"    \"-Clink-arg=-nostdlib\",             # Don't link the C standard library\n",[118,3928,3929],{"class":120,"line":145},[118,3930,3931],{},"    \"-Clink-arg=-nostartfiles\",         # Don't include default startup code\n",[118,3933,3934],{"class":120,"line":893},[118,3935,3936],{},"    \"-Clink-arg=-Wl,--no-gc-sections\", # Keep all sections (don't garbage-collect)\n",[118,3938,3939],{"class":120,"line":1168},[118,3940,3941],{},"    \"-Clink-arg=-Wl,--no-check-sections\", # Skip section overlap checks\n",[118,3943,3944],{"class":120,"line":1176},[118,3945,3946],{},"    \"-Clink-arg=-mtext-section-literals\",  # Xtensa-specific: inline literal pools\n",[118,3948,3949],{"class":120,"line":1184},[118,3950,3951],{},"    \"-Clink-arg=-Wl,--entry=rust_app_core_entry\", # Set the ELF entry point\n",[118,3953,3954],{"class":120,"line":1195},[118,3955,3956],{},"]\n",[118,3958,3959],{"class":120,"line":1203},[118,3960,2247],{"emptyLinePlaceholder":666},[118,3962,3963],{"class":120,"line":1209},[118,3964,3965],{},"[env]\n",[118,3967,3968],{"class":120,"line":1223},[118,3969,2247],{"emptyLinePlaceholder":666},[118,3971,3972],{"class":120,"line":1229},[118,3973,3974],{},"[build]\n",[118,3976,3977],{"class":120,"line":1241},[118,3978,3979],{},"# Default build target — no need to pass --target every time\n",[118,3981,3982],{"class":120,"line":1249},[118,3983,3984],{},"target = \"xtensa-esp32s3-none-elf\"\n",[118,3986,3987],{"class":120,"line":1266},[118,3988,2247],{"emptyLinePlaceholder":666},[118,3990,3991],{"class":120,"line":1285},[118,3992,3993],{},"[unstable]\n",[118,3995,3996],{"class":120,"line":1291},[118,3997,3998],{},"# Build the `core` library from source for our target.\n",[118,4000,4001],{"class":120,"line":1304},[118,4002,4003],{},"# The Xtensa target doesn't ship prebuilt standard libraries,\n",[118,4005,4006],{"class":120,"line":1310},[118,4007,4008],{},"# so Cargo needs to compile `core` itself.\n",[118,4010,4011],{"class":120,"line":1322},[118,4012,4013],{},"build-std = [\"core\"]\n",[794,4015,4017],{"id":4016},"the-linker-script","The Linker Script",[15,4019,4020,4021,4024,4025,4028],{},"In Part 0, the ",[52,4022,4023],{},".bss"," (uninitialized global variables) and ",[52,4026,4027],{},".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.",[15,4030,4031,4032,4035,4036,4039,4040,4042],{},"This is a critical piece of the puzzle. The linker script defines two memory regions: ",[52,4033,4034],{},"FLASH_TEXT"," (where our code lives in flash, mapped to a virtual address via the MMU) and ",[52,4037,4038],{},"DRAM"," (our reserved 128KB of RAM from the ",[52,4041,2231],{}," macro).",[15,4044,4045],{},[37,4046,4047],{},"link.x",[87,4049,4051],{"className":3045,"code":4050,"language":3047,"meta":95,"style":95},"/* 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",[52,4052,4053,4058,4063,4067,4072,4076,4081,4086,4091,4096,4101,4106,4111,4115,4119,4124,4129,4134,4138,4143,4147,4151,4156,4160,4164,4169,4174,4179,4184,4188,4193,4198,4203,4207,4211,4216,4221,4226,4230,4235,4240,4244,4248,4253,4258,4262,4266,4271,4276,4281,4286,4291,4295,4299,4303,4308,4313,4318,4323,4328,4333,4338,4342,4347,4352,4357,4362,4367,4372,4376,4380,4385,4390,4395,4399,4404,4409,4414,4419,4424,4428,4433,4438,4443,4449,4455,4461,4467,4472],{"__ignoreMap":95},[118,4054,4055],{"class":120,"line":121},[118,4056,4057],{},"/* Declare our Rust entry function as the ELF entry point */\n",[118,4059,4060],{"class":120,"line":127},[118,4061,4062],{},"ENTRY(rust_app_core_entry)\n",[118,4064,4065],{"class":120,"line":133},[118,4066,2247],{"emptyLinePlaceholder":666},[118,4068,4069],{"class":120,"line":139},[118,4070,4071],{},"MEMORY\n",[118,4073,4074],{"class":120,"line":145},[118,4075,2478],{},[118,4077,4078],{"class":120,"line":893},[118,4079,4080],{},"    /*\n",[118,4082,4083],{"class":120,"line":1168},[118,4084,4085],{},"     * FLASH_TEXT: Where our code will be mapped in the address space.\n",[118,4087,4088],{"class":120,"line":1176},[118,4089,4090],{},"     * 0x42400000 is a virtual address — the MMU will map our flash\n",[118,4092,4093],{"class":120,"line":1184},[118,4094,4095],{},"     * partition to this region at runtime (we'll set that up in C).\n",[118,4097,4098],{"class":120,"line":1195},[118,4099,4100],{},"     * 512K should be plenty for most Rust programs.\n",[118,4102,4103],{"class":120,"line":1203},[118,4104,4105],{},"     */\n",[118,4107,4108],{"class":120,"line":1209},[118,4109,4110],{},"    FLASH_TEXT (rx)  : ORIGIN = 0x42400000, LENGTH = 512K\n",[118,4112,4113],{"class":120,"line":1223},[118,4114,2247],{"emptyLinePlaceholder":666},[118,4116,4117],{"class":120,"line":1229},[118,4118,4080],{},[118,4120,4121],{"class":120,"line":1241},[118,4122,4123],{},"     * DRAM: The 128KB block we reserved with SOC_RESERVE_MEMORY_REGION.\n",[118,4125,4126],{"class":120,"line":1249},[118,4127,4128],{},"     * This is physical SRAM that both cores can access directly.\n",[118,4130,4131],{"class":120,"line":1266},[118,4132,4133],{},"     * Our stack, .data, and .bss all live here.\n",[118,4135,4136],{"class":120,"line":1285},[118,4137,4105],{},[118,4139,4140],{"class":120,"line":1291},[118,4141,4142],{},"    DRAM       (rw)  : ORIGIN = 0x3FCC9710, LENGTH = 128K\n",[118,4144,4145],{"class":120,"line":1304},[118,4146,2644],{},[118,4148,4149],{"class":120,"line":1310},[118,4150,2247],{"emptyLinePlaceholder":666},[118,4152,4153],{"class":120,"line":1322},[118,4154,4155],{},"SECTIONS\n",[118,4157,4158],{"class":120,"line":1328},[118,4159,2478],{},[118,4161,4162],{"class":120,"line":1340},[118,4163,4080],{},[118,4165,4166],{"class":120,"line":1347},[118,4167,4168],{},"     * 4-byte header at offset 0 of the binary.\n",[118,4170,4171],{"class":120,"line":1358},[118,4172,4173],{},"     * This is a simple convention: the first 4 bytes of our binary\n",[118,4175,4176],{"class":120,"line":1364},[118,4177,4178],{},"     * contain the address of rust_app_core_entry. The C bootloader\n",[118,4180,4181],{"class":120,"line":1377},[118,4182,4183],{},"     * reads this to know where to jump.\n",[118,4185,4186],{"class":120,"line":1388},[118,4187,4105],{},[118,4189,4190],{"class":120,"line":1395},[118,4191,4192],{},"    .header : {\n",[118,4194,4195],{"class":120,"line":1407},[118,4196,4197],{},"        LONG(rust_app_core_entry)\n",[118,4199,4200],{"class":120,"line":1413},[118,4201,4202],{},"    } > FLASH_TEXT\n",[118,4204,4205],{"class":120,"line":2469},[118,4206,2247],{"emptyLinePlaceholder":666},[118,4208,4209],{"class":120,"line":2475},[118,4210,4080],{},[118,4212,4213],{"class":120,"line":2481},[118,4214,4215],{},"     * Xtensa puts function literal pools (constants used by instructions)\n",[118,4217,4218],{"class":120,"line":2487},[118,4219,4220],{},"     * in .literal sections. We place the entry function's literals and\n",[118,4222,4223],{"class":120,"line":2493},[118,4224,4225],{},"     * code first to ensure they're near the beginning of the binary.\n",[118,4227,4228],{"class":120,"line":2498},[118,4229,4105],{},[118,4231,4232],{"class":120,"line":2504},[118,4233,4234],{},"    .entry_lit : {\n",[118,4236,4237],{"class":120,"line":2510},[118,4238,4239],{},"        KEEP(*(.literal.rust_app_core_entry))\n",[118,4241,4242],{"class":120,"line":2516},[118,4243,4202],{},[118,4245,4246],{"class":120,"line":2522},[118,4247,2247],{"emptyLinePlaceholder":666},[118,4249,4250],{"class":120,"line":2527},[118,4251,4252],{},"    .entry : {\n",[118,4254,4255],{"class":120,"line":2533},[118,4256,4257],{},"        KEEP(*(.text.rust_app_core_entry))\n",[118,4259,4260],{"class":120,"line":2539},[118,4261,4202],{},[118,4263,4264],{"class":120,"line":2545},[118,4265,2247],{"emptyLinePlaceholder":666},[118,4267,4268],{"class":120,"line":2550},[118,4269,4270],{},"    /* All remaining code and read-only data goes into flash */\n",[118,4272,4273],{"class":120,"line":2556},[118,4274,4275],{},"    .text : {\n",[118,4277,4278],{"class":120,"line":2562},[118,4279,4280],{},"        *(.literal .literal.*)    /* Xtensa literal pools */\n",[118,4282,4283],{"class":120,"line":2568},[118,4284,4285],{},"        *(.text .text.*)          /* Executable code */\n",[118,4287,4288],{"class":120,"line":2573},[118,4289,4290],{},"        *(.rodata .rodata.*)      /* Read-only data (strings, constants) */\n",[118,4292,4293],{"class":120,"line":2579},[118,4294,4202],{},[118,4296,4297],{"class":120,"line":2585},[118,4298,2247],{"emptyLinePlaceholder":666},[118,4300,4301],{"class":120,"line":2591},[118,4302,4080],{},[118,4304,4305],{"class":120,"line":2596},[118,4306,4307],{},"     * .data: Initialized global/static variables.\n",[118,4309,4310],{"class":120,"line":2602},[118,4311,4312],{},"     * These live in DRAM at runtime (VMA), but their initial values\n",[118,4314,4315],{"class":120,"line":2608},[118,4316,4317],{},"     * are stored in flash (LMA). Our Rust startup code must copy\n",[118,4319,4320],{"class":120,"line":2613},[118,4321,4322],{},"     * them from flash to RAM before using them.\n",[118,4324,4325],{"class":120,"line":2619},[118,4326,4327],{},"     *\n",[118,4329,4330],{"class":120,"line":2624},[118,4331,4332],{},"     * The \"AT> FLASH_TEXT\" part means: \"put the content in flash,\n",[118,4334,4335],{"class":120,"line":2630},[118,4336,4337],{},"     * but assign addresses as if it's in DRAM.\"\n",[118,4339,4340],{"class":120,"line":2635},[118,4341,4105],{},[118,4343,4344],{"class":120,"line":2641},[118,4345,4346],{},"    .data : {\n",[118,4348,4349],{"class":120,"line":2647},[118,4350,4351],{},"        _data_start = .;\n",[118,4353,4354],{"class":120,"line":2652},[118,4355,4356],{},"        *(.data .data.*)\n",[118,4358,4359],{"class":120,"line":2658},[118,4360,4361],{},"        _data_end = .;\n",[118,4363,4364],{"class":120,"line":2664},[118,4365,4366],{},"    } > DRAM AT> FLASH_TEXT\n",[118,4368,4369],{"class":120,"line":2670},[118,4370,4371],{},"    _data_load = LOADADDR(.data);  /* Flash address where .data content lives */\n",[118,4373,4374],{"class":120,"line":2675},[118,4375,2247],{"emptyLinePlaceholder":666},[118,4377,4378],{"class":120,"line":2681},[118,4379,4080],{},[118,4381,4382],{"class":120,"line":2686},[118,4383,4384],{},"     * .bss: Uninitialized global/static variables.\n",[118,4386,4387],{"class":120,"line":2692},[118,4388,4389],{},"     * NOLOAD means the linker doesn't store anything in the binary for\n",[118,4391,4392],{"class":120,"line":2697},[118,4393,4394],{},"     * this section — our startup code just zeroes the region at boot.\n",[118,4396,4397],{"class":120,"line":2703},[118,4398,4105],{},[118,4400,4401],{"class":120,"line":2709},[118,4402,4403],{},"    .bss (NOLOAD) : {\n",[118,4405,4406],{"class":120,"line":2714},[118,4407,4408],{},"        _bss_start = .;\n",[118,4410,4411],{"class":120,"line":2720},[118,4412,4413],{},"        *(.bss .bss.* COMMON)\n",[118,4415,4416],{"class":120,"line":2726},[118,4417,4418],{},"        _bss_end = .;\n",[118,4420,4421],{"class":120,"line":2732},[118,4422,4423],{},"    } > DRAM\n",[118,4425,4426],{"class":120,"line":2738},[118,4427,2247],{"emptyLinePlaceholder":666},[118,4429,4430],{"class":120,"line":2744},[118,4431,4432],{},"    /* Discard sections we don't need — saves space in the binary */\n",[118,4434,4435],{"class":120,"line":2750},[118,4436,4437],{},"    /DISCARD/ : {\n",[118,4439,4440],{"class":120,"line":2756},[118,4441,4442],{},"        *(.eh_frame)         /* Exception handling frames (unused in no_std) */\n",[118,4444,4446],{"class":120,"line":4445},84,[118,4447,4448],{},"        *(.eh_frame_hdr)\n",[118,4450,4452],{"class":120,"line":4451},85,[118,4453,4454],{},"        *(.stack)\n",[118,4456,4458],{"class":120,"line":4457},86,[118,4459,4460],{},"        *(.xtensa.info)      /* Xtensa toolchain metadata */\n",[118,4462,4464],{"class":120,"line":4463},87,[118,4465,4466],{},"        *(.comment)          /* Compiler version strings */\n",[118,4468,4470],{"class":120,"line":4469},88,[118,4471,2753],{},[118,4473,4475],{"class":120,"line":4474},89,[118,4476,2644],{},[794,4478,4480],{"id":4479},"initializing-data-and-bss-from-rust","Initializing .data and .bss from Rust",[15,4482,4483,4484,4486,4487,4489,4490,4493],{},"When our Rust code was a library linked into ESP-IDF, the IDF startup code handled copying ",[52,4485,4027],{}," from flash to RAM and zeroing ",[52,4488,4023],{},". Now that we're standalone, we have to do it ourselves. This must happen ",[18,4491,4492],{},"before"," any static or global variables are accessed, or we'll read garbage.",[87,4495,4497],{"className":112,"code":4496,"language":114,"meta":95,"style":95},"// 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",[52,4498,4499,4504,4509,4514,4519,4524,4529,4534,4539,4544,4548,4552,4557,4562,4567,4572,4577,4582,4587,4592,4597,4602,4607,4612,4617,4621,4625,4630,4635,4640,4645,4650,4655,4660,4664],{"__ignoreMap":95},[118,4500,4501],{"class":120,"line":121},[118,4502,4503],{},"// These symbols are defined by our linker script (link.x).\n",[118,4505,4506],{"class":120,"line":127},[118,4507,4508],{},"// They don't contain data — their *addresses* ARE the data.\n",[118,4510,4511],{"class":120,"line":133},[118,4512,4513],{},"// For example, &_data_start gives us the RAM address where .data begins.\n",[118,4515,4516],{"class":120,"line":139},[118,4517,4518],{},"unsafe extern \"C\" {\n",[118,4520,4521],{"class":120,"line":145},[118,4522,4523],{},"    static _data_start: u8;  // Start of .data in RAM\n",[118,4525,4526],{"class":120,"line":893},[118,4527,4528],{},"    static _data_end: u8;    // End of .data in RAM\n",[118,4530,4531],{"class":120,"line":1168},[118,4532,4533],{},"    static _data_load: u8;   // Start of .data's initial values in flash\n",[118,4535,4536],{"class":120,"line":1176},[118,4537,4538],{},"    static _bss_start: u8;   // Start of .bss in RAM\n",[118,4540,4541],{"class":120,"line":1184},[118,4542,4543],{},"    static _bss_end: u8;     // End of .bss in RAM\n",[118,4545,4546],{"class":120,"line":1195},[118,4547,2644],{},[118,4549,4550],{"class":120,"line":1203},[118,4551,2247],{"emptyLinePlaceholder":666},[118,4553,4554],{"class":120,"line":1209},[118,4555,4556],{},"/// Copy .data initial values from flash to RAM, and zero .bss.\n",[118,4558,4559],{"class":120,"line":1223},[118,4560,4561],{},"/// MUST be called before accessing any static/global variables.\n",[118,4563,4564],{"class":120,"line":1229},[118,4565,4566],{},"unsafe fn init_sections() {\n",[118,4568,4569],{"class":120,"line":1241},[118,4570,4571],{},"    // Calculate how many bytes the .data section occupies\n",[118,4573,4574],{"class":120,"line":1249},[118,4575,4576],{},"    let data_size = &raw const _data_end as usize - &raw const _data_start as usize;\n",[118,4578,4579],{"class":120,"line":1266},[118,4580,4581],{},"    if data_size > 0 {\n",[118,4583,4584],{"class":120,"line":1285},[118,4585,4586],{},"        // Copy initial values from flash (where the linker stored them)\n",[118,4588,4589],{"class":120,"line":1291},[118,4590,4591],{},"        // to RAM (where the program expects them at runtime).\n",[118,4593,4594],{"class":120,"line":1304},[118,4595,4596],{},"        core::ptr::copy_nonoverlapping(\n",[118,4598,4599],{"class":120,"line":1310},[118,4600,4601],{},"            &raw const _data_load,          // Source: flash\n",[118,4603,4604],{"class":120,"line":1322},[118,4605,4606],{},"            &raw const _data_start as *mut u8, // Destination: RAM\n",[118,4608,4609],{"class":120,"line":1328},[118,4610,4611],{},"            data_size,\n",[118,4613,4614],{"class":120,"line":1340},[118,4615,4616],{},"        );\n",[118,4618,4619],{"class":120,"line":1347},[118,4620,2753],{},[118,4622,4623],{"class":120,"line":1358},[118,4624,2247],{"emptyLinePlaceholder":666},[118,4626,4627],{"class":120,"line":1364},[118,4628,4629],{},"    // Calculate how many bytes the .bss section occupies\n",[118,4631,4632],{"class":120,"line":1377},[118,4633,4634],{},"    let bss_size = &raw const _bss_end as usize - &raw const _bss_start as usize;\n",[118,4636,4637],{"class":120,"line":1388},[118,4638,4639],{},"    if bss_size > 0 {\n",[118,4641,4642],{"class":120,"line":1395},[118,4643,4644],{},"        // Zero out .bss. C and Rust both assume uninitialized globals\n",[118,4646,4647],{"class":120,"line":1407},[118,4648,4649],{},"        // start as zero. Without this, they'd contain whatever was\n",[118,4651,4652],{"class":120,"line":1413},[118,4653,4654],{},"        // previously in RAM — likely garbage from the bootloader.\n",[118,4656,4657],{"class":120,"line":2469},[118,4658,4659],{},"        core::ptr::write_bytes(&raw const _bss_start as *mut u8, 0, bss_size);\n",[118,4661,4662],{"class":120,"line":2475},[118,4663,2753],{},[118,4665,4666],{"class":120,"line":2481},[118,4667,2644],{},[794,4669,4671],{"id":4670},"the-updated-rust-entry-point","The Updated Rust Entry Point",[15,4673,4674],{},"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.",[15,4676,4677,4678,4681],{},"For this demo, I'm using the start of our reserved memory region (",[52,4679,4680],{},"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.",[87,4683,4685],{"className":112,"code":4684,"language":114,"meta":95,"style":95},"// 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",[52,4686,4687,4692,4697,4702,4707,4711,4716,4721,4725,4730,4734,4739,4744,4749,4754,4758,4762,4767,4772,4777,4782,4787,4792,4797,4801,4806,4811,4815,4819,4824,4829,4833,4838,4842,4846,4850,4854],{"__ignoreMap":95},[118,4688,4689],{"class":120,"line":121},[118,4690,4691],{},"// Fixed memory address for the shared counter.\n",[118,4693,4694],{"class":120,"line":127},[118,4695,4696],{},"// Both the C side and Rust side must agree on this address.\n",[118,4698,4699],{"class":120,"line":133},[118,4700,4701],{},"// We're using the very start of our reserved DRAM region.\n",[118,4703,4704],{"class":120,"line":139},[118,4705,4706],{},"const COUNTER_ADDR: usize = 0x3FCC9710;\n",[118,4708,4709],{"class":120,"line":145},[118,4710,2247],{"emptyLinePlaceholder":666},[118,4712,4713],{"class":120,"line":893},[118,4714,4715],{},"// #[unsafe(link_section = \".text.rust_app_core_entry\")] places this\n",[118,4717,4718],{"class":120,"line":1168},[118,4719,4720],{},"// function in a specific linker section making it easy to find.\n",[118,4722,4723],{"class":120,"line":1176},[118,4724,3278],{},[118,4726,4727],{"class":120,"line":1184},[118,4728,4729],{},"#[unsafe(link_section = \".text.rust_app_core_entry\")]\n",[118,4731,4732],{"class":120,"line":1195},[118,4733,3325],{},[118,4735,4736],{"class":120,"line":1203},[118,4737,4738],{},"    // FIRST THING: initialize .data and .bss before touching any statics.\n",[118,4740,4741],{"class":120,"line":1209},[118,4742,4743],{},"    // If we skip this, any global variable could contain garbage.\n",[118,4745,4746],{"class":120,"line":1223},[118,4747,4748],{},"    unsafe {\n",[118,4750,4751],{"class":120,"line":1229},[118,4752,4753],{},"        init_sections();\n",[118,4755,4756],{"class":120,"line":1241},[118,4757,2753],{},[118,4759,4760],{"class":120,"line":1249},[118,4761,2247],{"emptyLinePlaceholder":666},[118,4763,4764],{"class":120,"line":1266},[118,4765,4766],{},"    // Create an atomic reference to our shared counter.\n",[118,4768,4769],{"class":120,"line":1285},[118,4770,4771],{},"    // We cast the raw memory address to an AtomicU32 pointer.\n",[118,4773,4774],{"class":120,"line":1291},[118,4775,4776],{},"    // This is unsafe because we're asserting that this address is:\n",[118,4778,4779],{"class":120,"line":1304},[118,4780,4781],{},"    //   1. Valid and aligned\n",[118,4783,4784],{"class":120,"line":1310},[118,4785,4786],{},"    //   2. Not being used for anything else\n",[118,4788,4789],{"class":120,"line":1322},[118,4790,4791],{},"    //   3. Accessible by both cores\n",[118,4793,4794],{"class":120,"line":1328},[118,4795,4796],{},"    let counter = unsafe { &*(COUNTER_ADDR as *const AtomicU32) };\n",[118,4798,4799],{"class":120,"line":1340},[118,4800,2247],{"emptyLinePlaceholder":666},[118,4802,4803],{"class":120,"line":1347},[118,4804,4805],{},"    // Initialize the counter to zero (in case there was leftover data)\n",[118,4807,4808],{"class":120,"line":1358},[118,4809,4810],{},"    counter.store(0, Ordering::Relaxed);\n",[118,4812,4813],{"class":120,"line":1364},[118,4814,2247],{"emptyLinePlaceholder":666},[118,4816,4817],{"class":120,"line":1377},[118,4818,3330],{},[118,4820,4821],{"class":120,"line":1388},[118,4822,4823],{},"        // Increment the shared counter atomically\n",[118,4825,4826],{"class":120,"line":1395},[118,4827,4828],{},"        counter.fetch_add(1, Ordering::Relaxed);\n",[118,4830,4831],{"class":120,"line":1407},[118,4832,2247],{"emptyLinePlaceholder":666},[118,4834,4835],{"class":120,"line":1413},[118,4836,4837],{},"        // Busy-wait delay (same as before)\n",[118,4839,4840],{"class":120,"line":2469},[118,4841,3384],{},[118,4843,4844],{"class":120,"line":2475},[118,4845,3389],{},[118,4847,4848],{"class":120,"line":2481},[118,4849,3394],{},[118,4851,4852],{"class":120,"line":2487},[118,4853,2753],{},[118,4855,4856],{"class":120,"line":2493},[118,4857,2644],{},[568,4859,4861],{"id":4860},"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",[15,4863,4864],{},"Now that our Rust code is a standalone binary instead of a linked library, the ESP-IDF side needs several changes.",[794,4866,4868],{"id":4867},"create-a-flash-partition","Create a Flash Partition",[15,4870,4871,4872,4875],{},"The Rust binary needs its own partition in flash. We add a ",[52,4873,4874],{},"rust_app"," entry after the factory partition (where the main ESP-IDF firmware lives):",[15,4877,4878],{},[37,4879,4880],{},"partitions.csv",[87,4882,4886],{"className":4883,"code":4884,"language":4885,"meta":95,"style":95},"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",[52,4887,4888,4893,4898,4903],{"__ignoreMap":95},[118,4889,4890],{"class":120,"line":121},[118,4891,4892],{},"nvs,         data, nvs,     0x9000,     0x6000,\n",[118,4894,4895],{"class":120,"line":127},[118,4896,4897],{},"phy_init,    data, phy,     0xf000,     0x1000,\n",[118,4899,4900],{"class":120,"line":133},[118,4901,4902],{},"factory,     app,  factory, 0x10000,    0x1F0000,\n",[118,4904,4905],{"class":120,"line":139},[118,4906,4907],{},"rust_app,    data, 0x40,    0x200000,   0x80000,\n",[15,4909,3115,4910,4912,4913,4916,4917,4920,4921,4924],{},[52,4911,4874],{}," partition starts at offset ",[52,4914,4915],{},"0x200000"," (2MB into flash) and is ",[52,4918,4919],{},"0x80000"," (512KB) in size. The subtype ",[52,4922,4923],{},"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.",[794,4926,4928],{"id":4927},"map-the-partition-into-memory-via-the-mmu","Map the Partition into Memory via the MMU",[15,4930,4931],{},"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.",[15,4933,4934,4935,4937,4938,4941],{},"The function below finds our ",[52,4936,4874],{}," partition in flash and maps it page-by-page to virtual address ",[52,4939,4940],{},"0x42400000"," (the same address our linker script targets). After mapping, the CPU can execute code from this region as if it were regular memory.",[87,4943,4945],{"className":2116,"code":4944,"language":2118,"meta":95,"style":95},"#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",[52,4946,4947,4952,4957,4962,4967,4971,4976,4981,4986,4990,4995,5000,5004,5009,5013,5018,5023,5028,5033,5037,5042,5046,5051,5056,5060,5064,5069,5074,5079,5084,5089,5094,5098,5103,5107,5112,5117,5122,5127,5132,5137,5142,5146,5150,5155,5160,5165,5169,5174,5179,5184],{"__ignoreMap":95},[118,4948,4949],{"class":120,"line":121},[118,4950,4951],{},"#include \u003Cstring.h>\n",[118,4953,4954],{"class":120,"line":127},[118,4955,4956],{},"#include \"esp_partition.h\"\n",[118,4958,4959],{"class":120,"line":133},[118,4960,4961],{},"#include \"hal/mmu_hal.h\"\n",[118,4963,4964],{"class":120,"line":139},[118,4965,4966],{},"#include \"hal/cache_hal.h\"\n",[118,4968,4969],{"class":120,"line":145},[118,4970,2247],{"emptyLinePlaceholder":666},[118,4972,4973],{"class":120,"line":893},[118,4974,4975],{},"// Virtual address where the Rust binary will be mapped.\n",[118,4977,4978],{"class":120,"line":1168},[118,4979,4980],{},"// This MUST match the FLASH_TEXT origin in link.x.\n",[118,4982,4983],{"class":120,"line":1176},[118,4984,4985],{},"#define RUST_VADDR 0x42400000\n",[118,4987,4988],{"class":120,"line":1184},[118,4989,2247],{"emptyLinePlaceholder":666},[118,4991,4992],{"class":120,"line":1195},[118,4993,4994],{},"// Will hold the entry point address read from the binary's header\n",[118,4996,4997],{"class":120,"line":1203},[118,4998,4999],{},"uint32_t rust_entry_addr = 0;\n",[118,5001,5002],{"class":120,"line":1209},[118,5003,2247],{"emptyLinePlaceholder":666},[118,5005,5006],{"class":120,"line":1223},[118,5007,5008],{},"static void load_rust_app(void)\n",[118,5010,5011],{"class":120,"line":1229},[118,5012,2478],{},[118,5014,5015],{"class":120,"line":1241},[118,5016,5017],{},"    // Find the \"rust_app\" partition we defined in partitions.csv.\n",[118,5019,5020],{"class":120,"line":1249},[118,5021,5022],{},"    // We search by type (DATA) and subtype (0x40, our custom value).\n",[118,5024,5025],{"class":120,"line":1266},[118,5026,5027],{},"    const esp_partition_t *part =\n",[118,5029,5030],{"class":120,"line":1285},[118,5031,5032],{},"        esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 0x40, \"rust_app\");\n",[118,5034,5035],{"class":120,"line":1291},[118,5036,2247],{"emptyLinePlaceholder":666},[118,5038,5039],{"class":120,"line":1304},[118,5040,5041],{},"    if (!part)\n",[118,5043,5044],{"class":120,"line":1310},[118,5045,2735],{},[118,5047,5048],{"class":120,"line":1322},[118,5049,5050],{},"        ESP_LOGE(TAG, \"rust_app partition not found!\");\n",[118,5052,5053],{"class":120,"line":1328},[118,5054,5055],{},"        return;\n",[118,5057,5058],{"class":120,"line":1340},[118,5059,2753],{},[118,5061,5062],{"class":120,"line":1347},[118,5063,2247],{"emptyLinePlaceholder":666},[118,5065,5066],{"class":120,"line":1358},[118,5067,5068],{},"    // Map the partition into the CPU's address space page by page.\n",[118,5070,5071],{"class":120,"line":1364},[118,5072,5073],{},"    // The MMU works in pages (typically 64KB on ESP32-S3), so we\n",[118,5075,5076],{"class":120,"line":1377},[118,5077,5078],{},"    // calculate how many pages we need and map each one.\n",[118,5080,5081],{"class":120,"line":1388},[118,5082,5083],{},"    uint32_t page_size = CONFIG_MMU_PAGE_SIZE;\n",[118,5085,5086],{"class":120,"line":1395},[118,5087,5088],{},"    uint32_t pages = (part->size + page_size - 1) / page_size; // Round up\n",[118,5090,5091],{"class":120,"line":1407},[118,5092,5093],{},"    uint32_t actual_mapped_size = 0;\n",[118,5095,5096],{"class":120,"line":1413},[118,5097,2247],{"emptyLinePlaceholder":666},[118,5099,5100],{"class":120,"line":2469},[118,5101,5102],{},"    for (uint32_t i = 0; i \u003C pages; i++)\n",[118,5104,5105],{"class":120,"line":2475},[118,5106,2735],{},[118,5108,5109],{"class":120,"line":2481},[118,5110,5111],{},"        uint32_t mapped = 0;\n",[118,5113,5114],{"class":120,"line":2487},[118,5115,5116],{},"        // Map one page: virtual address → physical flash address\n",[118,5118,5119],{"class":120,"line":2493},[118,5120,5121],{},"        mmu_hal_map_region(0, MMU_TARGET_FLASH0,\n",[118,5123,5124],{"class":120,"line":2498},[118,5125,5126],{},"                           RUST_VADDR + (i * page_size),    // Virtual addr\n",[118,5128,5129],{"class":120,"line":2504},[118,5130,5131],{},"                           part->address + (i * page_size), // Flash addr\n",[118,5133,5134],{"class":120,"line":2510},[118,5135,5136],{},"                           page_size, &mapped);\n",[118,5138,5139],{"class":120,"line":2516},[118,5140,5141],{},"        actual_mapped_size += mapped;\n",[118,5143,5144],{"class":120,"line":2522},[118,5145,2753],{},[118,5147,5148],{"class":120,"line":2527},[118,5149,2247],{"emptyLinePlaceholder":666},[118,5151,5152],{"class":120,"line":2533},[118,5153,5154],{},"    // Invalidate the cache for this region so the CPU doesn't serve\n",[118,5156,5157],{"class":120,"line":2539},[118,5158,5159],{},"    // stale data from a previous mapping.\n",[118,5161,5162],{"class":120,"line":2545},[118,5163,5164],{},"    cache_hal_invalidate_addr(RUST_VADDR, part->size);\n",[118,5166,5167],{"class":120,"line":2550},[118,5168,2247],{"emptyLinePlaceholder":666},[118,5170,5171],{"class":120,"line":2556},[118,5172,5173],{},"    ESP_LOGI(TAG, \"Rust app mapped at 0x%lx (%lu bytes, flash 0x%lx)\",\n",[118,5175,5176],{"class":120,"line":2562},[118,5177,5178],{},"             (unsigned long)RUST_VADDR, (unsigned long)actual_mapped_size,\n",[118,5180,5181],{"class":120,"line":2568},[118,5182,5183],{},"             (unsigned long)part->address);\n",[118,5185,5186],{"class":120,"line":2573},[118,5187,2644],{},[794,5189,5191],{"id":5190},"update-the-boot-function","Update the Boot Function",[15,5193,3115,5194,5197,5198,5201],{},[52,5195,5196],{},"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 ",[52,5199,5200],{},".header"," section from our linker script) and stores it in a global variable that the assembly trampoline will read.",[87,5203,5205],{"className":2116,"code":5204,"language":2118,"meta":95,"style":95},"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",[52,5206,5207,5211,5215,5220,5225,5229,5234,5239,5244,5249,5254,5258,5263,5267,5272,5276,5280,5284,5288,5292,5296,5300,5304,5308,5312,5316,5320],{"__ignoreMap":95},[118,5208,5209],{"class":120,"line":121},[118,5210,2472],{},[118,5212,5213],{"class":120,"line":127},[118,5214,2478],{},[118,5216,5217],{"class":120,"line":133},[118,5218,5219],{},"    // Step 1: Map the Rust binary from flash into the address space\n",[118,5221,5222],{"class":120,"line":139},[118,5223,5224],{},"    load_rust_app();\n",[118,5226,5227],{"class":120,"line":145},[118,5228,2247],{"emptyLinePlaceholder":666},[118,5230,5231],{"class":120,"line":893},[118,5232,5233],{},"    // Step 2: Read the entry point from the binary's 4-byte header.\n",[118,5235,5236],{"class":120,"line":1168},[118,5237,5238],{},"    // Our linker script placed LONG(rust_app_core_entry) at offset 0,\n",[118,5240,5241],{"class":120,"line":1176},[118,5242,5243],{},"    // so the first 4 bytes at RUST_VADDR contain the function's address.\n",[118,5245,5246],{"class":120,"line":1184},[118,5247,5248],{},"    uint32_t entry = *(volatile uint32_t *)RUST_VADDR;\n",[118,5250,5251],{"class":120,"line":1195},[118,5252,5253],{},"    rust_entry_addr = entry;  // Store globally for the trampoline to read\n",[118,5255,5256],{"class":120,"line":1203},[118,5257,2247],{"emptyLinePlaceholder":666},[118,5259,5260],{"class":120,"line":1209},[118,5261,5262],{},"    ESP_LOGI(TAG, \"Rust entry at 0x%lx\", (unsigned long)entry);\n",[118,5264,5265],{"class":120,"line":1223},[118,5266,2247],{"emptyLinePlaceholder":666},[118,5268,5269],{"class":120,"line":1229},[118,5270,5271],{},"    // Step 3: Same hardware boot sequence as before\n",[118,5273,5274],{"class":120,"line":1241},[118,5275,2519],{},[118,5277,5278],{"class":120,"line":1249},[118,5279,2247],{"emptyLinePlaceholder":666},[118,5281,5282],{"class":120,"line":1266},[118,5283,2559],{},[118,5285,5286],{"class":120,"line":1285},[118,5287,2565],{},[118,5289,5290],{"class":120,"line":1291},[118,5291,2582],{},[118,5293,5294],{"class":120,"line":1304},[118,5295,2588],{},[118,5297,5298],{"class":120,"line":1310},[118,5299,2559],{},[118,5301,5302],{"class":120,"line":1322},[118,5303,2616],{},[118,5305,5306],{"class":120,"line":1328},[118,5307,2582],{},[118,5309,5310],{"class":120,"line":1340},[118,5311,2627],{},[118,5313,5314],{"class":120,"line":1347},[118,5315,2247],{"emptyLinePlaceholder":666},[118,5317,5318],{"class":120,"line":1358},[118,5319,2638],{},[118,5321,5322],{"class":120,"line":1364},[118,5323,2644],{},[794,5325,5327],{"id":5326},"update-the-main-function","Update the Main Function",[15,5329,5330,5331,5333],{},"Since we can no longer reference ",[52,5332,2306],{}," by name (the Rust binary isn't linked into our C project anymore), we read the counter from its known memory address directly:",[87,5335,5337],{"className":2116,"code":5336,"language":2118,"meta":95,"style":95},"// 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",[52,5338,5339,5344,5349,5354,5358,5362,5366,5370,5374,5378,5382,5387,5392,5397,5402,5407,5411,5415,5419,5424,5429,5433],{"__ignoreMap":95},[118,5340,5341],{"class":120,"line":121},[118,5342,5343],{},"// The Rust code writes its counter to this fixed address.\n",[118,5345,5346],{"class":120,"line":127},[118,5347,5348],{},"// Both sides must agree on this — it's defined as COUNTER_ADDR in the Rust code.\n",[118,5350,5351],{"class":120,"line":133},[118,5352,5353],{},"#define RUST_COUNTER_ADDR 0x3FCC9710\n",[118,5355,5356],{"class":120,"line":139},[118,5357,2247],{"emptyLinePlaceholder":666},[118,5359,5360],{"class":120,"line":145},[118,5361,2678],{},[118,5363,5364],{"class":120,"line":893},[118,5365,2478],{},[118,5367,5368],{"class":120,"line":1168},[118,5369,2689],{},[118,5371,5372],{"class":120,"line":1176},[118,5373,2247],{"emptyLinePlaceholder":666},[118,5375,5376],{"class":120,"line":1184},[118,5377,2706],{},[118,5379,5380],{"class":120,"line":1195},[118,5381,2247],{"emptyLinePlaceholder":666},[118,5383,5384],{"class":120,"line":1203},[118,5385,5386],{},"    // Create a volatile pointer to the shared counter.\n",[118,5388,5389],{"class":120,"line":1209},[118,5390,5391],{},"    // \"volatile\" tells the C compiler: \"this value can change at any time\n",[118,5393,5394],{"class":120,"line":1223},[118,5395,5396],{},"    // (because another CPU core is writing to it), so always read from\n",[118,5398,5399],{"class":120,"line":1229},[118,5400,5401],{},"    // memory — don't cache it in a register.\"\n",[118,5403,5404],{"class":120,"line":1241},[118,5405,5406],{},"    volatile uint32_t *counter = (volatile uint32_t *)RUST_COUNTER_ADDR;\n",[118,5408,5409],{"class":120,"line":1249},[118,5410,2247],{"emptyLinePlaceholder":666},[118,5412,5413],{"class":120,"line":1266},[118,5414,2729],{},[118,5416,5417],{"class":120,"line":1285},[118,5418,2735],{},[118,5420,5421],{"class":120,"line":1291},[118,5422,5423],{},"        ESP_LOGI(TAG, \"Rust Core 1 counter: %lu\", (unsigned long)*counter);\n",[118,5425,5426],{"class":120,"line":1304},[118,5427,5428],{},"        vTaskDelay(pdMS_TO_TICKS(1000));\n",[118,5430,5431],{"class":120,"line":1310},[118,5432,2753],{},[118,5434,5435],{"class":120,"line":1322},[118,5436,2644],{},[794,5438,5440],{"id":5439},"update-the-assembly-trampoline","Update the Assembly Trampoline",[15,5442,5443,5444,5447,5448,5451,5452,5454],{},"The trampoline can no longer use ",[52,5445,5446],{},"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 ",[52,5449,5450],{},"rust_entry_addr"," global variable (which ",[52,5453,5196],{}," populated) and does an indirect jump:",[87,5456,5458],{"className":2790,"code":5457,"language":2792,"meta":95,"style":95},"/*\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",[52,5459,5460,5464,5469,5473,5478,5483,5488,5492,5496,5501,5505,5509,5514,5518,5522,5527,5531,5535,5540,5545,5550,5555,5560,5564,5569,5574,5578],{"__ignoreMap":95},[118,5461,5462],{"class":120,"line":121},[118,5463,2446],{},[118,5465,5466],{"class":120,"line":127},[118,5467,5468],{}," * app_core_trampoline.S (updated for runtime loading)\n",[118,5470,5471],{"class":120,"line":133},[118,5472,2808],{},[118,5474,5475],{"class":120,"line":139},[118,5476,5477],{}," * Same job as before: set the stack pointer, then jump to Rust.\n",[118,5479,5480],{"class":120,"line":145},[118,5481,5482],{}," * But now the Rust entry address isn't known at link time — it's\n",[118,5484,5485],{"class":120,"line":893},[118,5486,5487],{}," * stored in the rust_entry_addr global variable by the C code.\n",[118,5489,5490],{"class":120,"line":1168},[118,5491,2466],{},[118,5493,5494],{"class":120,"line":1176},[118,5495,2247],{"emptyLinePlaceholder":666},[118,5497,5498],{"class":120,"line":1184},[118,5499,5500],{},"    .section .iram1, \"ax\"\n",[118,5502,5503],{"class":120,"line":1195},[118,5504,2850],{},[118,5506,5507],{"class":120,"line":1203},[118,5508,2855],{},[118,5510,5511],{"class":120,"line":1209},[118,5512,5513],{},"    .align   4\n",[118,5515,5516],{"class":120,"line":1223},[118,5517,2247],{"emptyLinePlaceholder":666},[118,5519,5520],{"class":120,"line":1229},[118,5521,2869],{},[118,5523,5524],{"class":120,"line":1241},[118,5525,5526],{},"    /* Set up the stack pointer (same as before) */\n",[118,5528,5529],{"class":120,"line":1249},[118,5530,2889],{},[118,5532,5533],{"class":120,"line":1266},[118,5534,2247],{"emptyLinePlaceholder":666},[118,5536,5537],{"class":120,"line":1285},[118,5538,5539],{},"    /* Load the entry address from the global variable.\n",[118,5541,5542],{"class":120,"line":1291},[118,5543,5544],{},"     * movi loads the ADDRESS of rust_entry_addr into a2,\n",[118,5546,5547],{"class":120,"line":1304},[118,5548,5549],{},"     * then l32i loads the VALUE at that address into a0. */\n",[118,5551,5552],{"class":120,"line":1310},[118,5553,5554],{},"    movi  a2, rust_entry_addr\n",[118,5556,5557],{"class":120,"line":1322},[118,5558,5559],{},"    l32i  a0, a2, 0           /* a0 = *(rust_entry_addr) */\n",[118,5561,5562],{"class":120,"line":1328},[118,5563,2247],{"emptyLinePlaceholder":666},[118,5565,5566],{"class":120,"line":1340},[118,5567,5568],{},"    /* Indirect jump to the Rust entry point */\n",[118,5570,5571],{"class":120,"line":1347},[118,5572,5573],{},"    jx    a0\n",[118,5575,5576],{"class":120,"line":1358},[118,5577,2247],{"emptyLinePlaceholder":666},[118,5579,5580],{"class":120,"line":1364},[118,5581,2922],{},[568,5583,5585],{"id":5584},"step-3-build-and-flash","Step 3: Build and Flash",[15,5587,5588],{},"Now we have two separate build steps — one for the Rust binary, one for the ESP-IDF firmware — and two separate flash steps.",[15,5590,5591],{},[37,5592,5593],{},"Build and flash the ESP-IDF side:",[87,5595,5597],{"className":3659,"code":5596,"language":3661,"meta":95,"style":95},"# 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",[52,5598,5599,5604,5612,5616,5621],{"__ignoreMap":95},[118,5600,5601],{"class":120,"line":121},[118,5602,5603],{"class":1135},"# Build the ESP-IDF project (which no longer includes any Rust code)\n",[118,5605,5606,5609],{"class":120,"line":127},[118,5607,5608],{"class":3678},"idf.py",[118,5610,5611],{"class":1113}," build\n",[118,5613,5614],{"class":120,"line":133},[118,5615,2247],{"emptyLinePlaceholder":666},[118,5617,5618],{"class":120,"line":139},[118,5619,5620],{"class":1135},"# Flash the main firmware and partition table\n",[118,5622,5623,5625],{"class":120,"line":145},[118,5624,5608],{"class":3678},[118,5626,5627],{"class":1113}," flash\n",[15,5629,5630],{},[37,5631,5632],{},"Build and flash the Rust binary:",[87,5634,5636],{"className":3659,"code":5635,"language":3661,"meta":95,"style":95},"# 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",[52,5637,5638,5643,5655,5659,5664,5669,5674,5679,5692,5704,5709,5713,5718,5723],{"__ignoreMap":95},[118,5639,5640],{"class":120,"line":121},[118,5641,5642],{"class":1135},"# Build the standalone Rust binary\n",[118,5644,5645,5647,5649,5651,5653],{"class":120,"line":127},[118,5646,3679],{"class":3678},[118,5648,3682],{"class":1113},[118,5650,3685],{"class":1113},[118,5652,3688],{"class":1113},[118,5654,3691],{"class":1113},[118,5656,5657],{"class":120,"line":133},[118,5658,2247],{"emptyLinePlaceholder":666},[118,5660,5661],{"class":120,"line":139},[118,5662,5663],{"class":1135},"# Convert from ELF format to raw binary.\n",[118,5665,5666],{"class":120,"line":145},[118,5667,5668],{"class":1135},"# The ELF file contains metadata (section headers, debug info, etc.)\n",[118,5670,5671],{"class":120,"line":893},[118,5672,5673],{"class":1135},"# that we don't need — objcopy strips all of that and outputs just\n",[118,5675,5676],{"class":120,"line":1168},[118,5677,5678],{"class":1135},"# the raw machine code that the CPU will execute.\n",[118,5680,5681,5684,5687,5690],{"class":120,"line":1176},[118,5682,5683],{"class":3678},"xtensa-esp32s3-elf-objcopy",[118,5685,5686],{"class":1113}," -O",[118,5688,5689],{"class":1113}," binary",[118,5691,3712],{"class":3711},[118,5693,5694,5697,5700,5702],{"class":120,"line":1184},[118,5695,5696],{"class":1109},"    '",[118,5698,5699],{"class":1113},"target/xtensa-esp32s3-none-elf/release/esp_rust_app",[118,5701,1279],{"class":1109},[118,5703,3712],{"class":3711},[118,5705,5706],{"class":120,"line":1195},[118,5707,5708],{"class":1113},"    rust_app.bin\n",[118,5710,5711],{"class":120,"line":1203},[118,5712,2247],{"emptyLinePlaceholder":666},[118,5714,5715],{"class":120,"line":1209},[118,5716,5717],{"class":1135},"# Flash the raw binary to the rust_app partition.\n",[118,5719,5720],{"class":120,"line":1223},[118,5721,5722],{"class":1135},"# 0x200000 is the offset we defined in partitions.csv.\n",[118,5724,5725,5728,5731,5734,5737,5741],{"class":120,"line":1229},[118,5726,5727],{"class":3678},"esptool.py",[118,5729,5730],{"class":1113}," --port",[118,5732,5733],{"class":1113}," /dev/ttyACM0",[118,5735,5736],{"class":1113}," write_flash",[118,5738,5740],{"class":5739},"sbssI"," 0x200000",[118,5742,5743],{"class":1113}," rust_app.bin\n",[15,5745,5746,5747,5750],{},"The two flash steps are independent. You can update the Rust binary without rebuilding or reflashing the ESP-IDF firmware — just flash the new ",[52,5748,5749],{},"rust_app.bin"," to the same partition offset.",[568,5752,5754],{"id":5753},"verifying-it-works","Verifying It Works",[15,5756,5757,5758,5761],{},"Open your serial monitor (",[52,5759,5760],{},"idf.py monitor"," or any terminal at 115200 baud) and you should see output like this:",[87,5763,5766],{"className":5764,"code":5765,"language":92},[90],"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",[52,5767,5765],{"__ignoreMap":95},[15,5769,5770],{},"There are several things to confirm in this output:",[2284,5772,5773,5784,5800,5808],{},[184,5774,5775,5778,5779,5781,5782,40],{},[37,5776,5777],{},"The partition table"," shows our ",[52,5780,4874],{}," partition at offset ",[52,5783,4915],{},[184,5785,5786,5789,5790,5792,5793,5796,5797,5799],{},[37,5787,5788],{},"The heap_init logs"," show that our reserved 128KB region (starting at ",[52,5791,4680],{},") is ",[18,5794,5795],{},"not"," listed as available for dynamic allocation — ",[52,5798,2231],{}," worked.",[184,5801,5802,5805,5806,40],{},[37,5803,5804],{},"The MMU mapping"," succeeded — the Rust binary is mapped at ",[52,5807,4940],{},[184,5809,5810,5813],{},[37,5811,5812],{},"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.",[22,5815],{},[44,5817,5819],{"id":5818},"whats-next","What's Next",[15,5821,5822],{},"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.",[15,5824,5825],{},"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.",[15,5827,5828],{},"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.",[646,5830,5831],{},"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":95,"searchDepth":127,"depth":127,"links":5833},[5834,5835,5836,5845,5851],{"id":2042,"depth":133,"text":2043},{"id":2105,"depth":127,"text":2106},{"id":2210,"depth":127,"text":2211,"children":5837},[5838,5839,5840,5841,5842,5843,5844],{"id":2217,"depth":133,"text":2218},{"id":2278,"depth":133,"text":2279},{"id":2761,"depth":133,"text":2762},{"id":2925,"depth":133,"text":2926},{"id":3095,"depth":133,"text":3096},{"id":3405,"depth":133,"text":3406},{"id":3652,"depth":133,"text":3653},{"id":3729,"depth":127,"text":3730,"children":5846},[5847,5848,5849,5850],{"id":3749,"depth":133,"text":3750},{"id":4860,"depth":133,"text":4861},{"id":5584,"depth":133,"text":5585},{"id":5753,"depth":133,"text":5754},{"id":5818,"depth":127,"text":5819},"2026-03-12",{},"/embedded/esp32/run_rust_on_app_core",{"title":2034,"description":2043},"embedded/ESP32/run_rust_on_app_core","NlFKgR5pqwUJSufyeMP6s9eNUWA9iRPOfZAISOlO_3o",{"id":5859,"title":5860,"body":5861,"date":7141,"description":5869,"extension":664,"meta":7142,"navigation":666,"path":7143,"seo":7144,"stem":7145,"__hash__":7146},"content/embedded/TrustZone-M/hello_from_non_secure.md","Getting Hello World from the Non-Secure World (Part 1)",{"type":8,"value":5862,"toc":7128},[5863,5866,5870,5874,5882,5885,5889,5892,5898,5907,5914,5918,5924,5927,5930,5945,5949,5956,5976,5979,5983,5997,6039,6043,6049,6052,6078,6152,6156,6163,6166,6180,6395,6399,6402,6405,6423,6552,6556,6957,6961,6967,6970,6984,6989,6995,7046,7081,7085,7088,7123,7126],[11,5864,5860],{"id":5865},"getting-hello-world-from-the-non-secure-world-part-1",[680,5867,5869],{"id":5868},"the-first-switch-to-the-non-secure-world","The First Switch to the Non-Secure World",[44,5871,5873],{"id":5872},"_1-catch-up","1. Catch up",[15,5875,5876,5877,5881],{},"In ",[606,5878,5880],{"href":5879},"qemu_an521_setup","last post",", we created a QEMU-based test environment to test our ARMv8-M code. In this post, we will perform the first switch from the Secure world.",[15,5883,5884],{},"TrustZone-M chips always default to starting user code in the Secure world. It is our responsibility to configure the memory (text/RAM) regions for the Non-Secure world to use. Since all resources are Secure by default, if we skip the setup process in the Secure world and jump straight to Non-Secure code, the CPU will trigger a Secure Fault or Bus Fault when it attempts to fetch the first instruction.",[44,5886,5888],{"id":5887},"_2-create-a-non-secure-app","2. Create a Non-Secure App.",[15,5890,5891],{},"Since code executed in the Secure world and Non-Secure world basically acts like two separate programs, our Secure code acts as a secure bootloader to set up the environment and start the Non-Secure code.",[15,5893,5894,5895,5897],{},"First, we need to create a Non-Secure project. We can follow the same process as the ",[606,5896,5880],{"href":5879},", but this time we will name the project trustzone_non_secure_helloworld. Most steps will be the same, but we will change the print message to:",[87,5899,5901],{"className":112,"code":5900,"language":114,"meta":95,"style":95},"hprintln!(\"Hello from the Non-Secure World!\");\n",[52,5902,5903],{"__ignoreMap":95},[118,5904,5905],{"class":120,"line":121},[118,5906,5900],{},[15,5908,5909,5910,5913],{},"We also have to modify the ",[52,5911,5912],{},"memory.x"," file. We cannot use the same memory space as the Secure world, as this would cause a conflict.",[15,5915,5916],{},[37,5917,5912],{},[87,5919,5922],{"className":5920,"code":5921,"language":92},[90],"MEMORY\n{\n  /* Code stays in SSRAM1 (0x00000000) */\n  FLASH : ORIGIN = 0x00200000, LENGTH = 2M\n  \n  /* Stack/Data moves to SSRAM3 (Non-Secure Address) */\n  /* AN505 SSRAM3 starts at 0x28200000 and is 2MB */\n  RAM   : ORIGIN = 0x28200000, LENGTH = 2M\n}\n",[52,5923,5921],{"__ignoreMap":95},[15,5925,5926],{},"Notice now we are using a different code reigion. And instead of SSRAM2 we are now using SSRAM3. And both addresses are using the non-Secure address.",[15,5928,5929],{},"Now we just need to run cargo build to build the binary file.",[87,5931,5933],{"className":800,"code":5932,"language":802,"meta":95,"style":95},"$ file target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld\ntarget/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped\n",[52,5934,5935,5940],{"__ignoreMap":95},[118,5936,5937],{"class":120,"line":121},[118,5938,5939],{},"$ file target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld\n",[118,5941,5942],{"class":120,"line":127},[118,5943,5944],{},"target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped\n",[44,5946,5948],{"id":5947},"_3-the-secure-world-bootloader","3. The Secure World Bootloader",[15,5950,5951,5952,5955],{},"Now we need to write the Secure World code (",[52,5953,5954],{},"main.rs","). This code acts as the system manager. It has three main jobs:",[181,5957,5958,5964,5970],{},[184,5959,5960,5963],{},[37,5961,5962],{},"Configure the SAU (Security Attribution Unit):"," Tell the CPU core which memory addresses are Non-Secure.",[184,5965,5966,5969],{},[37,5967,5968],{},"Configure the MPC (Memory Protection Controller):"," Tell the memory bus that specific RAM banks (or halves of them) are Non-Secure.",[184,5971,5972,5975],{},[37,5973,5974],{},"Jump:"," Switch the CPU state and hand over control.",[15,5977,5978],{},"Let's break down the implementation function by function.",[568,5980,5982],{"id":5981},"step-1-imports-and-setup","Step 1: Imports and Setup",[15,5984,5985,5986,5988,5989,5992,5993,5996],{},"We need a standard ",[52,5987,2049],{}," setup. We use ",[52,5990,5991],{},"cortex_m"," for hardware access and ",[52,5994,5995],{},"cortex_m_semihosting"," for debug printing.",[87,5998,6000],{"className":112,"code":5999,"language":114,"meta":95,"style":95},"#![no_std]\n#![no_main]\n\nuse cortex_m::peripheral::sau;\nuse cortex_m_rt::entry;\nuse cortex_m_semihosting::hprintln;\nuse core::arch::asm;\nuse panic_halt as _;\n",[52,6001,6002,6006,6010,6014,6019,6024,6029,6034],{"__ignoreMap":95},[118,6003,6004],{"class":120,"line":121},[118,6005,3139],{},[118,6007,6008],{"class":120,"line":127},[118,6009,3158],{},[118,6011,6012],{"class":120,"line":133},[118,6013,2247],{"emptyLinePlaceholder":666},[118,6015,6016],{"class":120,"line":139},[118,6017,6018],{},"use cortex_m::peripheral::sau;\n",[118,6020,6021],{"class":120,"line":145},[118,6022,6023],{},"use cortex_m_rt::entry;\n",[118,6025,6026],{"class":120,"line":893},[118,6027,6028],{},"use cortex_m_semihosting::hprintln;\n",[118,6030,6031],{"class":120,"line":1168},[118,6032,6033],{},"use core::arch::asm;\n",[118,6035,6036],{"class":120,"line":1176},[118,6037,6038],{},"use panic_halt as _;\n",[568,6040,6042],{"id":6041},"step-2-configuring-the-sau-cpu-level","Step 2: Configuring the SAU (CPU Level)",[15,6044,3115,6045,6048],{},[37,6046,6047],{},"SAU (Security Attribution Unit)"," is inside the CPU. It checks every address the CPU tries to access. By default, everything is Secure. We must explicitly mark regions as Non-Secure (NS) so the NS application can run.",[15,6050,6051],{},"In this function, we define two regions:",[181,6053,6054,6067],{},[184,6055,6056,6059,6060,6063,6064,40],{},[37,6057,6058],{},"Region 0 (Flash/Code)",": ",[52,6061,6062],{},"0x0020_0000"," to ",[52,6065,6066],{},"0x003F_FFFF",[184,6068,6069,6059,6072,6063,6075,40],{},[37,6070,6071],{},"Region 1 (RAM/Data)",[52,6073,6074],{},"0x2820_0000",[52,6076,6077],{},"0x283F_FFFF",[87,6079,6081],{"className":112,"code":6080,"language":114,"meta":95,"style":95},"fn configure_sau(sau: &mut cortex_m::peripheral::SAU) {\n    unsafe {\n        sau.ctrl.write(sau::Ctrl(0));\n\n        sau.rnr.write(sau::Rnr(0));\n        sau.rbar.write(sau::Rbar(0x0020_0000));\n        sau.rlar.write(sau::Rlar((0x003F_FFFF & 0xFFFF_FFE0) | 1));\n\n        sau.rnr.write(sau::Rnr(1));\n        sau.rbar.write(sau::Rbar(0x2820_0000));\n        sau.rlar.write(sau::Rlar((0x283F_FFFF & 0xFFFF_FFE0) | 1));\n\n        sau.ctrl.write(sau::Ctrl(1));\n    }\n}\n",[52,6082,6083,6088,6092,6097,6101,6106,6111,6116,6120,6125,6130,6135,6139,6144,6148],{"__ignoreMap":95},[118,6084,6085],{"class":120,"line":121},[118,6086,6087],{},"fn configure_sau(sau: &mut cortex_m::peripheral::SAU) {\n",[118,6089,6090],{"class":120,"line":127},[118,6091,4748],{},[118,6093,6094],{"class":120,"line":133},[118,6095,6096],{},"        sau.ctrl.write(sau::Ctrl(0));\n",[118,6098,6099],{"class":120,"line":139},[118,6100,2247],{"emptyLinePlaceholder":666},[118,6102,6103],{"class":120,"line":145},[118,6104,6105],{},"        sau.rnr.write(sau::Rnr(0));\n",[118,6107,6108],{"class":120,"line":893},[118,6109,6110],{},"        sau.rbar.write(sau::Rbar(0x0020_0000));\n",[118,6112,6113],{"class":120,"line":1168},[118,6114,6115],{},"        sau.rlar.write(sau::Rlar((0x003F_FFFF & 0xFFFF_FFE0) | 1));\n",[118,6117,6118],{"class":120,"line":1176},[118,6119,2247],{"emptyLinePlaceholder":666},[118,6121,6122],{"class":120,"line":1184},[118,6123,6124],{},"        sau.rnr.write(sau::Rnr(1));\n",[118,6126,6127],{"class":120,"line":1195},[118,6128,6129],{},"        sau.rbar.write(sau::Rbar(0x2820_0000));\n",[118,6131,6132],{"class":120,"line":1203},[118,6133,6134],{},"        sau.rlar.write(sau::Rlar((0x283F_FFFF & 0xFFFF_FFE0) | 1));\n",[118,6136,6137],{"class":120,"line":1209},[118,6138,2247],{"emptyLinePlaceholder":666},[118,6140,6141],{"class":120,"line":1223},[118,6142,6143],{},"        sau.ctrl.write(sau::Ctrl(1));\n",[118,6145,6146],{"class":120,"line":1229},[118,6147,2753],{},[118,6149,6150],{"class":120,"line":1241},[118,6151,2644],{},[568,6153,6155],{"id":6154},"step-3-configuring-the-mpc-bus-level","Step 3: Configuring the MPC (Bus Level)",[15,6157,6158,6159,6162],{},"While the SAU lives inside the CPU, the ",[37,6160,6161],{},"MPC (Memory Protection Controller)"," lives on the bus. It prevents Secure data from leaking even if the CPU allows access.",[15,6164,6165],{},"We use two different strategies here:",[181,6167,6168,6174],{},[184,6169,6170,6173],{},[37,6171,6172],{},"Split Strategy (SSRAM0)",": We calculate the midpoint of the memory block. We leave the lower half as Secure (default) and unlock the upper half for Non-Secure use. This is useful for shared memory.",[184,6175,6176,6179],{},[37,6177,6178],{},"Full Unlock Strategy (SSRAM3)",": We unlock the entire bank because the Non-Secure world needs it for its Stack.",[87,6181,6183],{"className":112,"code":6182,"language":114,"meta":95,"style":95},"fn configure_mpc() {\n    // --- 1. Configure SSRAM0 (Split Region) ---\n    // Physical Base: 0x5800_7000\n    {\n        let base = 0x5800_7000;\n        let ctrl_reg = base as *mut u32;\n        let blk_max  = (base + 0x10) as *mut u32;\n        let blk_idx  = (base + 0x18) as *mut u32;\n        let blk_lut  = (base + 0x1C) as *mut u32;\n\n        unsafe { ctrl_reg.write_volatile(0x110); }\n\n        let max_index = unsafe { blk_max.read_volatile() };\n        let total_indices = max_index + 1;\n        let midpoint = total_indices / 2;\n\n        unsafe { blk_idx.write_volatile(midpoint); }\n\n        for _ in midpoint..total_indices {\n            unsafe { blk_lut.write_volatile(0xFFFF_FFFF); }\n        }\n    }\n\n    // --- 2. Configure SSRAM3 (Full Unlock) ---\n    // Physical Base: 0x5800_9000\n    {\n        let base = 0x5800_9000;\n        let ctrl_reg = base as *mut u32;\n        let blk_max  = (base + 0x10) as *mut u32;\n        let blk_idx  = (base + 0x18) as *mut u32;\n        let blk_lut  = (base + 0x1C) as *mut u32;\n\n        unsafe {\n            ctrl_reg.write_volatile(0x110);\n            blk_idx.write_volatile(0);\n        }\n        \n        let max_idx = unsafe { blk_max.read_volatile() };\n        for _ in 0..=max_idx {\n            unsafe { blk_lut.write_volatile(0xFFFF_FFFF); }\n        }\n    }\n\n    cortex_m::asm::dsb();\n    cortex_m::asm::isb();\n}\n",[52,6184,6185,6190,6195,6200,6204,6209,6214,6219,6224,6229,6233,6238,6242,6247,6252,6257,6261,6266,6270,6275,6280,6284,6288,6292,6297,6302,6306,6311,6315,6319,6323,6327,6331,6336,6341,6346,6350,6355,6360,6365,6369,6373,6377,6381,6386,6391],{"__ignoreMap":95},[118,6186,6187],{"class":120,"line":121},[118,6188,6189],{},"fn configure_mpc() {\n",[118,6191,6192],{"class":120,"line":127},[118,6193,6194],{},"    // --- 1. Configure SSRAM0 (Split Region) ---\n",[118,6196,6197],{"class":120,"line":133},[118,6198,6199],{},"    // Physical Base: 0x5800_7000\n",[118,6201,6202],{"class":120,"line":139},[118,6203,2735],{},[118,6205,6206],{"class":120,"line":145},[118,6207,6208],{},"        let base = 0x5800_7000;\n",[118,6210,6211],{"class":120,"line":893},[118,6212,6213],{},"        let ctrl_reg = base as *mut u32;\n",[118,6215,6216],{"class":120,"line":1168},[118,6217,6218],{},"        let blk_max  = (base + 0x10) as *mut u32;\n",[118,6220,6221],{"class":120,"line":1176},[118,6222,6223],{},"        let blk_idx  = (base + 0x18) as *mut u32;\n",[118,6225,6226],{"class":120,"line":1184},[118,6227,6228],{},"        let blk_lut  = (base + 0x1C) as *mut u32;\n",[118,6230,6231],{"class":120,"line":1195},[118,6232,2247],{"emptyLinePlaceholder":666},[118,6234,6235],{"class":120,"line":1203},[118,6236,6237],{},"        unsafe { ctrl_reg.write_volatile(0x110); }\n",[118,6239,6240],{"class":120,"line":1209},[118,6241,2247],{"emptyLinePlaceholder":666},[118,6243,6244],{"class":120,"line":1223},[118,6245,6246],{},"        let max_index = unsafe { blk_max.read_volatile() };\n",[118,6248,6249],{"class":120,"line":1229},[118,6250,6251],{},"        let total_indices = max_index + 1;\n",[118,6253,6254],{"class":120,"line":1241},[118,6255,6256],{},"        let midpoint = total_indices / 2;\n",[118,6258,6259],{"class":120,"line":1249},[118,6260,2247],{"emptyLinePlaceholder":666},[118,6262,6263],{"class":120,"line":1266},[118,6264,6265],{},"        unsafe { blk_idx.write_volatile(midpoint); }\n",[118,6267,6268],{"class":120,"line":1285},[118,6269,2247],{"emptyLinePlaceholder":666},[118,6271,6272],{"class":120,"line":1291},[118,6273,6274],{},"        for _ in midpoint..total_indices {\n",[118,6276,6277],{"class":120,"line":1304},[118,6278,6279],{},"            unsafe { blk_lut.write_volatile(0xFFFF_FFFF); }\n",[118,6281,6282],{"class":120,"line":1310},[118,6283,3394],{},[118,6285,6286],{"class":120,"line":1322},[118,6287,2753],{},[118,6289,6290],{"class":120,"line":1328},[118,6291,2247],{"emptyLinePlaceholder":666},[118,6293,6294],{"class":120,"line":1340},[118,6295,6296],{},"    // --- 2. Configure SSRAM3 (Full Unlock) ---\n",[118,6298,6299],{"class":120,"line":1347},[118,6300,6301],{},"    // Physical Base: 0x5800_9000\n",[118,6303,6304],{"class":120,"line":1358},[118,6305,2735],{},[118,6307,6308],{"class":120,"line":1364},[118,6309,6310],{},"        let base = 0x5800_9000;\n",[118,6312,6313],{"class":120,"line":1377},[118,6314,6213],{},[118,6316,6317],{"class":120,"line":1388},[118,6318,6218],{},[118,6320,6321],{"class":120,"line":1395},[118,6322,6223],{},[118,6324,6325],{"class":120,"line":1407},[118,6326,6228],{},[118,6328,6329],{"class":120,"line":1413},[118,6330,2247],{"emptyLinePlaceholder":666},[118,6332,6333],{"class":120,"line":2469},[118,6334,6335],{},"        unsafe {\n",[118,6337,6338],{"class":120,"line":2475},[118,6339,6340],{},"            ctrl_reg.write_volatile(0x110);\n",[118,6342,6343],{"class":120,"line":2481},[118,6344,6345],{},"            blk_idx.write_volatile(0);\n",[118,6347,6348],{"class":120,"line":2487},[118,6349,3394],{},[118,6351,6352],{"class":120,"line":2493},[118,6353,6354],{},"        \n",[118,6356,6357],{"class":120,"line":2498},[118,6358,6359],{},"        let max_idx = unsafe { blk_max.read_volatile() };\n",[118,6361,6362],{"class":120,"line":2504},[118,6363,6364],{},"        for _ in 0..=max_idx {\n",[118,6366,6367],{"class":120,"line":2510},[118,6368,6279],{},[118,6370,6371],{"class":120,"line":2516},[118,6372,3394],{},[118,6374,6375],{"class":120,"line":2522},[118,6376,2753],{},[118,6378,6379],{"class":120,"line":2527},[118,6380,2247],{"emptyLinePlaceholder":666},[118,6382,6383],{"class":120,"line":2533},[118,6384,6385],{},"    cortex_m::asm::dsb();\n",[118,6387,6388],{"class":120,"line":2539},[118,6389,6390],{},"    cortex_m::asm::isb();\n",[118,6392,6393],{"class":120,"line":2545},[118,6394,2644],{},[568,6396,6398],{"id":6397},"step-4-the-jump-main","Step 4: The Jump (Main)",[15,6400,6401],{},"Finally, we perform the switch. This requires reading the Non-Secure Vector Table to find the correct Stack Pointer and Reset Vector.",[15,6403,6404],{},"We then use inline assembly to:",[181,6406,6407,6413],{},[184,6408,6409,6412],{},[52,6410,6411],{},"msr msp_ns",": Set the Non-Secure Main Stack Pointer.",[184,6414,6415,6418,6419,6422],{},[52,6416,6417],{},"bxns",": Branch and Exchange to Non-Secure state. The address we jump to must have the Least Significant Bit (LSB) cleared (",[52,6420,6421],{},"& !1",") to indicate the target state.",[87,6424,6426],{"className":112,"code":6425,"language":114,"meta":95,"style":95},"#[entry]\nfn main() -> ! {\n    hprintln!(\"Secure World: Initializing...\");\n\n    let mut peripherals = cortex_m::Peripherals::take().unwrap();\n    \n    configure_sau(&mut peripherals.SAU);\n    configure_mpc();\n\n    let ns_vector_table_addr: *const u32 = 0x0020_0000 as *const u32;\n\n    hprintln!(\"Secure World: Jumping to Non-Secure...\");\n\n    unsafe {\n        let ns_msp = *ns_vector_table_addr;\n        let ns_reset_vector = *ns_vector_table_addr.add(1);\n\n        asm!(\n            \"msr msp_ns, {ns_msp}\",\n            \"bxns {ns_reset_vector}\",\n            ns_msp = in(reg) ns_msp,\n            ns_reset_vector = in(reg) ns_reset_vector & !1, \n        );\n    }\n    \n    loop {}\n}\n",[52,6427,6428,6433,6438,6443,6447,6452,6457,6462,6467,6471,6476,6480,6485,6489,6493,6498,6503,6507,6512,6517,6522,6527,6532,6536,6540,6544,6548],{"__ignoreMap":95},[118,6429,6430],{"class":120,"line":121},[118,6431,6432],{},"#[entry]\n",[118,6434,6435],{"class":120,"line":127},[118,6436,6437],{},"fn main() -> ! {\n",[118,6439,6440],{"class":120,"line":133},[118,6441,6442],{},"    hprintln!(\"Secure World: Initializing...\");\n",[118,6444,6445],{"class":120,"line":139},[118,6446,2247],{"emptyLinePlaceholder":666},[118,6448,6449],{"class":120,"line":145},[118,6450,6451],{},"    let mut peripherals = cortex_m::Peripherals::take().unwrap();\n",[118,6453,6454],{"class":120,"line":893},[118,6455,6456],{},"    \n",[118,6458,6459],{"class":120,"line":1168},[118,6460,6461],{},"    configure_sau(&mut peripherals.SAU);\n",[118,6463,6464],{"class":120,"line":1176},[118,6465,6466],{},"    configure_mpc();\n",[118,6468,6469],{"class":120,"line":1184},[118,6470,2247],{"emptyLinePlaceholder":666},[118,6472,6473],{"class":120,"line":1195},[118,6474,6475],{},"    let ns_vector_table_addr: *const u32 = 0x0020_0000 as *const u32;\n",[118,6477,6478],{"class":120,"line":1203},[118,6479,2247],{"emptyLinePlaceholder":666},[118,6481,6482],{"class":120,"line":1209},[118,6483,6484],{},"    hprintln!(\"Secure World: Jumping to Non-Secure...\");\n",[118,6486,6487],{"class":120,"line":1223},[118,6488,2247],{"emptyLinePlaceholder":666},[118,6490,6491],{"class":120,"line":1229},[118,6492,4748],{},[118,6494,6495],{"class":120,"line":1241},[118,6496,6497],{},"        let ns_msp = *ns_vector_table_addr;\n",[118,6499,6500],{"class":120,"line":1249},[118,6501,6502],{},"        let ns_reset_vector = *ns_vector_table_addr.add(1);\n",[118,6504,6505],{"class":120,"line":1266},[118,6506,2247],{"emptyLinePlaceholder":666},[118,6508,6509],{"class":120,"line":1285},[118,6510,6511],{},"        asm!(\n",[118,6513,6514],{"class":120,"line":1291},[118,6515,6516],{},"            \"msr msp_ns, {ns_msp}\",\n",[118,6518,6519],{"class":120,"line":1304},[118,6520,6521],{},"            \"bxns {ns_reset_vector}\",\n",[118,6523,6524],{"class":120,"line":1310},[118,6525,6526],{},"            ns_msp = in(reg) ns_msp,\n",[118,6528,6529],{"class":120,"line":1322},[118,6530,6531],{},"            ns_reset_vector = in(reg) ns_reset_vector & !1, \n",[118,6533,6534],{"class":120,"line":1328},[118,6535,4616],{},[118,6537,6538],{"class":120,"line":1340},[118,6539,2753],{},[118,6541,6542],{"class":120,"line":1347},[118,6543,6456],{},[118,6545,6546],{"class":120,"line":1358},[118,6547,3216],{},[118,6549,6550],{"class":120,"line":1364},[118,6551,2644],{},[568,6553,6555],{"id":6554},"step-5-full-source-code-mainrs","Step 5: Full Source Code (main.rs)",[87,6557,6559],{"className":112,"code":6558,"language":114,"meta":95,"style":95},"#![no_std]\n#![no_main]\n\nuse cortex_m::peripheral::sau;\nuse cortex_m_rt::entry;\nuse cortex_m_semihosting::hprintln;\nuse core::arch::asm;\nuse panic_halt as _;\n\nfn configure_sau(sau: &mut cortex_m::peripheral::SAU) {\n    unsafe {\n        sau.ctrl.write(sau::Ctrl(0));\n\n        sau.rnr.write(sau::Rnr(0));\n        sau.rbar.write(sau::Rbar(0x0020_0000));\n        sau.rlar.write(sau::Rlar((0x003F_FFFF & 0xFFFF_FFE0) | 1));\n\n        sau.rnr.write(sau::Rnr(1));\n        sau.rbar.write(sau::Rbar(0x2820_0000));\n        sau.rlar.write(sau::Rlar((0x283F_FFFF & 0xFFFF_FFE0) | 1));\n\n        sau.ctrl.write(sau::Ctrl(1));\n    }\n}\n\nfn configure_mpc() {\n    // --- 1. Configure SSRAM0 (Split Region) ---\n    {\n        let base = 0x5800_7000;\n        let ctrl_reg = base as *mut u32;\n        let blk_max  = (base + 0x10) as *mut u32;\n        let blk_idx  = (base + 0x18) as *mut u32;\n        let blk_lut  = (base + 0x1C) as *mut u32;\n\n        unsafe { ctrl_reg.write_volatile(0x110); }\n\n        let max_index = unsafe { blk_max.read_volatile() };\n        let total_indices = max_index + 1;\n        let midpoint = total_indices / 2;\n\n        unsafe { blk_idx.write_volatile(midpoint); }\n\n        for _ in midpoint..total_indices {\n            unsafe { blk_lut.write_volatile(0xFFFF_FFFF); }\n        }\n    }\n\n    // --- 2. Configure SSRAM3 (Full Unlock) ---\n    {\n        let base = 0x5800_9000;\n        let ctrl_reg = base as *mut u32;\n        let blk_max  = (base + 0x10) as *mut u32;\n        let blk_idx  = (base + 0x18) as *mut u32;\n        let blk_lut  = (base + 0x1C) as *mut u32;\n\n        unsafe {\n            ctrl_reg.write_volatile(0x110);\n            blk_idx.write_volatile(0);\n        }\n        \n        let max_idx = unsafe { blk_max.read_volatile() };\n        for _ in 0..=max_idx {\n            unsafe { blk_lut.write_volatile(0xFFFF_FFFF); }\n        }\n    }\n\n    cortex_m::asm::dsb();\n    cortex_m::asm::isb();\n}\n\n#[entry]\nfn main() -> ! {\n    hprintln!(\"Secure World: Initializing...\");\n\n    let mut peripherals = cortex_m::Peripherals::take().unwrap();\n    \n    configure_sau(&mut peripherals.SAU);\n    configure_mpc();\n\n    let ns_vector_table_addr: *const u32 = 0x0020_0000 as *const u32;\n\n    hprintln!(\"Secure World: Jumping to Non-Secure...\");\n\n    unsafe {\n        let ns_msp = *ns_vector_table_addr;\n        let ns_reset_vector = *ns_vector_table_addr.add(1);\n\n        asm!(\n            \"msr msp_ns, {ns_msp}\",\n            \"bxns {ns_reset_vector}\",\n            ns_msp = in(reg) ns_msp,\n            ns_reset_vector = in(reg) ns_reset_vector & !1, \n        );\n    }\n    \n    loop {}\n}\n",[52,6560,6561,6565,6569,6573,6577,6581,6585,6589,6593,6597,6601,6605,6609,6613,6617,6621,6625,6629,6633,6637,6641,6645,6649,6653,6657,6661,6665,6669,6673,6677,6681,6685,6689,6693,6697,6701,6705,6709,6713,6717,6721,6725,6729,6733,6737,6741,6745,6749,6753,6757,6761,6765,6769,6773,6777,6781,6785,6789,6793,6797,6801,6805,6809,6813,6817,6821,6825,6829,6833,6837,6841,6845,6849,6853,6857,6861,6865,6869,6873,6877,6881,6885,6889,6893,6897,6901,6905,6909,6913,6917,6922,6927,6932,6937,6942,6947,6952],{"__ignoreMap":95},[118,6562,6563],{"class":120,"line":121},[118,6564,3139],{},[118,6566,6567],{"class":120,"line":127},[118,6568,3158],{},[118,6570,6571],{"class":120,"line":133},[118,6572,2247],{"emptyLinePlaceholder":666},[118,6574,6575],{"class":120,"line":139},[118,6576,6018],{},[118,6578,6579],{"class":120,"line":145},[118,6580,6023],{},[118,6582,6583],{"class":120,"line":893},[118,6584,6028],{},[118,6586,6587],{"class":120,"line":1168},[118,6588,6033],{},[118,6590,6591],{"class":120,"line":1176},[118,6592,6038],{},[118,6594,6595],{"class":120,"line":1184},[118,6596,2247],{"emptyLinePlaceholder":666},[118,6598,6599],{"class":120,"line":1195},[118,6600,6087],{},[118,6602,6603],{"class":120,"line":1203},[118,6604,4748],{},[118,6606,6607],{"class":120,"line":1209},[118,6608,6096],{},[118,6610,6611],{"class":120,"line":1223},[118,6612,2247],{"emptyLinePlaceholder":666},[118,6614,6615],{"class":120,"line":1229},[118,6616,6105],{},[118,6618,6619],{"class":120,"line":1241},[118,6620,6110],{},[118,6622,6623],{"class":120,"line":1249},[118,6624,6115],{},[118,6626,6627],{"class":120,"line":1266},[118,6628,2247],{"emptyLinePlaceholder":666},[118,6630,6631],{"class":120,"line":1285},[118,6632,6124],{},[118,6634,6635],{"class":120,"line":1291},[118,6636,6129],{},[118,6638,6639],{"class":120,"line":1304},[118,6640,6134],{},[118,6642,6643],{"class":120,"line":1310},[118,6644,2247],{"emptyLinePlaceholder":666},[118,6646,6647],{"class":120,"line":1322},[118,6648,6143],{},[118,6650,6651],{"class":120,"line":1328},[118,6652,2753],{},[118,6654,6655],{"class":120,"line":1340},[118,6656,2644],{},[118,6658,6659],{"class":120,"line":1347},[118,6660,2247],{"emptyLinePlaceholder":666},[118,6662,6663],{"class":120,"line":1358},[118,6664,6189],{},[118,6666,6667],{"class":120,"line":1364},[118,6668,6194],{},[118,6670,6671],{"class":120,"line":1377},[118,6672,2735],{},[118,6674,6675],{"class":120,"line":1388},[118,6676,6208],{},[118,6678,6679],{"class":120,"line":1395},[118,6680,6213],{},[118,6682,6683],{"class":120,"line":1407},[118,6684,6218],{},[118,6686,6687],{"class":120,"line":1413},[118,6688,6223],{},[118,6690,6691],{"class":120,"line":2469},[118,6692,6228],{},[118,6694,6695],{"class":120,"line":2475},[118,6696,2247],{"emptyLinePlaceholder":666},[118,6698,6699],{"class":120,"line":2481},[118,6700,6237],{},[118,6702,6703],{"class":120,"line":2487},[118,6704,2247],{"emptyLinePlaceholder":666},[118,6706,6707],{"class":120,"line":2493},[118,6708,6246],{},[118,6710,6711],{"class":120,"line":2498},[118,6712,6251],{},[118,6714,6715],{"class":120,"line":2504},[118,6716,6256],{},[118,6718,6719],{"class":120,"line":2510},[118,6720,2247],{"emptyLinePlaceholder":666},[118,6722,6723],{"class":120,"line":2516},[118,6724,6265],{},[118,6726,6727],{"class":120,"line":2522},[118,6728,2247],{"emptyLinePlaceholder":666},[118,6730,6731],{"class":120,"line":2527},[118,6732,6274],{},[118,6734,6735],{"class":120,"line":2533},[118,6736,6279],{},[118,6738,6739],{"class":120,"line":2539},[118,6740,3394],{},[118,6742,6743],{"class":120,"line":2545},[118,6744,2753],{},[118,6746,6747],{"class":120,"line":2550},[118,6748,2247],{"emptyLinePlaceholder":666},[118,6750,6751],{"class":120,"line":2556},[118,6752,6296],{},[118,6754,6755],{"class":120,"line":2562},[118,6756,2735],{},[118,6758,6759],{"class":120,"line":2568},[118,6760,6310],{},[118,6762,6763],{"class":120,"line":2573},[118,6764,6213],{},[118,6766,6767],{"class":120,"line":2579},[118,6768,6218],{},[118,6770,6771],{"class":120,"line":2585},[118,6772,6223],{},[118,6774,6775],{"class":120,"line":2591},[118,6776,6228],{},[118,6778,6779],{"class":120,"line":2596},[118,6780,2247],{"emptyLinePlaceholder":666},[118,6782,6783],{"class":120,"line":2602},[118,6784,6335],{},[118,6786,6787],{"class":120,"line":2608},[118,6788,6340],{},[118,6790,6791],{"class":120,"line":2613},[118,6792,6345],{},[118,6794,6795],{"class":120,"line":2619},[118,6796,3394],{},[118,6798,6799],{"class":120,"line":2624},[118,6800,6354],{},[118,6802,6803],{"class":120,"line":2630},[118,6804,6359],{},[118,6806,6807],{"class":120,"line":2635},[118,6808,6364],{},[118,6810,6811],{"class":120,"line":2641},[118,6812,6279],{},[118,6814,6815],{"class":120,"line":2647},[118,6816,3394],{},[118,6818,6819],{"class":120,"line":2652},[118,6820,2753],{},[118,6822,6823],{"class":120,"line":2658},[118,6824,2247],{"emptyLinePlaceholder":666},[118,6826,6827],{"class":120,"line":2664},[118,6828,6385],{},[118,6830,6831],{"class":120,"line":2670},[118,6832,6390],{},[118,6834,6835],{"class":120,"line":2675},[118,6836,2644],{},[118,6838,6839],{"class":120,"line":2681},[118,6840,2247],{"emptyLinePlaceholder":666},[118,6842,6843],{"class":120,"line":2686},[118,6844,6432],{},[118,6846,6847],{"class":120,"line":2692},[118,6848,6437],{},[118,6850,6851],{"class":120,"line":2697},[118,6852,6442],{},[118,6854,6855],{"class":120,"line":2703},[118,6856,2247],{"emptyLinePlaceholder":666},[118,6858,6859],{"class":120,"line":2709},[118,6860,6451],{},[118,6862,6863],{"class":120,"line":2714},[118,6864,6456],{},[118,6866,6867],{"class":120,"line":2720},[118,6868,6461],{},[118,6870,6871],{"class":120,"line":2726},[118,6872,6466],{},[118,6874,6875],{"class":120,"line":2732},[118,6876,2247],{"emptyLinePlaceholder":666},[118,6878,6879],{"class":120,"line":2738},[118,6880,6475],{},[118,6882,6883],{"class":120,"line":2744},[118,6884,2247],{"emptyLinePlaceholder":666},[118,6886,6887],{"class":120,"line":2750},[118,6888,6484],{},[118,6890,6891],{"class":120,"line":2756},[118,6892,2247],{"emptyLinePlaceholder":666},[118,6894,6895],{"class":120,"line":4445},[118,6896,4748],{},[118,6898,6899],{"class":120,"line":4451},[118,6900,6497],{},[118,6902,6903],{"class":120,"line":4457},[118,6904,6502],{},[118,6906,6907],{"class":120,"line":4463},[118,6908,2247],{"emptyLinePlaceholder":666},[118,6910,6911],{"class":120,"line":4469},[118,6912,6511],{},[118,6914,6915],{"class":120,"line":4474},[118,6916,6516],{},[118,6918,6920],{"class":120,"line":6919},90,[118,6921,6521],{},[118,6923,6925],{"class":120,"line":6924},91,[118,6926,6526],{},[118,6928,6930],{"class":120,"line":6929},92,[118,6931,6531],{},[118,6933,6935],{"class":120,"line":6934},93,[118,6936,4616],{},[118,6938,6940],{"class":120,"line":6939},94,[118,6941,2753],{},[118,6943,6945],{"class":120,"line":6944},95,[118,6946,6456],{},[118,6948,6950],{"class":120,"line":6949},96,[118,6951,3216],{},[118,6953,6955],{"class":120,"line":6954},97,[118,6956,2644],{},[44,6958,6960],{"id":6959},"_4-configuring-the-runner-cargoconfigtoml","4. Configuring the Runner (.cargo/config.toml)",[15,6962,6963,6964,6966],{},"We have written the Secure bootloader and the Non-Secure application. Now, we need a way to run them together. This is where ",[52,6965,3896],{}," comes in. It automates the complex QEMU command required to load two separate binaries into memory at once.",[15,6968,6969],{},"For a TrustZone setup, QEMU needs to know two things:",[181,6971,6972,6978],{},[184,6973,6974,6977],{},[37,6975,6976],{},"Where is the Secure code?"," (This is our current project, passed via -kernel).",[184,6979,6980,6983],{},[37,6981,6982],{},"Where is the Non-Secure code?"," (This is the external binary we built in Step 2, passed via -device loader).",[15,6985,6986],{},[37,6987,6988],{},"The Configuration Breakdown",[15,6990,6991,6992,6994],{},"Create or edit ",[52,6993,3896],{}," in your Secure project root:",[87,6996,6998],{"className":3435,"code":6997,"language":3437,"meta":95,"style":95},"[build]\ntarget = \"thumbv8m.main-none-eabi\" # Cortex-M33 (ARMv8-M Mainline)\n\n[target.thumbv8m.main-none-eabi]\nrunner = \"\"\"qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial mon:stdio -semihosting \\\n -device loader,file=../trustzone_non_secure_helloworld/target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld,addr=0x00200000 \\\n -kernel \"\"\"\nrustflags = [\n  \"-C\", \"link-arg=-Tlink.x\"\n]\n",[52,6999,7000,7004,7009,7013,7018,7023,7028,7033,7037,7042],{"__ignoreMap":95},[118,7001,7002],{"class":120,"line":121},[118,7003,3974],{},[118,7005,7006],{"class":120,"line":127},[118,7007,7008],{},"target = \"thumbv8m.main-none-eabi\" # Cortex-M33 (ARMv8-M Mainline)\n",[118,7010,7011],{"class":120,"line":133},[118,7012,2247],{"emptyLinePlaceholder":666},[118,7014,7015],{"class":120,"line":139},[118,7016,7017],{},"[target.thumbv8m.main-none-eabi]\n",[118,7019,7020],{"class":120,"line":145},[118,7021,7022],{},"runner = \"\"\"qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial mon:stdio -semihosting \\\n",[118,7024,7025],{"class":120,"line":893},[118,7026,7027],{}," -device loader,file=../trustzone_non_secure_helloworld/target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld,addr=0x00200000 \\\n",[118,7029,7030],{"class":120,"line":1168},[118,7031,7032],{}," -kernel \"\"\"\n",[118,7034,7035],{"class":120,"line":1176},[118,7036,3916],{},[118,7038,7039],{"class":120,"line":1184},[118,7040,7041],{},"  \"-C\", \"link-arg=-Tlink.x\"\n",[118,7043,7044],{"class":120,"line":1195},[118,7045,3956],{},[181,7047,7048,7067],{},[184,7049,7050,7053,7054,7057,7058],{},[52,7051,7052],{},"-device loader,file=...,addr=0x00200000",": This is the crucial part for TrustZone. It tells QEMU to side-load the Non-Secure binary into memory at address ",[52,7055,7056],{},"0x00200000"," before starting the CPU.",[181,7059,7060],{},[184,7061,7062,7063,7066],{},"Note: The path ",[52,7064,7065],{},"../trustzone_non_secure_helloworld/..."," assumes your Non-Secure project folder is next to your Secure project folder. Adjust if necessary.",[184,7068,7069,7072,7073,7076,7077,7080],{},[52,7070,7071],{},"-kernel",": This is where Cargo inserts the path to the Secure binary (the current project) automatically. This binary is loaded at the default reset address (usually ",[52,7074,7075],{},"0x00000000"," or ",[52,7078,7079],{},"0x10000000",") and the CPU starts executing here.",[44,7082,7084],{"id":7083},"_5-running-the-system","5. Running the System",[15,7086,7087],{},"Now, simply run:",[87,7089,7091],{"className":800,"code":7090,"language":802,"meta":95,"style":95},"$ cargo run\n    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s\n     Running `qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial 'mon:stdio' -semihosting -device loader,file=../trustzone_non_secure_helloworld/target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld,addr=0x00200000 -kernel target/thumbv8m.main-none-eabi/debug/trustzone_helloworld`\nSecure World: Initializing...\nSecure World: Jumping to Non-Secure...\nHello from the Non-Secure World!\n",[52,7092,7093,7098,7103,7108,7113,7118],{"__ignoreMap":95},[118,7094,7095],{"class":120,"line":121},[118,7096,7097],{},"$ cargo run\n",[118,7099,7100],{"class":120,"line":127},[118,7101,7102],{},"    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s\n",[118,7104,7105],{"class":120,"line":133},[118,7106,7107],{},"     Running `qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial 'mon:stdio' -semihosting -device loader,file=../trustzone_non_secure_helloworld/target/thumbv8m.main-none-eabi/debug/trustzone_non_secure_helloworld,addr=0x00200000 -kernel target/thumbv8m.main-none-eabi/debug/trustzone_helloworld`\n",[118,7109,7110],{"class":120,"line":139},[118,7111,7112],{},"Secure World: Initializing...\n",[118,7114,7115],{"class":120,"line":145},[118,7116,7117],{},"Secure World: Jumping to Non-Secure...\n",[118,7119,7120],{"class":120,"line":893},[118,7121,7122],{},"Hello from the Non-Secure World!\n",[15,7124,7125],{},"Congratulations! We have successfully booted a Secure world manager and handed off control to a Non-Secure application.",[646,7127,648],{},{"title":95,"searchDepth":127,"depth":127,"links":7129},[7130,7131,7132,7139,7140],{"id":5872,"depth":127,"text":5873},{"id":5887,"depth":127,"text":5888},{"id":5947,"depth":127,"text":5948,"children":7133},[7134,7135,7136,7137,7138],{"id":5981,"depth":133,"text":5982},{"id":6041,"depth":133,"text":6042},{"id":6154,"depth":133,"text":6155},{"id":6397,"depth":133,"text":6398},{"id":6554,"depth":133,"text":6555},{"id":6959,"depth":127,"text":6960},{"id":7083,"depth":127,"text":7084},"2026-2-8",{},"/embedded/trustzone-m/hello_from_non_secure",{"title":5860,"description":5869},"embedded/TrustZone-M/hello_from_non_secure","WNsoZCUuTw1kXFsp3f4ca2Hy87niU7_xcCjErpUMi2k",{"id":7148,"title":7149,"body":7150,"date":7645,"description":7158,"extension":664,"meta":7646,"navigation":666,"path":7647,"seo":7648,"stem":7649,"__hash__":7650},"content/embedded/TrustZone-M/qemu_an521_setup.md","Setup a QEMU based TrustZone-M development environment (Part 0)",{"type":8,"value":7151,"toc":7634},[7152,7155,7159,7161,7172,7183,7189,7193,7197,7211,7222,7226,7233,7264,7268,7279,7288,7294,7303,7306,7315,7320,7343,7347,7354,7361,7371,7409,7416,7425,7431,7436,7444,7465,7471,7475,7478,7482,7584,7588,7593,7603,7632],[11,7153,7149],{"id":7154},"setup-a-qemu-based-trustzone-m-development-environment-part-0",[680,7156,7158],{"id":7157},"setting-up-an-emulated-environment-for-bare-metal-testing","Setting up an emulated environment for bare-metal testing",[44,7160,699],{"id":698},[15,7162,7163,7164,7167,7168,7171],{},"Recently, I have been working on an embedded project based on the ",[37,7165,7166],{},"RP2350"," microcontroller, which features ",[37,7169,7170],{},"TrustZone-M"," support. Unlike the general software development space, many small projects in the embedded world do not allocate many resources to securing their code. Consequently, most embedded projects can be easily reverse-engineered via exposed debug ports or by simply reading the flash memory.",[15,7173,7174,7175,7178,7179,7182],{},"Since the product I am working on requires firmware encryption, certain parts of the code must be encrypted and remain inaccessible to the user. While the device allows users to execute custom code, I could theoretically enforce permission control based on ",[37,7176,7177],{},"privilege levels",". In that scenario, privileged code would be responsible for decrypting secrets and configuring the ",[37,7180,7181],{},"Memory Protection Unit (MPU)"," to prevent user code from reading or writing to sensitive areas. User code would then interact with the secret code via a trampoline syscall.",[15,7184,7185,7186,7188],{},"However, this safety model requires all security measures to be implemented within the privileged code. If the syscall or the memory barrier setup has a bug, a user could easily gain access to the encryption keys. The attack surface in privileged code is simply too large. Instead of relying on a single layer of privileged/unprivileged levels, ",[37,7187,7170],{}," allows us to create a parallel \"Secure World.\" This separates security-sensitive functions into a distinct processor state with its own privileged levels and a significantly smaller attack surface.",[44,7190,7192],{"id":7191},"_1-select-the-qemu-target","1. Select the QEMU target",[568,7194,7196],{"id":7195},"why-qemu","Why QEMU?",[15,7198,7199,7200,7202,7203,7206,7207,7210],{},"While the ",[37,7201,7166],{}," has excellent documentation and support, flashing a Pico 2 for every minor code change is tedious. While using a debugger is possible, constantly resetting and re-flashing the hardware is inconvenient for rapid iteration. The RP2350 is an SoC featuring ",[37,7204,7205],{},"dual Cortex-M33 cores"," based on the ",[37,7208,7209],{},"ARMv8-M Mainline"," architecture.",[15,7212,7213,7214,7217,7218,7221],{},"Instead of relying solely on physical hardware, we can use ",[37,7215,7216],{},"QEMU"," to emulate the target. Since QEMU does not yet have a dedicated machine model for the RP2350, we must select a similar target that shares the M33 design. An excellent option is the ",[37,7219,7220],{},"mps2-an521",", which mirrors the RP2350’s dual-core Cortex-M33 configuration and TrustZone capabilities.",[568,7223,7225],{"id":7224},"the-trustzone-m-boot-process","The TrustZone-M Boot Process",[15,7227,7228,7229,7232],{},"In the ARM TrustZone-M boot sequence, the processor always starts in ",[37,7230,7231],{},"Secure Privileged"," mode after a reset. In this stage, the secure code acts as a primary bootloader. Its responsibilities include:",[181,7234,7235,7249,7258],{},[184,7236,7237,7240,7241,7244,7245,7248],{},[37,7238,7239],{},"Stack Initialization:"," Setting up the Main Stack Pointer (MSP) for both the ",[37,7242,7243],{},"Secure"," and ",[37,7246,7247],{},"Non-secure"," worlds.",[184,7250,7251,7254,7255,7257],{},[37,7252,7253],{},"Resource Partitioning:"," Configuring the ",[37,7256,6047],{}," to define which memory regions and peripherals belong to the Secure or Non-secure domains.",[184,7259,7260,7263],{},[37,7261,7262],{},"Vector Table Setup:"," Defining the addresses for the Secure and Non-secure vector tables before transitioning execution to the Non-secure world.",[44,7265,7267],{"id":7266},"_2-start-with-an-empty-rust-project","2. Start with an Empty Rust Project",[15,7269,7270,7271,7274,7275,7278],{},"First, we need to add the ",[37,7272,7273],{},"ARMv8-M"," toolchain using ",[52,7276,7277],{},"rustup"," :",[87,7280,7282],{"className":800,"code":7281,"language":802,"meta":95,"style":95},"$ rustup target add thumbv8m.main-none-eabi\n",[52,7283,7284],{"__ignoreMap":95},[118,7285,7286],{"class":120,"line":121},[118,7287,7281],{},[15,7289,7290,7291,7293],{},"Next, let's create a new ",[52,7292,2049],{}," Rust project:",[87,7295,7297],{"className":800,"code":7296,"language":802,"meta":95,"style":95},"$ cargo new --bin trustzone_helloworld\n",[52,7298,7299],{"__ignoreMap":95},[118,7300,7301],{"class":120,"line":121},[118,7302,7296],{},[15,7304,7305],{},"We also need to add a few crates for hardware support and basic functionality:",[87,7307,7309],{"className":800,"code":7308,"language":802,"meta":95,"style":95},"$ cargo add cortex-m-rt cortex-m-semihosting panic-halt\n",[52,7310,7311],{"__ignoreMap":95},[118,7312,7313],{"class":120,"line":121},[118,7314,7308],{},[15,7316,7317],{},[37,7318,7319],{},"Dependency Breakdown:",[181,7321,7322,7331,7337],{},[184,7323,7324,7327,7328,7330],{},[52,7325,7326],{},"cortex-m-rt",": This crate provides the minimal runtime environment required for an embedded ",[52,7329,2049],{}," Rust project to run on Cortex-M processors (e.g., memory layout and reset handling).",[184,7332,7333,7336],{},[52,7334,7335],{},"cortex-m-semihosting",": This allows us to output our \"Hello, World!\" message to the host machine's console via the debugger, bypassing the need to initialize a complex UART peripheral.",[184,7338,7339,7342],{},[52,7340,7341],{},"panic-halt",": A simple panic handler that puts the processor into an infinite loop (halts) if a panic occurs.",[44,7344,7346],{"id":7345},"_3-setting-up-the-environment","3. Setting Up the Environment",[15,7348,7349,7350,7353],{},"To finalize our environment, we need to configure how Cargo handles the build process and how the memory is laid out for the ",[37,7351,7352],{},"MPS2-AN521"," board.",[15,7355,7356,6059,7359],{},[37,7357,7358],{},"Configuration",[52,7360,3896],{},[15,7362,7363,7364,7366,7367,7370],{},"First, we create ",[52,7365,3896],{},". This file instructs Cargo to compile the project for the ARMv8-M architecture instead of our host machine's architecture (like x86_64). It also defines a runner, which allows us to use ",[52,7368,7369],{},"cargo run"," to automatically launch QEMU and execute our binary, streamlining the debug process.",[87,7372,7374],{"className":3435,"code":7373,"language":3437,"meta":95,"style":95},"[build]\ntarget = \"thumbv8m.main-none-eabi\" # Cortex-M33 (ARMv8-M Mainline)\n\n[target.thumbv8m.main-none-eabi]\nrunner = \"qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial mon:stdio -semihosting -kernel\"\nrustflags = [\n  \"-C\", \"link-arg=-Tlink.x\"\n]\n",[52,7375,7376,7380,7384,7388,7392,7397,7401,7405],{"__ignoreMap":95},[118,7377,7378],{"class":120,"line":121},[118,7379,3974],{},[118,7381,7382],{"class":120,"line":127},[118,7383,7008],{},[118,7385,7386],{"class":120,"line":133},[118,7387,2247],{"emptyLinePlaceholder":666},[118,7389,7390],{"class":120,"line":139},[118,7391,7017],{},[118,7393,7394],{"class":120,"line":145},[118,7395,7396],{},"runner = \"qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial mon:stdio -semihosting -kernel\"\n",[118,7398,7399],{"class":120,"line":893},[118,7400,3916],{},[118,7402,7403],{"class":120,"line":1168},[118,7404,7041],{},[118,7406,7407],{"class":120,"line":1176},[118,7408,3956],{},[15,7410,7411,6059,7414],{},[37,7412,7413],{},"Memory Layout",[52,7415,5912],{},[15,7417,7418,7419,7421,7422,7424],{},"Next, we create ",[52,7420,5912],{},". This file is required by the ",[52,7423,7326],{}," crate to define the device's memory map. By providing these addresses, the linker knows exactly where to place our code (Flash) and our variables (RAM) in the final binary.",[87,7426,7429],{"className":7427,"code":7428,"language":92},[90],"MEMORY\n{\n  /* Code in SSRAM1 (0x10000000) */\n  FLASH : ORIGIN = 0x10000000, LENGTH = 2M\n  \n  /* Stack/Data in SSRAM2 (Secure Address) */\n  /* AN521 SSRAM2 starts at 0x38000000 and is 2MB */\n  RAM   : ORIGIN = 0x38000000, LENGTH = 2M\n}\n",[52,7430,7428],{"__ignoreMap":95},[15,7432,7433,1110],{},[37,7434,7435],{},"A Note on Address Bit 28 and Security",[15,7437,7438,7439,66,7441,7443],{},"You might notice we are using origins like ",[52,7440,7079],{},[52,7442,7075],{},". In the MPS2-AN521 (and many other TrustZone-M implementations), bit 28 of the address acts as a hardware-level security filter.",[181,7445,7446,7456],{},[184,7447,7448,7455],{},[37,7449,7450,7451,7454],{},"Bit 28 = 0 (e.g., ",[52,7452,7453],{},"0x1000_0000","):"," Accesses the memory via the Secure alias.",[184,7457,7458,7464],{},[37,7459,7460,7461,7454],{},"Bit 28 = 1 (e.g., ",[52,7462,7463],{},"0x0000_0000"," Accesses the memory via the Non-secure alias.",[15,7466,7467,7468,7470],{},"While the processor is in ",[37,7469,7243],{}," Mode, it can technically access either alias without triggering a security fault. However, using the Secure alias addresses (0x1... and 0x3...) in our linker script is a best practice. It ensures that the Secure code explicitly operates within the Secure address space, making the security boundaries clear and preventing accidental leaks or \"aliasing\" bugs when we eventually transition to the Non-secure world.",[44,7472,7474],{"id":7473},"_4-time-to-write-the-first-line-of-code","4. Time to Write the First Line of Code",[15,7476,7477],{},"Now that the environment is fully configured, we can modify main.rs to transform it into a proper no_std project. This is the absolute minimum code required to verify that our QEMU runner, memory map, and semihosting are all working correctly.",[15,7479,7480],{},[37,7481,5954],{},[87,7483,7485],{"className":112,"code":7484,"language":114,"meta":95,"style":95},"// Don't link the Rust standard library (requires an OS)\n#![no_std]\n// Disable the standard main() entry point; we use cortex-m-rt instead\n#![no_main]\n\nuse cortex_m_rt::entry;\nuse cortex_m_semihosting::hprintln;\n// If the program panics, stay in an infinite loop\nuse panic_halt as _;\n\n// The #[entry] macro ensures the bootloader knows this is the starting point.\n// In TrustZone, the CPU starts here in Secure Privileged mode by default.\n#[entry]\nfn main() -> ! {\n    // hprintln sends data back to QEMU's stdout through the debugger interface.\n    // This is much easier than writing a full UART driver for a simple test.\n    hprintln!(\"Hello from the Secure World!\");\n\n    // Embedded programs must never return.\n    loop {\n    }\n}\n",[52,7486,7487,7492,7496,7501,7505,7509,7513,7517,7522,7526,7530,7535,7540,7544,7548,7553,7558,7563,7567,7572,7576,7580],{"__ignoreMap":95},[118,7488,7489],{"class":120,"line":121},[118,7490,7491],{},"// Don't link the Rust standard library (requires an OS)\n",[118,7493,7494],{"class":120,"line":127},[118,7495,3139],{},[118,7497,7498],{"class":120,"line":133},[118,7499,7500],{},"// Disable the standard main() entry point; we use cortex-m-rt instead\n",[118,7502,7503],{"class":120,"line":139},[118,7504,3158],{},[118,7506,7507],{"class":120,"line":145},[118,7508,2247],{"emptyLinePlaceholder":666},[118,7510,7511],{"class":120,"line":893},[118,7512,6023],{},[118,7514,7515],{"class":120,"line":1168},[118,7516,6028],{},[118,7518,7519],{"class":120,"line":1176},[118,7520,7521],{},"// If the program panics, stay in an infinite loop\n",[118,7523,7524],{"class":120,"line":1184},[118,7525,6038],{},[118,7527,7528],{"class":120,"line":1195},[118,7529,2247],{"emptyLinePlaceholder":666},[118,7531,7532],{"class":120,"line":1203},[118,7533,7534],{},"// The #[entry] macro ensures the bootloader knows this is the starting point.\n",[118,7536,7537],{"class":120,"line":1209},[118,7538,7539],{},"// In TrustZone, the CPU starts here in Secure Privileged mode by default.\n",[118,7541,7542],{"class":120,"line":1223},[118,7543,6432],{},[118,7545,7546],{"class":120,"line":1229},[118,7547,6437],{},[118,7549,7550],{"class":120,"line":1241},[118,7551,7552],{},"    // hprintln sends data back to QEMU's stdout through the debugger interface.\n",[118,7554,7555],{"class":120,"line":1249},[118,7556,7557],{},"    // This is much easier than writing a full UART driver for a simple test.\n",[118,7559,7560],{"class":120,"line":1266},[118,7561,7562],{},"    hprintln!(\"Hello from the Secure World!\");\n",[118,7564,7565],{"class":120,"line":1285},[118,7566,2247],{"emptyLinePlaceholder":666},[118,7568,7569],{"class":120,"line":1291},[118,7570,7571],{},"    // Embedded programs must never return.\n",[118,7573,7574],{"class":120,"line":1304},[118,7575,3330],{},[118,7577,7578],{"class":120,"line":1310},[118,7579,2753],{},[118,7581,7582],{"class":120,"line":1322},[118,7583,2644],{},[44,7585,7587],{"id":7586},"_5-hello-world","5. Hello World",[15,7589,7590,7591,40],{},"With the configuration complete and our code written, we finally have a functional project. Testing it is as simple as executing ",[52,7592,7369],{},[15,7594,7595,7596,7599,7600,7602],{},"Because we configured the ",[52,7597,7598],{},"runner"," in our ",[52,7601,3896],{},", Cargo handles the heavy lifting of compiling the binary and passing it to QEMU with the correct machine and CPU parameters.",[87,7604,7606],{"className":800,"code":7605,"language":802,"meta":95,"style":95},"$ cargo run\n   Compiling trustzone_helloworld v0.1.0 (/Users/peterw/Documents/armv8m/trustzone_helloworld)\n    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s\n     Running `qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial 'mon:stdio' -semihosting -kernel target/thumbv8m.main-none-eabi/debug/trustzone_helloworld`\nHello from the Secure World!\n",[52,7607,7608,7612,7617,7622,7627],{"__ignoreMap":95},[118,7609,7610],{"class":120,"line":121},[118,7611,7097],{},[118,7613,7614],{"class":120,"line":127},[118,7615,7616],{},"   Compiling trustzone_helloworld v0.1.0 (/Users/peterw/Documents/armv8m/trustzone_helloworld)\n",[118,7618,7619],{"class":120,"line":133},[118,7620,7621],{},"    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s\n",[118,7623,7624],{"class":120,"line":139},[118,7625,7626],{},"     Running `qemu-system-arm -machine mps2-an521 -cpu cortex-m33 -nographic -serial 'mon:stdio' -semihosting -kernel target/thumbv8m.main-none-eabi/debug/trustzone_helloworld`\n",[118,7628,7629],{"class":120,"line":145},[118,7630,7631],{},"Hello from the Secure World!\n",[646,7633,648],{},{"title":95,"searchDepth":127,"depth":127,"links":7635},[7636,7637,7641,7642,7643,7644],{"id":698,"depth":127,"text":699},{"id":7191,"depth":127,"text":7192,"children":7638},[7639,7640],{"id":7195,"depth":133,"text":7196},{"id":7224,"depth":133,"text":7225},{"id":7266,"depth":127,"text":7267},{"id":7345,"depth":127,"text":7346},{"id":7473,"depth":127,"text":7474},{"id":7586,"depth":127,"text":7587},"2026-1-31",{},"/embedded/trustzone-m/qemu_an521_setup",{"title":7149,"description":7158},"embedded/TrustZone-M/qemu_an521_setup","wwsnnmZkjsJau9qTKc-9gUWfmYP-kyS72TmUtEQRVQE",1776777282463]