65816 Processor¶
Resources¶
This looks like an excellent resource:
http://www.6502.org/tutorials/65c816opcodes.html
Test suites for the 65816 / 6502¶
https://github.com/gilyon/snes-tests
- this test does assume snes hardware. So, moving on from that..
https://forums.nesdev.org/viewtopic.php?t=24940
https://github.com/SingleStepTests/ProcessorTests
This is one John Brooks pointed out, it's got some sample code but also test traces: https://emudev.de/q00-snes/65816-the-cpu/
Discussion¶
Evaluating expanding the current template-based approach, with five templates total for the 816:
- emulation mode
- native + 8 A + 8 Index
- native + 16 A + 8 Index
- native + 8 A + 16 Index
- native + 16 A + 16 Index
m_16 and x_16 are template traits.
So there are a number of areas we need to deal with 16-bit values in addition to 8-bit values.
addressing mode helper functions opcode stanzas
for example:
inline byte_t get_operand_immediate(cpu_state *cpu) {
byte_t N = cpu->read_byte_from_pc();
TRACE(cpu->trace_entry.operand = N;)
return N;
}
This is used by all these opcodes: lda, ldx, ldy, adc, sbc, bit, and, cmp, cpx, cpy, etc. (14 in 65c02).
case OP_LDA_IMM: /* LDA Immediate */
{
_A(cpu) = get_operand_immediate(cpu);
set_n_z_flags(cpu, _A(cpu));
}
break;
get_operand_immediate doesn't know whether the destination is A or X/Y or what the result is. What if:
inline byte_t get_operand_immediate(cpu_state *cpu) {
byte_t N = cpu->read_byte_from_pc();
TRACE(cpu->trace_entry.operand = N;)
return N;
}
inline word_t get_operand_immediate_16(cpu_state *cpu) {
word_t N = cpu->read_word_from_pc();
TRACE(cpu->trace_entry.operand = N;)
return N;
}
and then:
case OP_LDA_IMM: /* LDA Immediate */
{
if (constexpr (m_16==true))
_A(cpu) = get_operand_immediate_16(cpu);
else
_A(cpu) = get_operand_immediate(cpu);
set_n_z_flags(cpu, _A(cpu));
}
break;
Then get_operand doesn't have to deduce register size. This implies for most of those address mode funcs, we'll have 8-and-16 bit versions, and then this kind of constexpr logic in the opcodes. This seems relatively readable. it reads like if then else code but is handled at compile time.
There were a number there that were the same, but here is a new pattern:
case OP_SBC_IMM: /* SBC Immediate */
{
if constexpr (CPUTraits::m_16) {
word_t N = get_operand_immediate_16(cpu);
subtract_and_set_flags(cpu, N);
} else {
byte_t N = get_operand_immediate(cpu);
subtract_and_set_flags(cpu, N);
}
}
break;
this is where we call a utility function with an intermediate value, instead of a direct assignment or operator like the above. Now, subtract_and_set_flags could have a special 16 bit version, or, it can be templated and check constexpr m_16 internally. maybe it can't, because subtract_core is used for cmp, cpx, cpy also and these need to check x_16 instead. So maybe we do need _16 version.
ok at this point except for cmp, cpx, cpy, and 16-bit decimal adc/sbc, I have now implemented 16-bit-capable versions of all the immediate mode instructions. So far, this isn't actually that bad! I might get excited about this, ha ha.
Oct 1, 2025¶
So I had never implemented the Emulation Mode bit. Right now, I just set this up as its own flag. Apparently KEGS makes this bit 9 of the P register, however, the only way to manipulate it is with XCE. You can't push or pull it or anything. There are a few opcodes that work slightly differently if it's set; otherwise, we only want it in the trace record. [ ] store 'e' bit into trace->flags. [x] store m and x register width flags into trace->flags. [ ] we don't need to display 'data' on immediate operand. it is the data.
CPU mask flags need to be more like - at least. So if we say an instruction is a 65c02, then the 65c02 AND the 65816 have it. if it's 6502, they all have it.
so when we're in emulation mode (and we start on a reset in emulation mode) the m and x bits should be 1 (8-bit), and they are forced that way. Currently I initialize registers in cpu_struct. That's not gonna work.
Debugging¶
instead of storing -current- m / x bits in trace->flags, maybe we need to store the operand size. these are different based on bits: immediate mode (1 byte or 2 bytes of operand) data path: is either 1 byte or 2 bytes depending on register involved. So we need 3 bits of flags for this - 2 bits for operand length (0-3) and 1 bit for data width. And the various trace operations need to set it. They default to 8bit and 0 bytes (flags=0) so we can only set when needed. I want a bitfield for this.
So really, the only opcodes where the number of bytes differs based on M/X is immediate mode. So maybe only set the size flag for immediate mode.
so if it's JUST immediate mode, maybe we even just use one bit (16-bit imm) and check that bit when the address mode is IMM.
Debug Line Formatter¶
We're going to want different formatters for: 6502 / 65c02, and 65816. So that implies a trace_entry base class, two subclasses, and some differentiation. We also want a "line_format" buffer concept to make it easier to emit the pieces of trace debug output. I suppose I could just do a big "if" statement for now. So let's start with the line_format.
line_format - char(255) line_format->pos(int cursor) - set "cursor" to specified position. int line_format->pos() get cursor line_format->put(byte_t) line_format->put(word_t) line_format->put(uint32_t) line_format->put(char *) line_format->put(char)
ok had to fix this:
tb->a = cpu->a;
tb->x = cpu->x;
tb->y = cpu->y;
originally I was only copying in the low byte! This is probably actually faster or no speed difference.
Looking at TXS now. in Emulation mode, S is always 01XX. in any other mode, S is 16-bits. If x is 8 bit, s becomes 00XX.
Oct 3, 2025¶
Currently, cpu_struct has the methods for read_byte, read_byte_from_pc, the word variants, etc. The 816 needs its own versions of these, because it needs to include the D(ata) and P(rogram) bank registers and generate a 24-bit address to the MMU.
Currently, the cpu core does a call to say read_byte, which then does a call to the MMU. read_byte is in the header and defined as inline so in theory it should be optimized. But, I think these methods really need to be in BaseCPU. BaseCPU can still access MMU by cpu_struct->mmu. Though I am also thinking, should I merge cpu_struct into BaseCPU? it's kind of weird having data in a separate class from the methods that work on it. Howeve, cpu_struct does act as a "neutral middleman". It's how we can change the 65816 cores on the fly. Let's leave alone for now. However, should keep working on removing stuff from cpu_struct that shouldn't be there.
The main chunk that I think about is all the clocking stuff. There are 15+ variables in cpu_struct related to clock tracking, what incr_cycles does, etc. I feel like this maybe should be modularized. It's possible the compiler is already not inlining it since there are several hundred repetitions in BaseCPU meaning we would not harm performance by doing so. Something to think about..
Now, for accessing memory!
zeropage
Add:
bank 0
D
zero page address
to get the effective address.
absolute
D
absolute address
to get the effective address.
program fetch
P
absolute address
to get the effective address.
For example: STA $2100
will do the second of those. This calls:
case OP_STA_ABS: /* STA Absolute */
{
store_operand_absolute(cpu, cpu->a_lo);
}
break;
inline void store_operand_absolute(cpu_state *cpu, byte_t N) {
absaddr_t addr = get_operand_address_absolute(cpu);
write_byte(cpu,addr, N);
TRACE(cpu->trace_entry.data = N;)
}
since this is not long mode (STL) store_operand_absolute should do the D bank thing. It get_operand_address_absolute (the 16-bit partial address). Then if it's an 816 and in native mode, it needs to generate the 24-bit address. For the trace to be correct, it sort of wants to be here. But then we have to have the same code everywhere we use it. Does it makes sense to have a write_byte_absolute that takes a 16-bit address and fills it out? then we'd have a write_byte_direct also for zeropage. and a write_byte_long.
push_byte currently calls write_byte, which creates conflicting eaddr - and we're doubling-up on setting eaddr in a number of places. But that's the only way we don't calculate eaddr twice with the data bank. So we'll have to redo placement of the trace calls.
ok I like the idea of re-defining word_t to be something like
union {
struct {
uint8_t lo;
uint8_t hi;
}
uint16_t w;
}
I had actually done that with the addr_t type. however, Claude suggests it might be more efficient to use bit shifts and OR to assemble these things. So, ok. I've now refactored read and write byte and word, as well as fetch from PC routines. I think the last chunk will be the stack functions push/pop. For the trace function, and as an aid to development of all the complex shadow reads/writes, maybe the trace record should optionally have an array of address/data times potential number of cycles. i.e., we keep a log of all of them. That would make the records quite a lot larger I guess, and of limited utility in production, but for dev it would be great. We still need to record the 'real' or 'final' such access. I guess they typically will be the last access. Still need dp and long versions of these funcs.
Also considering that the rules around wrapping are somewhat complex and maybe should be buried into functions that also have an "index" or "offset" argument. E.g.
read_byte_direct (uint8_t address, uint16_t offset)
read_byte_absolute (uint16_t address, uint16_t offset)
The offset might be 8-bit, it might be 16-bit. Could be X, Y, etc. The Direct page rules are particularly complex. There are two classes of "wrapping" behavior: incrementing an address for the hi byte of a word access; offset of base address by an X or Y index. And these can be combined. Let's look at one of the examples, lda abs,x. My notation here differs a little from the 6502.org doc.
If a portion of a value is encased in () then this says "this will wrap on this boundary". $DDHHLL is "Take data bank, combine with 16-bit address, add X without any kind of wrapping." Well, I suppose technically
- $BB = data bank
- $PP = program bank
- $dd = direct page low byte
- $DD = direct page high byte
- $LL = operand low byte
- $HH = operand high byte
For long instructions, * $LL = operand low byte * $MM = operand mid byte * $HH = operand high byte
Indirect modes have two steps - calculation of the base effective address, then calculation of the effective address +1 (and maybe +2) for lo/mid/hi parts of data.
| Address Mode | Emu | Emu Hi | Nat | Nat Hi | Nat Long |
|---|---|---|---|---|---|
| ABS P | $PPHHLL | $PPHHLL | |||
| ABS D | $BBHHLL | $BBHHLL | $BBHHLL+1 | ||
| (ABS,x) | $PP(HHLL+X) | $PP(HHLL+X) | $PP(HHLL+X+1) | JMP/JSR only | |
| ABS, xy | $BBHHLL+X | $BBHHLL+X | $BBHHLL+X+1 | ||
| (ABS), [ABS] *4 address | $00(HHLL) | $00(HHLL) | $00(HHLL+1) | JMP or JML | |
| (ABS) data | $PPEEEE | $PPEEEE+1 | $PPEEEE | $PPEEEE+1 | $PPEEEE+2 (JMP only) |
| [ABS] data | $EEEEEE | $EEEEEE+1 | $EEEEEE | $EEEEEE+1 | $EEEEEE+2 (JML only) |
| ABSL, x | $HHMMLL+X | $HHMMLL+X | $HHMMLL+X+1 | ||
| ABSL | $HHMMLL | $HHMMLL | $HHMMLL+1 | ||
| (DIRECT,x) Add | $00DD(dd+LL+X) | $00DD(dd+LL+X+1) | $00(DDdd+LL+X) | $00(DDdd+LL+X+1) | |
| (DIRECT,x) Data | $BBEEEE | $BBEEEE | $BBEEEE+1 | ||
| DIRECT, xy | $00DD(dd+LL+X) | $00(DDdd+LL+X) | $00(DDdd+LL+X+1) | ||
| (DIRECT),y Add | $00DD(dd+LL) | $00DD(dd+LL+1) | $00(DDdd+LL) | $00(DDdd+LL+1) | |
| (DIRECT),y Data | $BBEEEE+Y | $BBEEEE+y | $BBEEEE+y+1 | ||
| [DIRECT],y Add | $00(DDdd+LL) | $00(DDdd+LL+1) | $00(DDdd+LL+2) | ||
| [DIRECT],y Data | $EEEEEE+Y | $EEEEEE+y | $EEEEEE+y+1 | ||
| [DIRECT] Add | $00(DDdd+LL) | $00(DDdd+LL+1) | $00(DDdd+LL+2) | ||
| [DIRECT] Data | $EEEEEE | $EEEEEE | $EEEEEE+1 | ||
| (DIRECT) Add *1 | $00DD(dd+LL) | $00DD(dd+LL+1) | $00(DDdd+LL) | $00(DDdd+LL+1) | |
| (DIRECT) Data | $BBEEEE | $BBEEEE | $BBEEEE+1 | ||
| DIRECT *2 | $00DD(dd+LL) | $00(DDdd+LL) | $00(DDdd+LL+1) | ||
| STACK d,s *3 | $00SS(ss+LL) | $00(SSss+LL) | $00(SSss+LL+1) | ||
| STACK (d,s),y Add | $00SS(ss+LL) | $00(SSss+LL) | $00(SSss+LL+1) | ||
| STACK (d,s),y Data | $BB(EEEE+y) | $BBEEEE+y | $BBEEEE+y+1 |
- if low byte of DDdd is non-zero, then even in emu mode the lookup is performed as it is in Native mode, with no page wrapping. If it's zero, then it performs as it would on a 6502 (with page wrapping).
- an extra cycle is required whenever dd is non-zero (i.e. DDdd not page-aligned).
- LL is a signed 8-bit from -128 to 127.
- on an nmos 6502, the behavior is actually $PPHH(LL) and $PPHH(LL+1), which is a bug (unintentional).
These modes have registers used to calculate the effective address.
For some of these calculations, a wrap takes an extra cpu cycle.
Questions: 1. does [DIRECT] work in Emulation mode, if so, what bank is used in eaddr? 1. confirm that in E mode, if PP is non-zero, the effective address will be PPPCPC. (i.e. that K and B apply even in E mode)
Width of Address Mode Calculation Register¶
| Mode | Emulation | Native |
|---|---|---|
| Direct | 8 | 16 |
| Absolute | 16 | 24 |
Conclusions¶
I am confusing the concepts of reading data, effective address generation, and might have been before.
Each address mode should look something like this. Currently I have the read/write inside the address mode handler. That's wrong. We move it out.
OP_LDA_ABS
read_abs(cpu, _A(cpu))
set_n_z_flags(cpu, _A(cpu));
OP_STA_ABS
write_abs(cpu, _A(cpu))
OP_LDA_DIRECT_X
read_direct_x(cpu, _A(cpu))
uint8_t fetch_pc(cpu_state *cpu)
uint8_t b = mmu->read(_PC());
cpu->pc++;
cpu->incr_cycles();
return b;
template<typename T>
void read_abs(cpu_state *cpu, T ®)
uint32_t eaddr = fetch_pc(cpu);
eaddr |= fetch_pc(cpu) << 8;
if constexpr (is_65816)
eaddr |= (cpu->db << 16)
if constexpr sizeof(T) == 1
reg = mmu->read(eaddr);
TRACE(tb->eaddr = eaddr; tb->data = reg; tb->datawidth = 0;)
if constexpr sizeof(T) == 2
uint16_t tmp = mmu->read(eaddr);
tmp |= (mmu->read(cpu, eaddr+1) << 8)
TRACE(tb->eaddr = eaddr; tb->data = tmp; tb->datawidth = 1;)
eaddr_abs() will return either a 24-bit address (if is_65816) or a 16-bit address (otherwise) in a 32-bit word, ready to pass into mmu->read(). _A = read() will automatically do either an 8-bit read or a 16-bit read depending on A register width (m16); that will use the appropriate set_n_z_flags based on register width (templated by type of 'value' in that call);
That's fine for LDA ABS. But for say DIRECT,x, the increment for hi byte of 16-bit read, has to be done differently: we have to either page wrap or bank wrap. For each byte of a 16-bit transfer, and for different address modes (DIRECT, stack, etc) I need to have different rules for calculating the address.
OK so second iteration: read_abs takes a reference to the register we're reading into; we fetch two bytes of ABS address from the program counter (which adds K if necessary); we take that 16-bits, and if an 816, add the data bank; if the width of the destination is 1 byte, we do a single read, store it in the register (by reference); if width of destination is 2 bytes, we do two reads, constructing the 16-bit value according to the address construction rules of ABS mode.
The code produced from the one template, will vary based on: 816 or not; (emulation mode does not matter for address construction); size of target (based on m_16 1 or 0.). There could be 4 or 5 variations. But all generated automagically based on the traits.
Let's try implementing this. OK, I had to add incr_cycles which I forgot, but, it's working. passes 6502 and cycletest.
Phantom Read handling.¶
I am thinking of having two routines: PhantomRead (maybe also PhantomWrite) and PhantomReadIgn (and maybe also PhantomWriteIgn).
PhantomRead just executes a bus read to the address specified. PhantomReadIgn is based on a Traits check - and will either execute a bus read or not depending on the setting of the flag. Some of the phantom reads can have no effect on I/O in an Apple II because they do things like (for instance) re-read the last operand byte. So there's no need to do the work. However, if someone later wants to use this core and have these work exactly like in the real thing, they can instantiate with that flag set and the PhantomReadIgn will execute.
Commonality of SBC and CMP¶
Can SBC and CMP be shared now by performing the subtraction into a tmp reference, instead of performing it into _A ?
yes. I think the subtract routine is the same. But a CMP should store the subtract result in a temp register; SBC stores it in _A(cpu).
Stack register transfers¶
There are: TXS, TSX; TCS, TSC
S is always 16 bits wide. | Mode | Effect | |-|-| | Emul | SH = 0x01, SL = RL | | width 8 | SH = 0x00, SL = RL | | width 16 | S = R |
When x_16 is 8-bit, high byte of registers is forced to zero? however NOT same with accumulator since you can swap the accumulator bytes.
MVN / MVP handling¶
These are a sort of complex, compound instruction. It's a copy loop implemented in the hardware - but it can be interrupted (every 7 cycles); the state is maintained in the A (counter), X (source address), and Y (destination address) registers. In order for all of our other stuff to work, we will have to do this instruction like so: we will need a mode flag "mvn in progress", "mvp in progress", or "no move in progress". Then each byte copied acts like one instruction executed. For trace record, we can log every iteration of the loop, along with the register values, which could be useful.. or only log it once. After each 7 cycle iteration, we exit execute_next. So we can do frame updates etc just the way we do now, in the middle of a mvn/mvp.
Tracing¶
Tracing can now be enabled/disabled through use of the TraceTraits traits. Either TraceEnabled or TraceDisabled can be passed to CPU template as 2nd argument. 6502, trace enabled gets us ~ 370MHz. Trace disabled gets us ~ 430MHz. So that's a 15% performance hit to have tracing on. So for MAXIMUM OVERDRIVE you can turn trace off. Maybe we do that in Ludicrous Speed.
I will also add a flag that will enable full bus tracing per cycle - read / write info kept for every cycle.
Clocking¶
I should figure out a way to inject the clocking into the CPU Core. Right now it's just done with some if statements and a hard call to video scanner, which creates a dependency there.
Address Modes¶
This is the list of address modes from the 65816 data sheet, and checked off if we have completed.
[x] 1a. Absolute a
[x] 1b. Absolute a (jmp)
[x] 1c. Absolute a (jsr)
[x] 1d. Absolute (r-m-w) a
[x] 2a. Absolute indexed indirect (a,x) (jmp)
[x] 2b. absolute indexed indirect (a,x) (jsr)
[x] 3a. Absolute indirect [a] JML [a]
[x] 3b. Absolute indirect (a) JMP (a) TODO: normalize to new methods
[x] 4a. Absolute long al
[x] 4b. Absolute long AL - JML
[x] 4c. Absolute long AL - JSL
[x] 5 . Absolute Long, X al,x
[x] 6a. Absolute, X
[x] 6b. Absolute, X (rmw)
[x] 7 . Absolute, Y
[x] 8. Accumulator (a)
[x] 9a. Block Move Negative
[x] 9b. Block Move Positive
[x] 10a. Direct d
[x] 10b. Direct (rmw) d
[x] 11. Direct indexed indirect (d,x)
[x] 12. Direct Indirect (d)
[x] 13. Direct Indirect Indexed (d),y
[x] 14. Direct Indirect Indexed Long [d],y
[x] 15. direct Indirect Long [d]
[x] 16a. Direct, X d,x
[x] 16b. Direct, X (rmw) d,x
[x] 17. Direct, Y d,y
[x] 18. Immediate
[x] 19a. Implied i
[x] 19b. Implied i (xba)
[ ] 19c. Stop the clock (stp)
[ ] 19d. wait for interrupt (WAI)
[x] 20. Relative r
[x] 21. Relative long rl
[ ] 22a. Stack s (abort, irq, nmi, res)
[x] 22b. Stack s (pla, plb, pld, ..)
[x] 22c. Stack s pha, phb, php...
[x] 22d. Stack s pea
[x] 22e. Stack s pei
[x] 22f. Stack s per
[x] 22g. Stack s rti
[x] 22h. Stack s rts
[x] 22i. Stack s rtl
[x] 22j. Stack s brk, cop
[x] 23. Stack Relative d,s
[x] 24. Stack Relative Indirect Indexed (d,s),y
Commonality of address mode handling¶
There is a ton of common code, especially around:
read byte or word of data write byte or word of data
for example:
if constexpr (is_byte<T>) {
cpu->mmu->write(eaddr, reg);
cpu->incr_cycles();
}
if constexpr (is_word<T>) {
cpu->mmu->write(eaddr, word_lo(reg));
cpu->incr_cycles();
cpu->mmu->write(eaddr+1, word_hi(reg));
cpu->incr_cycles();
}
this pattern is used all over the place. Four times, to be precise. So let's factor that out into:
template<typename T>
inline void write_data(cpu_state *cpu, uint32_t eaddr, T ®, ) {
if constexpr (is_byte<T>) {
cpu->mmu->write(eaddr, reg);
cpu->incr_cycles();
}
if constexpr (is_word<T>) {
cpu->mmu->write(eaddr, word_lo(reg));
cpu->incr_cycles();
cpu->mmu->write(eaddr+1, word_hi(reg));
cpu->incr_cycles();
}
}
and the same for read.. and another for write_tada <- data in reverse chrono order
Bah I introduced a regression into Apple II Audit. It's the same one from before I think, zellyn.com/A2AUDIT/V0#E0007
it's rmw_abs_x , inc. if constexpr (CPUTraits::has_65c02_ops || (CPUTraits::has_65816_ops && !CPUTraits::e_mode)) phantom_read(cpu, eaddr+1); I suspect eaddr+1 is wrong.. why would it be +1 in emul mode. it won't be, because AA+X won't have been incremented to AA+X+1 in any 8-bit mode.
816+emul mode: write nmos: write 65c02: read 816non-emu and 8-bit: read 816non-emu and 16-bit: read+1
if constexpr (CPUTraits::has_65816_ops) {
if constexpr CPUTraits::e_mode
phantom_write(cpu, eaddr, reg);
else {
if constexpr (sizeof(reg) == 1)
phantom_read(cpu, eaddr);
else phantom_read(cpu, eaddr+1);
}
} else if constexpr (CPUTraits::has_65c02_ops) {
phantom_read(cpu, eaddr);
} else { // nmos
phantom_write(cpu, eaddr, reg);
}
ok, fixed! now REALLY fixed.
direct indirect modes
the next chunk of modes with similarities is 11-15. they all grab either a 16-bit or a 24-bit address from a direct page address and the 1 or 2 bytes past it - and use that as an address to read/write 1 or 2 bytes.
Method Nomenclature¶
let's call methods:
read; write; rmw; prefixes indicating what type of address mode. let's call actual reading/writing from the memory bus, to be bus_read, bus_write; and let's call snippets that combine a bus_read/write with incr_cycles to be: mem_read; mem_write
so we'll go through here and do a refactor to combine a
inline uint8_t bus_read(cpu_state *cpu, uint32_t addr) {
uint8_t data = cpu->mmu->read(addr);
cpu->incr_cycles();
return data;
}
inline void bus_write(cpu_state *cpu, uint32_t addr, uint8_t data) {
cpu->mmu->write(addr, data);
cpu->incr_cycles();
}
Oct 9, 2025¶
oopsie, the stack routines are reading and writing to the program bank (via write_byte). That's just not right. ha ha. read_byte is also used by jml [absolute] let's have the stack routines use bus_read and bus_write. read_byte and read_word convert an abs address to a long address, which I now do differently for the most part. ok those are good..
ok, test0004 fails - expected results are 0000 / 10 / 78 - with 8 bit X. However, I think what's happening is on the X width switch we're not setting the high bytes of X/Y to 0.
16-bit X = 1234, switch to 8-bit, X = 34, switch to 16-bit, is X now:, 0034 or 1234?: 0034.
"So TAX with 8 bit accumulator and 16 bit index will set X to 1234." No, accumulator has B always. Yes, moving into emulation mode truncates X, Y and SP. so with A 8bit X 16 bit, what does TXA do? TXA will set A and B 8 A | 8 X | ?? 8 A | 16 X | B and A -> X 16 A | 8 X | ?? 16 A | 16 X | B and A -> X"
ohoh. This says "the size of the destination register determines whether these instructions are 8-bit operations or 16-bit operations". when the destination is 8 bits, 8 bits are transferred. When the destination is 16 bits, 16 bits are transferred."
ok to screw it, we do: OP_TYA: if constexpr (sizeof(_A) == 2) cpu->a = cpu->y else cpu->a_lo = cpu->y_lo
OP_TAY: if constexpr (sizeof(_Y) == 2) cpu->y = cpu->a else cpu->y_lo = cpu->a_lo
ok these are in .. I didn't break 6502.. I hope these work, ha ha.
test0005 - missing opcode 63. ah, ADC $32,S. Of course that doesn't work. I didn't make it yet.
test0024
this is whack:
Test 0024: adc ($EF,x) Input: A=$1112 X=$0010 Y=$5678 P=$21 E=1 DBR=$7f D=$0100 ($0001ff)=$34 ($000100)=$12 ($7f1234)=$ed Expected output: A=$1100 X=$0010 Y=$0078 P=$33 E=1 ($7f1234)=$ed
the setup: 1ff: 34 100: 12 7f/1234: ED D = 0100; X=10; A=12; B = 0x7F ($EF,X) -> 100 + EF + 10 = 1FF, 100 so in E mode we are supposed to: wrap the hi address inside whatever 1 page it is, to get the two indirect bytes. then add data bank.
ok.. read_data_direct when reading a word might need special handling in Emul mode. ok I fixed, but, it's kind of gnarly. I wonder where the fix should really go..
On test0028, the stack is on page 0, instead of page 1. The tracer was not displaying correctly, it was forcing display of stackhi as 0x01. Displaying the correct 16-bit sp, I now see there are problems where on reset sp hi is not 0x01 as it's supposed to be. adding in some other reset condition register sets.
after test E5 we're jumping into code that is doing test $243. I think that's because I'm not loading the rom correctly. the rom is mapped using "lorom", so each 32K of the rom file is put at 8000-FFFF in a bank. So there are 8 banks represented here, not 4.
I had to update get_operand_relative to use fetch_pc(). deprecated read_byte_from_pc which don't work right somehow. now getting to test 0100!!
and that appears to be wanting to test BRK, so uh I'm gonna have to take out my "stop emulation on brk".
Wow we cleared $0104 (brl) and have proceeded straight to $026C. which craps out and runs back to $0001 somehow.
test 0274 - this is demonstrating "UNSAFE" stack operation on 816 with JSL instruction. Stack starts at 100 and the JSL pushes to 100, FF, and FE - followed by the SP being reset at end of instruction to 1FD.
Kent's discussion of this: "Direct, Stack, and things like JMP (Abs), JMP (abs,x) and JSR (Abs,x) stay within the bank (so JSR (Abs,x) does all reads to the current K bank, regardless of Abs and X--the add wraps, and the reading of the address wraps and stays within the bank). Instruction fetch always stays within the bank. Other data accesses cross banks using 24-bit math, including Abs,X, and if reading/writing 16-bits, that will also cross banks. Stack accesses come in two types which KEGS calls SAFE and UNSAFE. Old 6502-based instructions are always safe, and in emulation mode always stay within $01xx (so JSR with S=$00 will write the address to $100 and $1ff). Newer instructions are UNSAFE, and push/pull is done ignoring stack wrapping in emulation mode, and so they can touch $ff and $200, and at the end of the instruction, the stack pointer is forced to $01xx. The stack is always bank [edited] 0, even unsafe operations cannot leave bank 0. (edited) "
The Data Sheet discusses this too: "7.1 Stack Addressing When in the Native mode, the Stack may use memory locations 000000 to 00FFFF. The effective address of Stack, Stack Relative, and Stack Relative Indirect Indexed addressing modes will always be within this range. In the Emulation mode, the Stack address range is 000100 to 0001FF. The following opcodes and addressing modes will increment or decrement beyond this range when accessing two or three bytes: JSL, JSR (a,x), PEA, PEI, PER, PHD, PLD, RTL"
That comports with the snes-test. Unsure whether the silicon does it like that, but the net effect is what we want. So a list of "new" instructions that touch the stack -
COP - wdc says NO * JSL * RTL * PER * PEI * PEA * JSR (abs,x) * PHD * PLD
At the end of the instruction, in E mode, S_HI forced back to 0x01. so weirdo. So we want: push_byte_new push_word_new pop_byte_new pop_word_new these are only called from 65816; can we put in a compile-time assert so we don't accidentally call them elsewhere? and these actually don't need to check e_mode. They just bump SP.
then at end of each instruction: stack_fix_new - "if 65816 and e_mode then s_hi = 0x01"; test 275 -jsr (a,x) needed same treatment as jmp (a,x) meaning some very special handling of wrapping, and also "new" stack mode.
now at 0281! oops, wasn't setting_n_z after lda absl or lda absl,x. 0296.. ldx Direct not setting flags? Chortle. set_n_z_flags(cpu, cpu->x_lo); oops, needs to be _X. and found another one like that (_Y(cpu)).
ooh, 036A!! we're starting to crank right along now.. ah ha, we're at MVN. I haven't implemented that yet. Takin a break..
blasting through these - got to $4A0. Done for the night.
boom boom - now at 05AC. This appears to be a 16-bit decimal mode test. ugh I have dreaded this! tee hee.
This is a discussion of efficient BCD arithmetic using a correction factor after the add: https://gemini.google.com/app/809b15a0c56c7248
this is probably what the 8086 did with its bcd instruction (add and correct) or something like that. 8-bit isn't bad, 16-bit is pretty gnarly. but it's branchless and is better than doing gobs of multiplies and divides I'm doing now to convert back and forth to bcd.
on nmos 6502 V N Z were considered invalid in decimal mode so probably no software relied on them. however, 65c02/816 claim that the flags are valid. In fact I set n/z for both (unclear what a real nmos 6502 does).
my current bcd stuff is horrific. So what we need to do is change it to something more like what the CPU must do internally.
- alu first performs a normal 8-bit binary addition of accumulator, operand, carry flag.
- lower 4-bit nibble checked; if > 9, or "if a half carry occurred from bit 3 to bit 4", hardware adds 6 to the low nibble.
- upper 4-bit nibble checked. if > 9, adds 6 to the high nibble and sets the overall carry flag.
ok, the 816 is not using the 8086 algo. When digits are valid everything works ok, but if digits are -invalid- the low nibble is not right.
alright, let's say we're doing this nibble by nibble. Let's take a (valid digits) first.
78 + 13 = 91
8 + 3 = B B is > 9, so add 6. So that's a 1 with a carry ("11")
(maximum sum of two nibbles is 0x1E. (F + F). So the high nibble of a carry nibble is always 1.)
so let's do next nibble.
7 + 1 + 1 (carry) = 9
that is not > 9, so we're done, and the result is 91.
So let's look at an example with two carries.
78 + 23 = 101
8 + 3 = B B is > 9, so add 6 That's 1 with a carry ("11")
7 + 2 + 1 (carry) = A
that is > 9, so we have a carry. add 6 to A to get x10. now, that's 0 with a carry, so it's (1)01.
so let's look at our weird example.
CA + BA
A + A = 14 this result is > 9, so add 6 => 1A. plus a carry. C + B + 1 (carry) = 18 - so that's also > 9 and needs 6 added, to get 0x1E. (with a carry) (> 9 means carry) so we get EA.
So we should be checking if result & 0x1F > 9; no. We need to check each nibble >9, and add 6, and set carry. This is a nibble-by-nibble operation.
And I got new code inserted.. it's working except for the V flag failing.
http://www.6502.org/tutorials/decimal_mode.html
says:
2f. The V flag result is 1 if A < -128 or A > 127, and is 0 if -128 <= A <= 127 in 32-bit
Well that's an ugly way to do it. This article has pseudo-code about halfway down that -seems- to be working so far for ADC. it's sort of mask intensive but surely it's faster than my old multiply-divide nonsense.
Alright, looking at Kent's code from KEGS:
This is the code unraveled for only decimal subtraction. It mostly follows the 6502.org article with a couple of tweaks. (particularly, he can use unsigned because he adjusts for positive comparisons). For now I'll leave exactly as it is as 6502. make a note to come back and optimize later. (honestly, dec mode is not commonly used it's probably not worth a lot of time, which was my original attitude towards it lol.
overflow = 0;
/* decimal */
word32 tmp1, tmp2;
// lo byte
tmp1 = do_adc_sbc8(in1 & 0xff, in2 & 0xff, psr, sub);
// this is carry
psr = (tmp1 >> 16);
tmp2 = do_adc_sbc8((in1 >> 8) & 0xff, (in2 >> 8) & 0xff, psr, sub);
in2 = (in2 ^ 0xfffff);
sum = ((tmp2 & 0xff) << 8) + (tmp1 & 0xff) +
(((tmp2 >> 16) & 1) << 16);
// do_adc_sbc8 returns the -entire- P register in bits 16-23 of this result.
// so this is checking if V was set on the high byte math.
overflow = (tmp2 >> 16) & 0x40;
if((in1 ^ in2) & 0x8000) {
overflow = 0;
}
carry = (sum >= 0x10000);
and here's the 8-bit part:
do_adc_sbc8(word32 in1, word32 in2, word32 psr, int sub)
{
word32 sum, carry, overflow;
word32 zero;
int decimal;
overflow = 0;
decimal = psr & 8;
in2 = (in2 ^ 0xff);
/* decimal */
sum = (in1 & 0xf) + (in2 & 0xf) + (psr & 1);
if(sum < 0x10) {
sum = (sum - 0x6) & 0xf;
}
sum = (in1 & 0xf0) + (in2 & 0xf0) + sum;
// 10101010 => 0_0_101010 bit 8 (carry)
// 10101010 => 0_1_010101 bit 7
overflow = ((sum >> 2) ^ (sum >> 1)) & 0x40;
if(sum < 0x100) {
sum = (sum + 0xa0) & 0xff;
}
if((in1 ^ in2) & 0x80) {
overflow = 0;
}
psr = psr & (~0xc3);
psr = psr + (sum & 0x80) + overflow + (zero << 1) + carry;
return (psr << 16) + (sum & 0xff);
}
WHOO!
Good news: We now pass both the original 6502/65c02 tests and the snes-tests.
Bad news: effective 65816 speed is only between 140 and 150mhz. Here are some things to look at:
what is this unique_ptr business, is it slow, and, update_current_core_if_needed should be in the core, not called every single cycle in the outer loop.
make sure I don't have excessive TRACE statements in there. (in fact I set trace=no but it's still this slow speed).
examine some of the address_mode handler routines. for instance there is stuff like this:
if constexpr (CPUTraits::e_mode) {
if ((cpu->d & 0xFF) == 0) {
eaddr_16 = cpu->d | operand;
} else {
eaddr_16 = (cpu->d + operand); // with just direct there can be no page wrapping.
}
where we probably don't need that IF statement at all. (cuz if d is zero, ).
Additionally, I know there is just more cache contention in this suite (it's all over 16MB of RAM, instead of packed in 128K of RAM).. and I know there is a fair bit of work being done in the system to generate the 24-bit addresses.
I put in a lot of phantom_read stuff - those are mostly the same for 6502 except that the 24-bit address calculation is more complex.
Should run a profile on this and see where the time is being spent.
BUGS
[ ] inline void read_direct_x_ind(cpu_state cpu, T ®, U &index ) {
and its write counterpart-
my code assumes these are 65816 only, but in fact, they go back to the 6502. So I am inappropriately putting a (uninitialized) bank address in 6502 mode.
/ Address Mode: 11. Direct Indexed Indirect (d,x) (Read) /
template
if constexpr (CPUTraits::has_65816_ops && !CPUTraits::e_mode) {
read_data_direct(cpu, eaddr_16, temp);
eaddr = (cpu->db << 16) | temp; // add data bank
}else {
// if constexpr (CPUTraits::e_mode) {
// in e mode the +1 needs to wrap inside its page.
eaddr = bus_read(cpu, (uint16_t)(eaddr_16));
eaddr |= bus_read(cpu, (uint16_t)(eaddr_16 & 0xFF00) | (uint8_t)((eaddr_16 & 0x00FF) + 1)) << 8;
}
read_data(cpu, eaddr, reg);
TRACE(cpu->trace_entry.eaddr = eaddr_16; cpu->trace_entry.data = reg; cpu->trace_entry.f_data_sz = sizeof(T)-1;)
}
ok I fixed the memory issue in cputest/Airheart BUT the 816 test fails now. It's because: emul mode needs to add bank, AND also do the fonky wrap.
I also crashed it by reading a word from FFFFFF.
[ ] bug: booting ProDOS v2.4.3 in emulation mode (what other mode is there?) at PC: 59595 E8CB, it tries to LDA FF48,Y with Y=FE I don't know if that's intentional or not, but it causes an exception.
Thinking about having some helper functions for constructing: index_16_8(word_t base, byte_t index) - create 16-bit address with 8-bit wrap index_16_16(word_t base, word_t index) - 16-bit address with 16-bit wrap
scrutinize code - [ ] in plain 6502 mode we sometimes index e.g. address_abs_x with high values. Creates address >= 0x1'0000. causes crash. [ ] what happens if we index past 0xFFFFFF ?
The quick fix is to and xFFFF eaddr if we're a normal 6502 or in emulation mode. Maybe what we do in these cases is trigger a log of the bad memory reference: PC, registers, EADDR. maybe mask it, and set an 'invalid eaddr' flag in the trace record.
Who all does this:
- address_abs_x
- address_abs_x_write
- address_rmw_abs_x
SO these on 816 even in EMU mode would wrap to next bank. However, we need to force to 16-bit in 6502/65c02 mode.
read_direct_ind_x write_direct_ind_x ABSL 16-bit read/write
the basic question is: 1. fix addresses in bus_read/bus_write 1. fix addresses in the mmu interface (the mmu knows how much memory is in the computer) 1. fix them in the calling functions
Until now, the cpu and mmu have mutually assumed 64K of memory, and mmu assumed cpu would only ever send it a 16-bit address; further, we always gave the mmu at least 64K of ram block to work with.
I currently do an assert -> crash if the page is out of range of the page table. This would change to: 1) mask with a mask value created when template is defined; mask and if masked addr is not same as input address then flag some kind of warning.
Let's say we instantiate MMU as a template with arguments: address size, page_size. Then we can use some relatively inexpensive static bounds checks.
also I note: union { struct { uint16_t pc; / Program Counter - lower 16 bits of the 24-bit program counter / uint8_t pb; / Program Bank - upper 8 bits of the 24-bit program counter / uint8_t unused; / Padding to align with 32-bit full_pc / }; uint32_t full_pc; / Full 24-bit program counter (with 8 unused bits) / };
- We don't init 'unused' here to 0.
- I don't have to do all this dumbass bit manipulation to construct a 24-bit PC. I ALREADY HAVE A 24-BIT PC, I just need to read full_pc instead of pc.
[x] optimize generation of pb + pc.
[x] Hm, what if we store the data bank as part of a 32-bit union also. For example: ``` union { struct { uint16_t unused_db; uint16_t db; }; uint32_t full_db; }
Then we are never shifting it, just using full_db instead of (db << 16) all over the place. That seems to be working ok.