Skip to content

Commit

Permalink
sapemu: Implement some addition instructions and others
Browse files Browse the repository at this point in the history
187/256
  • Loading branch information
kleinesfilmroellchen committed Sep 14, 2024
1 parent 20dd3ca commit 74d82c5
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 32 deletions.
2 changes: 1 addition & 1 deletion doc/src/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ In practice, spcasm will automatically compute the correct offset necessary to p

### Endianness and 16-bit instructions

The SPC700 is a little endian architecture, meaning that the least-significant byte comes first in memory. This is not only relevant for instruction encoding of memory addresses which the user does not need to worry about, but also the behavior of the 16-bit instructions. Some of these instructions contain the suffix "W" for "wide", but plenty of control flow instructions also need to consider whole words of memory. Whenever they operate on the direct page, the specified memory address forms the lower byte, and the memory address after this forms the higher byte. Note: It is not known what happens if the second memory address would be outside the direct page. The pseudo-register YA is also used by these instructions, and here the A register forms the lower byte.
The SPC700 is a little endian architecture, meaning that the least-significant byte comes first in memory. This is not only relevant for instruction encoding of memory addresses which the user does not need to worry about, but also the behavior of the 16-bit instructions. Some of these instructions contain the suffix "W" for "wide", but plenty of control flow instructions also need to consider whole words of memory. Whenever they operate on the direct page, the specified memory address forms the lower byte, and the memory address after this forms the higher byte. The second address wraps around at page boundaries, so if the first address has the lower byte $FF, the second address has the lower byte $00 but the same upper byte! For instance, when accessing a direct page pointer through double-indirect addressing `($FF+X)`, X is zero and the P flag is 1, this would read the pointer's low byte from $01FF and the high byte from $0100. In the pseudo-register YA, the A register forms the lower byte.

The little endianness is also preserved for stack operations, most importantly all call instructions, which push the 16-bit program counter. This means they first push the higher 8 bits of the program counter to the stack, and then the lower 8 bits, and since the stack grows downwards, this yields a little-endian address. That simplifies any operations which manually retrieve the return address.

Expand Down
2 changes: 1 addition & 1 deletion opcodes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ to carry out tests with conditional branching commands.
Half Carry Flag (H)

After operation execution, this flag is set when there has been
a carry from form bit 3 of the ALU to bit 4, or when there has
a carry from from bit 3 of the ALU to bit 4, or when there has
not been any borrow. There is no command to set, however, it is
reset by reset by means of the CLRV command. At his time, the
overflow flag is also set.
Expand Down
2 changes: 1 addition & 1 deletion sapemu/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! S-APU / SPC700 emulator.
#![feature(slice_as_chunks, generic_const_exprs, adt_const_params, let_chains)]
#![feature(slice_as_chunks, generic_const_exprs, adt_const_params, let_chains, bigint_helper_methods)]
#![cfg_attr(test, feature(try_blocks))]

use std::fs;
Expand Down
23 changes: 18 additions & 5 deletions sapemu/src/smp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,17 @@ impl Smp {
self.psw.set(ProgramStatusWord::Carry, expanded_result & 0x100 > 0);
}

/// Set the carry flag if the addition overflows (produces a carry bit).
/// Sets all relevant flags of an 8-bit addition with carry in.
#[inline]
#[allow(unused)]
fn set_add_carry(&mut self, op1: u8, op2: u8) {
self.psw.set(ProgramStatusWord::Carry, op1.overflowing_add(op2).1);
fn set_add_carry_flags(&mut self, op1: u8, op2: u8) {
let (result, has_carry) = op1.carrying_add(op2, self.carry());
let half_carry_result = (op1 & 0x0f) + (op2 & 0x0f) + self.carry() as u8 >= 0x10;

Check failure on line 462 in sapemu/src/smp.rs

View workflow job for this annotation

GitHub Actions / clippy

casts from `bool` to `u8` can be expressed infallibly using `From`

error: casts from `bool` to `u8` can be expressed infallibly using `From` --> sapemu/src/smp.rs:462:57 | 462 | let half_carry_result = (op1 & 0x0f) + (op2 & 0x0f) + self.carry() as u8 >= 0x10; | ^^^^^^^^^^^^^^^^^^ | = help: an `as` cast can become silently lossy if the types change in the future = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless = note: `-D clippy::cast-lossless` implied by `-D clippy::pedantic` = help: to override `-D clippy::pedantic` add `#[allow(clippy::cast_lossless)]` help: use `u8::from` instead | 462 | let half_carry_result = (op1 & 0x0f) + (op2 & 0x0f) + u8::from(self.carry()) >= 0x10; | ~~~~~~~~~~~~~~~~~~~~~~

self.psw.set(ProgramStatusWord::Overflow, (op1 as i8).carrying_add(op2 as i8, self.carry()).1);
self.psw.set(ProgramStatusWord::Sign, (result as i8) < 0);
self.psw.set(ProgramStatusWord::Zero, result == 0);
self.psw.set(ProgramStatusWord::Carry, has_carry);
self.psw.set(ProgramStatusWord::HalfCarry, half_carry_result);
}

/// Set the interrupt flag.
Expand Down Expand Up @@ -505,10 +511,17 @@ impl Smp {
/// Pushes a value onto the stack. Note that this actually takes two cycles in hardware, which users must account
/// for.
fn push(&mut self, value: u8, memory: &mut Memory) {
memory.write(self.stack_top(), value);
self.memory_write(self.stack_top(), value, memory);
self.sp = self.sp.wrapping_sub(1);
}

/// Pops a value from the stack. Note that this actually takes two cycles in hardware, which users must account
/// for.
fn pop(&mut self, memory: &Memory) -> u8 {
self.sp = self.sp.wrapping_add(1);
memory.read(self.stack_top(), self.control.contains(ControlRegister::BootRomEnable))
}

/// Returns the address of the hardware stack top, i.e. the lowest stack address that is free.
#[inline]
#[must_use]
Expand Down
211 changes: 188 additions & 23 deletions sapemu/src/smp/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ fn cmpw_ya_dp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: Instructi
let result = (expanded_result & 0xffff) as u16;
cpu.psw.set(ProgramStatusWord::Sign, (result as i16) < 0);
cpu.psw.set(ProgramStatusWord::Zero, result == 0);
cpu.psw.set(ProgramStatusWord::Carry, expanded_result > 0x1_0000);
cpu.psw.set(ProgramStatusWord::Carry, expanded_result >= 0x1_0000);
MicroArchAction::Next
},
_ => unreachable!(),
Expand Down Expand Up @@ -1195,10 +1195,68 @@ fn push_y(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionIn
push::<{ Register::Y }>(cpu, memory, cycle, state)
}
fn dbnz_dp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("dbnz (dp)", cycle, cpu);
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let address = cpu.read_next_pc(memory) as u16 + cpu.direct_page_offset();
MicroArchAction::Continue(state.with_address(address))
},
2 => {
let value = cpu.read(state.address, memory);
let decremented = value.wrapping_sub(1);
MicroArchAction::Continue(state.with_operand(decremented))
},
3 => {
cpu.write(state.address, state.operand, memory);
MicroArchAction::Continue(state)
},
4 => {
let offset = cpu.read_next_pc(memory) as i8;
if state.operand == 0 {
MicroArchAction::Next
} else {
trace!(
"decremented {:04x} to non-zero value {}, taking branch to {:+x}",
state.address,
state.operand,
offset
);
MicroArchAction::Continue(state.with_relative(offset))
}
},
5 => {
// Hardware calculates branch target in this step, we do everything at the very end since memory accesses
// don't happen.
MicroArchAction::Continue(state)
},
6 => {
cpu.pc = (cpu.pc as isize + state.relative as isize) as u16;
MicroArchAction::Next
},
_ => unreachable!(),
}
}
fn ret(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("ret", cycle, cpu);
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let pc_low = cpu.pop(memory);
MicroArchAction::Continue(state.with_address(pc_low as u16))
},
2 => MicroArchAction::Continue(state),
3 => {
let pc_high = cpu.pop(memory);
MicroArchAction::Continue(state.with_address(u16::from(pc_high) << 8 | state.address))
},
4 => {
trace!("[{:04x}] return to {:04x}, stack size {}", cpu.pc, state.address, 0xff - cpu.sp);
cpu.pc = state.address;
MicroArchAction::Next
},
_ => unreachable!(),
}
}

fn bvs(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
Expand Down Expand Up @@ -1318,6 +1376,7 @@ fn cmp_x_y_indirect(
cycle: usize,
state: InstructionInternalState,
) -> MicroArchAction {
debug_instruction!("cmp (x), (y)", cycle, cpu);
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
Expand All @@ -1329,18 +1388,51 @@ fn cmp_x_y_indirect(
MicroArchAction::Continue(state.with_operand2(operand2))
},
3 => {
let result = (state.operand2 as i8).wrapping_sub(state.operand as i8);
trace!("cmp {:02x} - {:02x} = {:+}", state.operand2, state.operand, result);
let result = (state.operand as i8).wrapping_sub(state.operand2 as i8);
trace!("cmp {:02x} - {:02x} = {:+}", state.operand, state.operand2, result);
cpu.set_negative_zero(result as u8);
cpu.set_subtract_carry(state.operand2, state.operand);
cpu.set_subtract_carry(state.operand, state.operand2);
MicroArchAction::Continue(state)
},
4 => MicroArchAction::Next,
_ => unreachable!(),
}
}
fn addw_ya_dp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("addw ya, dp", cycle, cpu);

match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let address = u16::from(cpu.read_next_pc(memory)) + cpu.direct_page_offset();
MicroArchAction::Continue(state.with_address(address))
},
2 => {
let low_byte = cpu.read(state.address, memory);
MicroArchAction::Continue(state.with_address2(low_byte as u16))
},
3 => {
let high_byte = cpu.read(increment_wrap_within_page(state.address), memory);
let memory_value = (high_byte as u16) << 8 | state.address2;

let ya = cpu.ya();
let (result, has_carry) = ya.overflowing_add(memory_value);

cpu.psw.set(ProgramStatusWord::Sign, (result as i16) < 0);
cpu.psw.set(ProgramStatusWord::Zero, result == 0);
cpu.psw.set(ProgramStatusWord::Carry, has_carry);
cpu.psw.set(ProgramStatusWord::Overflow, (ya as i16).overflowing_add(memory_value as i16).1);

let half_carry_result = (ya & 0x0fff) + (memory_value & 0x0fff) >= 0x1000;
cpu.psw.set(ProgramStatusWord::HalfCarry, half_carry_result);

cpu.y = ((result >> 8) & 0xff) as u8;
cpu.a = (result & 0xff) as u8;
MicroArchAction::Continue(state)
},
4 => MicroArchAction::Next,
_ => unreachable!(),
}
}
fn ror_dp_x(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
debug_instruction!("ror (dp+x)", cycle, cpu);
Expand All @@ -1353,14 +1445,37 @@ fn ror_a(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInt
})
}
fn mov_a_x(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("mov a, x", cycle, cpu);
mov_register_register::<{ Register::A }, { Register::X }>(cpu, memory, cycle, state)
}
fn cmp_y_dp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
debug_instruction!("cmp y, dp", cycle, cpu);
cmp_register_dp::<{ Register::Y }>(cpu, memory, cycle, state)
}
fn reti(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("reti", cycle, cpu);
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
cpu.psw = ProgramStatusWord(cpu.pop(memory));
MicroArchAction::Continue(state)
},
2 => {
let pc_low = cpu.pop(memory);
MicroArchAction::Continue(state.with_address(pc_low as u16))
},
3 => MicroArchAction::Continue(state),
4 => {
let pc_high = cpu.pop(memory);
MicroArchAction::Continue(state.with_address(u16::from(pc_high) << 8 | state.address))
},
5 => {
trace!("[{:04x}] return to {:04x}, stack size {}", cpu.pc, state.address, 0xff - cpu.sp);
cpu.pc = state.address;
MicroArchAction::Next
},
_ => unreachable!(),
}
}

fn setc(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
Expand Down Expand Up @@ -1388,10 +1503,48 @@ fn bbs_4(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInt
branch_on_bit::<4, true>(cpu, memory, cycle, state)
}
fn adc_a_dp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("adc a, dp", cycle, cpu);

match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let address = u16::from(cpu.read_next_pc(memory)) + cpu.direct_page_offset();
MicroArchAction::Continue(state.with_address(address))
},
2 => {
let value = cpu.read(state.address, memory);
trace!("{} + {} + {}", cpu.a, value, cpu.carry());
let result = cpu.a.wrapping_add(value).wrapping_add(cpu.carry() as u8);
cpu.set_add_carry_flags(cpu.a, value);
cpu.a = result;
MicroArchAction::Next
},
_ => unreachable!(),
}
}
fn adc_a_addr(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("adc a, addr", cycle, cpu);
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let address_low = cpu.read_next_pc(memory) as u16;
MicroArchAction::Continue(state.with_address(address_low))
},
2 => {
let address_high = cpu.read_next_pc(memory) as u16;
let address = address_high << 8 | state.address;
MicroArchAction::Continue(state.with_address(address))
},
3 => {
let value = cpu.read(state.address, memory);
trace!("{} + {} + {}", cpu.a, value, cpu.carry());
let result = cpu.a.wrapping_add(value).wrapping_add(cpu.carry() as u8);
cpu.set_add_carry_flags(cpu.a, value);
cpu.a = result;
MicroArchAction::Next
},
_ => unreachable!(),
}
}
fn adc_a_x_indirect(
cpu: &mut Smp,
Expand Down Expand Up @@ -1512,7 +1665,8 @@ fn dec_a(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInt
dec_register::<{ Register::A }>(cpu, cycle)
}
fn mov_x_sp(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("mov x, sp", cycle, cpu);
mov_register_register::<{ Register::X }, { Register::SP }>(cpu, memory, cycle, state)
}
fn div(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
Expand Down Expand Up @@ -1957,16 +2111,7 @@ fn dec_y(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInt
}
fn mov_a_y(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
debug_instruction!("mov a, y", cycle, cpu);

match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
cpu.a = cpu.y;
cpu.set_negative_zero(cpu.a);
MicroArchAction::Next
},
_ => unreachable!(),
}
mov_register_register::<{ Register::A }, { Register::Y }>(cpu, memory, cycle, state)
}
fn cbne_dp_x(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
Expand Down Expand Up @@ -2121,7 +2266,8 @@ fn inc_y(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInt
inc_register::<{ Register::Y }>(cpu, cycle)
}
fn mov_y_a(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
debug_instruction!("mov y, a", cycle, cpu);
mov_register_register::<{ Register::Y }, { Register::A }>(cpu, memory, cycle, state)
}
fn dbnz_y(cpu: &mut Smp, memory: &mut Memory, cycle: usize, state: InstructionInternalState) -> MicroArchAction {
todo!()
Expand Down Expand Up @@ -3113,3 +3259,22 @@ fn inc_dec_word(
_ => unreachable!(),
}
}

#[inline]
fn mov_register_register<const TARGET: Register, const SOURCE: Register>(
cpu: &mut Smp,
memory: &Memory,
cycle: usize,
state: InstructionInternalState,
) -> MicroArchAction {
match cycle {
0 => MicroArchAction::Continue(InstructionInternalState::default()),
1 => {
let value = cpu.register_read::<SOURCE>();
cpu.register_write::<TARGET>(value);
cpu.set_negative_zero(value);
MicroArchAction::Next
},
_ => unreachable!(),
}
}
2 changes: 1 addition & 1 deletion sapemu/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ impl TryFrom<(Option<u16>, Option<u8>, String)> for Cycle {

/// Tests that are not run due to issues with `SingleStepTests`' disregard of hardware properties.
/// See <https://github.com/SingleStepTests/spc700/issues/1>.
const IGNORED_TESTS: &[&str] = &["09 01A8", "39 0295", "59 0146", "3A 0355", "47 02DD"];
const IGNORED_TESTS: &[&str] = &["09 01A8", "39 0295", "59 0146", "3A 0355", "47 02DD", "6E 0344"];

/// rstest limitation: we have to generate all values in advance, unfortunately.
/// python: `for i in range(0xff+1): print(hex(i), end=', ')`
Expand Down

0 comments on commit 74d82c5

Please sign in to comment.