Compare commits

...

10 Commits

Author SHA1 Message Date
Chris Sanden
ac5e94cad5 Building final images and binaries 2026-04-20 12:36:07 +00:00
Chris
5f57b955aa finalising report 2026-04-20 00:24:59 +02:00
Teodor
d4d5665ddc Assignment 6 completed 2026-04-18 22:58:45 +00:00
Teodor
1840342765 Created a basic snake game (not using any OS features yet)v2 2026-04-16 22:41:23 +00:00
Teodor
b55d58bf76 Created a basic snake game (not using any OS features yet) 2026-04-16 22:40:21 +00:00
Chris
af1d383940 small notes edit 2026-04-16 13:43:37 +02:00
Teodor
21cc65beb1 Assignment 5 completed 2026-04-15 23:04:33 +00:00
Chris
1bef0f3bcb adding report fragment 2026-04-15 21:44:14 +02:00
Chris Sanden
9a213dcff2 finished PIT implementation 2026-04-15 15:49:11 +00:00
Chris Sanden
46a91312f4 updated gitignore 2026-04-15 12:53:38 +00:00
35 changed files with 1967 additions and 114 deletions

5
.gitignore vendored
View File

@@ -19,4 +19,7 @@ src/group_name/src/kernel.c
devcontainer.json
.devcontainer/devcontainer.json
docs/
assignment_files/
assignment_files/
build/OSDev_18/Makefile
build/OSDev_18/compile_commands.json
build/OSDev_18/cmake_install.cmake

View File

@@ -1,54 +0,0 @@
# Install script for directory: /workspaces/AdvOpSys/src/OSDev_18
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/local/bin/i686-elf-objdump")
endif()
if(CMAKE_INSTALL_COMPONENT)
set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt")
else()
set(CMAKE_INSTALL_MANIFEST "install_manifest.txt")
endif()
string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
"${CMAKE_INSTALL_MANIFEST_FILES}")
file(WRITE "/workspaces/AdvOpSys/build/OSDev_18/${CMAKE_INSTALL_MANIFEST}"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")

View File

@@ -1,27 +0,0 @@
[
{
"directory": "/workspaces/AdvOpSys/build/OSDev_18",
"command": "/usr/bin/nasm -I/workspaces/AdvOpSys/src/OSDev_18/include -f elf32 -o CMakeFiles/uiaos-kernel.dir/src/multiboot2.asm.o /workspaces/AdvOpSys/src/OSDev_18/src/multiboot2.asm",
"file": "/workspaces/AdvOpSys/src/OSDev_18/src/multiboot2.asm"
},
{
"directory": "/workspaces/AdvOpSys/build/OSDev_18",
"command": "/usr/bin/nasm -I/workspaces/AdvOpSys/src/OSDev_18/include -f elf32 -o CMakeFiles/uiaos-kernel.dir/src/arch/i386/gdt_flush.asm.o /workspaces/AdvOpSys/src/OSDev_18/src/arch/i386/gdt_flush.asm",
"file": "/workspaces/AdvOpSys/src/OSDev_18/src/arch/i386/gdt_flush.asm"
},
{
"directory": "/workspaces/AdvOpSys/build/OSDev_18",
"command": "/usr/local/bin/i686-elf-gcc -I/workspaces/AdvOpSys/src/OSDev_18/include -g -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-stack-protector -fno-stack-check -fno-lto -fPIE -m32 -march=i386 -mno-mmx -mno-sse -mno-sse2 -mno-red-zone -Wno-main -g -Wno-unused-variable -Wno-unused-parameter -std=gnu99 -o CMakeFiles/uiaos-kernel.dir/src/kernel.c.o -c /workspaces/AdvOpSys/src/OSDev_18/src/kernel.c",
"file": "/workspaces/AdvOpSys/src/OSDev_18/src/kernel.c"
},
{
"directory": "/workspaces/AdvOpSys/build/OSDev_18",
"command": "/usr/local/bin/i686-elf-gcc -I/workspaces/AdvOpSys/src/OSDev_18/include -g -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-stack-protector -fno-stack-check -fno-lto -fPIE -m32 -march=i386 -mno-mmx -mno-sse -mno-sse2 -mno-red-zone -Wno-main -g -Wno-unused-variable -Wno-unused-parameter -std=gnu99 -o CMakeFiles/uiaos-kernel.dir/src/gdt.c.o -c /workspaces/AdvOpSys/src/OSDev_18/src/gdt.c",
"file": "/workspaces/AdvOpSys/src/OSDev_18/src/gdt.c"
},
{
"directory": "/workspaces/AdvOpSys/build/OSDev_18",
"command": "/usr/local/bin/i686-elf-gcc -I/workspaces/AdvOpSys/src/OSDev_18/include -g -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-stack-protector -fno-stack-check -fno-lto -fPIE -m32 -march=i386 -mno-mmx -mno-sse -mno-sse2 -mno-red-zone -Wno-main -g -Wno-unused-variable -Wno-unused-parameter -std=gnu99 -o CMakeFiles/uiaos-kernel.dir/src/terminal.c.o -c /workspaces/AdvOpSys/src/OSDev_18/src/terminal.c",
"file": "/workspaces/AdvOpSys/src/OSDev_18/src/terminal.c"
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

118
notes/report/1.tex Normal file
View File

@@ -0,0 +1,118 @@
\section{Boot Process and Processor Setup}
The first stage of the project established a more complete early boot path for the kernel. Before this work, the kernel entered C code, initialized the VGA terminal, printed \texttt{Hello, World!}, and then remained in an infinite halt loop. The visible output remained the same after this stage, but the processor setup became more explicit and reliable.
The main change was the addition of a minimal Global Descriptor Table for the i386 kernel. Even though the kernel uses a flat memory model, protected mode still requires valid segment descriptors. The GDT therefore gives the processor valid code and data segment definitions before the kernel continues into higher-level initialization.
\subsection{Kernel Entry Flow}
After the GDT work, the early kernel flow became:
\begin{enumerate}
\item the bootloader transfers control to the kernel entry point,
\item the kernel begins execution in C,
\item the GDT is initialized and loaded,
\item segment registers are reloaded,
\item the VGA terminal is initialized,
\item \texttt{Hello, World!} is written to the screen,
\item the kernel enters an infinite halt loop.
\end{enumerate}
This keeps processor setup before terminal setup. Later stages build on this ordering by adding interrupts, memory management, timing, and applications after the low-level CPU state has been initialized.
\subsection{GDT Layout}
The implemented GDT contains three descriptors:
\begin{itemize}
\item a null descriptor,
\item a kernel code segment descriptor,
\item a kernel data segment descriptor.
\end{itemize}
The code and data segments both use base address \texttt{0x00000000}, a 4 GiB address span, 4 KiB granularity, and ring 0 privilege level. The code segment uses selector \texttt{0x08}, while the data segment uses selector \texttt{0x10}.
\begin{listing}[H]
\begin{minted}{c}
void GdtInitialize(void) {
gdtDescriptor.size = sizeof(gdtEntries) - 1;
gdtDescriptor.offset = (uint32_t)&gdtEntries;
GdtSetEntry(0, 0, 0, 0, 0);
GdtSetEntry(1, 0, 0x000FFFFF, 0x9A, 0xCF);
GdtSetEntry(2, 0, 0x000FFFFF, 0x92, 0xCF);
GdtFlush((uint32_t)&gdtDescriptor);
}
\end{minted}
\caption{Minimal GDT initialization with null, kernel code, and kernel data descriptors.}
\end{listing}
\subsection{Descriptor Construction}
The GDT setup is split between C and assembly. The C code builds the descriptor table and prepares the GDT descriptor that contains the table size and address. The helper that creates each descriptor splits the base and limit into the fields expected by the processor.
\begin{listing}[H]
\begin{minted}{c}
static void GdtSetEntry(
uint32_t index,
uint32_t base,
uint32_t limit,
uint8_t access,
uint8_t granularity
) {
gdtEntries[index].base_low = (uint16_t)(base & 0xFFFF);
gdtEntries[index].base_middle = (uint8_t)((base >> 16) & 0xFF);
gdtEntries[index].base_high = (uint8_t)((base >> 24) & 0xFF);
gdtEntries[index].limit_low = (uint16_t)(limit & 0xFFFF);
gdtEntries[index].granularity = (uint8_t)((limit >> 16) & 0x0F);
gdtEntries[index].granularity |= (uint8_t)(granularity & 0xF0);
gdtEntries[index].access = access;
}
\end{minted}
\caption{Building an x86 GDT descriptor from base, limit, access, and granularity fields.}
\end{listing}
\clearpage
\subsection{Reloading Segment Registers}
Loading the GDT with \texttt{lgdt} is not enough by itself. The CPU caches segment descriptor information in the segment registers, so the kernel must reload those registers after installing the new table. The assembly routine performs the architecture-specific part: it loads the GDT, performs a far jump to reload \texttt{cs}, and then reloads the data segment registers.
\begin{listing}[H]
\begin{minted}{nasm}
GdtFlush:
mov eax, [esp + 4]
lgdt [eax]
jmp GDT_CODE_SELECTOR:.reload_cs
.reload_cs:
mov ax, GDT_DATA_SELECTOR
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
ret
\end{minted}
\caption{Loading the GDT and refreshing the cached code and data segment registers.}
\end{listing}
\section{Build and Verification}
The build configuration was updated so that both the C GDT implementation and the assembly reload routine are compiled and linked into the kernel binary. New build artifacts were generated, including an updated kernel binary and bootable ISO image.
\subsection{Static Binary Inspection}
The generated kernel binary was inspected to confirm that the expected GDT setup sequence was present. The inspection showed a call to the GDT load routine, a far jump to selector \texttt{0x08}, and data segment reloads using selector \texttt{0x10}.
\subsection{Runtime Debugging}
The GDT setup was also verified in QEMU using \texttt{gdb-multiarch}. Execution was stopped inside the GDT initialization and reload path. The GDT descriptor limit was \texttt{0x17}, which matches three 8-byte entries minus one. After the far jump, \texttt{cs} contained \texttt{0x08}; after the reload instructions, \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss} contained \texttt{0x10}.
\section{State After Processor Setup}
At the end of this stage, the kernel:
\begin{itemize}
\item builds inside the development container,
\item includes a minimal i386 GDT implementation,
\item loads the GDT during startup,
\item reloads the code and data segment registers correctly,
\item still initializes the VGA terminal,
\item still prints \texttt{Hello, World!} after boot.
\end{itemize}
This stage did not add new user-facing behaviour. Its purpose was to make the early processor state explicit so later kernel subsystems could be added on top of a controlled protected-mode setup.

182
notes/report/2.tex Normal file
View File

@@ -0,0 +1,182 @@
\newpage
\section{Interrupt Handling}
The next stage added interrupt support to the kernel. This required an Interrupt Descriptor Table so the processor has defined entry points for CPU exceptions and hardware interrupts. The implementation follows the same model used by the OSDev Wiki material, the \texttt{os-tutorial} project, and the lecturer-provided assignment files \cite{osdevInterruptsTutorial,fenollosaOsTutorial,assignmentFiles}.
Interrupt support changed the kernel from a purely linear boot program into a system that can react to events while it is running. CPU exceptions are handled through ISR stubs, hardware interrupts are handled through IRQ stubs, and both paths eventually reach C handlers with a saved register state.
\subsection{IDT Descriptor Setup}
Each IDT entry stores the handler address, the kernel code segment selector, and the descriptor attributes used by the processor. The handler address is split into low and high parts because this is the layout expected by the 32-bit x86 IDT format.
\begin{listing}[H]
\begin{minted}{c}
void IdtSetDescriptor(uint8_t vector, uint32_t interrupt, uint8_t flags) {
struct IdtEntry* descriptor = &idt[vector];
descriptor->interrupt_low = (uint32_t)interrupt & 0xFFFF;
descriptor->kernel_cs = GDT_CODE_SELECTOR;
descriptor->attributes = flags;
descriptor->interrupt_high = (uint32_t)interrupt >> 16;
descriptor->reserved = 0;
}
\end{minted}
\caption{Constructing an IDT gate for a specific interrupt vector.}
\end{listing}
\subsection{IDT Initialization}
During initialization, the kernel fills vectors \texttt{0} through \texttt{31} with CPU exception stubs and vectors \texttt{32} through \texttt{47} with hardware IRQ stubs. This means the implementation provides ISR coverage for all 32 standard CPU exception vectors, which is more complete than only defining the minimum three handlers required for basic testing. The PIC is then remapped before the IDT is loaded with \texttt{lidt} and interrupts are enabled with \texttt{sti}.
\begin{listing}[H]
\begin{minted}{c}
void IdtInitialize(void) {
idtr.base = (uint32_t)&idt[0];
idtr.limit = (uint16_t)sizeof(struct IdtEntry) * IDT_ENTRIES - 1;
for (uint8_t iNum = 0; iNum < 32; iNum++) {
IdtSetDescriptor(iNum, (uint32_t)isr_stub_table[iNum], 0x8E);
}
for (uint8_t iNum = 32; iNum < 48; iNum++) {
IdtSetDescriptor(iNum, (uint32_t)irq_stub_table[iNum - 32], 0x8E);
}
PicRemap();
__asm__ volatile ("lidt %0" : : "m"(idtr));
__asm__ volatile ("sti");
}
\end{minted}
\caption{Initializing the IDT with CPU exception and hardware IRQ entries.}
\end{listing}
\clearpage
\section{CPU Exceptions}
Basic Interrupt Service Routines were implemented for CPU exceptions. The low-level stubs save the CPU state and pass a \texttt{Registers} structure into the shared C handler. This lets the kernel inspect which exception occurred and print useful debugging information to the terminal. Instead of each ISR containing its own separate print logic, the individual stubs share the same C handler, which prints the interrupt number and message for the triggered exception.
\begin{listing}[H]
\begin{minted}{c}
void IsrHandler(struct Registers* registers) {
TerminalWriteString("\n=== INTERRUPT RECEIVED ===\n");
TerminalWriteString("Interrupt Number: ");
TerminalWriteUInt(registers->int_no);
TerminalWriteString("\n");
TerminalWriteString("Message: ");
if (registers->int_no < 32) {
TerminalWriteString(isrMessages[registers->int_no]);
} else {
TerminalWriteString("Unknown Interrupt");
}
for (;;) {
__asm__ volatile("cli; hlt");
}
}
\end{minted}
\caption{Shared CPU exception handler using the saved interrupt number.}
\end{listing}
The handler stops the kernel after reporting the exception. This is appropriate for the current stage because exception recovery has not yet been implemented, and halting prevents the kernel from continuing after an undefined or unsafe processor state.
\section{Hardware Interrupts}
Hardware interrupt support was added for IRQ0 through IRQ15. These interrupts come from devices through the Programmable Interrupt Controller. The PIC is remapped so that hardware IRQs begin at vector \texttt{32} instead of overlapping with the CPU exception vectors.
\begin{listing}[H]
\begin{minted}{c}
void PicRemap(void) {
OutPortByte(PIC1_COMMAND, 0x11);
OutPortByte(PIC2_COMMAND, 0x11);
OutPortByte(PIC1_DATA, 0x20);
OutPortByte(PIC2_DATA, 0x28);
OutPortByte(PIC1_DATA, 0x04);
OutPortByte(PIC2_DATA, 0x02);
OutPortByte(PIC1_DATA, 0x01);
OutPortByte(PIC2_DATA, 0x01);
OutPortByte(PIC1_DATA, 0x00);
OutPortByte(PIC2_DATA, 0x00);
}
\end{minted}
\caption{Remapping the PIC so IRQs use interrupt vectors \texttt{32} through \texttt{47}.}
\end{listing}
The IRQ handler dispatches to a registered C handler when one exists. After the interrupt has been handled, the kernel sends an end-of-interrupt signal to the PIC. This tells the controller that the current interrupt has been processed and that new interrupts may be delivered.
\begin{listing}[H]
\begin{minted}{c}
void IrqHandler(struct Registers* registers) {
uint8_t irqNum = (uint8_t)(registers->int_no - 32);
if (interruptHandlers[registers->int_no] != 0) {
interruptHandlers[registers->int_no](registers);
} else if (irqNum != 0) {
TerminalWriteString("IRQ triggered: ");
TerminalWriteUInt(irqNum);
TerminalWriteString("\n");
}
PicSendEoi(irqNum);
}
\end{minted}
\caption{Dispatching hardware interrupts and acknowledging them with the PIC.}
\end{listing}
\clearpage
\section{Keyboard Input}
Keyboard support was implemented using IRQ1. The keyboard handler reads the scancode from port \texttt{0x60}, stores it in a small buffer, ignores key-release events, and translates key-press scancodes into ASCII through a lookup table.
The assignment describes the keyboard task as printing translated ASCII characters to the screen. In this project, the keyboard logic was extended slightly beyond a direct logger: the handler stores the latest translated ASCII character, and higher-level terminal or application code consumes it. This allows the same keyboard path to support the application menu and Snake input instead of only echoing characters immediately from the interrupt handler.
\begin{listing}[H]
\begin{minted}{c}
void KeyboardHandler(struct Registers* registers) {
(void) registers;
uint8_t scancode = InPortByte(KEYBOARD_DATA_PORT);
if (index < KEYBOARD_BUFFER_SIZE) {
keyboardBuffer[index] = scancode;
index++;
}
if (scancode & 0x80) {
return;
}
if (scancode < 128) {
char ascii = scancodeToAscii[scancode];
if (ascii != 0) {
lastKeyPressed = ascii;
}
}
}
\end{minted}
\caption{Keyboard IRQ handler reading scancodes and storing the latest ASCII key.}
\end{listing}
The most recent translated key can then be consumed by higher-level code through \texttt{GetLastKeyPressed()}. This becomes important in later stages where the application menu and Snake game need keyboard input without directly handling raw scancodes.
\section{State After Interrupt Work}
At the end of this stage, the kernel:
\begin{itemize}
\item initializes and loads an Interrupt Descriptor Table,
\item handles CPU exceptions through ISR support,
\item supports hardware IRQs from IRQ0 to IRQ15,
\item remaps the PIC so hardware interrupts do not overlap with CPU exceptions,
\item dispatches hardware interrupts to registered handlers,
\item sends end-of-interrupt signals after IRQ handling,
\item handles keyboard input through IRQ1,
\item reads keyboard scancodes from port \texttt{0x60},
\item translates key presses into ASCII characters,
\item stores the latest key for later application-level input.
\end{itemize}
This stage moved the kernel from static startup output to event-driven execution. The later PIT, music, menu, and Snake work all depend on this interrupt layer.

164
notes/report/3.tex Normal file
View File

@@ -0,0 +1,164 @@
\section{Memory Management}
The next stage added basic memory management to the kernel. This work introduced kernel heap initialization, paging, dynamic allocation, page-aligned allocation, and memory debugging output. Together these changes allow later kernel components to request memory dynamically instead of relying only on static storage.
\subsection{Kernel Heap Initialization}
The heap is initialized from the linker-provided end of the kernel image. The kernel stores this address in \texttt{last\_alloc}, then reserves a normal heap region below the page heap area. A separate page heap is placed below \texttt{0x400000}, with one descriptor byte per page-aligned allocation.
\begin{listing}[H]
\begin{minted}{c}
void InitKernelMemory(uint32_t* kernel_end) {
uint32_t kernelEndAddr = (uint32_t)kernel_end;
last_alloc = kernelEndAddr + 0x1000;
heap_begin = last_alloc;
pheap_end = 0x400000;
pheap_begin = pheap_end - (MAX_PAGE_ALIGNED_ALLOCS * 4096);
heap_end = pheap_begin;
memset((char*)heap_begin, 0, heap_end - heap_begin);
pheap_desc = (uint8_t *)malloc(MAX_PAGE_ALIGNED_ALLOCS);
}
\end{minted}
\caption{Kernel heap initialization using the linker-provided kernel end symbol.}
\end{listing}
This creates two allocation areas:
\begin{itemize}
\item a byte-addressed heap for normal \texttt{malloc()} allocations,
\item a page heap for 4 KiB page-aligned allocations through \texttt{pmalloc()}.
\end{itemize}
\subsection{Paging}
Paging was implemented using a page directory at \texttt{0x400000} and page tables starting at \texttt{0x404000}. The setup identity-maps the first 4 MiB of memory and the 4 MiB region beginning at \texttt{0x400000}. This keeps virtual addresses equal to physical addresses for the early kernel environment while still enabling the processor's paging mechanism.
The paging setup writes the page directory address to \texttt{cr3} and sets the paging bit in \texttt{cr0}. After that point, memory references are translated through the page tables.
\begin{listing}[H]
\begin{minted}{c}
void paging_enable() {
asm volatile("mov %%eax, %%cr3": :"a"(page_dir_loc));
asm volatile("mov %cr0, %eax");
asm volatile("orl $0x80000000, %eax");
asm volatile("mov %eax, %cr0");
}
\end{minted}
\caption{Enabling paging by loading \texttt{cr3} and setting the paging bit in \texttt{cr0}.}
\end{listing}
\subsection{Dynamic Allocation}
A simple heap allocator was implemented for \texttt{malloc()} and \texttt{free()}. Each allocation is preceded by a small metadata header containing the allocation status and size. New allocations advance \texttt{last\_alloc}, while freed blocks are marked as unused and may be reused by later allocations.
The allocator scans existing blocks before creating a new one. If it finds an unused block large enough for the request, that block is reactivated and returned. This was verified by freeing one allocation, requesting a smaller block, and observing that the allocator reused the same address.
\begin{listing}[H]
\begin{minted}{c}
if(a->size >= size) {
a->status = 1;
memset(mem + sizeof(alloc_t), 0, size);
memory_used += a->size + sizeof(alloc_t) + 4;
return (char*)(mem + sizeof(alloc_t));
}
\end{minted}
\caption{Reusing a freed heap block when it is large enough for a new allocation.}
\end{listing}
\subsection{Memory Debugging}
The kernel also gained memory debugging output. \texttt{PrintMemoryLayout()} prints the amount of memory used, the amount free, the heap size, and the start and end addresses of both the normal heap and page heap. \texttt{TerminalWriteHex()} was added so addresses can be printed in hexadecimal instead of being confused with decimal values.
\clearpage
\section{Programmable Interval Timer}
The Programmable Interval Timer was then added so the kernel can measure time and schedule delays. The PIT is configured on channel 0 at \texttt{1000 Hz}, so one timer tick corresponds approximately to one millisecond.
\subsection{PIT Initialization}
The PIT driver defines the command port, channel 0 port, base frequency, target frequency, divider, and tick conversion in \texttt{pit.h}. During initialization, the kernel registers an IRQ0 handler, programs the PIT command byte, and writes the divisor as low byte followed by high byte.
\begin{listing}[H]
\begin{minted}{c}
void PitInitialize(void){
uint16_t divisor = DIVIDER;
RegisterInterruptHandler(IRQ0, PitIrqHandler);
OutPortByte(PIT_CMD_PORT, 0x36);
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)(divisor & 0xFF));
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)((divisor >> 8) & 0xFF));
}
\end{minted}
\caption{PIT channel 0 initialization and IRQ0 handler registration.}
\end{listing}
The IRQ0 handler increments a \texttt{volatile} tick counter. Keeping the counter inside \texttt{pit.c} reduces the chance of accidental writes from unrelated kernel code.
\subsection{Sleep Functions}
Two sleep functions were implemented. \texttt{SleepBusy()} repeatedly checks the tick counter until enough time has passed. This is simple but consumes CPU time for the whole delay.
\texttt{SleepInterrupt()} uses \texttt{sti; hlt} inside the loop. This enables interrupts and halts the CPU until the next interrupt occurs. The loop then rechecks the current tick and halts again if the requested delay has not elapsed.
\begin{listing}[H]
\begin{minted}{c}
void SleepInterrupt(uint32_t ticks_to_wait){
uint32_t start_tick = GetCurrentTick();
uint32_t end_tick = start_tick + ticks_to_wait;
while(GetCurrentTick() < end_tick){
__asm__ volatile ("sti; hlt");
}
}
\end{minted}
\caption{Interrupt-based sleeping using the PIT tick counter and \texttt{hlt}.}
\end{listing}
\subsection{Verification}
Several integration issues were fixed during PIT work. Incorrect inline assembly syntax was corrected, terminal output functions replaced unsuitable \texttt{printf()} calls, and the missing \texttt{TerminalWriteHex()} declaration was added to \texttt{terminal.h}.
The build process also showed that rebuilding only \texttt{uiaos-kernel} updates \texttt{kernel.bin}, but does not update the bootable ISO image. Rebuilding \texttt{uiaos-create-image} was necessary before QEMU booted the newest kernel. Runtime testing then showed paging output, heap output, allocation output, and repeated sleep-test messages for both busy-wait and interrupt-based sleeping.
\section{PC Speaker Music Player}
Assignment 5 used the PIT and PC speaker to implement a simple music player. The PC speaker is controlled through I/O port \texttt{0x61}, while PIT channel 2 generates the square wave used for audible notes.
\subsection{Sound Generation}
The speaker is enabled by setting bits 0 and 1 on port \texttt{0x61}. To play a note, the kernel calculates a divisor from the PIT base frequency and the requested note frequency, programs PIT channel 2, and then enables the speaker.
\begin{listing}[H]
\begin{minted}{c}
void PlaySound(uint32_t frequency) {
if (!frequency) return;
uint32_t divisor = PIT_BASE_FREQ / frequency;
OutPortByte(PIT_CMD_PORT, 0xB6);
OutPortByte(PIT_CHANNEL2_PORT, divisor & 0xFF);
OutPortByte(PIT_CHANNEL2_PORT, (divisor >> 8) & 0xFF);
EnableSpeaker();
}
\end{minted}
\caption{Programming PIT channel 2 to generate a PC speaker tone.}
\end{listing}
\clearpage
\subsection{Song Representation and Playback}
Songs are represented as arrays of notes. Each note stores a frequency and duration in milliseconds. A rest is represented by frequency \texttt{R}, which is defined as \texttt{0}. Playback therefore consists of selecting a frequency, sleeping for the note duration, and then stopping the speaker.
The implementation uses \texttt{SleepInterrupt()} for note timing. This replaced busy-wait delays because the interrupt-based delay gave better timing and avoided wasting CPU cycles while each note was playing.
QEMU's PC speaker support made fast note changes unclear. To make playback more distinguishable, durations were doubled during playback.
\begin{listing}[H]
\begin{minted}{c}
if (currentNote.frequency == R) {
StopSound();
SleepInterrupt(currentNote.duration * 2);
} else {
PlaySound(currentNote.frequency);
SleepInterrupt(currentNote.duration * 2);
StopSound();
}
\end{minted}
\caption{Song playback using rests, PC speaker tones, and interrupt-based timing.}
\end{listing}
\clearpage

104
notes/report/4.tex Normal file
View File

@@ -0,0 +1,104 @@
\section{Application Framework and Snake}
The final stage integrated the individual OS components into a simple application framework. Instead of booting into a single test routine, the kernel now presents a terminal menu where the user can choose between the music player and Snake.
\subsection{Menu-Based Control Flow}
The kernel initialization path now sets up the terminal, GDT, IDT, PIT, keyboard interrupt handler, kernel memory, and paging. After that initialization, the kernel repeatedly clears the terminal, asks for an application number, and dispatches to either the music player or Snake.
\begin{listing}[H]
\begin{minted}{c}
while (1) {
TerminalClear();
TerminalWriteString("Enter application number (0 for music, 1 for snake): ");
char input = TerminalGetChar();
switch (input) {
case '0':
PlayMusic();
break;
case '1':
PlayGame();
break;
}
}
\end{minted}
\caption{Application menu dispatch in the kernel main loop.}
\end{listing}
This demonstrates that the terminal, keyboard, interrupt, timer, memory, and application code can cooperate through a single kernel control flow.
\clearpage
\subsection{Snake Game State}
Snake uses a dynamically allocated game state. The game state contains the board, snake, food, score, and pseudo-random state. Creating and destroying the game therefore exercises the kernel's \texttt{malloc()} and \texttt{free()} implementation in a more realistic setting than isolated allocation tests.
\begin{listing}[H]
\begin{minted}{c}
struct GameState* CreateGame(void) {
struct GameState* game = (struct GameState*)malloc(sizeof(struct GameState));
if (!game) return 0;
game->snake = (struct Snake*)malloc(sizeof(struct Snake));
if (!game->snake) {
free(game);
return 0;
}
game->food = (struct Food*)malloc(sizeof(struct Food));
if (!game->food) {
free(game->snake);
free(game);
return 0;
}
game->score = 0;
game->rngState = GetCurrentTick();
return game;
}
\end{minted}
\caption{Snake creates its game objects using the kernel heap allocator.}
\end{listing}
\clearpage
\subsection{Input, Timing, and Feedback}
The Snake loop reads the last key pressed by the keyboard handler, updates the snake direction, moves the snake, checks collisions, redraws the board, and then sleeps using the PIT. The game uses \texttt{GAME\_SPEED\_MS} to control pacing.
Sound is also integrated into the game. Eating food, dying, and winning each trigger short PC speaker effects. This connects the application layer back to the PIT and PC speaker support implemented earlier.
\begin{listing}[H]
\begin{minted}{c}
input = GetLastKeyPressed();
HandleInput(game, input);
tail = MoveSnake(game->snake);
collisionType = CheckCollision(game->snake, game->food);
if (collisionType == FOOD) {
game->score++;
PlayFoodSound();
AddSegment(game->snake, tail.x, tail.y);
SpawnFood(game);
}
DrawBoard(game);
SleepInterrupt(GAME_SPEED_MS);
\end{minted}
\caption{Main Snake loop combining keyboard input, game state, sound, drawing, and PIT timing.}
\end{listing}
\clearpage
\section{Final State of the Project}
At the end of the documented work, the kernel contains:
\begin{itemize}
\item early GDT setup and segment register reloading,
\item IDT, ISR, IRQ, PIC, and keyboard interrupt support,
\item kernel heap initialization and simple dynamic allocation,
\item identity-mapped paging for the early kernel address space,
\item PIT-based timer ticks and sleep functions,
\item PC speaker sound generation through PIT channel 2,
\item a menu-driven application flow,
\item a music player application,
\item an interactive Snake game using memory allocation, keyboard input, timing, terminal drawing, and sound feedback.
\end{itemize}
The project therefore moved from a minimal booting kernel that printed static text into a small interactive operating-system environment with hardware interrupts, timing, memory management, terminal input, sound, and application-level control flow.

19
notes/report/bib.tex Normal file
View File

@@ -0,0 +1,19 @@
@misc{osdevInterruptsTutorial,
author = {{OSDev Wiki}},
title = {{Interrupts Tutorial}},
howpublished = {\url{https://wiki.osdev.org/Interrupts_Tutorial}},
note = {Accessed 2026-04-16}
}
@misc{fenollosaOsTutorial,
author = {Carlos Fenollosa},
title = {{os-tutorial}},
howpublished = {\url{https://github.com/cfenollosa/os-tutorial/tree/master}},
note = {Accessed 2026-04-16}
}
@misc{assignmentFiles,
author = {{Course lecturer}},
title = {{assignment\_files.zip}},
note = {Source code and assignment material provided by the lecturer}
}

View File

@@ -0,0 +1,101 @@
\section{Boot Process Background}
This section gives background context for the boot process before the kernel-specific implementation described in the rest of the report. It covers the firmware stages, bootloader responsibilities, memory layout, and the effect of virtualization on booting.
\subsection{Power-On Self-Test}
When a computer is powered on or reset, the processor begins execution from a firmware-defined reset vector. At this point, the operating system is not running yet. The first responsibility belongs to the system firmware, either BIOS on older x86 systems or UEFI on newer systems.
One of the earliest firmware tasks is the Power-On Self-Test, usually shortened to POST. POST checks that the basic hardware needed to continue booting is present and responding. This includes components such as:
\begin{itemize}
\item the CPU,
\item system memory,
\item firmware storage,
\item video output,
\item keyboard or input controllers,
\item storage controllers and bootable devices.
\end{itemize}
POST is important because later boot stages assume that the basic machine state is usable. If a required device fails or memory cannot be initialized, the firmware may stop the boot process and report the failure through screen output, status LEDs, or beep codes.
The interaction between POST and hardware is therefore direct and low-level. Firmware initializes chipset state, configures memory controllers, discovers attached devices, and prepares enough of the platform that a bootloader can be found and executed. This is different from normal operating-system hardware management, because POST happens before the OS has loaded drivers or enabled its own abstractions.
\subsection{Boot Sequence After POST}
After a successful POST, the firmware chooses a boot device according to its configured boot order. On BIOS systems, this usually means reading the first sector of a bootable disk into memory and jumping to it. On UEFI systems, the firmware instead loads an EFI application from a filesystem on the EFI System Partition.
The high-level sequence after POST is:
\begin{enumerate}
\item firmware completes hardware initialization,
\item firmware selects a boot device,
\item the first bootloader stage or EFI boot application is loaded,
\item the bootloader prepares the environment expected by the kernel,
\item the kernel image is loaded into memory,
\item control is transferred to the kernel entry point.
\end{enumerate}
In a BIOS boot flow, the first loaded code is small because the initial boot sector is limited to 512 bytes. This usually forces the bootloader to use multiple stages. The first stage is responsible for loading a larger second stage, and the second stage can then parse filesystems, load the kernel, and prepare boot information.
In a UEFI boot flow, the firmware provides more services before the operating system starts. A UEFI bootloader can use firmware-provided filesystem and device services, and it is loaded as a normal executable rather than as raw code from a fixed disk sector. This makes UEFI bootloaders more flexible, but the kernel must still eventually take control and stop depending on firmware runtime assumptions.
\subsection{Bootloaders}
A bootloader is the bridge between firmware and the operating-system kernel. Its purpose is not only to start the kernel, but also to place the machine into a state the kernel understands.
Common bootloader responsibilities include:
\begin{itemize}
\item locating and loading the kernel image,
\item loading additional modules or initrd files,
\item obtaining a memory map from the firmware,
\item selecting or setting a graphics mode,
\item preparing boot information structures,
\item switching CPU mode when required,
\item transferring control to the kernel entry point.
\end{itemize}
Different bootloaders provide different levels of support. GRUB is a general-purpose bootloader with filesystem support, configuration files, multiboot support, and broad hardware compatibility. Limine is a modern boot protocol and bootloader often used in hobby OS development because it provides a clear boot protocol and supports both BIOS and UEFI systems. A manually implemented bootloader gives full control over the boot path, but requires more work and exposes the project to more early-stage hardware and filesystem details.
The choice of bootloader depends on the goals of the operating-system project. For a kernel-focused project, using an existing bootloader is usually the most practical choice because it avoids spending most of the work on disk loading, filesystem parsing, firmware differences, and CPU mode transitions. For a project specifically about bootstrapping, implementing a bootloader manually can be useful because it exposes the details normally hidden by existing tools.
Manual bootloader implementation is challenging because the environment is very limited. In a BIOS first-stage bootloader, code size is constrained, there is no standard library, only a small amount of state is initialized, and disk access must use firmware interrupts or direct hardware access. The bootloader must also be careful about where it places itself, the kernel, stacks, tables, and temporary buffers in memory.
\subsection{Memory Layout in the Boot Process}
In a traditional i386 BIOS boot process, early memory layout is constrained by legacy conventions. The first MiB of memory contains several important regions, including the interrupt vector table, BIOS data area, conventional memory, video memory, and firmware regions.
The bootloader and kernel must avoid overwriting memory that is already used by firmware, hardware mappings, or the bootloader itself. A common simple kernel load address is \texttt{0x10000}. This address is above the lowest BIOS data structures and gives the kernel more room than the original boot sector area, while still being within conventional memory that is easy to access in early boot stages.
Starting near \texttt{0x10000} has practical implications:
\begin{itemize}
\item the bootloader needs to copy or load the kernel to a known address,
\item the kernel linker script must match the address where the kernel expects to run,
\item early stacks and temporary buffers must be placed so they do not overlap the kernel,
\item later memory management must identify which regions are already occupied.
\end{itemize}
Once the kernel has control, it can replace this simple early memory model with its own memory management. In this project, that later stage is represented by kernel heap initialization, page-aligned allocation, and paging setup.
\subsection{Boot Process in Modern Operating Systems}
Modern operating systems use more complex boot processes than small teaching kernels, but the same basic idea remains: firmware initializes the platform, a bootloader or boot manager loads the kernel, and the kernel takes control of the machine.
Linux systems commonly use firmware to start a bootloader such as GRUB or systemd-boot. The bootloader loads the kernel and an initramfs, passes a command line and boot information, and then transfers control to the Linux kernel. Windows systems use Windows Boot Manager, which loads the Windows OS loader before the kernel and core system components are started. macOS uses Apple's boot chain, which is tightly integrated with Apple hardware, APFS, Secure Boot policies, and system volume verification.
Modern boot processes have changed because hardware and security requirements have changed. UEFI replaced many BIOS conventions, disks moved from MBR partitioning toward GPT, and Secure Boot introduced signature verification into the boot chain. Storage devices are also more complex than older BIOS-era disks, and modern systems often need early support for NVMe, encryption, graphics initialization, and platform security features.
The result is that modern booting is both more capable and more controlled. Firmware and bootloaders provide richer services, but the operating system must also participate in a stricter trust chain and handle more platform variation.
\subsection{Virtual Machines and Booting}
Booting inside a virtual machine follows the same conceptual stages as booting on physical hardware, but the hardware being initialized is virtual. The guest operating system still sees firmware, CPU state, memory, storage devices, timers, and interrupt controllers. However, those devices are provided or emulated by the hypervisor.
The main difference is that a virtual machine does not start from real motherboard hardware. Instead, the hypervisor creates a virtual hardware environment and then starts the guest firmware inside it. The guest firmware performs a boot sequence that looks normal from inside the VM, but many device operations are handled by the hypervisor on the host.
The hypervisor has several roles in this process:
\begin{itemize}
\item allocating guest memory,
\item exposing virtual CPUs,
\item providing virtual disks and network devices,
\item emulating or virtualizing interrupt controllers and timers,
\item presenting BIOS or UEFI firmware to the guest,
\item handling privileged operations that cannot run directly on the host CPU.
\end{itemize}
Virtualized booting has advantages for operating-system development. It is faster to test than rebooting physical hardware, the virtual machine can be reset easily, debugging is easier with tools such as QEMU and GDB, and hardware behaviour is more reproducible. This is why the kernel in this project was tested through QEMU rather than directly on physical hardware.
There are also limitations. Emulated hardware may not behave exactly like real hardware, and some devices are simplified compared to physical machines. For example, PC speaker output in QEMU can be limited, which affected the clarity of fast note changes in the music player. Even so, virtualization is highly useful for kernel development because it provides a controlled environment for testing boot, interrupts, memory management, and device interaction.

View File

@@ -0,0 +1,14 @@
\section{Conclusion}
The project started with a minimal bootable kernel and developed it into a small interactive operating-system environment. The final kernel includes early processor setup, interrupt handling, memory management, PIT-based timing, PC speaker sound, keyboard input, terminal output, a menu system, a music player, and a Snake game.
The first major step was to make the processor setup explicit by adding a minimal Global Descriptor Table and reloading the segment registers correctly. This provided a controlled protected-mode foundation for later work. Interrupt handling then extended the kernel from linear startup code into an event-driven system, with CPU exceptions, hardware IRQs, PIC remapping, and keyboard input.
Memory management and paging added another important layer. The kernel gained heap initialization, dynamic allocation, page-aligned allocation, memory utility functions, and identity-mapped paging. These features made it possible for later code to allocate state dynamically rather than relying only on static data.
The PIT then provided a timing source through IRQ0. This supported both busy-wait and interrupt-based sleeping, with the interrupt-based version becoming important for later features. The same timing infrastructure was reused by the music player and Snake game. The PC speaker work showed how PIT channel 2 could be used for sound generation, while also showing the practical limits of testing audio through QEMU.
The final application framework tied the lower-level pieces together. The menu system used terminal output and keyboard input to choose between applications. The music player used PIT timing and PC speaker output. Snake combined memory allocation, keyboard input, PIT-based pacing, terminal drawing, and sound effects. This made the final application stage a practical test of whether the kernel subsystems could work together.
Overall, the project demonstrates how small operating-system mechanisms build on each other. GDT setup makes the early CPU state reliable, interrupts make asynchronous hardware events usable, memory management allows dynamic state, timing enables delays and pacing, and device output makes applications interactive. The result is not a complete general-purpose operating system, but it is a working kernel environment that shows the relationship between low-level hardware control and higher-level program behaviour.

11
notes/report/intro.tex Normal file
View File

@@ -0,0 +1,11 @@
\section{Introduction}
This report documents both the boot-process background behind operating-system startup and the development of a small 32-bit operating-system kernel for the \texttt{OSDev\_18} project. It starts by explaining the firmware and bootloader stages that happen before a kernel runs, then follows the project implementation from a minimal bootable kernel into a more interactive environment with processor setup, interrupts, memory management, timing, sound, keyboard input, and simple applications.
The background section covers the general boot sequence: Power-On Self-Test, firmware handoff, BIOS and UEFI differences, bootloader responsibilities, early i386 memory layout, modern operating-system boot chains, and the role of virtualization. This gives context for why the project can rely on a bootloader, why the kernel starts from a constrained early machine state, and why QEMU is useful for testing this kind of low-level work.
The implementation itself is intentionally low-level. Most of the kernel interacts directly with x86 processor structures and hardware interfaces, including the Global Descriptor Table, Interrupt Descriptor Table, Programmable Interrupt Controller, Programmable Interval Timer, VGA text output, keyboard controller, and PC speaker. Because the kernel runs in a freestanding environment, basic facilities that are normally provided by an operating system or standard library also had to be implemented inside the project.
After the boot background, the report follows the same order as the implementation work. The first implementation part covers the early boot path and processor setup, with particular focus on the Global Descriptor Table and the need to reload segment registers after installing it. The second part describes interrupt handling, including CPU exceptions, hardware IRQs, PIC remapping, and keyboard input. The third part introduces memory management, paging, PIT-based timing, sleep functions, and PC speaker music playback. The final part shows how these individual subsystems were combined into a small menu-driven application environment with a music player and an interactive Snake game.
The overall goal was not to build a complete general-purpose operating system, but to demonstrate how core operating-system mechanisms fit together. Each stage adds a specific capability, and later stages reuse earlier ones: the PIT depends on interrupt handling, the music player depends on PIT timing and PC speaker output, and Snake depends on memory allocation, keyboard input, terminal drawing, timing, and sound feedback. By the end of the project, the kernel has moved from static startup output to an event-driven system that can run simple interactive applications.

41
notes/report/problems.tex Normal file
View File

@@ -0,0 +1,41 @@
\section{Problems and Challenges}
Several issues appeared during the project because the kernel runs in a freestanding environment where there is little separation between build configuration, CPU state, hardware programming, and application behaviour. Most problems were not isolated to one source file; they came from interactions between the boot image, low-level initialization, interrupts, timing, and terminal output.
\subsection{Processor State and Verification}
The GDT implementation required careful verification because a mistake in this stage can prevent the kernel from continuing at all. One important detail was that loading the GDT with \texttt{lgdt} does not automatically update the cached segment registers. The implementation therefore needed a far jump to reload \texttt{cs}, followed by explicit reloads of \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss}.
This was verified both statically and at runtime. Static inspection confirmed that the kernel binary contained the expected GDT load sequence, far jump, and data segment reloads. Runtime debugging in QEMU with \texttt{gdb-multiarch} confirmed that \texttt{cs} became \texttt{0x08} and that the data segment registers became \texttt{0x10}. This was necessary because the code could compile correctly while still failing if the processor did not actually enter the expected segment state.
\subsection{Build and Boot Image Mismatch}
A major integration problem appeared during the PIT work. Rebuilding only the kernel target updated \texttt{kernel.bin}, but did not update the bootable ISO image. As a result, QEMU could still boot an older image even after the source code and kernel binary had changed.
This made the runtime output misleading. The newer \texttt{kernel.bin} contained PIT and memory-management strings, while the older \texttt{kernel.iso} still showed the earlier \texttt{Hello, World!} output. The issue was resolved by rebuilding the bootable image target so the ISO contained the updated kernel.
This problem showed that kernel development requires verifying not only the source code, but also the exact artifact being booted. When the build system produces multiple outputs, it is possible to test the wrong one without noticing immediately.
\subsection{Freestanding C and Header Consistency}
Because the kernel does not run with a normal hosted C environment, ordinary library assumptions do not always apply. During the PIT integration, \texttt{printf()} calls had to be removed from test code and replaced with the kernel's terminal output functions. The terminal interface also needed to expose \texttt{TerminalWriteHex()} through the header because the function was implemented in \texttt{terminal.c} but not declared for other modules.
Inline assembly syntax also caused a build issue. An incorrect form of \texttt{\_\_asm\_\_volatile} had to be corrected to \texttt{\_\_asm\_\_ volatile}. This was a small syntax problem, but in low-level code it blocked the PIT sleep implementation because \texttt{sti; hlt} was part of the interrupt-based delay loop.
\subsection{Interrupt Dependencies}
Several later features depended on interrupt handling being correct. The PIT relies on IRQ0, keyboard input relies on IRQ1, and both the music player and Snake depend on timer and keyboard behaviour. This meant that interrupt setup was not only an isolated assignment task; it became part of the foundation for the rest of the project.
The PIC also had to be remapped so hardware IRQs did not overlap with CPU exception vectors. Without this separation, hardware events and processor exceptions would be harder to distinguish, and debugging later features would become much less reliable.
\subsection{Terminal Output Limitations}
The terminal output implementation was sufficient for early debugging and application display, but repeated output exposed a limitation. During the PIT sleep test, once the VGA text buffer wrapped back to the top of the screen, new text overwrote old text directly. This made long-running debug output harder to read.
The later application framework reduced this issue by clearing the terminal before drawing menus and game frames. However, the underlying limitation remains: the terminal would benefit from proper scrolling or a cleaner screen-management model for continuous logs.
\subsection{PC Speaker and QEMU Limitations}
The music player used the PC speaker and PIT channel 2 to generate square wave tones. The implementation worked, but QEMU's PC speaker support limited the clarity of playback. Fast note changes could sound unclear, and songs degraded when played at the original speed.
The practical adjustment was to double note durations during playback. This slowed down the music and made individual notes easier to distinguish. The limitation was therefore not mainly in the note representation or PIT timing logic, but in the quality and behaviour of the emulated sound output.
\subsection{Subsystem Integration}
The final Snake application exposed the main integration challenge of the project. Snake uses dynamic memory allocation for game state, keyboard interrupts for input, PIT timing for movement speed, terminal output for drawing, and PC speaker output for feedback. A problem in any of these lower-level systems can appear as an application-level bug.
This made the final stage useful as more than a demonstration. It acted as an integration test for the kernel. The menu and Snake game showed that the separate subsystems could cooperate through a single control flow, but they also made clear that kernel features must be built in a stable order: processor setup first, then interrupts, then memory and timing, then application behaviour.

View File

@@ -0,0 +1,4 @@
Used for assignments 3, 4, and 5
https://wiki.osdev.org/Interrupts_Tutorial
https://github.com/cfenollosa/os-tutorial/tree/master
source code given by lecturer in `assignment_files.zip`

View File

@@ -0,0 +1,81 @@
# Assignment 5
## #Overview
This assignment implements a simple music player using the **PC speaker (PCSPK)** and the **Programmable Interval Timer (PIT)**.
Songs are played by generating frequencies and controlling timing between notes.
---
## #HowItWorks
### #PCSpeaker
The PC speaker is controlled through **port `0x61`**:
- Bits 0 and 1 enable/disable sound output
---
### #PIT
The PIT is used for:
#### #SoundGeneration
- Channel 2 generates square wave audio
- Frequency is set using:
divisor = PIT_BASE_FREQ / frequency
#### #Timing
- Channel 0 runs at ~1000 Hz
- A tick counter is incremented via interrupts
- `SleepInterrupt()` is used for accurate note timing
---
## #Implementation
### #Playback
Each note contains:
- Frequency (Hz)
- Duration (ms)
Playback works by:
1. Setting frequency with `PlaySound()`
2. Waiting using `SleepInterrupt()`
3. Stopping sound with `StopSound()`
Rests are handled using `R = 0`.
---
## #Challenges
### #Timing
Busy-wait delays caused incorrect timing.
This was solved by using interrupt-based sleeping.
### #QEMU
QEMU has limited PC speaker support:
- Fast note changes sound unclear
- Songs degrade at high speed
---
## #Adjustments
To improve playback clarity:
duration = original_duration * 2
This slows down the songs and makes notes more distinguishable.
---
## #Conclusion
The system successfully:
- Generates sound using the PC speaker
- Uses PIT for timing and frequency control
- Plays songs with multiple notes
Limitations in QEMU affect sound quality, but the implementation itself works as i

View File

@@ -0,0 +1,221 @@
# Assignment 4 Part 2: Programmable Interval Timer
## Overview
In this part of the assignment, work was done on adding a basic driver for the Programmable Interval Timer (PIT) to the `OSDev_18` kernel. The goal was to initialize the PIT, count timer ticks using IRQ0, and use those ticks to support both busy-wait sleeping and interrupt-based sleeping.
The PIT was configured to use channel 0 and generate regular timer interrupts at `1000 Hz`. This gives a simple timing model where one timer tick is approximately equal to one millisecond.
---
## Implemented Features
### PIT Header and Constants
- Added:
```c
include/kernel/pit.h
```
- Defined constants for:
- PIT command port
- PIT channel 0 port
- PIT base frequency
- target frequency
- calculated divider
- ticks-per-millisecond conversion
This provides a single place for PIT-related configuration and function declarations.
---
### PIT Initialization
- Implemented:
```c
void PitInitialize(void);
```
- Configures the PIT using:
```c
OutPortByte(PIT_CMD_PORT, 0x36);
```
- Loads the PIT divisor into channel 0:
- low byte first
- high byte second
- Registers the PIT interrupt handler on:
```c
IRQ0
```
This allows the kernel to start receiving regular timer interrupts from the PIT.
---
### Tick Counter
- Added a global tick counter:
```c
static volatile uint32_t pit_ticks = 0;
```
- Implemented a helper function:
```c
static uint32_t GetCurrentTick(void);
```
The tick counter is incremented every time IRQ0 is triggered. Since the PIT is configured to `1000 Hz`, this gives a simple way to measure elapsed time in milliseconds.
---
### PIT IRQ Handler
- Implemented:
```c
static void PitIrqHandler(struct Registers* regs);
```
- The handler:
- ignores the register argument
- increments `pit_ticks`
This keeps the timer interrupt path minimal and lets the rest of the kernel measure time by reading the current tick count.
---
### Busy-Wait Sleep
- Implemented:
```c
void SleepBusy(uint32_t milliseconds);
```
- Uses the current tick count to wait until enough time has passed
- Continuously checks the tick counter in a loop
This version works by actively spinning on the CPU, so it is simple but inefficient.
---
### Interrupt-Based Sleep
- Implemented:
```c
void SleepInterrupt(uint32_t ticks);
```
- Uses:
```c
__asm__ volatile ("sti; hlt");
```
inside a loop
- Re-checks the current tick count after each wake-up
This version is more efficient than busy waiting because the CPU is halted between interrupts instead of constantly polling in a tight loop.
---
### Kernel Integration
- Added:
```c
#include <kernel/pit.h>
```
to `kernel.c`
- Added:
```c
PitInitialize();
```
during kernel startup
- Added:
```c
SleepTest();
```
after the existing memory-allocation test code
This connects the PIT driver to the boot flow so the timer logic is available after interrupt setup is complete.
---
### Build System Update
- Added:
```c
src/pit.c
```
to `CMakeLists.txt`
This ensures the PIT implementation is compiled and linked into the kernel binary together with the rest of the source files.
---
### Terminal Output Fix
- Added the missing declaration for:
```c
void TerminalWriteHex(uint32_t num);
```
to `terminal.h`
This was necessary because `TerminalWriteHex()` was already implemented in `terminal.c` and used in several source files, but it was not exposed through the header.
---
## Testing & Verification
### Build Issues Resolved
During integration, several issues were found and corrected:
- incorrect inline assembly syntax:
```c
__asm__volatile
```
was corrected to:
```c
__asm__ volatile
```
- `printf()` calls were removed from PIT test code and replaced with terminal output functions
- the missing `TerminalWriteHex()` declaration in `terminal.h` was added
These changes allowed the kernel to build again with the PIT source included.
---
### Boot Image Verification
- Verified that rebuilding only:
```c
uiaos-kernel
```
updates `kernel.bin`, but does not update the bootable ISO image
- Confirmed that:
- the new `kernel.bin` contained the new PIT- and memory-related strings
- the older `kernel.iso` still contained `Hello, World!`
- Rebuilt the image using:
```c
uiaos-create-image
```
so QEMU would boot the updated kernel
This explained why old output was still appearing even after the source code had changed.
---
### Runtime Result
After rebuilding the bootable image, the kernel successfully booted into the updated code and displayed:
- paging setup output
- heap and allocation debug output
- the `memory1`, `memory2`, `memory3`, and `memory4` address prints
This confirms that the updated kernel binary is now being booted correctly.
The PIT sleep test loop was also verified at runtime. The kernel repeatedly prints the expected sleep messages for both:
- busy-wait sleeping
- interrupt-based sleeping
This shows that the PIT tick counter is advancing and that both sleep functions return as expected during execution.
During this test, one remaining issue was observed in the terminal output: once the VGA text buffer wraps back to the top of the screen, new text overwrites old text directly. A cleanup or scrolling improvement for the terminal was noted as a TODO in `kernel.c`.
---
## Conclusion
- Added a PIT driver skeleton to the kernel
- Configured PIT channel 0 for periodic timer interrupts
- Registered an IRQ0 handler and added a global tick counter
- Implemented both busy-wait and interrupt-based sleep functions
- Integrated PIT support into `kernel.c`
- Added `pit.c` to the build system
- Fixed the missing `TerminalWriteHex()` declaration
- Verified that the updated kernel is now booting from the rebuilt ISO image
- Verified that the PIT sleep test loop runs at runtime
This means the code structure for Part 2 is now present in the kernel and has been tested at runtime. The next step is to improve terminal behavior so repeated output does not overwrite earlier lines when the screen wraps.
---
## Next Step
Improve the terminal output behavior so repeated sleep-test messages scroll or clear cleanly instead of overwriting text when the screen wraps back to the top.

View File

@@ -0,0 +1,7 @@
I developed a simple application framework inside my OS by creating a menu system that lets the user switch between applications, currently music and Snake. I implemented a basic terminal input method using keyboard interrupts so the user can select an application from the menu and return back to it after use. This demonstrates integration between screen output, keyboard handling, and overall OS control flow.
For the Snake application, I designed a full game loop with its own game state, including the snake, food, score, and board representation. I used dynamic memory allocation to create and destroy the game state, which shows practical use of memory management in my OS. The game uses PIT/timing to control movement speed and game pacing, while keyboard input is used in real time to change direction, restart, or quit.
I also integrated sound through the PC speaker to give feedback for events such as eating food, dying, and winning. Overall, this task shows integration of multiple OS components, real-time system programming, practical application of operating system concepts, and creative problem-solving through building an interactive menu-driven game environment.
Finally everything is printed to the screen using existing TerminalWrite...(); infrastructure.

View File

@@ -70,6 +70,9 @@ add_executable(uiaos-kernel
src/malloc.c
src/memutils.c
src/memory.c
src/pit.c
src/songPlayer.c
src/snake.c
)

View File

@@ -6,6 +6,8 @@
#define KEYBOARD_DATA_PORT 0x60
#define KEYBOARD_BUFFER_SIZE 256
char GetLastKeyPressed(void);
void KeyboardHandler(struct Registers* registers);
#endif

View File

@@ -8,9 +8,9 @@ typedef struct {
uint32_t size;
} alloc_t;
void init_kernel_memory(uint32_t* kernel_end);
void InitKernelMemory(uint32_t* kernel_end);
void init_paging(void);
void InitPaging(void);
void paging_map_virtual_to_phys(uint32_t virt, uint32_t phys);
char* pmalloc(size_t size);
@@ -21,6 +21,6 @@ void* memcpy(void* dest, const void* src, size_t num);
void* memset (void* ptr, int value, size_t num);
void* memset16 (void* ptr, uint16_t value, size_t num);
void print_memory_layout(void);
void PrintMemoryLayout(void);
#endif

View File

@@ -0,0 +1,27 @@
#ifndef KERNEL_PIT_H
#define KERNEL_PIT_H
#include <libc/stdint.h>
#include <libc/stdbool.h>
#define PIT_CMD_PORT 0x43
#define PIT_CHANNEL0_PORT 0x40
#define PIT_CHANNEL1_PORT 0x41
#define PIT_CHANNEL2_PORT 0x42
#define PC_SPEAKER_PORT 0x61
#define PIT_DEFAULT_DIVISOR 0x4E20 //20000, gets just shy of 60Hz
#define PIC1_CMD_PORT 0x20
#define PIC1_DATA_PORT 0x21
#define PIT_BASE_FREQ 1193180
#define TARGET_FREQ 1000
#define DIVIDER (PIT_BASE_FREQ / TARGET_FREQ)
#define TICKS_PER_MS (TARGET_FREQ / TARGET_FREQ) // = 1, needed for converting ms into ticks
uint32_t GetCurrentTick(void);
void PitInitialize();
void SleepInterrupt(uint32_t ticks);
void SleepBusy(uint32_t milliseconds);
void SleepTest();
#endif

View File

@@ -26,6 +26,7 @@ enum vga_colour{
uint8_t VgaEntryColour(enum vga_colour fg, enum vga_colour bg);
uint16_t VgaEntry(unsigned char uc, uint8_t color);
void TerminalClear(void);
void TerminalEntryAt(char c, uint8_t colour, size_t x, size_t y);
void TerminalInitialize(void);
void TerminalSetColour(uint8_t colour);
@@ -34,5 +35,7 @@ void TerminalPutChar(char c);
void TerminalWrite(const char* data, size_t size);
void TerminalWriteString(const char* data);
void TerminalWriteUInt(uint32_t num);
void TerminalWriteHex(uint32_t memory);
char TerminalGetChar(void);
#endif

View File

@@ -0,0 +1,72 @@
#ifndef SNAKEAPP_SNAKE_H
#define SNAKEAPP_SNAKE_H
#include <libc/stdint.h>
#define BOARD_SIZE 15
#define SNAKE_MAX_LENGTH (BOARD_SIZE * BOARD_SIZE)
#define GAME_SPEED_MS 500
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
};
enum CollisionType {
NONE,
FOOD,
SELF,
WALL
};
struct SnakeSegment {
int x;
int y;
};
struct Snake {
uint32_t length;
enum Direction direction;
struct SnakeSegment body[SNAKE_MAX_LENGTH];
};
struct Food {
int x;
int y;
};
struct GameState {
char board[BOARD_SIZE][BOARD_SIZE][3];
struct Snake* snake;
struct Food* food;
uint32_t score;
uint32_t rngState;
};
struct GameState* CreateGame(void);
void DestroyGame(struct GameState* game);
void ResetGame(struct GameState* game);
void InitializeBoard(struct GameState* game);
void InitializeSnake(struct Snake* snake);
void InitializeFood(struct Food* food);
uint32_t Random(uint32_t* rngState);
void HandleInput(struct GameState* game, char input);
void PlayFoodSound(void);
void PlayDeathSound(void);
void PlayWinSound(void);
struct SnakeSegment MoveSnake(struct Snake* snake);
void SpawnFood(struct GameState* game);
void AddSegment(struct Snake* snake, int x, int y);
enum CollisionType CheckCollision(struct Snake* snake, struct Food* food);
void DrawBoard(struct GameState* game);
void PlayGame(void);
#endif

View File

@@ -0,0 +1,139 @@
#ifndef SONGAPP_FREQUENCIES_H
#define SONGAPP_FREQUENCIES_H
// Note frequencies (in Hz)
#define C0 16
#define Cs0 17
#define D0 18
#define Ds0 19
#define E0 21
#define F0 22
#define Fs0 23
#define G0 25
#define Gs0 26
#define A0 27
#define As0 29
#define B0 31
#define C1 33
#define Cs1 35
#define D1 37
#define Ds1 39
#define E1 41
#define F1 44
#define Fs1 46
#define G1 49
#define Gs1 52
#define A1 55
#define As1 58
#define B1 62
#define C2 65
#define Cs2 69
#define D2 73
#define Ds2 78
#define E2 82
#define F2 87
#define Fs2 92
#define G2 98
#define Gs2 104
#define A2 110
#define As2 117
#define B2 123
#define C3 131
#define Cs3 139
#define D3 147
#define Ds3 156
#define E3 165
#define F3 175
#define Fs3 185
#define G3 196
#define Gs3 208
#define A3 220
#define As3 233
#define B3 247
#define C4 262
#define Cs4 277
#define D4 294
#define Ds4 311
#define E4 330
#define F4 349
#define Fs4 370
#define G4 392
#define Gs4 415
#define A4 440
#define As4 466
#define B4 494
#define C5 523
#define Cs5 554
#define D5 587
#define Ds5 622
#define E5 659
#define F5 698
#define Fs5 740
#define G5 784
#define Gs5 831
#define A5 880
#define As5 932
#define B5 988
#define C6 1047
#define Cs6 1109
#define D6 1175
#define Ds6 1245
#define E6 1319
#define F6 1397
#define Fs6 1480
#define G6 1568
#define Gs6 1661
#define A6 1760
#define As6 1865
#define B6 1976
#define C7 2093
#define Cs7 2217
#define D7 2349
#define Ds7 2489
#define E7 2637
#define F7 2794
#define Fs7 2960
#define G7 3136
#define Gs7 3322
#define A7 3520
#define As7 3729
#define B7 3951
#define C8 4186
#define Cs8 4435
#define D8 4699
#define Ds8 4978
#define E8 5274
#define F8 5588
#define Fs8 5919
#define G8 6272
#define Gs8 6645
#define A8 7040
#define As8 7459
#define B8 7902
#define C9 8372
#define Cs9 8870
#define D9 9397
#define Ds9 9956
#define E9 10548
#define F9 11175
#define Fs9 11839
#define G9 12543
#define Gs9 13290
#define A9 14080
#define As9 14917
#define B9 15804
#define A_SHARP4 466
#define G_SHARP4 415
#define R 0 // R (no sound)
#endif //UIAOS_FREQUENCIES_H

View File

@@ -0,0 +1,89 @@
#ifndef SONGAPP_SONG_H
#define SONGAPP_SONG_H
#include <libc/stdint.h>
#include "frequencies.h"
// Define a struct to represent a single musical note
typedef struct {
uint32_t frequency; // The frequency of the note in Hz (e.g., A4 = 440 Hz)
uint32_t duration; // The duration of the note in milliseconds
} Note;
// Define a struct to represent a song
typedef struct {
Note* notes; // Pointer to an array of Note structs representing the song
uint32_t length; // The number of notes in the song
} Song;
// Define a struct to represent a song player
typedef struct {
void (*play_song)(Song* song); // Function pointer to a function that plays a song
} SongPlayer;
// Function prototype for creating a new SongPlayer instance
// Returns a pointer to a newly created SongPlayer object
SongPlayer* CreateSongPlayer(void);
static Note music_1[] = {
{E5, 250}, {R, 125}, {E5, 125}, {R, 125}, {E5, 125}, {R, 125},
{C5, 125}, {E5, 125}, {G5, 125}, {R, 125}, {G4, 125}, {R, 250},
{C5, 125}, {R, 250}, {G4, 125}, {R, 125}, {E4, 125}, {R, 125},
{A4, 125}, {B4, 125}, {R, 125}, {A_SHARP4, 125}, {A4, 125}, {R, 125},
{G4, 125}, {E5, 125}, {G5, 125}, {A5, 125}, {F5, 125}, {G5, 125},
{R, 125}, {E5, 125}, {C5, 125}, {D5, 125}, {B4, 125}, {R, 125},
{C5, 125}, {R, 250}, {G4, 125}, {R, 125}, {E4, 125}, {R, 125},
{A4, 125}, {B4, 125}, {R, 125}, {A_SHARP4, 125}, {A4, 125}, {R, 125},
{G4, 125}, {E5, 125}, {G5, 125}, {A5, 125}, {F5, 125}, {G5, 125},
{R, 125}, {E5, 125}, {C5, 125}, {D5, 125}, {B4, 125}, {R, 125},
};
static Note music_2[] = {
{A4, 200}, {E5, 200}, {A5, 200}, {R, 100}, {A5, 200}, {A5, 200}, {Gs5, 200}, {A5, 200},
{R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200},
{A4, 200}, {E5, 200}, {A5, 200}, {R, 100}, {A5, 200}, {A5, 200}, {Gs5, 200}, {A5, 200},
{R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200},
{A4, 200}, {E5, 200}, {A5, 200}, {R, 100}, {A5, 200}, {A5, 200}, {Gs5, 200}, {A5, 200},
{R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200}, {R, 100}, {E5, 200}
};
static Note music_3[] = {
{E4, 200}, {E4, 200}, {F4, 200}, {G4, 200}, {G4, 200}, {F4, 200}, {E4, 200}, {D4, 200},
{C4, 200}, {C4, 200}, {D4, 200}, {E4, 200}, {E4, 400}, {R, 200},
{D4, 200}, {D4, 200}, {E4, 200}, {F4, 200}, {F4, 200}, {E4, 200}, {D4, 200}, {C4, 200},
{A4, 200}, {A4, 200}, {A4, 200}, {G4, 400}
};
static Note music_4[] = {
{C4, 500}, {D4, 500}, {E4, 500}, {C4, 500},
{C4, 500}, {D4, 500}, {E4, 500}, {C4, 500},
{E4, 500}, {F4, 500}, {G4, 1000},
{E4, 500}, {F4, 500}, {G4, 1000},
{G4, 250}, {A4, 250}, {G4, 250}, {F4, 250}, {E4, 500}, {C4, 500},
{G4, 250}, {A4, 250}, {G4, 250}, {F4, 250}, {E4, 500}, {C4, 500},
{C4, 500}, {G3, 500}, {C4, 1000},
{C4, 500}, {G3, 500}, {C4, 1000}
};
static Note music_5[] = {
{E4, 375}, {C4, 375}, {D4, 375}, {A3, 375}, {B3, 375}, {D4, 375}, {C4, 375}, {A3, 375},
{E4, 375}, {C4, 375}, {D4, 375}, {A3, 375}, {B3, 375}, {D4, 375}, {C4, 375}, {A3, 375},
};
static Note music_6[] = {
{F4, 250}, {F4, 250}, {F4, 250}, {C5, 250}, {A_SHARP4, 250}, {G_SHARP4, 250}, {F4, 500},
{F4, 250}, {F4, 250}, {F4, 250}, {C5, 250}, {A_SHARP4, 250}, {G_SHARP4, 250}, {F4, 500},
{A_SHARP4, 250}, {A_SHARP4, 250}, {A_SHARP4, 250}, {F5, 250}, {D5, 250}, {C5, 250}, {A_SHARP4, 500},
{A_SHARP4, 250}, {A_SHARP4, 250}, {A_SHARP4, 250}, {F5, 250}, {D5, 250}, {C5, 250}, {A_SHARP4, 500},
};
void EnableSpeaker(void);
void DisableSpeaker(void);
void PlaySound(uint32_t frequency);
void StopSound(void);
void PlaySongImpl(Song* song);
void PlayMusic(void);
#endif

View File

@@ -5,43 +5,67 @@
#include <kernel/interrupt.h>
#include <kernel/keyboard.h>
#include <kernel/memory.h>
#include <kernel/pit.h>
#include <songApp/song.h>
#include <songApp/frequencies.h>
#include <snakeApp/snake.h>
extern uint32_t end;
void PlayMusic(void) {
Song songs[] = {
{music_1, sizeof(music_1) / sizeof(Note)},
{music_2, sizeof(music_2) / sizeof(Note)},
{music_3, sizeof(music_3) / sizeof(Note)},
{music_4, sizeof(music_4) / sizeof(Note)},
{music_5, sizeof(music_5) / sizeof(Note)},
{music_6, sizeof(music_6) / sizeof(Note)}
};
uint32_t songCount = sizeof(songs) / sizeof(Song);
SongPlayer* player = CreateSongPlayer();
if (!player) {
TerminalWriteString("Failed to create SongPlayer.\n");
return;
}
for(uint32_t i = 0; i < songCount; i++) {
player->play_song(&songs[i]);
SleepInterrupt(1000);
}
}
void main(void) {
TerminalInitialize();
GdtInitialize();
IdtInitialize();
PitInitialize();
RegisterInterruptHandler(IRQ1, KeyboardHandler);
init_kernel_memory(&end);
init_paging();
print_memory_layout();
InitKernelMemory(&end);
InitPaging();
PrintMemoryLayout();
void* memory1 = malloc(48261);
void* memory2 = malloc(27261);
void* memory3 = malloc(12617);
while (1) {
TerminalClear();
TerminalWriteString("Enter application number (0 for music, 1 for snake): ");
char input = TerminalGetChar();
TerminalWriteString("memory1 = ");
TerminalWriteHex((uint32_t)memory1);
TerminalWriteString("\n");
TerminalWriteString("memory2 = ");
TerminalWriteHex((uint32_t)memory2);
TerminalWriteString("\n");
TerminalWriteString("memory3 = ");
TerminalWriteHex((uint32_t)memory3);
TerminalWriteString("\n");
free(memory2);
void* memory4 = malloc(1000);
TerminalWriteString("memory4 = ");
TerminalWriteHex((uint32_t)memory4);
TerminalWriteString("\n");
switch (input) {
case '0':
PlayMusic();
break;
case '1':
PlayGame();
break;
default:
TerminalWriteString("Invalid application number.\n");
break;
}
}
for (;;) {
__asm__ volatile("hlt");

View File

@@ -7,6 +7,8 @@
static uint32_t index = 0;
static uint8_t keyboardBuffer[KEYBOARD_BUFFER_SIZE];
static volatile char lastKeyPressed = 0;
static const char scancodeToAscii[128] = {
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
@@ -19,6 +21,12 @@ static const char scancodeToAscii[128] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
char GetLastKeyPressed(void) {
char key = lastKeyPressed;
lastKeyPressed = 0;
return key;
}
void KeyboardHandler(struct Registers* registers) {
(void) registers;
@@ -37,7 +45,7 @@ void KeyboardHandler(struct Registers* registers) {
char ascii = scancodeToAscii[scancode];
if (ascii != 0) {
TerminalPutChar(ascii);
lastKeyPressed = ascii;
}
}
}

View File

@@ -12,7 +12,7 @@ static uint32_t pheap_end = 0;
static uint8_t *pheap_desc = 0;
static uint32_t memory_used = 0;
void init_kernel_memory(uint32_t* kernel_end) {
void InitKernelMemory(uint32_t* kernel_end) {
uint32_t kernelEndAddr = (uint32_t)kernel_end;
last_alloc = kernelEndAddr + 0x1000;
@@ -28,7 +28,7 @@ void init_kernel_memory(uint32_t* kernel_end) {
TerminalWriteString("\n");
}
void print_memory_layout(void) {
void PrintMemoryLayout(void) {
TerminalWriteString("Memory used:");
TerminalWriteUInt(memory_used);
TerminalWriteString(" bytes\n");

View File

@@ -25,7 +25,7 @@ void paging_enable() {
asm volatile("mov %eax, %cr0");
}
void init_paging(void) {
void InitPaging(void) {
TerminalWriteString("Setting up paging\n");
page_directory = (uint32_t*)0x400000;

59
src/OSDev_18/src/pit.c Normal file
View File

@@ -0,0 +1,59 @@
#include <kernel/pit.h>
#include <kernel/interrupt.h>
#include <kernel/io.h>
#include <kernel/terminal.h>
static volatile uint32_t pit_ticks = 0; // Encasulated to pit.c to avoid accidental overwrite
uint32_t GetCurrentTick(void){
return pit_ticks;
}
static void PitIrqHandler(struct Registers* regs){
(void)regs; // Required by interface, not needed in this implementation
pit_ticks++;
}
void PitInitialize(void){
uint16_t divisor = DIVIDER;
RegisterInterruptHandler(IRQ0, PitIrqHandler);
OutPortByte(PIT_CMD_PORT, 0x36);
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)(divisor & 0xFF));
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)((divisor >> 8) & 0xFF));
}
//More efficient way of sleeping compared to SleepBusy()
void SleepInterrupt(uint32_t ticks_to_wait){
uint32_t start_tick = GetCurrentTick();
uint32_t end_tick = start_tick + ticks_to_wait;
while(GetCurrentTick() < end_tick){
__asm__ volatile ("sti; hlt");
}
}
/* "Consumes" 100% CPU power to effectively put entire system into sleep
for (ticks_to_wait) amount of ticks / milliseconds */
void SleepBusy(uint32_t milliseconds){
uint32_t start_tick = GetCurrentTick();
uint32_t ticks_to_wait = milliseconds * TICKS_PER_MS;
while((GetCurrentTick() - start_tick) < ticks_to_wait){
/* Occupy the CPU by with handling a whole lot of nothing.
Also known as busy waiting */
}
}
void SleepTest(){
while (GetCurrentTick() < 15000) {
TerminalWriteString("Sleeping with busy-waiting (HIGH CPU).\n");
SleepBusy(1000);
TerminalWriteString("Slept using busy-waiting.\n");
TerminalWriteString("Sleeping with interrupts (LOW CPU).\n");
SleepInterrupt(1000);
TerminalWriteString("Slept using interrupts.\n");
}
}

357
src/OSDev_18/src/snake.c Normal file
View File

@@ -0,0 +1,357 @@
#include <snakeApp/snake.h>
#include <kernel/memory.h>
#include <kernel/pit.h>
#include <kernel/keyboard.h>
#include <kernel/terminal.h>
#include <songApp/song.h>
#include <songApp/frequencies.h>
struct GameState* CreateGame(void) {
struct GameState* game = (struct GameState*)malloc(sizeof(struct GameState));
if (!game) {
return 0;
}
game->snake = (struct Snake*)malloc(sizeof(struct Snake));
if (!game->snake) {
free(game);
return 0;
}
game->food = (struct Food*)malloc(sizeof(struct Food));
if (!game->food) {
free(game->snake);
free(game);
return 0;
}
game->score = 0;
game->rngState = GetCurrentTick();
InitializeBoard(game);
InitializeSnake(game->snake);
InitializeFood(game->food);
return game;
}
void DestroyGame(struct GameState* game) {
if (!game) return;
if (game->snake) {
free(game->snake);
}
if (game->food) {
free(game->food);
}
free(game);
}
void ResetGame(struct GameState* game) {
if (!game) return;
InitializeBoard(game);
InitializeSnake(game->snake);
InitializeFood(game->food);
game->score = 0;
game->rngState = GetCurrentTick();
}
void InitializeBoard(struct GameState* game) {
for (uint32_t i = 0; i < BOARD_SIZE; i++) {
for (uint32_t j = 0; j < BOARD_SIZE; j++) {
game->board[i][j][0] = ' ';
game->board[i][j][1] = ' ';
game->board[i][j][2] = '\0';
}
}
}
void InitializeSnake(struct Snake* snake) {
snake->length = 1;
snake->direction = LEFT;
snake->body[0].x = BOARD_SIZE / 2;
snake->body[0].y = BOARD_SIZE / 2;
}
void InitializeFood(struct Food* food) {
food->x = (BOARD_SIZE / 2) - 3;
food->y = (BOARD_SIZE / 2) - 3;
}
uint32_t Random(uint32_t* rngState) {
*rngState = (*rngState * 1103515245) + 12345;
return *rngState;
}
void HandleInput(struct GameState* game, char input) {
switch (input) {
case 'w': {
if (game->snake->direction != DOWN) {
game->snake->direction = UP;
}
break;
}
case 's': {
if (game->snake->direction != UP) {
game->snake->direction = DOWN;
}
break;
}
case 'a': {
if (game->snake->direction != RIGHT) {
game->snake->direction = LEFT;
}
break;
}
case 'd': {
if (game->snake->direction != LEFT) {
game->snake->direction = RIGHT;
}
break;
}
case 'r': {
if (game) {
ResetGame(game);
}
break;
}
case 'q': {
TerminalWriteString("Quitting game...\n");
DestroyGame(game);
}
default: {
break;
}
}
}
void PlayFoodSound(void) {
PlaySound(B5);
SleepInterrupt(50);
StopSound();
}
void PlayDeathSound(void) {
PlaySound(G3);
SleepInterrupt(100);
StopSound();
}
void PlayWinSound(void) {
PlaySound(C5);
SleepInterrupt(100);
StopSound();
PlaySound(E5);
SleepInterrupt(100);
StopSound();
PlaySound(G5);
SleepInterrupt(200);
StopSound();
}
struct SnakeSegment MoveSnake(struct Snake* snake) {
int x = snake->body[0].x;
int y = snake->body[0].y;
switch (snake->direction) {
case UP: {
snake->body[0].y--;
for (uint32_t i = 1; i < snake->length; i++) {
int tempX = snake->body[i].x;
int tempY = snake->body[i].y;
snake->body[i].x = x;
snake->body[i].y = y;
x = tempX;
y = tempY;
}
break;
}
case DOWN: {
snake->body[0].y++;
for (uint32_t i = 1; i < snake->length; i++) {
int tempX = snake->body[i].x;
int tempY = snake->body[i].y;
snake->body[i].x = x;
snake->body[i].y = y;
x = tempX;
y = tempY;
}
break;
}
case LEFT: {
snake->body[0].x--;
for (uint32_t i = 1; i < snake->length; i++) {
int tempX = snake->body[i].x;
int tempY = snake->body[i].y;
snake->body[i].x = x;
snake->body[i].y = y;
x = tempX;
y = tempY;
}
break;
}
case RIGHT: {
snake->body[0].x++;
for (uint32_t i = 1; i < snake->length; i++) {
int tempX = snake->body[i].x;
int tempY = snake->body[i].y;
snake->body[i].x = x;
snake->body[i].y = y;
x = tempX;
y = tempY;
}
break;
}
default: {
break;
}
}
return (struct SnakeSegment){ x, y };
}
void SpawnFood(struct GameState* game) {
int x, y;
uint32_t occupied;
if (game->snake->length == SNAKE_MAX_LENGTH) {
return;
}
do {
x = Random(&game->rngState) % BOARD_SIZE;
y = Random(&game->rngState) % BOARD_SIZE;
occupied = 0;
for (uint32_t i = 0; i < game->snake->length; i++) {
if (game->snake->body[i].x == x && game->snake->body[i].y == y) {
occupied = 1;
break;
}
}
} while (occupied);
game->food->x = x;
game->food->y = y;
}
void AddSegment(struct Snake* snake, int x, int y) {
if (snake->length < SNAKE_MAX_LENGTH) {
snake->body[snake->length].x = x;
snake->body[snake->length].y = y;
snake->length++;
}
}
enum CollisionType CheckCollision(struct Snake* snake, struct Food* food) {
if (snake->body[0].x == food->x && snake->body[0].y == food->y) {
return FOOD;
} else if (snake->body[0].x < 0 || snake->body[0].x >= BOARD_SIZE || snake->body[0].y < 0 || snake->body[0].y >= BOARD_SIZE) {
return WALL;
} else {
for (uint32_t i = 1; i < snake->length; i++) {
if (snake->body[0].x == snake->body[i].x && snake->body[0].y == snake->body[i].y) {
return SELF;
}
}
return NONE;
}
}
void DrawBoard(struct GameState* game) {
TerminalClear();
InitializeBoard(game);
if (game->snake->direction == UP) {
game->board[game->snake->body[0].y][game->snake->body[0].x][0] = '^';
game->board[game->snake->body[0].y][game->snake->body[0].x][1] = '^';
} else if (game->snake->direction == DOWN) {
game->board[game->snake->body[0].y][game->snake->body[0].x][0] = 'v';
game->board[game->snake->body[0].y][game->snake->body[0].x][1] = 'v';
} else if (game->snake->direction == LEFT) {
game->board[game->snake->body[0].y][game->snake->body[0].x][0] = '<';
game->board[game->snake->body[0].y][game->snake->body[0].x][1] = '<';
} else if (game->snake->direction == RIGHT) {
game->board[game->snake->body[0].y][game->snake->body[0].x][0] = '>';
game->board[game->snake->body[0].y][game->snake->body[0].x][1] = '>';
}
for (uint32_t i = 1; i < game->snake->length; i++) {
game->board[game->snake->body[i].y][game->snake->body[i].x][0] = '[';
game->board[game->snake->body[i].y][game->snake->body[i].x][1] = ']';
}
game->board[game->food->y][game->food->x][0] = '{';
game->board[game->food->y][game->food->x][1] = '}';
for (uint32_t i = 0; i < BOARD_SIZE + 2; i++) {
TerminalWriteString("##");
}
TerminalPutChar('\n');
for (uint32_t i = 0; i < BOARD_SIZE; i++) {
TerminalWriteString("##");
for (uint32_t j = 0; j < BOARD_SIZE; j++) {
TerminalWriteString(game->board[i][j]);
}
TerminalWriteString("##\n");
}
for (uint32_t i = 0; i < BOARD_SIZE + 2; i++) {
TerminalWriteString("##");
}
TerminalPutChar('\n');
TerminalWriteString("Score: ");
TerminalWriteUInt(game->score);
TerminalPutChar('\n');
}
void PlayGame(void) {
struct GameState* game = CreateGame();
if (!game) {
TerminalWriteString("Failed to create game.\n");
return;
}
char input = 0;
struct SnakeSegment tail;
enum CollisionType collisionType = NONE;
while(1) {
if (game->snake->length == SNAKE_MAX_LENGTH) {
break;
}
input = GetLastKeyPressed();
HandleInput(game, input);
if (input == 'q') return;
tail = MoveSnake(game->snake);
collisionType = CheckCollision(game->snake, game->food);
if (collisionType == FOOD) {
game->score++;
PlayFoodSound();
AddSegment(game->snake, tail.x, tail.y);
SpawnFood(game);
} else if (collisionType == WALL || collisionType == SELF) {
PlayDeathSound();
ResetGame(game);
}
collisionType = NONE;
DrawBoard(game);
SleepInterrupt(GAME_SPEED_MS);
}
TerminalWriteString("You win!\n");
PlayWinSound();
DestroyGame(game);
}

View File

@@ -0,0 +1,55 @@
#include <songApp/song.h>
#include <kernel/pit.h>
#include <kernel/io.h>
#include <kernel/memory.h>
#include <libc/stdint.h>
void EnableSpeaker(void) {
uint8_t speakerState = InPortByte(PC_SPEAKER_PORT);
OutPortByte(PC_SPEAKER_PORT, speakerState | 0x03);
}
void DisableSpeaker(void) {
uint8_t speakerState = InPortByte(PC_SPEAKER_PORT);
OutPortByte(PC_SPEAKER_PORT, speakerState & 0xFC);
}
void PlaySound(uint32_t frequency) {
if (!frequency) return;
uint32_t divisor = PIT_BASE_FREQ / frequency;
OutPortByte(PIT_CMD_PORT, 0xB6);
OutPortByte(PIT_CHANNEL2_PORT, divisor & 0xFF);
OutPortByte(PIT_CHANNEL2_PORT, (divisor >> 8) & 0xFF);
EnableSpeaker();
}
void StopSound(void) {
DisableSpeaker();
}
void PlaySongImpl(Song* song) {
for (uint32_t i = 0; i < song->length; i++) {
Note currentNote = song->notes[i];
if (currentNote.frequency == R) {
StopSound();
SleepInterrupt(currentNote.duration * 2); // *2 is a forced delay, added because QEMU struggles with frequencies switching fast
} else {
PlaySound(currentNote.frequency);
SleepInterrupt(currentNote.duration * 2); // *2 is a forced delay, added because QEMU struggles with frequencies switching fast
StopSound();
}
}
}
SongPlayer* CreateSongPlayer(void) {
SongPlayer* player = (SongPlayer*)malloc(sizeof(SongPlayer));
if (!player) return 0;
player->play_song = PlaySongImpl;
return player;
}

View File

@@ -1,6 +1,8 @@
#include <kernel/gdt.h>
#include <kernel/terminal.h>
#include <kernel/io.h>
#include <kernel/keyboard.h>
#include <kernel/pit.h>
const size_t VGA_HEIGHT = 25;
const size_t VGA_WIDTH = 80;
@@ -21,6 +23,18 @@ uint16_t VgaEntry(unsigned char uc, uint8_t color) {
return (uint16_t) uc | (uint16_t) color << 8;
}
void TerminalClear(void) {
for (size_t y = 0; y < VGA_HEIGHT; y++) {
for (size_t x = 0; x < VGA_WIDTH; x++) {
TerminalEntryAt(' ', terminal_colour, x, y);
}
}
terminal_row = 0;
terminal_column = 0;
TerminalUpdateCursor();
}
void TerminalEntryAt(char c, uint8_t colour, size_t x, size_t y){
size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = VgaEntry(c, colour);
@@ -125,4 +139,16 @@ void TerminalWriteHex(uint32_t num) {
uint8_t digit = (num >> i) & 0xF;
TerminalPutChar(hexChars[digit]);
}
}
char TerminalGetChar(void) {
while (1) {
char key = GetLastKeyPressed();
if (key != 0) {
return key;
}
SleepInterrupt(100);
}
}