After getting sidetracked for a while with other projects, I wanted to get back into emulation development with something fun, easy and short. So I decided to go for the very well-known arcade game of Space Invaders.
Space Invaders is running on an Intel 8080 clocked at 1996800 Hz. It has a 256×224 monochrome display (which is actually laying on the side) fed by a 0x1c00 big VRAM. It can push some 8-bit data to output devices (sound) and grab some 8-bit data from input devices (controllers and DIP switches for game options). Other than that we only need to implement 2 simple interrupts (VBLANK and some kind of beam-centered interrupt), and we should be good to go.
setting up
To change stuff for once, I decided to create this emulator in Python, just for funsies. The library pygame helps us with setting up a game window and handle controls.
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
from cpu import getPC, getVRAM, cpu_8080, loadROM, memory, getREGS, getSP, getFLAGS, loadSI, setKeys, interrupt
def ppu(screen, surface):
surface = pygame.Surface((256, 224))
pa = pygame.PixelArray(surface)
vram = getVRAM()
for y in range(224):
for x in range(256):
white = vram[y*0x20+x//8] >> (x % 8) & 1
if white != 0:
pa[x,y] = (255, 255, 255)
else:
pa[x,y] = (0, 0, 0)
del pa
surface = pygame.transform.scale(surface, (512, 448))
surface = pygame.transform.rotate(surface, 90)
screen.blit(surface, (0,0))
def main():
loadSI()
# initialize the pygame module
pygame.init()
# set title
pygame.display.set_caption("[ q00.si - space invaders arcade emuladore ]")
# create a surface on screen that has the size of 240 x 180
screen = pygame.display.set_mode((448,512)) # orig size is 256 / 224
surface = pygame.Surface((256, 224))
pa = pygame.PixelArray(surface)
vram = getVRAM()
for y in range(224):
for x in range(256):
white = vram[y*0x20+x//8] >> (7-(x % 8)) & 1
if white != 0:
pa[x,y] = (255, 255, 255)
else:
pa[x,y] = (0, 0, 0)
del pa
surface = pygame.transform.scale(surface, (512, 448))
screen.blit(surface, (0,0))
# define a variable to control the main loop
running = True
# main loop
cnt = 0
last_cnt = 0
frame = 0
while running:
# event handling, gets all event from the event queue
L = 0
R = 0
F = 0
C = 0
ST = 0
for event in pygame.event.get():
# only do something if the event is of type QUIT
if event.type == pygame.QUIT:
# change the value to False, to exit the main loop
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
print("Left")
L = 1
if event.key == pygame.K_RIGHT:
print("Right")
R = 1
if event.key == pygame.K_SPACE:
print("Space")
F = 1
if event.key == pygame.K_RETURN:
print("Return")
C = 1
if event.key == pygame.K_ESCAPE:
print("Escape")
ST = 1
setKeys(L, R, F, C, ST)
# step PPU
if cnt > 16640 and last_cnt <= 16640:
interrupt(0x08)
if cnt > 33280:
cnt = cnt - 33280
interrupt(0x10)
ppu(screen, surface)
pygame.display.update()
# step CPU
last_cnt = cnt
cnt = cnt + cpu_8080[memory[getPC()]]()
# run the main function only if this module is executed as the main script
# (if you import this as a module then nothing is executed)
if __name__=="__main__":
# call the main function
main()
(This is the already finished main.py, so don’t be irritated. It just shows how easy we set up the pygame code and modify the texture to be displayed)
the cpu – intel 8080
`After already having emulated a (modified) Z80 for the GameBoy, we can see that emulating the 8080 is very similar to it, and doesn’t need a lot of special work. We have a simple 256 opcode matrix:
x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 | xA | xB | xC | xD | xE | xF | |
0x | NOP 1 4 – – – – – | LXI B,d16 3 10 – – – – – | STAX B 1 7 – – – – – | INX B 1 5 – – – – – | INR B 1 5 S Z A P – | DCR B 1 5 S Z A P – | MVI B,d8 2 7 – – – – – | RLC 1 4 – – – – C | *NOP 1 4 – – – – – | DAD B 1 10 – – – – C | LDAX B 1 7 – – – – – | DCX B 1 5 – – – – – | INR C 1 5 S Z A P – | DCR C 1 5 S Z A P – | MVI C,d8 2 7 – – – – – | RRC 1 4 – – – – C |
1x | *NOP 1 4 – – – – – | LXI D,d16 3 10 – – – – – | STAX D 1 7 – – – – – | INX D 1 5 – – – – – | INR D 1 5 S Z A P – | DCR D 1 5 S Z A P – | MVI D,d8 2 7 – – – – – | RAL 1 4 – – – – C | *NOP 1 4 – – – – – | DAD D 1 10 – – – – C | LDAX D 1 7 – – – – – | DCX D 1 5 – – – – – | INR E 1 5 S Z A P – | DCR E 1 5 S Z A P – | MVI E,d8 2 7 – – – – – | RAR 1 4 – – – – C |
2x | *NOP 1 4 – – – – – | LXI H,d16 3 10 – – – – – | SHLD a16 3 16 – – – – – | INX H 1 5 – – – – – | INR H 1 5 S Z A P – | DCR H 1 5 S Z A P – | MVI H,d8 2 7 – – – – – | DAA 1 4 S Z A P C | *NOP 1 4 – – – – – | DAD H 1 10 – – – – C | LHLD a16 3 16 – – – – – | DCX H 1 5 – – – – – | INR L 1 5 S Z A P – | DCR L 1 5 S Z A P – | MVI L,d8 2 7 – – – – – | CMA 1 4 – – – – – |
3x | *NOP 1 4 – – – – – | LXI SP,d16 3 10 – – – – – | STA a16 3 13 – – – – – | INX SP 1 5 – – – – – | INR M 1 10 S Z A P – | DCR M 1 10 S Z A P – | MVI M,d8 2 10 – – – – – | STC 1 4 – – – – C | *NOP 1 4 – – – – – | DAD SP 1 10 – – – – C | LDA a16 3 13 – – – – – | DCX SP 1 5 – – – – – | INR A 1 5 S Z A P – | DCR A 1 5 S Z A P – | MVI A,d8 2 7 – – – – – | CMC 1 4 – – – – C |
4x | MOV B,B 1 5 – – – – – | MOV B,C 1 5 – – – – – | MOV B,D 1 5 – – – – – | MOV B,E 1 5 – – – – – | MOV B,H 1 5 – – – – – | MOV B,L 1 5 – – – – – | MOV B,M 1 7 – – – – – | MOV B,A 1 5 – – – – – | MOV C,B 1 5 – – – – – | MOV C,C 1 5 – – – – – | MOV C,D 1 5 – – – – – | MOV C,E 1 5 – – – – – | MOV C,H 1 5 – – – – – | MOV C,L 1 5 – – – – – | MOV C,M 1 7 – – – – – | MOV C,A 1 5 – – – – – |
5x | MOV D,B 1 5 – – – – – | MOV D,C 1 5 – – – – – | MOV D,D 1 5 – – – – – | MOV D,E 1 5 – – – – – | MOV D,H 1 5 – – – – – | MOV D,L 1 5 – – – – – | MOV D,M 1 7 – – – – – | MOV D,A 1 5 – – – – – | MOV E,B 1 5 – – – – – | MOV E,C 1 5 – – – – – | MOV E,D 1 5 – – – – – | MOV E,E 1 5 – – – – – | MOV E,H 1 5 – – – – – | MOV E,L 1 5 – – – – – | MOV E,M 1 7 – – – – – | MOV E,A 1 5 – – – – – |
6x | MOV H,B 1 5 – – – – – | MOV H,C 1 5 – – – – – | MOV H,D 1 5 – – – – – | MOV H,E 1 5 – – – – – | MOV H,H 1 5 – – – – – | MOV H,L 1 5 – – – – – | MOV H,M 1 7 – – – – – | MOV H,A 1 5 – – – – – | MOV L,B 1 5 – – – – – | MOV L,C 1 5 – – – – – | MOV L,D 1 5 – – – – – | MOV L,E 1 5 – – – – – | MOV L,H 1 5 – – – – – | MOV L,L 1 5 – – – – – | MOV L,M 1 7 – – – – – | MOV L,A 1 5 – – – – – |
7x | MOV M,B 1 7 – – – – – | MOV M,C 1 7 – – – – – | MOV M,D 1 7 – – – – – | MOV M,E 1 7 – – – – – | MOV M,H 1 7 – – – – – | MOV M,L 1 7 – – – – – | HLT 1 7 – – – – – | MOV M,A 1 7 – – – – – | MOV A,B 1 5 – – – – – | MOV A,C 1 5 – – – – – | MOV A,D 1 5 – – – – – | MOV A,E 1 5 – – – – – | MOV A,H 1 5 – – – – – | MOV A,L 1 5 – – – – – | MOV A,M 1 7 – – – – – | MOV A,A 1 5 – – – – – |
8x | ADD B 1 4 S Z A P C | ADD C 1 4 S Z A P C | ADD D 1 4 S Z A P C | ADD E 1 4 S Z A P C | ADD H 1 4 S Z A P C | ADD L 1 4 S Z A P C | ADD M 1 7 S Z A P C | ADD A 1 4 S Z A P C | ADC B 1 4 S Z A P C | ADC C 1 4 S Z A P C | ADC D 1 4 S Z A P C | ADC E 1 4 S Z A P C | ADC H 1 4 S Z A P C | ADC L 1 4 S Z A P C | ADC M 1 7 S Z A P C | ADC A 1 4 S Z A P C |
9x | SUB B 1 4 S Z A P C | SUB C 1 4 S Z A P C | SUB D 1 4 S Z A P C | SUB E 1 4 S Z A P C | SUB H 1 4 S Z A P C | SUB L 1 4 S Z A P C | SUB M 1 7 S Z A P C | SUB A 1 4 S Z A P C | SBB B 1 4 S Z A P C | SBB C 1 4 S Z A P C | SBB D 1 4 S Z A P C | SBB E 1 4 S Z A P C | SBB H 1 4 S Z A P C | SBB L 1 4 S Z A P C | SBB M 1 7 S Z A P C | SBB A 1 4 S Z A P C |
Ax | ANA B 1 4 S Z A P C | ANA C 1 4 S Z A P C | ANA D 1 4 S Z A P C | ANA E 1 4 S Z A P C | ANA H 1 4 S Z A P C | ANA L 1 4 S Z A P C | ANA M 1 7 S Z A P C | ANA A 1 4 S Z A P C | XRA B 1 4 S Z A P C | XRA C 1 4 S Z A P C | XRA D 1 4 S Z A P C | XRA E 1 4 S Z A P C | XRA H 1 4 S Z A P C | XRA L 1 4 S Z A P C | XRA M 1 7 S Z A P C | XRA A 1 4 S Z A P C |
Bx | ORA B 1 4 S Z A P C | ORA C 1 4 S Z A P C | ORA D 1 4 S Z A P C | ORA E 1 4 S Z A P C | ORA H 1 4 S Z A P C | ORA L 1 4 S Z A P C | ORA M 1 7 S Z A P C | ORA A 1 4 S Z A P C | CMP B 1 4 S Z A P C | CMP C 1 4 S Z A P C | CMP D 1 4 S Z A P C | CMP E 1 4 S Z A P C | CMP H 1 4 S Z A P C | CMP L 1 4 S Z A P C | CMP M 1 7 S Z A P C | CMP A 1 4 S Z A P C |
Cx | RNZ 1 11/5 – – – – – | POP B 1 10 – – – – – | JNZ a16 3 10 – – – – – | JMP a16 3 10 – – – – – | CNZ a16 3 17/11 – – – – – | PUSH B 1 11 – – – – – | ADI d8 2 7 S Z A P C | RST 0 1 11 – – – – – | RZ 1 11/5 – – – – – | RET 1 10 – – – – – | JZ a16 3 10 – – – – – | *JMP a16 3 10 – – – – – | CZ a16 3 17/11 – – – – – | CALL a16 3 17 – – – – – | ACI d8 2 7 S Z A P C | RST 1 1 11 – – – – – |
Dx | RNC 1 11/5 – – – – – | POP D 1 10 – – – – – | JNC a16 3 10 – – – – – | OUT d8 2 10 – – – – – | CNC a16 3 17/11 – – – – – | PUSH D 1 11 – – – – – | SUI d8 2 7 S Z A P C | RST 2 1 11 – – – – – | RC 1 11/5 – – – – – | *RET 1 10 – – – – – | JC a16 3 10 – – – – – | IN d8 2 10 – – – – – | CC a16 3 17/11 – – – – – | *CALL a16 3 17 – – – – – | SBI d8 2 7 S Z A P C | RST 3 1 11 – – – – – |
Ex | RPO 1 11/5 – – – – – | POP H 1 10 – – – – – | JPO a16 3 10 – – – – – | XTHL 1 18 – – – – – | CPO a16 3 17/11 – – – – – | PUSH H 1 11 – – – – – | ANI d8 2 7 S Z A P C | RST 4 1 11 – – – – – | RPE 1 11/5 – – – – – | PCHL 1 5 – – – – – | JPE a16 3 10 – – – – – | XCHG 1 5 – – – – – | CPE a16 3 17/11 – – – – – | *CALL a16 3 17 – – – – – | XRI d8 2 7 S Z A P C | RST 5 1 11 – – – – – |
Fx | RP 1 11/5 – – – – – | POP PSW 1 10 S Z A P C | JP a16 3 10 – – – – – | DI 1 4 – – – – – | CP a16 3 17/11 – – – – – | PUSH PSW 1 11 – – – – – | ORI d8 2 7 S Z A P C | RST 6 1 11 – – – – – | RM 1 11/5 – – – – – | SPHL 1 5 – – – – – | JM a16 3 10 – – – – – | EI 1 4 – – – – – | CM a16 3 17/11 – – – – – | *CALL a16 3 17 – – – – – | CPI d8 2 7 S Z A P C | RST 7 1 11 – – – – – |
|
|
Duration of conditional calls and returns is different when action is taken or not. This is indicated by two numbers separated by “/”. The higher number (on the left side of “/”) means duration of instruction when action is taken, the lower number (on the right side of “/”) means duration of instruction when action is not taken. All instructions marked by “*” are only alternative opcodes for existing instructions. Those alternative opcodes should not be used. |
(credits to https://pastraiser.com/)
Again equipped with the already known Flags (Sign, Zero, Auxiliary Carry, Parity, Carry) and Registers (AF, BC, DE, HL), emulating the 8080 doesn’t really turn out to be a big deal.
testing the cpu
To verify the 8080 works correctly, I decided to go for the TST8080.COM test rom, which can be found here.
def loadROM(file):
with open(file, "rb") as f:
res = f.read()
c = 0
for r in res:
memory[0x100 + c] = r
c = c + 1
memory[0x7] = 0xc9 # patch
memory[0x5] = 0xd3 # patch
memory[0x6] = 0x01 # patch
memory[0x0] = 0xd3 # patch
The test rom gets loaded to 0x100 in memory and we need to apply some special patches for it to run properly. The PC then just starts at 0x100 as well, an we are good to go, to compare to a proper stack trace of the ROM.
*** TEST: cpu_tests/TST8080.COM PC: 0100, AF: 0002, BC: 0000, DE: 0000, HL: 0000, SP: 0000, CYC: 0 (C3 B2 01 4D) (D3) (0000) PC: 01B2, AF: 0002, BC: 0000, DE: 0000, HL: 0000, SP: 0000, CYC: 10 (31 BD 07 21) (D3) (0000) PC: 01B5, AF: 0002, BC: 0000, DE: 0000, HL: 0000, SP: 07BD, CYC: 20 (21 03 01 CD) (D3) (0000) PC: 01B8, AF: 0002, BC: 0000, DE: 0000, HL: 0103, SP: 07BD, CYC: 30 (CD 4B 01 E6) (4D) (0103) PC: 014B, AF: 0002, BC: 0000, DE: 0000, HL: 0103, SP: 07BB, CYC: 47 (D5 EB 0E 09) (4D) (0103) PC: 014C, AF: 0002, BC: 0000, DE: 0000, HL: 0103, SP: 07B9, CYC: 58 (EB 0E 09 CD) (4D) (0103) PC: 014D, AF: 0002, BC: 0000, DE: 0103, HL: 0000, SP: 07B9, CYC: 62 (0E 09 CD 05) (D3) (0000) PC: 014F, AF: 0002, BC: 0009, DE: 0103, HL: 0000, SP: 07B9, CYC: 69 (CD 05 00 D1) (D3) (0000) PC: 0005, AF: 0002, BC: 0009, DE: 0103, HL: 0000, SP: 07B7, CYC: 86 (D3 01 C9 00) (D3) (0000) MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC VERSION 1.0 (C) 1980 PC: 0007, AF: 0002, BC: 0009, DE: 0103, HL: 0000, SP: 07B7, CYC: 96 (C9 00 00 00) (D3) (0000) PC: 0152, AF: 0002, BC: 0009, DE: 0103, HL: 0000, SP: 07B9, CYC: 106 (D1 C9 0E 02) (D3) (0000) PC: 0153, AF: 0002, BC: 0009, DE: 0000, HL: 0000, SP: 07BB, CYC: 116 (C9 0E 02 CD) (D3) (0000) PC: 01BB, AF: 0002, BC: 0009, DE: 0000, HL: 0000, SP: 07BD, CYC: 126 (E6 00 CA C3) (D3) (0000) PC: 01BD, AF: 0046, BC: 0009, DE: 0000, HL: 0000, SP: 07BD, CYC: 133 (CA C3 01 CD) (D3) (0000) ...
The full working tracelog can be downloaded here.
Comments