CS452 Hardware Information

The Eos systems

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.

Configuration of systems

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.

Interrupt Numbers

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.

others
NumberSuggested nameComments
0INT_DIVIDE_EXCEPT Occurs after a divide by zero
1INT_DEBUG_EXCEPT Results from single-stepping
2INT_NMI Non Maskable Interrupt: after button is pressed
3INT_BREAKPOINT Opcode 0xCC generates this, or \Funct{breakpoint()}
4INT_OVERFLOW_EXCEPTAn exception: numeric overflow
6INT_OPCODE_EXCEPT Bad opcode exception
8INT_DOUBLE_EXCEPT Two exceptions occurred ``simultaneously''
12INT_STACK_EXCEPT Trouble with the stack
13INT_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.

I/O Port Map

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.

FunctionDescription
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.

Serial ports (NS16550)

PortNumberComments
0x03F80Connected to train track.
0x02F81Connected to WYSE terminal.
0x03E82Unused/Unavailable
0x02E83Unused/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.

OffsetModeNameFunction (see data sheet for more information)
0x0wTHRTransmitter holding register
0x0rRBRReceiver buffer register
 r/wDLLBDivisor latch, low byte (when DLAB=1)
0x1r/wDLHBDivisor latch, high byte (when DLAB=1)
 r/wIERInterrupt enable register (when DLAB=0)
0x2rIIRInterrupt identification register
 wFCRFIFO control register
0x3r/wLCRLine control register
0x4r/wMCRModem control register
0x5rLSRLine status register
0x6r/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.

Timer (PIT)

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).

PortModeDescription
0x0040r/wCounter 0, counter divisor
0x0041r/wCounter 1, RAM refresh counter (unused)
0x0042r/wCounter 2, speaker tone control
0x0043r/wMode port, control word register for counters 0-2

The bits of the mode port are as follows:

Bits Meaning
bit 7-600counter 0 select
 01counter 1 select
 10counter 2 select
bit 5-400counter latch command
 01read/write counter bits 0-7 only
 10read/write counter bits 8-15 only
 11read/write counter bits 0-7 first, then 8-15
bit 3-1000mode 0 select
 001mode 1 select - programmable one shot
 x10mode 2 select - rate generator
 x11mode 3 select - square wave generator
 100mode 4 select - software triggered strobe
 101mode 5 select - hardware triggered strobe
bit 00binary counter 16 bits
 1BCD 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 (RTC)

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>.

IndexDescription
0x00current second in BCD
0x01alarm second in BCD
0x02current minute in BCD
0x03alarm minute in BCD
0x04current hour in BCD
0x05alarm hour in BCD
0x06day of week in BCD
0x07day of month in BCD
0x08month in BCD
0x09year in BCD (00-99)
0x0Astatus register A
 bit 7 = 1update in progress
 bit 6-4 divider that identifies the time-based frequency
 bit 3-0 rate selection output frequency and int. rate
0x0Bstatus register B
 bit 7= 0run
  = 1halt
 bit 6= 1enable periodic interrupt
 bit 5= 1enable alarm interrupt
 bit 4= 1enable update-ended interrupt
 bit 3= 1enable square wave interrupt
 bit 2= 1calendar is in binary format
  = 0calendar is in BCD format
 bit 1= 124-hour mode
  = 012-hour mode
 bit 0= 1enable daylight savings time. Only in USA, useless in Europe.
0x0Cstatus register C
 bit 7= 1Real-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.

Interrupt Control Unit (ICU)

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.

Keyboard Controller (PPI)

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.

Footnotes

  1. ...IRQ1: IRQs 0-7 going to the first ICU are mapped to interrupt numbers 32-39, while IRQs 0-7 going to the second ICU are numbered 40-47 as interrupts.
  2. ...summarized below2: DLAB is bit 7 of the LCR. When set, it changes the function of the first two registers.