Snowdrop OS's system organization is simple. Its main components are the boot loader, the kernel, and a startup application.
Upon computer start-up, the BIOS (or UEFI or newer, running in legacy BIOS mode) loads Snowdrop's single-stage 512 bytes boot loader. The BIOS then jumps to the beginning of the memory where the 512 bytes were loaded. This transfers control to the boot loader.
The boot loader then reads the disk's FAT to locate the file SNOWDROP.KRN. It then loads it, and jumps into its beginning, giving control to the kernel proper.
The Snowdrop OS kernel begins by initializing and registering all system interrupt services which are to be used by user applications. These include a random number generator, FAT12 driver, serial port driver, PS/2 mouse driver, etc.
The kernel then reads the file SNOWDROP.CFG, which holds configuration data, including the name of the startup application.
At the end of its initialization, the kernel reads the startup app binary, creating a task based on it. The kernel's final step is to start the scheduler, effectively yielding control to the startup application.
This user application is automatically started by the Snowdrop kernel. Generally (and by default in the Snowdrop package) this will be a shell, allowing the user to issue commands and run applications. This, and all other applications rely on the kernel-provided services.
Program arguments are declared uniquely in Snowdrop OS; they are always named, and their order does not matter.
This originated from the updated format of the kernel configuration file, which specifies properties as such: [propertyName=propertyValue]. Once I could parse these, I realized that it'd be very easy if program argument strings also had the same format.
Thus, when starting a task, the string containing arguments looks like this:
[message=Hello, World!] [user=Sebastian]
This approach has the advantage of disambiguation: the purpose of each value passed in is clear. An additional advantage is that the order in which they are specified does not matter. The cost is extra letters which must be typed on the command line.
PS/2 mouse driver
Snowdrop OS includes a mouse driver, which allows applications to rely on standard PS/2 mice. As with everything around it, I have designed and written the driver from scratch, mainly from PS/2 specifications. I have used two real computers and two virtual machines as my testing environment.
The driver has two conceptual layers. The first layer handles the communication and capture of raw mouse data from the mouse hardware, via IRQ12. The second layer translates the raw data into an absolute location and applies acceleration for quicker mouse motions.
A user application which needs the mouse will first initialize the mouse manager via an interrupt (see System Calls page), passing in the dimensions of the bounding box within which the mouse cursor can move. Usually, these dimensions will be the width and height of the whole screen. This is called "managed" mode, and the driver will ensure that the mouse never leaves the user-defined bounding box.
For example, when using graphics mode 13h, whose resolution is 320x200, the mouse's horizontal location will be between 0 and 319, inclusive. See test applications MMANAGED and MOUSEGUI for usages of "managed" mode.
Optionally, a consumer application can gain access to raw mouse data, allowing it to track the location of the mouse itself. See the test applications MINTRRAW and MINTRPOLL to see how to obtain raw (delta X, delta Y, button status) mouse data.
Multi-tasking components (scheduler, memory manager, virtual displays)
With the addition of basic multi-tasking, a few kernel components had to be added to the Snowdrop OS kernel:
The memory manager
is responsible for the allocation and deallocation of memory. I kept it simple, only allowing operation on entire 64kb memory segments. Prior to its existence, programs would simply use the next segment from where they were loaded, by virtue of only one task running at a time.
The introduction of multi-tasking support invalidated the assumption of the ready availability of the memory past where a task was loaded.
The second component I added was a virtual display driver
. This was needed because I wanted tasks to have their own, isolated display. At a low level, I had to implement my own direct-to-memory string output routines, complete with cursor management and scrolling. These routines more or less replace int 10h, the BIOS screen services interrupt. At a high level, the driver supports switching between the virtual displays, keeping only one of them active (on-screen) at a time.
The third component I implemented was a scheduler
. The scheduler is in charge of managing tasks by handling requests for task addition, exit, and yield. The scheduling mechanism uses a non-preemptive (cooperative), round robin approach. Starvation (that is, running out of tasks) will cause the kernel to lock the CPU.
Snowdrop's FAT12 file system driver supports read, write, and delete operations. It was written mostly from the FAT12 specification.
While the read and delete functionality was reasonably difficult to implement, writing a file was among the most algorithmically-difficult pieces I've implemented for Snowdrop OS. It involved working with all parts of a FAT12 file system: FAT linked list, root directory, 12bit cluster references, and the data area.
Testing FAT12 driver writes and deletes took some time because I wanted consistency between Snowdrop, and Microsoft OSes (MSDOS6.22, Windows XP). Any change to the disk (file creation or deletion) was required to keep the disk in a good state between operating systems.
All file system driver functionality has been tested on real hardware, on both floppy disks and USB keys.
Serial port driver
Snowdrop OS supports communication over serial port. I have created a simple serial port driver, which allows programs to hook an interrupt handler to receive bytes from the serial port. An additional kernel service interrupt allows blocking writes to the serial port.
An accompanying test app hooks into the "read byte from serial" interrupt to read, as well as send keystrokes via serial. This was then used to test communication between two instances of Snowdrop, as well as between Snowdrop and HyperTerminal running in Windows XP. The tests took place on both virtual and real hardware.
Parallel port driver
Similarly to the serial port driver, Snowdrop OS can output single bytes to the parallel port via a blocking write interrupt.
The included paratest app shows how to use this functionality.
Sound driver (internal speaker)
In version 11, I added a sound driver (for the IBM PC internal speaker) to Snowdrop OS's kernel.
While the kernel already offered a way to turn on the speaker at a desired frequency, I wanted a way for a consumer (such as a game) to "fire-and-forget" calls which play sounds of specified durations.
The sound driver accomplishes this using a queue. Consumers add sound definitions to this queue. Periodically (driven by system timer ticks), an interrupt handler will consume sounds in the queue, turning the speaker on and off accordingly.
The driver offers a few sound modes, which modify the behaviour of sounds. For example, instead of simply enqueueing a sound, a consumer might choose to use "immediate" mode, causing the newly-added sound to be played immediately.
Version 12 saw the introduction of a keyboard driver. It doesn't replace BIOS keyboard services, but extends them with functionality meant for video games:
- the ability to check which keys are pressed at any given moment, and
- the ability to handle multiple keys pressed at once
The driver accomplishes this by handling IRQ1 (keyboard) events and keeping track of which keys are up and which are down, based on the scan codes read from the keyboard controller.
Depending on the current mode, the driver will then call the previous (BIOS) handler, so that BIOS keyboard services continue uninterrupted.
Consumer apps (such as video games) can simply poll the keyboard driver for key status.
Snowdrop OS has the ability to replicate itself on other disks, starting from unformatted disks. Since it still assumes a 1.44Mb floppy disk, formatting such a disk is as simple as raw-writing a pre-generated blank FAT12 image.
The next step in the workflow is writing Snowdrop's boot loader to the disk's boot sector. What follows is the transfer of core Snowdrop files, such as kernel, configuration, etc. At this point, the user can copy whatever apps he chooses, as well as edit the configuration file on the new disk.
At every step of this process, I've tested the disk in a few other operating systems, to ensure that they see the disk as "proper".
"Keep memory" task lifetime mode
A given task can choose to tell the scheduler that it wants its memory to be kept (and not freed) after the task has exited. This allows the task to register an interrupt handler (such as a service, driver, etc.), after which it exits.
The result is that other programs can now invoke the interrupt handler which lives in the "kept" memory area. This concept is similar to MS-DOS's Terminate and Stay Resident (TSR) concept.
This feature was relatively easy to implement, given the way the scheduler was designed.
Snowdrop OS contains graphics routines which can load and display bitmaps from 256-colour BMP files. Furthermore, sprites can be used to achieve movable objects (such as video game characters, etc.) on top of a background.
See the Game Development section for more information.
Version 16 introduced the GUI framework, which lets user applications make use of UI elements such as buttons, checkboxes, etc. via the mouse and keyboard.
See the GUI Framework section for more information.
Dynamic memory support and data structures
In version 25, I introduced support for dynamic memory. For the first time, kernel and applications are able to request chunks of memory (analogous to libc's malloc, free, and realloc).
This allowed the creation of a suite of dynamic data structure libraries such as linked list, stack, queue, N-ary tree, binary tree, and binary search tree (BST).
In version 26 I introduced a pseudo-mouse driver. It allows a user to control mouse-driven applications without using an actual hardware mouse.
When configured to use the pseudo-mouse driver, the kernel bypasses PS/2-specific communication layer in favour of a communication layer driven by system timer ticks. It draws data from the keyboard state and generates data that "looks" like it was generated by a genuine PS/2 controller. Thus, the rest of the mouse driver - that is, high level functionality - remains unchanged. No changes to consumer applications or frameworks are required.
Runtime libraries (RTL)
In version 31 I introduced support for runtime libraries (RTL). They effectively increase the possible size of an executable by allowing invocation of functions contained within libraries loaded at runtime by an aplication.
To achieve this, I wrote two reusable modules. The first is expected to be included in every RTL. It looks up and calls functions by name within the RTL.
The second module is contained within the consumer and is responsible for loading RTLs into memory and invoking functions on the RTL-side module. The fact that RTL are generally loaded in a different memory segment is opaque to the consumer.
The two modules (consumer-side and RTL-side) act as a bridge between consumer and RTL, while abstracting the internals of invocations. To the consumer, invocations of RTL functions are reduced to usual, near calls.