Shift Instructions
Implementing the NES 6502 in Rust has been quiet the adventure. I was trying to keep the logic simple, avoid magic numbers and make it easy to read. Then I got to the shift instructions...
By no means am I an expert in bitwise logic, in fact I actively avoid it when solving leetcode or any other problems. Luckily it does not come up a whole lot in day to day life for me. But I knew implementing the 6502, this was only going to be a matter of time.
ASL - Arithmetic Shift Left
- ASL shifts either the accumulator or the address memory location 1 bit to the left.
- Bit 0 always being set to 0 and the input bit 7 being stored in the carry flag.
- ASL either shifts the accumulator left 1 bit or is a read/modify/write instruction that affects only memory.
INITIAL STATE
We have the value 172 (Binary 1010 1100).
[ Carry ] [ Bit 7 ............ Bit 0 ]
+---------+ +---------------------------+
| ? | | 1 0 1 0 1 1 0 0 |
+---------+ +---------------------------+
^
|
This '1' is about to fall off!
THE SHIFT (<<<<)
Everything moves one spot to the left. A new '0' is always inserted at the right (Bit 0).
/---- The '1' is pushed out...
|
v
+---------+ +---------------------------+
| 1 | <------- | 0 1 0 1 1 0 0 0 | <-- [0]
+---------+ +---------------------------+ New 0
^ enters
|
CAPTURED!
WHY WE NEED CARRY:
- Data Preservation: That '1' isn't lost; it's saved in the Status Register.
- Multi-Byte Math: If this was the lower half of a 16-bit number, the next instruction (ROL) would pick this '1' up and move it into the upper byte.
Where I Went Wrong
I wanted a helper function called shift_left (how imaginative right?)
fn shift_left(cpu: &mut CPU, data: u8) -> u8 {
// Check if the 7th bit is non-zero
if data & (1 << 7) == 1 {
cpu.registers.status.insert(Status::CARRY);
} else {
cpu.registers.status.remove(Status::CARRY);
}
let result = data << 1;
cpu.update_nz_flags(result);
result
}
What I imagined would happen is this would check if the 7th bit is 1, if so set the carry flag, otherwise clear it.
Right? WRONG
1 << 7equals 128 (1000 0000).- ANDing
datawith128is either0or128. - The result will never be
1!
The Fix
fn shift_left(cpu: &mut CPU, data: u8) -> u8 {
// Check if the 7th bit is non-zero
if (data & (1 << 7)) != 0 {
cpu.registers.status.insert(Status::CARRY);
} else {
cpu.registers.status.remove(Status::CARRY);
}
let result = data << 1;
cpu.update_nz_flags(result);
result
}
Read-Modify-Write
Up until this point I was fine using:
pub(crate) enum AccessMode {
Read, // LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT
Write, // STA, STX, STY, INC, DEC
Until I realized that these instructions technically do both a read and a write.
(it's a little more complex than that but I'll get into that later).
I could have made a ReadModifyWrite but since this is the only type of instruction that does both a read and a write, I decided to keep it simple.
pub fn asl(cpu: &mut CPU, bus: &mut dyn Bus, mode: &AddressingMode) {
if *mode == AddressingMode::Accumulator {
cpu.read(bus, cpu.registers.program_counter);
let value = cpu.registers.accumulator;
cpu.registers.accumulator = shift_left(cpu, value);
} else {
let addr = cpu.get_operand_address(mode, bus, AccessMode::Write);
let value = cpu.read(bus, addr);
cpu.write(bus, addr, value);
let new_val = shift_left(cpu, value);
cpu.write(bus, addr, new_val);
}
}
OpCodes
