My current challenge is to see if the expansion port works electrically, and what changes we need to make to it, if any.
(Note that the MEGA65 has a C64-compatible 44-pin cartridge port, not a C65-compatible 50-pin cartridge port. We believe that improved compatibility with C64 accessories is more important that compatibility with non-existant C65 cartridges. There are a few other ways that we have worked to improve C64 compatibility in the MEGA65, such as ensuring that RMW instructions retain their old side-effects that are often relied upon for clearing VIC-II interrupts -- but that's a topic for other blog posts.)
We already know that we accidentally made /EXROM and /GAME bidirectional instead of inputs, but that is only a minor inconvenience, as we can work around that by scanning them as inputs periodically (their direction is switched together with a bunch of other bidirectional signals on the bus, otherwise we could leave them set as inputs the whole time).
A while back I implemented an entire cartridge port framework for the MEGA65, and tested it with a dummy cartridge implemented in VHDL, with both ROMs and I/O area expansions. So I know that, in principle at least, it should work. But let's just go over the theory of how a cartridge works on the C64 first. We will stick with simple cartridges, and only C64 mode cartridges, not C128 ones or C65 ones, partly because that covers most cartridges, and partly because the MEGA65 will support only C64 and MEGA65 cartridges.
The cartridge signals what resources it has to the C64 via the /EXROM and /GAME input lines. These are internally pulled up in the C64, and cartridges connect them to ground if they want to activate them. You can read more about their effects here, but a quick summary is:
/EXROM = 5V, /GAME = 5V gives the normal C64 memory map, as when no cartridge is connected.
/EXROM = 5V, /GAME = 0V gives "Ultimax" mode, where most of internal RAM is banked out, and $8000-$9FFF and $E000-$FFFF are provided by the cartridge. Mostly of interest only if you want to make a cartridge that replaces the KERNAL in a C64.
/EXROM = GND, /GAME = 5V gives a single 8KB cartridge rom at $8000-$9FFF
If both /EXROM and /GAME = 0V, then $8000-$BFFF is a 16KB ROM provided by the cartridge.
In most cases, the contents of the CPU IO port at $01 controls which of these ROMs are actually visible, versus the RAM underneath. The signals from $01 and from /EXROM and /GAME, together with the upper bits off the address that the C64 is trying to access at any point in time go to the PLA chip in the C64, which works out which chip-select lines get activated, and hence, what memory the C64 sees at that loccation at that time.
Now, for cartridges, the external ROMs also have chip-select lines, /ROML and /ROMH, which are driven to 0V by the PLA in the C64 when trying to access the ROM.
For cartridges that provide I/O expansions at $DExx or $DFxx, the process is similar, except that the C64 doesn't need to be told, it simply pulls the /IO1 or /IO2 line to ground, if either of those areas are being accessed.
All up, this makes for surprisingly simple electronics in a cartridge.
The MEGA65 supports all those signals, and also allows the Hypervisor to disable a connected cartridge, by refusing to take the /GAME and /EXROM signals into account, and thus never activating the chip-select lines for them.
For convenience, and as part of the mechanism for supporting much larger capacity cartridges on the MEGA65 (a topic that will be covered in more detail another day), the full 64KB of address space exposed by the cartridge is able to be accessed at $7FFxxxx. Accesses to $8000-$BFFF in that space will activate /ROML, while those to $E000-$FFFF will activate /ROMH, and of course $DExx and $DFxx accesses will activate /IO1 and /IO2.
This makes for convenient cartridge testing, independent of the memory map, because we can simply issue an access request to the cartridge port. Trying this, I found that there appeared to be activity when accessing $DExx and $DFxx. So far so good. However, the address decoding for $DExx and $DFxx is somewhat special, as it appears in every memory bank when IO is visible. However, a quick check revealed that it also attempts to access the cartridge port when I use addresses $7FFDExx and $7FFDFxx. This is expected.
So at this point, I know that the CPU can delegate memory accesses to the cartridge port, but not whether anything sensible happens there. Also, critically for cartridges, I don't know whether the /ROML and /ROMH accesses are also being sent to the cartridge port. Attempting to access $7FF8xxx or $7FFExxx should reveal that -- and indeed, this works also.
So now we know that all of the cartridge chip select lines are being driven. Quick checks revealed that R/_W is floating high to read, and that the 1MHz phi2 clock. The DMA line is floating high, indicating that the bus is not unavailable due to DMA. That leaves just the BA signal outstanding -- which is also floating high, which tells the bus that the VIC-II doesn't need the bus for anything (this is in fact always high on the MEGA65, because the VIC-IV doesn't use the cartridge port for anything).
Thus, all the conditions are ripe for a cartridge to be visible on the expansion port, and for us to be able to read the data from a ROM or IO bank in it. Time to insert a cartridge. In thtis case, an exemplar of the classic "International Soccer"
Quick initial attempts to read memory locations $7FF8000, $7FFA000, $7FFDE00, $7FFDF00 and $7FFE00, however, showed no signs of data. At this point, I don't know if the cartridge is not presenting data, or whether the MEGA65 is not taking note of the read data. Sticking the oscilloscope onto one of the data lines, and accessing $7FFE000 and above, I can see signs of data:
That we are seeing varying data indicates that we are able to drive the address lines correctly, something which I was also able to confirm using the oscilloscope.
There is something a bit odd, however, in that the data lines are resting low in many cases, rather than floating high. I need to investigate the cause of this. It might be that we need pull-ups on the data lines, or it might be that something in the cartridge is continuing to drive the lines. Removing the cartridge has the lines continuing to rest low, which means it must be the lack of pull-up resistors.
This in fact points to two problems that we need to solve: First, the lack of pull-ups, and second, if the lines are sitting low, and we see data being read from the cartridge, why on earth we are not seeing the data being read by the CPU? Is it a timing/logic error, or are the data lines not able to be read for some reason? Again, in the absence of pull-ups, the data being read should be all zeroes, but what I am seeing is all ones.
One challenge to finding and fixing these bugs is that it takes several hours to resynthesise the FPGA program, even if making just a trivial change. Thus, it can be helpful to have a minimal FPGA program that allows us to probe some of these pins, and see what we can see there. This allows iteration taking only a few minutes a time, instead of several hours.
Thus my next step is to see whether I can read from the data lines on the bus, so that I know whether the problem is there, or whether it is in the logic of my expansion port controller in VHDL. To make my life as easy as possible, this program generates a simple VGA frame, and displays incoming signals as white or black squares for 1 or 0. It also allows setting upto 256 different signals, some of which I bound to the cartridge port control and data lines. You then just use a joystick to move around and toggle the lines.
The display of me using it to read a byte from an International Soccer cartridge can be seen below:
The top white and green row is the cartridge port control signals. White = 1, green = 0. They are, from left to right:
cart_ctrl_dir - Sets the direction of R/W, /ROMH, /IO1, /GAME, /EXROM, /IO2, /ROML and /BA to be output if set.
cart_data_dir - Sets the direction of the data lines to output if set.
cart_haddr_dir - Sets the direction of the upper address lines to output if set.
cart_laddr_dir - Sets the direction of the lower address lines to output if set.
cart_phi2 - Directly sets the phi2 clock line.
cart_dotclock - Directly sets the dotclock line.
cart_reset - Directly sets the /RESET line.
cart_nmi - Reads the /NMI line.
cart_irq - Reads the /IRQ line.
cart_dma - Reads the /DMA line.
cart_exrom - Reads/writes the /EXROM line.
cart_game - Read/writes the /GAME line.
cart_ba - Reads/writes the /BA line.
cart_rw - Reads/writes the R/_W line.
cart_roml - Reads/writes the /ROML line.
cart_romh - Reads/writes the /ROMH line.
cart_io1 - Reads/writes the /IO1 line.
cart_io2 - Reads/writes the /IO2 line.
cart_data_en - If 0, connects the data lines between FPGA and the cartridge slot.
cart_addr_en - If 0, connects the address lines between FPGA and the cartridge slot.
Below this line are a couple of white blocks. They just mark where /ROML and /ROMH are in the line above, for my convenience. Then there is a blank and unused line. The far left blocks on this line if set (which they are not) would tri-state the address and data lines, respectively.
The fourth line has the address bits (bit 15 is on the far left) in white=1 and green=0, a gap and then the data bits (bit 7 on the left). These are the bits that the FPGA is outputting (or wishes to output, if the _en and _dir lines are not set correctly). The fifth line, in red=1 and black=0, are the values read from the cartridge port, if it is connected. More precisely, they read back whatever the FPGA has on those pins, whether connected or not.
With this VHDL test setup working, I was able to quite quickly establish that the problem was real - I couldn't drive or read the real pins at all. This turned out to be because I had not connected the _en lines. After fixing that, I was able to obtain the picture above, where it can be seen that memory location $FFFA has the value of $9F on this cartridge, when /ROML is active low. This corresponds to address $9FFA
Better, once I had corrected the _en bug in the MEGA65 VHDL, I could now read the contents of the ROM at $8000:
:7FF8000 27 80 A8 80 C3 C2 CD 38 30 00 40 C0 40 C0 40 C0
:7FF8010 40 C0 40 C0 40 C0 40 C0 00 80 80 10 10 10 10 20
:7FF8020 20 20 20 40 40 40 40 20 84 FF 20 87 FF A9 2F 85
:7FF8030 00 A9 00 8D 0E DC 8D 1A D0 A9 7F 8D 0D DC AD 0D
:7FF8040 DC AD 19 D0 8D 19 D0 A9 25 8D FE FF A9 91 8D FF
:7FF8050 FF A9 9C 8D FA FF A9 80 8D FB FF A9 00 85 6F A9
:7FF8060 80 85 70 A0 00 B1 6F 91 6F C8 D0 F9 E6 70 A5 70
Further hunting around in the $7FFxxxx address space, I can see that this cartridge has 16KB of ROM at $8000, and 8KB at $E000, and indeed, my test program was reading the correct value:
:7FF9FF0 85 75 68 85 73 68 85 72 4C FE 9F 68 68 68 A9 00
That is, $9F is indeed at location $9FFA in the ROM.
So, this is a really nice significant step forward, in that I can correctly access the memory from the cartridge.
The next trick is to get it enabled in the computer's memory map. The Hypervisor on the MEGA65 has the ability to disable cartridges in software, so that they don't affect the C64 mode memory map (although they continue to appear at $7FFxxxx). This is the default mode in the current version of Kickstart. To enable the cartridge, one just needs to set bit 0 of $D67D from within the Hypervisor (if you try to touch this register from user land, it instead causes a trap to the Hypervisor).
The only trouble is, when I set this bit, the cartridge didn't suddenly get mapped into memory. I also realised that I hadn't added any means to examine what the CPU thinks the /EXROM and /GAME lines are set to. So, I have added these in, and resynthesised the design. As sometimes happens with VHDL for me, although it is deeply unsatisfying, the act of putting instrumentation on a signal seems to suddenly make it work.
Anyway, these signals are now available to the CPU, and when the Hypervisor enables cartridges, the ROM suddenly maps at $8000-$BFFF as it should, and I can trace it single-step through its startup process. However, it wouldn't start the game. Tracing further, I found that it copies the ROM to the RAM underneath, which is not uncommon for cartridges to do. Seeing this, I checked to make sure that I had this implemented for cartridge ROMs, but apparently not. It works for the standard ROMs, but I had the cartridge memory space mapping for both reads and writes. Again, a quick fix once I knew what the problem was, but again, I have to resynthesise to test again, which probably means waiting until morning. But now I am getting excited -- everything looks like this cartridge should work when I try it with this fix. We will see whether my excitement is misplaced or not.
Hmmm... Well, that managed to get the cartridge copied into RAM. Now, however the cartridge is getting stuck in a funny little loop:
$8211 98 TYA
$8214 91 1D STA ($1D),Y
$8216 11 1D ORA ($1D),Y
$8218 C9 CE CMP #$CE
$821A D0 F7 BNE $8211
The vector at $1D points to $0000, the data-direction register for the 6510, and Y=0. Thus, this has the effect of ... well, to be honest, I am not quite sure what it is supposed to be achieving.
So I started tracing again from reset.
The cartridge entry point is at $8027, which calls some KERNAL routines and begins to setup memory, and copying the cartridge to RAM as described above, it then switches out the ROM, and continues running the cartridge from the RAM beneath at $8074:
$8074 A9 05 LDA #$05
$8076 85 01 STA $01 ; Bank out cartridge ROM
$8078 58 CLI
$8079 A0 18 LDY #$18
$807B A9 00 LDA #$00
$807D 99 00 D4 STA $D400,Y ; Clear SID registers
$8080 88 DEY
$8081 10 FA BPL $807D
$8083 A9 8F LDA #$8F
$8085 8D 18 D4 STA $D418 ; Set SID volume
$8088 20 43 AC JSR $AC43
$808D 20 40 AC JSR $AC40
All is fine until it gets to these two instructions. The trouble being that $AC43 appears to be in the middle of an instruction. At first I thought that maybe the copy loop had failed for some reason, but comparing the ROM and RAM sections of memory, and also double checking that this matches the memory actually mapped by the CPU there, it all looks the same:
:000AC40 69 40 85 87 A9 00 85 3C 85 3F B9 A1 C1 85 3A B9
:701AC40 69 40 85 87 A9 00 85 3C 85 3F B9 A1 C1 85 3A B9
:777AC40 69 40 85 87 A9 00 85 3C 85 3F B9 A1 C1 85 3A B9
The sensible disassembly of this would seem to be:
$AC40 69 40 ORA #$40
$AC42 85 87 STA $87
In which case there is no reason to ever JSR into $AC43, which would in fact be an illegal opcode (SAX $A9, i.e., store the and of X and A into location $A9 if you are curious). This doesn't seem to make any sense to me.
So then I wondered if setting $01 to $05 might just bank out the cartridge ROM, and leave C64 BASIC visible. $AC43 is at least the beginning of an instruction in the C64 BASIC, but not the start of a routine. Indeed, it is in the middle of the GET command, which seems an unlikely choice here. Also, $AC40 is not the start of an instruction in the C64 BASIC ROM. But more to the point, after consulting the C64 memory banking table, it is not possible to bank the BASIC ROM in, while /EXROM and /GAME are both low.
So at this point, I am beginning to wonder if the cartridge isn't faulty, and that it is supposed to have a different byte sequence at $8088. So I downloaded a copy of International Soccer, which I know is based off of at least some cartridge version, and took a look. Not surprisingly, the loadable version has the ROM to RAM copy routine replaced by NOP instructions. Then in the process of saving it, I spotted something important: $AC40 is completely different in that version to what I was seeing. Then, finally the penny dropped: The MEGA65 was seeing the first 8KB of the cartridge ROM twice, instead of all 16KB, because I should be asserting /ROMH for the second 8KB, instead of /ROML again. Time for another synthesis run ... Now I am less excited, perhaps because it is almost 4AM (I got up early, not went to bed late), when part of me says I should be more excited, since I found and fixed what seems like it should be the last barrier to the cartridge mapping properly, and indeed automatically on power-up.
So, that fix did indeed fix the mapping of the 2nd 8KB of the cartridge ROM. However, it still doesn't work. It now makes the JSR to $AC43, without trouble. However, then jumps to $AF6C which is an RTI instruction. On the disk version of the game, there is something completely different there. Comparing the two, I am seeing a lot of repetative data from the cartridge, but not from the disk version.
At this point, I am left wondering if this cartridge is working correctly, or if it has a hardware fault, or indeed, if my cartridge port bus has poor timing, that is causing it to read incorrect data, or perhaps that my signalling is so bad in some way, that it is harming the cartridge, e.g., through cross-driving data lines.
As I don't have an easy way to hook up an actual C64 here, I don't have too many options except to try a different cartridge. For (the as it turned out unfounded) fear that the problem might be the MEGA65 toasting cartridges, the only one I had a double of was Radar Rat Race, so I tried that, but that turns out to be an Multimax-mode cartridges, and trying that revealed some fun bugs with my Multimax Mode setup for cartridges. Bugs hopefully fixed, and once again, off synthesising.
So now I was back to the International Soccer cartridge. I was really beginning to think that it was a MEGA65 logic problem, but the repeated blocks I was seeing had raised a suspicion in me that it could be a faulty ROM in the cartridge. So I opened the cartridge up, and was very happy to find this:
First up, we can see that this is a cartridge made on Commodore's multi-purpose PCB, with the three traces on the left that allow reconfiguration between different cartridge types, and has spots for two 8KB ROMs, plus one small RAM, so that it can be used for Multimax cartridges. What isn't quite so obvious above, is that one of the ROMs was looking a bit short on solder on one side, and it looks like a couple of pins were bridged by a solder thread:
On the reverse side, it was obvious that someone at some point had tried (in vain) to desolder one of the ROMs:
That someone is quite possibly a much younger me, who wanted to make a custom cartridge, although I don't specifically remember. The empty socket for the RAM is also just sitting there, not soldered down at all. All most curious.
Anyway, for the task at hand, I needed to re-solder those dry pins, so that all the address lines would work again, and thus, hopefully, fix the repeated blocks problem.
That I did, and then boldly put the cartridge back together, and inserted it into the MEGA65, and started it up. Finally, after tracking down such a diverse variety of problems, I was greeted with scenes like this:
Finally! So, we can now say that the MEGA65 can use (at least some) real C64 cartridges.
Here is a short video I made, showing the cartridge being inserted, and the MEGA65 powering up from cold to run the cartridge. These r1 PCBs have a larger FPGA that takes longer to configure on power up, so the power-on process will likely be a shade quicker on the final machines. Nonetheless, it is already very fast, and feels like a real 8-bit computer, which of course is the point.