I/O basics: memory-mapped I/O and polling
How the keyboard and the display are connected to the LC-3: the KBSR/KBDR/DSR/DDR registers, the ready bit, the polling loop, the difference from interrupts and an introduction to TRAP service routines.
A computer that only did calculations would be useless: it has to get data from somewhere and give the result somewhere. The input and output parts of the von Neumann model do this job. On this page you will learn how the LC-3 talks to the keyboard and the display. The mechanism is simple: the device has two registers, one holds the answer to the question “are you ready?”, the other holds the character itself.
This topic uses the LC-3 instructions of weeks 7-8 (LDI, STI, BR). If you have not seen those instructions yet, reread this page after them; here we explain what each instruction does in a single sentence.
Device registers
Every I/O device has at least two registers:
- Data register: the data transferred (the character).
- Status register: the state of the device, especially the “ready” information.
The registers of the keyboard and display in the LC-3:
| Register | Address | Contents |
|---|---|---|
| KBSR (keyboard status) | xFE00 | bit 15: is there a new character |
| KBDR (keyboard data) | xFE02 | bits 7:0: the ASCII code of the key pressed |
| DSR (display status) | xFE04 | bit 15: is the display ready to accept a new character |
| DDR (display data) | xFE06 | bits 7:0: the character to be written to the display |
Memory-mapped I/O
Some processors have special instructions for I/O (like IN, OUT). The LC-3 does not do this: the device registers are given memory addresses. The processor accesses them with ordinary load and store instructions like memory. When the memory interface sees that the address is in the xFE00 region, it routes the request to the device, not to memory. This is called memory-mapped I/O.
The result: “read from the keyboard” = “read address xFE02”. “Write to the screen” = “write to address xFE06”.
Why LDI and STI?
The LD and ST instructions can access an address near the PC: PC + offset, with a 9-bit offset, that is -256..+255. The program is around x3000. xFE00 is too far. The solution: put the device’s address next to the program as a pointer and access it indirectly.
KBSRP .FILL xFE00 ; the address of KBSR, inside the program
...
LDI R1, KBSRP ; R1 ← M[M[KBSRP]] = M[xFE00] = KBSR
LDI R1, KBSRP reads in two steps: first it takes the value in KBSRP (xFE00), then it puts the contents of that address in R1. STI does the same for writing.
Synchronization: the ready bit
The keyboard is asynchronous: it is not known when the user will press a key. The processor, meanwhile, executes billions of instructions per second. For the two to work together a handshake is needed. The ready bit provides it.
Keyboard side:
- The user presses a key. The keyboard puts the ASCII code in KBDR and sets KBSR[15] = 1.
- The processor reads KBDR. At the moment of reading, KBSR[15] automatically becomes 0.
- While KBSR[15] = 0, the keyboard does not write a new character to KBDR; this way an unread character is not overwritten.
Display side:
- If DSR[15] = 1, the display is ready.
- The processor writes the character to DDR. DSR[15] becomes 0.
- After the display has shown the character, DSR[15] becomes 1.
TIP · Why BRzp? The ready bit is bit 15, that is, the sign bit of the 16-bit number. When ready, the value loaded into the register is negative (N). When not ready it is 0 or positive (Z or P). “Wait as long as it is not ready” = loop back with
BRzp.
Polling
The processor reading the ready bit again and again in a loop is called polling.
Read a character from the keyboard:
POLL LDI R1, KBSRP ; R1 ← KBSR
BRzp POLL ; bit 15 = 0 → not ready, ask again
LDI R0, KBDRP ; R0 ← KBDR (the character)
Write a character to the screen:
POLL2 LDI R1, DSRP ; R1 ← DSR
BRzp POLL2 ; the display is busy, wait
STI R0, DDRP ; DDR ← R0
Worked example: echo
Read a character from the keyboard and write it to the screen.
.ORIG x3000
START LDI R1, KBSRP
BRzp START
LDI R0, KBDRP
ECHO LDI R1, DSRP
BRzp ECHO
STI R0, DDRP
HALT
KBSRP .FILL xFE00
KBDRP .FILL xFE02
DSRP .FILL xFE04
DDRP .FILL xFE06
.END
When the user types k:
k
When the program ends, R0 = x006B (the ASCII code of ‘k’), R1 = x8000 (the last DSR read, ready), condition code N.
The difference between polling and interrupts
| Polling | Interrupt-driven | |
|---|---|---|
| Who asks? | The processor keeps reading the ready bit | The device sends a signal to the processor when ready |
| The processor while waiting | Spins uselessly in a loop | Runs another program |
| Efficiency | Low (most loops are wasted) | High |
| Simplicity | Very simple | An interrupt mechanism is needed |
A person presses perhaps 5 keys per second, and in that time the processor can execute billions of instructions. Polling wastes almost all of this time. With interrupts, the processor does its own work, a short service routine runs when the device is ready, and the processor returns to where it left off.
TRAP: leave it to the operating system
Writing these polling loops in every program is both tedious and dangerous (you do not want a user program to play with the device registers). In the LC-3, this job is left to service routines in the operating system. The user program calls them with the TRAP instruction.
| TRAP | Alias | What does it do? |
|---|---|---|
| x20 | GETC | Read a character, put it in R0 (does not echo to the screen) |
| x21 | OUT | Write the character in R0[7:0] to the screen |
| x22 | PUTS | Write the string starting at the address in R0 up to x0000 |
| x23 | IN | Print a prompt, read a character, echo it to the screen too, put it in R0 |
| x25 | HALT | Stop the machine |
When TRAP x23 runs:
- The return address is saved: R7 ← PC (the already incremented PC).
- PC ← M[x0023]. The range x0000-x00FF is the trap vector table; each row holds the starting address of a service routine.
- The service routine does its job with its own polling loop.
- At the end it returns to the user program with
RET(=JMP R7).
Worked example: read a digit from the keyboard and print its double.
GETC ; R0 ← '3' = x33
LD R1, NEG30 ; R1 ← -48
ADD R2, R0, R1 ; R2 ← 3 (the number)
ADD R0, R2, R2 ; R0 ← 6
LD R1, POS30 ; R1 ← 48
ADD R0, R0, R1 ; R0 ← '6'
OUT
HALT
NEG30 .FILL #-48
POS30 .FILL #48
If the user types 3:
6
PITFALL · overwriting R7. TRAP writes the return address into R7. If your program keeps an important value in R7, it is lost after a TRAP. Save it somewhere else first.
Common mistakes
| Wrong idea | The truth |
|---|---|
| The ready bit is bit 0 | Bit 15 (the sign bit) |
| When ready the value is positive | Bit 15 = 1 → negative, N |
| The wait loop uses BRn | Loop back while not ready: BRzp |
LD R1, xFE00 works | offset9 does not reach that far; LDI + .FILL |
| Reading KBDR does not change KBSR | When read, KBSR[15] = 0 |
| Write to DDR without looking at DSR | If the display is busy the character is lost |
| Polling frees the processor | The processor spins uselessly in the loop |
| TRAP pushes the return address on the stack | It writes it to R7 |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is memory-mapped I/O?
Show answer
Device registers (KBSR, KBDR, DSR, DDR) are placed at memory addresses. The processor does not use separate I/O instructions; it accesses these addresses with the same LD/ST family (LDI, STI). When the address reaches xFE00 and above, the memory interface routes the request to the device, not to memory.
Give the LC-3 memory-mapped addresses of KBSR, KBDR, DSR and DDR.
Show answer
Keyboard: status xFE00, data xFE02. Display: status xFE04, data xFE06. In the status registers you find the ready bit, in the data registers the character (bits [7:0]).
Which bit of KBSR tells the processor that a new character is available?
Show answer
KBSR[15] is the ready bit. When a key is pressed, the keyboard puts the character in KBDR and sets KBSR[15] = 1. When the processor reads KBDR, this bit is automatically cleared to 0.
A polling loop is: POLL LDI R1, KBSRP followed by BRzp POLL. Why is BRzp the right branch?
Show answer
LDI loads KBSR into R1 and sets the condition codes. If bit 15 = 1, the 16-bit value is negative in 2's complement (N). If not ready, the value is 0 or positive (Z/P). BRzp loops back as long as it is not ready; when it is ready, it falls through to the next instruction.
Why does the program use LDI R1, KBSRP (with KBSRP .FILL xFE00) instead of LD R1, xFE00?
Show answer
The address of LD is PC + offset9, that is, within -256..+255 of the PC. The distance from x3001 to xFE00 is 52735, which does not fit. So the address is written to a nearby location with .FILL and read indirectly with LDI.
When the keyboard is ready and KBSR = x8000, what condition code is set by LDI R1, KBSRP?
Show answer
x8000 = 1000 0000 0000 0000, MSB 1 → negative. According to the loaded value N = 1, Z = P = 0. When not ready, KBSR = x0000 → Z.
Why must the program check DSR[15] before writing a character into DDR?
Show answer
The processor is much faster than the display. While the character written to DDR is being displayed, DSR[15] = 0. If you write a new character before it finishes, it overwrites the previous one and the character is lost. When DSR[15] = 1, it is safe to write.
What is the difference between polling and interrupt-driven I/O?
Show answer
In polling the processor reads the ready bit continuously in a loop and cannot do other work in the meantime (wasted time). In interrupt-driven I/O the processor continues its own work; when the device is ready it interrupts the processor, which runs a short service routine and returns to where it left off.
Is keyboard input synchronous or asynchronous? Why?
Show answer
The user presses keys not according to the processor's clock but at his own pace. It is not known when a character will arrive. That is why a handshake mechanism like the ready bit is needed.
The program below reads two characters and prints each one plus 1. If the user types HI, what is printed?
AND R2, R2, #0
ADD R2, R2, #2
AGAIN LDI R1, KBSRP
BRzp AGAIN
LDI R0, KBDRP
ADD R0, R0, #1
WAIT LDI R1, DSRP
BRzp WAIT
STI R0, DDRP
ADD R2, R2, #-1
BRp AGAIN
HALTShow answer
R2 is the counter (2). In each round one character is read, its ASCII code is incremented by 1, and it is written to the screen. 'H' = x48 → x49 = 'I'. 'I' = x49 → x4A = 'J'. When R2 reaches 0, BRp falls through, HALT.
In the program above, why is the counter decremented right before BRp AGAIN and not earlier?
Show answer
The condition codes are determined by the last instruction that wrote to a register. Because ADD R2, R2, #-1 comes right before, BRp looks at the new value of R2. If a load like LDI R1 were in between, BRp would decide based on R1 and the loop would break.
A user program executes TRAP x23. Where does the processor find the starting address of the service routine?
Show answer
The TRAP instruction zero-extends the 8-bit trapvect to 16 bits and loads the value at that address into the PC: PC ← M[x0023]. The range x0000-x00FF is the trap vector table. Also the return address is saved in R7 (R7 ← incremented PC).
Verification: The polling programs and the TRAP example were run on our own LC-3 simulator (with the keyboard and display registers mapped to addresses xFE00-xFE06); the outputs, register values and condition codes were taken from there (verify/SEN2201/06.mjs).