Add Huc6280 Support - #134
Conversation
| stack_pointer: StackPointer(0), // Real hardware: random value on power-on | ||
| program_counter: 0, // Set by reset vector in practice | ||
| status: Status::default(), | ||
| mpr: [0; 8], // HuC6280 MMU registers; unused by other variants |
There was a problem hiding this comment.
Is there a way to move this into the Bus trait somehow? That would be cleaner. But then, I have no idea how this works or what it needs to do.
There was a problem hiding this comment.
Hmm... maybe? But then again, I believe that they are actual registers. See here:
Software running on the HuC6280 operates on 16-bit memory addresses, same as 6502 software, but the HuC6280 has a builtin MMU that expands the physical address space from 16-bit to 21-bit (2 MB address range). It’s extremely simple as far as MMUs go: it splits the 16-bit logical address space into eight 8 KB pages, and each page has its own 8-bit MPR (Memory Page Register) that maps it directly to an 8 KB physical page. It’s very similar to the memory-banking mappers seen in many NES / Game Boy / Master System / Game Gear cartridges, only it’s built into the CPU, so game cartridges don’t need to include their own mapper hardware (though one game still does due to its massive ROM size).
There was a problem hiding this comment.
I would keep it in there for now.
| /// For every non-HuC6280 variant the mapping registers are zero, so this is | ||
| /// the identity mapping within the low 64 KB. | ||
| #[must_use] | ||
| pub const fn physical_address(&self, logical: u16) -> u32 { |
There was a problem hiding this comment.
Question: Can the MMU functionality move into an implementation of the Bus trait? (that would include this function, physical_address, the mpr registers and the handling of relevant opcodes.
There was a problem hiding this comment.
Right now we have to shadow the MPR registers because translation runs inside bus.get_byte(), which isn't idea. The MPRs live in the cpu, so the bus needs to copy. What's worse is that we have to create a copy whenever TAM runs...
I don't think there's an easy solution here.
We could try moving MPRs into the bus (then the bus needs MMU-specific trait methods + HuC6280 can no longer run on a plain Memory bus), or we try moving translation into the CPU (then Bus has to take 21-bit physical addresses, which pollutes the generic core for one variant).
Both of those make the generic core worse to fix one variant. So I'd say the current tradeoff (generic u16 Bus, MMU as this thin integrator-side adapter) is the more idiomatic way.
But there are still ways to make it more ergonomic. I noticed that there's quite a bit of redundancy, and I'll push a commit in a sec to fix that.
Also voting to add set_mapping_register to the Bus trait. The default implementation does nothing, which is correct for all variants without an MMU. I will add this method, but we can remove it again if it doesn't carry its own weight.
|
Good job! My only remaining question would be the red |
|
To properly handle HuC6280's IRQ vector multiplexing with a cleaner design, I'm reconsidering how the vector abstraction should work. Right now the |
|
I moved the interrupt handler to the bus. It's the only place that can see the runtime state needed to pick the correct IRQ vector. This is a more accurate abstraction than the previous version. The previous commit put Now, I split the responsibility along the real hardware boundary:
This way, we can now express the TIQ > IRQ1 > IRQ2 priority by overriding the method. |
…n see the runtime state needed to pick the correct IRQ vector. This is a more accurate abstraction than the previous version. The previous commit put `irq_vector()` on the `Variant` trait alongside `reset_vector()`/`nmi_vector()`/`brk_vector()`. That was inaccurate: a `Variant` is a zero-sized marker type, so it can't see the runtime state (which sources are pending and unmasked) that the HuC6280's on-chip interrupt controller uses to pick the IRQ vector. The previous version could only ever return a single static address. Now, I split the responsibility along the real hardware boundary: - `Variant` contains `reset_vector()`, `nmi_vector()`, `brk_vector()`. They never vary at runtime. - `Bus::irq_vector(&mut self) -> u16`, which defaults to `$FFFE`. It is only being consulted while an IRQ is pending. This way, we can now express the TIQ > IRQ1 > IRQ2 priority by overriding the method.
|
Fixed a small correctness bug: the core didn't advance cycles while in WAI/STP/JAM or on an undecodable opcode, so any host using the cycle delta for pacing deadlocks (e.g. "WAI-for-vblank" games). I made |
|
Done with my refactorings. I think this is ready for another round of reviews / mergeable. |
|
I will go ahead and merge this. Should be relatively clean and I am planning to release my little Turbografx-16 emulator based on this work. We can make more tweaks in the future, but so far I didn't find any issues with the code when trying it on a few games. See the other repo. |
This adds support for the Hudson Soft / NEC HuC6280, the CPU powering the NEC
TurboGrafx-16/ PC Engine from 1987.Apart from the additional instructions, it adds a 21-bit address space. Addressable memory starts at $2000, which is why I introduced support for relocating the zero-page. (Other variants are unaffected.)
The
T(memory-operation) flag, which redirects ORA/AND/EOR/ADC and friends to operate on memory instead of the accumulator is not implemented yet. It's a bigger change, that will probably touch the other variants, so I left that out for now.I found https://github.com/SingleStepTests/huc6280, which provides Mesen-generated tests. It's in JSON format, which is why I added a little translator to be able to run these tests. Found a few bugs this way. The corpus is quite large, which is why I didn't include it in the PR. We can consider changing that if worthwhile.
TODO (for future PRs)
I checked what's missingand it's quite a lot, but it's probably okay to brush up this PR and merge what we already have. (Well, except for the mising Bus support, which I want to add before merging.)
Busimplementation I think).SET(T-flag) prefix instruction is not implemented, so the T-flag memory-operand mode for arithmetic/logic ops is unsupported.CSL/CSH(clock speed select) are no-ops; the emulator tracks per-instruction cycle counts, not wall-clock timing, so the 1.79/7.16 MHz switch has no effect. I think that's fine. Don't think that clock cycles should change when the clock speed gets changed.ST0/ST1/ST2just write the immediate to fixed memory offsets ($0000/$0002/$0003) and rely on aBusimplementation to intercept them as VDC port writes; there is no actual VDC (Video Display Controller).Bus(the integration tests use a translatingPhysBusfor this, but maybe we can remove that).base_cyclestable and execute match arms with all other variants; they're gated only by per-variantdecode, so nothing structurally prevents another variant from reaching them if its decoder were changed to emit those instructions, but I think that's okay since adding another level of abstraction is just adding complexity for little gain.MPRregisters exist on every variant'sRegistersstruct (zeroed and unused for non-HuC6280 parts), so the type doesn't distinguish HuC6280 state from the rest. We discussed moving more of that functionality into the Bus. Not sure if we can get rid of those registers entirely, though. I was considering to move the register definition into the variant trait, but it would add complexity and the number of unused registers per variant is pretty limited anyway.