diff --git a/crates/cli/tests/e2e_esp32c3_blinky.rs b/crates/cli/tests/e2e_esp32c3_blinky.rs new file mode 100644 index 00000000..ab888bc8 --- /dev/null +++ b/crates/cli/tests/e2e_esp32c3_blinky.rs @@ -0,0 +1,75 @@ +// LabWired - Firmware Simulation Platform +// Copyright (C) 2026 Andrii Shylenko +// +// This software is released under the MIT License. +// See the LICENSE file in the project root for full license information. +// +// End-to-end coverage for examples/esp32c3-blinky: the canonical ESP32-C3 +// Super Mini "hello world" (GPIO8 user LED blink + UART narration). This is +// the demo binary the Playground falls back to when a shared/agent C3 lab +// carries no firmware of its own, so a regression here means every shared +// bare C3 lab stops running ("Cannot run: no firmware" class of bug). +// +// Runs the committed firmware ELF through the committed scenario script via +// the `labwired test` CLI — no cross-compiler toolchain needed. It also +// pins rv32 C.JAL decoding: the blinky is small enough that the linker emits +// a compressed jal from _start to main, which a CJ-immediate decode bug once +// sent off into unmapped flash. + +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("canonicalize repo root") +} + +#[test] +fn esp32c3_blinky_blinks_gpio8_and_narrates_over_uart() { + let root = repo_root(); + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let out_dir = std::env::temp_dir().join(format!("labwired-c3-blinky-{nonce}")); + std::fs::create_dir_all(&out_dir).expect("create out dir"); + + let output = Command::new(env!("CARGO_BIN_EXE_labwired")) + .current_dir(&root) + .args([ + "test", + "--script", + "examples/esp32c3-blinky/test-blink.yaml", + "--no-uart-stdout", + "--output-dir", + out_dir.to_str().unwrap(), + ]) + .output() + .expect("execute labwired"); + + let result_json = + std::fs::read_to_string(out_dir.join("result.json")).expect("read result.json"); + let result: serde_json::Value = serde_json::from_str(&result_json).expect("parse result.json"); + let uart = std::fs::read_to_string(out_dir.join("uart.log")).unwrap_or_default(); + let _ = std::fs::remove_dir_all(&out_dir); + + assert_eq!( + result["status"].as_str(), + Some("pass"), + "blinky scenario failed (exit {:?})\n--- stderr ---\n{}\n--- uart ---\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stderr), + uart, + ); + + // The loop must actually toggle, not print once and wedge. + let ons = uart.matches("LED ON").count(); + let offs = uart.matches("LED OFF").count(); + assert!( + ons >= 2 && offs >= 2, + "expected several LED ON/OFF transitions, got {ons} on / {offs} off\n--- uart ---\n{uart}", + ); +} diff --git a/examples/esp32c3-blinky/README.md b/examples/esp32c3-blinky/README.md new file mode 100644 index 00000000..925c1100 --- /dev/null +++ b/examples/esp32c3-blinky/README.md @@ -0,0 +1,51 @@ +# ESP32-C3 Super Mini Blinky + +The canonical "hello world" for the ESP32-C3 Super Mini: bare-metal rv32imc +firmware that enables GPIO8 (the board's user LED) as an output and toggles it +forever, narrating each transition over UART0. + +This is also the demo binary the LabWired Playground falls back to when a +shared or agent-generated C3 lab carries no firmware of its own, so shared +`esp32c3-supermini` labs run out of the box instead of dying with +"Cannot run: no firmware". + +## Run it + +```sh +labwired test --script examples/esp32c3-blinky/test-blink.yaml +``` + +Expected UART: + +``` +C3 BLINKY BOOT +LED ON +LED OFF +LED ON +... +``` + +## Rebuild the firmware + +Requires the Espressif RISC-V GCC toolchain (`riscv32-esp-elf-gcc`, from +PlatformIO or ESP-IDF): + +```sh +cd examples/esp32c3-blinky/firmware +make # produces esp32c3_blinky.elf +``` + +The firmware drives the plain R/W `GPIO_OUT` / `GPIO_ENABLE` registers rather +than the `W1TS`/`W1TC` aliases: the C3's gpio block is the declarative register +file (no write-1-to-set side effects), and full-value writes are equally valid +on real silicon. + +## Files + +- `firmware/main.c` — GPIO8 blink + UART narration +- `firmware/startup.S`, `firmware/c3.ld` — bare-metal C3 boot (shared with the + Leo air-quality example: `_start` sets gp/sp, copies `.data`, zeroes `.bss`) +- `firmware/c3_uart.{c,h}` — polled UART0 debug output +- `system.yaml` — C3 Super Mini system: `status_led` board_io on GPIO8 +- `test-blink.yaml` — headless scenario: boot banner, LED transitions, and + `GPIO_ENABLE` bit 8 asserted diff --git a/examples/esp32c3-blinky/firmware/Makefile b/examples/esp32c3-blinky/firmware/Makefile new file mode 100644 index 00000000..11e345d4 --- /dev/null +++ b/examples/esp32c3-blinky/firmware/Makefile @@ -0,0 +1,42 @@ +# ESP32-C3 Super Mini blinky firmware build. +# +# Compiles a minimal bare-metal GPIO8 blink for the C3 (riscv32imc) with the +# Espressif RISC-V GCC toolchain, producing a bare ELF the LabWired simulator +# boots directly (no boot ROM). +# +# Toolchain: riscv32-esp-elf-gcc (from PlatformIO or ESP-IDF). Override CC if it +# lives elsewhere, e.g.: +# make CC=$$HOME/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc + +ifneq ($(origin CC),command line) +DISCOVERED_CC := $(shell command -v riscv32-esp-elf-gcc 2>/dev/null) +ifeq ($(strip $(DISCOVERED_CC)),) +DISCOVERED_CC := $(firstword $(wildcard \ + $(HOME)/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc \ + $(HOME)/.platformio/tools/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc \ + $(HOME)/.espressif/tools/riscv32-esp-elf/*/riscv32-esp-elf/bin/riscv32-esp-elf-gcc)) +endif +CC := $(DISCOVERED_CC) +endif + +CFLAGS := -march=rv32imc_zicsr_zifencei -mabi=ilp32 -ffreestanding -O2 -g \ + -Wall -Wextra -ffunction-sections -fdata-sections -I. +LDFLAGS := -march=rv32imc_zicsr_zifencei -mabi=ilp32 -nostartfiles \ + -Wl,--gc-sections -T c3.ld -lgcc + +APP_SRC := startup.S c3_uart.c main.c + +ELF := esp32c3_blinky.elf + +all: $(ELF) + +$(ELF): $(APP_SRC) c3.ld + @if [ -z "$(strip $(CC))" ]; then \ + echo "ERROR: riscv32-esp-elf-gcc not found. Install via PlatformIO or ESP-IDF,"; \ + echo " or pass CC=/path/to/riscv32-esp-elf-gcc."; exit 1; fi + $(CC) $(CFLAGS) $(APP_SRC) $(LDFLAGS) -o $@ + +clean: + rm -f $(ELF) *.o + +.PHONY: all clean diff --git a/examples/esp32c3-blinky/firmware/c3.ld b/examples/esp32c3-blinky/firmware/c3.ld new file mode 100644 index 00000000..e6b41834 --- /dev/null +++ b/examples/esp32c3-blinky/firmware/c3.ld @@ -0,0 +1,69 @@ +/* ESP32-C3 (rv32imc) linker script for the LabWired sim. + * + * Matches the chip yaml memory map (configs/chips/esp32c3.yaml): + * FLASH (IROM) : 0x42000000, 4 MB — code + rodata (XIP, loaded by paddr) + * RAM : 0x3FC80000, 400 KB — data + bss + stack + * + * The simulator loads each PT_LOAD segment by its physical address (LMA), so + * .data is placed in RAM (VMA) but loaded from flash (LMA); startup copies it. + */ +ENTRY(_start) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x42000000, LENGTH = 4M + RAM (rwx) : ORIGIN = 0x3FC80000, LENGTH = 400K +} + +_stack_top = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS +{ + .text : + { + KEEP(*(.init)) + *(.text*) + *(.gnu.linkonce.t.*) + } > FLASH + + .rodata : + { + *(.rodata*) + *(.srodata*) + *(.gnu.linkonce.r.*) + . = ALIGN(4); + } > FLASH + + /* Global-pointer anchor: centred so ±2 KB gp-relative addressing covers the + * small-data sections. */ + . = ALIGN(8); + PROVIDE(__global_pointer$ = . + 0x800); + + _sidata = LOADADDR(.data); + .data : + { + . = ALIGN(4); + _sdata = .; + *(.data*) + *(.sdata*) + . = ALIGN(4); + _edata = .; + } > RAM AT > FLASH + + .bss (NOLOAD) : + { + . = ALIGN(4); + _sbss = .; + *(.sbss*) + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > RAM + + . = ALIGN(8); + PROVIDE(end = .); + PROVIDE(_end = .); + + /DISCARD/ : { *(.eh_frame*) *(.riscv.attributes) *(.comment) *(.note*) } +} diff --git a/examples/esp32c3-blinky/firmware/c3_uart.c b/examples/esp32c3-blinky/firmware/c3_uart.c new file mode 100644 index 00000000..1567b797 --- /dev/null +++ b/examples/esp32c3-blinky/firmware/c3_uart.c @@ -0,0 +1,58 @@ +/* ESP32-C3 polled debug UART — see c3_uart.h. */ +#include "c3_uart.h" + +#define UART0_TX (*(volatile uint8_t *)0x60000000u) + +void uart_putc(char c) { UART0_TX = (uint8_t)c; } + +void uart_puts(const char *s) { + while (*s) { + uart_putc(*s++); + } +} + +void uart_puti(int32_t v) { + char buf[12]; + int i = 0; + uint32_t u; + if (v < 0) { + uart_putc('-'); + u = (uint32_t)(-(int64_t)v); + } else { + u = (uint32_t)v; + } + if (u == 0) { + uart_putc('0'); + return; + } + while (u > 0) { + buf[i++] = (char)('0' + (u % 10u)); + u /= 10u; + } + while (i > 0) { + uart_putc(buf[--i]); + } +} + +void uart_putfix2(int32_t v_x100) { + int32_t whole; + int32_t frac; + if (v_x100 < 0) { + uart_putc('-'); + v_x100 = -v_x100; + } + whole = v_x100 / 100; + frac = v_x100 % 100; + uart_puti(whole); + uart_putc('.'); + uart_putc((char)('0' + (frac / 10))); + uart_putc((char)('0' + (frac % 10))); +} + +void uart_puthex(uint32_t n, int width) { + static const char hexd[] = "0123456789ABCDEF"; + int i; + for (i = (width - 1) * 4; i >= 0; i -= 4) { + uart_putc(hexd[(n >> i) & 0xFu]); + } +} diff --git a/examples/esp32c3-blinky/firmware/c3_uart.h b/examples/esp32c3-blinky/firmware/c3_uart.h new file mode 100644 index 00000000..af81ed9a --- /dev/null +++ b/examples/esp32c3-blinky/firmware/c3_uart.h @@ -0,0 +1,23 @@ +/* Polled debug UART for the ESP32-C3 in the LabWired simulator. + * + * The simulator maps a generic UART model at 0x60000000 (UART0). Its Stm32F1 + * register layout treats a byte write at offset 0 as a TX-data write, so a + * single `*(volatile uint8_t*)0x60000000 = c` transmits one character — the + * same path the firmware-esp32c3-demo crate uses. The simulator's test runner + * captures every transmitted byte into its UART log for `uart_contains` + * assertions. */ +#ifndef C3_UART_H +#define C3_UART_H + +#include + +void uart_putc(char c); +void uart_puts(const char *s); +/* Print a signed integer in decimal. */ +void uart_puti(int32_t v); +/* Print a fixed-point value given as value*100 (e.g. 4910 -> "49.10"). */ +void uart_putfix2(int32_t v_x100); +/* Print `n` as exactly `width` hex chars (upper-case, zero-padded). */ +void uart_puthex(uint32_t n, int width); + +#endif /* C3_UART_H */ diff --git a/examples/esp32c3-blinky/firmware/esp32c3_blinky.elf b/examples/esp32c3-blinky/firmware/esp32c3_blinky.elf new file mode 100755 index 00000000..4b2aa9bb Binary files /dev/null and b/examples/esp32c3-blinky/firmware/esp32c3_blinky.elf differ diff --git a/examples/esp32c3-blinky/firmware/main.c b/examples/esp32c3-blinky/firmware/main.c new file mode 100644 index 00000000..0c56268c --- /dev/null +++ b/examples/esp32c3-blinky/firmware/main.c @@ -0,0 +1,35 @@ +/* ESP32-C3 Super Mini blinky. + * + * Toggles GPIO8 — the Super Mini's user LED — via the plain R/W GPIO_OUT and + * GPIO_ENABLE registers, logging each transition over UART0. Bare-metal + * rv32imc; the LabWired simulator boots the ELF directly (no boot ROM). The + * C3's gpio block is the declarative register file, so the demo drives the + * full-value registers (silicon-valid) rather than the W1TS/W1TC aliases. + */ +#include + +#include "c3_uart.h" + +#define GPIO_BASE 0x60004000u +#define GPIO_OUT (*(volatile uint32_t *)(GPIO_BASE + 0x04u)) +#define GPIO_ENABLE (*(volatile uint32_t *)(GPIO_BASE + 0x20u)) + +#define LED_PIN 8u + +static void delay(void) { + for (volatile uint32_t i = 0; i < 20000u; i++) { + } +} + +int main(void) { + uart_puts("C3 BLINKY BOOT\n"); + GPIO_ENABLE |= 1u << LED_PIN; + for (;;) { + GPIO_OUT |= 1u << LED_PIN; + uart_puts("LED ON\n"); + delay(); + GPIO_OUT &= ~(1u << LED_PIN); + uart_puts("LED OFF\n"); + delay(); + } +} diff --git a/examples/esp32c3-blinky/firmware/startup.S b/examples/esp32c3-blinky/firmware/startup.S new file mode 100644 index 00000000..e61e0678 --- /dev/null +++ b/examples/esp32c3-blinky/firmware/startup.S @@ -0,0 +1,50 @@ +/* Minimal ESP32-C3 (RISC-V rv32imc) startup. + * + * The LabWired simulator loads this bare ELF and jumps to its entry point + * (`_start`) — there is no boot ROM in the C3 sim path. We set up the global + * and stack pointers, clear .bss, copy .data from flash (LMA) to RAM (VMA), + * then call main(). On return we spin. + */ + .section .init, "ax" + .global _start + .type _start, @function +_start: + /* Set up the global pointer (linker-relaxation anchor). */ + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + /* Stack pointer = top of RAM (set by the linker as _stack_top). */ + la sp, _stack_top + + /* Copy .data from flash (LMA) to RAM (VMA). */ + la a0, _sdata + la a1, _edata + la a2, _sidata +1: + bgeu a0, a1, 2f + lw a3, 0(a2) + sw a3, 0(a0) + addi a0, a0, 4 + addi a2, a2, 4 + j 1b +2: + + /* Zero .bss. */ + la a0, _sbss + la a1, _ebss +3: + bgeu a0, a1, 4f + sw zero, 0(a0) + addi a0, a0, 4 + j 3b +4: + + /* main(). */ + call main + + /* If main returns, spin forever. */ +5: + j 5b + .size _start, . - _start diff --git a/examples/esp32c3-blinky/system.yaml b/examples/esp32c3-blinky/system.yaml new file mode 100644 index 00000000..b1411a29 --- /dev/null +++ b/examples/esp32c3-blinky/system.yaml @@ -0,0 +1,16 @@ +# LabWired - ESP32-C3 Super Mini blinky. +# +# The canonical "hello world" for the C3 Super Mini: the firmware enables +# GPIO8 (the board's user LED) as an output and toggles it forever, narrating +# each transition over UART0. This is the demo binary the Playground falls +# back to when a shared/agent C3 lab carries no firmware of its own. +name: "esp32c3-blinky" +chip: "../../configs/chips/esp32c3.yaml" +external_devices: [] +board_io: + - id: "status_led" + kind: "led" + peripheral: "gpio" + pin: 8 + signal: "output" + active_high: true diff --git a/examples/esp32c3-blinky/test-blink.yaml b/examples/esp32c3-blinky/test-blink.yaml new file mode 100644 index 00000000..1f9f59ac --- /dev/null +++ b/examples/esp32c3-blinky/test-blink.yaml @@ -0,0 +1,21 @@ +# Headless run of the ESP32-C3 blinky firmware. +# +# Asserts the firmware actually drives the LED pin: GPIO8 is enabled as an +# output (GPIO_ENABLE, 0x60004020, bit 8) and the blink loop runs long enough +# to log several on/off transitions over UART0. +schema_version: "1.0" +inputs: + system: "./system.yaml" + firmware: "./firmware/esp32c3_blinky.elf" +limits: + max_steps: 400000 +assertions: + - uart_contains: "C3 BLINKY BOOT" + - uart_contains: "LED ON" + - uart_contains: "LED OFF" + # GPIO_ENABLE bit 8: the LED pin is configured as an output. + - memory_value: + address: 0x60004020 + expected_value: 0x00000100 + mask: 0x00000100 + - expected_stop_reason: max_steps