Eos is the name given to the computer systems used to teach CS452/CS652. Each Eos system is a common PC system assembled from off-the-shelf components. The setup is chosen such that the underlying hardware is generic enough that hardware replacement is not a problem.
Eos is the Greek goddess of dawn. Since "Eos" is not an acronym, it should not be written in all caps.
The following sections serve to introduce the hardware of the Eos systems.
Because these computers are PCs, which have a long history of backwards compatability, a great deal of programming information is available freely on the Internet. Much of this information is likely applicable to these machines. But remember that we do not run MS-DOS, Windows, Linux or any other external operating system on these machines, so advice based on those platforms may rely on services not available on the "bare bones" hardware. Also, since the machine is run in protected mode, the services normally available from the BIOS are unavailable. The BIOS is only used to get the system started up, once your kernel starts it is, for all intents and purposes, gone.
The Eos systems used to follow a very particular specification. Here's a quote from a fairly recent lab manual:
... In fact, the parts were chosen so that replacement parts would be available well into the future.
Unfortunately we seem to now be beyond "well into the future" and 486DX2 CPUs and Mach32 graphics cards have become sparse to say the least.
Thus, we have aimed to instead try to use well-defined standards that don't rely on a particular make or model of components wherever possible. With that in mind, here are the important specifications for the machines:
Processor: Single Intel x86 compatible processor with integrated floating-point unit (e.g. a Pentium-II 400 processor).
Memory: At least 32 MB of RAM.
I/O Facilities:
Graphics: VBE 2 (VESA BIOS Extensions) compatible graphics card with at least 1024x768x16bpp resolution. Mode switching is performed by the GRUB boot loader.
Sound: Sound Blaster 16 compatible sound card.
Network: RTL8139 compatible network card on the PCI bus.
An "interrupt number" is an internal number. From the CPU's point of view it's the index into the IDT (Interrupt Descriptor Table). "IRQ numbers" are the numbers which people normally talk about when dealing with PC hardware. IRQ stands for Interrupt Request and refers to a physical wire on the backplane bus. Each device that wishes to generate an interrupt must be assigned an IRQ.
All 16 IRQ numbers are mapped into the range 32-47 of the interrupt numbers. Interrupt numbers below 32 are reserved by Intel for their use. You may define your own meaning for those 48 and above, since they are generated by software only.
External hardware interrupts (numbered from 32 to 47) are controlled by the ICUs (Interrupt Control Units: the i8259 chip). It is the ICU that manages the hardware IRQ lines and converts them to interrupt requests for the CPU. To get interrupts 32-47, you must unmask (i.e. turn on/enable) the interrupt in the ICU and get the hardware to generate the IRQ1. How to enable the IRQ varies from device to device. Some always generate interrupts, like the keyboard contorller. Other chips, like the USARTs, allow you to select what serial events will generate an IRQ (but there is only one interrupt line for each port, so if you ask for two interrupt sources, they are combined and you must poll in your handler).
The CPU by default ignores all interrupts (except NMI, which is
"not maskable" so it can't be ignored). To enable
interrupts, you must execute the instruction STI
which
changes a bit in the processor status word. Once you've done this,
interrupts from 0-31 might occur. Next, you must enable the i8259 in
question to allow it to pass-on a particular IRQ. Then, you have to
get the device to generate an interrupt. For the keyboard, it will
always generate an interrupt when a key is pressed. For the serial
ports, you must configure what event you want to cause an interrupt
in the chip. Most people configure the chip to generate an interrupt
whenever a character is received and whenever a character has been
transmitted.
In addition to all that work, you also have to configure the
i8259s such that they perform the mapping. Moreover, the CPU uses
a table (the IDT mentioned above) to decide what to do when an
interrupt occurs. However, much of that work has been done for you:
you just have to call the library function loadIdt()
found in examples/kernel/idt.cc
. This function call
sets every interrupt to point to a default handler which prints out
a message (and halts the machine). See the next chapter on software
for more details.
Some interesting interrupt numbers are shown below.
Number | Suggested name | Comments |
---|---|---|
0 | INT_DIVIDE_EXCEPT | Occurs after a divide by zero |
1 | INT_DEBUG_EXCEPT | Results from single-stepping |
2 | INT_NMI | Non Maskable Interrupt: after button is pressed |
3 | INT_BREAKPOINT | Opcode 0xCC generates this, or \Funct{breakpoint()} |
4 | INT_OVERFLOW_EXCEPT | An exception: numeric overflow |
6 | INT_OPCODE_EXCEPT | Bad opcode exception |
8 | INT_DOUBLE_EXCEPT | Two exceptions occurred ``simultaneously'' |
12 | INT_STACK_EXCEPT | Trouble with the stack |
13 | INT_GP_EXCEPT | GPF: General Protection fault, the catch-all exception |
others | See data book for other internal exceptions/interrupts. | |
(32+0) | INT_TIMER | (IRQ0 to first 8259) Interval timer interrupt |
(32+1) | INT_KEYBOARD | (IRQ1 to first 8259) Keyboard (key pressed) |
(32+4) | INT_SERIAL0 | (IRQ4 to first 8259) Serial port 0 event |
(32+5) | INT_SOUND | (IRQ5 to first 8259) Sound event (end of DMA) |
(32+3) | INT_SERIAL1 | (IRQ3 to first 8259) Serial port 1 event |
(32+8) | INT_RTC | (IRQ0 to second 8259) Real-time clock tick |   | Ethernet card, vertical refresh, parallel port, etc. |
Most hardware devices on PC machines are I/O mapped. That is, their
control registers appear in a separate address space dedicated to
I/O. Access to the "other" address space requires special
I/O instructions (see header file <machine/pio.h>
). In
particular, the functions listed in the following table can be used
to access I/O space.
Function | Description |
---|---|
u_char inb(u_short port) | Input a single byte from the given port. |
void insb(u_short port, void *addr, int cnt) | Input a block of bytes from the given port. |
u_short inw(u_short port) | Input a single word (2 bytes) from the given port. |
void insw(u_short port, void *addr, int cnt) | Input a block of words (2 bytes each) from the given port. |
void outb(u_short port, u_char data) | Output a single byte to the given port. |
void outsb(u_short port, void *addr, int cnt) | Output a block of bytes to the given port. |
void outw(u_short port, u_short data) | Output a single word (2 bytes) to the given port. |
void outsw(u_short port, void *addr, int cnt) | Output a block of words to the given port. |
The header file <machine/pc/isareg.h>
defines
most of the port numbers discussed here.
Port | Number | Comments |
---|---|---|
0x03F8 | 0 | Connected to train track. |
0x02F8 | 1 | Connected to WYSE terminal. |
0x03E8 | 2 | Unused/Unavailable |
0x02E8 | 3 | Unused/Unavailable |
Each serial port operates identically and consists of a number of registers. Each register has its own I/O port, the numbers above are just the base address of each set. The registers are summarized below2.
Offset | Mode | Name | Function (see data sheet for more information) |
---|---|---|---|
0x0 | w | THR | Transmitter holding register |
0x0 | r | RBR | Receiver buffer register |
r/w | DLLB | Divisor latch, low byte (when DLAB=1) | |
0x1 | r/w | DLHB | Divisor latch, high byte (when DLAB=1) |
r/w | IER | Interrupt enable register (when DLAB=0) | |
0x2 | r | IIR | Interrupt identification register |
w | FCR | FIFO control register | |
0x3 | r/w | LCR | Line control register |
0x4 | r/w | MCR | Modem control register |
0x5 | r | LSR | Line status register |
0x6 | r/w | Scratch register |
To gain access to a particular register of a serial port, take the
base I/O port address and add the offset of the register in
question. Thus, to read the "Line status register" of port
one, the command inb(0x02fd)
could be used. However,
all the useful values have been defined in the header
file <machine/serial.h>
. Therefore, you can also
write the above as:
#include <machine/serial.h> #include <machine/pio.h> char c = inb(USART_1_BASE + USART_LSR);
For more information on programming the serial ports, please consult the data sheets. You may find this web transcription convenient. The serial ports are implemented using an NS16550. This is an improved version of the Intel chip, i8250. The only addition is the on-chip FIFO and the NS16550 should be software-compatible with i8250 drivers.
The programmable interval timer is implemented with a very limited Intel chip, i8253. It has three independent counters. Each is 16 bits wide and the whole chip is clocked with just one clock frequency. There is no way (in hardware) to chain or cascade these counters.
The output of counter zero is connected to IRQ0, so it can generate an interrupt when terminal count is reached. Counter 1 is available, but its output pin is not connected to anything useful (DRAM refresh is done in dedicated hardware now). Counter 2 is used to generate square waves for the tiny speaker inside the case. Its output is gated by a bit in the PPI chip (keyboard controller).
Port | Mode | Description |
---|---|---|
0x0040 | r/w | Counter 0, counter divisor |
0x0041 | r/w | Counter 1, RAM refresh counter (unused) |
0x0042 | r/w | Counter 2, speaker tone control |
0x0043 | r/w | Mode port, control word register for counters 0-2 |
The bits of the mode port are as follows:
Bits | Meaning | |
---|---|---|
bit 7-6 | 00 | counter 0 select |
01 | counter 1 select | |
10 | counter 2 select | |
bit 5-4 | 00 | counter latch command |
01 | read/write counter bits 0-7 only | |
10 | read/write counter bits 8-15 only | |
11 | read/write counter bits 0-7 first, then 8-15 | |
bit 3-1 | 000 | mode 0 select |
001 | mode 1 select - programmable one shot | |
x10 | mode 2 select - rate generator | |
x11 | mode 3 select - square wave generator | |
100 | mode 4 select - software triggered strobe | |
101 | mode 5 select - hardware triggered strobe | |
bit 0 | 0 | binary counter 16 bits |
1 | BCD counter |
The header file <machine/pc/timerreg.h>
has a
good description of how to interact with this chip:
There are three mode registers and three countdown registers. The countdown registers are addressed directly, via the first three I/O ports. The three mode registers are accessed via the fourth I/O port, with two bits in the mode byte indicating the register. (Why are hardware interfaces always so braindead?).
To write a value into the countdown register, the mode register is first programmed with a command indicating which byte of the two byte register is to be modified. The three possibilities are load msb (TMR_MR_MSB), load lsb (TMR_MR_LSB), or load lsb then msb (TMR_MR_BOTH).
To read the current value ("on the fly") from the countdown register, you write a "latch" command into the mode register, then read the stable value from the corresponding I/O port. For example, you write TMR_MR_LATCH into the corresponding mode register. Presumably, after doing this, a write operation to the I/O port would result in undefined behavior (but hopefully not fry the chip). Reading in this manner has no side effects.
The same header has registers defined for this chip (including those mentioned above). Please see the data sheets for more details on this chip.
The frequency supplied to this chip is fixed at 1,193,182Hz. This means if you use 216-1 (0xffff) as a divisor, you'll get interrupts at a rate of 18.2Hz, just as MS-DOS does.
The following code sets the PIT to cause an interrupt every 20th of a second (20Hz).
#include <machine/pc/timerreg.h> outb(TIMER_MODE, TIMER_SEL0|TIMER_RATEGEN|TIMER_16BIT); outb(IO_TIMER1, TIMER_DIV(20)%256); outb(IO_TIMER1, TIMER_DIV(20)/256);
The ``real time clock'' on PC-compatible machines is meant to provide time and date functions---it is not a high-resolution or high-precision timer. The same chip, since it must maintain its time with the power off, also carries a small amount of battery-backed SRAM. Configuration values can be stored when the power is off in this memory.
The DS12887 chip, an implementation of the RTC, provides 114 bytes of NVRAM (non-volatile RAM), often called the "CMOS memory" due to the way it is fabricated. Please don't change the values in the NVRAM.
Ports 0x070 and 0x071 are used to access this chip. The first port (0x070) is an address port, and 0x071 serves as a data port. To access an address (one of 128) inside the RTC, first output the index (i.e., address) to port 0x070, then read or write the desired data from port 0x071. You must be careful to always follow a write to 0x070 some action on 0x071, or the RTC will be left in an unknown state.
The following table lists each important address and its
function. For the exact meaning of all these registers, consult the
data sheets. Some useful functions and defines can be found
in <machine/clock.h>
.
Index | Description | ||
---|---|---|---|
0x00 | current second in BCD | ||
0x01 | alarm second in BCD | ||
0x02 | current minute in BCD | ||
0x03 | alarm minute in BCD | ||
0x04 | current hour in BCD | ||
0x05 | alarm hour in BCD | ||
0x06 | day of week in BCD | ||
0x07 | day of month in BCD | ||
0x08 | month in BCD | ||
0x09 | year in BCD (00-99) | ||
0x0A | status register A | ||
bit 7 | = 1 | update in progress | |
bit 6-4 | divider that identifies the time-based frequency | ||
bit 3-0 | rate selection output frequency and int. rate | ||
0x0B | status register B | ||
bit 7 | = 0 | run | |
= 1 | halt | ||
bit 6 | = 1 | enable periodic interrupt | |
bit 5 | = 1 | enable alarm interrupt | |
bit 4 | = 1 | enable update-ended interrupt | |
bit 3 | = 1 | enable square wave interrupt | |
bit 2 | = 1 | calendar is in binary format | |
= 0 | calendar is in BCD format | ||
bit 1 | = 1 | 24-hour mode | |
= 0 | 12-hour mode | ||
bit 0 | = 1 | enable daylight savings time. Only in USA, useless in Europe. | |
0x0C | status register C | ||
bit 7 | = 1 | Real-Time Clock has power | |
bit 6-0 | reserved |
The high bit (7) of the RTC address port seems to gate the NMI input to the CPU, but there is no good documentation on this. Normally it is left clear. The interrupt output of the RTC is connected to IRQ0 of the second ICU (i.e., IRQ8) so it can be used to generate periodic interrupts. These interrupts can be configured to occur at a much higher rate than is possible with the PIT. In fact, with careful programming, it is possible to accurately measure times with microsecond resolution using this chip.
Please see the software section for a discussion of which timer chip (RTC or PIT) is the best for your kernel.
All hardware interrupts on an IBM PC are funneled through a chip called variously, the ICU (Interrupt Control Unit) or PIC (Programmable Interrupt Controller). The design is based on the Intel i8259. The function of this chip is to simplify the interrupts that the CPU sees. For example, you can mask (turn off) interrupts from a given device using the ICU. Also, the ICU prioritizes interrupts so that higher-priority devices will preempt lower-priority ones.
The first ICU is located at 0x020. The secondary (slave) ICU is located at 0x0a0. The first ICU controls 8 IRQs, one of which (IRQ2) is an IRQ from the secondary ICU, which controls another 8 IRQs. When the secondary ICU needs to cause an interrupt, it generates an interrupt for the other ICU. Their interaction is not quite as simple as that, however.
The documentation for this chip is extremely difficult to understand. What follows is a simplification, and learning more about this chip may turn out to be a painful exercise.
A function, loadIdt()
is provided in the sample
code. This function initializes and configures the i8259 for
you. You may want to examine the code it contains, but I don't
recommend changing how it works. After you have called that code,
your interaction with the ICU can be limited to masking and
unmasking individual interrupts, and sending EOI (End
Of Interrupt).
Each IRQ can be enabled or disabled individually. The code in
loadIdt()
disables all the IRQs on both chips. Therefore,
once your have a device generating IRQs (interrupt requests), you must
unmask that device's IRQ line in the ICU. You can do that with the
following function:
#include <machine/pc/isareg.h> #include <machine/pio.h> inline void maskIRQ(uchar irq, bool flag) { uint port = IO_ICU1+1; uchar val; if(irq >= 8) { /* an IRQ on the secondary ICU */ port = IO_ICU2+1; irq -= 8; } val = inb(port); if(flag) val |= (1<<irq); else val &= ~(1<<irq); outb(port, val); /* mask the interrupt enable */ }
If flag
is true
the IRQ
is disabled, while false
enables it. The
values IO_ICU1
and IO_ICU2
are efined in the
header <machine/pc/isareg.h>
as 0x020 and 0x0A0
respectively.
When an IRQ occurs, the ICU prioritizes it and decides (if that IRQ is unmasked) to signal the CPU. The ICU must be notified when the processing for that interrupt is complete, so that it knows when it is safe to interrupt the processor again. The signal to the ICU that does this is EOI. You must tell the ICU that the interrupt handling is complete, or else you will never get an interrupt again from that device (IRQs at higher priority will still occur). Use the following code to signal EOI.
outb(IO_ICU1, 0x020); /* non-specific EOI */ outb(IO_ICU2, 0x020); /* non-specific EOI */
This code will acknowledge whatever the last interrupt was from the ICU. It is possible to acknowledge individual IRQs (i.e., to service interrupts out of order) but you don't need to do that. To simplify things, this code sends an EOI to both ICUs, so that the same code can be used for IRQs from either chip. Technically, you shouldn't send an EOI to the secondary ICU if the IRQ was caused from an IRQ handled from the first ICU, but it doesn't cause any trouble.
The keyboard is controlled by an Intel i8042 chip. This chip is normally used for a Programmable Parallel Interface (PPI) but has be used over the years to add kludges to the PC for enhancements. Since you normally will never need to program this chip, it will not be extensively documented here.
The keyboard controller is located at 0x060 to 0x064 in I/O address space. If you want to read the PC keyboard, use the sample code provided to you.