tiny8.cpu module

A simplified AVR-like 8-bit CPU simulator.

This module provides a lightweight CPU model inspired by the ATmega family. The CPU class is the primary export and implements a small, extensible instruction-dispatch model. The implementation favors readability over cycle-accurate emulation. Add instruction handlers by defining methods named op_<mnemonic> on CPU.

class tiny8.cpu.CPU(memory: Memory | None = None)[source]

Bases: object

In-memory 8-bit AVR-like CPU model.

The CPU implements a compact instruction-dispatch model. Handlers are methods named op_<mnemonic> and are invoked by step() for the currently-loaded program.

Attributes:

regs (list[int]): 32 8-bit general purpose registers (R0..R31). pc (int): Program counter (index into program). sp (int): Stack pointer (index into RAM in the associated

sreg (int): Status register bits stored in a single integer (I, T,

H, S, V, N, Z, C).

cycle (int): Instruction execution counter. reg_trace (list[tuple[int, int, int]]): Per-cycle register change

trace entries of the form (cycle, reg, new_value).

mem_trace (list[tuple[int, int, int]]): Per-cycle memory change

trace entries of the form (cycle, addr, new_value).

step_trace (list[dict]): Full per-step snapshots useful for

visualization and debugging.

Note:

This implementation simplifies many AVR specifics (flag semantics, exact cycle counts, IO mapping) in favor of clarity. Extend or replace individual op_ handlers to increase fidelity.

get_flag(bit: int) bool[source]

Return the boolean value of a specific SREG flag bit.

Args:

bit: Integer bit index (0..7).

Returns:

True if the bit is set, False otherwise.

load_program(program: list[tuple[str, tuple]], labels: dict[str, int])[source]

Load an assembled program into the CPU.

Args:
program: list of (mnemonic, operands) tuples returned by the

assembler.

labels: Mapping of label strings to instruction indices.

Note:

After loading the program, the program counter is reset to zero.

op_adc(rd: int, rr: int)[source]

Add with carry (Rd := Rd + Rr + C) and update flags.

op_add(rd: int, rr: int)[source]

Add register rr to rd (Rd := Rd + Rr) and update flags.

Sets C, H, N, V, S, Z per AVR semantics.

op_adiw(rd_word_low: int, imm_word: int)[source]

Add immediate to word register pair (Rd:Rd+1) - simplified.

rd_word_low is the low register of the pair (even register index). imm_word is a 16-bit immediate to add.

op_and(rd: int, rr: int)[source]

Logical AND (Rd := Rd & Rr) — updates N, Z, V=0, C=0, H=0, S.

op_andi(rd: int, imm: int)[source]
op_brcc(label: str)[source]

Branch to a label if the carry flag is clear.

Args:

label (str): Destination label to jump to if the carry flag is clear.

op_brcs(label: str)[source]

Branch to a label if the carry flag is set.

Args:

label (str): Destination label to jump to if the carry flag is set.

op_breq(label: str)[source]

Branch to label if Zero flag is set (BREQ).

Args:

label: Destination label to jump to if Z flag is set.

op_brge(label: str | int)[source]

BRGE - Branch if Greater or Equal (Signed)

Args:

label: Destination label or address to jump to if the condition is met.

op_brlt(label: str | int)[source]

BRLT - Branch if Less Than (Signed).

Args:

label: Destination label or address to jump to if the condition is met.

op_brmi(label: str | int)[source]

BRMI - Branch if Minus (Negative flag set).

Branches when the N flag is set (negative result).

Args:

label: Destination label or address to jump to if the condition is met.

op_brne(label: str)[source]

Branch to label if Zero flag is not set (BRNE).

Args:

label: Destination label to jump to if Z flag is not set.

op_brpl(label: str | int)[source]

BRPL - Branch if Plus (Negative flag clear).

Branches when the N flag is clear (non-negative result).

Args:

label: Destination label or address to jump to if the condition is met.

op_call(label: str)[source]

Call a subroutine by pushing the return address and jumping to label.

The return address (pc+1) is pushed as two bytes (high then low) onto the stack, decrementing the stack pointer after each write. Control then jumps to label.

Args:

label (str): Label to call.

op_cbi(io_addr: int, bit: int)[source]

Clear a bit in an I/O/memory-mapped address (use RAM area).

Args:

io_addr: I/O or RAM address to modify. bit: Bit index to clear (0..7).

op_cli()[source]

Clear Global Interrupt Enable (I bit).

op_clr(rd: int)[source]

Clear register (Rd := 0). Behaves like EOR Rd,Rd for flags.

op_com(rd: int)[source]

One’s complement: Rd := ~Rd. Updates N,V,S,Z,C per AVR-ish semantics.

op_cp(rd: int, rr: int)[source]
op_cpi(rd: int, imm: int)[source]
op_cpse(rd: int, rr: int)[source]

Compare and Skip if Equal: compare Rd,Rr; if equal, skip next instruction.

op_dec(rd: int)[source]

Decrement (Rd := Rd - 1) — updates V,N,S,Z; does not change C/H.

op_div(rd: int, rr: int)[source]

Unsigned divide convenience instruction: quotient -> Rd, remainder -> Rd+1.

op_eor(rd: int, rr: int)[source]

Exclusive OR (Rd := Rd ^ Rr) — updates N, Z, V=0, C=0, H=0, S.

op_eori(rd: int, imm: int)[source]
op_in(rd: int, port: int)[source]
op_inc(rd: int)[source]

Increment (Rd := Rd + 1) — updates V,N,S,Z; does not change C/H.

op_jmp(label: str | int)[source]

Jump to a given label or numeric address by updating the program counter.

This operation sets the CPU’s program counter (self.pc) to the target address minus one. The subtraction of one accounts for the fact that the instruction dispatcher will typically increment the program counter after the current instruction completes.

Args:
label (str | int): The jump target. If a string, it is treated as a symbolic label

and looked up in self.labels to obtain its numeric address. If an int (or any value convertible to int), it is used directly as the numeric address.

op_ld(rd: int, addr_reg: int)[source]

Load from RAM at address contained in register addr_reg into rd.

Args:

rd: Destination register index. addr_reg: Register index containing the RAM address to load from.

op_ldi(reg_idx: int, imm: int)[source]

Load an immediate value into a register.

Args:

reg_idx: Destination register index. imm: Immediate value to load.

Note:

The AVR LDI instruction is normally restricted to R16..R31; this simplified implementation accepts any register index.

op_lsl(rd: int)[source]
op_lsr(rd: int)[source]
op_mov(rd: int, rr: int)[source]

Copy the value from register rr into rd.

Args:

rd: Destination register index. rr: Source register index.

op_mul(rd: int, rr: int)[source]

Multiply 8x8 -> 16: store low in Rd, high in Rd+1. Update Z and C conservatively.

op_neg(rd: int)[source]

Two’s complement (negate): Rd := 0 - Rd. Flags as subtraction from 0.

op_nop()[source]

No-operation: does nothing for one cycle.

op_or(rd: int, rr: int)[source]

Logical OR (Rd := Rd | Rr) — updates N, Z, V=0, C=0, H=0, S.

op_ori(rd: int, imm: int)[source]
op_out(port: int, rr: int)[source]
op_pop(rd: int)[source]

Pop a value from the stack into a register.

The stack pointer is incremented, the byte at the new stack pointer is read from RAM, and the value is written into register rd.

Args:

rd (int): Destination register index to receive the popped value.

op_push(rr: int)[source]

Push a register value onto the stack.

The value of register rr is written to the RAM at the current stack pointer, and the stack pointer is then decremented.

Args:

rr (int): Source register index to push.

op_rcall(label: str)[source]

Relative call — push return address and jump relatively or to label.

op_ret()[source]

Return from subroutine by popping the return address and setting PC.

Two bytes are popped from the stack (low then high) to reconstruct the return address, which is then loaded into the program counter (adjusted because step() will increment PC after execution).

op_reti()[source]

Return from interrupt: pop return address and set I flag.

op_rjmp(label: str)[source]

Relative jump — label may be an int or string label.

op_rol(rd: int)[source]
op_ror(rd: int)[source]
op_sbc(rd: int, rr: int)[source]
op_sbci(rd: int, imm: int)[source]

Subtract immediate with carry: Rd := Rd - K - C

op_sbi(io_addr: int, bit: int)[source]

Set a bit in an I/O/memory-mapped address (use RAM area).

Args:

io_addr: I/O or RAM address to modify. bit: Bit index to set (0..7).

op_sbic(io_addr: int, bit: int)[source]

Skip if bit in IO/RAM-mapped address is clear.

op_sbis(io_addr: int, bit: int)[source]

Skip if bit in IO/RAM-mapped address is set.

op_sbiw(rd_word_low: int, imm_word: int)[source]

Subtract immediate from word register pair (Rd:Rd+1) — simplified.

rd_word_low is the low register of the pair (even register index). imm_word is a 16-bit immediate to subtract.

op_sbrc(rd: int, bit: int)[source]

Skip next if bit in register is clear.

op_sbrs(rd: int, bit: int)[source]

Skip next if bit in register is set.

op_sei()[source]

Set Global Interrupt Enable (I bit).

op_ser(rd: int)[source]

Set register all ones (Rd := 0xFF). Update flags conservatively.

op_st(addr_reg: int, rr: int)[source]

Store register rr into RAM at address contained in register addr_reg.

Args:

addr_reg: Register index containing the RAM address to write to. rr: Source register index to store.

op_sub(rd: int, rr: int)[source]

Subtract (Rd := Rd - Rr) and set flags C,H,N,V,S,Z.

op_subi(rd: int, imm: int)[source]
op_swap(rd: int)[source]

Swap nibbles in register: Rd[7:4] <-> Rd[3:0]. Does not affect SREG.

op_tst(rd: int)[source]

Test: perform AND Rd,Rd and update flags but do not store result.

read_ram(addr: int) int[source]

Read a byte from RAM at the given address.

Args:

addr: RAM address to read.

Returns:

Byte value stored at addr (0..255).

read_reg(r: int) int[source]

Return the 8-bit value from register r.

Args:

r: Register index (0..31).

Returns:

The 8-bit value (0..255) stored in the register.

run(max_cycles: int = 100000) None[source]

Run instructions until program end or max_cycles is reached.

Args:
max_cycles: Maximum number of instruction cycles to execute

(default 100000).

Note:

This repeatedly calls step() until it returns False or the maximum cycle count is reached.

set_flag(bit: int, value: bool) None[source]

Set or clear a specific SREG flag bit.

Args:

bit: Integer bit index (0..7) representing the flag position. value: True to set the bit, False to clear it.

step() bool[source]

Execute a single instruction at the current program counter.

Performs one fetch-decode-execute cycle. A pre-step snapshot of registers and non-zero RAM is recorded, the instruction handler (op_<mnemonic>) is invoked, and a post-step trace entry is appended to step_trace.

Returns:

True if an instruction was executed; False if the PC is out of range and execution should stop.

trigger_interrupt(vector_addr: int)[source]

Trigger an interrupt vector if it is enabled.

If the interrupt vector is enabled in self.interrupts, the current PC+1 is pushed onto the stack (high then low byte) and control jumps to vector_addr.

Args:

vector_addr (int): Interrupt vector address to jump to.

Returns:

None

write_ram(addr: int, val: int) None[source]

Write an 8-bit value to RAM at addr and record the trace.

Args:

addr: RAM address to write. val: Value to write; will be truncated to 8 bits.

Note:

The underlying Memory object stores the value; a (cycle, addr, val) tuple is appended to mem_trace for visualizers/tests.

write_reg(r: int, val: int) None[source]

Write an 8-bit value to register r and record the change.

Args:

r: Register index (0..31). val: Value to write; will be truncated to 8 bits.

Note:

A trace entry is appended only when the register value actually changes to avoid noisy traces.