; ; 6-May-98 ; ; RCS: $Id$ ; Lecture notes for Wednesday, 6-May-98 Example of interfacing a memory-mapped I/O device, including interrupts. handout: spec for serial controller 1. tour of I/O device functionality a. block diagram: dual serial controllers b. bus interface wires & visible locations when memory-mapped 0b11: adata 0b10: acontrol 0b01: bdata 0b00: bcontrol c. internal registers for programming. Wierd wierd wierd... The registers are advertised as 16 byte-wide locations, but in reality they are a bunch of random bits of state inside the chip. In other words, there isn't really a register file in the usual sense, but rather a bunch of muxes collecting state bits from all over the place. Further, the register-to-be-addressed is addressed in a funny way. By default, when you read/write acontrol/bcontrol you read/write RR0/WR0. BUT, you can read/write other registers by first writing a code into the bottom bits of WR0. This trick was put in most probably to save four pins on the IC. Dirt like this is common in I/O devices. It's part of the reason that writing device drivers is considered black art. d. Timing. Take a look at the timing diagrams following the register definitions... they define propagation delay/setup time/hold time/pulse width for basically every signal to every other signal... This is a consequence of the fact that this isn't really an edge-triggered, sequential device in the style we have adopted. 2. physical interfacing: pick a location in the address space & add circuitry to select the chip when that range of addresses is given. Also, wire up correct flavor of the control pins and the interrupt. Memory mapping a chip (memory or I/O) in a particular range of address implies that you will contrive to enable that chip _only_ when the memory address emitted by the processor falls within that range. For instance, to have the 8530 appear in the range 0x00010000 to 0x00010003 (four locations), you construct a circuit to produce an enable only when the top 30 bits of address are equal to 0b000000000000000100000000000000 (the bits common to every address in the range). You can do this with combinational logic. 3. software. First, the simple stuff. Here's some polling based code to read and write the device now that we have it memory mapped. I'll write most of this code in C for clarity (i.e. assuming the translation to assembly is trivial) and resort to assembly language only when C won't work. /* * First, define a template for the memory-mapped device. * The four locations are actually 8 bits; they will appear * as the bottom 8 bits in each of the four words. * * I like to use a structure as the template and then define * bit fields as masks. */ typedef struct { int bcontrol; /* 0b00 */ int bdata; /* 0b01 */ int acontrol; /* 0b10 */ int adata; /* 0b11 */ } Z8530; Z8530 *serialport = (Z8530 *)0x00010000; #define DAV 0x01 /* data available (cntl bit 0) */ #define TOK 0x04 /* ok to transmit (cntl bit 2) */ /* * polling-based I/O is easy: loop until read to receive (send) * and then read (write) the character. */ char getchar() { while (!(serialport->acontrol & DAV)); return(serialport->adata & 0xff); } void putchar(char blah) { while (!(serialport->acontrol & TOK)); serialport->adata = blah; } Polling is easy but has limitations. One trouble is that you lose characters when you are not actively polling; The second problem is that the whole CPU is stalled while you are sitting in the busy-wait loop polling. On a multitasking system you can partically alleviate the latter problem by yielding the processor in the loop. 4. Interrupt-based control can solve both problems by letting you define large software buffers for inputs and outputs. I'll define one for inputs. /* * first, a little queue package. */ #define BUFSIZE 0x100 typedef struct { int head; /* index of next available */ int tail; /* index of last used */ char buffer[BUFSIZE]; } queue; int queue_empty(queue *q) { return(q->head == q->tail); } int queue_full(queue *q) { return((q->head + 1) % BUFSIZE == q->tail); } void queue_insert(queue *q, char blah) { if (!queue_full(q)) { q->buffer[q->head] = blah; q->head = (q->head + 1) % BUFSIZE; } } char queue_extract(queue *q) { char blah = q->buffer[q->tail]; if (!queue_empty(q)) q->tail = (q->tail + 1) % BUFSIZE; return(blah); } /* * next the interrupt system. A convenient thing to do is * to turn the exception handler into an ordinary procedure * call as quickly as possible. This exception handler * is a "stub" that saves all the registers in known * locations, then makes a procedure call to a routine that * could be coded in C with the cause and exception PC as * arguments. */ 0x4000: sw 0 1 save1 ! save visible state sw 0 2 save2 [...] sw 0 15 save15 mfc0 1 1 ! get cause code as ARG1 mfc0 0 2 ! get EPC as ARG2 lw 0 13 exhandler ! get pointer to C routine jalr 13 14 ! run handler procedure for this cause ! note: runs with interrupts *off* lw 0 1 save1 ! restore visible state lw 0 2 save2 [...] lw 0 15 save15 rfe ! exit as if nothing happened /* * a couple of utility routines to enable and disable interrupts */ disable_interrupts: mtc0 0 2 ! set EI = 0 jalr 14 13 ! return enable_interrupts: addi 0 1 1 ! set EI = 1 mtc0 1 2 jalr 14 13 ! return /* * now the rest of the interrupt system can be written in C. * The exhandler procedure is a big switch on the cause. */ void exhandler(int cause, int epc) { switch(cause) { case 0: handle_arithmetic_overflow(epc); break; case 1: handle_illegal_instruction(epc); break; case 2: handler_interrupt(); break; default: panic(); /* :-) */ break; /* * finally, the character input code is split across the * getchar() routine and the exception handler for the * interrupt. The exception handler inserts values into * the queue and getchar() removes values from the queue. * incoming characters are only lost if the (large) software * queue overflows. * * note the careful manipulation of the EI bit: in order to * provide mutual exclusion during access to the queue * structure, all the queue routines must run with interrupts * disabled. The handler_interrupt routine runs with * interrupts disabled because it runs as an exception. For * new_getchar, however, we must manipulate the interrupt * enable bit manually. */ static queue inqueue = { 0 }; void handler_interrupt(void) { queue_insert(&inqueue, serialport->adata & 0xff); } char new_getchar(void) { while (1) { disable_interrupts(); if (!queue_empty(&inqueue)) { char value = queue_extract(&inqueue); enable_interrupts(); return(value); } enable_interrupts(); }