TORTOISE is a simple CPU emulator implemented in C designed for educational purposes and to demonstrate the fundamentals of CPU architecture and instruction execution. The emulator models a minimalist 16bit CPU with a flags register, a fixed size memory space, and general purpose registers.
- Registers: 8 general purpose 16bit registers (R0, R7).
- Program Counter (PC): 32bit register pointing to the current instruction in memory, allowing addressing of the full 64KB memory space.
- Stack Pointer (SP): 32bit register managing the call stack in memory.
- Flags Register: 8bit register updated by arithmetic instructions, carrying Zero (Z), Carry (C), Sign (S), and Overflow (V) flags.
- Memory: 64KB byte addressable memory representing RAM.
- Halted Flag: Indicates whether the CPU is halted.
The emulator supports a set of instructions facilitating data movement, arithmetic, logical operations, control flow, and program termination:
| Opcode | Mnemonic | Description |
|---|---|---|
| 0x00 | NOP | No operation |
| 0x01 | LOAD | Load immediate 16bit value into register |
| 0x02 | ADD | Add immediate 16bit value to register |
| 0x03 | STORE | Store register value into memory |
| 0x04 | JMP | Unconditional jump to memory address |
| 0x05 | JZ | Jump if zero flag is set |
| 0x06 | AND | Bitwise AND immediate with register |
| 0x07 | OR | Bitwise OR immediate with register |
| 0x08 | XOR | Bitwise XOR immediate with register |
| 0x09 | NOT | Bitwise NOT of register |
| 0x0A | CALL | Call subroutine at memory address |
| 0x0B | RET | Return from subroutine |
| 0x0C | IN | Input from device (simulated) |
| 0x0D | OUT | Output to device (simulated) |
| 0x0E | SUB | Subtract immediate 16bit value from register |
| 0x0F | CMP | Compare register against immediate, set flags, discard result |
| 0x10 | JC | Jump if carry flag is set |
| 0x11 | JN | Jump if sign flag is set (negative) |
| 0x12 | JO | Jump if overflow flag is set |
| 0x13 | JNZ | Jump if zero flag is not set |
| 0xFF | HALT | Stop execution |
ADD and SUB update all four flags after every execution. CMP performs a subtraction, updates flags, and discards the result, making it the standard way to drive conditional jumps without modifying register state.
Z (Zero): Set when the result is 0x0000. C (Carry): Set on unsigned borrow in subtraction, or unsigned overflow in addition. S (Sign): Set when bit 15 of the result is 1. V (Overflow): Set when the operation overflows the signed 16bit range.
The CPU operates by repeatedly performing the following steps:
- Fetch: The instruction byte is read from memory at the address indicated by the PC.
- Decode: The opcode and operands (register indices, immediate values, addresses) are parsed.
- Execute: The instruction modifies CPU state by changing registers, memory, flags, or program counter.
- Advance: The PC is updated to point to the next instruction unless altered by control flow instructions (JMP, CALL, RET, or a taken conditional jump).
Instructions use fixed size formats for simplicity of decoding.
src/ contains the CPU emulator source files. cpu.c implements CPU registers, flags, memory, and instruction execution logic. main.c is the entrypoint initializing the CPU and starting execution.
include/ contains header files exposing the CPU API and data structures, including the flag bit definitions (FLAG_Z, FLAG_C, FLAG_S, FLAG_V).
tests/ contains unit tests. test_cpu.c covers basic and extended instruction tests including arithmetic, logical, and control flow operations.
assembler/ contains the assembler source and all assembly test programs. assembler.c converts human readable assembly into binary machine code. The .asm files are the assembly source programs.
Makefile provides build targets for the emulator, assembler, and all tests.
Build the emulator:
make
Run the emulator with a binary program:
./tortoise program.bin
Run the unit tests:
make run_test
Build the assembler:
make assembler
Run all assembler tests:
make run_asm_test
The TORTOISE assembler is a command line tool written in C that converts human readable assembly language programs into binary machine code executable by the TORTOISE CPU emulator. It supports the full instruction set of the emulator, including data movement (LOAD, STORE), arithmetic (ADD, SUB, CMP), logical operations (AND, OR, XOR, NOT), control flow (JMP, JZ, JC, JN, JO, JNZ, CALL, RET), I/O instructions (IN, OUT), and program termination (HALT).
The assembler reads assembly source files line by line, ignoring comments (;) and blank lines, and parses instructions and operands which can be registers or immediate numeric values in decimal or hexadecimal. It outputs machine code in a compact binary format ready for loading into emulator memory.
assembler/logic.asm tests the core bitwise instructions in sequence. It loads values into R0 and R1, then applies AND, OR, XOR, and NOT operations, verifying register state at each step.
LOAD R0, 0x0F0F
LOAD R1, 0x3333
AND R0, 0x00FF
OR R1, 0x00CC
XOR R0, 0x00FF
NOT R1
HALT
23 bytes written to assembler/logic.bin
cpu_init: PC=0x0 SP=0x10000
loaded 23 bytes into memory
executing opcode 0x1 at PC=0x0
executing opcode 0x1 at PC=0x4
executing opcode 0x6 at PC=0x8
AND before: R0=0xF0F val=0xFF
AND after: R0=0xF
executing opcode 0x7 at PC=0xC
OR before: R1=0x3333 val=0xCC
OR after: R1=0x33FF
executing opcode 0x8 at PC=0x10
XOR before: R0=0xF val=0xFF
XOR after: R0=0xF0
executing opcode 0x9 at PC=0x14
NOT before: R1=0x33FF
NOT after: R1=0xCC00
executing opcode 0xFF at PC=0x16
Initial values are loaded into R0 and R1. AND masks the lower byte of R0 correctly. OR sets bits in R1 as expected. XOR toggles bits in R0 as expected. NOT inverts R1 correctly. The program halts cleanly after all instructions.
assembler/truth.asm verifies the truth table for two input bits stored in R0 and R1. It outputs inputs and results of AND, OR, XOR, and NOT across all four input combinations (00, 01, 10, 11) before halting. The AND result is 1 only when both inputs are 1, OR is 1 when either input is 1, XOR is 1 when inputs differ, and NOT outputs the bitwise complement of input A.
assembler/bitwise_masking.asm verifies AND and XOR chained together. It loads 0xABCD into R0, applies AND with 0x00FF to isolate the lower byte (yielding 0xCD), then XORs with 0x00FF to toggle those bits (yielding 0x32). The result is output before halting.
assembler/or_chain.asm tests consecutive OR operations. It loads 0x0001 into R0, ORs with 0x0004 yielding 0x0005, then ORs with 0x0008 yielding 0x000D. This validates that OR operations accumulate bit sets correctly across multiple instructions.
assembler/toggle_bits.asm verifies the NOT instruction. R0 is loaded with 0x0F0F and NOT is applied, flipping all 16 bits to yield 0xF0F0. The result is output before halting.
assembler/xor_flip_flop.asm tests XOR against a full mask. R0 is loaded with the alternating pattern 0xAAAA and XORed with 0xFFFF, yielding 0x5555. This confirms that XOR correctly toggles every bit when the mask is all ones.
assembler/flags_test.asm exercises the flags register and all conditional jump instructions. It runs four independent tests in sequence and outputs one character per test to confirm each flag path was taken correctly.
Test 1, JNZ countdown loop. R0 is loaded with 3. The loop prints 1, subtracts 1 from R0, and jumps back while the zero flag is not set. It iterates exactly three times, printing 111, then falls through and prints A to confirm loop exit.
Test 2, JN on negative result. R1 is loaded with 5 and compared against 10 using CMP. Since 5 < 10, the subtraction borrows and sets the sign flag (S). JN is taken, printing B.
Test 3, JO on signed overflow. R2 is loaded with 0x7FFF (the maximum positive signed 16bit value) and 1 is added. The result wraps to 0x8000, setting the overflow flag (V). JO is taken, printing C.
Test 4, JZ on equal comparison. R3 is loaded with 0x00AA and compared against 0x00AA using CMP. The result is zero, setting the zero flag (Z). JZ is taken, printing D.
The expected output is 111ABCD. The three 1s confirm the loop body ran exactly three times before the zero flag terminated it. Each letter confirms one flag and its corresponding conditional jump worked correctly.
95 bytes written to assembler/flags_test.bin
cpu_init: PC=0x0 SP=0x10000
loaded 95 bytes into memory
...
SUB: R0=0x3 - 0x1 = 0x2 flags=0x0
...
SUB: R0=0x2 - 0x1 = 0x1 flags=0x0
...
SUB: R0=0x1 - 0x1 = 0x0 flags=0x1
...
CMP: R1=0x5 vs 0xA flags=0x6
...
CMP: R3=0xAA vs 0xAA flags=0x1
...
executing opcode 0xFF at PC=0x5E
flags=0x1 on the final SUB confirms Z was set. flags=0x6 on the CMP confirms both C and S were set for the underflow case. flags=0x1 on the equal CMP confirms Z was set. All four conditional jumps resolved correctly.
The emulator is structured to encourage experimentation and learning. Possible extensions include:
Developing a simple assembler and loader for human readable program writing.Fix compiler warnings.Flags register with SUB, CMP, and conditional jumps (JC, JN, JO, JNZ).- Adding shift instructions (SHL, SHR) to complete the ALU.
- Adding multiplication and division instructions.
- Implementing a two-pass assembler to support named labels in assembly source.
- Implementing interrupt handling and I/O devices beyond simple simulated input/output.
- Supporting larger memory sizes or different data widths (32bit registers).
Gain understanding of CPU architecture components and their interaction. Practical experience writing an emulator and instruction decoding. Insight into instruction set design including arithmetic, flags, logical, and control flow operations. Observe real time instruction execution and CPU state mutation debugging. Understand how flags registers drive conditional execution in real hardware.
This project is provided under the GPL3 License.