Chapter 29 Exercises: Device I/O
Section A: Port-Mapped I/O
1. Port I/O Warmup
Write NASM functions for each of the following port I/O operations:
- inb(port) → al: read byte from 16-bit port address in DI
- outb(port, val): write byte val (SIL) to port (DI)
- inw(port) → ax: read word
- outw(port, val): write word
- inl(port) → eax: read dword
- outl(port, val): write dword
Note: DI holds the port address; since port addresses are 16-bit, move DI to DX before the IN/OUT instruction.
2. CMOS Clock Read The CMOS RTC stores system time at I/O ports 0x70 (address) and 0x71 (data). Read the current time:
; Read CMOS register N:
; out 0x70, N ; select register
; in al, 0x71 ; read value (BCD format)
; Registers: 0x00=seconds, 0x02=minutes, 0x04=hours,
; 0x07=day, 0x08=month, 0x09=year
; BCD to binary: al = (al >> 4) * 10 + (al & 0x0F)
Read and print the current time in HH:MM:SS format to the serial port or VGA.
3. I/O Privilege Level Test
Write a user-mode C program that attempts IN AL, 0x40 (read PIT counter). Observe the SIGSEGV. Then:
1. Look up the iopl(2) system call
2. Add iopl(3) before the IN instruction (requires root)
3. Verify the IN now succeeds
Explain what the IOPB (I/O Permission Bitmap) in the TSS controls and how iopl() modifies it.
4. REP INSB / REP OUTSB
The REP INSB instruction reads multiple bytes from an I/O port into memory. This is used for ATA/IDE disk reads (port 0x1F0). Implement:
; read_sectors_pio: read N 512-byte sectors from ATA drive
; RDI = buffer address
; RSI = sector count
; rdx = LBA starting sector
; (Simplified ATA PIO mode — for exercise purposes)
; ATA command sequence:
; 1. Wait for drive ready (port 0x1F7, bit 6 = RDY, bit 7 = BSY must be 0)
; 2. Write sector count to 0x1F2
; 3. Write LBA bits 0-7, 8-15, 16-23, 24-27 to 0x1F3, 0x1F4, 0x1F5, 0x1F6
; 4. Write READ command (0x20) to 0x1F7
; 5. Wait for DRQ (0x1F7 bit 3 = 1)
; 6. REP INSW from port 0x1F0, count=256 (256 words = 512 bytes)
Section B: PIT Programming
5. PIT to Various Frequencies
Using the formula divisor = 1,193,182 / Hz, configure the PIT for each of these frequencies:
- 18.2 Hz (the BIOS default — divisor = 65535)
- 100 Hz (MinOS standard — divisor = 11931)
- 1000 Hz (Linux default — divisor = 1193)
Write the pit_set_frequency(hz: rdi) function that calculates the divisor using integer division and programs the PIT.
6. Measuring Time with the PIT
Using your 100Hz PIT timer, implement a sleep_ms(ms: rdi) function:
; sleep_ms: busy-wait for approximately N milliseconds
; RDI = milliseconds to wait
; Uses the global tick_count variable from the timer IRQ handler
; 100Hz = 10ms per tick; sleep N/10 ticks (minimum 1)
Test by sleeping 1000ms (1 second) and measuring wall clock time.
7. PIT Mode 0 vs Mode 2 vs Mode 3 Program the PIT in each of these modes and observe the IRQ0 behavior: - Mode 0 (interrupt on terminal count): fires once when counter reaches 0 - Mode 2 (rate generator): fires periodically at freq/(reload) - Mode 3 (square wave): fires at 2× the rate of mode 2
For each mode, count the IRQ0 firings in 1 second (using a spin loop) and compare to the expected count. Explain any differences.
8. PIT Channel 2 / PC Speaker
Channel 2 of the PIT is connected to the PC speaker. Generate a 440 Hz (A4 note) tone:
- Configure Channel 2 with divisor 1193182 / 440 = 2712
- Enable speaker via port 0x61 (bits 0 and 1)
; Enable PC speaker and set tone
pit_speaker_tone:
; Configure channel 2: mode 3 (square wave), frequency N Hz
mov al, 0xB6 ; 10_11_011_0: ch2, lo+hi, mode3, binary
out 0x43, al
; Set divisor
mov ax, 2712
out 0x42, al
shr ax, 8
out 0x42, al
; Enable speaker: port 0x61 bits 0+1
in al, 0x61
or al, 0x03
out 0x61, al
ret
Play the tone for 500ms in QEMU. Disable it:
in al, 0x61
and al, ~0x03
out 0x61, al
Section C: UART Serial Port
9. Complete Serial Driver
Implement the full serial driver from the chapter and add:
- serial_getchar() → al: poll LSR bit 0 (data ready), read if available, return 0 if not
- serial_readline(buf: rdi, maxlen: rsi): read until \r, \n, or maxlen bytes; null-terminate
Test by sending a command over serial from a terminal (use screen or minicom) and echoing the input back.
10. Serial Debug Printf
Implement serial_printf_hex(msg, val) that prints a message string followed by a 64-bit value in hex:
[DEBUG] CR3 = 0xFFFF800001000000
Use this throughout your MinOS kernel for initialization messages. Every major initialization step should log its status over serial before enabling interrupts.
11. Serial Interrupt Mode
Configure the 16550 UART for interrupt-driven receive (rather than polling):
- Enable Receive Data Available interrupt: write 0x01 to Interrupt Enable Register (port +1)
- Install an IRQ4 handler (COM1 = IRQ4 = vector 36 after PIC remapping)
- In the IRQ4 handler: read byte from port 0x3F8, store in ring buffer, send EOI
- Implement serial_getchar() that reads from the ring buffer (returns 0 if empty)
Compare the CPU usage of polling mode vs. interrupt mode when waiting for serial input.
Section D: APIC
12. APIC Detection and Enable Check if the LAPIC is present and enable it:
; Check CPUID leaf 1, bit 9 of EDX: APIC present
mov eax, 1
cpuid
test edx, (1 << 9)
jz .no_apic
; Read APIC base MSR (IA32_APIC_BASE = 0x1B)
mov ecx, 0x1B
rdmsr
; EDX:EAX = APIC base MSR
; Bits 11:8 = flags (bit 8 = BSP, bit 11 = global enable)
; Bits 51:12 = APIC base physical address (typically 0xFEE00000)
; Bit 11: enable bit; set to 1 to enable APIC
or eax, (1 << 11)
wrmsr
Print the LAPIC base address to serial.
13. APIC vs PIC Implement the same 100Hz timer using the LAPIC timer instead of the PIT: - Calibrate the LAPIC timer using the PIT: count LAPIC ticks in one PIT tick period - From the LAPIC tick rate, calculate the reload value for 100Hz - Configure LAPIC LVT Timer register for periodic mode
The LAPIC timer has much higher resolution (and can be calibrated to nanoseconds), which is why Linux and modern OSes prefer it over the PIT.
Section E: PCI Enumeration
14. PCI Device Scan Implement a full PCI device scan that: 1. Iterates bus 0–255, device 0–31, function 0–7 2. For each function: reads vendor ID (offset 0, low 16 bits) 3. Skips if vendor ID = 0xFFFF (no device) 4. Reads: device ID, class code, subclass, revision 5. Prints: "Bus=%d Dev=%d Func=%d: VendorID=%04X DeviceID=%04X Class=%02X:%02X"
Verify in QEMU — you should see entries for: the PCI host controller (class 0x06:0x00), the IDE controller (class 0x01:0x01), the VGA adapter (class 0x03:0x00), and the ACPI/Power management device.
15. Reading PCI BAR (Base Address Register)
A PCI device's BARs (offset 0x10–0x24 in config space) tell you where the device's MMIO regions are mapped. For a network card:
1. Find a network device (class 0x02:0x00) in QEMU by adding -nic e1000
2. Read BAR0 (offset 0x10)
3. Determine if it is MMIO (bit 0 = 0) or PIO (bit 0 = 1)
4. For MMIO: mask off lower 4 bits to get the base address
5. Read the E1000's device status register (BAR0 + 0x08) to confirm
This demonstrates how device drivers discover hardware resources — they read the BARs, map the MMIO region into kernel address space, then access registers normally.