Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions crates/cli/tests/e2e_esp32c3_blinky.rs
Original file line number Diff line number Diff line change
@@ -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}",
);
}
51 changes: 51 additions & 0 deletions examples/esp32c3-blinky/README.md
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions examples/esp32c3-blinky/firmware/Makefile
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions examples/esp32c3-blinky/firmware/c3.ld
Original file line number Diff line number Diff line change
@@ -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*) }
}
58 changes: 58 additions & 0 deletions examples/esp32c3-blinky/firmware/c3_uart.c
Original file line number Diff line number Diff line change
@@ -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]);
}
}
23 changes: 23 additions & 0 deletions examples/esp32c3-blinky/firmware/c3_uart.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

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 */
Binary file not shown.
35 changes: 35 additions & 0 deletions examples/esp32c3-blinky/firmware/main.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

#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();
}
}
Loading
Loading