Back to Technology

Phase 4: Display, Input & Output

February 6, 2026 Wasil Zafar 22 min read

Implement VGA text mode for screen output, handle keyboard input via PS/2, and build the printf/putc functions that let your kernel communicate with users.

Table of Contents

  1. Introduction
  2. VGA Text Mode
  3. Screen Functions
  4. Keyboard Input
  5. Input Functions
  6. What You Can Build
  7. Next Steps

Introduction: Making Your Kernel Talk

Phase 4 Goals: By the end of this phase, your kernel will have a working text display with colors, a cursor, scrolling, and keyboard input. You'll have printf() for output and getchar() for input—the basic tools for user interaction.

Your kernel can now execute C code in protected mode - but it's silent. A kernel that can't communicate with users isn't very useful! In this phase, we'll build the fundamental I/O layer that every operating system needs: text display and keyboard input.

A Brief History of Text Mode

VGA (Video Graphics Array) was introduced by IBM in 1987 and became the standard PC display interface. Its text mode (Mode 3) provides:

  • 80 columns × 25 rows = 2000 characters
  • 16 colors for foreground and background
  • 256 characters (extended ASCII/Code Page 437)
  • Memory-mapped I/O at 0xB8000 for instant updates

Even today, most BIOS/UEFI setups default to VGA-compatible text mode before loading a graphical OS!

Key Insight: VGA text mode is elegant in its simplicity: each character on screen is just two bytes in memory—one for the character, one for the color. Direct memory access means instant updates without any driver complexity.

Why I/O Matters

Without input and output, your kernel is a black box. Proper I/O allows you to:

Debugging
  • Print variable values and addresses
  • Trace function calls and execution flow
  • Display error messages and stack traces
  • Inspect memory contents visually
User Interaction
  • Accept commands and input
  • Display status and progress
  • Build a command-line interface
  • Enable interactive testing

I/O Architecture Overview

I/O LAYER ARCHITECTURE
═══════════════════════════════════════════════════════════════

   User Level (printf, scanf)
          ↓
   Kernel I/O Library (kprintf, kgetchar)
          ↓
   Device Drivers
   ├── VGA Driver (vga.c) ─────→ Video Memory (0xB8000)
   │                              │
   │                              ↓
   │                         ┌─────────────┐
   │                         │  MONITOR    │
   │                         └─────────────┘
   │
   └── Keyboard Driver (kbd.c) ─→ PS/2 Controller (ports 0x60, 0x64)
                                  │
                                  ↓
                             ┌─────────────┐
                             │  KEYBOARD   │
                             └─────────────┘

VGA Text Mode Deep Dive

Video Memory (0xB8000)

VGA text mode uses memory-mapped I/O. The video buffer lives at physical address 0xB8000, and any writes to this memory instantly appear on screen!

VGA TEXT MODE MEMORY LAYOUT
═══════════════════════════════════════════════════════════════

Address Range: 0xB8000 - 0xB8F9F (80×25×2 = 4000 bytes)

Screen Position → Memory Address:
┌────────────────────────────────────────────────────────────┐
│ (0,0)      (1,0)      (2,0)  ...              (79,0)      │
│ 0xB8000    0xB8002    0xB8004                 0xB809E     │
├────────────────────────────────────────────────────────────┤
│ (0,1)      (1,1)                              (79,1)      │
│ 0xB80A0    0xB80A2                            0xB813E     │
├────────────────────────────────────────────────────────────┤
│            ... (rows 2-23) ...                             │
├────────────────────────────────────────────────────────────┤
│ (0,24)     (1,24)                             (79,24)     │
│ 0xB8F00    0xB8F02                            0xB8F9E     │
└────────────────────────────────────────────────────────────┘

Formula: address = 0xB8000 + (y × 80 + x) × 2

Character Format

Each screen position takes 2 bytes (a word):

CHARACTER CELL FORMAT (2 bytes)
═══════════════════════════════════════════════════════════════

     Byte 1 (Even address)       Byte 0 (Odd address)
   ┌───────────────────────┐   ┌───────────────────────┐
   │    Character Code     │   │  Attribute (Color)    │
   │     (ASCII/CP437)     │   │   FG + BG + Blink     │
   └───────────────────────┘   └───────────────────────┘

   Example: 'A' in white on blue
   Character: 0x41 (ASCII 'A')
   Attribute: 0x1F (white foreground, blue background)
   
   Full word (little-endian): 0x1F41
/* vga.h - VGA text mode definitions */
#ifndef VGA_H
#define VGA_H

#include <stdint.h>

/* VGA constants */
#define VGA_ADDRESS     0xB8000
#define VGA_WIDTH       80
#define VGA_HEIGHT      25
#define VGA_SIZE        (VGA_WIDTH * VGA_HEIGHT)

/* VGA character structure */
typedef struct {
    uint8_t character;      /* ASCII/CP437 character code */
    uint8_t attribute;      /* Color attribute byte */
} __attribute__((packed)) vga_char_t;

/* Pointer to VGA buffer */
extern volatile vga_char_t* const vga_buffer;

/* Make a VGA entry from character and color */
static inline uint16_t vga_entry(char c, uint8_t color) {
    return (uint16_t)c | ((uint16_t)color << 8);
}

#endif

Colors & Attributes

The attribute byte packs foreground, background, and blink into 8 bits:

ATTRIBUTE BYTE FORMAT
═══════════════════════════════════════════════════════════════

Bit:  7     6  5  4     3     2  1  0
    ┌─────┬────────────┬─────┬────────────┐
    │BLINK│  BG COLOR  │ INT │  FG COLOR  │
    └─────┴────────────┴─────┴────────────┘

BLINK: 1 = Blinking text (or bright background if disabled)
BG:    Background color (0-7, 3 bits)
INT:   Intensity/bright (foreground becomes bright)
FG:    Foreground color (0-7, 3 bits)

STANDARD VGA COLORS (16 total)
═══════════════════════════════════════════════════════════════
 0 = Black           8 = Dark Gray (bright black)
 1 = Blue            9 = Light Blue
 2 = Green          10 = Light Green
 3 = Cyan           11 = Light Cyan
 4 = Red            12 = Light Red
 5 = Magenta        13 = Light Magenta
 6 = Brown          14 = Yellow (bright brown)
 7 = Light Gray     15 = White (bright light gray)
/* vga_colors.h - VGA color definitions */

/* VGA color enumeration */
typedef enum {
    VGA_BLACK         = 0,
    VGA_BLUE          = 1,
    VGA_GREEN         = 2,
    VGA_CYAN          = 3,
    VGA_RED           = 4,
    VGA_MAGENTA       = 5,
    VGA_BROWN         = 6,
    VGA_LIGHT_GREY    = 7,
    VGA_DARK_GREY     = 8,
    VGA_LIGHT_BLUE    = 9,
    VGA_LIGHT_GREEN   = 10,
    VGA_LIGHT_CYAN    = 11,
    VGA_LIGHT_RED     = 12,
    VGA_LIGHT_MAGENTA = 13,
    VGA_YELLOW        = 14,
    VGA_WHITE         = 15
} vga_color_t;

/* Create color attribute from foreground and background */
static inline uint8_t vga_make_color(vga_color_t fg, vga_color_t bg) {
    return (uint8_t)(fg | (bg << 4));
}

/* Common color combinations */
#define COLOR_DEFAULT    vga_make_color(VGA_LIGHT_GREY, VGA_BLACK)
#define COLOR_ERROR      vga_make_color(VGA_LIGHT_RED, VGA_BLACK)
#define COLOR_SUCCESS    vga_make_color(VGA_LIGHT_GREEN, VGA_BLACK)
#define COLOR_WARNING    vga_make_color(VGA_YELLOW, VGA_BLACK)
#define COLOR_INFO       vga_make_color(VGA_LIGHT_CYAN, VGA_BLACK)

Hardware Cursor Control

The blinking hardware cursor is controlled through VGA I/O ports (not memory):

/* vga_cursor.c - Hardware cursor control */

#include "io.h"  /* inb(), outb() functions */

/* VGA CRTC registers */
#define VGA_CRTC_ADDR   0x3D4   /* Index register */
#define VGA_CRTC_DATA   0x3D5   /* Data register */

/* Cursor location registers */
#define VGA_CURSOR_HIGH 0x0E    /* Cursor location high byte */
#define VGA_CURSOR_LOW  0x0F    /* Cursor location low byte */

/* Cursor shape registers */
#define VGA_CURSOR_START 0x0A   /* Cursor start scanline */
#define VGA_CURSOR_END   0x0B   /* Cursor end scanline */

/* Move hardware cursor to (x, y) */
void vga_set_cursor(int x, int y) {
    uint16_t pos = y * VGA_WIDTH + x;
    
    /* Set cursor position low byte */
    outb(VGA_CRTC_ADDR, VGA_CURSOR_LOW);
    outb(VGA_CRTC_DATA, (uint8_t)(pos & 0xFF));
    
    /* Set cursor position high byte */
    outb(VGA_CRTC_ADDR, VGA_CURSOR_HIGH);
    outb(VGA_CRTC_DATA, (uint8_t)((pos >> 8) & 0xFF));
}

/* Get current cursor position */
uint16_t vga_get_cursor(void) {
    uint16_t pos = 0;
    
    outb(VGA_CRTC_ADDR, VGA_CURSOR_LOW);
    pos |= inb(VGA_CRTC_DATA);
    
    outb(VGA_CRTC_ADDR, VGA_CURSOR_HIGH);
    pos |= ((uint16_t)inb(VGA_CRTC_DATA)) << 8;
    
    return pos;
}

/* Enable cursor with specified shape (start/end scanlines 0-15) */
void vga_enable_cursor(uint8_t start, uint8_t end) {
    outb(VGA_CRTC_ADDR, VGA_CURSOR_START);
    outb(VGA_CRTC_DATA, (inb(VGA_CRTC_DATA) & 0xC0) | start);
    
    outb(VGA_CRTC_ADDR, VGA_CURSOR_END);
    outb(VGA_CRTC_DATA, (inb(VGA_CRTC_DATA) & 0xE0) | end);
}

/* Disable cursor (hide it) */
void vga_disable_cursor(void) {
    outb(VGA_CRTC_ADDR, VGA_CURSOR_START);
    outb(VGA_CRTC_DATA, 0x20);  /* Bit 5 disables cursor */
}
Cursor Shapes: The cursor is drawn between scanlines. Standard text mode has 16 scanlines per character cell. enable_cursor(13, 15) gives an underline cursor; enable_cursor(0, 15) gives a block cursor.

Screen Output Functions

Now we'll build a complete terminal-style output system. We'll create layered functions: putc for single characters, puts for strings, and kprintf for formatted output—the foundation of all kernel debugging!

putc Implementation

The putc function is the lowest-level output primitive. It handles every character including special control characters:

CONTROL CHARACTERS FOR TERMINAL OUTPUT
═══════════════════════════════════════════════════════════════

Character     Code    Effect
─────────────────────────────────────────────────────────────
'\n' (LF)     0x0A    Move to start of next line (newline)
'\r' (CR)     0x0D    Move to start of current line
'\t' (TAB)    0x09    Move to next 8-column boundary
'\b' (BS)     0x08    Move cursor back, erase character
'\f' (FF)     0x0C    Clear screen (form feed)
Printable    0x20-7E  Display character at cursor position
/* terminal.c - Complete terminal output implementation */

#include "vga.h"
#include "io.h"
#include <stdint.h>
#include <stdbool.h>

/* Terminal state */
static int term_col = 0;           /* Current column (0-79) */
static int term_row = 0;           /* Current row (0-24) */
static uint8_t term_color = 0x0F;  /* White on black default */

/* VGA buffer pointer */
static volatile vga_char_t* const VGA_BUFFER = (vga_char_t*)VGA_ADDRESS;

/* Forward declarations */
static void terminal_scroll(void);
static void terminal_newline(void);
static void terminal_update_cursor(void);

/* Set terminal color */
void terminal_set_color(uint8_t color) {
    term_color = color;
}

/* Clear the entire screen */
void terminal_clear(void) {
    for (int i = 0; i < VGA_SIZE; i++) {
        VGA_BUFFER[i].character = ' ';
        VGA_BUFFER[i].attribute = term_color;
    }
    term_col = 0;
    term_row = 0;
    terminal_update_cursor();
}

/* Print a single character */
void putc(char c) {
    switch (c) {
        case '\n':  /* Newline - move to next line, column 0 */
            terminal_newline();
            break;
            
        case '\r':  /* Carriage return - column 0 */
            term_col = 0;
            break;
            
        case '\t':  /* Tab - advance to next 8-column boundary */
            term_col = (term_col + 8) & ~7;
            if (term_col >= VGA_WIDTH) {
                terminal_newline();
            }
            break;
            
        case '\b':  /* Backspace - erase previous character */
            if (term_col > 0) {
                term_col--;
                int idx = term_row * VGA_WIDTH + term_col;
                VGA_BUFFER[idx].character = ' ';
                VGA_BUFFER[idx].attribute = term_color;
            }
            break;
            
        case '\f':  /* Form feed - clear screen */
            terminal_clear();
            break;
            
        default:    /* Printable character */
            if (c >= ' ' && c <= '~') {
                int idx = term_row * VGA_WIDTH + term_col;
                VGA_BUFFER[idx].character = c;
                VGA_BUFFER[idx].attribute = term_color;
                term_col++;
                
                if (term_col >= VGA_WIDTH) {
                    terminal_newline();
                }
            }
            break;
    }
    
    terminal_update_cursor();
}

/* Move to next line, scrolling if necessary */
static void terminal_newline(void) {
    term_col = 0;
    term_row++;
    
    if (term_row >= VGA_HEIGHT) {
        terminal_scroll();
        term_row = VGA_HEIGHT - 1;
    }
}

/* Update hardware cursor to match terminal position */
static void terminal_update_cursor(void) {
    vga_set_cursor(term_col, term_row);
}

puts Implementation

Building on putc, puts prints entire strings at once:

/* Print a null-terminated string */
void puts(const char* str) {
    while (*str) {
        putc(*str++);
    }
}

/* Print a string with newline */
void puts_nl(const char* str) {
    puts(str);
    putc('\n');
}

/* Print a string at specific position with color */
void puts_at(const char* str, int x, int y, uint8_t color) {
    uint8_t old_color = term_color;
    int old_col = term_col;
    int old_row = term_row;
    
    term_col = x;
    term_row = y;
    term_color = color;
    
    puts(str);
    
    term_color = old_color;
    term_col = old_col;
    term_row = old_row;
    terminal_update_cursor();
}

kprintf Implementation

A kernel-mode printf is essential for debugging. Here's a complete implementation supporting common format specifiers:

KPRINTF FORMAT SPECIFIERS
═══════════════════════════════════════════════════════════════

Specifier   Type               Example           Output
─────────────────────────────────────────────────────────────
%c          char               kprintf("%c", 'A')    A
%s          string             kprintf("%s", "hi")   hi
%d          signed int         kprintf("%d", -42)    -42
%u          unsigned int       kprintf("%u", 42)     42
%x          hex (lowercase)    kprintf("%x", 255)    ff
%X          hex (uppercase)    kprintf("%X", 255)    FF
%p          pointer            kprintf("%p", ptr)    0x00102030
%%          literal %          kprintf("%%")         %
/* kprintf - kernel printf implementation */

#include <stdarg.h>

/* Helper: print unsigned integer in given base */
static void print_uint(unsigned int value, int base, bool uppercase) {
    static const char digits_lower[] = "0123456789abcdef";
    static const char digits_upper[] = "0123456789ABCDEF";
    const char* digits = uppercase ? digits_upper : digits_lower;
    
    char buffer[32];
    int pos = 0;
    
    /* Special case for zero */
    if (value == 0) {
        putc('0');
        return;
    }
    
    /* Build digits in reverse */
    while (value > 0) {
        buffer[pos++] = digits[value % base];
        value /= base;
    }
    
    /* Print in correct order */
    while (pos > 0) {
        putc(buffer[--pos]);
    }
}

/* Helper: print signed integer */
static void print_int(int value) {
    if (value < 0) {
        putc('-');
        /* Handle INT_MIN edge case */
        if (value == (-2147483647 - 1)) {
            puts("2147483648");
            return;
        }
        value = -value;
    }
    print_uint((unsigned int)value, 10, false);
}

/* Helper: print pointer */
static void print_ptr(void* ptr) {
    puts("0x");
    unsigned long value = (unsigned long)ptr;
    /* Print all 8 hex digits (32-bit pointer) */
    for (int i = 28; i >= 0; i -= 4) {
        int digit = (value >> i) & 0xF;
        putc("0123456789ABCDEF"[digit]);
    }
}

/* Main kprintf implementation */
int kprintf(const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);
    
    int count = 0;
    
    while (*fmt) {
        if (*fmt != '%') {
            putc(*fmt++);
            count++;
            continue;
        }
        
        fmt++;  /* Skip '%' */
        
        switch (*fmt) {
            case 'c':
                putc((char)va_arg(args, int));
                count++;
                break;
                
            case 's': {
                const char* s = va_arg(args, const char*);
                while (*s) {
                    putc(*s++);
                    count++;
                }
                break;
            }
            
            case 'd':
            case 'i':
                print_int(va_arg(args, int));
                count++;
                break;
                
            case 'u':
                print_uint(va_arg(args, unsigned int), 10, false);
                count++;
                break;
                
            case 'x':
                print_uint(va_arg(args, unsigned int), 16, false);
                count++;
                break;
                
            case 'X':
                print_uint(va_arg(args, unsigned int), 16, true);
                count++;
                break;
                
            case 'p':
                print_ptr(va_arg(args, void*));
                count++;
                break;
                
            case '%':
                putc('%');
                count++;
                break;
                
            default:
                /* Unknown specifier - print as-is */
                putc('%');
                putc(*fmt);
                count += 2;
                break;
        }
        
        fmt++;
    }
    
    va_end(args);
    return count;
}
Understanding <stdarg.h>: The macros va_start, va_arg, and va_end work directly with the CPU stack to access variadic arguments. GCC provides these as builtins, so they work without any standard library!

Screen Scrolling

When text reaches the bottom of the screen, we need to scroll up. This involves copying memory and clearing the last line:

/* Scroll the screen up by one line */
static void terminal_scroll(void) {
    /* Move lines 1-24 up to positions 0-23 */
    for (int y = 0; y < VGA_HEIGHT - 1; y++) {
        for (int x = 0; x < VGA_WIDTH; x++) {
            int dst = y * VGA_WIDTH + x;
            int src = (y + 1) * VGA_WIDTH + x;
            VGA_BUFFER[dst] = VGA_BUFFER[src];
        }
    }
    
    /* Clear the last line */
    int last_line = (VGA_HEIGHT - 1) * VGA_WIDTH;
    for (int x = 0; x < VGA_WIDTH; x++) {
        VGA_BUFFER[last_line + x].character = ' ';
        VGA_BUFFER[last_line + x].attribute = term_color;
    }
}

/* Optimized scroll using memcpy (requires memory functions) */
void terminal_scroll_fast(void) {
    /* Move all rows except first up by one row worth of bytes */
    size_t line_bytes = VGA_WIDTH * sizeof(vga_char_t);
    size_t copy_bytes = (VGA_HEIGHT - 1) * line_bytes;
    
    /* Copy memory up */
    memcpy((void*)VGA_BUFFER, 
           (void*)(VGA_BUFFER + VGA_WIDTH), 
           copy_bytes);
    
    /* Clear last line */
    vga_char_t blank = { .character = ' ', .attribute = term_color };
    for (int x = 0; x < VGA_WIDTH; x++) {
        VGA_BUFFER[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = blank;
    }
}

Complete Terminal Demo

Put it all together in your kernel entry point:

/* kernel.c - Demonstrating terminal functions */

void kernel_main(void) {
    /* Initialize terminal */
    terminal_clear();
    
    /* Set colors and print welcome message */
    terminal_set_color(vga_make_color(VGA_LIGHT_GREEN, VGA_BLACK));
    puts_nl("MiniOS v0.1 - Display System Demo");
    
    terminal_set_color(vga_make_color(VGA_WHITE, VGA_BLACK));
    puts("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
    
    /* Test kprintf */
    kprintf("Testing kprintf:\n");
    kprintf("  Integer: %d\n", -12345);
    kprintf("  Unsigned: %u\n", 12345);
    kprintf("  Hex: 0x%X\n", 0xDEADBEEF);
    kprintf("  String: %s\n", "Hello, kernel!");
    kprintf("  Pointer: %p\n", (void*)0xB8000);
    kprintf("  Char: '%c'\n", 'X');
    
    /* Test colors */
    terminal_set_color(vga_make_color(VGA_LIGHT_RED, VGA_BLACK));
    kprintf("\n[ERROR] ");
    terminal_set_color(vga_make_color(VGA_WHITE, VGA_BLACK));
    puts("This is an error message\n");
    
    terminal_set_color(vga_make_color(VGA_YELLOW, VGA_BLACK));
    kprintf("[WARN]  ");
    terminal_set_color(vga_make_color(VGA_WHITE, VGA_BLACK));
    puts("This is a warning\n");
    
    terminal_set_color(vga_make_color(VGA_LIGHT_CYAN, VGA_BLACK));
    kprintf("[INFO]  ");
    terminal_set_color(vga_make_color(VGA_WHITE, VGA_BLACK));
    puts("This is informational\n");
    
    /* Halt */
    for (;;) {
        __asm__ volatile ("hlt");
    }
}
Output Demo

Keyboard Input

Now let's tackle the input side. Reading from a keyboard is more complex than writing to a screen because keyboards generate scan codes that we must translate to characters.

PS/2 Controller

The PS/2 controller (Intel 8042) is the traditional keyboard controller. Even modern PCs emulate it for compatibility. It uses two I/O ports:

PS/2 CONTROLLER PORTS
═══════════════════════════════════════════════════════════════

Port 0x60 - Data Port (R/W)
├── Read:  Get scan code or response from keyboard
└── Write: Send command to keyboard

Port 0x64 - Status/Command Port
├── Read:  Get controller status byte
└── Write: Send command to controller

STATUS REGISTER (Port 0x64, Read)
┌────┬────┬────┬────┬────┬────┬────┬────┐
│Bit7│Bit6│Bit5│Bit4│Bit3│Bit2│Bit1│Bit0│
│PRTY│TMOT│AUX │KLCK│CMD │SYS │IBF │OBF │
└────┴────┴────┴────┴────┴────┴────┴────┘

OBF (Bit 0): Output Buffer Full - data available to read
IBF (Bit 1): Input Buffer Full - controller processing command
SYS (Bit 2): System Flag - 1 after self-test passed
CMD (Bit 3): Command/Data - 1 = last write was command
AUX (Bit 5): Auxiliary Output - 1 = mouse data, 0 = keyboard
/* keyboard.h - PS/2 keyboard interface */
#ifndef KEYBOARD_H
#define KEYBOARD_H

#include <stdint.h>
#include "io.h"

/* PS/2 controller ports */
#define PS2_DATA_PORT    0x60   /* Read/write keyboard data */
#define PS2_STATUS_PORT  0x64   /* Read status register */
#define PS2_COMMAND_PORT 0x64   /* Write commands */

/* Status register bits */
#define PS2_STATUS_OUTPUT_FULL   0x01  /* Can read from port 0x60 */
#define PS2_STATUS_INPUT_FULL    0x02  /* Cannot write yet */
#define PS2_STATUS_SYSTEM        0x04  /* Self-test passed */
#define PS2_STATUS_COMMAND       0x08  /* Last write was command */
#define PS2_STATUS_TIMEOUT       0x40  /* Timeout error */
#define PS2_STATUS_PARITY        0x80  /* Parity error */

/* Wait until we can read from keyboard */
static inline void ps2_wait_output(void) {
    while (!(inb(PS2_STATUS_PORT) & PS2_STATUS_OUTPUT_FULL));
}

/* Wait until we can write to keyboard */
static inline void ps2_wait_input(void) {
    while (inb(PS2_STATUS_PORT) & PS2_STATUS_INPUT_FULL);
}

/* Check if data is available */
static inline int ps2_data_available(void) {
    return inb(PS2_STATUS_PORT) & PS2_STATUS_OUTPUT_FULL;
}

#endif

Scan Codes

Keyboards don't send ASCII characters—they send scan codes representing physical key positions. Every key press generates a "make" code, and every release generates a "break" code:

SCAN CODE SETS
═══════════════════════════════════════════════════════════════

Scan Code Set 1 (XT) - Legacy, what BIOS emulates
Scan Code Set 2 (AT) - Most common native keyboard set
Scan Code Set 3 (PS/2) - Rarely used

SCAN CODE SET 1 EXAMPLES (what we'll use):
┌────────────────────────────────────────────────────────────┐
│ Key      │ Make Code │ Break Code │ Notes                 │
├──────────┼───────────┼────────────┼───────────────────────┤
│ A        │ 0x1E      │ 0x9E       │ Break = Make + 0x80   │
│ Enter    │ 0x1C      │ 0x9C       │                       │
│ Space    │ 0x39      │ 0xB9       │                       │
│ Escape   │ 0x01      │ 0x81       │                       │
│ L-Shift  │ 0x2A      │ 0xAA       │ Modifier key          │
│ L-Ctrl   │ 0x1D      │ 0x9D       │ Modifier key          │
│ Keypad * │ 0xE0 0x37 │ 0xE0 0xB7  │ Extended (2-byte)     │
│ Arrow Up │ 0xE0 0x48 │ 0xE0 0xC8  │ Extended (2-byte)     │
└────────────────────────────────────────────────────────────┘

The 0xE0 prefix indicates extended keys (added with 101-key keyboards)
/* US QWERTY scan code to ASCII mapping (Set 1) */
static const char scancode_ascii[128] = {
    /* 0x00 */  0,   27, '1', '2', '3', '4', '5', '6', 
    /* 0x08 */ '7', '8', '9', '0', '-', '=', '\b', '\t',
    /* 0x10 */ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i',
    /* 0x18 */ 'o', 'p', '[', ']', '\n',   0, 'a', 's',  /* 0 = Ctrl */
    /* 0x20 */ 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',
    /* 0x28 */'\'', '`',   0,'\\', 'z', 'x', 'c', 'v',  /* 0 = LShift */
    /* 0x30 */ 'b', 'n', 'm', ',', '.', '/',   0, '*',  /* 0 = RShift */
    /* 0x38 */   0, ' ',   0,   0,   0,   0,   0,   0,  /* Alt, Space, Caps, F1-F5 */
    /* 0x40 */   0,   0,   0,   0,   0,   0,   0, '7',  /* F6-F10, NumLock, Scroll, KP7 */
    /* 0x48 */ '8', '9', '-', '4', '5', '6', '+', '1',  /* KP8-9, KP-, KP4-6, KP+, KP1 */
    /* 0x50 */ '2', '3', '0', '.',   0,   0,   0,   0,  /* KP2-3, KP0, KP., ?, ?, F11, F12 */
    /* 0x58-0x7F all zeros for remaining keys */
};

/* Shifted versions (when Shift is held) */
static const char scancode_ascii_shift[128] = {
    /* 0x00 */  0,   27, '!', '@', '#', '$', '%', '^',
    /* 0x08 */ '&', '*', '(', ')', '_', '+', '\b', '\t',
    /* 0x10 */ 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I',
    /* 0x18 */ 'O', 'P', '{', '}', '\n',   0, 'A', 'S',
    /* 0x20 */ 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':',
    /* 0x28 */ '"', '~',   0, '|', 'Z', 'X', 'C', 'V',
    /* 0x30 */ 'B', 'N', 'M', '<', '>', '?',   0, '*',
    /* Rest same as unshifted... */
};

Polling Input

Polling means continuously checking if data is available. It's simple but wasteful—later we'll replace this with interrupts:

/* keyboard.c - Keyboard polling driver */

#include "keyboard.h"

/* Keyboard state */
static int shift_pressed = 0;
static int ctrl_pressed = 0;
static int alt_pressed = 0;
static int caps_lock = 0;

/* Special scan codes */
#define KEY_ESCAPE      0x01
#define KEY_BACKSPACE   0x0E
#define KEY_TAB         0x0F
#define KEY_ENTER       0x1C
#define KEY_CTRL        0x1D
#define KEY_LSHIFT      0x2A
#define KEY_RSHIFT      0x36
#define KEY_ALT         0x38
#define KEY_SPACE       0x39
#define KEY_CAPSLOCK    0x3A
#define KEY_F1          0x3B
#define KEY_F10         0x44
#define KEY_NUMLOCK     0x45
#define KEY_SCROLLLOCK  0x46

/* Read a raw scan code (blocking) */
uint8_t keyboard_read_scancode(void) {
    ps2_wait_output();
    return inb(PS2_DATA_PORT);
}

/* Read a raw scan code (non-blocking, returns 0 if none) */
uint8_t keyboard_read_scancode_nowait(void) {
    if (ps2_data_available()) {
        return inb(PS2_DATA_PORT);
    }
    return 0;
}

/* Get a character (blocking, handles modifiers) */
char keyboard_getchar(void) {
    while (1) {
        uint8_t scancode = keyboard_read_scancode();
        
        /* Handle key release (break code) */
        if (scancode & 0x80) {
            uint8_t release = scancode & 0x7F;
            
            if (release == KEY_LSHIFT || release == KEY_RSHIFT) {
                shift_pressed = 0;
            } else if (release == KEY_CTRL) {
                ctrl_pressed = 0;
            } else if (release == KEY_ALT) {
                alt_pressed = 0;
            }
            continue;  /* Keep waiting for a key press */
        }
        
        /* Handle modifier key presses */
        if (scancode == KEY_LSHIFT || scancode == KEY_RSHIFT) {
            shift_pressed = 1;
            continue;
        }
        if (scancode == KEY_CTRL) {
            ctrl_pressed = 1;
            continue;
        }
        if (scancode == KEY_ALT) {
            alt_pressed = 1;
            continue;
        }
        if (scancode == KEY_CAPSLOCK) {
            caps_lock = !caps_lock;
            continue;
        }
        
        /* Look up ASCII value */
        char c;
        int use_shift = shift_pressed;
        
        /* Caps lock affects only letters */
        if (scancode >= 0x10 && scancode <= 0x19) {  /* Q-P */
            use_shift ^= caps_lock;
        } else if (scancode >= 0x1E && scancode <= 0x26) {  /* A-L */
            use_shift ^= caps_lock;
        } else if (scancode >= 0x2C && scancode <= 0x32) {  /* Z-M */
            use_shift ^= caps_lock;
        }
        
        if (use_shift) {
            c = scancode_ascii_shift[scancode];
        } else {
            c = scancode_ascii[scancode];
        }
        
        /* Return valid characters */
        if (c != 0) {
            return c;
        }
    }
}
Polling Wastes CPU! The keyboard_getchar() function spins in a tight loop, consuming 100% CPU while waiting. In Phase 5, we'll replace this with interrupt-driven input where the CPU sleeps until a key is pressed.

Extended Keys & Special Handling

Modern keyboards have extended keys (arrows, Insert, Delete, etc.) that send multi-byte scan codes starting with 0xE0:

/* Extended key handling */

/* Extended key codes (after 0xE0 prefix) */
#define KEY_EXT_UP       0x48
#define KEY_EXT_DOWN     0x50
#define KEY_EXT_LEFT     0x4B
#define KEY_EXT_RIGHT    0x4D
#define KEY_EXT_INSERT   0x52
#define KEY_EXT_DELETE   0x53
#define KEY_EXT_HOME     0x47
#define KEY_EXT_END      0x4F
#define KEY_EXT_PAGEUP   0x49
#define KEY_EXT_PAGEDOWN 0x51

/* Special key result codes (returned instead of ASCII) */
#define KEYCODE_UP       0x100
#define KEYCODE_DOWN     0x101
#define KEYCODE_LEFT     0x102
#define KEYCODE_RIGHT    0x103
#define KEYCODE_INSERT   0x104
#define KEYCODE_DELETE   0x105
#define KEYCODE_HOME     0x106
#define KEYCODE_END      0x107
#define KEYCODE_PAGEUP   0x108
#define KEYCODE_PAGEDOWN 0x109

/* Get key with extended key support - returns int to allow codes > 255 */
int keyboard_getkey(void) {
    uint8_t scancode = keyboard_read_scancode();
    
    /* Handle 0xE0 extended keys */
    if (scancode == 0xE0) {
        scancode = keyboard_read_scancode();
        
        /* Ignore break codes for extended keys */
        if (scancode & 0x80) {
            return 0;
        }
        
        switch (scancode) {
            case KEY_EXT_UP:       return KEYCODE_UP;
            case KEY_EXT_DOWN:     return KEYCODE_DOWN;
            case KEY_EXT_LEFT:     return KEYCODE_LEFT;
            case KEY_EXT_RIGHT:    return KEYCODE_RIGHT;
            case KEY_EXT_INSERT:   return KEYCODE_INSERT;
            case KEY_EXT_DELETE:   return KEYCODE_DELETE;
            case KEY_EXT_HOME:     return KEYCODE_HOME;
            case KEY_EXT_END:      return KEYCODE_END;
            case KEY_EXT_PAGEUP:   return KEYCODE_PAGEUP;
            case KEY_EXT_PAGEDOWN: return KEYCODE_PAGEDOWN;
            default:               return 0;
        }
    }
    
    /* Handle regular keys (reuse modifier logic from keyboard_getchar) */
    /* ... same logic as before ... */
    
    return scancode_ascii[scancode];
}

High-Level Input Functions

Let's build user-friendly input functions that combine our keyboard driver with terminal output for proper echoing and line editing.

getchar Implementation

A proper getchar echoes the character to the screen so users see what they're typing:

/* stdio.c - High-level I/O functions */

#include "terminal.h"
#include "keyboard.h"

/* Get a single character with echo */
char getchar(void) {
    char c = keyboard_getchar();
    
    /* Echo printable characters */
    if (c >= ' ' && c <= '~') {
        putc(c);
    } else if (c == '\n' || c == '\r') {
        putc('\n');
    }
    
    return c;
}

/* Get a single character without echo (for passwords, etc.) */
char getchar_noecho(void) {
    return keyboard_getchar();
}

/* Check if a key is available (non-blocking) */
int kbhit(void) {
    return ps2_data_available();
}

gets Implementation with Line Editing

A proper line input function needs to handle backspace, display limits, and buffer overflow protection:

/* Read a line of input with editing support */
char* gets(char* buffer, int max_length) {
    int pos = 0;
    max_length--;  /* Reserve space for null terminator */
    
    while (1) {
        char c = keyboard_getchar();
        
        if (c == '\n' || c == '\r') {
            /* Enter pressed - complete the line */
            buffer[pos] = '\0';
            putc('\n');
            return buffer;
        }
        else if (c == '\b' || c == 127) {  /* Backspace or DEL */
            if (pos > 0) {
                pos--;
                /* Erase character on screen: back, space, back */
                puts("\b \b");
            }
        }
        else if (c == 0x1B) {  /* Escape - cancel input */
            buffer[0] = '\0';
            putc('\n');
            return NULL;
        }
        else if (c == 0x15) {  /* Ctrl+U - clear line */
            while (pos > 0) {
                pos--;
                puts("\b \b");
            }
        }
        else if (c >= ' ' && c <= '~') {  /* Printable character */
            if (pos < max_length) {
                buffer[pos++] = c;
                putc(c);
            }
            /* Silently ignore if buffer is full */
        }
    }
}

/* Read a line with a prompt */
char* prompt(const char* prompt_str, char* buffer, int max_length) {
    puts(prompt_str);
    return gets(buffer, max_length);
}

/* Read a password (no echo) */
char* getpass(const char* prompt_str, char* buffer, int max_length) {
    puts(prompt_str);
    
    int pos = 0;
    max_length--;
    
    while (1) {
        char c = keyboard_getchar();
        
        if (c == '\n' || c == '\r') {
            buffer[pos] = '\0';
            putc('\n');
            return buffer;
        }
        else if (c == '\b' || c == 127) {
            if (pos > 0) {
                pos--;
                /* No visual feedback for password */
            }
        }
        else if (c >= ' ' && c <= '~') {
            if (pos < max_length) {
                buffer[pos++] = c;
                putc('*');  /* Show asterisk instead of actual char */
            }
        }
    }
}
Buffer Overflow Protection: Never use unbounded input functions! Always pass a maximum length. The classic gets() without bounds was so dangerous it was removed from C11.

Complete I/O Header

Here's a complete header file declaring all our I/O functions:

/* stdio.h - Standard I/O declarations */
#ifndef STDIO_H
#define STDIO_H

#include <stdarg.h>
#include <stdint.h>

/* Output functions */
void putc(char c);
void puts(const char* str);
void puts_nl(const char* str);
void puts_at(const char* str, int x, int y, uint8_t color);
int kprintf(const char* fmt, ...);

/* Terminal control */
void terminal_clear(void);
void terminal_set_color(uint8_t color);

/* Input functions */
char getchar(void);
char getchar_noecho(void);
int kbhit(void);
char* gets(char* buf, int maxlen);
char* prompt(const char* prompt, char* buf, int maxlen);
char* getpass(const char* prompt, char* buf, int maxlen);

#endif

What You Can Build

Phase 4 Project: A mini shell UI for your kernel! Your OS can now display colored text, accept keyboard input, and respond to user commands in a basic read-eval-print loop.

Mini Shell Implementation

Let's build a basic interactive shell that responds to commands:

/* shell.c - Mini shell implementation */

#include "stdio.h"
#include "string.h"  /* We'll need strcmp, etc. */

#define MAX_CMD_LEN 256

/* Simple string comparison (implement yourself or use ours) */
static int str_equal(const char* a, const char* b) {
    while (*a && *b) {
        if (*a++ != *b++) return 0;
    }
    return *a == *b;
}

/* Command handlers */
static void cmd_help(void) {
    puts_nl("Available commands:");
    puts_nl("  help    - Show this help message");
    puts_nl("  clear   - Clear the screen");
    puts_nl("  info    - Display system information");
    puts_nl("  version - Show OS version");
    puts_nl("  color   - Cycle through colors");
    puts_nl("  echo    - Echo text back");
    puts_nl("  reboot  - Restart the system");
}

static void cmd_info(void) {
    puts_nl("System Information:");
    puts_nl("  Architecture: x86 (i386)");
    puts_nl("  Mode: 32-bit Protected Mode");
    kprintf("  VGA Buffer: %p\n", (void*)0xB8000);
    puts_nl("  Display: 80x25 text mode");
}

static void cmd_version(void) {
    terminal_set_color(vga_make_color(VGA_LIGHT_CYAN, VGA_BLACK));
    puts_nl("MiniOS v0.4 - Display & Input");
    terminal_set_color(vga_make_color(VGA_LIGHT_GREY, VGA_BLACK));
    puts_nl("Kernel Development Tutorial - Phase 4");
}

static void cmd_reboot(void) {
    puts_nl("Rebooting...");
    
    /* Triple fault method - CPU resets */
    /* Load an invalid IDT and trigger interrupt */
    __asm__ volatile (
        "lidt (%0)"
        : : "r" ((void*)0)
    );
    __asm__ volatile ("int $0");
}

/* Color demo */
static void cmd_color(void) {
    const char* names[] = {
        "Black", "Blue", "Green", "Cyan",
        "Red", "Magenta", "Brown", "Light Grey",
        "Dark Grey", "Light Blue", "Light Green", "Light Cyan",
        "Light Red", "Light Magenta", "Yellow", "White"
    };
    
    puts_nl("VGA Colors:");
    for (int i = 0; i < 16; i++) {
        terminal_set_color(vga_make_color(i, VGA_BLACK));
        kprintf("  %d: %s\n", i, names[i]);
    }
    terminal_set_color(vga_make_color(VGA_LIGHT_GREY, VGA_BLACK));
}

/* Parse and execute command */
static void execute_command(char* cmd) {
    /* Skip leading whitespace */
    while (*cmd == ' ') cmd++;
    
    /* Empty command */
    if (*cmd == '\0') return;
    
    /* Match commands */
    if (str_equal(cmd, "help") || str_equal(cmd, "?")) {
        cmd_help();
    }
    else if (str_equal(cmd, "clear") || str_equal(cmd, "cls")) {
        terminal_clear();
    }
    else if (str_equal(cmd, "info")) {
        cmd_info();
    }
    else if (str_equal(cmd, "version") || str_equal(cmd, "ver")) {
        cmd_version();
    }
    else if (str_equal(cmd, "color") || str_equal(cmd, "colors")) {
        cmd_color();
    }
    else if (str_equal(cmd, "reboot")) {
        cmd_reboot();
    }
    else if (cmd[0] == 'e' && cmd[1] == 'c' && cmd[2] == 'h' && 
             cmd[3] == 'o' && cmd[4] == ' ') {
        /* echo command with argument */
        puts_nl(cmd + 5);
    }
    else {
        terminal_set_color(vga_make_color(VGA_LIGHT_RED, VGA_BLACK));
        kprintf("Unknown command: %s\n", cmd);
        terminal_set_color(vga_make_color(VGA_LIGHT_GREY, VGA_BLACK));
        puts_nl("Type 'help' for available commands.");
    }
}

/* Main shell loop */
void shell_run(void) {
    char buffer[MAX_CMD_LEN];
    
    /* Welcome banner */
    terminal_clear();
    terminal_set_color(vga_make_color(VGA_LIGHT_GREEN, VGA_BLACK));
    puts_nl("╔═══════════════════════════════════════════╗");
    puts_nl("║       MiniOS Shell - Phase 4 Demo         ║");
    puts_nl("║   Type 'help' for available commands      ║");
    puts_nl("╚═══════════════════════════════════════════╝");
    terminal_set_color(vga_make_color(VGA_LIGHT_GREY, VGA_BLACK));
    putc('\n');
    
    /* Read-Eval-Print Loop */
    while (1) {
        /* Print prompt */
        terminal_set_color(vga_make_color(VGA_LIGHT_CYAN, VGA_BLACK));
        puts("minios");
        terminal_set_color(vga_make_color(VGA_WHITE, VGA_BLACK));
        puts("> ");
        terminal_set_color(vga_make_color(VGA_LIGHT_GREY, VGA_BLACK));
        
        /* Get input */
        if (gets(buffer, MAX_CMD_LEN) != NULL) {
            execute_command(buffer);
        }
    }
}

/* Kernel entry point */
void kernel_main(void) {
    /* Initialize subsystems */
    terminal_clear();
    vga_enable_cursor(13, 15);  /* Underline cursor */
    
    /* Run interactive shell */
    shell_run();
}

Exercise 1: Add More Commands

Extend the shell with these commands:

  • time - Display uptime (you'll need a tick counter in Phase 5)
  • mem - Display memory information (parse from BIOS)
  • beep - Make the PC speaker beep
Implementation

Exercise 2: Hex Dump Utility

Create a hexdump command that displays memory contents:

void cmd_hexdump(uint32_t address, int length) {
    uint8_t* ptr = (uint8_t*)address;
    
    for (int i = 0; i < length; i += 16) {
        kprintf("%08X: ", address + i);
        
        /* Hex bytes */
        for (int j = 0; j < 16 && (i + j) < length; j++) {
            kprintf("%02X ", ptr[i + j]);
        }
        
        puts(" |");
        
        /* ASCII representation */
        for (int j = 0; j < 16 && (i + j) < length; j++) {
            char c = ptr[i + j];
            putc((c >= 32 && c < 127) ? c : '.');
        }
        puts("|\n");
    }
}
/* Usage: hexdump 0xB8000 256 */
Memory Tools

Exercise 3: Text Editor Mode

Create a simple full-screen text editor:

  • Arrow keys move the cursor (using keyboard_getkey())
  • Type anywhere on screen
  • Escape exits editor mode
  • Hint: Store text in a 80×25 character buffer, redraw on changes
Advanced UI

Next Steps

Your kernel can now communicate with users! But there's a fundamental problem: our keyboard input uses polling, constantly checking for keypresses and wasting CPU cycles. And what about handling errors gracefully?

What's Coming in Phase 5: Interrupts

In Phase 5, we'll implement proper interrupt-driven I/O:

  • Interrupt Descriptor Table (IDT) – Tell the CPU where to find handler code
  • Programmable Interrupt Controllers (PIC) – Configure hardware interrupt routing
  • Exception Handlers – Handle division by zero, page faults, etc.
  • Keyboard IRQ – Wake up only when a key is actually pressed
  • System Timer (PIT) – Implement scheduling and uptime tracking
POLLING vs INTERRUPT-DRIVEN I/O
═══════════════════════════════════════════════════════════════

POLLING (Current - Phase 4):
┌─────────────────────────────────────────────────────────────┐
│  CPU → Check keyboard → No key → Check keyboard → No key → │
│       → Check keyboard → No key → ... → Key! → Process     │
│                                                             │
│  Problem: CPU is 100% busy doing nothing useful!            │
└─────────────────────────────────────────────────────────────┘

INTERRUPT-DRIVEN (Phase 5):
┌─────────────────────────────────────────────────────────────┐
│  CPU → Do useful work → HLT (sleep)                         │
│                           ↑                                 │
│             Key pressed! ─┘  ← Hardware interrupt           │
│                           ↓                                 │
│  CPU → Handle key → Resume work or back to HLT              │
│                                                             │
│  Benefit: CPU only wakes when there's actual work!          │
└─────────────────────────────────────────────────────────────┘
Key Takeaways from Phase 4:
  • VGA text mode is simply memory-mapped I/O at 0xB8000
  • Each screen position is 2 bytes: character + attribute
  • The PS/2 keyboard sends scan codes, not ASCII characters
  • Polling is simple but inefficient—interrupts are better
  • Always protect against buffer overflows in input functions
Technology