Running Altair BASIC on a Modern Computer, Part 2: Emulating the Intel 8080

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.

© Rhys Rustad-Elliott. Built using Pelican. Best viewed with NCSA Mosaic on a 250MHz Pentium 2 machine running Windows 3.1 or later.