Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 2 additions & 9 deletions platform/dev-qemu/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -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"]

Expand All @@ -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"']
29 changes: 23 additions & 6 deletions platform/dev-qemu/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 36 additions & 7 deletions platform/dev-qemu/README.md
Original file line number Diff line number Diff line change
@@ -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/<N> 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. |
151 changes: 151 additions & 0 deletions platform/dev-qemu/qemu-ec.sh
Original file line number Diff line number Diff line change
@@ -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 <PATH_TO_ELF>`.
#
# 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 <elf> | 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"
}
5 changes: 5 additions & 0 deletions scripts/integration-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
Loading