QSOE Systems

Release notes

HFI BIOS 1.4

A fourth board, and a processor nobody was using.

September 2026 · succeeds 1.3 (August 2026) · GPL-2.0-or-later

The StarFive VisionFive 2 boots HFI BIOS on real silicon, lights an HDMI panel from the SoC's own display controller, and hands a bootloader — and then an operating system — a text console they can repaint for themselves. What repaints it is the JH7110's S7 monitor core: a hart with no supervisor mode, marked status = "disabled" in every device tree in its family, which no operating system will ever schedule, and which firmware used to abandon inside an SPL image it had already dissolved.

That the display needed a character generator was known from the K3, whose VideoBIOS module borrows an application hart for the job. What was new here is that the standard way to get a processor could not be used at all: SBI's hart-state extension starts a hart at the caller's privilege, so a core without supervisor mode cannot be started through it — the request is not refused, it is unaskable. The answer was to stop asking. The SPL hands that hart a permanent home before firmware disappears, and the VideoBIOS module adopts it afterwards.

The component set

ComponentWhat it is1.31.4
HFI BIOS the firmware layer over U-Boot: POST, Set-Up, menus, console, the module loader 1.31.4
vf2-videobios StarFive JH7110 — DC8200 + Innosilicon HDMI; the generator on the S7 0.2
k3-videobios SpaceMiT K3 — the SoC display over DisplayPort; the generator on a borrowed hart 0.60.7
gk208-videobios NVIDIA GK208 over PCIe, for the HiFive Unmatched; the generator on the card 0.170.18
virtio-gpu-videobios QEMU virt + virtio-gpu, the hardware-free platform 0.20.2
libhfivbios the runtime every module shares: relocation, font, text mode, the character generator 0.3
mr-bml QSOE's bootloader, the reference consumer of the handoff block 0.90.9

Each VideoBIOS module is a separate repository and a separate blob; the firmware image embeds the one for its board. libhfivbios did not exist at 1.3 — the shared half of every module was factored out of them during this cycle.

On this page

Highlights

What makes 1.4 a different firmware to run than 1.3.

A fourth board. make TARGET=visionfive2, selectable in Set-Up beside the K3, the Unmatched and QEMU, with its own U-Boot patch series and file manifest. The display is the DC8200 through an Innosilicon HDMI 2.0 transmitter, driven by the vf2-videobios module: EDID read over the transmitter's own DDC master, the sink's preferred timing programmed, and no firmware blob anywhere in the path. Verified on a v1.3B at the sink's native 1280×1024 and at 1920×1080.

A character generator on a core no operating system can use. The JH7110's S7 has no MMU, no supervisor mode and no data cache; it is the core the boot ROM starts, and until now the SPL ran on it, lost the boot lottery, and left it spinning inside an image about to be handed back to the cache. Now the SPL parks it on a mailbox in the S7's own 8 KiB of tightly coupled RAM — memory that belongs to no operating system, appears in no memory node, and needs no reservation from anybody. The VideoBIOS module adopts that hart in machine mode, with no SBI beneath it, and leaves it waiting on the handoff block's doorbell. A consumer writes cells, fences, stores one word, and a processor that belongs to nobody else turns them into pixels.

It survives into the operating system. The point of publishing a handoff block was always that an OS which understands it gets a text console with no display driver at all. 1.4 is the first release where that is demonstrated rather than described: QSOE's devc-hficon rings the doorbell after ExitBootServices, the S7 answers, and both QSOE variants have a monitor-and-keyboard console on the VisionFive 2 without a line of display code in the OS. The bootloader asks the firmware to keep the controller alive and withholds the processor from the device tree the OS receives — in that order, and never the second without the first.

Two facts measured, not assumed. The S7 executes from DRAM at whatever address the firmware relocated the module to; and the JH7110's core complex is coherent from the S7 to a U74 including a line the U74 left dirty in its L1 — four tokens out of four, read with no cache maintenance at all. So a consumer owes nothing before ringing the doorbell but a fence. The new spare command in the System Console is the instrument that answered both questions, and it stays in the firmware for the next board.

The mechanism is generic. CONFIG_SPL_RISCV_SPARE_HART describes a shape the whole SiFive-derived family shares — the FU740's S7, the FU540's E51 — and it guards itself: the SPL will not stand down unless a hart the device tree actually endorses is available to take the boot. The VideoBIOS runtime gained a second hart provider beside SBI HSM, chosen at build time, so a module says CHARGEN_HART := parked and nothing else in it has to know which world it lives in.

Damage is the host's to report. A firmware console that named "something changed" and got "repaint everything" back was free where the repaint is one store — a GPFIFO kick, a doorbell to a hart — and ruinous where the module rasterizes on the calling CPU: a full screen of glyphs plus an 8 MB writeback per character, and a menu that took 49 seconds to appear. The host now names the cells it touched, and a keystroke costs a character.

The whole machine, again. Ethernet brought to a negotiated link by firmware and handed over still running, with the link state written into the device tree the OS receives; the SoC temperature sensor on the PC Health Status page; the Flash Utility taught this board's NOR — two images with the environment between them and no partition table anywhere on the device; and a way to run a build over the serial cable alone that writes nothing to the machine, which is both the bring-up path and the recovery path.

Two point releases folded in. 1.3.1 found the M.2 slot's NVMe drive, invisible to firmware and OS alike: its PCIe port was being configured as an endpoint rather than a root complex — the vendor's own header comments the mode bit the wrong way round — and the fault hid behind three working ports only because port A alone implements that bit. 1.3.2 fixed the machine resetting instead of booting whenever the Ethernet controller was enabled — U-Boot kept a watchdog's servicing schedule inside memory that ExitBootServices freed, and the next pass through that schedule called through whatever had been allocated over it. That one is generic U-Boot, and was submitted upstream.

The full change list

Everything that changed since 1.3, by area. Open the parts you want — Open every section · the same ground is covered in CHANGELOG.md in the source repository.

The VisionFive 2 port

A JH7110 board, brought from nothing to a whole machine.

  • Target and build. TARGET=visionfive2 with its own U-Boot patch series and file manifest; the build refuses a VideoBIOS module belonging to a different target rather than embedding it and failing on the glass.
  • Display. DC8200 modeset, the Innosilicon HDMI PHY (pre- and post-PLL, LDO, serializer, TMDS drivers), EDID over the transmitter's DDC master, the sink's preferred timing. The VOUT power island, clocks, resets and the DDC pad mux, in the order the hardware requires.
  • The console. The text grid and Controller Handoff Block, with damage reporting so a keystroke costs a character and not a screen.
  • Storage, USB, sensors. NVMe in the M.2 slot, a second USB pass so the keyboard is found, the SoC temperature sensor with the clocks it needs registered so it can be read at all.
  • Ethernet. The JH7110 PHY is woken before the MAC reset — resetting the MAC while the PHY still slept left the link down after a cold start and reliably up after a warm one, which is the tell of an ordering fault. The link is negotiated by firmware and handed over live.
  • A device tree of our own. Upstream U-Boot's control FDT for this board has no display subsystem in it; the image now carries a full board DTB and installs it at hand-off, so an OS is not handed a tree that cannot drive the panel the firmware just lit.
  • The Flash Utility. Two images in SPI NOR — SPL at 0 and the FIT at 0x100000 — with the environment between them and no GPT anywhere. The packaged SPL is the one the boot ROM will actually take: the header-carrying image, not the raw binary, which bricks a NOR boot with "Main section boot fail".
  • Booting over the serial cable alone. scripts/vf2-uart-boot.sh sends the SPL by XMODEM and the FIT by YMODEM, waits for the ROM's poll to go clean before speaking (the ROM's second greeting lands exactly where an ACK belongs, and the result is indistinguishable from a bad cable), and writes nothing to the board. SD and eMMC boot are gone from upstream for this SoC, so this is the recovery path.
The character generator on the S7

A processor the machine was not otherwise using, and how it was had.

  • The SPL parks it. CONFIG_SPL_RISCV_SPARE_HART: before entering OpenSBI — the last moment the SPL still controls the machine — a small machine-mode stub is copied into memory that outlives firmware and the spare hart is sent there, parked on a mailbox. On the JH7110 that memory is the S7's own DTIM at 0x0110_0000. The spare is the hart the SPL is standing on, so it names no boot-hart preference, sends the U74s into OpenSBI, and walks into the stub itself. It refuses to stand down unless a device-tree-endorsed hart is available to take the boot.
  • The stub is careful. lla and never la: U-Boot is built with a global offset table, and la resolves through it — a load from the very image this hart must stop depending on. It finds its area from its own program counter rather than from an argument register, so the area is still findable after a call returns. It records which harts reached the SPL at all, and each one's misa — the only place a monitor core's missing supervisor mode is visible.
  • The module adopts it. libhfivbios gained CHARGEN_HART: sbi asks firmware for an idle hart, in supervisor mode, and goes on calling SBI for its timer; parked adopts one an earlier stage left waiting, in machine mode, with the memory-mapped CLINT as its clock. Chosen at build time, for the same reason the wait strategy is — a blob carries one strategy, not two.
  • Which hart is calling is part of the contract. The generator runs in machine mode and may read the CLINT; the firmware that dispatched it runs in supervisor mode, where OpenSBI PMP-protects the CLINT precisely so supervisor code cannot bypass the SBI timer. Giving the whole provider one clock put a machine-mode device read on the supervisor-mode hart and reset-looped a board. The split is by privilege, not by provider.
  • Retiring is a return. A hart with no supervisor mode has no HART_STOP to call. The loop returns, the park stub takes its ret, clears the mailbox entry, and the hart is back on its mailbox — which is how an OS gives it back through retire_pa, and how the firmware retires it at ExitBootServices.
  • The blob is built for both cores. Everything in the module may run on the S7, which has no FPU, so it is compiled for the intersection of the two (rv64imac_zicsr_zifencei_zba_zbb, lp64) rather than the U74's rv64gc. A floating-point instruction emitted for something as ordinary as a struct copy would be an illegal instruction with no handler. The build fails if one ever appears.
  • A hart that will not answer is recorded as stuck, not left active: those are two different facts. Not active means publish no doorbell and render inline; stuck means the module's memory must never be released. Conflating them was a frozen console rather than a slow one.
  • spare status and spare probe. Whether a hart is parked, whether its beat advances, what the SPL saw on its way out — and a routine run on the hart itself that answers whether it observes a write another hart left dirty in its cache. Four rounds, four tokens, no flush: it does.
The handoff block and the operating system

What a consumer is told, and what it can rely on.

  • FIRMWARE_ONLY, a fourth doorbell kind. "Only the producer's CPU can turn a cell into a pixel." It is not NONE, and a consumer that reads it as NONE loses the display: mr-bml did, on a v1.3B, silencing the firmware console to write a menu into cells nobody would rasterize. The rule for a consumer is that an unrecognized kind reads as FIRMWARE_ONLY, never as NONE.
  • present_rect() in the VideoBIOS module ABI: the host names the cells it touched and the module rasterizes exactly those. A module must not narrow the rectangle by diffing the grid — the host also writes pixels straight into the framebuffer, under cells it marks transparent, and a diff cannot see them. Re-blitting an emblem changed no cell, so nothing was flushed, and half of it stayed in the cache: a torn logo on the glass.
  • A retired processor is no longer advertised. When a module gave its hart back at ExitBootServices it withdrew the doorbell — from the loader's temporary copy of the block, not the one the OS reads, which went on naming a doorbell for a processor that had gone home. The firmware owns the published copy, so the firmware now withdraws it, only after the module confirms it let go. This fixes the same latent fault on the K3.
  • "Reserve video memory" in Set-Up. Whether the OS is told which memory the controller owns — cell array, palette, control block, doorbell, module image — as /reserved-memory. It was a bare environment variable reachable only by setenv; it is now a row under Advanced Settings, and the refusal at hand-off names it. Default Disabled: those nodes cost an OS that has never heard of us a few megabytes and buy it nothing.
  • Keeping the controller alive across the hand-off is requested by the bootloader — mr-bml's hfi_vidctrl_persist=1 — and honored only when that memory was reserved. The bootloader withholds the controller's harts from the OS's device tree first and asks the firmware second, and never the second if the first fails. That ordering is why a Set-Up row for the request itself was built and then deliberately removed: firmware cannot do the first half, and a switch whose safety depends on which board it is on is worse than no switch.
Set-Up and the console
  • Advanced Settings sizes itself to its own help text. A fixed width clipped a description to "…which memory the video controller ow", and the person reading that is the one person who cannot know what was cut off. The page now measures its longest help line and opens far enough for it.
  • The text grid's cell size comes from the controller that was just programmed, not from a constant — which was only correct while every board used the same font.
  • A console device is never held across a command. Resetting USB frees the keyboard's stdio device; code that kept a pointer to it was still holding one afterwards, and crashed the firmware at address zero.
  • U.S. English throughout the source, comments and documentation.
Fixed — including 1.3.1 and 1.3.2

The two point releases, and what was found on the way to 1.4.

1.3.1 — the NVMe drive that hid behind three working ports

  • The K3's M.2 slot enumerated no drive — not in the BIOS, not in lspci under Linux — while the other three PCIe ports worked. Port A was being configured as an endpoint: bit 31 of its command register selects the role, the vendor's header comments it 0: EP, 1: RC, and on this part it is the comment that is inverted. An endpoint waits to be handed link and lane numbers rather than handing them out, so training fell back to Detect forever. Only port A implements that bit, which is why the other three never noticed. And because Linux's own driver never writes it on this SoC, an endpoint left behind by firmware was inherited by the OS — one line in a bootloader took a disk away from the operating system as well.

1.3.2 — the machine reset instead of booting, whenever Ethernet was enabled

  • U-Boot keeps a watchdog's servicing schedule inside the watchdog device's own private data, and nothing takes that entry off the schedule — while ExitBootServices removes every active device and frees exactly that memory. The next pass through the schedule called through whatever had since been allocated over it. Enabling Ethernet put one more device into the teardown, and that was enough to change what landed on the freed block. The watchdog is now taken off the schedule before its memory goes. Generic U-Boot, and submitted upstream.
  • Why one payload booted and another did not: nothing in the failure depended on the payload's code, only on the order and size of the allocations each boot happened to make. A kernel loaded one way survived what a kernel loaded another way did not.
  • Why a crash this loud presented as a silent reset: the firmware printed a complete exception dump — and to the monitor, an instant before the reset cleared it, because a payload that drives the display hands the console back just before launching the OS. The dump was never lost; it was on the glass.

In 1.4

  • The JH7110 Ethernet PHY reset out from under itself (see the VisionFive 2 section).
  • A console device remembered for longer than it existed (see Set-Up).
  • A retired processor still advertised as able to repaint (see the handoff block).
  • The K3's keyboard poll mode named with a symbol that exists.
  • The two vendored ABI headers brought to the version the block actually reports.

Known gaps

Stated rather than implied away.

  • The S7 spins. Its generator waits with a paced poll, not a timer: the first bring-up depended on no CLINT programming, no interrupt enable and no wfi — three things that fail in the same silent way. It burns a core nothing else in the machine can use, so this is a power question, not a feature one. The timer comes next.
  • Every ring repaints the whole screen. That is the K3's contract too, and on the S7 — no data cache, every pixel a store to the shared L2, then an 8 MB writeback — it costs tens of milliseconds. It is asynchronous, the firmware stores and walks away, rings coalesce, and scrolling keeps up with a repeated help; but a damage rectangle in the handoff block would let the hart repaint only what changed, and it is not there yet.
  • Keeping the controller alive needs a bootloader that knows how. mr-bml does. An operating system started through a boot protocol cannot ask for itself, and the firmware deliberately does not decide which processors an OS may online. A different bootloader has to do what mr-bml does, in the same order.
  • The VisionFive 2 boots from NOR or over the serial cable, and nothing else. Upstream U-Boot dropped the SD and eMMC first-stage path for this SoC; a modern SPL has no MMC in it. The Flash Utility and the UART script are the two ways in.
  • The K3 still runs the vendor's first stage. Its OpenSBI delegates hart state to coprocessors that the first stage has to load and start, and ours does not. The HFI deliverable on that board is the one image in the uboot partition, and the vendor's stages below it are left exactly as shipped.
  • One diagnostic on the K3 cries wolf. "PHY reset line 15 will not go high" is printed at probe on every boot and contradicted by the next lines, which negotiate the link. It was true once, during bring-up; the readback now samples a register that does not reflect the pad's level under its final mux. It will be corrected in a point release.

Getting it

One firmware package per board, written with the BIOS Flash Utility from Set-Up — or, on a board already running Linux, by the board's own means. Each is a FIT carrying every stage the board's flash takes, and the Flash Utility refuses one built for a different board.

Extracting the stages

A package is a FIT — a flattened image tree — and dumpimage from u-boot-tools lists and extracts it. dumpimage -l on any of the three names its images; the extraction lines below are what that listing gives. Which partition or offset each stage belongs at is described above, per board, and is the reader's business from there.

# SpacemiT K3 Pico-ITX: one image, U-Boot proper with the board's device tree
dumpimage -T flat_dt -p 0 -o u-boot.itb HFI-SpaceMiT-K3-PicoITX-SysBIOS-1.4.hfifw

# SiFive HiFive Unmatched: the SPL, then the OpenSBI + U-Boot FIT
dumpimage -T flat_dt -p 0 -o u-boot-spl.bin HFI-SiFive-Unmatched-SysBIOS-1.4.hfifw
dumpimage -T flat_dt -p 1 -o u-boot.itb     HFI-SiFive-Unmatched-SysBIOS-1.4.hfifw

# StarFive VisionFive 2: the same two -- but this SPL carries the boot ROM's header
# (the "sfspl" form, u-boot-spl.bin.normal.out); the raw SPL is not what the ROM loads
dumpimage -T flat_dt -p 0 -o u-boot-spl.bin.normal.out HFI-StarFive-VisionFive2-SysBIOS-1.4.hfifw
dumpimage -T flat_dt -p 1 -o u-boot.itb                HFI-StarFive-VisionFive2-SysBIOS-1.4.hfifw

SHA-256 of each package and of every stage inside it, with the stage sizes the listing reports. (Two of the packages extract a file called u-boot.itb; they are not the same file.)

b5e39666e0127bedb59cab921366dc1aa8756b5aa1bb702220a9f4cde265beb7  HFI-SpaceMiT-K3-PicoITX-SysBIOS-1.4.hfifw
f438d962a3685c606cff57c89097b00c1b316d188e4e08e5591247f63714e6c8  u-boot.itb                # K3, 886649 bytes

ca8836ec2aadabe719b45f27332ae08bb11e199dbf0e19b3250f267ed75e9dd9  HFI-SiFive-Unmatched-SysBIOS-1.4.hfifw
b9c33d57565b1cf692c8d98c70b68ac9449cf18d0c2c99979a8ec4f2a8e900fe  u-boot-spl.bin            # Unmatched, 94195 bytes
9f3fe2203f0287e28ef3d39494bea32a74f5a5f96ef9384386f1d4950b0d5972  u-boot.itb                # Unmatched, 943685 bytes

abc3347868f4840423e295d96803094b8aedb8f240e56f8aa812492aac6005ce  HFI-StarFive-VisionFive2-SysBIOS-1.4.hfifw
40478fa89e2392b7ed79803619485be47bd24fcdb9e5ddeb0e8de4492762dae8  u-boot-spl.bin.normal.out # VisionFive 2, 149385 bytes
e7671850fcdd3f5c43e5a5440fc96661cfa596ff855941085b8e3e17c0764636  u-boot.itb                # VisionFive 2, 1205849 bytes

The packages live at github.com/qsoe-dev/hfi-bios, one directory per release. To build from source:

git clone https://gitlab.com/bios-riscv/hfi-bios
cd hfi-bios
make TARGET=visionfive2      # or k3, unmatched, qemu -- fetches U-Boot and OpenSBI itself
make fwpkg TARGET=visionfive2 # the .hfifw for the Flash Utility

The VideoBIOS modules are their own repositories under gitlab.com/bios-riscv/videobios — one per display controller, plus libhfivbios, the runtime they share. The build looks for the module beside the firmware tree and embeds it; the two ABI headers a consumer needs, hfi_vidctrl_handoff.h and hfi_vbm.h, are MIT so that any bootloader or operating system can carry them verbatim.

Next: the S7's generator moves from a paced poll to a timer, and the handoff block learns to carry a damage rectangle so a hart repaints what changed rather than the screen. Both are performance work on a path that is already correct.

Back to HFI