Merge pull request #4 from Plexi09/feature/full-interrupts-time-input

io.h (inb/outb/io_wait), 8259 PIC remap IRQ 0–15 → vectors 32–47, pic_eoi/mask/unmask, 16 IRQ gates + irq_dispatch/irq_register_handler in isr.asm/idt.c. Exceptions 0–31 untouched.

8254 PIT ch0 mode 3 @100hz on IRQ0, volatile tick counter, pit_sleep hlt-loop.

COM1 38400 8N1 polling log (serial_ok open-bus check), PS/2 set-1 IRQ1 driver (128-entry ring, Shift/Caps XOR, E0/release handling, \b/\n passthrough).

init order idt_init → pic_remap → idt_install_irqs → serial → pit → keyboard → unmask(0,1) → sti, then hlt loop with uptime S.Ts ~1/s + keyboard echo (VGA + serial mirror). \b erases via console_putchar.
This commit is contained in:
Pierre-Emmanuel Gob. 2026-09-18 21:04:34 +02:00 • committed by GitHub
commit 3868884e3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1003 additions and 11 deletions

View file

@ -20,7 +20,7 @@ CFLAGS := -std=c17 -ffreestanding -nostdlib -fno-builtin \
-fno-pic -fno-pie -fno-stack-protector \
-Wall -Wextra -Werror -O2 -Iinclude
OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/console.o $(BUILD)/idt.o
OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/console.o $(BUILD)/idt.o $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o
all: $(IMG)
@ -33,13 +33,25 @@ $(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD)
$(BUILD)/isr.o: $(ARCH_DIR)/isr.asm | $(BUILD)
$(NASM) -f elf64 $< -o $@
$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h | $(BUILD)
$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h include/pic.h include/pit.h include/serial.h include/keyboard.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/idt.o: $(ARCH_DIR)/idt.c include/idt.h include/console.h | $(BUILD)
$(BUILD)/idt.o: $(ARCH_DIR)/idt.c include/idt.h include/console.h include/pic.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/pic.o: $(ARCH_DIR)/pic.c include/pic.h include/io.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/pit.o: $(ARCH_DIR)/pit.c include/pit.h include/idt.h include/io.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/serial.o: $(ARCH_DIR)/serial.c include/serial.h include/io.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/keyboard.o: $(ARCH_DIR)/keyboard.c include/keyboard.h include/idt.h include/pic.h include/io.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel.elf: $(OBJS) $(ARCH_DIR)/link.ld

View file

@ -4,9 +4,12 @@
* Privileged primitive: `lidt` in idt_init(). Fault behavior: before
* idt_init() any exception triple-faults (no IDT); after, CPU vectors
* 0-31 land in the assembly stubs (isr.asm) and then in
* exception_handler(), which prints and halts. Vectors 32-255 are left
* zero (not present) on purpose because PIC remap + IRQ handling is a later step.
* exception_handler(), which prints and halts. Vectors 32-47 stay
* not-present until the 8259 PIC is remapped and idt_install_irqs()
* installs the IRQ gates; then they land in irq_dispatch(), which
* sends the PIC EOI and calls the per-IRQ handler (if any).
* Interrupts stay disabled throughout (IF=0); exceptions still fire.
* IRQs only fire after the integrator enables them with `sti`.
*/
#ifndef NADIR_IDT_H
@ -17,6 +20,10 @@
/* Number of CPU exception vectors this base installs (0-31). */
#define IDT_EXCEPTION_COUNT 32
/* First remapped IRQ vector (IRQ 0 -> vector 32) and IRQ line count. */
#define IDT_IRQ_BASE 32
#define IDT_IRQ_COUNT 16
/* Stack layout on entry to exception_handler().
*
* Built by the CPU (RIP/CS/RFLAGS, plus an error code on vectors
@ -63,4 +70,22 @@ void idt_init(void);
* frame above. Prints vector/error/RIP and halts; never returns. */
void exception_handler(struct interrupt_frame *frame);
/* C entry point for vectors 32-47, called from irq_common with the
* same frame layout (vector = 32 + irq, error = 0 dummy). Runs at
* Ring 0 with IF cleared; sends the PIC EOI and calls the handler
* registered for that IRQ, if any. Spurious vectors outside 32-47
* are ignored. */
void irq_dispatch(struct interrupt_frame *frame);
/* Register `handler` for IRQ line `irq` (0-15); NULL unregisters.
* Preconditions: Ring 0, `irq` < 16 (out-of-range calls ignored).
* Failure modes: none; takes effect on the next IRQ after return. */
void irq_register_handler(uint8_t irq, void (*handler)(void));
/* Install the 16 IRQ gates (vectors 32-47 -> irq stubs) with selector
* 0x18 and flags 0x8E (present, DPL 0, 64-bit interrupt gate).
* Preconditions: Ring 0, idt_init() already ran, PIC already remapped
* via pic_remap(). Leaves IF cleared. Failure modes: none. */
void idt_install_irqs(void);
#endif /* NADIR_IDT_H */

47
include/io.h Normal file
View file

@ -0,0 +1,47 @@
/* Nadir x86 port I/O primitives (inb/outb/io_wait).
*
* Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT.
* Privileged instructions: `in`/`out` trap with #GP(0) when CPL > IOPL,
* so these helpers are Ring 0 only and must never be exposed to
* userspace. Fault behavior: a bad port number cannot fault by itself
* (unused ports read back 0xFF or are ignored); a fault here means the
* caller programmed the wrong device, not a CPU trap.
* Interrupts stay disabled by the caller where timing matters; these
* helpers do not touch IF themselves.
*/
#ifndef NADIR_IO_H
#define NADIR_IO_H
#include <stdint.h>
/* Read one byte from ISA port `port`.
* Preconditions: CPL 0, port is a valid x86 I/O address (0-0xFFFF).
* Failure modes: none (returns bus value; 0xFF on unmapped ports). */
static inline uint8_t inb(uint16_t port)
{
uint8_t value;
__asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port));
return value;
}
/* Write byte `value` to ISA port `port`.
* Preconditions: CPL 0, port is writable for this device.
* Failure modes: none from the CPU; wrong port = device misbehavior. */
static inline void outb(uint16_t port, uint8_t value)
{
__asm__ volatile("outb %0, %1" ::"a"(value), "Nd"(port));
}
/* Tiny delay for legacy ISA devices (writes to unused port 0x80).
*
* WHY: the 8259 PIC and other slow devices need ~1us between back to
* back command bytes; port 0x80 is the conventional throwaway delay.
* Preconditions: CPL 0. Failure modes: none. */
static inline void io_wait(void)
{
outb(0x80U, 0U);
}
#endif /* NADIR_IO_H */

42
include/keyboard.h Normal file
View file

@ -0,0 +1,42 @@
/* Nadir PS/2 keyboard: scancode set-1 IRQ1 driver with ASCII queue.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0); uses `inb` on ports
* 0x60 (data) / 0x64 (status), which fault with #GP above CPL 0, so
* this driver is kernel-only. QEMU/SeaBIOS provides a PS/2 controller
* in set-1 mode; the 8259 routes IRQ1 to vector 33 after the PIC remap.
* Fault behavior: port I/O cannot fault at CPL 0. Reading 0x60 when
* the controller has no byte returns stale data, so keyboard_on_irq()
* must run only from IRQ1 (data-ready guaranteed).
*
* Locking: single core, one producer (IRQ1 handler) and one consumer
* (polling kmain). `volatile` head/tail plus single-byte loads/stores
* suffice — no locks, no atomics. IF stays cleared; the queue is both
* polling-safe and IRQ-safe under that model.
*
* Preconditions: call keyboard_init() once before use. It registers
* keyboard_on_irq() for IRQ1 (via irq_register_handler) and unmasks
* the line (via pic_unmask); the PIC remap, IRQ-gate install, and
* `sti` stay integrator duty, and IF stays cleared until then.
* Failure modes: unknown/extended/release codes are dropped silently;
* a full queue drops the newest byte; dequeue on empty returns 0.
*/
#ifndef NADIR_KEYBOARD_H
#define NADIR_KEYBOARD_H
/* Clear the queue and modifier state, register keyboard_on_irq() for
* IRQ1, and unmask IRQ1. */
void keyboard_init(void);
/* IRQ1 entry point: read one scancode from 0x60, translate, enqueue.
* Runs in interrupt context. */
void keyboard_on_irq(void);
/* Nonzero if at least one translated byte waits in the queue. */
int keyboard_has_key(void);
/* Remove and return the oldest byte, or 0 when the queue is empty.
* Backspace arrives as '\b', Enter as '\n'. */
char keyboard_dequeue(void);
#endif /* NADIR_KEYBOARD_H */

58
include/pic.h Normal file
View file

@ -0,0 +1,58 @@
/* Nadir 8259 PIC driver: remap, EOI, IRQ masking.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT.
* Privileged instructions: `outb`/`inb` (via io.h) trap with #GP(0)
* when CPL > IOPL, so every entry point below is Ring 0 only.
* Fault behavior: no CPU fault is possible beyond a programming bug;
* a wrong sequence masks the wrong IRQ or loses an EOI (hung line),
* never a triple fault by itself. IF stays cleared by the caller;
* this driver never executes `sti`/`cli`.
*/
#ifndef NADIR_PIC_H
#define NADIR_PIC_H
#include <stdint.h>
/* Command/data ports for the master (PIC1) and slave (PIC2). */
#define PIC1_CMD 0x20U
#define PIC1_DATA 0x21U
#define PIC2_CMD 0xA0U
#define PIC2_DATA 0xA1U
/* End-of-interrupt command byte. */
#define PIC_EOI 0x20U
/* Remapped vector bases: IRQ 0-7 -> vectors 32-39, IRQ 8-15 -> 40-47. */
#define PIC_IRQ0_VECTOR 0x20U
#define PIC_IRQ8_VECTOR 0x28U
/* Number of IRQ lines behind the cascaded 8259 pair. */
#define PIC_IRQ_COUNT 16U
/* Remap the cascaded 8259 pair to vectors 32-47 and mask everything.
* After reset IRQs overlap CPU exceptions 0-15, so without a
* remap a timer tick looks like a fault. Mask-all leaves each device
* opt-in via pic_unmask().
* Preconditions: Ring 0, called once before idt_install_irqs().
* Failure modes: none. */
void pic_remap(void);
/* Signal end-of-interrupt for IRQ line `irq` (0-15).
* WHY cascade order: the slave must be acked before the master or the
* master keeps the cascade line busy and IRQ 2 stops firing.
* Preconditions: Ring 0, `irq` < 16.
* Failure modes: out-of-range `irq` is ignored. */
void pic_eoi(uint8_t irq);
/* Mask (disable) IRQ line `irq` (0-15).
* Preconditions: Ring 0, `irq` < 16.
* Failure modes: out-of-range `irq` is ignored. */
void pic_mask(uint8_t irq);
/* Unmask (enable) IRQ line `irq` (0-15).
* Preconditions: Ring 0, `irq` < 16, PIC already remapped.
* Failure modes: out-of-range `irq` is ignored. */
void pic_unmask(uint8_t irq);
#endif /* NADIR_PIC_H */

56
include/pit.h Normal file
View file

@ -0,0 +1,56 @@
/* Nadir 8254 PIT timer driver (see pit.c).
*
* Runs in 64-bit long mode at Ring 0 (CPL 0): programs the PIT via `outb`
* to ISA ports PIT_CMD (0x43) and PIT_CH0 (0x40), and handles IRQ0
* (8259 PIC IRQ0 -> CPU vector 32 after pic_remap).
*
* Ports used: 0x43 (command/mode register, write-only), 0x40 (channel 0
* data, write-only for the divisor latch).
*
* Preconditions: caller owns interrupt policy — pit_init() neither enables
* (sti) nor disables (cli) interrupts; target init order is
* pic_remap -> idt_install_irqs -> pit_init -> pic_unmask(0) -> sti.
* pit_sleep() requires interrupts enabled, otherwise `hlt` never wakes.
*
* Failure modes: divisor is fixed at compile time (PIT_DIVISOR); a zero
* divisor would mean "65536" to the hardware and the wrong rate, so it is
* rejected with a compile-time _Static_assert. Frequency is fixed at 100Hz;
* any other rate needs a deliberate driver change, not a runtime argument.
*/
#ifndef NADIR_PIT_H
#define NADIR_PIT_H
#include <stdint.h>
/* PIT input clock: crystal-derived base rate shared by all IBM-PC PITs. */
#define PIT_BASE_HZ 1193182U
/* Driver tick rate: 100Hz = 10ms per tick. */
#define PIT_HZ 100U
/* Divisor latched into channel 0 (1193182/100 = 11931). */
#define PIT_DIVISOR (PIT_BASE_HZ / PIT_HZ)
/* 8259 PIC IRQ line carrying channel-0 ticks (remapped to vector 32). */
#define PIT_IRQ 0U
/* Program channel 0 for mode 3 square-wave at PIT_HZ and register
* pit_on_tick as the IRQ0 handler (if irq_register_handler is linked in).
* Ring 0 only. Idempotent: second and later calls do nothing. Leaves IF
* exactly as found; the caller decides when to sti. */
void pit_init(void);
/* IRQ0 handler: increments the tick counter. Called from IRQ context
* (interrupts already gated by the CPU/IRQ stub); must stay async-safe
* and never block. */
void pit_on_tick(void);
/* Monotonic tick count since pit_init (10ms per tick at 100Hz). Wraps only
* after ~5.8 billion years; unsigned subtraction stays correct across it. */
uint64_t pit_ticks(void);
/* Busy-wait `ticks` ticks using `hlt` to save power between interrupts.
* Requires interrupts enabled (IF=1) and the PIT IRQ unmasked, otherwise
* the CPU sleeps forever. Uses unsigned delta so a counter wrap mid-sleep
* still terminates. */
void pit_sleep(uint64_t ticks);
#endif /* NADIR_PIT_H */

35
include/serial.h Normal file
View file

@ -0,0 +1,35 @@
/* Nadir serial log: COM1 (16550 UART) polling driver.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0); uses `inb`/`outb` which fault
* with #GP if executed above CPL 0, so this driver is kernel-only.
* Ports: COM1 0x3F8-0x3FF (data, int-enable, divisor latch, FIFO, line
* control, modem control, line status). QEMU/SeaBIOS provides COM1; the
* VGA console stays primary and serial is the secondary log.
* Fault behavior: port I/O cannot fault at CPL 0. The TX poll spins on
* LSR bit 5 and hangs only if the UART hardware is absent or wedged —
* call serial_ok() first to check LSR sanity.
*
* Preconditions: call serial_init() once before any put. serial_puts()
* requires a valid NUL-terminated string (NULL is ignored, not crashed
* on). Failure modes: on absent hardware, init writes go nowhere and
* serial_ok() returns 0; TX polls may then spin forever.
*/
#ifndef NADIR_SERIAL_H
#define NADIR_SERIAL_H
/* Program COM1 for 38400 8N1 polling (interrupts off, FIFO on).
* Ring 0 only. Safe to call again to re-program the port. */
void serial_init(void);
/* Block until the THR is empty (LSR bit 5), then emit one byte. */
void serial_putchar(char c);
/* Emit a NUL-terminated string via serial_putchar(). NULL is a no-op. */
void serial_puts(const char *s);
/* Nonzero if a UART looks present (LSR reads back sane, not 0xFF
* as an open bus returns). Zero means "do not trust TX polls". */
int serial_ok(void);
#endif /* NADIR_SERIAL_H */

View file

@ -46,6 +46,21 @@ void console_putchar(char c)
if (c == '\n') {
cursor_row++;
cursor_col = 0;
} else if (c == '\b') {
/* kmain echoes keyboard '\b' for editing. Without this the
* raw 0x08 glyph would print instead of erasing. Move back one
* cell and blank it (IRQ-safe: mainline only, IF=1 but the
* IRQ handlers never touch VGA). */
if (cursor_col > 0) {
cursor_col--;
} else if (cursor_row > 0) {
cursor_row--;
cursor_col = VGA_WIDTH - 1;
} else {
return;
}
vga[cursor_row * VGA_WIDTH + cursor_col] =
(uint16_t)((COLOR_DEFAULT << 8) | ' ');
} else {
vga[cursor_row * VGA_WIDTH + cursor_col] =
(uint16_t)((COLOR_DEFAULT << 8) | c);

View file

@ -4,14 +4,17 @@
* (kernel code selector 0x18). Privileged primitive: `lidt` in
* idt_init(). Fault behavior: before idt_init() any fault
* triple-faults; after, vectors 0-31 print diagnostics and halt.
* Vectors 32-255 stay not-present until the PIC is remapped.
* IF stays cleared; exceptions fire regardless of IF.
* Vectors 32-47 stay not-present until pic_remap() +
* idt_install_irqs() install the IRQ gates; vectors 48-255 stay
* not-present (no APIC yet). IF stays cleared; exceptions fire
* regardless of IF, IRQs only after the integrator runs `sti`.
*/
#include <stdint.h>
#include "console.h"
#include "idt.h"
#include "pic.h"
/* 64-bit interrupt gate descriptor (Intel SDM Vol. 3, IDT gate). */
struct idt_entry {
@ -42,6 +45,14 @@ static struct idtr idtr;
/* Stub addresses from isr.asm, one per exception vector 0-31. */
extern uint64_t isr_stub_table[IDT_EXCEPTION_COUNT];
/* Stub addresses from isr.asm, one per remapped IRQ vector 32-47. */
extern uint64_t irq_stub_table[IDT_IRQ_COUNT];
/* Per-IRQ handlers for lines 0-15; NULL means "EOI only".
* WHY a plain table: one subsystem per file — device drivers own
* their logic and only register a callback here. BSS-zeroed. */
static void (*irq_handlers[IDT_IRQ_COUNT])(void);
/* Install one gate: handler address + segment + IST index + flags. */
static void idt_set_gate(uint8_t vector, uint64_t handler, uint16_t selector,
uint8_t ist, uint8_t flags)
@ -86,6 +97,41 @@ void idt_init(void)
__asm__ volatile("lidt %0" ::"m"(idtr) : "memory");
}
void idt_install_irqs(void)
{
for (uint16_t i = 0; i < IDT_IRQ_COUNT; i++) {
idt_set_gate((uint8_t)(IDT_IRQ_BASE + i), irq_stub_table[i],
KERNEL_CS, 0, IDT_FLAG_PRESENT_INT);
}
}
void irq_register_handler(uint8_t irq, void (*handler)(void))
{
if (irq >= IDT_IRQ_COUNT) {
return;
}
irq_handlers[irq] = handler;
}
void irq_dispatch(struct interrupt_frame *frame)
{
uint64_t vector = frame->vector;
uint8_t irq;
if (vector < IDT_IRQ_BASE || vector >= IDT_IRQ_BASE + IDT_IRQ_COUNT) {
return;
}
irq = (uint8_t)(vector - IDT_IRQ_BASE);
/* WHY handler before EOI: the EOI re-arms the (edge-triggered) PIC
* line, so acking first would let the same IRQ re-enter before the
* driver finished. A NULL slot just gets an EOI. */
if (irq_handlers[irq] != 0) {
irq_handlers[irq]();
}
pic_eoi(irq);
}
void exception_handler(struct interrupt_frame *frame)
{
uint64_t vector = frame->vector;

View file

@ -16,7 +16,9 @@ bits 64
section .text
global isr_stub_table
global irq_stub_table
extern exception_handler
extern irq_dispatch
; Vectors without a CPU error code: normalize with a dummy 0.
%macro ISR_NOERR 1
@ -105,3 +107,91 @@ isr_stub_table:
dq isr_stub_20, isr_stub_21, isr_stub_22, isr_stub_23
dq isr_stub_24, isr_stub_25, isr_stub_26, isr_stub_27
dq isr_stub_28, isr_stub_29, isr_stub_30, isr_stub_31
; ---------------------------------------------------------------------------
; Nadir IRQ stubs: vectors 32-47 (remapped 8259 IRQs 0-15) -> irq_dispatch.
;
; Same Ring 0 / CPL 0 entry context as the exception stubs above: the CPU
; entered with IF cleared (interrupt gate), no privilege change, so only
; RIP/CS/RFLAGS were pushed. IRQs never carry a CPU error code, so every
; stub normalizes with a dummy 0 exactly like ISR_NOERR, keeping the
; [vector, error] layout — and thus struct interrupt_frame — identical to
; the exception path. Field order must stay in sync with include/idt.h.
; The EOI is sent by the C dispatcher (pic_eoi), not here, so a future
; per-IRQ EOI policy only touches C. -mno-red-zone keeps pushes safe.
section .text
%macro IRQ_NOERR 1
global isr_stub_%1
isr_stub_%1:
push 0
push %1
jmp irq_common
%endmacro
IRQ_NOERR 32
IRQ_NOERR 33
IRQ_NOERR 34
IRQ_NOERR 35
IRQ_NOERR 36
IRQ_NOERR 37
IRQ_NOERR 38
IRQ_NOERR 39
IRQ_NOERR 40
IRQ_NOERR 41
IRQ_NOERR 42
IRQ_NOERR 43
IRQ_NOERR 44
IRQ_NOERR 45
IRQ_NOERR 46
IRQ_NOERR 47
; Common IRQ entry: save GPRs, call void irq_dispatch(frame in RDI),
; restore GPRs, drop [vector, error], return to the interrupted code.
; Unlike isr_common (which halts), this path must iretq: IRQs are
; resumable. Interrupts stay disabled throughout (IF=0 on entry via an
; interrupt gate; irq_dispatch runs with IF cleared; iretq restores the
; caller's RFLAGS).
irq_common:
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
push rbp
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
mov rdi, rsp
call irq_dispatch
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rbp
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
add rsp, 16
iretq
section .rodata
align 8
irq_stub_table:
dq isr_stub_32, isr_stub_33, isr_stub_34, isr_stub_35
dq isr_stub_36, isr_stub_37, isr_stub_38, isr_stub_39
dq isr_stub_40, isr_stub_41, isr_stub_42, isr_stub_43
dq isr_stub_44, isr_stub_45, isr_stub_46, isr_stub_47

View file

@ -0,0 +1,189 @@
/* PS/2 scancode set-1 IRQ1 driver (see keyboard.h).
*
* IRQ wiring uses the live framework: keyboard_init() registers
* keyboard_on_irq() via irq_register_handler() (idt.h) and unmasks
* line 1 via pic_unmask() (pic.h), both feature-detected with
* __has_include so this file still compiles if either header is
* absent (registration then falls back to caller duty). The driver
* itself never executes `sti`: IF stays cleared and IRQs only fire
* after the integrator remaps the PIC, installs the IRQ gates, and
* enables interrupts. 8259 IRQ1 -> vector 33.
*/
#include <stdint.h>
#include "keyboard.h"
/* Port I/O owned by include/io.h; no local inb duplicate. */
#include "io.h"
#if defined(__has_include)
#if __has_include("idt.h")
#include "idt.h"
#define NADIR_HAS_IRQ_REG 1
#endif
#if __has_include("pic.h")
#include "pic.h"
#define NADIR_HAS_PIC 1
#endif
#endif
#define KBD_DATA 0x60U /* PS/2 data port: one scancode per IRQ1 */
#define KBD_BUF_SIZE 128U /* power of two so wrap is cheap */
#define SC_RELEASE 0x80U /* high bit set = key release, ignore */
#define SC_EXTENDED 0xE0U /* prefix for arrows/extended keys, ignore */
#define SC_LSHIFT 0x2AU
#define SC_RSHIFT 0x36U
#define SC_CAPS 0x3AU
/* US set-1 codes without Shift (0 = no printable mapping). Backspace
* 0x0E stays '\b' and Enter 0x1C stays '\n' so editing works raw. */
static const char k_unshifted[128] = {
[0x02] = '1', [0x03] = '2', [0x04] = '3', [0x05] = '4',
[0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8',
[0x0A] = '9', [0x0B] = '0', [0x0C] = '-', [0x0D] = '=',
[0x0E] = '\b', [0x0F] = '\t',
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1A] = '[', [0x1B] = ']',
[0x1C] = '\n',
[0x1E] = 'a', [0x1F] = 's', [0x20] = 'd', [0x21] = 'f',
[0x22] = 'g', [0x23] = 'h', [0x24] = 'j', [0x25] = 'k',
[0x26] = 'l', [0x27] = ';', [0x28] = '\'', [0x29] = '`',
[0x2B] = '\\',
[0x2C] = 'z', [0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v',
[0x30] = 'b', [0x31] = 'n', [0x32] = 'm', [0x33] = ',',
[0x34] = '.', [0x35] = '/', [0x39] = ' ',
};
/* Same codes with Shift held (letters stay lowercase here; the Shift
* + CapsLock XOR in keyboard_on_irq() uppercases them instead). */
static const char k_shifted[128] = {
[0x02] = '!', [0x03] = '@', [0x04] = '#', [0x05] = '$',
[0x06] = '%', [0x07] = '^', [0x08] = '&', [0x09] = '*',
[0x0A] = '(', [0x0B] = ')', [0x0C] = '_', [0x0D] = '+',
[0x0E] = '\b', [0x0F] = '\t',
[0x10] = 'Q', [0x11] = 'W', [0x12] = 'E', [0x13] = 'R',
[0x14] = 'T', [0x15] = 'Y', [0x16] = 'U', [0x17] = 'I',
[0x18] = 'O', [0x19] = 'P', [0x1A] = '{', [0x1B] = '}',
[0x1C] = '\n',
[0x1E] = 'A', [0x1F] = 'S', [0x20] = 'D', [0x21] = 'F',
[0x22] = 'G', [0x23] = 'H', [0x24] = 'J', [0x25] = 'K',
[0x26] = 'L', [0x27] = ':', [0x28] = '"', [0x29] = '~',
[0x2B] = '|',
[0x2C] = 'Z', [0x2D] = 'X', [0x2E] = 'C', [0x2F] = 'V',
[0x30] = 'B', [0x31] = 'N', [0x32] = 'M', [0x33] = '<',
[0x34] = '>', [0x35] = '?', [0x39] = ' ',
};
/* Single-producer (IRQ) / single-consumer (poll) ring. `volatile` stops
* the compiler caching head/tail across the IRQ boundary; on one core
* with byte-sized index traffic no lock is needed because the producer
* only writes head (+buf slot) and the consumer only writes tail. */
static char k_buf[KBD_BUF_SIZE];
static volatile uint8_t k_head;
static volatile uint8_t k_tail;
/* IRQ-only state: touched solely in keyboard_on_irq(), so plain. */
static uint8_t k_shift;
static uint8_t k_caps;
static uint8_t k_e0;
/* Queue helpers below; port I/O comes from io.h. */
/* Drop-newest on full: keeps the consumer's prefix intact for editing. */
static void enqueue(char c)
{
uint8_t next = (uint8_t)((k_head + 1) % KBD_BUF_SIZE);
if (next == k_tail) {
return;
}
k_buf[k_head] = c;
k_head = next;
}
void keyboard_init(void)
{
k_head = 0;
k_tail = 0;
k_shift = 0;
k_caps = 0;
k_e0 = 0;
#ifdef NADIR_HAS_IRQ_REG
/* Route vector 33 -> keyboard_on_irq(); without idt.h this stays
* caller duty (manual irq_register_handler call). */
irq_register_handler(1, keyboard_on_irq);
#endif
#ifdef NADIR_HAS_PIC
pic_unmask(1);
#endif
}
void keyboard_on_irq(void)
{
uint8_t code = inb(KBD_DATA);
if (code == SC_EXTENDED) {
k_e0 = 1;
return;
}
if (k_e0 != 0) {
/* Extended make/break: swallow, maps to 0. */
k_e0 = 0;
return;
}
/* only Shift release changes state the rest is ignored. */
if ((code & SC_RELEASE) != 0) {
uint8_t make = (uint8_t)(code & (uint8_t)~SC_RELEASE);
if (make == SC_LSHIFT || make == SC_RSHIFT) {
k_shift = 0;
}
return;
}
if (code == SC_LSHIFT || code == SC_RSHIFT) {
k_shift = 1;
return;
}
/* CapsLock toggles on press only */
if (code == SC_CAPS) {
k_caps = (uint8_t)(k_caps == 0 ? 1 : 0);
return;
}
if (code >= sizeof(k_unshifted)) {
return;
}
char base = k_unshifted[code];
if (base == '\0') {
return;
}
char out = base;
if (base >= 'a' && base <= 'z') {
/* Shift XOR CapsLock uppercases letters. */
if ((k_shift != 0) != (k_caps != 0)) {
out = (char)(base - 'a' + 'A');
}
} else if (k_shift != 0) {
char shifted = k_shifted[code];
if (shifted != '\0') {
out = shifted;
}
} else if (k_caps != 0) {
/* CapsLock alone must not shift digits/symbols */
}
enqueue(out);
}
int keyboard_has_key(void)
{
return k_head != k_tail;
}
char keyboard_dequeue(void)
{
if (k_head == k_tail) {
return 0;
}
char c = k_buf[k_tail];
k_tail = (uint8_t)((k_tail + 1) % KBD_BUF_SIZE);
return c;
}

90
kernel/arch/x86_64/pic.c Normal file
View file

@ -0,0 +1,90 @@
/* Nadir 8259 PIC driver: cascaded master/slave remap + EOI + masking.
*
* Runs in 64-bit long mode at Ring 0 (CPL 0) with the stage-2 GDT.
* Privileged instructions: `outb`/`inb` via io.h (trap #GP if CPL >
* IOPL); no `cli`/`sti`/`lidt`.
* Fault behavior: no CPU fault possible a wrong ICW order only
* misroutes IRQs.
*/
#include <stdint.h>
#include "io.h"
#include "pic.h"
/* Init control words: ICW1 starts init, ICW2 sets the vector base,
* ICW3 wires the cascade (master IRQ2 <-> slave identity 2), ICW4
* selects 8086 mode. io_wait() after each byte gives the slow 8259
* time to latch it. */
#define PIC_ICW1_INIT 0x11U
#define PIC_ICW3_MASTER_SLAVE_LINE 0x04U
#define PIC_ICW3_SLAVE_ID 0x02U
#define PIC_ICW4_8086 0x01U
void pic_remap(void)
{
/* WHY this exact order: the 8259 latches ICW2-4 only while in init
* mode, and the cascade identities must match or slave IRQs never
* reach the CPU. Mask-all at the end keeps devices quiet until
* their driver calls pic_unmask(). */
outb(PIC1_CMD, PIC_ICW1_INIT);
io_wait();
outb(PIC2_CMD, PIC_ICW1_INIT);
io_wait();
outb(PIC1_DATA, PIC_IRQ0_VECTOR);
io_wait();
outb(PIC2_DATA, PIC_IRQ8_VECTOR);
io_wait();
outb(PIC1_DATA, PIC_ICW3_MASTER_SLAVE_LINE);
io_wait();
outb(PIC2_DATA, PIC_ICW3_SLAVE_ID);
io_wait();
outb(PIC1_DATA, PIC_ICW4_8086);
io_wait();
outb(PIC2_DATA, PIC_ICW4_8086);
io_wait();
outb(PIC1_DATA, 0xFFU);
io_wait();
outb(PIC2_DATA, 0xFFU);
io_wait();
}
void pic_eoi(uint8_t irq)
{
if (irq >= PIC_IRQ_COUNT) {
return;
}
if (irq >= 8U) {
outb(PIC2_CMD, PIC_EOI);
}
outb(PIC1_CMD, PIC_EOI);
}
void pic_mask(uint8_t irq)
{
uint16_t port;
uint8_t mask;
if (irq >= PIC_IRQ_COUNT) {
return;
}
port = (irq < 8U) ? PIC1_DATA : PIC2_DATA;
mask = inb(port) | (uint8_t)(1U << (irq & 7U));
outb(port, mask);
}
void pic_unmask(uint8_t irq)
{
uint16_t port;
uint8_t mask;
if (irq >= PIC_IRQ_COUNT) {
return;
}
port = (irq < 8U) ? PIC1_DATA : PIC2_DATA;
mask = inb(port) & (uint8_t) ~(1U << (irq & 7U));
outb(port, mask);
}

95
kernel/arch/x86_64/pit.c Normal file
View file

@ -0,0 +1,95 @@
/* 8254 PIT timer driver (see pit.h).
*
* Programs channel 0 in mode 3 (square-wave generator) at 100Hz on IRQ0.
*
* Why 1193182Hz base: the PIT input clock is the IBM-PC crystal frequency
* (14.31818MHz / 12); every PC-compatible PIT ticks at this rate, so the
* divisor for N Hz is always 1193182 / N.
* Why mode 3: square-wave output stays high/low for half the period and
* retriggers the IRQ line cleanly each cycle; mode 2 (rate generator)
* emits narrow pulses that are fine too, but mode 3 is the conventional
* choice for periodic system ticks on channel 0.
* Why volatile + no locking: single-core kernel; s_ticks is written only
* in IRQ0 context and read in mainline code. `volatile` keeps the compiler
* from caching the counter across the hlt loop, and on x86-64 an aligned
* 64-bit load/store is atomic, so no spinlock is needed while IF gates the
* writer.
*/
#include <stdint.h>
#include "idt.h"
#include "io.h"
#include "pit.h"
#define PIT_CMD 0x43U
#define PIT_CH0 0x40U
/* Ch0, lobyte/hibyte access, mode 3 square-wave, binary (not BCD). */
#define PIT_CMD_CH0_MODE3 0x36U
/* Compile-time guard: a zero divisor would mean 65536 to the hardware and
* silently run at ~18Hz instead of PIT_HZ. Frequency is fixed on purpose. */
_Static_assert(PIT_DIVISOR != 0, "PIT divisor must not be zero");
_Static_assert(PIT_DIVISOR <= 0xFFFFU, "PIT divisor must fit in 16 bits");
/* Port I/O is owned by include/io.h (single owner); irq_register_handler
* is owned by idt.c via include/idt.h. */
/* Monotonic tick counter: written in IRQ context. */
static volatile uint64_t s_ticks;
/* pit_init is idempotent; re-entry after the first programming is a no-op. */
static uint8_t s_initialized;
void pit_init(void)
{
uint16_t divisor;
uint8_t lo;
uint8_t hi;
if (s_initialized != 0) {
return;
}
/* No cli/sti here on purpose: latch programming is two plain outb
* writes (safe with IF=0 at boot) and interrupt policy belongs to the
* caller (kmain integrator), which unmasks IRQ0 and stis afterwards. */
divisor = (uint16_t)PIT_DIVISOR;
lo = (uint8_t)(divisor & 0xFFU);
hi = (uint8_t)((divisor >> 8) & 0xFFU);
/* Latch order matters: command byte first selects ch0 + lobyte/hibyte
* mode, then the divisor follows low byte first, high byte second. */
outb(PIT_CMD, PIT_CMD_CH0_MODE3);
outb(PIT_CH0, lo);
outb(PIT_CH0, hi);
/* Strong idt.h registration: IRQ0 -> vector 32 wires pit_on_tick. */
irq_register_handler(PIT_IRQ, pit_on_tick);
s_initialized = 1;
}
void pit_on_tick(void)
{
/* Single increment async-safe, no logging or port I/O here. The PIC
* EOI is owned by the IRQ dispatcher not this driver. */
s_ticks++;
}
uint64_t pit_ticks(void)
{
return s_ticks;
}
void pit_sleep(uint64_t ticks)
{
uint64_t start = s_ticks;
/* hlt (not a spin loop) so the CPU sleeps until the next interrupt.
* Assumes IF=1 and IRQ0 unmasked; with IF=0 this would halt forever,
* which is why pit_init leaves interrupt policy to the caller. The
* unsigned delta keeps working if s_ticks wraps mid-sleep. */
while ((s_ticks - start) < ticks) {
__asm__ volatile("hlt");
}
}

View file

@ -0,0 +1,65 @@
/* COM1 16550 UART polling log (see serial.h). */
#include <stdint.h>
#include "io.h"
#include "serial.h"
/* Register offsets from the COM1 base. */
#define COM1_BASE 0x3F8U
#define COM_DATA 0x3F8U /* THR/RBR (DLAB=0) / divisor low (DLAB=1) */
#define COM_IER 0x3F9U /* interrupt enable / divisor high (DLAB=1) */
#define COM_FIFO 0x3FAU /* FIFO control */
#define COM_LCTL 0x3FBU /* line control + DLAB bit 7 */
#define COM_MCTL 0x3FCU /* modem control */
#define COM_LSR 0x3FDU /* line status: bit 5 = THR empty, bit 0 = RX ready */
#define LSR_TX_EMPTY 0x20U /* THR holds no byte: safe to write */
#define LSR_RX_READY 0x01U /* RBR holds a byte (reserved for future RX) */
/* Port I/O owned by include/io.h; no local inb/outb duplicates. */
void serial_init(void)
{
/* No UART interrupts: pure polling, so the 8259/IDT stays untouched. */
outb(COM_IER, 0x00);
/* DLAB on to program the divisor. */
outb(COM_LCTL, 0x80);
/* Divisor 3 = 115200/3 = 38400 baud. */
outb(COM_DATA, 0x03);
outb(COM_IER, 0x00);
/* 8N1: 8 bits, no parity, one stop bit; DLAB back off. */
outb(COM_LCTL, 0x03);
/* FIFO on, clear both queues, 14-byte threshold. */
outb(COM_FIFO, 0xC7);
/* DTR+RTS on, OUT2 on so the UART drives interrupts (unused). */
outb(COM_MCTL, 0x0B);
}
void serial_putchar(char c)
{
/* Spin until THR is empty; hangs only if hardware is absent/wedged,
* which is why callers gate on serial_ok() when unsure. */
while ((inb(COM_LSR) & LSR_TX_EMPTY) == 0) {
/* Poll. */
}
outb(COM_DATA, (uint8_t)c);
}
void serial_puts(const char *s)
{
if (s == 0) {
return;
}
while (*s != '\0') {
serial_putchar(*s);
s++;
}
}
int serial_ok(void)
{
/* An absent port reads back 0xFF (open bus); a real 16550 never
* reports all status bits set at once, so that means no UART. */
return inb(COM_LSR) != 0xFFU;
}

View file

@ -2,30 +2,157 @@
*
* Called once from the stage-2 entry stub after the CPU is in 64-bit long
* mode with a valid stack, zeroed BSS and identity-mapped memory. Runs at
* Ring 0 (CPL 0) interrupts disabled. Installs the exception IDT first
* so from then on a fault prints diagnostics instead of triple-faulting.
* It never returns; on return the caller halts the CPU.
* Ring 0 (CPL 0) with IF=0 on entry under the stage-2 GDT (CS 0x18);
* `-mno-red-zone` keeps IRQ pushes off the red zone so interrupt frames
* are safe. Installs the exception IDT first so from then on a fault
* prints diagnostics instead of triple-faulting. `sti` runs exactly once
* after every device is programmed; the main loop keeps IF=1 and sleeps
* in `hlt` (which wakes on each IRQ) — never `cli`-parks, or timer ticks
* and keystrokes would stop. It never returns; on return the caller halts.
*/
#include <stdint.h>
#include "console.h"
#include "idt.h"
#include "keyboard.h"
#include "pic.h"
#include "pit.h"
#include "serial.h"
/* A value that only comes out right if the CPU truly executes 64-bit
* arithmetic: every nibble survives the shifts above 32 bits. */
#define PROOF_VALUE 0x0123456789ABCDEFULL
/* Print an unsigned decimal via console_putchar (no libc printf). */
static void print_dec(uint64_t v)
{
char buf[20];
int n = 0;
if (v == 0) {
console_putchar('0');
return;
}
while (v != 0 && n < (int)sizeof(buf)) {
buf[n++] = (char)('0' + (v % 10U));
v /= 10U;
}
while (n > 0) {
n--;
console_putchar(buf[n]);
}
}
/* Mirror one echoed keystroke to COM1 when the UART looks present. */
static void serial_echo(char c, int ok)
{
char tmp[2];
if (ok == 0) {
return;
}
if (c == '\b') {
/* Erase on a serial terminal: back, blank, back. */
serial_puts("\b \b");
} else if (c == '\n') {
serial_puts("\r\n");
} else {
tmp[0] = c;
tmp[1] = '\0';
serial_puts(tmp);
}
}
void kmain(void)
{
int serial_present;
uint64_t last_uptime;
console_clear();
console_puts("Hello World from Nadir Kernel !\n");
console_puts("Running C code in 64-bit long mode (Ring 0).\n");
/* WHY this order: pic_remap must precede idt_install_irqs so IRQ
* gates land on valid remapped vectors 32-47 (pre-remap they would
* overlap CPU exceptions 0-15 and a tick would look like a fault);
* devices (serial/PIT/keyboard) are programmed before sti so no IRQ
* fires into a half-wired handler; pic_remap masks everything and
* each device opts in via pic_unmask, so stray lines stay silent. */
idt_init();
console_puts("IDT installed (vectors 0-31).\n");
pic_remap();
console_puts("PIC remapped (IRQ 0-15 -> vectors 32-47, masked).\n");
idt_install_irqs();
console_puts("IRQ gates installed (vectors 32-47).\n");
serial_init();
serial_present = serial_ok();
console_puts("Serial COM1 38400 8N1 ready.\n");
if (serial_present != 0) {
serial_puts("Hello World from Nadir Kernel !\n");
serial_puts("Serial COM1 38400 8N1 ready.\n");
}
pit_init();
console_puts("PIT 100Hz on IRQ0 ready.\n");
if (serial_present != 0) {
serial_puts("PIT 100Hz on IRQ0 ready.\n");
}
keyboard_init();
console_puts("Keyboard PS/2 IRQ1 ready.\n");
if (serial_present != 0) {
serial_puts("Keyboard PS/2 IRQ1 ready.\n");
}
/* Explicit opt-in for the two live lines; idempotent even though
* keyboard_init already unmasked IRQ1. */
pic_unmask(0);
pic_unmask(1);
__asm__ volatile("sti");
console_puts("Interrupts enabled (IRQs 0-1).\n");
if (serial_present != 0) {
serial_puts("Interrupts enabled (IRQs 0-1).\n");
}
console_puts("64-bit proof value: ");
console_puthex64(PROOF_VALUE);
console_puts("\nHalting.\n");
console_puts("\nHalting in hlt loop (timer + keyboard live).\n");
/* Main loop: hlt sleeps until the next IRQ (IF=1 throughout), then
* one poll pass prints uptime ~1/s and drains echoed keys. */
last_uptime = 0;
for (;;) {
uint64_t now;
__asm__ volatile("hlt");
now = pit_ticks();
if ((now - last_uptime) >= 100U) {
uint64_t secs = now / 100U;
uint64_t tenths = (now / 10U) % 10U;
last_uptime = now;
console_puts("uptime ");
print_dec(secs);
console_puts(".");
print_dec(tenths);
console_puts("s (ticks ");
console_puthex64(now);
console_puts(")\n");
if (serial_present != 0) {
serial_puts("uptime (see VGA for value)\n");
}
}
while (keyboard_has_key() != 0) {
char c = keyboard_dequeue();
/* console_putchar erases on '\b' and wraps on '\n'. */
console_putchar(c);
serial_echo(c, serial_present);
}
}
}