Skip to main content
Evert's website

Running MS-DOS on the Intel Galileo

The Intel Galileo was a single-board computer sold by Intel from 2013 to 2017. While it was intended to be used as an Arduino-like platform (it was Arduino certified, i.e. it could be programmed from the Arduino IDE and was compatible with Arduino shields), its 400 MHz CPU and 256 MB of RAM put it much closer to the likes of the Raspberry Pi than to the 'classic' Arduino options.

In my opinion, the CPU is definitely the most interesting part of the package: this is not an ARM SoC like most SBCs, but an Intel SoC called the Quark. Essentially, the Quark is a reimplementation of the original Pentium on a modern process node and with modern features (sleep states, Secure Boot) and interfaces (eMMC, USB, PCIe, SPI, I2C etc.). In other words, it implements the i586 instruction set1. This led me to a question: can the Galileo run MS-DOS natively?

Galileo hardware and boot process #

Out of the box the Galileo runs a 32-bit UEFI, which boots GRUB, which in turn loads a Yocto Linux build running Intel's Arduino layer. This poses a problem: MS-DOS runs in real mode (i.e. a 16-bit environment) and depends heavily on interrupt routines provided by the BIOS. My idea was to build an EFI application that performs the necessary preparation and then returns the CPU to real mode. From there, a small setup program installs custom versions of the required interrupt routines before finally handing control over to DOS.

Approach #

To keep this project manageable I decided to split it into three phases:

  1. A proof-of-concept in QEMU showing that we can hook interrupts before DOS boots;
  2. A proof-of-concept that demonstrates we can put the Galileo into real mode;
  3. The actual port of DOS to the Galileo.

QEMU #

For the QEMU proof-of-concept I decided on the following:

After a few prompts I had the basic setup implemented. The most notable deviation from the above plan was the need for a second stage for the loader, as the hooking and chainloading required more than the 512 bytes provided by the boot sector.

Demo of the keyboard, teletype output and conventional memory size hooks. Note that I later tested some more hooks (e.g. disk reads and timers) within QEMU, so the loader reports more interrupts than strictly necessary for the mentioned functionality.

Real mode #

The next step was seeing whether the Galileo could actually run in real mode. This is not a given2, as the Galileo does not boot in legacy BIOS mode, but instead runs a 32-bit UEFI firmware (based on EDK II). The minimal demonstration of this is the following:

By putting the application on a FAT32-formatted SD card as EFI/BOOT/BOOTIA32.EFI the Galileo will automatically load it:

Outputting to the UART from 16-bit real mode

Port #

For actually running DOS on the Galileo we don't need to modify the floppy image. Instead, any preparation (jumping to real mode, installing interrupt handlers, setting up the UART) can be done from the EFI application, or from the trampoline or setup routine loaded by it, before we hand control over to DOS.

On the Galileo, we need to provide a lot more interrupts than in QEMU. In QEMU, unimplemented functions could fall back on the QEMU-provided BIOS, but on the real hardware no such fallback is available.

Unlike in QEMU, there is also no emulated floppy drive available. The EFI application instead loads the floppy image into RAM. The INT 13h read-sector routine serves reads from there.

The boot flow thus becomes:

UEFI application -> trampoline -> setup code -> MS-DOS

The UEFI application reserves the memory regions needed for the DOS boot sector, trampoline, setup code, interrupt handlers and state information. It loads the floppy image (msdos.img in the root of the SD card) and the trampoline code (embedded in the application) from the root of the SD card. These are not loaded to hardcoded addresses, but to memory regions that the UEFI provides at runtime. By reserving the regions mentioned above, we prevent the UEFI from storing the floppy image or trampoline there.

The application then loads the DOS boot sector, the setup code and the interrupt handlers to low memory, and writes a handoff block containing the address and size of the floppy image and the base address of the UART.

Flat address Purpose
0x00006000 Setup code
0x00007C00 DOS boot sector
0x00008000 Interrupt handlers (before being moved)
0x0009C000 Interrupt handlers (after being moved)
0x0009CF00 Handoff block
somewhere above 0x01000000 Trampoline, floppy image

The UEFI application then hands control over to the trampoline. The trampoline puts the system into real mode (as described in the previous section) and hands control over to the setup code. The setup code moves the interrupt handlers to the reserved memory area, installs them into the interrupt vector table, sets up the PIC/PIT and BIOS Data Area, and jumps to the DOS boot sector at 0000:7C00.

I started by hooking the interrupts normally provided by the BIOS (08h-19h) and the exception handlers (00h-07h) with a small handler that prints the interrupt or exception details to the UART, so we immediately notice if an exception or unimplemented interrupt request occurs.

From there, it was a matter of booting the EFI application on the Galileo, seeing on which interrupt or exception DOS hung, implementing or fixing that, rebuilding the application and booting it again, until DOS succesfully booted to the shell. Note that the interrupt handlers implemented in this way only provide the minimal functionality to allow DOS to continue booting!

The following functionality seems to be required at minimum to boot MS-DOS 6.22 to the shell:

Interrupt Function Description Hook
00h-07h CPU exceptions Print diagnostic state (CS:IP, nearby bytes and stack words) to the UART, then halt.
08h Timer interrupt Increment the BIOS tick counter and acknowledge the PIC.
0Fh 09h PIC-generated IRQ7 Acknowledge the PIC.
10h 0Eh Teletype output Output the character to the UART.
11h Equipment list Return a hardcoded configuration: 1 floppy drive, 80x25 display, no serial ports.
12h Get conventional memory size Return 620 KiB instead of the maximum 640 KiB so we have some space to store hooks and state.
13h 00h Reset disk system Return success for the first floppy drive only.
13h 01h Get drive status Return success for the first floppy drive only.
13h 02h Read sectors Return the requested sectors from the RAM disk.
13h 03h Write sectors Return the 'write protected' error code.
13h 08h Get drive parameters Return geometry derived from the BIOS parameter block on the floppy image.
13h 15h Get drive type Return diskette drive for the first drive, 'not installed' for others.
13h 16h Detect media change Return 'no media change' for the first drive, 'invalid drive number' for others.
14h Serial port functions Return an error code as no serial ports are available.
15h C0h Get system parameters Return a hardcoded configuration: AT-class PC, no support for any optional features.
16h 00h/10h Read (expanded) keyboard Return a single character from the keyboard buffer. We can populate this from the UART input.
16h 01h/11h Get (expanded) keyboard status Return whether a character is waiting to be read (from the UART).
16h 02h Get keyboard flags Return no modifier keys, as these cannot be entered over serial anyway.
17h 01h Initialize printer Return status 'I/O error'.
1Ah 00h Get BIOS ticks Return the BDA tick count maintained by INT 08h.
All other IVT entries Unknown interrupts Log the vector and AX, then halt instead of jumping through stale firmware vectors.

The end result: MS-DOS 6.22 running on the Intel Galileo natively:

MS-DOS 6.22 running on the Galileo

The source code for all three phases is available on my Forgejo instance.

Limitations and possible next steps #

Of course, while it is cool to have DOS running natively, the current implementation is very limited:

As the Galileo has a mini PCIe slot on the back, it would be interesting to see if a simple graphics card would work using a PCIe riser. If so, it might be possible to port coreboot and SeaBIOS, opening the door to a full DOS port and maybe even allowing the Galileo to run early Windows versions.

Remarks #

1: To be exact, it is derived from a Pentium-class core, so it supports x87 instructions, but not MMX or SSE instructions. 2: I later found out that this is in fact a given: just like regular x86 CPUs the Quark boots in 16-bit real mode, as noted by the Intel® Quark SoC X1000 Core Developer’s Manual. It is actually the UEFI implementation that transitions it to 32-bit protected mode.

Sources #