Carnegie Mellon
SCS logo
Computer Science Department

15-410 Project 1: Night Driver

Table of Contents

Project Overview

In this introductory project you will be writing three device drivers. These three device drivers will constitute a library which will be used to construct single-task "kernel applications". You will write a small game application to run on top of your library. In addition, we will provide you with a basic application that will test some aspects of your device-driver library.

The first is the console device driver. The console device driver is responsible for printing characters on the screen. Without it, it would be rather difficult later in the semester to obtain feedback as to what is going on in your operating system or user applications.

The second device driver you will write is the keyboard device driver. Whenever the user presses a key, the keyboard device driver needs to find out what key it is, whether it was pushed or released, and then make that information available to the operating system. An important aspect of the keyboard device driver is that it is interrupt driven. We will take a closer look at what this means later.

The third "device driver" is the timer handler. While we do not often think of the on board PC timer as a device, it generates interrupts just like the keyboard. The timer is a critical device that will be used later in context switching between processes.

It is not possible to test these device drivers on their own. In order to develop and test them, you will be provided with a skeleton kernel. The kernel performs some very basic machine initialization such as transitioning to protected mode. Once this initialization is complete, it transfers control to the application's kernel_main() function, which will intialize your device drivers and begin executing application-specific code. There is no virtual memory and no file system.

Once you have implemented the three device drivers, you will use the three of them to implement a video game modeled after Atari's 1976 arcade game Night Driver. A detailed explanation of the desired behavior is presented later in this handout.


Despite the fact that this is a small project relative to the kernel, it is important to pay attention to the key concepts in Project 1. The ideas taught here will provide the foundation for the next three projects. In particular, we would like you to be comfortable with:
  1. Writing clean code in C. Many people like the C programming language because it gives the programmer a lot of freedom (pointers, casting, etc). It is very easy to hang yourself with this rope. Developing (and sticking with) a consistent system of variable definitions, commenting, and separation of functionality is essential.

    People have asked about using C++ in this class. This is probably much harder than you think, since you would need to begin by implementing your own thread-safe (or, at least, interrupt-aware) versions of new and delete. In addition, you would probably find yourself implementing other pieces of C++ runtime code; this could turn into quite a hobby. As a result, you should do this program in C as a way of re-familiarizing yourself with the language you'll be using for the remainder of the course.

  2. Writing psuedocode or code outlines. For systems programming, it is very important to think out crucial data structures and algorithms ahead of time since they become important primitives for the rest of the system.
  3. Commenting. Though you will not be working with a partner for the first project, you will be on all subsequent projects. It is important to include comments so someone else looking at or maintaining your code can quickly understand what your code is doing without having to look at its internals. We will be using a system called doxygen, which creates documents similar to javadoc.
  4. Debugging/testing operating system code. We will be using Simics, an instruction set simulator with a built-in debugger to debug and test; it is similar to gdb, but has some important differences and will take some getting used to.
  5. Using common development tools (gcc, ld).
  6. Understanding device drivers: their role in the system, and their requirements.
  7. Communicating with the TAs using various channels of communication (zephyr, bulletin board, staff-410 at the CS domain, Q&A archive, course web page, office hours).
In addition to these primary goals, mundane details such as software installation (e.g., getting AFS installed on a personal Linux machine) should be taken care of by the end of Project 1.

Technology Disclaimer

Because of the availability, low cost, and widespread use of the x86 architecture, it was chosen as the platform for this sequence of projects. As its creator, Intel Corporation has provided much of the documentation used in the development of these projects. In its literature, Intel uses and defines terms like interrupt, fault, etc. On top of this the x86 architecture will be the only platform used in these projects.

The goal of this project set is certainly not to teach the idiosyncrasies of the x86 architecture (or Intel's documentation). That said, it will be necessary to become accustomed to the x86 way of doing things, and the Intel nomenclature, for the purposes of completing this project set. Just keep in mind that the x86 way of doing things is not the only way of doing things. It is the price to be paid for learning the principles of operating systems on a real world system instead of a simulated architecture.

Header Files

In many places within this document, we will refer to #defines in several .h files. The directories mentioned will be created when you untar the project tarball.

Important Dates

Friday, January 21st:
Project 1 assigned.
Tuesday, January 25th:
You should be familiar with using Simics, have console output working, and have started on the keyboard and timer interrupts.
Monday, January 31st:
Project 1 is due at 11:59pm.

Welcome to x86 Kernel Programming

These projects will be a new experience for many of you because they are special in two ways. The first is that, as kernel programmers, you are subject to a number of constraints that do not show up in user-level programming. The second is that you will need to look down at the hardware you are running on and manipulate device registers/processor data structures to get your kernel to work. Because this is new for many of you, the following section is devoted to saying a few things about kernel programming, and then summarizing some of the important features of the x86 architecture which you will need to know.

Kernel Programming: Always Assuming the Worst

Programming in the kernel is quite different from writing user level applications. As has been emphasized in class, safety is a priority in kernel code - that is, checking pointers passed to you by user code before you dereference them, testing all possible cases to ensure there are no infinite loops, etc. Kernels need to be as absolutely bulletproof as possible. For example, the following line will happily execute as part of your kernel:

*(char*)0x0 = 0x1badd00d;

Wild pointer accesses are much more dangerous in kernel mode because valuable kernel data structures are not protected by the user mode/kernel mode protection schemes of the processor (discussed here shortly). Dereferencing and writing to a bad pointer like this within the kernel can overwrite crucial kernel data or reboot the (simulated) machine.

Privilege Levels

The x86 architecture uses privilege levels to make defending the kernel against wild user processes easier. There are four privilege levels ranging from zero to three, with zero being the most privileged and three being the least. At any given time, the processor is "executing" in one of these four privilege levels. As you might have already guessed, your kernel code will execute at privilege level zero, while user code should execute at privilege level three. We will not be using privilege levels one or two in these projects. We will refer to privilege levels zero and three as PL0 and PL3 respectively. Because of the way privilege levels are explained in section 4.5 of intel-sys.pdf, privilege levels are often called "rings" (as in ring zero or ring three), though we will try to avoid that notation in these projects.

The processor checks the privilege level (PL) in a number of circumstances, such as when an attempt is made to execute a privileged instruction. These are instructions that modify control registers or change how the processor is running (a list of these instructions can be found in section 4.9 of intel-sys.pdf). These are instructions we would want only the kernel to be able to execute, so they are only allowed while the processor is running in more privileged levels (such as PL0). If you try to execute such instructions at other privilege levels, a fault occurs. Fun thought experiment: If privileged instructions cause a fault, how does one then change privilege levels?


While we will try to avoid using segmentation as much as possible for the duration of these projects, the x86 architecture requires at least minimal use of segmentation. In order to install interrupts and manage processes you will need to understand what a segment is. Luckily, it is simpler to set up segmentation for Project 1 than it will be for you to set up virtual memory in Project 3 (especially since we have done much of the work for you).

A segment is simply a portion of the address space. Once defined, we can associate important characteristics with the segment, like the privilege level required to access the segment or whether it contains code or data. A segment does not need to represent memory that actually exists - for example, we could define a segment ranging from 0 to 4GB (the entire 32 bit address space), even though we might only have 128MB of physical memory.

Because your kernels are going to be complicated enough as it is, we will steer clear of segmentation by using only four segments. These four segments all span the entire 32 bit address space (overlapping each other completely). Two of them require privilege level 0 to be accessed, and two of them require only privilege level 3. Of each privilege level pair, one segment represents code and the other data. We have set up these segments for you.

In this project, since no code will execute in user mode, we will use only the two PL 0 segments. To refer to these segments when configuring kernel data structures (such as IDT entries), use the KERNEL_CS_SEGSEL and KERNEL_DS_SEGSEL constants defined in lib/inc/x86/seg.h. These numbers are segment selectors, which are simply indices into another processor data structure, the global descriptor table (GDT). The GDT is the place we have defined our segments. You do not have to reconfigure it or know its format, however.

Please refer to our segmentation document for a more detailed explanation of segmentation. Do not worry if parts of it do not make sense at the moment as they will become clearer later in the course.

Communicating with Devices

There are two ways to communicate with a device on the x86 architecture. The first is to send bytes to an I/O port. The second is through memory-mapped I/O.

Most devices in the x86 architecture are accessed through I/O ports. These ports are controlled by special system hardware that has access to the data, address, and control lines on the processor. By using special hardware instructions to read and write from these ports, we can use I/O ports without infringing upon the normal address space of the kernel or user applications. This is because these special instructions tell the hardware that this "memory" reference is actually an I/O port, not a location in actual memory. For more information on I/O ports, consult chapter 10 of intel-arch.pdf. Both the timer and the keyboard use I/O ports. For convenience, an assortment of C wrapper functions are provided to you to save you from having to break into assembly language to read or write I/O ports. These are located in lib/inc/x86/pio.h. The various in functions read from an I/O port while the out functions write to an I/O port. The letter after the name indicates the size of the data being sent (b - byte, w - word/short, l - long/int). For this project, in which you will be talking only to primeval 8-bit I/O devices which date to the 1980's, you will probably need only the byte-wide versions of these instructions, i.e., outb() and inb().

There are also some devices which are accessed by reading and writing particular addresses in traditional memory. This is called "memory-mapped I/O". This kind of memory is part of the regular address space and therefore needs to be carefully managed. The video display hardware uses both memory-mapped I/O and I/O ports, so you must clearly understand the distinction.

Hardware Interrupts

A kernel programmer often uses either I/O ports or memory-mapped I/O to send commands to devices (ranging in size from single byte commands to many words). How do hardware devices communicate with the kernel? When a packet arrives at a network interface, the user presses a key on the keyboard or moves the mouse, or any other type of event occurs at a hardware device, that device needs a way of getting the attention of the kernel. One way is for the kernel to keep asking the device if it has something new to report (either periodically or continuously). This is called polled I/O. However, this is wasteful of CPU time, and there is a better mechanism. That mechanism is the hardware interrupt.

When a device wants to raise a hardware interrupt, it communicates this desire to one of two programmable interrupt controllers (PICs) by asserting some control signals on interrupt request (IRQ) lines. The PICs are responsible for serializing the interrupts (taking possibly simultaneous interrupts and ordering them), and then communicating the interrupts to the processor through special control lines. The PICs tell the processor that a hardware interrupt has occurred and which request line the interrupt occurred on so the processor knows how to handle the interrupt. How devices are assigned to particular interrupt request lines is extremely complicated, but there are some conventions which are usually followed, displayed below.

2Second PIC
6Floppy Disk
8Real Time Clock
9General I/O
10General I/O
11General I/O
12General I/O
14IDE Bus
15IDE Bus

The PIC chip used in old IBM compatibles only had 8 interrupt request (IRQ) lines. This proved to be limiting, so a second PIC was daisy chained off of the first one. When an interrupt is triggered on the second PIC, it in turn triggers an interrupt on the first PIC (on IRQ 2). The interrupt is then communicated to the processor.

Once the processor receives an interrupt from the PIC, it needs to know how to respond. The processor reads a data structure called the interrupt descriptor table (IDT). There is a descriptor in this table for each interrupt. A descriptor contains various information about how to resolve the interrupt, most importantly where the interrupt handler is located. The interrupt handler is a piece of code that the author of the device driver writes that gets executed when that device issues an interrupt. The IDT also stores the privilege level that is required to call the handler (DPL) and the segment to use while running the handler (which determines the privilege level to run under). Once the processor locates the appropriate entry in the IDT, it saves some information about what it was doing before the interrupt occurred, then starts executing at the address of the interrupt handler. Once the interrupt handler has run to completion, the processor uses the saved information to resume its previous task. Note that IRQ numbers do not necessarily correspond to matching indices into the IDT. The PICs have the ability to map their IRQ lines to any entry in the IDT. You will be given information on which IDT entries correspond to interrupts pertaining to your projects. For Project 1, the IDT entries will correspond to the timer interrupt and the keyboard interrupt. These IDT entries are further discussed in their respective sections.

Interrupt handlers typically need to execute as quickly as possible so that the processor is free to accept future interrupts. When an interrupt is sent by the PIC, the PIC will not send another interrupt from that same source until it gets acknowledged through an I/O port. This is because interrupt handlers usually manipulate critical data structures and would not withstand being interrupted by new invocations of themselves (i.e. they are not reentrant). In particular, an interrupt handler must never block on anything. Most interrupt handlers simply make a note of work that must be done as a result of the interrupt, clear the interrupt, then terminate, leaving the work to be done at a more convenient time. Note that it may be possible for one interrupt handler to be interrupted by a different interrupt handler, so long as they do not share data structures.

Software Interrupts and Exceptions

Hardware interrupts are not the only type of interrupt. Programs can issue software interrupts as well. These interrupts are often used as a way to transfer execution to the kernel in a controlled manner, for example during a system call. To perform a software interrupt a user application will execute a special instruction (INT n) which will cause the processor to execute the nth handler in the IDT (we will see this in Project 3).

In addition to hardware and software interrupt handlers, the IDT also contains information about exception handlers. Exceptions are conditions in the processor that are usually unintended and need to be addressed. Page faults, divide-by-zero, and segmentation faults are all types of exceptions. We will cover both software interrupts and exceptions in more detail later, especially in Project 3. For now, we just need to look at how to install an interrupt handler.

Configuring Interrupts

As mentioned previously, an x86 processor uses the interrupt descriptor table (IDT) to find the address of the proper interrupt handler when an interrupt occurs. To install your keyboard (and timer) interrupt handler, you will have to install an entry in this table.

An entry in the IDT can be one of three different types: a task gate, an interrupt gate, or a trap gate (a "gate" is just a descriptor that contains information about a transition to a different piece of code - think "gate to new procedure"). We will not be using task gates because they make use of the processor's hardware task switching functionality (which we also will not use for reasons we will discuss later in the course). The difference between an interrupt gate and a trap gate is that when the processor accesses an interrupt handler through an interrupt gate, it disables all further interrupts by clearing a flag in one of the processor's registers. Handling interrupts through trap gates does not do this.

For the interrupts in this project, we will be using trap gates (in later projects you will probably use a mixture of trap and interrupt gates). Note that a single interrupt source (timer, keyboard, etc.) will not signal a new interrupt to the processor until the processor has indicated that handling of the previous interrupt is ``done'', even if the system-wide interrupt-enable flag is on. You will need to give careful consideration to how long it is reasonable to disable interrupts globally and how long it is reasonable to wait before acknowledging a particular interrupt.

The format of the trap gate is given on page 151 of intel-sys.pdf. Note that regardless of the type of the gate, the descriptor is 64 bits long. You can use this fact to index into the IDT without knowing the type of the other gates in the table. To get the base address of the IDT, we use an instruction called SIDT. We have provided a C wrapper function, sidt(), for this instruction so you do not have to break into assembly. This is supplied in lib/inc/interrupts.h.

Initially, the PICs are programmed so that they call entries in the IDT that conflict with processor exceptions. The pic_init() function in lib/inc/x86/pic.h remaps the PICs to higher IDT entries. We have provided you with a skeleton version of night.c which shows you how and when to call pic_init(). Some fields in the trap gate are self-explanatory, however others are not so they are summarized here:
DPLThe privilege level required to call the handler. If it is set to 3, then user processes can call the handler directly. For hardware interrupts (like the keyboard and timer), it should be set to 0.
OffsetThe offset into the segment of the interrupt handler. Since all of our segments take up the whole address space, this is simply the address of your function handler. This can be obtained in C by typing the name of the function (without any parenthesis or arguments).
Present (P)Must be set to 1 for a working interrupt handler.
Segment SelectorThe segment selector for the target code segment. This defines the privilege level the handler will run at. The interrupt handler is going to be executed at PL0 and it is code, so this needs to be set to KERNEL_CS_SEGSEL.
DSize of the gate. All our gates are 32 bits (D = 1).

This may be the first time you are required to write code which packs a data structure into a hardware-defined memory format. There are several different ways to write this kind of code. Please make sure that, regardless of the approach or style you use, your code is structured in a reasonable way.

Writing an Interrupt Handler

As mentioned above, when the processor receives an interrupt it uses the IDT to start executing the interrupt handler. Before the interrupt handler executes, however, the processor pushes some information onto the stack so that it can resume its previous task when the handler has completed. The exact contents and order of this information is presented on page 153 of intel-sys.pdf. In Project 1 there are no user tasks, so the processor will be executing in PL0 all the time. This means interrupts will not cause a privilege level change, so the EFLAGS (a system register containing an assortment of important flags), current code segment, and the instruction pointer will be pushed on the stack. There is no error code - this is for exception handlers.

This information is indeed enough to resume executing whatever code was running when the interrupt first arrived. However, in order to service the interrupt we need to execute some code; this code will clobber the values in the general purpose registers. When we resume executing normal code, that code will expect to see the same values in the registers, as if no interrupt had ever occurred. So the first thing an interrupt handler must do is save all the general purpose registers, plus %ebp. We need not save the stack pointer because if there is no stack change (as is the case in this project since we have no user processes), the stack pointer will be correct once we pop off all of our interrupt-related information. If there is a stack change (as there will be in Project 3 when you do have user processes), the interrupt saves the stack pointer for us. So, to recap, the registers you need to save are %eax, %ebx, %ecx, %edx, %esi, %edi, and %ebp.

The easiest way to save these registers is to just save them on the stack. This is easily done with the PUSHA and POPA instructions (more information on these can be found on pages 624 and 576 of intel-isr.pdf). To save the registers before anything else occurs, part of your interrupt handler, which we will call a "wrapper", will be written in assembly language. All you must do in assembly is save the registers, call your C handler (try the CALL instruction), then restore the registers. To return from an interrupt, we recommend the IRET instruction, which you doubtless will wish to read up on. It uses the information initially saved on the stack by the interrupt (EFLAGS, code segment, instruction pointer) to return to the code executing when the interrupt occurred.

A final note about writing assembly. Comment your assembly profusely. One comment per instruction is not a bad rule of thumb. Keep your assembly code in separate files, and to export a symbol (e.g., asm_timer_wrapper) so it is visible outside the file containing it, use the .globl directive:

.globl asm_timer_wrapper

For those of you who have not written assembly before, here is the expected form:

.globl function_name
     assembly_instruction1 #comment
     assembly_instruction2 #comment
     assembly_instruction3 #comment

Assembly files normally have a .s or .S extension. The two extensions have different meanings to gcc; gcc will run the preprocessor on assembly files named with .S, while it passes files ending in .s to the assembler unaltered. Since you'll probably want to use #defines and other preprocessor statements within your assembly code, we strongly suggest naming your assembly with the .S extension. Note that the C preprocessor will swallow C-style comments in your .S files so they won't annnoy the assembler.

For C code to call your assembly-language functions, you should declare them in a header (.h) file. This is a good place for you to place doxygen comments for the assembly-languge code.

Console Device Driver Specification

The job of the console device driver seems simple: take characters and print them on the screen. For the most part, it is that simple. Keep in mind that this console will be the front end of your kernel for the rest of the semester, however. For this reason you will probably want it to do a few basic things like scroll when the output reaches the bottom of the console, line wrap when a line exceeds the width of the console, etc. The first thing to do is to understand how to print a single character to the console.

Writing a Character to the Console

When operating in primeval text mode, the job of a PC graphics adaptor is to convert a rectangular array of character-display information into a rectangular array of beta radiation, which is beamed toward your face by a linear accelerator. Much of this radiation is converted to photons by a layer of toxic phosphorous compounds. Starting Project 1 early will afford you the opportunity to take occasional breaks from exposure to this complex mixture of subatomic particle radiation.

The pixel pattern displayed by the graphics adaptor is controlled by a region of main memory (memory mapped I/O). Each character on the console is represented in this region by a byte pair. The first byte in this pair is simply the character itself. The second byte controls the foreground and background colors used to draw the character. These byte pairs are stored in row-major order. We have configured your graphics controller to paint a screen consisting of 25 rows of 80 characters each. The location of video memory, as well as the color codes used for the second byte of each pair, are defined in 410kern/lib/inc/video_defines.h.

Writing a character to the console is as simple as writing a byte pair to video memory. Assuming the top left corner is (0,0), to write the character 'M' on the 2nd character of the 1st line, you would do something like this:

*(char *)(CONSOLE_MEM_BASE + 2*(CONSOLE_WIDTH + 2)) = 'M';
*(char *)(CONSOLE_MEM_BASE + 2*(CONSOLE_WIDTH + 2) + 1) = console_color;

where CONSOLE_MEM_BASE is the start location of the console memory, CONSOLE_WIDTH is the width of the console in characters (defined to be 80), and console_color is a variable containing the current foreground and background color specification of your choice.

While our support code has already put the hardware in 80x25 format, you will also need to reposition or hide the hardware cursor. The hardware cursor is controlled by the Cathode Ray Tube Controller (CRTC), an important device on the video card. Communication with the CRTC is accomplished with a special pair of registers. Accessing these registers is a little different from writing to console memory as they use the I/O ports (discussed earlier).

Communicating with the CRTC

The CRTC is one of the devices which is accessed through I/O ports. It has two registers, an index register and a data register. The index register tells the CRTC what function you are performing on it, such as setting the hardware cursor position. The data register then accepts a data value associated with the operation. Because the data register is only one byte long, and the offset of the hardware cursor (which is measured in single bytes, not two-byte pairs) is a 16-bit integer, setting the hardware cursor position is done in two steps. The commands to send to the CRTC, as well as the location of the CRTC I/O ports, are defined in 410kern/lib/inc/video_defines.h. To hide the hardware cursor completely, simply set the position to an offset greater than the size of the console.

Console Device Driver Interface

Now that you know how to write characters to the console and move/hide the hardware cursor, you have the primitives you need to build a console device driver. Here is the API that you need to implement for the driver. Note that the API description refers to a logical cursor, a sometimes-visible indicator of where the next character will be placed on the screen. You are responsible for manipulating the hardware cursor-offset register as necessary to make sure the logical cursor's visual appearance on the screen is correct. Please make sure your code for doing this is simple and easy to understand.

int putbyte( char ch );
Description: Prints character ch at the current location of the cursor. If the character is a newline ('\n'), the cursor should be moved to the first column of the next line (scrolling if necessary). If the character is a carriage return ('\r'), the cursor should be immediately reset to the beginning of the current line, causing any future output to overwrite any existing output on the line. If backsapce ('\b') is encountered, the previous character should be erased (write a space over it and move the cursor back one column). It is up to you how you want to handle a backspace occurring at the beginning of a line.
Returns: The input character.

void putbytes(const char* s, int len);
Description: Prints the character array s, starting at the current location of the cursor. If there are more characters than there are spaces remaining on the current line, the characters should fill up the current line and then continue on the next line. If the characters exceed available space on the entire console, the screen should scroll up one line, and then printing should continue on the new line. If '\n', '\r', and '\b' are encountered within the string, they should be handled as per putbyte. If len is not a positive integer or s is null, the function has no effect. When you scroll the screen, you are not required to remember characters which are "pushed off" of the screen.
const char* s: The character array to be printed.
int len: The number of characters to print from the array.
Returns: Void.

void draw_char(int row, int col, int ch, int color);
Description: Prints character ch with the specified color at position (row, col). If any argument is invalid, the function has no effect.
int row: The row in which to display the character.
int col: The column in which to display the character.
int ch: The character to display.
int color: The color to use to display the character.
Returns: Void.

char get_char(int row, int col);
Description: Returns the character displayed at position (row, col).
int row: Row of the character.
int col: Column of the character.
Returns: The character at (row, col)

void set_term_color(int color);
Description: Changes the foreground and background color of future characters printed on the console. If the color code is invalid, the function has no effect.
int color: The new color code.
Returns: Void.

void get_term_color(int* color);
Description: Writes the current foreground and background color of characters printed on the console into the argument color.
int* color: The address to which the current color information will be written.
Returns: Void.

void set_cursor(int row, int col);
Description: Sets the position of the cursor to the position (row, col). Subsequent calls to putbyte or putbytes should cause the console output to begin at the new position. If the cursor is currently hidden, a call to set_cursor() must not show the cursor.
int row: The new row for the cursor.
int col: The new column for the cursor.
Returns: Void.

void get_cursor(int* row, int* col);
Description: Writes the current position of the cursor into the arguments row and col.
int* row: The address to which the current cursor row will be written.
int* col: The address to which the current cursor column will be written.
Returns: Void.

void hide_cursor();
Description: Causes the cursor to become invisible, without changing its location. Subsequent calls to putbyte or putbytes must not cause the cursor to become visible again.
Returns: Void.

void show_cursor();
Description: Shows the cursor. If the cursor is already shown, the function has no effect.
Returns: Void.

void clear_console();
Description: Clears the entire console and resets the cursor to the home position (top left corner). If the cursor is currently hidden it should stay hidden.
Returns: Void.

Timer Device Driver Specification

Unlike the console, the timer generates interrupts that must be handled properly. If you do not handle timer interrupts quickly enough, the timer will generate the next timer interrupt before the PIC has been reset by your timer interrupt handler, and a timer interrupt will be lost. This may not seem like a big deal, but even if you miss just 0.1% of the interrupts, after one day your clock would already be minutes off.

The actual timer interrupt handler in this project is quite simple, although in Project 3 you will be using the timer interrupt to trigger your scheduler. For this project, you'll be using the timer interrupt for to measure elapsed times.

Configuring the Timer

Communicating with the timer is done through I/O ports. These I/O ports are defined in 410kern/lib/inc/timer_defines.h. Also defined in 410kern/lib/inc/timer_defines.h is the internal rate of the PC timer, 1193182 Hz (please memorize this number, as it will be used as an encryption key in a final exam question). Fortunately, we can configure the timer to give us interrupts at a fraction of that rate. For Project 1, you should configure the timer to generate interrupts every 10 milliseconds.

To initialize the timer, first set its mode by sending TIMER_SQUARE_WAVE (defined in 410kern/lib/inc/timer_defines.h) to the TIMER_MODE_IO_PORT. The timer will then expect you to send it the number of timer cycles between interrupts. This rate is a two-byte quantity, so first send it the least significant byte, then the most significant byte. These bytes should be sent to the TIMER_PERIOD_IO_PORT.

When the timer interrupt occurs the processor consults the IDT to find out where the timer handler is. The index into the IDT for the timer is TIMER_IDT_ENTRY. You will need to fill in this IDT entry for your timer handler to execute properly.

Your timer interrupt handler should save and restore the general purpose registers. You also need to tell the PIC that you have processed the most recent interrupt that the PIC delivered. This is done by sending an INT_CTL_DONE to one of the PIC's I/O ports, INT_CTL_REG. These are defined in 410kern/lib/inc/interrupts.h.

Note: You will be testing this on an instruction set simulator. Even though you are simulating an older processor on relatively fast machine, Simics does not make an effort to exactly correlate the simulation to real wall clock time. If you have it set up properly, it will run in real time on real hardware.

Timer Device Driver Interface

Your timer interrupt handler is required to call an application-provided callback function, void tick(unsigned int numTicks), on every interrupt it catches. The numTicks argument should be the total number of timer interrupts your handler has caught since your program began running. Each application linked against your device-driver library will provide its own tick() implementation (which may immediately return to your handler). The tick() function is declared in inc/410_reqs.h.

Keyboard Device Driver Specification

Like the timer, the keyboard is also interrupt driven. However we are not only interested in the fact that a keyboard interrupt happened; each keyboard interrupt also has data that comes along with it. The information retrieved from the keyboard also needs to be processed in order to turn it into a stream of intelligible characters suitable for delivery to application processes. At a high level, the keyboard device driver provides a buffer that contains characters that are returned by the readchar() function.

Interacting with the Keyboard

One would think that reading keys from the keyboard would be as simple as receiving a simple character stream. Unfortunately it is not that easy. For one thing, both key presses and key releases are reported by the keyboard via interrupts. The other complicating factor is that the data reported by the keyboard is in a special format called scan codes. These codes need to be converted into normal ASCII characters. For your convenience, we provide a function that converts a scan code into an ASCII character. This function (called process_scancode) is in 410kern/lib/inc/keyhelp.h. The process_scancode function maps the scancode returned by the keyboard hardware to an ASCII character, or returns -1 if the keyboard event does not map directly to a character. For example, releasing the right "shift" key will generate a "right shift key released" scan code, but process_scancode() will typically map this to -1 instead of to an ASCII character.

The index into the IDT for the keyboard is KEY_IDT_ENTRY (defined in 410kern/lib/inc/keyhelp.h). When you receive a keyboard interrupt, your keyboard handler needs to read the scancode from the keyboard by reading a byte from the keyboard's I/O port (KEYBOARD_PORT defined in 410kern/lib/inc/keyhelp.h). You also need to tell the PIC that you have processed the keyboard interrupt. This is done by sending a INT_CTL_DONE to one of the PIC's I/O ports, INT_CTL_REG.

So that your device driver library might be used by the widest range of applications, such as a real-time game, we will ask you to structure your keyboard interrupt handler so it does as little work as reasonably possible. In particular, you should postpone processing the scan code until you are no longer inside an interrupt handler. For Project 1 you should assume that the process_scancode function is expensive timewise, so that performing that processing in the keyboard interrupt handler might disrupt the application.

Do not forget that your keyboard interrupt handler needs to save and restore the general purpose registers! Also, we have included functions that enable and disable interrupts in 410kern/lib/inc/x86/proc_reg.h (namely enable_interrupts() and disable_interrupts() respectively). You will most likely need to use them once in your keyboard driver to ensure there are no interrupt-related concurrency problems. Certain implementations may not need them. If you believe this is the case for your implementation, please add a comment explaining why this is so.

Keyboard Device Driver Interface

The keyboard device driver has a very simple interface - it is just one function, readchar().

int readchar();
Description: Returns the next character in the keyboard buffer. This function does not block if there are no characters in the keyboard buffer.
Returns: The next character in the keyboard buffer, or -1 if the keyboard buffer does not currently contain an ASCII character.

Skeleton Kernel

There is a lot of functionality missing in our bare kernel - there is no virtual memory and no filesystem, and none of the devices are initialized. However, we have taken care of some of these things for you, such as bootstrapping. We also have provided many of the standard C library functions you know and love (see the next section). The entrypoint into the kernel is the kernel_main() function in kern/night.c, which is called after bootstrapping and does some setup and then should run an infinite while loop. In addition, we have provided kern/console.c containing empty functions which you must implement and kern/inc/console.h containing the prototypes for those functions to get you started. A few other files are provided containing empty function bodies for the functions declared in inc/410_reqs.h, which you should read but should not modify.

Before handing in your project, you should make certain that your drivers work with our test code, provided in 410test.c. To test your drivers against the provided test code, build your project using the 410test make target using "make 410test". Please note that this make target does not update the project's support files automatically; you should run make (or make afs or make web), followed by make clean, before running make 410test.

Provided C Functions

We have provided many of the standard C library functions for you. To see which functions, feel free to look at the following header files in the 410kern/lib/inc directory:

Although various printing functions are provided in stdio.h (namely printf(), putchar(), and puts()) they will not work until you implement putbyte() and putbytes().

Also, we have provided a function called lprintf_kern() (in 410kern/lib/inc/stdio.h) and a MAGIC_BREAK macro (in 410kern/lib/inc/kerndebug.h) that you may find useful for debugging purposes. lprintf_kern() works like printf(), but will send its output to Simics (not to the console you're writing) and to the kernel.log file. Unlike printf(), lprintf_kern() does not depend on any of your console device driver functions. Whenever a MAGIC_BREAK is encountered within your code, Simics will act as if it has reached a previously-set breakpoint and will enter debugging mode.


It is very important to thoroughly test the console, timer, and keyboard driver on your own before moving on to implement your game. Your game (described next) will NOT test the complete functionality expected in these drivers. You will be provided with a 410_test.c that you can use to make sure your drivers are working. This will NOT be a complete test. We will run our own version of 410_test.c and other tests to check your functionality. Extensive testing is also in your best interest since you will use your own drivers again for Project 3.

Tying it All Together

Congratulations. By now you should have a console driver, a timer driver, and a keyboard driver. These can now be used by any application which is linked against them, such as 410_test or your game application. The build set up should already link 410_test.c and night.c against your drivers. It is each application's responsibility to provide a kernel_main() function and the tick() function mentioned above. When you launch Simics, you will be given the choice to run either night or 410test (see the BIOS and Bootloader section below).

Atari introduced the Night Driver arcade game in 1976, when Professor Maggs was thirteen. As you can imagine, it made quite an impression. The object of this game is to drive as quickly as possible along a curvy, unlit, road on a moonless night. We're talking pitch black here. Of course when you drive at night, you run the risk of veering off the road into one of the deep and booby-trapped ditches on either side. Fortunately for you, the Federal Government has conscientiously applied the windfall of tax dollars collected from .com millionaires through the pernicious Alternative Minimum Tax. At the very edge of the road, on either side, they have placed reflectors. You should not drive over these reflectors, for if you do, disaster will ensue.

In the arcade version of the game, the driver controlled the car with a steering wheel, gear shift, and gas and brake pedals. (What, no clutch?) The score was based on how quickly the driver covered a fixed course (called a "level") without crashing (i.e., running over a reflector). The video display was very simple. It was black, with white reflectors scrolling off the bottom of the screen as the car passed them. Here's a screen shot of the arcade version of Night Driver from the video game emulator MAME. (This screenshot was taken from the web site Arcade@Home.)

The white box at the bottom indicates the width of the car. (You can think of the box as the hood of the car, as seen through the windshield.) The car itself never moved at all on the screen. Note that a much lesser home version of Night Driver was released by Atari in 1978. A serious gamer would not confuse the home version with the real thing.

You will write a simple version of Night Driver, in which the only control for the car is the steering wheel. (Since you do not have a steering wheel input device, you will use the keyboard to steer.) Your steering mechanism can be very simple, e.g., pressing the key to turn the steering wheel left might simply move the car one character to the left. You may either move the image of the car left and right, or you may keep the car in a fixed position, and move the reflectors left and right.

Your program must scroll the reflectors down the screen in real-time, and detect if the car has run over a reflector. The car may move along the road at a constant velocity, i.e., the reflectors may stream by your car at a constant rate. The car should not move up and down. The intent is to provide a driver's perspective of the road. It is NOT necessary, however, to draw the road in perspective (i.e., narrower at the top of the screen), as the arcade version did. The game should be playable in SIMICS WHEN SITTING AT AN ANDREW WORKSTATION, as that is how we will test it. A driving level should not be too long, i.e., it should not take more than thirty seconds to complete. (Unfortunately, throughout this assignment you will find yourself annoyed by the differences in speed between simics at an andrew workstation, simics over the network, and real PC hardware.)

If the the car hits a reflector, the game should stop and issue a suitably stern admonishment, and then give the driver the option of starting over. Otherwise, if the driver completes the level, the program should the tell the driver how much time has elapsed. The driver should then move on to the next level. It is not necessary to give a "score" to the driver.

You do not have to worry about designing roads. We will provide a pseudo-random-road generator that you can use to create a curvy road "on the fly". This is contained in 410kern/levels.c.

The levels API is very simple. There are two functions for you to call, seed_levels and init_levels. seed_levels takes in an int with which it will seed the random number generator. After the random number generator has been seeded you can call init_levels which will generate the levels for you. init_levels takes in the number of levels you wish to generate, the height (in lines) that each level should be, and some bounds information. All levels generated will be placed in the levels [] struct. More documentation is in 410kern/inc/levels.h.

Feel free to write your own levels or level generator in place of ours and have fun with this!

  • We will require that a certain set of keys be supported for the game. If you want to support other input keys that is fine but you must support the following keys: 'f' for turning the steering wheel sharply to the left, 'g' for turning the steering wheel slightly to the left, 'h' for turning the steering wheel slightly to the right, 'j' for turning the steering wheel sharply to the right, and 'r' for reset (This is the most important key! Please don't forget it!).
  • The above description is a bare-minimum requirement for the project. Feel free to be more creative: add perspective, model momentum in steering, provide gears, brakes, and a gas pedal, or do whatever you think would be fun. If an idea seems too unusual, feel free to check with a TA before implementing it.


    As mentioned in the goals section, commenting is an important part of writing code. We will be using doxygen, which generates html documents similar to javadoc. Please see our doxygen documentation to see how to include comments in your code that can be read by doxygen. When we grade your projects, this is the first thing we will look at. Lack of documentation will be reflected in your grade. The provided night.c, console.c, and console.h files contain example doxygen comments with the sort of information we are expecting to see. Although we put the doxygen comments for our functions in the .h file, you can put yours in either the .h or .c file. In addition, we have provided a rule in the Makefile to take care of generating the documents for you. This rule is make html_doc and will be how we generate your documents.

    Other Important Notes

    • Since we will be running and testing your code on Andrew linux machines, your code will be compiled, linked, and run under gcc 3.2.1. If you are working on AFS, then you don't have to worry about anything. If you are working on a non-AFS machine, you can check the version of gcc you are using by running gcc --version on the command line. If your version is not 3.2.1, you must make sure that your code compiles, links, and runs fine under 3.2.1.
    • Please do not change any of the provided files in the 410kern/ subdirectory. We will run your code using our versions of the files, so any changes you make will be lost.
    • After writing putbyte in the console driver, if you have not already implemented the timer interrupt handler, you may see the message Unexpected Interrupt 0 appear on the console (multiple times). This is because the timer is sending interrupts which you are not yet handling. The default handler just prints a message, which uses a function that calls your putbyte function. Since you have finished this function, you are now seeing this message. When you write and install your timer handler, this message will disappear. However, until then, to make it go away you can either write and install a timer handler which does nothing, or you can make a call to disable_interrupts() right after our pic_init call in the kernel. Don't forget to remove that call later when you want to actually receive the timer and keyboard interrupts.
    • Be sure to put your driver initialization and installation code—and only your driver init and install code—into handler_install.c. We need this code in a consolidated place to be able to test your drivers independently of your game implementation.
    • As noted above, the drivers should be thought of as a library provided to your game application. Thus there should not be any game specific code in your driver implementations.
    • You should read the Software Setup Guide for more information on the software setup for this class and for information on files commonly found in the project tarballs.

    Testing and Debugging with Simics

    Debugging operating system code is typically quite difficult. One of way of doing it is to have two machines, one running a standard production OS like Linux, and the other running (and presumably crashing) the OS being developed. A serial cable is used to attach the two machines and special serial handlers installed on the development OS allow a debugger on the production OS to debug the development code. Fortunately, you will not have to deal with this. For the duration of this course, you will be testing and debugging your projects with Simics. Simics is an instruction set simulator. This means that every instruction that Simics runs is simulated, rather than run on real hardware as in VMware. For this reason Simics runs quite a bit slower than VMware. The amount of code in your operating system is small enough, however, that the difference will not be noticeable.

    Because Simics simulates every instruction executed, it has some powerful debugging capabilities. It allows, for example, breakpoints to be set on memory accesses to certain locations (specifically reads, writes, executes, or any combination thereof). It also allows temporal breakpoints - breakpoints that occur after a certain number of instructions have been executed. Like the popular debugger GDB (which most of you should be familiar with from 15-213) it has some symbolic debugger capabilities as well.

    By typing help at the Simics prompt, you will be presented with a list of different categories of commands. To see the commands in a specific category, type help CATEGORY. To get help on any of these specific commands, type help COMMAND. Please see our Simics Commands Guide for a list of important commands to know.

    Getting Started with Simics

    Simics has been installed on AFS and thus may be used from any Linux machine connected to AFS, subject to licensing restrictions. If you have added the 15-410 bin directory to your path, you can execute simics on your kernel by executing You may also use a Solaris machine, but your code still must be compiled and run on a Linux machine (you can ssh into and compile and run there).

    Instructions on getting Simics to work within your development environment are on the Projects section of the course website.

    The BIOS and Bootloader

    Before your kernel starts running, you will always see the BIOS startup followed by the bootloader. We chose GRUB as our bootloader for various reasons which we will not go into here. The bootloader has been configured to present you with a boot menu consisting of two choices. You will be able to choose between running your video game or the 410_test program.

    A Note on "Triple Faults"

    At some point in your simics career, you will probably encounter a "triple fault" condition. This means that while your operating system was running it ran into an exception, and while that exception was being handled a second exception was raised, and while that exception was being handled a third one was raised. At this point your average PC would just blank the screen and reboot, leaving you eternally mystified about the cause (in contrast, most non-PC computer systems provide a way to view an exception traceback). One of the nice things about simics is that it lets you easily debug this most difficult situation.

    Triple faults are easy to run into in Project 1. Since you are not installing many fault handlers, most IDT entries are invalid. If, for example, your Project 1 code makes an invalid memory reference, a protection fault could be raised. Since you have not installed a protection fault handler, there is a good chance that referencing the nonexistent handler will cause a "segment not present" exception to be raised. Since you have not installed a "segment not present" handler...

    For more information see the 410 Triple Fault Page.

    The meaning of each fault condition is described in Chapter 5 of intel-sys.pdf.

    Grading Makefile and Dual Images

    Your code should (already) be structured so that the kernel_main() function in your night.c file, which implements your game, links with the console API, the keyboard API, and the timer device driver, which are implemented by code contained in the external modules (.o files) specified as $KERNEL_OBJS in your Don't forget to provide the callback function required by the timer API.

    The 410test program provides a different kernel_main() function which will link against the same $OBJS list and will call your console and keyboard API functions. Before it does that, however, it will call handler_install() so you can set up your interrupt handlers and initialize global variables your drivers might need.

    Hand-in Instructions

    You will be required to hand in all your .c, .S, .h, and any other files necessary to run your code. Minimally this will include the code for your console driver, timer driver, keyboard driver, and your game. When we run your code, it should display the behavior described in the Tying It All Together section above.

    See for details.

    Where do I start???

    Up to you! But here are some suggestions.

    1. You'll probably need to read this assignment document and the lecture notes more than once.
    2. You might want to write down a brief outline in a text file, specifying which files you intend to organize your code in and which functions will be written in each. If you don't want to outline each function, you might want to write down, for each function, a list of issues you don't know how to solve.
    3. Set yourself an initial goal, such as displaying your name in the middle of the screen in some approximation of your favorite color. This will take longer to accomplish than you think.

    [Last modified Friday January 21, 2005]