Handout:
makefile doc
Ref:
Notes
on using C
to access hardware registers, Tan, 5.1 to pg. 274.
The
(32-bit) x86 architecture specifies 32-bit memory addresses and,
separately,
16-bit i/o port numbers. A
device is
assigned a little block of i/o port numbers for communication over the
bus. For example,
COM2 has 0x2f8-0x2ff,
and COM1 has 0x3f8-0x3ff. This
is very
standard across all PC models and vendors.
x86
CPU Registers
EAX, 32 bits, AX, its low 16 bits,
AL, its low 8
bits, and imilarly
EBX, BX, BL,
ECX, CX, CL,
EDX, DX, DL,
a few other general registers
ESP, 32-bit stack pointer
EIP, 32-bit instruction pointer,
also known as the
program counter or PC for short
Gnu
assembler uses %eax for EAX, %ax, %al, etc. It turns the order of
operands
around from the Intel syntax, which is weird, but we’ll use
it anyway since
it’s the Linux assembler that goes with our software.
IN and OUT instructions,
the x86 i/o instructions.
In
Gnu assembler, for 8-bit i/o, what we’ll be using:
outb
%al, %dx
Put
8-bit data to output in %al, i/o port number in %dx (CPU registers)
Execution
puts the 8 bits of data in the device register specified by the i/o
port
number, by sending it over the bus.
inb
%dx, %al
Put
i/o port number in %dx.
Execution
gets the 8 bits of data from the device register specified by the i/o
port
number, by sending it over the bus, into the CPU register %al.
PC Serial port device (COM1 or COM2, a
“UART”)
Each
has 8 I/O ports, but
luckily we only need to use a few of them.
COM2’s
“base port” is
0x2f8. Its 8 ports
are 0x2f8, 0x2f9,
0x2fa, ..., 0x2ff. Other
devices have
other I/O port assignments.
See
$pcinc/serial.h for def—
#define COM2_BASE 0x2f8
Each
UART has several
registers accessible over the bus by various i/o ports:
Transmit
register
Receiver
register
IER—interrupt
enable register
LSR—line
status register
others
we don’t need to use…
BTW,
what’s a register? It’s
an array of
hardware bits, usually in flip-flops.
You
can have a register chip on a breadboard holding bits completely
separately
from any computer.
Important
idea that the UART can have registers and hold
“state”, i.e. data, on its own
separate from the CPU.
The
UART’s base port, 0x2f8, is use for both input and output
using the in and out
instructions:
So
two registers in the UART are in use via the one i/o port. This is a common trick to
save on i/o
ports. The UART is
fully aware of the
difference between a read and a write access over the bus, so no
confusion
arises.
Luckily
we can avoid
programming in assembler by writing a C-callable assembler function
once and
for all to do each of these instructions.
These are in the SAPC support library, prototyped in
$pcinc/cpu.h:
ch
= inpt(port)
outpt(port,
ch)
Here
ch is an 8-bit quantity, usually an unsigned char.
For example, to output ‘A’ to COM2,
outpt(0x2f8, ‘A’);
But
we don’t want to use wired-in numbers like this in real
programs. We could
write:
outpt(COM2_BASE, ‘A’);
This
is better but still not perfect—base addresses are usually
accompanied with
offsets to say which port of the set is being used:
from
serial.h: offsets
for a PC serial port
device
#define
UART_RX
0
/* receiver reg */
#define
UART_TX
0
/* transmit reg */
#define
UART_IER
1
/* interrupt enable reg */
#define
UART_LSR
5
/* line status reg */
So
we end up with
outpt(COM2_BASE + UART_TX,
‘A’);
/* output ‘A’ to COM2,
spec. to COM2’s transmit buffer reg */
/*
using i/o port
0x2f8 + 0 = 0x2f8 */
Similarly,
if we know there’s a character ready to be read in from COM2:
ch = inpt (COM2_BASE +
UART_RX); /* input
char from COM2,
spec. from COM2’s receiver buffer reg */
/* using i/o
port 0x2f8 + 0 =
0x2f8 */
Note
that this involves communication with two different hardware registers
in the
serial device. The
byte being read or
written travels over the bus between the CPU and the device. The device knows when it
is selected (by
logic sensing the i/o port # on the address bus) and whether it is a
read or
write over the bus. Thus
there is no
ambiguity caused by using the same i/o port for both actions.
stat
= inpt (COM2_BASE + UART_LSR); /*
get
the current value of the line status register (8 bits) */
We usually want a particular bit from this byte, most commonly the DR bit for data-ready (receiver has a char) or THRE (transmitter can take another char). The mask for DR is #defined in serial.h with name UART_LSR_DR, and the mask for THRE is UART_LSR_THRE. These names come from the Linux serial driver sources.
Numbering bits: we count the rightmost bit as bit 0. The highest bit number in a byte is 7, in a 32-bit int it's 31.
Idea of mask for one bit: just that one bit is on. Here are some examples.
hex binary
Mask for bit 0: 0x01 0000 0001
Mask for bit 1: 0x02 0000 0010
Mask for bit 2 0x04 0000 0100
...
Mask for bit 7: 0x80 1000 0000
In general, mask for bit i can be written:
(1 << i)
This takes 1, a mask for bit 0, and left shifts it i bits, making it produce a mask for bit i. You'll see some expressions like this in serial.h.
We can test the one bit we want in stat by bitwise-anding it with the mask for the bit we want. Thus for the DR bit, we form
stat & UART_DR
The upshot is that can write these basic tests on COM2's status:
inpt(COM2_BASE + UART_LSR)& UART_DR TRUE or FALSE depending if the receiver is ready
inpt(COM2_BASE + UART_LSR)& UART_THRE TRUE or FALSE depending if the transmitter is ready
We can make a loop of inpt's testing the DR bit and thus wait for the UART to get a new character. Or a loop of the THRE inpt's to wait for the transmitter to be ready for another byte. For THRE:
while (inpt(COM2_BASE + UART_LSR)& UART_DR) == 0)
; /* not ready yet, keep trying */
/* here when transmitter is ready, OK to output another character */
outpt(COM2_BASE + UART_TX, ch);
This is like the code of Figure 5-7 in Tanenbaum, pg. 285, except that we use the IN instruction to get the device's statis register value instead of memory-mapped i/o.
Idea of “programmed
I/O”
When
the CPU loops testing a device ready bit, waiting for the device to
produce or accept
data, we call it “programmed I/O”, or
“polling for data”.
The loop is called a “busy loop” or a
“polling loop.”
The other alternative is
interrupt-driven I/O. Polling
I/O is
simpler and is commonly used in programs that don’t utilize
an OS for I/O, for
example, all ordinary programs on the SAPC.
When you write printf(“hi”) in the
SAPC environment, printf calls putc
in Tutor, which does a polling loop on COM2 (or whatever the console
device
is.) OS drivers
usually use interrupt-driven
I/O, so as not to waste CPU in busy loops.
Look
at echo.c. There
you see a polling loop
for input. Surprisingly,
there is no
polling loop for output, but this is a special case where the output is
slowed
down so much by the input that the transmitter will always be ready
when it’s
used.
We
have looked at COM2 here. COM1
is
exactly the same device, so just use COM1_BASE instead of COM2_BASE in
the
examples above. Also
read about LPT1 in
the linked document above.
Interrupts
Ref:
Slides
on Interrupts.,
Tan., pp 30-31, 279-294.
Intro.
Look at Tan. Fig. 5-5, pg. 279.
Add a device for COM2.
See how the interrupt controller (or PIC for
programmable int. controller) sits between the devices and the CPU,
much like a
secretary sits between clients and an executive.
Our software, running on the CPU, can tell
the PIC what signals to allow through to the CPU, and can control
whether the
CPU pays any attention to interrupts by changing the IF bit in EFLAGS,
the main
control register of the CPU.
Note on hw1: Problem 0 is due Friday midnight
(at very end of Friday).
You are ready to do problem 1 and
2a, 2b. Wait until
next week for 2c, the transmitter
interrupt part.
Notes on Tanenbaum’s
coverage on I/O.
p.
273: We use IN and OUT i/o instructions of the x86 CPU.
Although the x86 is capable of memory-mapped
i/o, it is not used with typical i/o devices.
It is used for video memory, for example.
p.
275: Don’t worry about bus
architecture—it’s hidden from us anyway. Just use the one-bus model
to think about how
it works.
p.
276: DMA—just know what this means.
Skip
details in 5.1.4.
p.
280: In line 8, the bold “interrupt vector” should
be “interrupt vector
number”, the number such as 0x23 for COM2 that tells the CPU
which entry of the
IDT (int. descriptor table) to use to load the interrupt vector itself,
i.e.,
the address of the int. service procedure (ISP).
p.
281: Don’t worry about pipelining and super-scalar. The bottom line is on p.
282, where he says
that the Pentium CPU does all the bookkeeping necessary to make sure
that an
interrupt happens in effect between two specific
instructions.
The interrupt signals going from a device
to the PIC to the
CPU. Additionally,
all three are on the
bus, so our programs can do IN or OUT instructions to certain i/o ports
to
communicate—the PIC has ports 0x20 and 0x21, and (for
example) COM2 has 0x2f8,
0x2f9, ...
It is crucial to think of each of the three participants in an interrupt as “living” an independent existence. Each can hold state (remember things) in hardware registers and is responsible for certain behavior over time. They talk to each other over the bus. Make a drawing of CPU, PIC, and interrupting devices all on a bus and show how the signals go from one to another.
The
fact that the EIP contains this address (the address of the interrupt handler in memory) means that
the
interrupt handler will run immediately, since the EIP (or PC) at each instruction holds the address of the next instruction to execute.
As the CPU executes the interrupt handler, it
is not aware of anything special—there is no
“interrupt-in-progress”
information in the CPU registers like there is in the PIC and the UART.