Second in a series on building a firmware-free VideoBIOS for RISC-V. In Part 1 I explained why I wanted a RISC-V machine that greets its monitor at power-on the way an old IBM PC does. This post is about the first idea I had for how to get there — and the first-in-the-world compile that it needed.
The story so far. I built a RISC-V personal computer — a SiFive HiFive Unmatched with a real NVIDIA graphics card and a real monitor — and it stayed dark until Linux loaded the
nouveaudriver. I wanted it to speak to the screen before any operating system. The question was how.
The card already knows how to wake itself
Here is a fact that took me an embarrassingly long time to fully appreciate, even though I had known it in the abstract for years:
Every PC graphics card carries its own little program that knows how to bring itself up.
It is called the video BIOS — the VBIOS — and it lives in a small ROM on the card. When an old PC powers on, the motherboard firmware finds that ROM, and runs it. The VBIOS initializes the card's memory, sets up a display mode, wires up the outputs, and hands back a card that will happily show text on a monitor. That is the reason a PC can greet you on screen before it has loaded a single byte from disk: the intelligence to light the display is already sitting on the card, and the CPU simply executes it.
So my card — a GK208, a modest little NVIDIA Kepler, sold as a GeForce GT 710 — already contained, in ROM, a complete recipe for waking itself into a usable display. I did not have to invent that recipe. It was right there.
There was just one problem, and it is the whole reason this series is not three paragraphs long.
The VBIOS is an x86 program. It is machine code for Intel processors. My computer is RISC-V. My CPU cannot execute a single instruction of it.
What if the RISC-V CPU could pretend to be x86?
The idea that started everything was simple to state: if my RISC-V processor cannot run the card's x86 firmware directly, perhaps it can emulate an x86 processor well enough to run it. Interpret the x86 instructions one by one, in software, on the RISC-V core. Slow, yes — but this program runs exactly once, at boot, for a fraction of a second. Speed does not matter. Correctness does.
It felt almost too obvious. Surely, I thought, this is a solved problem. And in a sense, it was — which brings me to the buried treasure.
The emulator that was already there
U-Boot — the bootloader I use on the Unmatched — turns out to
already contain a small x86 emulator. It lives in
drivers/bios_emulator/, and its lineage goes back to the old XFree86
/ SciTech x86emu, a compact interpreter that was used, years ago, to
POST video cards on non-x86 machines — PowerPC Macs, some
ARM systems — for this exact purpose: running a card's x86 VBIOS on a CPU
that is not an x86.
It was, in other words, precisely the tool I needed. It had just been sitting there, more or less forgotten, aimed at architectures nobody uses for this anymore.
The obvious next step: build it for RISC-V and point it at my GK208.
Except, as far as I could tell, nobody had ever compiled
bios_emulator for RISC-V. It had support for x86, PowerPC,
MIPS, ARM, SH — the architectures of a previous era. RISC-V simply was not
in the code. And when I tried to build it anyway, it did exactly what old,
architecture-specific code does when you point it at an architecture it has never
met: it fell over.
What follows is the story of the small, stubborn fixes it took to make a decades-old x86 emulator compile and link on RISC-V — as best I can tell, for the first time. None of them are large. Several are almost comically small. Together they are the difference between "does not build" and "builds."
Fix 1: teach the assembler how to write a comment
The very first wall was the smallest imaginable. The emulator's header,
x86emu.h, needs to know which character starts a comment in the
target's assembler — because parts of it emit inline assembly. It had an
entry for x86, for ARM, for MIPS, for PowerPC, for SH… and nothing for
RISC-V. So the build stopped before it really started.
The fix is one line — really, one word:
#if defined(CONFIG_ARM)
#define GAS_LINE_COMMENT "@"
-#elif defined(CONFIG_MIPS) || defined(CONFIG_PPC) || defined(CONFIG_X86)
+#elif defined(CONFIG_MIPS) || defined(CONFIG_PPC) || defined(CONFIG_X86) || defined(CONFIG_RISCV)
#define GAS_LINE_COMMENT "#"
RISC-V uses # for assembler comments, same as x86, MIPS and
PowerPC. That was the entire fix. And yet I find it a perfect little emblem of the
whole endeavor: the first thing standing between a RISC-V CPU and an x86 BIOS was
that the emulator did not know how RISC-V writes down a comment. You have to earn
every inch.
Fix 2: MTRRs are an x86 thing
Next, a neighbouring video driver (vesa.c) refused to compile. It
used MTRRs — Memory Type Range Registers — which are a specific x86
mechanism for controlling how regions of memory are cached. There is no such thing
on RISC-V. The code simply assumed everyone had them.
The fix was to wrap the MTRR use in preprocessor guards so it is compiled only on x86, and skipped everywhere else. Not glamorous, but it is the recurring theme of this entire post: code written when x86 was the only world that needed it.
Fix 3: the PCI config calls had the wrong first argument
Then the emulator's "back-end system" file, besys.c — the
part that lets the emulated BIOS talk to real PCI hardware — produced a wall
of compiler warnings on RISC-V. A batch of them came from calling U-Boot's
pci_read_config_* / pci_write_config_* helpers with the
wrong type of first argument. On x86 the old signatures had quietly matched; on a
modern RISC-V build they did not. Fixing the argument types cleared a whole class
of warnings and, more importantly, made those calls actually correct.
Fix 4: x86 has I/O ports. RISC-V does not.
This one is my favourite, because it is not really a compile fix at all — it is the moment the fundamental mismatch between the two worlds first showed its face.
The x86 architecture has two separate address spaces: normal memory, and a
distinct I/O port space that you reach with dedicated
IN and OUT instructions. A great deal of old PC hardware
— including video cards — is poked through those I/O ports. The
emulator models this with a set of macros, PM_inpb /
PM_outpb and friends, which on a real x86 turn into actual
in/out instructions.
RISC-V has no I/O port space at all: everything is
memory-mapped. There is no IN instruction to map those macros onto.
And the existing code, on a non-x86 target, did something quietly dangerous: those
port accesses would degrade into reads and writes of more or less random
memory locations — while also generating a heap of rightful compiler
warnings.
I was not ready to properly re-plumb every I/O port access yet, but I absolutely could not leave macros that scribble on random memory. So I replaced them, on non-x86 builds, with small functions that do the safe and honest thing: warn, and move on.
static inline u8 PM_inpb(u16 port)
{
printf("x86 port 0x%x read attempt, returning 0\n", port);
return 0;
}
static inline void PM_outpb(u16 port, u8 val)
{
printf("x86 port 0x%x write attempt, ignoring\n", port);
}
Now, when the emulated BIOS reached for an x86 I/O port that does not exist on my machine, U-Boot would tell me so, plainly, instead of corrupting memory. This turned out to be enormously useful later — those warnings became a running commentary on everything the VBIOS assumed about the machine underneath it. And in hindsight, this little fix is the first faint crack in the whole approach: an x86 BIOS expects an x86-shaped world, and my machine is not one.
Fix 5, 6, 7: includes, malloc, and a BAR that would not fit
Three more, briefly, because bring-up is always a long tail of small things:
- Missing includes in
debug.c. With the emulator's debug tracing turned on, two header files were missing and the compiler complained. Added them. (I would spend a lot of time in that debug tracing later — more on that in the next post.) - The SPL malloc pool was too small. The first time I actually
asked U-Boot to run the card's BIOS — via its
dm_pci_run_vga_bios()path — it stopped dead, reporting that its early-stage memory pool was exhausted. The emulator needs room to work. So I bumped the default pool size when both the emulator and RISC-V are configured. - The card's BAR0 did not fit in the memory window. This was a
proper head-scratcher. U-Boot's PCI setup was handing the card a small memory
region — "region 2," 16 MB, starting at
0x7000000. But the GK208's BAR0 (its main register window) is itself 16 MB and simply would not fit. The fix was to tell U-Boot to use the much larger "region 1" at0x60090000instead — which, reassuringly, is the very same address the Linux kernel uses for this card:U-Boot should use "region 1" instead (the one which starts at 0x60090000), because it has much bigger size, and easily accommodates BAR0 of the video card. Linux kernel also uses 0x60090000 as bus_start/phys_start.
It compiles — and it went upstream
Somewhere in this sequence, a decades-old x86 emulator quietly compiled and linked for RISC-V, I believe for the first time. That was a genuinely happy evening.
And these were not just private hacks. Several of them I cleaned up and sent to
the U-Boot mailing list, and two of them — the malloc-pool bump and the PCI
region fix — came back with a Reviewed-by: from a U-Boot
maintainer. Small patches, but real ones, now part of the conversation about
running video BIOSes on RISC-V. That mattered to me more than the size of the
diffs suggests.
But compiling is not running
Here is the sober note to end on. Making the emulator build was the easy half. An emulator that compiles is not an emulator that has successfully brought a graphics card to life. All I had, at this point, was a program that was willing to try — and a card whose x86 firmware had never in its existence been executed by anything other than a real Intel-compatible CPU.
The moment I actually pointed the emulator at the GK208's BIOS and let it start interpreting, a much stranger and harder adventure began — one where I would end up so deep inside the emulator that I had to fix its disassembler just to be able to read what the card's own firmware was doing.
That is the next post.
Next: Teaching a RISC-V Chip to Dream in x86.