I finally finished the MMU. Its still in alpha version but seems to work as intended. The most problematic parts were solved so that JSR, JMP and RTS works as they should. Even an interrupt happening in between instructions does not break the implementation.
What can it do?
The 6502MMU code is made in verilog and is very small. Even a 256 LUT CPLD is large enough. Its basically what the 6509 should have been, but never was. Address area is effectively increased from 64KiB up to about 256MiB. The current implementation has an 1MiB address space (because that was how much memory I had).
It uses 4 different unused opcodes: Three NOP $addr,X and one NOP #imm instruction. So it does not do anything special if you run the program on an normal 6502. All 6502/6510/8502/8510 should handle this the same way since the NOP instructions are present on all these versions. On the 65C02, many unused opcodes were changed to a simple "NOP", but not for the above 4 NOP's. E.g. even on the 65C02 the NOP will use the following bytes (even fetching its memory location), so they will still work.
These NOP's were repurposed into the following MMU instructions by the 6502MMU:
Code:
$82 $50 MMS $50 (MMS=Memory Management System register)
$FC $0A $EF MMF $0AEF (MMF=Memory Management Fetch)
$DC $0a $EF MMJ $0AEF (MMJ=Memory Management Jump)
$5C $0A $EF MMI $0AEF (MMI=Memory Management Interrupt vector)
What do they do?
The MMS instruction currently switch the MMU on or off. Setting bits 4-7 to %0101 switches it on, all other values switches it off. Bits 0-3 will be implemented in a future update.
MMF is a fetch command that modifies any memory fetch instruction that follows it. The current implementation takes the LSB and adds that to the top of the memory address:
MMF $0012
LDA $A345
Will load content of address $12345 into the accumulator. All other commands and address modes work the same way, e.g. LDA/LDY/LDX/STA/STX/STY/INC/ADD/SUB.. and so on, will all fetch or store to the memory address $12345.
You may notice that in the above example "LDA $A345" only uses address bits 0-11 and discard the bits 12-15 (e.g. "$A"). The reason is that in this example, memory location $A000-$AFFF is set apart for the MMU and is were it actually "lures" the 6502 to find the address content $12345. This is done by putting the memory bank $12 into the $A000-$AFFF area.
MMJ is a jump command that modifies the target address of JSR and JMP. It works by jumping to the address given in the same way as MMF:
MMF $0012
JSR $A345
Will jump to address $12345. If this command is used from another bank in the $A000-$AFFF area, the RTS instruction will return to that address (and bank). If it comes from another non-MMU address area, the RTS instruction will return to that address. If you try to use a JSR/JMP to the $A000-$AFFF range without a MMF preceding the instruction, the jump will default to memory bank $00.
The last instruction MMI will be implemented in the future to handle interrupt vectors into the extended 1+MiB memory area. They will work the same as the MMF or MMJ, but only trigger once an interrupt starts and changes the PC (program counter). Currently only interrupts within the normal $0000-$ffff address area of the 6502 are supported.
Since all these extended address modes require two opcodes to handle correctly, an interrupt occuring between the MMF/MMJ and the following instruction is handled by storing the MMF or MMJ vector into an internal stack. This stack can hold up to 256 entries so that several MMF and/or MMJ can be executed before the instruction that requires it is executed. For example:
MMF $0012
MMF $0013
LDA $A345
LDA $A345
Will load address $13345 first, then $12345 into the accumulator. The reason it does not load $12345 first is that the recurring MMF instruction will load the last values as the current memory bank pointer $13 (for the fetch), then after the first LDA, the previous memory bank pointer $12 is going to be taken from the internal stack and used by the second LDA.
For MMJ, the following is true:
MMJ $0012
MMJ $0013
JSR $A345
JSR $A345
The second jump vector $13 will be used by the first JSR, so that it jumps to address $13345. Then after return (through an RTS instruction), the second JSR will jump to $12345.
The MMS register lacks some control functions that will enable multiple fetches from/to the same memory bank. This will be implemented in the future. I am also looking into using opcode $44 to have an indirect MMF were high address is stored in ZP.
Known bugs:
- Currently one needs to put "JMP $A000" into memory location $B000. The reason for this is that the 6502 will increase the Program Counter from $AFFF into $B000 when that address is reached. For the same reason, any code that uses $xxxFFF must ensure that the instruction ends at $xxxFFF. This bug will be fixed in a future update so that no "JMP" is needed in $B000.
- Branching out of the current memory bank does not work. This will be fixed in the future, but requires an "internal" JMP that will not be visible on the software side. An extra 6 CPU cycles will result.
I am not going to publish the code yet since its part of a larger Verilog module. But I hope to eventually offer it as stand-alone and as a small CPLD with a memory chip to plug into a socket between a 6502 and its board. The code will then become public domain so that others may enjoy it as well. With all the large MPUs out there, 8-bit code still takes the least space and with more memory may become even more useful in the future.