VGA Device Driver
Why This Matters
Every character you see on a terminal got there because an OS kernel wrote bytes to hardware at a specific memory address. Device drivers are the thin layer that translates high-level OS actions ("print a character") into the exact register writes and memory stores the hardware expects. Understanding how xv6 drives CGA and VGA gives you a concrete, complete example of the interrupt → handler → driver pipeline — and the ioctl() mechanism that lets user programs control hardware behavior without bypassing the kernel.
The Interrupt-to-Driver Pipeline (Recap)
When a hardware event occurs (a key press, a serial byte arriving), the CPU saves state, looks up the interrupt vector, and jumps to a handler. In xv6, all interrupt entry points converge at alltraps in trapasm.S, which builds a trap frame on the stack and calls trap() in trap.c. Inside trap(), a switch statement dispatches to the right device handler:
case T_IRQ0 + IRQ_KBD:
kbdintr();
lapiceoi();
break;
The keyboard and COM1 handlers both eventually call consoleintr(), which echoes characters to the screen by calling consputc(). That is where the display driver starts.
CGA: The Display Hardware in xv6
What CGA Is
The Color Graphics Adapter is an early IBM PC video standard that xv6 emulates. It supports an 80×25 character text mode (80 columns, 25 rows = 2,000 character cells).
Memory-Mapped Display
CGA is controlled by writing directly to physical memory starting at 0xB8000. The kernel maps this into its virtual address space:
// console.c
static ushort *crt = (ushort*)P2V(0xb8000);
No I/O port instructions are needed — a write to this memory region goes straight to the display.
Character Cell Format
Each cell is 2 bytes (a ushort):
Bit: 15 8 7 0
+----------+----------+
| attr byte | char byte|
+----------+----------+
| Field | Bits | Meaning |
|---|---|---|
| char byte | 7–0 | ASCII code of the character |
| attr byte | 10–8 | foreground color (low 4 bits used) |
| attr byte | 15–11 | background color (high 3 bits) |
xv6 defaults to gray text on black:
crt[pos++] = (c & 0xff) | 0x0700; // 0x07 = gray on black
The 0x0700 attribute places 0x07 in the high byte: foreground = light gray (7), background = black (0).
cgaputc() in Context
consputc() calls cgaputc() to place a character at the current cursor position, advance the cursor, and scroll the screen when the last row is reached. Because this happens at interrupt time (inside consoleintr()), no sleeping or blocking is allowed.
ioctl(): Device Control Beyond Read/Write
The Problem
read() and write() move data. But hardware often needs control commands — set baud rate, change color, switch video mode. That is what ioctl() is for.
The "Everything Is a File" Model
In Unix (and xv6), devices appear as files. A user program opens /dev/console, gets a file descriptor, and can call ioctl() on it:
ioctl(1, 0, 3); // fd=1 (stdout), param=0, value=3
ioctl(2, 0, (color%0xe)+1); // pass a color value to the console
The Call Chain
sys_ioctl() [syscall dispatch, kernel/sysfile.c]
└─ fileioctl() [looks up device from file descriptor, file.c]
└─ devsw[major].ioctl(f, param, value)
└─ consoleioctl() [or displayioctl() for VGA]
The devsw Table
devsw is an array of function pointers indexed by major device number. Each entry holds the three operations a device can support:
// file.h
struct devsw {
int (*read) (struct file*, char*, int);
int (*write)(struct file*, char*, int);
int (*ioctl)(struct file*, int, int);
};
// file.c
struct devsw devsw[NDEV];
At boot, consoleinit() registers the console driver:
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].ioctl = consoleioctl;
fileioctl() dispatches to the correct slot using the file's major number (f->ip->major), so the kernel never hard-codes which driver handles which device.
VGA: A More Capable Display Standard
Overview
Video Graphics Array was introduced with IBM's PS/2 in 1987. It is backward compatible with CGA and adds higher-resolution and color modes. Its framebuffer starts at physical address 0xA0000 (different from CGA's 0xB8000).
Key Modes
| Mode | Name | Description |
|---|---|---|
| Mode 3 | Text mode | Compatible with CGA; 80×25 text |
| Mode 13h | Linear 256-color | 320×200 pixels, 1 byte per pixel |
xv6 exposes these via helper functions: vgaMode3() and vgaMode13().
Mode 13h Pixel Layout
In Mode 13h each pixel is one byte — its value is an index into a palette of 256 colors. To change which color an index maps to, you call vgaSetPalette(). This is how a "fade" effect works: gradually shift the palette entries toward black without touching framebuffer data.
HW4: Building Your Own Device Drivers
Part 1 — Console Color via ioctl()
prettyprint.c uses ioctl() to tell the console to print in a chosen color. Your task is to implement consoleioctl() so that when a color parameter arrives, it stores it and subsequent calls to cgaputc() use it instead of the hardcoded 0x0700.
The full path user code takes:
prettyprint → write(fd, ...) → ioctl(fd, 0, color)
↓
sys_ioctl → fileioctl → consoleioctl (your code)
Part 2 — VGA Display Driver
vga.c is provided (the low-level VGA register programming). You implement display.c, modeled on console.c, and register it in devsw. The ioctl() in this driver switches between Mode 3 and Mode 13h. For the optional fade effect, manipulate the palette via vgaSetPalette() using the third ioctl() parameter.
Key Takeaways
- Memory-mapped I/O lets the CPU treat device registers or framebuffers as ordinary memory; CGA lives at
0xB8000, VGA at0xA0000. - Each CGA character cell is 2 bytes: the low byte is the ASCII code, the high byte encodes foreground and background color.
ioctl()is the kernel's generic device-control mechanism: it follows the same file-descriptor dispatch asread/write, but carries an opaque parameter/value pair to the driver.devsw[]decouples the VFS layer from individual drivers: major device numbers index into the table, so adding a new device only requires registering its function pointers.- VGA extends CGA with a 320×200 256-color linear framebuffer (Mode 13h) where each pixel is one palette index byte.
- The full pipeline is: interrupt → trap handler → device driver → hardware memory/registers — this is the same pattern for keyboards, disks, network cards, and displays.