Thurs., Sept. 15: Programming the Hardware:  Using I/O Ports

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

     EFLAGS, control and status

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

 and this is a quantity which is either 0 or has the one bit on, i.e., equals UART_DR, the one-bit mask.

Recall that in C, there is no Boolean type, and instead we use ints with 0 representing FALSE, and any non-0 value representing TRUE.

Thus the expression (stat & UART_DR) is TRUE if the DR bit is on and FALSE otherwise.

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.

 

Intro to x86 Interrupts

  

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.