{"id":50677674,"url":"https://github.com/kassane/esp32-baremetal-zig","last_synced_at":"2026-06-08T16:35:02.480Z","repository":{"id":341258997,"uuid":"761397345","full_name":"kassane/esp32-baremetal-zig","owner":"kassane","description":"Hardware Abstraction Layers for ESP32 (Xtensa only)","archived":false,"fork":false,"pushed_at":"2026-05-27T17:30:15.000Z","size":1016,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T14:34:23.123Z","etag":null,"topics":["embedded","embedded-hal","esp32","espressif","xtensa","zig","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kassane.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-02-21T19:46:15.000Z","updated_at":"2026-05-27T17:25:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kassane/esp32-baremetal-zig","commit_stats":null,"previous_names":["kassane/esp32-baremetal-zig"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kassane/esp32-baremetal-zig","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kassane%2Fesp32-baremetal-zig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kassane%2Fesp32-baremetal-zig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kassane%2Fesp32-baremetal-zig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kassane%2Fesp32-baremetal-zig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kassane","download_url":"https://codeload.github.com/kassane/esp32-baremetal-zig/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kassane%2Fesp32-baremetal-zig/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34071657,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["embedded","embedded-hal","esp32","espressif","xtensa","zig","zig-package"],"created_at":"2026-06-08T16:35:01.839Z","updated_at":"2026-06-08T16:35:02.469Z","avatar_url":"https://github.com/kassane.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# esp32-baremetal-zig\n\nA bare-metal hardware abstraction layer for the Xtensa ESP32 family, written in\npure Zig — no vendor SDK, no RTOS, no libc, no C. Firmware boots directly on\nESP32, ESP32-S2 and ESP32-S3 from a generated linker script, reaches the silicon\nthrough SVD-generated register definitions, and is exercised in the Espressif\nQEMU fork on every CI run.\n\nThree reference applications anchor the HAL: the ESP32 verifies its hardware\nSHA-1/256 and AES-128/256 engines against `std.crypto` live in QEMU, the ESP32-S2\nruns a fixed-point FFT spectrum analyzer, and the ESP32-S3 drives its PIE/SIMD\nvector unit.\n\n### Highlights\n\n- **Generated register access.** `tools/svd2zig.zig` compiles vendored CMSIS-SVD\n  into a typed `@import(\"regs\")` at build time — `regs.GPIO.OUT_W1TS`, never a\n  hardcoded address.\n- **A comptime register HAL.** Every peripheral driver is parameterized on its\n  register addresses, so each MMIO access is provably aligned and non-null and\n  emits no panic path on this backend. Bit fields are named via `src/reg.zig`,\n  not hand-shifted.\n- **Fixed-point DSP.** Saturating add, dot product, FIR and a radix-2 Q15 FFT,\n  with ESP32-S3 PIE vector kernels selected at comptime and a scalar fallback.\n- **Freestanding-safe runtime.** Watchdogs are cleared at boot; a custom panic\n  handler and a `std.log` backend render over UART without pulling in `std.fmt`\n  (whose formatter will not link on this backend).\n- **Consumable as a package.** `zig fetch` the repository and import the modules;\n  every example under `examples/` is a standalone package that does exactly that.\n- **Continuously tested.** A Linux/macOS/Windows CI matrix builds every target\n  and boots the QEMU-capable chips on each push.\n\nThe flash and QEMU linker scripts are generated in `build.zig` — there are no\n`.ld` files to hand-edit.\n\n## Contents\n\n- [Toolchain requirement](#toolchain-requirement)\n- [How to build](#how-to-build)\n- [Use it as a dependency (`zig fetch`)](#use-it-as-a-dependency-zig-fetch)\n- [Documentation](#documentation)\n- [QEMU testing](#qemu-testing)\n  - [Memory layout](#memory-layout-qemu-iram-only)\n  - [Stack addresses](#stack-addresses-used-in-startup-prologue)\n- [Flashing to hardware](#flashing-to-hardware)\n  - [espflash](#espflash-alternative-1)\n  - [esptool.py](#esptoolpy-alternative-2)\n- [References](#references)\n- [License](#license)\n\n---\n\n## Toolchain requirement\n\nThis project **requires the Espressif LLVM fork of Zig** (`zig-espressif-bootstrap`).\nUpstream Zig does **not** expose `esp32` / `esp32s2` / `esp32s3` CPU models in\n`std.Target.xtensa.cpu`.\n\n| Item | Value |\n|---|---|\n| Toolchain | `zig-espressif-bootstrap` prebuilt, tag `0.16.0-xtensa` (reports `zig version` → `0.16.0`) |\n| Download | \u003chttps://github.com/kassane/zig-espressif-bootstrap/releases\u003e |\n\nUnpack it anywhere and put its directory on `PATH`:\n\n```bash\ncurl -L -o zig.tar.xz \\\n  https://github.com/kassane/zig-espressif-bootstrap/releases/download/0.16.0-xtensa/zig-relsafe-x86_64-linux-musl-baseline.tar.xz\ntar -xJf zig.tar.xz\nexport PATH=\"$PWD/zig-relsafe-x86_64-linux-musl-baseline:$PATH\"\n```\n\nEverything else is plain `zig build` — there is no build script and no\nhand-maintained linker script (both the flash and QEMU `.ld` files are\ngenerated in `build.zig` via `b.addWriteFiles`).\n\n---\n\n## How to build\n\n```bash\n# Build all chips (default) → zig-out/bin/\nzig build --summary all\n\n# Build a single chip\nzig build esp32\nzig build esp32s2\nzig build esp32s3\n\n# Release build\nzig build -Doptimize=ReleaseSmall\n\n# Build-time config knobs (see docs/getting-started.md#build-time-configuration)\nzig build esp32 -Dlog-level=debug -Dpanic-trace=false\n```\n\nPer chip this installs an `\u003cchip\u003e_baremetal_zig` ELF plus a raw\n`\u003cchip\u003e_baremetal_zig.bin` (see the flashing note below about its size).\n\nEvery example under [`examples/`](examples/) is also a standalone package: its\n`build.zig` consumes the repo root (`esp32_hal`) as a local path dependency for\nthe shared modules, generated registers and linker scripts, so you can build one\nexample on its own:\n\n```bash\ncd examples/esp32 \u0026\u0026 zig build           # → zig-out/bin/esp32_baremetal_zig(.bin)\ncd examples/esp32 \u0026\u0026 zig build run       # launch it in QEMU (esp32, esp32s3)\ncd examples/esp32 \u0026\u0026 zig build smoke     # non-interactive boot test (esp32, esp32s3)\n```\n\n| Source | Chip | CPU | LED | Demo |\n|---|---|---|---|---|\n| `examples/esp32/main.zig`   | ESP32    | Xtensa LX6 | GPIO2  | hardware SHA-1/256 + AES-128/256 (vs `std.crypto`) + RNG |\n| `examples/esp32s2/main.zig` | ESP32-S2 | Xtensa LX7 | GPIO18 | fixed-point FFT spectrum + TIMG timer |\n| `examples/esp32s3/main.zig` | ESP32-S3 | Xtensa LX7 | GPIO48 | PIE/SIMD vector kernels |\n\nSingle-feature programs live alongside them, each its own package you build with\n`cd examples/\u003cname\u003e \u0026\u0026 zig build`:\n\n- `blink` (GPIO + Delay), `button` (GPIO in→out), `efuse` (factory MAC) on ESP32\n- **Run in QEMU** (`zig build demo`): `efuse` (ESP32 — factory MAC over UART),\n  `rtc_store` (ESP32 — RTC scratch round-trip), `rsa` (ESP32 — known-answer modexp\n  on the RSA accelerator), `heap` (ESP32 — typed bump-arena allocation),\n  `rtc_time` (ESP32 — RTC main-timer uptime), `critical` (ESP32 — interrupt-masking\n  critical section) and `systimer` (ESP32-S3 — system-timer uptime over UART)\n- **Build-only**: `pwm` (LEDC) on ESP32-S2; `i2c` (I2C master), `spi` (SPI master),\n  `rmt` (IR remote transmit), `ws2812` (addressable RGB over RMT), `twai`\n  (CAN 2.0 transmit), `mcpwm` (motor-control PWM),\n  `i2s` (I2S master TX), `dac` (analog output), `adc` (analog input), `iomux` (pad\n  pull/drive config), `gpio_matrix` (signal↔pad routing), `watchdog` (TIMG WDT),\n  `reset_reason` (reset cause), `sw_reset` (software reset), `gpio_edge` (poll-based\n  edge detection), `clock_gate` (peripheral clock gating), `brownout` (supply\n  brownout detector), `touch` (capacitive touch sensor), `deep_sleep` (timer-wakeup\n  deep sleep), `pcnt` (pulse counter) and `flash` (SPI-flash read via ROM) on ESP32;\n  `usb_serial` (USB CDC-ACM console), `tsens`\n  (temperature sensor),\n  `hmac` (HMAC-SHA256 accelerator) and `stack_monitor` (ASSIST_DEBUG\n  stack-overflow monitor) on ESP32-S3\n- **ULP coprocessor** (RISC-V, build-only): `ulp_s2` — an ESP32-S2 ULP program\n  built for `riscv32imc` that drives an RTC GPIO through the generated ULP\n  registers (`svd/esp32s2-ulp.svd`) and heartbeats the main core via shared RTC\n  memory. A separate firmware the main core loads and starts (loader out of scope).\n\nShared register/timing helpers live in `src/mmio.zig` (imported as `mmio`).\n\n---\n\n## Use it as a dependency (`zig fetch`)\n\nThe repo root is itself a Zig package (`esp32_hal`) that publishes its\nbuilding blocks — so you can pull them into your own firmware instead of copying\nfiles around. Add it:\n\n```bash\nzig fetch --save=esp32_hal git+https://github.com/kassane/esp32-baremetal-zig\n```\n\n`--save=esp32_hal` pins the dependency key so `b.dependency(\"esp32_hal\", .{})`\nbelow resolves regardless of the repo's URL basename.\n\nThen wire the modules into your `build.zig`:\n\n```zig\nconst esp = b.dependency(\"esp32_hal\", .{});\n\nconst fw = b.addExecutable(.{ .name = \"fw\", .root_module = b.createModule(.{\n    .root_source_file = b.path(\"main.zig\"),\n    .target = b.resolveTargetQuery(.{\n        .cpu_arch = .xtensa,\n        .os_tag = .esp32,\n        .abi = .none,\n    }),\n}) });\nfw.root_module.addImport(\"mmio\", esp.module(\"mmio\"));        // MMIO + UART + memcpy\nfw.root_module.addImport(\"hal\", esp.module(\"hal\"));          // Output / Input / Delay\nfw.root_module.addImport(\"dsp\", esp.module(\"dsp\"));          // FFT / FIR / SIMD kernels\nfw.root_module.addImport(\"heap\", esp.module(\"heap\"));        // typed bump arena\nfw.root_module.addImport(\"init\", esp.module(\"init\"));        // watchdog disable\nfw.root_module.addImport(\"panic\", esp.module(\"panic\"));      // freestanding panic\nfw.root_module.addImport(\"startup\", esp.module(\"startup\"));  // shared reset vector\nfw.root_module.addImport(\"regs\", esp.module(\"esp32_regs\"));  // or esp32s2_regs / esp32s3_regs\nfw.setLinkerScript(esp.namedLazyPath(\"esp32.ld\"));           // flash; or \"esp32-qemu.ld\"\nfw.bundle_compiler_rt = false;\n```\n\nThe packages under `examples/` *are* this pattern in miniature — they consume the\nroot over a local `.path` dependency, so copy one as a working starting point.\n\n---\n\n## Documentation\n\nGuides and implementation notes live under [`docs/`](docs/):\n\n- **[docs/getting-started.md](docs/getting-started.md)** — from a fresh checkout to\n  firmware running in QEMU and on hardware, plus a minimal-firmware skeleton.\n- **[docs/hal.md](docs/hal.md)** — the `hal` driver reference: every peripheral\n  driver, what it does, and which run in QEMU vs. are build-only — plus the\n  connectivity / wireless boundary.\n- **[docs/internals.md](docs/internals.md)** — generated register access\n  (`svd2zig`), boot/startup, and the freestanding panic + `std.log` shim.\n- **[docs/dsp.md](docs/dsp.md)** — the fixed-point DSP kernels and the ESP32-S3\n  PIE/SIMD vector path.\n- **[docs/heap.md](docs/heap.md)** — the bare-metal typed bump arena, and why the\n  std allocator interface doesn't lower on this backend.\n\n---\n\n## QEMU testing\n\nESP32 and ESP32-S3 have machine models in the Espressif QEMU fork; **ESP32-S2\ndoes not**, so it is build-only. QEMU firmware places all code in IRAM so\n`qemu-system-xtensa -kernel \u003celf\u003e` runs without the ROM bootloader initialising\nthe flash cache.\n\n```bash\n# Build the QEMU ELFs (IRAM-only) → zig-out/bin/esp32_qemu, esp32s3_qemu\nzig build qemu\n\n# Build + launch QEMU interactively\nzig build run-esp32\nzig build run-esp32s3\n\n# Non-interactive boot test: boot each QEMU-capable chip and assert no CPU\n# faults (this is what CI runs).\nzig build smoke\nzig build smoke -Dsmoke-seconds=10\n\n# Show the example's UART output (captured from QEMU via `-serial file:`):\nzig build demo          # all QEMU-capable chips\nzig build demo-esp32    # just the ESP32 crypto example\n```\n\nThe firmware writes to UART0 (`regs.UART0.FIFO`); `demo` routes that to a file\nand prints it. **`esp32` is the crypto demo** — it runs SHA-1/256 and AES-128/256-ECB\non the hardware accelerators and checks each against `std.crypto`'s comptime\nreference (`esp32s3` is the PIE/SIMD example and drives the LED rather than\nprinting):\n\n```\nESP32 bare-metal Zig — hardware crypto demo\n[info] SHA-1(\"abc\") HW vs std.crypto: OK\n[info] SHA-256(\"abc\") HW vs std.crypto: OK\n[info] AES-128-ECB HW vs std.crypto: OK\n[info] AES-256-ECB HW vs std.crypto: OK\n[info] rng sample 3160650498, GPIO0 low\n```\n\n`build.zig` finds `qemu-system-xtensa` on `PATH`; override it with\n`-Dqemu=/path/to/qemu-system-xtensa`. The smoke test boots each chip for\n`-Dsmoke-seconds` (default 5) and fails if the CPU raises a fault or QEMU exits\nbefore the timeout — the blink loop never returns, so \"still running at the\ntimeout\" is the pass condition.\n\nInstall the emulator from the Espressif QEMU releases\n(\u003chttps://github.com/espressif/qemu/releases\u003e); on Linux it also needs\n`libsdl2` and `libslirp` at runtime.\n\n### Memory layout (QEMU, IRAM-only)\n\n| Chip | IRAM origin | DRAM origin |\n|---|---|---|\n| ESP32    | `0x40080000`, 1 MB | `0x3FFB0000`, 176 KB |\n| ESP32-S3 | `0x40370000`, 1 MB | `0x3FC88000`, 300 KB |\n\nIRAM is extended to 1 MB (real hw: 128 KB / 400 KB) to accommodate Debug builds.\n\n### Stack addresses used in startup prologue\n\n| Chip | DRAM top | Computation |\n|---|---|---|\n| ESP32    | `0x3FFDC200` | `0x40000000 − 0x23E00` (`0x23E` \u003c\u003c 8) |\n| ESP32-S2 | `0x3FFDE000` | `0x40000000 − 0x22000` (`0x220` \u003c\u003c 8) |\n| ESP32-S3 | `0x3FCD3000` | `0x40000000 − 0x32D000` (`0x32D` \u003c\u003c 12) |\n\nEach value is within the valid DRAM range on real hardware, so the same source\nfile works for both hardware and QEMU builds without conditional compilation.\n\n---\n\n## Flashing to hardware\n\n\u003e **Note:** The flat `.bin` produced by `zig build` via `objcopy` is large\n\u003e (tens of MB) because objcopy zero-fills the gap between the DROM and IROM\n\u003e segments. Use one of the methods below instead.\n\nHardware flashing requires the standard second-stage **bootloader** and\n**partition table** to already be present on flash (they initialise the\nflash-cache MMU so the app's `irom_seg` becomes accessible). Take them from any\nbuild of the vendor SDK:\n\n```\nbootloader.bin       → flash offset 0x0\npartition-table.bin  → flash offset 0x8000\n```\n\n### espflash (alternative 1)\n\n[espflash](https://github.com/esp-rs/espflash) is a Rust CLI that works\ndirectly with ELF files and avoids the large-binary problem.\n\n```bash\ncargo install espflash\n\n# Flash application only (bootloader + partition-table already on device):\nespflash flash --chip esp32s3 --baud 460800 zig-out/bin/esp32s3_baremetal_zig\n\n# Serial monitor:\nespflash monitor --chip esp32s3\n```\n\n### esptool.py (alternative 2)\n\n```bash\n# Convert ELF → correct-sized image (reads load segments, no zero-fill):\nesptool.py --chip esp32s3 elf2image \\\n    --flash_mode dio --flash_size 8MB \\\n    --output firmware.bin zig-out/bin/esp32s3_baremetal_zig\n\n# Flash (bootloader + partition-table must already be on device):\nesptool.py --chip esp32s3 write_flash 0x10000 firmware.bin\n\n# ESP32 / ESP32-S2 (same flow, different chip flag):\nesptool.py --chip esp32   elf2image --output firmware.bin zig-out/bin/esp32_baremetal_zig\nesptool.py --chip esp32s2 elf2image --output firmware.bin zig-out/bin/esp32s2_baremetal_zig\n```\n\n---\n\n## References\n\n- [zig-espressif-bootstrap](https://github.com/kassane/zig-espressif-bootstrap) — the Zig toolchain (Espressif LLVM fork) this project builds with\n- [esp-rs/esp-pacs](https://github.com/esp-rs/esp-pacs) — upstream of the vendored `svd/*.svd`; register access is generated from these by `tools/svd2zig.zig`\n- [espressif/esp-dsp](https://github.com/espressif/esp-dsp) — reference for the fixed-point FFT/DSP algorithms ported into `src/dsp.zig`\n- [esp-rs/espflash](https://github.com/esp-rs/espflash) — ELF-aware flashing tool used in the hardware-flashing instructions above\n- [esp-rs/esp-hal](https://github.com/esp-rs/esp-hal) — the Rust HAL whose register sequences (touch, RTC sleep/timer, ULP, bootloader app-descriptor) this project's drivers are cross-checked against\n\n---\n\n## License\n\nLicensed under the [Apache License, Version 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkassane%2Fesp32-baremetal-zig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkassane%2Fesp32-baremetal-zig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkassane%2Fesp32-baremetal-zig/lists"}