diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4d50b00..3f2fb31 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -29,7 +29,7 @@ There are no unit tests or integration tests in this repository — it is a `no_ |---|---|---|---|---| | `dev-imxrt` | i.MXRT685S | M33 | `thumbv8m.main-none-eabihf` | Minimal dev board | | `dev-npcx` | NPCX498M | M4F | `thumbv7em-none-eabihf` | NPCX dev board | -| `dev-qemu` | QEMU RISC-V virt | — | `riscv32imac-unknown-none-elf` | QEMU dev board | +| `dev-qemu` | QEMU RISC-V ec | — | `riscv32imac-unknown-none-elf` | QEMU dev board | ### Crate dependency graph diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ff8d6dc..0a783f4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # dev-qemu is intentionally excluded from binsize benchmarks: it targets QEMU virt + # dev-qemu is intentionally excluded from binsize benchmarks: it targets QEMU ec # and uses semihosting, so its ELF section layout doesn't represent real flash/RAM # footprint the way the SoC-backed platforms do. platform: ["dev-imxrt", "dev-npcx", "dev-mcxa"] diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec13dd2..f5c464b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -335,6 +335,9 @@ jobs: # We can update this commit when we want to pull in a new `ec-test-cli` EC_TEST_CLI_REV: d705cd4f + # Tag of the prebuilt QEMU image pulled from the odp-qemu-builder GHCR package + ODP_QEMU_TAG: sha-7e461b3 + steps: - uses: actions/checkout@v4 with: @@ -346,10 +349,16 @@ jobs: - name: rustup target add riscv32imac-unknown-none-elf run: rustup target add riscv32imac-unknown-none-elf - # Note: qemu-system-misc contains the `qemu-system-riscv32` binary which is needed to run `dev-qemu` - # libudev-dev is needed by `ec-test-cli` (via libudev-sys) + # libudev-dev is needed by `ec-test-cli` (via libudev-sys). + # + # The remaining libs are the runtime shared libraries the prebuilt + # `qemu-system-riscv32` (copied out of the odp-qemu-builder image) is + # dynamically linked against. - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y qemu-system-misc libudev-dev + run: > + sudo apt-get update && sudo apt-get install -y + libudev-dev + libfdt1 - name: Install ec-test-cli run: cargo install --git https://github.com/OpenDevicePartnership/odp-platform-common --locked --rev ${{ env.EC_TEST_CLI_REV }} ec-test-cli diff --git a/AGENTS.md b/AGENTS.md index eb201e4..6d23b0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ essentials so an autonomous agent has a single entry point. (requires `flip-link`). - `platform/dev-npcx` — Nuvoton NPCX498M, `thumbv7em-none-eabihf` (requires `flip-link`). - - `platform/dev-qemu` — QEMU `virt`, `riscv32imac-unknown-none-elf` + - `platform/dev-qemu` — QEMU `ec`, `riscv32imac-unknown-none-elf` (no `flip-link`). - Toolchain pinned in `rust-toolchain.toml` (stable + all three targets + `rust-src`, `rustfmt`, `clippy`, `llvm-tools-preview`). diff --git a/README.md b/README.md index c3c4798..245a795 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ platforms are maintained separately and are not in scope here. | `dev-imxrt` | Development target on NXP i.MXRT685S (Cortex-M33) | `thumbv8m.main-none-eabihf` | | `dev-mcxa` | Development target on NXP MCXA266 (Cortex-M33) | `thumbv8m.main-none-eabihf` | | `dev-npcx` | Development target on Nuvoton NPCX498M (Cortex-M4F) | `thumbv7em-none-eabihf` | -| `dev-qemu` | Development target under QEMU `virt` machine (RISC-V 32-bit) | `riscv32imac-unknown-none-elf` | +| `dev-qemu` | Development target under QEMU `ec` machine (RISC-V 32-bit) | `riscv32imac-unknown-none-elf` | `platform-common` is consumed by each `dev-*` crate and contains no platform-specific code. diff --git a/platform/dev-qemu/.cargo/config.toml b/platform/dev-qemu/.cargo/config.toml index 502289d..dba26f9 100644 --- a/platform/dev-qemu/.cargo/config.toml +++ b/platform/dev-qemu/.cargo/config.toml @@ -1,5 +1,5 @@ [target.riscv32imac-unknown-none-elf] -runner = "qemu-run --arch riscv32 --machine virt --uart-pty --bios none" +runner = "./qemu-ec.sh" rustflags = ["-C", "link-arg=-Tlink.x", "-C", "link-arg=-Tdefmt.x"] @@ -15,11 +15,4 @@ DEFMT_LOG = "trace" # # Main use-case for this is running `dev-qemu` in CI where we don't care about logging # and don't want to have to install `defmt-print` for it to run -run-headless = [ - "run", - "--release", - "--config", - 'env.DEFMT_LOG="off"', - "--config", - 'target.riscv32imac-unknown-none-elf.runner="qemu-system-riscv32 -machine virt -bios none -serial pty -nographic -monitor none -kernel"', -] +run-headless = ["run", "--release", "--config", 'env.DEFMT_LOG="off"'] diff --git a/platform/dev-qemu/Cargo.lock b/platform/dev-qemu/Cargo.lock index 1fa86da..7a3aa39 100644 --- a/platform/dev-qemu/Cargo.lock +++ b/platform/dev-qemu/Cargo.lock @@ -372,7 +372,7 @@ dependencies = [ [[package]] name = "embassy-qemu-riscv" version = "0.2.1" -source = "git+https://github.com/kurtjd/qemu-riscv-rs#702ab97339894ac996da80c88f70b2f6d4ae8f94" +source = "git+https://github.com/kurtjd/qemu-riscv-rs#7e6200200acb8a0156eb09a9d57694f3d50a136b" dependencies = [ "critical-section", "defmt 1.1.0", @@ -386,6 +386,7 @@ dependencies = [ "embedded-hal-async", "embedded-io 0.7.1", "embedded-io-async 0.7.0", + "embedded-mcu-hal 0.3.0", "qemu-riscv-pac", "riscv", "riscv-rt", @@ -521,6 +522,9 @@ name = "embedded-hal" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" +dependencies = [ + "defmt 0.3.100", +] [[package]] name = "embedded-hal-async" @@ -528,6 +532,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" dependencies = [ + "defmt 0.3.100", "embedded-hal 1.0.0", ] @@ -571,6 +576,18 @@ dependencies = [ "num_enum", ] +[[package]] +name = "embedded-mcu-hal" +version = "0.3.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#38acdc22911dc3c4e8278c315b6e458b6007fbbb" +dependencies = [ + "defmt 1.1.0", + "embedded-hal 1.0.0", + "embedded-hal-async", + "num_enum", + "smbus-pec", +] + [[package]] name = "embedded-sensors-hal" version = "0.1.1" @@ -868,7 +885,7 @@ dependencies = [ "embedded-hal-async", "embedded-io 0.7.1", "embedded-io-async 0.7.0", - "embedded-mcu-hal", + "embedded-mcu-hal 0.2.0", "embedded-sensors-hal-async", "embedded-services", "odp-service-common", @@ -934,7 +951,7 @@ dependencies = [ [[package]] name = "qemu-riscv-pac" version = "0.1.0" -source = "git+https://github.com/kurtjd/qemu-riscv-rs#702ab97339894ac996da80c88f70b2f6d4ae8f94" +source = "git+https://github.com/kurtjd/qemu-riscv-rs#7e6200200acb8a0156eb09a9d57694f3d50a136b" dependencies = [ "critical-section", "riscv", @@ -1269,7 +1286,7 @@ dependencies = [ "embassy-futures", "embassy-sync 0.8.0", "embassy-time", - "embedded-mcu-hal", + "embedded-mcu-hal 0.2.0", "embedded-services", "odp-service-common", "time-alarm-service-interface", @@ -1283,7 +1300,7 @@ source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch= dependencies = [ "bitfield 0.17.0", "defmt 0.3.100", - "embedded-mcu-hal", + "embedded-mcu-hal 0.2.0", "num_enum", "zerocopy", ] @@ -1294,7 +1311,7 @@ version = "0.1.0" source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#62d4ea9a87588c6096e1c2f149ac3263064cbde9" dependencies = [ "defmt 0.3.100", - "embedded-mcu-hal", + "embedded-mcu-hal 0.2.0", "embedded-services", "num_enum", "time-alarm-service-interface", diff --git a/platform/dev-qemu/README.md b/platform/dev-qemu/README.md index 459eae6..5404a9d 100644 --- a/platform/dev-qemu/README.md +++ b/platform/dev-qemu/README.md @@ -1,14 +1,43 @@ # dev-qemu -A platform targeting QEMU RISCV virt using mock embedded-services. +A platform targeting QEMU RISCV using mock embedded-services. -## Run -Install `qemu-run` (this is a convenience tool for spawning QEMU and defmt-print): -`cargo install --locked --git https://github.com/kurtjd/defmt --branch qemu-run-riscv qemu-run` +It runs on the custom ODP `ec` machine, which exposes the EC's I2C-target and GPIO lines as +sockets that external programs (such as another QEMU instance) can connect to, alongside a PTY for +the UART. + +## Prerequisites +- [Docker](https://docs.docker.com/get-docker/) — `qemu-ec.sh` pulls a prebuilt + `qemu-system-riscv32` (with `ec` machine support) from the + [`odp-qemu-builder`](https://github.com/OpenDevicePartnership/odp-qemu-builder) + GHCR image and caches it under `target/qemu-ec/`. To use a local QEMU instead, + set `QEMU=/path/to/qemu-system-riscv32`. +- `defmt-print`: `cargo install defmt-print` -Then run: +## Run `cargo run --release` -The PTY virtual serial port path will be displayed, and this can be used to connect over serial. +On first run the QEMU binary is pulled from GHCR. The PTY virtual serial port +path is displayed, and this can be used to connect over serial. -E.g. to connect with [ec-test-app](https://github.com/OpenDevicePartnership/odp-platform-common/tree/main/ec-test-app) built with the `serial` feature: +E.g. to connect with [ec-test-app](https://github.com/OpenDevicePartnership/odp-platform-common/tree/main/ec-test-app) built with the `serial` feature: `./ec-test-app /dev/pts/ none` + +To run without logging (skips `defmt-print`): +`cargo run-headless` + +## Sockets +While `dev-qemu` is running, the `ec` machine exposes two sockets that external +programs (such as another QEMU instance) can connect to: + +- I2C target: `/tmp/qemu-ec-i2c.sock` +- GPIO: `/tmp/qemu-ec-gpio.sock` + +## Configuration +`qemu-ec.sh` reads the following environment variables: + +| Variable | Default | Description | +|----------------|--------------------------|-------------------------------------------------| +| `QEMU` | (pulled from GHCR) | Override the `qemu-system-riscv32` binary. | +| `ODP_QEMU_TAG` | (pinned in `qemu-ec.sh`) | Tag of the odp-qemu-builder GHCR image to pull. | +| `EC_I2C_SOCK` | `/tmp/qemu-ec-i2c.sock` | Path for the I2C-target socket. | +| `EC_GPIO_SOCK` | `/tmp/qemu-ec-gpio.sock` | Path for the GPIO socket. | diff --git a/platform/dev-qemu/qemu-ec.sh b/platform/dev-qemu/qemu-ec.sh new file mode 100755 index 0000000..4a6bad0 --- /dev/null +++ b/platform/dev-qemu/qemu-ec.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# +# Unified QEMU launcher for `dev-qemu`. +# +# This is the single source of truth used by `cargo run`, `cargo run-headless`, +# `scripts/integration-test.sh`, and CI. It: +# +# 1. Resolves a `qemu-system-riscv32` binary that supports the `ec` machine +# (GPIO + I2C target sockets). The binary is pulled once from the +# `odp-qemu-builder` image published on GHCR and cached under `target/`. +# 2. Launches QEMU on the `ec` machine, exposing the EC's I2C-target and GPIO +# lines as UNIX-domain sockets that external programs can connect to, plus +# a PTY for the UART. +# 3. Routes the defmt log stream: in the normal (interactive) path the +# semihosting output is piped straight into `defmt-print`; in the headless +# path (`DEFMT_LOG=off`) QEMU is run raw with no logging. +# +# Invoked by cargo as: `./qemu-ec.sh `. +# +# It can also be invoked as `./qemu-ec.sh --prepare` to only resolve (and, on +# first use, pull) the QEMU binary into the cache and exit, without launching. +# This is useful for warming the cache before a time-sensitive launch so a cold +# `docker pull` doesn't count against a startup timeout. +# +# Environment knobs (all optional): +# QEMU Override the QEMU binary entirely (skips the GHCR pull). +# ODP_QEMU_TAG Tag of the GHCR image to pull. +# EC_I2C_SOCK Path for the I2C-target socket (default: /tmp/qemu-ec-i2c.sock). +# EC_GPIO_SOCK Path for the GPIO socket (default: /tmp/qemu-ec-gpio.sock). + +set -euo pipefail + +MODE="run" +if [[ "${1:-}" == "--prepare" ]]; then + MODE="prepare" + ELF="" +else + ELF="${1:?usage: qemu-ec.sh | qemu-ec.sh --prepare}" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-7e461b3}" +EC_I2C_SOCK="${EC_I2C_SOCK:-/tmp/qemu-ec-i2c.sock}" +EC_GPIO_SOCK="${EC_GPIO_SOCK:-/tmp/qemu-ec-gpio.sock}" + +# GHCR image that publishes the prebuilt QEMU (with `ec` machine support). +QEMU_IMAGE="ghcr.io/opendevicepartnership/odp-qemu-builder/qemu:${ODP_QEMU_TAG}" +# Location of the binary inside that image. +QEMU_IMAGE_BIN="/usr/local/bin/qemu-system-riscv32" +# Where we cache the extracted binary on the host. +QEMU_CACHE_DIR="${SCRIPT_DIR}/target/qemu-ec" +QEMU_CACHE_BIN="${QEMU_CACHE_DIR}/qemu-system-riscv32" + +# Resolve the QEMU binary, pulling it from GHCR on first use. +resolve_qemu() { + # 1. Explicit override. + if [[ -n "${QEMU:-}" ]]; then + if [[ ! -x "$QEMU" ]]; then + echo "error: \$QEMU is set to '$QEMU' but it is not executable" >&2 + exit 1 + fi + echo "$QEMU" + return + fi + + # 2. Previously cached binary. + if [[ -x "$QEMU_CACHE_BIN" ]]; then + echo "$QEMU_CACHE_BIN" + return + fi + + # 3. Pull from the GHCR image. + if ! command -v docker >/dev/null 2>&1; then + echo "error: docker is required to fetch qemu-system-riscv32 from ${QEMU_IMAGE}" >&2 + echo " (install docker, or set \$QEMU to a local qemu-system-riscv32 with 'ec' machine support)" >&2 + exit 1 + fi + + echo "Pulling QEMU from ${QEMU_IMAGE}..." >&2 + docker pull "$QEMU_IMAGE" >&2 + + mkdir -p "$QEMU_CACHE_DIR" + local cid + cid="$(docker create "$QEMU_IMAGE")" + # `resolve_qemu` runs in a command-substitution subshell, so an EXIT trap is + # scoped to that subshell and fires on both success and an errexit abort + # (e.g. a failing `docker cp`). RETURN would be skipped on errexit, leaking + # the temporary container. + # shellcheck disable=SC2064 + trap "docker rm -f '$cid' >/dev/null 2>&1 || true" EXIT + docker cp "${cid}:${QEMU_IMAGE_BIN}" "$QEMU_CACHE_BIN" >&2 + chmod +x "$QEMU_CACHE_BIN" + + echo "$QEMU_CACHE_BIN" +} + +QEMU_BIN="$(resolve_qemu)" + +# `--prepare` only warms the cache; report the resolved binary and exit. +if [[ "$MODE" == "prepare" ]]; then + echo "QEMU ready: $QEMU_BIN" >&2 + exit 0 +fi + +# QEMU arguments shared by both the interactive and headless paths. +# +# - `-machine ec` EC board exposing the I2C-target and GPIO sockets. +# - `-bios none` dev-qemu is a bare-metal kernel; no firmware needed. +# - `-serial pty` UART0 is bridged to a PTY for terminal/ec-test-cli. +# - `-chardev socket,...` The I2C-target and GPIO lines as UNIX sockets that +# external programs can connect to (server=on). +QEMU_ARGS=( + -machine ec + -bios none + -nographic + -monitor none + -serial pty + -chardev "socket,id=ec-i2c-target,path=${EC_I2C_SOCK},server=on,wait=off" + -chardev "socket,id=ec-gpio0,path=${EC_GPIO_SOCK},server=on,wait=off" + -kernel "$ELF" +) + +# Headless path: no defmt logging, so semihosting is left disabled and QEMU runs +# raw. The "char device redirected to /dev/pts/N" line goes to stdout where +# callers (integration-test.sh) grep it. +if [[ "${DEFMT_LOG:-}" == "off" ]]; then + exec "$QEMU_BIN" "${QEMU_ARGS[@]}" +fi + +# Interactive path enables semihosting so defmt can route its log stream to +# QEMU's own stdout (`target=native`), keeping it separate from UART0. +QEMU_ARGS+=(-semihosting-config enable=on,target=native) + +# Interactive path: defmt-print decodes the semihosting log stream. +if ! command -v defmt-print >/dev/null 2>&1; then + echo "error: defmt-print is required for the interactive run path" >&2 + echo " install it with: cargo install defmt-print" >&2 + echo " (or use 'cargo run-headless' to disable logging)" >&2 + exit 1 +fi + +# With `-serial pty`, QEMU prints a single "char device redirected to +# /dev/pts/N (label serial0)" line on stdout before any semihosting data. Peel +# that first line off to stderr (so the PTY path stays visible) and feed the +# remaining bytes to defmt-print. +"$QEMU_BIN" "${QEMU_ARGS[@]}" | { + IFS= read -r ptsline + printf '%s\n' "$ptsline" >&2 + defmt-print -e "$ELF" +} diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh index 9b4aaaa..ab82214 100755 --- a/scripts/integration-test.sh +++ b/scripts/integration-test.sh @@ -40,6 +40,11 @@ cd "$REPO_ROOT/platform/dev-qemu" echo "Building dev-qemu..." cargo build --locked --release --config 'env.DEFMT_LOG="off"' +# Warm the QEMU cache up front (pulls `qemu-system-riscv32` from the GHCR image +# on first use) so a cold `docker pull` doesn't count against the PTY poll below. +echo "Preparing QEMU..." +./qemu-ec.sh --prepare + # Then launch it in "headless mode" (again, DEFMT disabled), # and poll until serial comms are ready echo "Starting dev-qemu..."