Skip to content

real-hw-test: init - #71

Merged
phip1611 merged 22 commits into
rust-osdev:mainfrom
phip1611:real-hw-test
Sep 20, 2026
Merged

phip1611 merged 22 commits into
rust-osdev:mainfrom
phip1611:real-hw-test

Conversation

@phip1611

Copy link
Copy Markdown
Member

Init a new crate member that builds a EFI file that can be easily booted on real hardware.

@phip1611
phip1611 force-pushed the real-hw-test branch 2 times, most recently from 1de6fe5 to 9bc95d0 Compare August 16, 2026 15:17
@phip1611
phip1611 marked this pull request as draft August 16, 2026 15:24
@phip1611 phip1611 self-assigned this Aug 18, 2026
@phip1611
phip1611 marked this pull request as ready for review August 26, 2026 08:57

@phip1611 phip1611 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost. Let's drop the ci feature. The crate should not know anything required for a CI run. IF this means a QEMU CI run is not feasible than drop that entirely. If it is possible, keep the QEMU CI run.

Comment thread .github/workflows/real-hw-test.yml
Comment thread src/backend/mmio.rs Outdated
Comment thread real-hw-test/.envrc
@phip1611
phip1611 force-pushed the real-hw-test branch 2 times, most recently from d8f46d9 to 128bac2 Compare September 2, 2026 08:45
Comment thread real-hw-test/src/discovery/acpi.rs
Comment thread real-hw-test/src/main.rs Outdated
Comment thread real-hw-test/src/driver_test.rs
Comment thread real-hw-test/scripts/install-usb.sh Outdated
Comment thread src/backend/mmio.rs
Comment thread real-hw-test/src/discovery/acpi.rs
Comment thread real-hw-test/scripts/run-qemu-ci.sh
@phip1611
phip1611 force-pushed the real-hw-test branch 6 times, most recently from 763dae3 to 094020d Compare September 13, 2026 18:19
Comment thread real-hw-test/src/discovery.rs Outdated
Comment thread real-hw-test/src/firmware.rs Outdated
Comment thread real-hw-test/src/discovery.rs Outdated
Comment thread real-hw-test/src/logging.rs Outdated
Comment thread real-hw-test/src/main.rs Outdated
Comment thread real-hw-test/src/discovery/acpi.rs
Comment thread real-hw-test/src/discovery/acpi.rs
Comment thread real-hw-test/scripts/run-qemu-ci.sh Outdated
Comment thread real-hw-test/src/device.rs Outdated
Comment thread real-hw-test/src/discovery/acpi.rs Outdated
Comment thread real-hw-test/flake.nix Outdated
Comment thread real-hw-test/src/main.rs Outdated
Comment thread real-hw-test/src/discovery/pci.rs
Comment thread real-hw-test/src/discovery/pci.rs Outdated
Comment thread real-hw-test/src/discovery/pci.rs Outdated
Comment thread real-hw-test/README.md
Comment thread real-hw-test/scripts/install-usb.sh
Comment thread real-hw-test/README.md Outdated
Comment thread .github/workflows/real-hw-test.yml
Comment thread real-hw-test/scripts/run-qemu-ci.sh
Please note that the whole "real-hw-test" commit series was mainly
created by Codex and Claude, but with significant handholding and
various iterations by me.

The build is parameterized by ARCH from the start: the x86_64 and
aarch64 UEFI targets with their removable-media file names,
'make artifacts' to cross-compile all of them in one step, and the
architecture in the on-screen banner.

The uefi crate is bound as uefi_rs and re-exported through a crate-local
uefi module, the one place where single items such as println! can be
overridden for the whole test.
Booting the image under QEMU with OVMF gives a fast iteration loop that
needs no physical machine or USB stick. COM1 is wired to the launching
terminal, and an additional PCI serial device exposes an independently
discovered UART through a PTY.

QEMU follows ARCH: q35 with OVMF on x86_64, virt with pflash EDK2,
ramfb, and a USB keyboard on aarch64, where TCG is the default because
the development host is typically x86_64.

The machine and firmware selection lives in scripts/qemu-machine.sh so
that other QEMU front ends can share it.
The dev shell supplies QEMU, OVMF, and rustup. The .envrc enables
direnv integration.

The full QEMU package is used because qemu_kvm carries only the host
architecture's system emulator; the aarch64 firmware paths are exported
next to OVMF.
Candidates from every discovery path land in one inventory deduplicated
by address, so a UART described by several sources is still tested only
once. Firmware serial controllers are disconnected first because the
firmware and the driver under test must never program a UART
concurrently; the firmware baseline is recorded on screen beforehand.

COM1 at 0x3f8 is registered unconditionally: the targeted machines are
required to expose it, so its absence must surface as a test failure
rather than as silent non-discovery.

Port I/O exists only on x86, so the port address form and the COM1
source are cfg-gated from the start.

Every candidate records where it lives and which discovery paths found
it, printed as 'location:' and 'found by:' lines, so an operator can
tell a built-in UART from an add-in card and see when several paths
describe one device.
The conventional COM2-COM4 addresses are only accepted when the crate's
own presence check, Uart16550::check_present(), answers, because reading
an absent port yields junk. It is the same scratch-register test that
init() runs first, so discovery and the later driver tests agree on what
counts as a device. COM1 stays registered unconditionally.

The legacy probe is x86-only and cfg-gated accordingly.
The Serial Port Console Redirection table is how firmware names its
console UART on machines without ISA-conventional COM ports, which is
the norm on headless servers and on non-x86 platforms. It also carries
the address space, access width, and clock, so the port can be driven
without guessing; MMIO-mapped register blocks become testable here.

Only 16450/16550-compatible interface types with byte-wide access are
accepted; everything else (for example a PL011) is reported and
skipped rather than programmed blindly.

Without x86 port instructions, a System I/O SPCR is reported and
skipped.

Only ACPI 2.0 tables are consulted (XSDT); an RSDT fallback would serve
no UEFI machine.

The table's PCI identity fields (revision 2 and later) are read so that
a console that is a PCI function is classified as one; PCI evidence
wins when several paths describe the same address.
Serial add-in cards and paravirtual devices such as QEMU pci-serial
live behind BARs, so neither fixed-address probing nor SPCR sees them.
Enumerate serial-class endpoints through the UEFI PCI root bridges and
accept only an unambiguous 16550-compatible programming interface with
a usable BAR0; vendor-specific layouts are reported but not touched.

Firmware leaves the decoding of endpoints it never binds disabled; an
assigned BAR of an unambiguous UART is therefore enabled explicitly.
Without x86 port instructions an I/O BAR is translated through the
ACPI-described PCI I/O window, which makes this the first commit that
builds for aarch64, so 'make check' lints both targets from here on.

Each candidate records its PCI identity: vendor and device ID, and
whether the function sits on the root bridge's own bus (typically
integrated) or behind a bridge (typically an add-in card); known QEMU
serial devices are named.

The window itself comes from ACPI: the root bridge protocol reports the
I/O aperture without its CPU-side translation (QEMU virt: 0x0-0xfff,
translation 0), but the DSDT's resource templates embed the translated
window as fixed-format descriptors, which a strict byte scan finds
without interpreting AML.
Run the public driver API on every discovered candidate: init, the
register values it must leave behind, test_loopback with a restored
configuration afterwards, DSR/CTS connection signals, and the
try_send_byte/send_bytes/send_bytes_exact paths. init() carries the
crate's presence check, so an absent UART fails there rather than in a
later step.

Absent DSR/CTS is only a warning because three-wire and USB serial
cables legitimately omit modem-control lines.

The PIO backend exists only on x86; its driver variant is cfg-gated.
Automatic checks cannot prove that a real cable to a remote terminal
works. On operator request each passing UART offers register dumps,
a transmit line to the remote side, connection-signal inspection,
another loopback, and receive-with-echo of typed characters. Escape,
locally or as serial 0x1b, skips a UART that has no remote connected;
the skip is recorded as a warning instead of a failure.
Manual hardware checks can wait forever for an operator. Disable the
UEFI image watchdog so a long session does not reset the machine.
Report firmware failures on screen.
Mirror every test diagnostic to a dated file below
/uart_16550_test_logs while retaining UEFI console output. Keep one
flushed FAT file handle so a failed write is reported as critical and
aborts the test instead of losing failure evidence.

The file name carries the architecture so one stick can hold runs
from several machines.
Report the dated USB-drive log path before each normal test exit. This
keeps the result on screen when the automated checks fail as well as
when they complete successfully.
A real UART can still be draining the byte accepted by try_send_byte
when send_bytes is called. Retry the nonblocking API for one second
instead of treating temporary backpressure as a driver failure.

Preserve an earlier modem-signal warning if a later driver check fails.
Describe what the application discovers and checks, the recommended
hardware and cable setup, how to build it and run it under QEMU, how to
read its output, and the current state of architecture support.
The test is meant to travel on a USB stick: build with 'make artifacts',
plug in a stick that carries a FAT32 EFI partition, run 'make install',
pick the stick from the list, and boot the test machine from it. The
script copies every built architecture to its removable-media path
after verifying that the target is the exact mount point of a FAT32
partition on a GPT disk. It never partitions, formats, or mounts
anything itself, so a wrong pick or path stops with a diagnostic instead
of writing into another filesystem. Built-in disks are not offered at
all, because they carry the host's own EFI system partition; USB_MOUNT
names the target explicitly for scripted use.
Run the unmodified interactive image headlessly under TCG. The harness
answers the operator prompts through QEMU-monitor sendkey and judges
the run by the log persisted on the boot volume plus both serial
captures. Require automatic checks for legacy COM1 and a PCI UART.

The machine and firmware selection is shared with run-qemu.sh through
scripts/qemu-machine.sh; the harness only adds its headless front end.
The x86_64 smoke test only covers port I/O and the legacy COM path. The
aarch64 run is the counterpart for the MMIO backend, for the
firmware-described (SPCR) console, and for the PCI I/O-window
translation, so both halves of the driver are exercised on every push
without any real hardware.

The aarch64 virt machine has no 16550 except the PCI serial device, so
the run must reject the PL011 console via SPCR and drive the PCI UART
through the translated I/O window with the MMIO backend.
Point readers to the UEFI application for real hardware and to the
VM-based integration test.
@phip1611
phip1611 enabled auto-merge September 20, 2026 11:56
@phip1611
phip1611 added this pull request to the merge queue Sep 20, 2026
Merged via the queue into rust-osdev:main with commit 6298fdf Sep 20, 2026
18 checks passed
@phip1611
phip1611 deleted the real-hw-test branch September 20, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant