As stated in part one, I decided to write my 8080 emulation code as a library that could be used to emulate more 8080 based systems in the future. This post gives an overview of the Intel 8080 and then delves into the library and its implementation details. If you just want to see the code, check it out here, otherwise, read on.
Overview
The release of the Intel 8080 followed the release of the Intel 8008, which itself followed the release of the Intel 4004, the first microprocessor produced by Intel.
The 8080 had a 16-bit address bus and an 8-bit data bus. It had seven 8-bit general purpose registers: A, B, C, D, E, H and L. The program counter and stack pointer were each 16 bits wide. Although the general purpose registers were each 8 bits, there were instructions to support operating on them in pairs (BC, DE, HL) as if each pair was a 16-bit register.
Additionally, many instructions supported operating directly on memory through
the 16-bit address stored in register pair HL. This is referred to in
documentation and assembly code as "register" M. For example, MOV M, A
moves
the contents of the accumulator to memory at the address stored in register pair
HL.
The 8080 also maintained a flags register which contained five status flags:
- Sign: Set to the 7th bit of the result of an arithmetic operation
- Zero: Set if the result of an operation is zero
- Auxiliary Carry: Set if there is a carry out of bit 3 while performing an
arithmetic operation, this flag only affects one instruction -- DAA
- Parity: Set if the result of an operation has an even number of set
bits, unset otherwise
- Carry: Set if there is a carry out of bit 7 when performing addition, or no
carry if performing subtraction, also modified by rotation instructions
The more observant among you will probably notice that these are the same flags found in the lower eight bits of a modern x86's processor's FLAGS register. Indeed, the FLAGS register on a modern x86 CPU dates back to this register.
The 8080 had an 8-bit data bus, thus 28=256 possible opcodes. Twelve opcodes are undocumented but perform the same job as other instructions.
Interrupt handling on the 8080 was fairly straightforward. Unlike the forward
compatible Z80, which had maskable
and non-maskable interrupts, the 8080 only had one type of interrupt. An
interrupting device would pull the INT line high and write an opcode to the
8080's data bus. This would cause that opcode to be executed after the currently
executing instruction finished. While this opcode could be anything, in
practice, it was usually one of the eight single byte restart instructions RST
0
through RST 7
, which would call one of the eight restart subroutines at
addresses 0x00
through 0x40
.
The 8080 also had support for binary coded
decimal operations, in
which it treated an 8-bit number as a two digit base ten number, using the lower
four bits as the ones digit and the upper four bits as the tens digit. To work with
BCD numbers, an additional instruction -- DAA
(decimal adjust accumulator) was
used to adjust the contents of the accumulator to the correct BCD form after an
arithmetic operation was performed between two BCD values. This differs from the
BCD implementation on processors such as the MOS
6502, which had an internal
BCD flag that caused all arithmetic operations to convert results to BCD by on
their own, without the need for an extra instruction.
General Implementation
Since I hadn't used the language for a large personal project before, I decided to go back to basics and write lib8080 (as I'm calling it) in plain old C99.
All information about an emulated 8080 is stored in an i8080
struct, which
has the following format:
struct i8080 {
/* General purpose registers (8 bits) */
uint A, B, C, D, E;
/* High and low address registers (8 bits) */
uint H, L;
/* 8080 status flags (8 bits) */
uint flags;
/* Stack pointer (16 bits) */
uint SP;
/* Program counter (16 bits) */
uint PC;
/* Is the CPU currently halted? (boolean) */
int halted;
/* Pointer to beginning of memory */
char *memory;
/* Size of memory in bytes */
size_t memsize;
/* Interrupt enable/disable flag (boolean) */
int INTE;
/* Does the CPU have a pending interrupt? (boolean) */
int pending_interrupt;
/* If the CPU does have a pending interrupt,
* what is the address placed on the data bus
* by the interrupting device? (8 bits)
*/
uint interrupt_opcode;
/* IO handling callbacks (see section below) */
i8080_in_handler input_handler;
i8080_out_handler output_handler;
/* Number of CPU cycles since reset_cpu was last called */
uint cyc;
};
At its core, the beating heart of lib8080 is the same as most CPU emulators -- a large switch statement to call the correct emulation function for the current opcode.
void i8080_step(struct i8080 *cpu) {
if (cpu->halted) {
return;
}
/* Fetches the opcode in memory at the program
* counter, or from an external device if there is a
* pending interrupt.
*/
uint opcode = next_instruction_opcode(cpu);
switch (opcode) {
case 0x00: // NOP
case 0x08: // NOP (undocumented)
case 0x10: // NOP (undocumented)
case 0x18: // NOP (undocumented)
case 0x20: // NOP (undocumented)
case 0x28: // NOP (undocumented)
case 0x30: // NOP (undocumented)
case 0x38: // NOP (undocumented)
nop(cpu);
break;
case 0x22: // SHLD a16
shld(cpu);
break;
case 0x2A: // LDHD a16
ldhd(cpu);
break;
/* Many more opcodes down here */
}
}
Most instructions for the 8080 are encoded in a way that makes CPU emulation
fairly uncomplicated. For example, each of the 8080's 63 MOV
(move)
instructions are encoded as follows:
bit 7 bit 0
| |
| |
v v
0 1 X X X Y Y Y
X X X = Destination Register
Y Y Y = Source Register
0 0 0 = Register B
0 0 1 = Register C
0 1 0 = Register D
0 1 1 = Register E
1 0 0 = Register H
1 0 1 = Register L
1 1 0 = Memory Reference M
1 1 1 = Register A
This makes implementation a breeze for the most part. For example, the following
four-line function handles every MOV
instruction.
void mov(struct i8080 *cpu, uint opcode) {
/* Get destination register number (bits 3 through 5) */
uint dst = (opcode & 0x38) >> 3;
/* Get source register number (bits 0 through 2) */
uint src = opcode & 0x07;
/* Increment cycle count */
cpu->cyc += (dst == 6 || src == 6) ? 7 : 5;
/* Copy data from src register to dst register */
set_reg(cpu, dst, get_reg(cpu, src));
}
Emulating I/O
Unlike some other microprocessors, the 8080 did not use memory mapped I/O to
communicate with external devices. Instead it had two specialized instructions,
IN
and OUT
. These instructions supported interfacing with up to 256 external
devices. The format of these instructions was IN d8
and OUT d8
, where d8
is an 8-bit device number.
When an IN
instruction was executed, the device number to read from was placed
on the upper and lower eight bits of the 8080's address bus, the 8080's DBIN
(data bus in) line was pulled high, and the external device placed one byte on
the 8080's data bus, which was read into the accumulator.
Similarly, when an OUT
instruction was executed, the contents of the
accumulator were placed on the 8080's data bus, the 8080's WR (write) line was
pulled low (WR is active low) and the device number to write to was placed on
the upper and lower eight bits of the address bus. The appropriate external
device could then read the contents of the accumulator from the data bus.
lib8080 emulates this functionality using function pointers. Inside the i8080
struct are two function pointers, input_handler
and output_handler
. When an
IN
or OUT
instruction is executed, if the appropriate function pointer is
not NULL
, the function it points to will be invoked to handle input or output
of data respectively. This allows for projects to emulate external devices
easily using a callback function.
As a simple example, here's how you could use lib8080 to emulate a device that
always writes the byte 0x12
on I/O port zero and reads the latest byte written
on I/O port zero into a global variable.
uint last_byte_written;
uint handle_input(struct i8080 *cpu, uint dev) {
if (dev == 0) {
return 0x12; /* Return the byte 0x12 to the cpu */
} else {
/* Handle other devices */
}
}
void handle_output(struct i8080 *cpu, uint dev, uint val) {
if (dev == 0) {
last_byte_written = val;
} else {
/* Handle other devices */
}
}
/* Initialize i8080 cpu struct and memory */
struct i8080 *cpu = malloc(sizeof(struct i8080));
reset_cpu(cpu);
/* Set up memory, etc. */
cpu->input_handler = handle_input;
cpu->output_handler = handle_output;
/* Start executing instructions */
while (1) {
i8080_step(cpu);
}
Unit Testing
To achieve a high level of accuracy, lots of unit testing was required. Looking around at C unit testing frameworks, I found most were either too complex or too simple for my needs, so I created my own, AttoUnit. While a full discussion on AttoUnit is a blog post of its own, I created it to be header only, provide just what I needed for this project and keep boilerplate to an absolute minimum.
I wrote tests for each instruction as I implemented it. Each instruction is
unit tested at least once to verify basic behaviour, and most are tested several
times to check edge cases. As an example, here's the test for MOV B,D
(heavily
commented for demonstration purposes):
TEST_CASE(mov_b_d) {
/* Write the opcode 0x42 (MOV B, D) to address 0 */
i8080_write_byte(cpu, 0, 0x42);
/* Set up registers with test values */
cpu->B = 0; cpu->D = 1;
/* Execute the instruction */
i8080_step(cpu);
/* Verify the program counter was incremented */
ASSERT_EQUAL(cpu->PC, 1);
/* Verify the contents of D were copied to B */
ASSERT_EQUAL(cpu->B, 1);
/* Verify the instruction took 5 cycles */
ASSERT_EQUAL(cpu->cyc, 5);
}
As it currently stands, lib8080 has 1061 assertions in 421 unit test cases. For comparison purposes, the entire library is ~1300 lines.
Integration Testing
In addition to unit testing, I wanted to test the 8080 implementation as a whole. Thankfully, I managed to find several 8080 test programs from the 80s and 90s that very comprehensively test the 8080. These are CP/M binaries so they required a small amount of CP/M emulation, but that turned out to be very simple.
CP/M was an early operating system that, like modern operating systems,
abstracted away hardware details so that programmers could write code easier.
These abstractions were accessed through BDOS calls, which followed exactly the
same basic idea as system calls on modern operating systems. When a program
wanted to make a BDOS call, it loaded the BDOS function number into the 8080's C
register and jumped to memory address 0x05
. Note that the code for the program
you were running in CP/M was loaded at address 0x100
. The first 256 bytes were
reserved for CP/M, so address 0x05
was not a location in a user loaded
program.
The test programs I came across only needed BDOS calls 2 (write character to console) and 9 (write string to console) to work, so I emulated them with a very simple C program.
Now when I load a test binary, eg. CPUTEST.COM
with my simple CP/M emulator, I
get:
DIAGNOSTICS II V1.2 - CPU TEST
COPYRIGHT (C) 1981 - SUPERSOFT ASSOCIATES
ABCDEFGHIJKLMNOPQRSTUVWXYZ
CPU IS 8080/8085
BEGIN TIMING TEST
END TIMING TEST
CPU TESTS OK
As an interesting side note, from the small amount of info I can find online, SuperSoft Associates is a defunct software company located in Illinois that was in business in the 80s.
I set up Travis to run all these test binaries alongside the unit tests mentioned previously.
Conclusion
Implementing and testing all of this was roughly a month of work, but it was a lot of fun. The extra effort put into testing was well worth it. With a project this well tested, I'm confident the emulation is essentially perfect. If you're interested in using lib8080 in a project of your own, check out the API documentation for usage details.
With an emulated 8080, all that's needed to get Altair BASIC running now is an emulated teletype. I'll discuss implementing one in part three.