A working proof-of-concept CPU emulator for the Intel 8080 and Zilog Z80 processors, implemented in ABAP (SAP's programming language). This project demonstrates the feasibility of emulating vintage 8-bit CPUs using ABAP's bit manipulation and table processing capabilities.
Based on analysis of RunCPM, this implementation uses a hybrid architecture:
- Code-driven dispatch: Direct
CASE
statement for opcode execution (fast, debuggable) - Table-driven flag calculations: Pre-computed lookup tables for complex flag operations
" β FAST: Direct opcode dispatch
CASE lv_opcode.
WHEN '01'. mv_bc = read_word_pp( cv_addr = mv_pc ). " LD BC,nnnn
WHEN '04'. " INC B - uses pre-computed table for flags
mv_bc = mv_bc + 256.
lv_flags = mt_inc_table[ get_high_byte( mv_bc ) + 1 ].
ENDCASE.
" β SLOWER: Pure table-driven (metadata lookup + indirect dispatch)
READ TABLE gt_opcodes WITH KEY opcode = lv_op INTO ls_meta.
CALL METHOD (ls_meta-handler) ...
Advantages in ABAP:
- Direct execution path (no indirection overhead)
- CASE statement is optimized (hash table internally)
- BIT operations work well:
BIT-AND
,BIT-OR
,BIT-XOR
,BIT-SHIFT
- Debugger-friendly (step through actual logic, not metadata)
- Pre-computed tables still eliminate expensive flag calculations
cpm-abap/
βββ src/
β βββ zcl_cpu_8080.clas.abap - Main CPU emulator class
β βββ z_test_cpu_8080.prog.abap - Unit test suite
βββ RunCPM/ - Reference implementation (C)
βββ BRAINSTORM.md - Architecture design notes
βββ README.md - This file
Goal: Enable local TDD with instant test execution (< 1 second)
Status: IN PROGRESS
Tasks:
- Setup npm/transpiler packages
- Create transpiler-compatible test class
- Fix memory representation (table β XSTRING)
- Fix BIT operations (use proper hex types)
- Fix lookup table initialization
- Transpile and verify no errors
- Run tests locally with Node.js
- Commit working version
Why: This unlocks instant local testing without SAP server, enabling true TDD workflow
Time estimate: 2-4 hours
Goal: Run real CP/M .COM files (HELLO.COM)
Remaining opcodes: 220
- MOV r,r family (49 opcodes)
- ADD/ADC/SUB/SBC family (32 opcodes)
- AND/OR/XOR/CP family (32 opcodes)
- Conditional jumps/calls/returns (16 opcodes)
- Stack operations (PUSH, POP - 8 opcodes)
- I/O operations (IN, OUT - 8 opcodes)
- Rotate/shift operations (8 opcodes)
Goal: Run interactive CP/M programs (text adventures, BASIC)
Components:
- BDOS syscall interface (intercept CALL 0x0005)
- Console I/O (functions 1, 2, 9, 10, 11)
- File I/O (functions 15, 16, 20-26)
- Disk operations (simulated via ABAP tables or browser storage)
Goal: Run advanced software (Turbo Pascal, CP/M 3)
Extensions:
- CB prefix - Bit operations (BIT, SET, RES, shifts)
- ED prefix - Block operations (LDIR, CPIR, etc.)
- DD/FD prefix - IX/IY index registers
- Alternate register set (AF', BC', DE', HL')
Core CPU Features:
- 64KB addressable memory
- All 8080/Z80 registers (AF, BC, DE, HL, PC, SP)
- Pre-computed lookup tables (parity, inc, dec, carry bits)
- Bit manipulation helpers (high/low byte access)
- Memory access methods (byte/word, little-endian)
- CPU status management (running/halted)
Opcodes Implemented (20 core instructions):
-
0x00
- NOP -
0x01
- LD BC,nnnn -
0x02
- LD (BC),A -
0x03
- INC BC -
0x04
- INC B (with flag lookup table) -
0x05
- DEC B (with flag lookup table) -
0x06
- LD B,nn -
0x0A
- LD A,(BC) -
0x0B
- DEC BC -
0x0C
- INC C -
0x0D
- DEC C -
0x0E
- LD C,nn -
0x11
- LD DE,nnnn -
0x13
- INC DE -
0x1B
- DEC DE -
0x21
- LD HL,nnnn -
0x23
- INC HL -
0x2B
- DEC HL -
0x31
- LD SP,nnnn -
0x33
- INC SP -
0x3B
- DEC SP -
0x76
- HALT -
0xC3
- JP nnnn (unconditional jump) -
0xC9
- RET (return from subroutine) -
0xCD
- CALL nnnn (call subroutine)
Milestone 2: Complete i8080 (~244 opcodes)
- MOV r,r family (0x40-0x7F) - 49 opcodes
- ADD/ADC/SUB/SBC family (0x80-0x9F) - 32 opcodes
- AND/OR/XOR/CP family (0xA0-0xBF) - 32 opcodes
- Conditional jumps/calls/returns (JZ, JNZ, JC, etc.)
- Stack operations (PUSH, POP)
- I/O operations (IN, OUT)
- Rotate/shift operations (RLCA, RRCA, RLA, RRA)
Milestone 3: CP/M BDOS Emulation
- BDOS syscall interface
- Console I/O (functions 1, 2, 9, 10, 11)
- File I/O (functions 15, 16, 20-26)
- Disk operations (simulated)
Milestone 4: Z80 Extensions
- CB prefix - Bit operations (BIT, SET, RES, shifts)
- ED prefix - Block operations (LDIR, CPIR, etc.)
- DD/FD prefix - IX/IY index registers
- Alternate register set (AF', BC', DE', HL')
" Store register pairs as single 32-bit integers
DATA: mv_af TYPE i. " A=high byte, F=low byte
" Access via helper methods
METHOD get_high_byte.
rv_val = iv_pair DIV 256.
rv_val = rv_val MOD 256.
ENDMETHOD.
METHOD set_high_byte.
rv_new = ( iv_pair MOD 256 ) + ( iv_val * 256 ).
ENDMETHOD.
Why? Simpler arithmetic for 16-bit operations (INC BC, ADD HL,BC)
" 64KB as standard table
TYPES: ty_memory TYPE STANDARD TABLE OF x LENGTH 1 WITH EMPTY KEY.
DATA: mt_memory TYPE ty_memory.
" Index-based access (1-based in ABAP!)
READ TABLE mt_memory INDEX ( lv_addr + 1 ) INTO rv_val.
mt_memory[ lv_addr + 1 ] = iv_val.
Alternative considered: Hashed table with key=address
- Pro: O(1) lookup for sparse memory access
- Con: More overhead for sequential access, CP/M uses most of 64KB
" INC table (257 entries): flags for every possible result 0-256
mt_inc_table[0] = '50' " 0x50 - Zero + Half-carry
mt_inc_table[1] = '00' " No flags
...
mt_inc_table[128] = '90' " 0x90 - Sign set
" Usage in opcode:
WHEN '04'. " INC B
mv_bc = mv_bc + 256.
READ TABLE mt_inc_table INDEX ( get_high_byte( mv_bc ) + 1 )
INTO lv_flags.
" Combine with existing carry flag
lv_flags = lv_flags BIT-OR ( get_flags_byte( ) BIT-AND c_flag_c ).
set_flags_byte( lv_flags ).
Why? Flag calculation is complex (parity, half-carry, overflow). Pre-computing eliminates:
- Bit counting for parity (expensive)
- Conditional logic for each flag bit
- Potential bugs in flag calculation
METHOD execute_opcode.
CASE iv_opcode.
WHEN '00'. " NOP
WHEN '01'. mv_bc = read_word_pp( cv_addr = mv_pc ).
WHEN '02'. write_byte( mv_bc, get_high_byte( mv_af ) ).
" ... 256 cases
ENDCASE.
ENDMETHOD.
Why not pure table-driven?
- ABAP has no function pointers
- Dynamic method calls are slow:
CALL METHOD (lv_handler)...
- CASE is optimized by ABAP kernel (hash table for large switches)
- Direct code is easier to debug/trace
The test suite (z_test_cpu_8080.prog.abap
) validates:
- CPU Initialization - Registers, memory, status
- Memory Operations - Byte/word read/write, little-endian encoding
- Register Instructions - LD BC/DE/HL/SP, INC/DEC
- Flag Handling - INC/DEC flag table lookups
- Control Flow - JP, CALL, RET, HALT
- Multi-Instruction Programs - Execute sequence until HALT
======= Test 1: CPU Initialization =======
β PC starts at 0x0100
β AF register is zero
β BC register is zero
======= Test 12: CALL/RET (0xCD/0xC9) =======
β CALL jumps to 0x3000
β CALL pushes return address on stack
β RET returns to caller
β RET pops return address
Test Summary:
Passed: 42
Failed: 0
β All tests passed!
-
Internal table access -
READ TABLE ... INDEX
is O(1) but has overhead- Mitigation: Consider field-symbols for hot paths
-
Bit operations -
BIT-AND
,BIT-OR
are fast, butBIT-SHIFT
is slower- Mitigation: Use DIV/MOD for byte extraction (faster than shift)
-
Method calls - ABAP method call overhead is ~10x C function call
- Mitigation: Inline critical paths (memory access, flag checks)
On modern SAP NetWeaver ABAP 7.5+ (2020 hardware):
- ~500K-1M instructions/second (estimated)
- Compare: Original 8080 @ 2MHz = 500K instructions/second
- We can emulate at original speed!
On older systems (ABAP 7.0, 2010 hardware):
- ~100K-200K instructions/second
- Still enough for interactive CP/M programs
" Create CPU instance
DATA: lo_cpu TYPE REF TO zcl_cpu_8080.
CREATE OBJECT lo_cpu.
" Load a simple program
DATA(lv_program) = '01 34 12 03 76'. " LD BC,0x1234; INC BC; HALT
lo_cpu->load_com_file( iv_data = lv_program ).
" Execute until HALT
DATA(lv_count) = lo_cpu->execute_until_halt( iv_max_instructions = 1000 ).
" Inspect results
WRITE: / 'Executed', lv_count, 'instructions'.
WRITE: / 'BC =', lo_cpu->get_bc( ). " Should be 0x1235
If you wanted to convert this to fully table-driven:
-
Opcode Metadata Table
TYPES: BEGIN OF ty_opcode_meta, opcode TYPE x LENGTH 1, mnemonic TYPE string, handler TYPE string, " Method name operand1 TYPE string, operand2 TYPE string, length TYPE i, cycles TYPE i, flags_affect TYPE c LENGTH 5, END OF ty_opcode_meta.
-
Operation Handlers
- Still need ~50-100 unique operation methods
- LOAD_REG, ALU_ADD, ALU_SUB, JUMP, CALL, RET, etc.
- Same amount of code as switch/case!
-
Dynamic Dispatch
READ TABLE gt_opcodes WITH KEY opcode = lv_op INTO ls_meta. CALL METHOD (ls_meta-handler) " Dynamic call - slow! EXPORTING ...
- Metadata definition: 244 i8080 opcodes Γ 8 fields = 2-3 days
- Handler methods: ~50 unique handlers = 3-5 days (same as switch/case)
- Dynamic dispatch framework: 1-2 days
- Testing/debugging: +50% (harder to trace)
Total: 10-15 days
Hybrid approach (current): 5-8 days
Pure table-driven offers no performance benefit in ABAP:
- Dynamic method calls are slower than CASE dispatch
- Metadata lookup adds overhead
- Still need operation handler code
- Harder to debug (step through metadata, not logic)
Hybrid wins: Fast execution, readable code, easy debugging.
- RunCPM - Reference C implementation
- Z80 CPU User Manual
- i8080 Opcode Reference
- CP/M 2.2 System Manual
This is a proof-of-concept educational project. The RunCPM reference implementation is MIT licensed.
To actually run CP/M .COM files (like ZORK, Turbo Pascal, dBase):
- Complete i8080 opcode set (244 opcodes) - ~1 week
- Implement CP/M BDOS syscalls - ~1 week
- Console I/O (print string, read line)
- File operations (open, read, write, close)
- Disk simulation (map to ABAP database tables?)
- Test with real software - ongoing
- Start: HELLO.COM
- Then: Simple text adventures
- Finally: Turbo Pascal, MBASIC, dBase II
Total time to first "Hello World" .COM file: ~2-3 weeks
Built with ABAP bit-twiddling and a lot of coffee