By this point of the introduction series, we’ve established a good understanding of how the CPU, RAM and ROM work individually and together when connected by a data bus. While the CPU and memory are the heart (or brain) of the computer, they can’t do much by themselves. Have you ever tried using a computer without a keyboard/mouse/screen? It basically makes no sense. In this post we’ll see how these input/output devices are integrated into the system.
This time we’ll be looking at Intel’s 8080 processor which was introduced in 1974. The 8080 is an 8-bit CPU, and it improved on Intel’s first 8-bit CPU, the 8008 (which was introduced in 1972) design. 8 bits are considered to be a byte – a basic unit of data still used to this day (the less common 4 bit data unit is called a nibble). The 8008’s differs from the 4004 in the fact that it is designed to work with a variety of different memory components, while the 4004 was designed to work with other MCS-4 components. This “we build CPUs and you combine them with whatever you want” approach is the standard today – the CPU and RAM you are using right now is manufactured by different companies.
While the 8008 featured a wider 8-bit data bus, it still wasn’t wide enough to fit a 14-bit address which was held in the 8008’s PC and 7 stack registers. Lets look at the different packaging:
The 8008 has just 2 pins more compared to the 4004, while the 8080 has 40 pins. On closer inspection we can see that the 8008 and 8080 discarded the CM-RAM pins, and that the 8080 features 16 address pins (meaning up to 64KiB of memory was addressable) .
By this point you are probably wondering how RAM and ROM chip selection works in the 8008/8080, and how the instruction fetch/decode/execute cycle looks like. Lets start by taking a look at this image from the MCS-80 User’s manual:
This is a high level view of how different devices are connected by a single bus. Notice that the only part that has its part number printed on it is the 8080 (as opposed to the MCS-4 architecture), this is due to the fact that the 8080 can work with different chips, that can be produced by different manufacturers, as long as they fit Intel’s published specifications. Here’s a more detailed view of the bus:
We can now see that the bus is actually 3 different buses that connect much more chips compared to the MCS-4. There are many interesting old and new elements to examine. Lets begin with an element which was first introduced in Part 4 of the series:
- Clock generator and driver – The 8224 chip provides the clock for the system:
When the 8224 is connected to a crystal (pins 14 and 15) it outputs an oscillating square-wave to pin 12 (which can be used as clock input for other chips), and uses a “device by nine” counter to generate two slower non-overlapping square-waves (as the 8080 requires) on pins 10 and 11. The 8224 also features a RESET and READY signals for the CPU which are in sync with the CPU’s clock. The importance of these signals being in sync with the clock will be explained later in the post.
Also, notice the line above the STSTB and RESIN signals (pin 2 and 7). This means that the signal is “active low”.
- System controller – The 8228 chip sits between the data bus and the 8080. On output (to the bus) the chip’s job is to decode the signals from the 8080’s pins to a combination of outputs to the data and control bus. On input (from the bus), the chip passes the signals from the data bus to the D0-D7 pins on the 8080:
Another important feature of the 8228 is that is acts as a buffer/driver. This means that it has the needed circuitry to make sure there’s enough current to properly activate the transistors on all the chips that are connected to the data bus, and a minimum voltage level to be maintained on input (coming from the chips):
Notice the control bus pins (which are all active-low pins), they play an important role in chip selection (chip selection will be gradually explained through the post). Interestingly, Intel decided to add an example of an alternative setup using a combination of 8212 and 8216 chips instead of a single 8228 chip on page 3-4 in the manual (figure 3-5).
- Address buffers and decoders – Sitting between the address bus and the 8080 (optionally) are the 8212/8216 buffers and the 8205 decoder. The reason why the buffers are optional is because the they are needed only to drive the address buss with more current if a large number of chips are connected to it. The 8205 1-out-of-8 decoder is used for chip selection by being selectively connected to the address bus:
In the above configuration, address 0x300 will select chip 0, since it translates to binary 1100000000b (bits 10-14 are 0). Address 0x400 will select chip 1. This way RAM chip selection is implemented using just the address, alleviating the programmer from manual RAM chip selection (remember the 4004’s DCL instruction).
- ROM and RAM chips – The reasons why there are so many chip numbers in the ROM and RAM blocks is because they represent different possible combinations. For RAM, a system designer can chose the more expensive SRAM chip like the 8102, or a less expensive DRAM chip like the 8107B-4. If a DRAM chip is chosen, a 8210 TTL-TO-MOS chip must be added since the address bus transports a TTL signal while the RAM is a MOS device. More about TTL vs MOS can be read here (The listed SRAM chips can handle direct TTL input). Also it is recommended to add a 8222 memory refresh controller along with DRAM chips. It is possible to replace the 8222 by software that reads all bits from ram ever ~60m/s, but that’s like using a Ferrari as a farm tractor.
Chip selection is implemented by decoding the signal on the address bus with the 8205. To better understand how it’s done, lets examine an image from the user’s manual which shows an implementation of a 16KiB DRAM system which is based on an array of 8107B-4, 4096 x 1 bit word chips :
Two “service” chips are implemented here. The 8210 TTL-TO-MOS chips that handle the address signals that go to the DRAM chips and the 8212 buffer that drives the data input to the DRAM chips and buffers the output for the data bus.
Address lines A12-A13 combined with the 8205 chip act as row selectors. Each row contains eight 8107B-4 DRAM chips which read a 12 bit address from A0-A11, and output a single bit of stored data. This means that when a 14 bit address is fed into this system, 8 bits (one byte) will be read from the chips and stored in the 8212 buffer. Read/Write selection is implemented through the MEMW and MEMR signals on the control bus.
Chip selection for ROM chips works in the exact same way. A system can have a mix of ROM and RAM memory, and with a combination of 8205’s it is possible to select ROM and RAM simple by logically assigning them with their own address space. For example, memory reads from address 0-8KB are reads from ROM chips (writes to these addresses wont have any effect), and memory reads and write to addresses 8-16KB will read and write to RAM chips. This is done by using A13 as a main ROM/RAM selector. If A13 is 0, all RAM chips are deactivated, and if A13 is 1, all ROM chips are deactivated.
Another important difference over the MCS-4 is that the MCS-8/80 can fetch instructions anywhere in from the unified address space – meaning that instructions can be fetched from both ROM and RAM.
- Interrupts – The 8080 features a single INT (interrupt) pin. What’s an interrupt? It’s a signal coming to the CPU from an external source that causes the CPU to stop what it’s doing, and activate a predefined set of instructions to handle the interrupt. Interrupts are asynchronous by nature, and can come at any time.
Why use interrupts? Well, interrupts are probably the most sane way for a general purpose CPU to handle peripheral I/O (Input/Output). A CPU that doesn’t work with interrupts (like the 4004 for example) would have to constantly poll devices as part of its programming, meaning it would constantly waste cycles just to check if there’s any input from a device like a keyboard for example. Polling too often would harm performance, and polling less often might cause input lag, or miss the input completely.
Here’s an example of a simple instruction loop circuit with an interrupt circuit:
As usual, you can get the TXT file for the circuit and test it out in the circuit simulator (link in the side panel). The main loop lights up each LED light by order starting from the top and going down. At any moment the main loop can be interrupted and an interrupt handler will blink the blue LED 3 times. Once the interrupt handler is done blinking the blue LED, it’ll signal the system by toggling the flip-flop and the main loop will continue executing. An important thing to notice in the above circuit is the clock synchronization feature, which makes sure the interrupt handler begins execution at a rising edge of the clock. This makes sure that the blue LED gets 3 consistent blinks, and that each red LED will have an equal activation time (not including the time it takes to execute the interrupt handler) even if it’s interrupted.
Interrupt handling synchronization is even more critical when it comes to CPUs. What happens if an interrupt occurs half way through the instruction decode cycle? Intel takes care of that by sampling the INT line during the T1 instruction cycle (instruction cycles are discussed in detail starting from page 2-3 in the manual):
Also, an internal interrupt enable (or INTE) flip-flop is implemented along with an INTE output pin. Here’s a snip from the manual:
As we can see, the 8080 automatically disables interrupts during the T1 if an interrupt was accepted (to prevent another interrupt to interrupt with the interrupt handling).
So what happens when the 8080 accepts the interrupt? Lets break it down to the cycles:
T1 – The 8080 drives INTE low (disable interrupts).
T2 – The interrupting chip’s circuitry will drive the INT signal low after getting the low INTE signal. The 8080 sends an interrupt acknowledge signal (by sending a 1 on D0).
T3 – The 8080 drives DBIN (Data Bus IN) pin high, and it’s the interrupting chip’s job to send a single byte RST instruction down the data bus during this cycle.
T4… – RST instruction is decoded to an address which is loaded to the PC register, and from there we have the usual fetch decode and execute.
The RST instruction, which is a single byte instruction transmitted during T3, has 3 variable bits that indicate the interrupt handing routine’s address (the routine’s instructions will be fetched and executed during the following cycles). The value of the 3 bits get multiplied by 8 to get the routine’s address. This means that in the basic configuration (cascading interrupt controllers is discussed in the manual , the 8080 can have 8 different interrupt handling routines at the following addresses: 0x0, 0x8, 0x10, 0x18, 0x20, 0x28, 0x30 and 0x38.
This means that if the system designer wants to implement interrupt handling, he’ll need to introduce chips that can handle this interrupt protocol, and also to make sure that the relevant memory addresses contain the needed code to handle the interrupts.
An important question should be asked at this moment- if there’s only one INT pin on the 8080, what happens when there are multiple interrupting chip in the system?The 8214 is a priority interrupt controller is the answer:
It is possible to connect multiple interrupting chips to the 8214. The 8214 receives the interrupts from the chips and pitches the interrupts to the 8080 based on priority. The chip with the interrupts that are considered highest in priority will be connected to pin R0 on the 8214, and its interrupt handling code will start at address 0. The chip with the least important interrupts will be connected to pin R7 and its interrupt handling code will start at address 0x38. It is possible to cascade the 8214 chips in order to deal with more than 8 different interrupts. This technique is pretty straight forward and is explained on page 5-159 in the manual (and will be discussed in the part B of this post).
If the space giving to each interrupt handler looks too little to hold enough instructions to actually handle the interrupt, remember that it is possible to use the JMP instruction, or the CALL instruction (which is the improved version of the 4004’s JMS instruction) to continue execution of code that sits in another memory address. The 8080’s instruction set is detailed in the users’s manual.
On the software side of interrupt handling there are three important things to note. First, the execution of the RST instruction during T3 doesn’t have an effect on the PC register. Second, the programmer can disable interrupts by using the DI instruction [This is usually done in performance critical parts of the software, or when the programmer knows that the code in the interrupt handler might break the program’s logic if the program is interrupted at that moment] . Third, the address of the instruction that was supposed to be executed before the code flow was interrupt is stored on the stack.
- The stack – Starting with the 8008, the stack took a new form (this form is used to this day in modern Intel CPUs). It is now located in memory (and not in registers like 4004), meaning that an infinite (as long as you have enough memory) stack dept can now be reached. An RST or CALL instruction would cause the next instruction’s address to be stored in memory pointed by the stack pointer (SP, a 16-bit register) in an action called push. The push action also decrements the SP register’s value by 2 (representing the 2 bytes stored in memory).
That’s not a typo, the SP is decremented whenever data is pushed on the stack (into memory). The reason for this strange behavior lies in the fact that having an entity that grows (in the normal direction) autonomously in memory will force software developers to constantly think about stack placement and how to prevent it from destroying data in memory, or how not to corrupt the stack with memory writes (since the “return” addresses are usually stored on the stack, corrupting the stack will cause undefined behavior). With a stack that “grows down” in memory, it is possible to place the stack pointer at the top of RAM. This way, we have normal memory store instructions use memory while moving up the RAM (controlled directly by the programmer), and the stack moving down the RAM autonomously. This gives a clear indication on when (and if) the two memory segments are about to collide (if memory write address is greater or equal to the SP address, we have a problem). Memory could also be logically divided to segments – the code/data segment, and the stack segment, all having a predefined size.
A RET instruction will pop the address in memory pointed by the stack pointer to the PC register (and also increments the SP register’s value by 2). It is also possible to store general data on the stack by using the PUSH instruction, and read data from the stack by using the POP instruction. Both instructions effect the SP register’s value (POP increments, PUSH decrements). This gives programmers a general purpose LIFO (Last In First Out) memory mechanism to work with when needed.
Since interrupts usually disrupt normal program flow, it’s the programmer’s responsibility to first save all current CPU registers in memory before executing code that actually handles the interrupt. A comfortable way (and de-facto the standard way) of doing this is to push all registers on the stack:
Once the interrupt handling routine is done, the programmer must add instructions that pop the saved processor status from the stack in reverse order before the RET instruction.
* * * * * * *
This is a lot of information to digest. By this point you should have a good understanding on how memory chip selection works, and how interrupts are implemented in software. The way the 8080 handles interrupts, manages memory and stack is very similar to how modern Intel processors work. Those of you who know their way around x86 CPU architecture assembly would probably feel at home with the 8080, despite it being an “ancient” 8 bit CPU.
I’ve decided to split this post in two parts. In the next part, which will conclude the introduction series, we’ll examine how peripheral I/O (input/output), and what DMA (Direct Memory Access) means an how it’s is implemented.
Hope you found this post informative. Feel free to leave comments, and ask questions.