diff --git a/README.Savestates.FURiOUS.md b/README.Savestates.FURiOUS.md index ef1bacd5..74869a19 100644 --- a/README.Savestates.FURiOUS.md +++ b/README.Savestates.FURiOUS.md @@ -73,8 +73,34 @@ This file contains a few fixes for some games that has problems with APU. Exampl 64E9: 7E174B,2142;05C10D,A1 # This updates $7E174B with the latest $2142 state, it also patches 1 byte at the rom 0x05C10D (pc address) ``` +### Special chips +Savestates originally only worked with games without a special chip, because the +firmware loads a different FPGA core for those and it had no in-game hook to run +custom code from. The coprocessor cores now carry that machinery, so savestates +also work on: + +|Chip|Core|Notes| +---|---|--- +|**DSP1-4** (uPD7725)|`fpga_dsp`|ST0010 (uPD96050) is not covered| +|**SA-1**|`fpga_sa1`|Mk.III only (does not fit the Mk.II FPGA)| +|**Super FX / GSU**|`fpga_gsu`|| +|**CX4**|`fpga_cx4`|| +|**OBC1**|`fpga_obc1`|| +|**S-DD1**|`fpga_sdd1`|| + +SPC7110 and Super Game Boy are still not supported. As always, both the firmware +and the FPGA core files on the SD card have to be up to date — a stale +`fpga_` file makes the firmware fall back to a save without the chip state +instead of capturing garbage. + +On the **Mk.II** the GSU and CX4 cores had no room left on the FPGA for both the +savestate machinery and the six hardware ROM-cheat comparators, so on those two +cores (Mk.II only) ROM cheats are applied by patching the loaded ROM image +instead. They work as before with one difference: the in-game cheat toggle no +longer switches ROM codes on and off — those follow the setting as it was when the +game was loaded. WRAM cheats are unaffected, and the Mk.III is unchanged. + ### Known Savestates Issues -Savestates will only work with games that don't have special chips like SuperFX/SA-1/CX4, this is because the firmware loads another FPGA file for those games and it doesn't have necessary configuration to run custom code. Flashcart savestates aren't perfect, the code runs on NMI to save a bunch of addresses to another place. You'll notice the song in some games will keep playing after you load state, others will just crash. diff --git a/snes/Makefile b/snes/Makefile index e21a6f5e..624a852b 100644 --- a/snes/Makefile +++ b/snes/Makefile @@ -1,5 +1,5 @@ IPS = header.ips -O65 = reset.o65 main.o65 font.o65 palette.o65 data.o65 const.o65 logo.o65 logospr.o65 dma.o65 filesel.o65 pad.o65 time.o65 sysinfo.o65 spc700.o65 spcplay.o65 menu.o65 menudata.o65 ui.o65 nmihook.o65 common.o65 savestate.o65 near.o65 # cheat.o65 # gfx.o65 # vars.o65 +O65 = reset.o65 main.o65 font.o65 palette.o65 data.o65 const.o65 logo.o65 logospr.o65 dma.o65 filesel.o65 pad.o65 time.o65 sysinfo.o65 spc700.o65 spcplay.o65 menu.o65 menudata.o65 ui.o65 nmihook.o65 common.o65 savestate.o65 savestate_cx4.o65 near.o65 # cheat.o65 # gfx.o65 # vars.o65 OBJS = $(IPS) $(O65) # stopgap measure to create both files as long as they are identical... diff --git a/snes/savestate.a65 b/snes/savestate.a65 index 89a4ee43..097e1c64 100644 --- a/snes/savestate.a65 +++ b/snes/savestate.a65 @@ -21,12 +21,59 @@ savestate_handler: ; print "Savestate Bank Starting at: ", pc php : rep #$30 : .al : .xl : pha + ; With the IRQ hook active every IRQ lands here, including per-scanline raster + ; IRQs; running the full handler in those delays the game's own ISR every frame. + ; Only the once-per-frame vblank entry is needed, so mid-frame entries take the + ; short path below. + sep #$20 : .as + lda @$004212 + bpl ss_midframe_entry + rep #$20 : .al ; nmihook already handled button input lda @NMI_PAD sta @CS_INPUT_NEXT savestate_start: jmp ss_init +ss_midframe_entry: + .as : .xl ; reached with A still 8-bit from the $4212 read + ; A savestate in flight must never fast-exit: the resume wait needs the next + ; hook entry to bump CS_STATE, and in IRQ-driven scenes that entry is a + ; mid-frame IRQ. Exiting through the stub would drop snescmd_unlock and the + ; FPGA's savestate_force_entry with it. + lda @CS_STATE + bne ss_precheck_pass8 + ; Pre-check the pad the nmihook stub stored in NMI_PAD: only a held save/load/ + ; slot combo goes on to the full path. Masked match, extra bits ignored; + ; "all mask bits held" is (~NMI_PAD & mask) == 0. + rep #$20 : .al + lda @CS_SAVE_INPUT + beq + + lda @NMI_PAD : eor #$FFFF : and @CS_SAVE_INPUT + beq ss_precheck_pass ++ lda @CS_LOAD_INPUT + beq + + lda @NMI_PAD : eor #$FFFF : and @CS_LOAD_INPUT + beq ss_precheck_pass ++ lda @CS_SLOT_INPUT ; slot = Select + d-pad; require a d-pad bit too, + beq ss_precheck_fail ; so Select alone cannot keep the handler alive + lda @NMI_PAD : eor #$FFFF : and @CS_SLOT_INPUT + bne ss_precheck_fail + lda @NMI_PAD + and #$0F00 ; up/down/left/right present? + bne ss_precheck_pass +ss_precheck_fail: + .al : .xl + jmp ss_exit +ss_precheck_pass8: + .as : .xl + rep #$20 : .al +ss_precheck_pass: + .al : .xl + lda @NMI_PAD + sta @CS_INPUT_NEXT + jmp savestate_start + save_write_table ; Disable DMA .word $1000|$420B, $0000 @@ -144,20 +191,60 @@ return_align cmp #$EA ; entered via NMI? beq return_enable_nmi return_enable_irq - cli - lda @$004211 ; ack IRQ + ; Direct resume for IRQ entries: no CLI, no nested re-entry wait. Where the + ; frame tick itself is a coprocessor IRQ the redirect never re-fires with the + ; buttons released, so that wait would park forever. It is also unnecessary: + ; we entered at the vector fetch, so the game's interrupt context is still on + ; the stack and exiting through the stub hands the IRQ to the game's own ISR. + lda @$004211 ; ack any pending PPU timer IRQ (a cart IRQ is level, unaffected) lda #$31 ; mask off NMI enable - bra + + and @SRAM_OTH_BANK + sta @$4200 + lda #$02 ; post-save/load cooldown: ss_start_skip drains 2..15,0 + sta @CS_STATE + jsr ss_release_chips + ; reset picture + re-enable the game's interrupt mask + lda @SRAM_PPU_BANK + sta @$2100 + lda @SRAM_OTH_BANK + sta @$4200 + ; re-arm HDMA with the pre-freeze $420C (see the NMI path below) + lda @SRAM_OTH_BANK|$C + sta @$00420C + jmp ss_exit return_enable_nmi lda @$004210 ; ack NMI lda #$81 ; mask off IRQ enables -+ and @SRAM_OTH_BANK ; stored $4200 flags + ; The wait below is fed by the next NMI, but SRAM_OTH_BANK is the $4200 of the + ; state being RESTORED: a state taken with NMI off would leave the bit clear + ; and the wait could never complete. Force it here; the saved value is written + ; back before the game gets control. The NMI it lets through is redirected + ; into this handler and RTIs straight back into the loop. + ora #$80 sta @$4200 return_wait_irq: -- lda @CS_STATE + ; Bounded wait for the nested entry that bumps CS_STATE to 2. Only the FPGA can + ; deliver it, and every way that can fail ends with the PPU force-blanked and + ; the coprocessor frozen, so the spin is capped: the re-entry is at most one + ; frame away, ~65535 iterations is ~0.25 s of margin. X belongs to the game + ; here, hence the push/pull. + phx + ldx #$ffff +ss_wait_loop: + lda @CS_STATE cmp #$2 - bne - + beq ss_wait_done + dex + bne ss_wait_loop + ; Timed out: ss_wait_bail puts the machine in the state the successful path + ; would have left it in, then we fall into the same release/restore tail. + jsl @ss_wait_bail + .as : .xl +ss_wait_done: + plx + + jsr ss_release_chips ; reset picture lda @SRAM_PPU_BANK @@ -166,14 +253,37 @@ return_wait_irq: ; re-enable all interrupts lda @SRAM_OTH_BANK sta @$4200 - ;lda !SRAM_OTH_BANK|$C - ;stz $420C - + ; Re-arm HDMA with the pre-freeze $420C. The write tables cleared it and + ; register_restore_return skips it, so a scene that only arms HDMA on scene + ; events would come back with its window registers stuck at the last scanline + ; values and the screen fully masked. Armed here, on the vblank edge: arming + ; mid-frame would run the rest of the frame off the game's stale A2A. + lda @SRAM_OTH_BANK|$C + sta @$00420C + ; Code to run before returning back to the game ss_exit rep #$30 : .al : .xl : pla : plp rtl ; go back to in-game hook +; Release the coprocessor halts at the real game resume, so the chip wakes in +; lockstep with the SNES. Released any earlier it would free-run while the SNES is +; still frozen, and on the SA-1 its ROM fetches also starve the MCU of PSRAM slots +; while the .state is being written. A is 8-bit, X/Y untouched. +ss_release_chips + .as : .xl + lda @SS_SA1_GATE + beq + + lda #$00 + sta @SS_SA1_WINDOW|$07FF ++ lda @SS_GSU_GATE + beq + + lda #$00 + sta @SS_GSU_WINDOW|$00FE ++ jsl @ss_cx4_release + .as : .xl + rts + ; SMC (Self Modifying Code) to init on first pass ss_init rep #$30 : .al : .xl @@ -197,6 +307,10 @@ ss_init lda #$0000 sta @CS_SAVE_REQ + ; clear the resume-watchdog breadcrumb (PSRAM survives game loads) + sep #$20 : .as + sta @SS_DIAG_BAIL + rep #$20 : .al ply plx pla @@ -337,6 +451,30 @@ save_state inc sta @CS_STATE + ; --- freeze the coprocessor at the same instant the SNES CPU is cut --- + ; Halted any later it drifts while the SNES stands still and the handshake + ; desyncs on resume. Each core stops at a clean boundary of its own with an + ; FPGA-side timeout, so none of these can hang; save_return waits for the ack. + lda @SS_DSP_GATE + beq ss_dsp_early_done + lda #$01 + sta @SS_DSP_WINDOW|$07FF +ss_dsp_early_done: + lda @SS_SA1_GATE + beq ss_sa1_early_done + ; the halt request lives behind the window probe (see ss_sa1_probe_halt) + jsr ss_sa1_probe_halt +ss_sa1_early_done: + lda @SS_GSU_GATE + beq ss_gsu_early_done + lda #$01 + sta @SS_GSU_WINDOW|$00FE +ss_gsu_early_done: + jsl @ss_cx4_early + .as : .xl + lda #$00 + sta @CS_SA1_LOAD ; SAVE path: the restore blocks are no-ops + ; disable interrupts lda @$F90700|$0000 sta @SRAM_OTH_BANK @@ -421,22 +559,245 @@ run_vm save_return rep #$30 : .al : .xl - ; copy region + ; copy region -- WRAM mirrors F5-F6 -> state buffer F0-F1 only. Split from the + ; original single F5..F9:04FF op: VRAM, CGRAM, OAM and WRAM are captured by real + ; readback below (the bus-snoop mirror drops writes under load), so no copier op + ; may land in those buffer ranges afterwards. lda #$F5F0 ; srcBank:dstBank sta @$002020 lda #$0000 ; srcOffset sta @$002022 lda #$0000 ; dstOffset sta @$002024 - lda #$0500 ; length[15:0] + lda #$0000 ; length[15:0] sta @$002026 sep #$20 : .as - lda #$04 ; length[23:16] + lda #$02 ; length[23:16] -> $020000 = WRAM mirrors only sta @$002028 ; opcode[4:0], loop, dir, enable lda #$05 sta @$002029 - + ; second op: APU RAM mirror F8 + shadows F9:0000-04FF -> F3 + F4:0000-04FF. It + ; queues behind the first one (the copier trigger queue is one deep) and drains + ; during the VRAM readback below, which then overwrites F4:0000-041F. + rep #$20 : .al + lda #$F8F3 ; srcBank:dstBank + sta @$002020 + lda #$0000 ; srcOffset + sta @$002022 + lda #$0000 ; dstOffset + sta @$002024 + lda #$0500 ; length[15:0] + sta @$002026 + sep #$20 : .as + lda #$01 ; length[23:16] -> $010500 + sta @$002028 + lda #$05 + sta @$002029 + + ; --- DSP1-4 capture: wait for the freeze ack, then copy the window out --- + lda @SS_DSP_GATE + beq ss_dsp_cap_done + lda #$01 + sta @SS_DSP_WINDOW|$07FF ; ensure halt requested + ldx #$0000 +ss_dsp_cap_wait: + lda @SS_DSP_WINDOW|$07FF ; bit0 = frozen + and #$01 + bne ss_dsp_cap_go + inx + cpx #$2000 + bne ss_dsp_cap_wait +ss_dsp_cap_go: + ldx #$0000 +ss_dsp_cap_loop: + lda @SS_DSP_WINDOW, x ; $000-$1FF data RAM, $600-$6FF registers + sta @SS_DSP_SCRATCH, x + inx + cpx #$0700 + bne ss_dsp_cap_loop + lda #$00 + sta @SS_DSP_WINDOW|$07FF ; release halt -> DSP resumes +ss_dsp_cap_done: + + ; --- VRAM + CGRAM + OAM readback -> F2 / F4:0000-041F ------------------- + ; Runs in forced blank with GP-DMA/HDMA off. VMAIN/VMADD/CGADD/OAMADD are + ; clobbered and restored from the register shadow on resume like every other + ; PPU register. + ; $2139 prefetch only advances on reads, so the first read after setting + ; VMADD is a dummy -- and VMADD must not be re-set afterwards, or every + ; following word is shifted by one. $213B and $2138 have no such quirk. + sep #$20 : .as + lda #$80 : sta @$002115 ; VMAIN: word step after $213A read + rep #$30 : .al : .xl + lda #$0000 : sta @$002116 ; VMADD = 0 + lda @$002139 ; dummy read (loads word 0, VMADD -> 1) + ldx #$0000 +ss_save_vramrb: + lda @$002139 ; 16-bit read = $2139+$213A (word) + sta @$F20000, x ; VRAM -> state buffer + inx : inx + bne ss_save_vramrb ; 32768 words = full 64KB + sep #$20 : .as + lda #$00 : sta @$002121 ; CGADD = 0 + ldx #$0000 +ss_save_cgrb: + lda @$00213B + sta @$F40000, x + inx + cpx #$0200 + bne ss_save_cgrb + lda #$00 : sta @$002102 ; OAMADDL = 0 + lda #$00 : sta @$002103 ; OAMADDH = 0 (word addr 0, no rotation) + ldx #$0000 +ss_save_oamrb: + lda @$002138 ; low table (512 B) then high table (32 B) + sta @$F40200, x + inx + cpx #$0220 + bne ss_save_oamrb + + ; --- WRAM via CPU reads of $7E/$7F -> F0/F1 ----------------------------- + ; The copier staged the mirror, which drops writes under load; the SNES reads + ; its own WRAM directly, so overwrite the buffer with an exact copy. Runs last, + ; after the copier has drained. F0/F1 sit in the IS_PATCH window, so the stores + ; land in PSRAM. ~440 ms for 128 KB. + rep #$30 : .al : .xl + ldx #$0000 +ss_save_wram_lo: + lda @$7E0000, x + sta @$F00000, x + inx : inx + bne ss_save_wram_lo ; 32768 words = 64KB ($7E -> F0) + ldx #$0000 +ss_save_wram_hi: + lda @$7F0000, x + sta @$F10000, x + inx : inx + bne ss_save_wram_hi ; 32768 words = 64KB ($7F -> F1) + + ; --- SA-1 capture: IRAM $00:3000 (2 KB), BW-RAM $40:0000 (32 KB) and the + ; 256-byte window, all into the F4 scratch. IRAM and BW-RAM are read over + ; the SNES bus; BW-RAM is always taken as the full 32 KB, which SAVERAM_MASK + ; mirroring makes safe for smaller carts. + sep #$20 : .as + lda @SS_SA1_GATE + bne + ; block > 127 bytes: short branch is out of range + jmp ss_sa1_cap_done ++ lda #$01 + sta @SS_SA1_WINDOW|$07FF ; ensure halt requested + ldx #$0000 +ss_sa1_cap_wait: + lda @SS_SA1_WINDOW|$07FF ; bit0 = frozen + and #$01 + bne ss_sa1_cap_go + inx + cpx #$2000 + bne ss_sa1_cap_wait +ss_sa1_cap_go: + rep #$20 : .al + ldx #$0000 +ss_sa1_cap_iram: + lda @$003000, x + sta @SS_SA1_IRAM_SCRATCH, x + inx : inx + cpx #$0800 + bne ss_sa1_cap_iram + ldx #$0000 +ss_sa1_cap_bwram: + lda @$400000, x + sta @SS_SA1_BWRAM_SCRATCH, x + inx : inx + cpx #$8000 + bne ss_sa1_cap_bwram + sep #$20 : .as + ldx #$0000 +ss_sa1_cap_regs: + lda @SS_SA1_WINDOW, x + sta @SS_SA1_REGS_SCRATCH, x + inx + cpx #$0100 + bne ss_sa1_cap_regs +ss_sa1_cap_done: + + ; --- GSU capture: state block, instruction cache through its native $00:3100 + ; image and the 32 KB of cart RAM. The freeze is run-to-stop, so the ack + ; only comes once an in-flight GSU program has finished. + sep #$20 : .as + lda @SS_GSU_GATE + bne + ; long block: short branch out of range + jmp ss_gsu_cap_done ++ ldx #$0000 +ss_gsu_cap_wait: + lda @SS_GSU_WINDOW|$00FE ; bit0 = frozen + and #$01 + bne ss_gsu_cap_go + inx + cpx #$4000 + bne ss_gsu_cap_wait +ss_gsu_cap_go: + ldx #$0000 +ss_gsu_cap_regs: + lda @SS_GSU_WINDOW, x + sta @SS_GSU_REGS_SCRATCH, x + inx + cpx #$0100 + bne ss_gsu_cap_regs + ldx #$0000 +ss_gsu_cap_cache: + lda @$003100, x + sta @SS_GSU_CACHE_SCRATCH, x + inx + cpx #$0200 + bne ss_gsu_cap_cache + rep #$20 : .al + ldx #$0000 +ss_gsu_cap_ram: + lda @$700000, x + sta @SS_GSU_RAM_SCRATCH, x + inx : inx + cpx #$8000 + bne ss_gsu_cap_ram + sep #$20 : .as +ss_gsu_cap_done: + + ; --- CX4 capture (savestate_cx4.a65) --- + jsl @ss_cx4_capture + .as : .xl + + ; --- S-DD1 capture: the six config bytes that carry state, $4800/$4801 and the + ; four bank maps ($4802/$4803 read $00). The decompressor is never + ; mid-transfer at an NMI boundary, so there is nothing else to take. + lda @SS_SDD1_GATE + beq ss_sdd1_cap_done + lda @$004800 + sta @SS_SDD1_SCRATCH + lda @$004801 + sta @SS_SDD1_SCRATCH+1 + lda @$004804 + sta @SS_SDD1_SCRATCH+2 + lda @$004805 + sta @SS_SDD1_SCRATCH+3 + lda @$004806 + sta @SS_SDD1_SCRATCH+4 + lda @$004807 + sta @SS_SDD1_SCRATCH+5 +ss_sdd1_cap_done: + + ; --- OBC1 capture: no FSM to halt, just the SNES-visible $7800-$7FFF window --- + lda @SS_OBC1_GATE + beq ss_obc1_cap_done + rep #$20 : .al + ldx #$0000 +ss_obc1_cap: + lda @$007800, x + sta @SS_OBC1_SCRATCH, x + inx : inx + cpx #$0800 + bne ss_obc1_cap + .as ; fall through: the rep below re-enters .al +ss_obc1_cap_done: + rep #$20 : .al tsc sta @SRAM_SAVED_SP @@ -502,7 +863,23 @@ load_state bne load_state_jump_exit inc sta @CS_STATE - + + ; --- freeze the coprocessor, as on the save path: the restore blocks below + ; need a quiesced core. Released at the game resume, never here. + lda @SS_SA1_GATE + beq ss_sa1_ld_early_done + jsr ss_sa1_probe_halt ; probes the $E8 window before halting +ss_sa1_ld_early_done: + lda @SS_GSU_GATE + beq ss_gsu_ld_early_done + lda #$01 + sta @SS_GSU_WINDOW|$00FE +ss_gsu_ld_early_done: + jsl @ss_cx4_early + .as : .xl + lda #$01 + sta @CS_SA1_LOAD ; LOAD path: run the restore blocks + ; disable interrupts lda @$F90700|$0000 and #$01 @@ -533,7 +910,7 @@ load_return ; wait delay frames sep #$20 : .as - lda CS_DELAY + lda @CS_DELAY ; @: an absolute read would hit game WRAM $00:100E ss_frameloop - bit $4212 : bpl - - bit $4212 : bmi - @@ -561,8 +938,188 @@ load_dma_regs_start ldy #$0000 bra - + ; --- DSP1-4 restore: halt, wait for the ack, write the snapshot back through + ; the window, unhalt LAST (any earlier and the core runs over the restored + ; registers). ++ lda @SS_DSP_GATE + beq ss_dsp_rst_done + lda #$01 + sta @SS_DSP_WINDOW|$07FF ; request halt + ldx #$0000 +ss_dsp_rst_wait: + lda @SS_DSP_WINDOW|$07FF ; bit0 = frozen + and #$01 + bne ss_dsp_rst_go + inx + cpx #$2000 + bne ss_dsp_rst_wait +ss_dsp_rst_go: + ldx #$0000 +ss_dsp_rst_loop: + lda @SS_DSP_SCRATCH, x + sta @SS_DSP_WINDOW, x + inx + cpx #$0700 + bne ss_dsp_rst_loop + lda #$00 + sta @SS_DSP_WINDOW|$07FF ; release halt -> DSP resumes restored state +ss_dsp_rst_done: + + ; --- SA-1 restore: BW-RAM and IRAM first, then the state block ascending so + ; the $F0 normalize byte lands near the end. Left frozen for the resume. + sep #$20 : .as + lda @SS_SA1_GATE + bne + ; block > 127 bytes: short branch is out of range + jmp ss_sa1_rst_done ++ lda @CS_SA1_LOAD + bne + + jmp ss_sa1_rst_done ; SAVE path: the chip never left the frozen + ; capture state, so there is nothing to undo + ; Stale-scratch guard: a .state written without an SA-1 capture still carries + ; whatever sat in the F4 scratch, and the file persists F0-F4 wholesale. A real + ; capture copies offset $FF, the read-only $5A magic, so the snapshot proves its + ; own provenance. On failure skip the write-back but leave the gate alone -- + ; the release is gated on it, and zeroing it would strand the SA-1 frozen. ++ lda @SS_SA1_REGS_SCRATCH+$FF + cmp #$5A + beq + + jmp ss_sa1_rst_done ++ lda #$01 + sta @SS_SA1_WINDOW|$07FF ; request halt + ldx #$0000 +ss_sa1_rst_wait: + lda @SS_SA1_WINDOW|$07FF ; bit0 = frozen + and #$01 + bne ss_sa1_rst_go + inx + cpx #$2000 + bne ss_sa1_rst_wait +ss_sa1_rst_go: + rep #$20 : .al + ldx #$0000 +ss_sa1_rst_bwram: + lda @SS_SA1_BWRAM_SCRATCH, x + sta @$400000, x + inx : inx + cpx #$8000 + bne ss_sa1_rst_bwram + ldx #$0000 +ss_sa1_rst_iram: + lda @SS_SA1_IRAM_SCRATCH, x + sta @$003000, x + inx : inx + cpx #$0800 + bne ss_sa1_rst_iram + sep #$20 : .as + ldx #$0000 +ss_sa1_rst_regs: + lda @SS_SA1_REGS_SCRATCH, x + sta @SS_SA1_WINDOW, x + inx + cpx #$0100 + bne ss_sa1_rst_regs +ss_sa1_rst_done: + + ; --- GSU restore: cache image and cart RAM first, then the state block + ; ascending, so the cache-valid mask lands after the cache image's + ; line-valid side effects and the $F0 normalize near the end. + sep #$20 : .as + lda @SS_GSU_GATE + bne + ; long block: short branch out of range + jmp ss_gsu_rst_done ++ lda @CS_SA1_LOAD + bne + + jmp ss_gsu_rst_done ; SAVE path: no write-back ++ lda #$01 + sta @SS_GSU_WINDOW|$00FE ; ensure halt requested + ldx #$0000 +ss_gsu_rst_wait: + lda @SS_GSU_WINDOW|$00FE ; bit0 = frozen + and #$01 + bne ss_gsu_rst_go + inx + cpx #$4000 + bne ss_gsu_rst_wait +ss_gsu_rst_go: + ldx #$0000 +ss_gsu_rst_cache: + lda @SS_GSU_CACHE_SCRATCH, x + sta @$003100, x + inx + cpx #$0200 + bne ss_gsu_rst_cache + rep #$20 : .al + ldx #$0000 +ss_gsu_rst_ram: + lda @SS_GSU_RAM_SCRATCH, x + sta @$700000, x + inx : inx + cpx #$8000 + bne ss_gsu_rst_ram + sep #$20 : .as + ldx #$0000 +ss_gsu_rst_regs: + lda @SS_GSU_REGS_SCRATCH, x + sta @SS_GSU_WINDOW, x + inx + cpx #$0100 + bne ss_gsu_rst_regs +ss_gsu_rst_done: + + ; --- CX4 restore (savestate_cx4.a65; LOAD path only, tested inside) --- + jsl @ss_cx4_restore + .as : .xl + + ; --- OBC1 restore: no halt, but order matters. Writing $7FF0-$7FF4 also + ; scribbles obc_lower/obc_upper at the OLD register indices, so write the 8 + ; registers first and then rewrite the linear window, whose writes address + ; both arrays directly and overwrite the scribble. + sep #$20 : .as + lda @SS_OBC1_GATE + beq ss_obc1_rst_done + lda @CS_SA1_LOAD + beq ss_obc1_rst_done ; SAVE path: no write-back + ldx #$07F0 +ss_obc1_rst_regs: + lda @SS_OBC1_SCRATCH, x + sta @$007800, x + inx + cpx #$07F8 + bne ss_obc1_rst_regs + rep #$20 : .al + ldx #$0000 +ss_obc1_rst_win: + lda @SS_OBC1_SCRATCH, x + sta @$007800, x + inx : inx + cpx #$07F0 + bne ss_obc1_rst_win + .as ; register_restore_return does its own sep +ss_obc1_rst_done: + + ; --- S-DD1 restore: the four bank maps and the channel-enable mask only. + ; $4801 is deliberately NOT rewritten: a write there arms the decompressor + ; and would inject a phantom decompression on the next $420B. The game's + ; next real $4801 reloads everything anyway. + sep #$20 : .as + lda @SS_SDD1_GATE + beq ss_sdd1_rst_done + lda @CS_SA1_LOAD + beq ss_sdd1_rst_done ; SAVE path: no write-back + lda @SS_SDD1_SCRATCH+2 + sta @$004804 + lda @SS_SDD1_SCRATCH+3 + sta @$004805 + lda @SS_SDD1_SCRATCH+4 + sta @$004806 + lda @SS_SDD1_SCRATCH+5 + sta @$004807 + lda @SS_SDD1_SCRATCH + sta @$004800 +ss_sdd1_rst_done: + ; Restore registers and return. -+ jmp register_restore_return + jmp register_restore_return vm ; Data format: xx xx yy yy @@ -608,6 +1165,30 @@ vm_done ; code address to return to. tyx jmp ($0002,x) + +; ---- ss_sa1_probe_halt: prove the window, then request the halt -------------- +; SS_SA1_GATE only says the firmware saw an SA-1 core, not that the core on +; the card implements the $E8 window. Against a stale one $E8 falls through +; to the IS_PATCH identity window (plain PSRAM), the halt request reads back, +; the frozen spin passes immediately and the handler would capture garbage. +; Discriminate by writing: offset $FF is the read-only $5A magic, so on a real +; core the $A5 is swallowed and the read still yields $5A. +; On failure zero the gate: every SA-1 touchpoint tests it first, including +; the release, so nothing is left frozen. A 8-bit in and out. +ss_sa1_probe_halt: + .as + lda #$A5 + sta @SS_SA1_WINDOW|$00FF ; inert on a real core (read-only, not frozen yet) + lda @SS_SA1_WINDOW|$00FF + cmp #$5A ; magic intact -> window present + bne ss_sa1_probe_bad + lda #$01 + sta @SS_SA1_WINDOW|$07FF ; window present -> request the early halt + rts +ss_sa1_probe_bad: + lda #$00 + sta @SS_SA1_GATE ; stale/absent core -> degrade, never guess + rts .byt "SAVESTATE_END" ; print "Savestate Bank Ending at: ", pc diff --git a/snes/savestate.i65 b/snes/savestate.i65 index 8bcb4860..1a26a1d9 100644 --- a/snes/savestate.i65 +++ b/snes/savestate.i65 @@ -16,8 +16,56 @@ #define CS_SLOT $FE100F #define CS_SLOT_INPUT $FE1010 #define CS_CTRL $FE1012 +// Nonzero = run the coprocessor restore blocks (LOAD path only). Uses the free +// byte after CS_CTRL: the fix code from savestate_fixes.yml is emitted from +// CS_FIXES upward, so nothing may be allocated at or above that. +#define CS_SA1_LOAD $FE1013 #define CS_FIXES $FE1014 +// Coprocessor savestate support. Each SS_*_GATE byte is armed by the firmware on +// every game load: 0 means the running core has no capture/restore for that chip +// and the handler skips it, so an old core on the card degrades to a save without +// chip state instead of capturing garbage. Lockstep with src/savestate.h. +#define SS_DSP_GATE $FF0712 +#define SS_SA1_GATE $FF0713 +#define SS_GSU_GATE $FF0714 +#define SS_OBC1_GATE $FF0718 +#define SS_SDD1_GATE $FF0719 +#define SS_CX4_GATE $FF071F +// Nonzero = the NMI resume wait timed out (ss_wait_bail); bit7 mirrors the NMI +// enable of the restored $4200. Cleared by ss_init on every game load. +#define SS_DIAG_BAIL $FF0C00 + +// Scan windows. All four live at bank $E8 and are decoded only while the handler +// holds the snescmd unlock, so they are invisible to the running game. +#define SS_DSP_WINDOW $E80000 // uPD7725: $000-$1FF data RAM, $600-$6FF register + // file, $7FF halt ctl (w bit0=halt, r bit0=halted) +#define SS_SA1_WINDOW $E80000 // SA-1: $00-$FF state block ($F0 = normalize, + // $FF = magic $5A), $7FF halt ctl +#define SS_GSU_WINDOW $E80000 // GSU: $00-$3F native MMIO semantics, $40+ internal + // state, $F0 normalize, $FE halt ctl, $FF magic $5B +#define SS_CX4_WINDOW $E80000 // CX4: $00-$27 state block, $E0-$E2 idb staging, + // $E3 commit, $E4 readback index, $F0 normalize, + // $FE halt ctl, $FF magic $5C + +// PSRAM scratches, all inside the F0..F4 region the .state file persists, so the +// chip state travels with the save. They overlap on purpose: a cartridge never +// carries two of these coprocessors. +#define SS_DSP_SCRATCH $F47000 // 0x700 B: data RAM + register file +#define SS_SA1_IRAM_SCRATCH $F47000 // 2 KB : SA-1 IRAM ($00:3000) +#define SS_SA1_REGS_SCRATCH $F47800 // 256 B : SA-1 state block +#define SS_SA1_BWRAM_SCRATCH $F48000 // 32 KB : SA-1 BW-RAM ($40:0000) +#define SS_GSU_CACHE_SCRATCH $F47000 // 512 B : GSU instruction cache ($00:3100) +#define SS_GSU_REGS_SCRATCH $F47800 // 256 B : GSU state block +#define SS_GSU_RAM_SCRATCH $F48000 // 32 KB : GSU cart RAM ($70:0000) +#define SS_OBC1_SCRATCH $F47000 // 2 KB : OBC1 window ($00:7800-$7FFF) +#define SS_SDD1_SCRATCH $F47000 // 6 B : S-DD1 config ($4800/01/04-07) +#define SS_CX4_DATRAM_SCRATCH $F47000 // 3 KB : CX4 data RAM ($00:6000-$6BFF) +#define SS_CX4_NAT_SCRATCH $F47C00 // 112 B : CX4 native page, indexed by the + // low byte of $00:7Fxx ($40..$AF) +#define SS_CX4_WIN_SCRATCH $F47CC0 // 64 B : mirror of CX4 window $00-$3F +#define SS_CX4_CORE_SCRATCH $F47D00 // 16 B : CX4 24-bit core registers + #define SS_CODE $FC0000 ; should be unused #define SS_DATA $FC2000 ; should be unused #define SS_RETURN $002C04 diff --git a/snes/savestate_cx4.a65 b/snes/savestate_cx4.a65 new file mode 100644 index 00000000..d6005353 --- /dev/null +++ b/snes/savestate_cx4.a65 @@ -0,0 +1,332 @@ +; CX4 in-game savestate: capture and restore of the coprocessor state. Linked +; into the same bank as savestate.a65 and reached with jsl, so each of the six +; call sites there costs 4 bytes. +; +; Run-to-stop model: the halt is requested at the same instant the SNES CPU is +; cut, and the FPGA lets the CX4 run to its next clean boundary before reporting +; frozen (a ~46 ms timeout latches "dirty" instead of hanging). State is reached +; through the $E8:00xx scan window plus the native $00:6000-$7FFF space, which +; stays bus-visible while frozen. The program cache is not captured: it is +; replayed on restore through the native MMIO, whose FSM is not pause-gated. The +; core is left frozen after both paths; ss_cx4_release runs at the game resume. +; +; The file also carries ss_wait_bail, the watchdog for the NMI resume wait. + +.link page $c0 +#include "memmap.i65" +#include "savestate.i65" + +; --- early halt request (save and load paths); the capture re-asserts it --- +ss_cx4_early: + .as : .xl + lda @SS_CX4_GATE + beq ss_cx4_early_x + lda #$01 + sta @SS_CX4_WINDOW|$00FE +ss_cx4_early_x: + rtl + +; --- halt release, from both resume paths, so the CX4 wakes with the SNES --- +ss_cx4_release: + .as : .xl + lda @SS_CX4_GATE + beq ss_cx4_rel_x + lda #$00 + sta @SS_CX4_WINDOW|$00FE +ss_cx4_rel_x: + rtl + +; --- capture. $F0 is written BEFORE any read: a dirty freeze would otherwise +; keep hammering the bus during the copy, and this way the image taken is +; already the boundary the restore reproduces. +ss_cx4_capture: + .as : .xl + lda @SS_CX4_GATE + bne ss_cx4_cap_go + rtl +ss_cx4_cap_go: + lda #$01 + sta @SS_CX4_WINDOW|$00FE ; ensure halt requested + jsr ss_cx4_wait_frozen ; C=1 -> escape counter blew + php ; keep the carry across the copy + lda #$01 + sta @SS_CX4_WINDOW|$00F0 ; NORMALIZE-FIRST + ; --- core registers via the $E4 readback mux -> $00-$02 --- + lda #$00 : sta @SS_CX4_WINDOW|$00E4 + lda @SS_CX4_WINDOW|$0000 : sta @SS_CX4_CORE_SCRATCH+0 + lda @SS_CX4_WINDOW|$0001 : sta @SS_CX4_CORE_SCRATCH+1 + lda @SS_CX4_WINDOW|$0002 : sta @SS_CX4_CORE_SCRATCH+2 + lda #$08 : sta @SS_CX4_WINDOW|$00E4 + lda @SS_CX4_WINDOW|$0000 : sta @SS_CX4_CORE_SCRATCH+3 + lda @SS_CX4_WINDOW|$0001 : sta @SS_CX4_CORE_SCRATCH+4 + lda @SS_CX4_WINDOW|$0002 : sta @SS_CX4_CORE_SCRATCH+5 + lda #$0C : sta @SS_CX4_WINDOW|$00E4 + lda @SS_CX4_WINDOW|$0000 : sta @SS_CX4_CORE_SCRATCH+6 + lda @SS_CX4_WINDOW|$0001 : sta @SS_CX4_CORE_SCRATCH+7 + lda @SS_CX4_WINDOW|$0002 : sta @SS_CX4_CORE_SCRATCH+8 + lda #$13 : sta @SS_CX4_WINDOW|$00E4 + lda @SS_CX4_WINDOW|$0000 : sta @SS_CX4_CORE_SCRATCH+9 + lda @SS_CX4_WINDOW|$0001 : sta @SS_CX4_CORE_SCRATCH+10 + lda @SS_CX4_WINDOW|$0002 : sta @SS_CX4_CORE_SCRATCH+11 + lda #$1C : sta @SS_CX4_WINDOW|$00E4 + lda @SS_CX4_WINDOW|$0000 : sta @SS_CX4_CORE_SCRATCH+12 + lda @SS_CX4_WINDOW|$0001 : sta @SS_CX4_CORE_SCRATCH+13 + lda @SS_CX4_WINDOW|$0002 : sta @SS_CX4_CORE_SCRATCH+14 + ; --- linear image of window $04-$27 (36 B) --- + ldx #$0004 +ss_cx4_cap_win: + lda @SS_CX4_WINDOW, x + sta @SS_CX4_WIN_SCRATCH, x + inx + cpx #$0028 + bne ss_cx4_cap_win + ; --- flag "captured without a clean freeze" in bit4 of the diag byte --- + plp + bcc ss_cx4_cap_nat + lda @SS_CX4_WIN_SCRATCH+$1A + ora #$10 ; bit4 = dirty + sta @SS_CX4_WIN_SCRATCH+$1A +ss_cx4_cap_nat: + ; --- native page $00:7F40-$7FAF (112 B: MMIO + status + vectors + GPR) --- + ldx #$0040 +ss_cx4_cap_nl: + lda @$007F00, x + sta @SS_CX4_NAT_SCRATCH, x + inx + cpx #$00B0 + bne ss_cx4_cap_nl + ; --- data RAM 3 KB, 16-bit words --- + rep #$20 : .al + ldx #$0000 +ss_cx4_cap_dr: + lda @$006000, x + sta @SS_CX4_DATRAM_SCRATCH, x + inx : inx + cpx #$0C00 + bne ss_cx4_cap_dr + sep #$20 : .as + ; left frozen on purpose: released at the game resume, not here + rtl + +; --- restore, LOAD path only. Order matters: cache replay first (the fill +; consumes pgmoff/pgmpage and writes cachetag/cachevalid), pagemem before the +; final pgmpage (the $7F4C latch copies the current one), DMA before the data +; RAM (arming $7F47 fires a transfer that dirties one byte, neutralised to +; dmalen=$0001 -- $0000 would underflow to 65536), then GPR, which only accepts +; writes while frozen, the window fixups, the core registers and $F0 last. +; Every wait here has an escape counter. +ss_cx4_restore: + .as : .xl + lda @SS_CX4_GATE + bne ss_cx4_rst_g1 + rtl +ss_cx4_rst_g1: + lda @CS_SA1_LOAD + bne ss_cx4_rst_go + rtl ; SAVE path: no write-back +ss_cx4_rst_go: + lda #$01 : sta @SS_CX4_WINDOW|$00FE ; ensure halt requested + jsr ss_cx4_wait_frozen ; carry ignored: proceed either way + lda #$01 : sta @SS_CX4_WINDOW|$00F0 ; NORMALIZE-FIRST (kills a stuck + ; cpu_cache_en / BUSY_CPU / + ; cx4_cpu_datram_we) + ; ---- 1. program-cache replay through the NATIVE MMIO ---- + lda @SS_CX4_NAT_SCRATCH+$49 : sta @$007F49 + lda @SS_CX4_NAT_SCRATCH+$4A : sta @$007F4A + lda @SS_CX4_NAT_SCRATCH+$4B : sta @$007F4B + lda #$00 : sta @SS_CX4_WINDOW|$0017 ; cachevalid = 00 -> force the fills + lda @SS_CX4_WIN_SCRATCH+$17 + and #$01 + beq ss_cx4_rst_p1 + lda @SS_CX4_WIN_SCRATCH+$0C : sta @$007F4D + lda @SS_CX4_WIN_SCRATCH+$0D : sta @$007F4E + lda #$00 : sta @$007F48 + jsr ss_cx4_busywait +ss_cx4_rst_p1: + lda @SS_CX4_WIN_SCRATCH+$17 + and #$02 + beq ss_cx4_rst_pm + lda @SS_CX4_WIN_SCRATCH+$0E : sta @$007F4D + lda @SS_CX4_WIN_SCRATCH+$0F : sta @$007F4E + lda #$01 : sta @$007F48 + jsr ss_cx4_busywait +ss_cx4_rst_pm: + ; ---- 2. pagemem[] via the NATIVE $7F4C side effect ---- + lda @SS_CX4_WIN_SCRATCH+$10 : sta @$007F4D + lda @SS_CX4_WIN_SCRATCH+$11 : sta @$007F4E + lda #$01 : sta @$007F4C + lda @SS_CX4_WIN_SCRATCH+$12 : sta @$007F4D + lda @SS_CX4_WIN_SCRATCH+$13 : sta @$007F4E + lda #$02 : sta @$007F4C + ; ---- 3. DMA with the trigger neutralised ---- + lda @SS_CX4_NAT_SCRATCH+$40 : sta @$007F40 + lda @SS_CX4_NAT_SCRATCH+$41 : sta @$007F41 + lda @SS_CX4_NAT_SCRATCH+$42 : sta @$007F42 + lda #$01 : sta @$007F43 ; dmalen = $0001 (NEVER $0000: the + lda #$00 : sta @$007F44 ; counter underflows to 65536 bytes) + lda @SS_CX4_NAT_SCRATCH+$45 : sta @$007F45 + lda @SS_CX4_NAT_SCRATCH+$46 : sta @$007F46 + lda @SS_CX4_NAT_SCRATCH+$47 : sta @$007F47 ; fires the 1-byte DMA + jsr ss_cx4_busywait + lda @SS_CX4_NAT_SCRATCH+$43 : sta @$007F43 + lda @SS_CX4_NAT_SCRATCH+$44 : sta @$007F44 + ; ---- 4. data RAM 3 KB (overwrites the byte the DMA dirtied) ---- + rep #$20 : .al + ldx #$0000 +ss_cx4_rst_dr: + lda @SS_CX4_DATRAM_SCRATCH, x + sta @$006000, x + inx : inx + cpx #$0C00 + bne ss_cx4_rst_dr + sep #$20 : .as + ; ---- 5. vectors + GPR ($7F60-$7FAF; GPR only accepts writes while frozen) ---- + ldx #$0060 +ss_cx4_rst_vg: + lda @SS_CX4_NAT_SCRATCH, x + sta @$007F00, x + inx + cpx #$00B0 + bne ss_cx4_rst_vg + ; ---- 6. remaining MMIO + final pgmpage/pc ---- + ; Writing $7F4F pulses cpu_go_en_r for one cycle, but both of its + ; consumers sit under cx4_cpu_en == 0 while frozen, so the pulse dies + ; with no effect; the cachepage side effect is undone by step 7 ($18). + ; That is what saves a window offset for the program counter. + lda @SS_CX4_NAT_SCRATCH+$50 : sta @$007F50 + lda @SS_CX4_NAT_SCRATCH+$51 : sta @$007F51 + lda @SS_CX4_NAT_SCRATCH+$52 : sta @$007F52 + lda @SS_CX4_NAT_SCRATCH+$4D : sta @$007F4D + lda @SS_CX4_NAT_SCRATCH+$4E : sta @$007F4E + lda @SS_CX4_NAT_SCRATCH+$4F : sta @$007F4F + ; ---- 7. window-only fixups ---- + ldx #$0014 +ss_cx4_rst_sc: + lda @SS_CX4_WIN_SCRATCH, x ; $14 page_stack, $15 sp, $16 flags, + sta @SS_CX4_WINDOW, x ; $17 cachevalid, $18 cachepage, $19 savepage + inx + cpx #$001A + bne ss_cx4_rst_sc + ldx #$0020 +ss_cx4_rst_ps: + lda @SS_CX4_WIN_SCRATCH, x ; $20-$27 cpu_pc_stack[0..7] + sta @SS_CX4_WINDOW, x + inx + cpx #$0028 + bne ss_cx4_rst_ps + ; ---- 8. core registers (UNROLLED: no txa/adc/tax) ---- + ; 8a. multiplier -- cpu_a is the vehicle for mul_a + lda @SS_CX4_WIN_SCRATCH+$04 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_WIN_SCRATCH+$05 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_WIN_SCRATCH+$06 : sta @SS_CX4_WINDOW|$00E2 + lda #$00 : sta @SS_CX4_WINDOW|$00E3 ; commit 0: cpu_a = mul_a + lda @SS_CX4_WIN_SCRATCH+$08 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_WIN_SCRATCH+$09 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_WIN_SCRATCH+$0A : sta @SS_CX4_WINDOW|$00E2 + lda #$05 : sta @SS_CX4_WINDOW|$00E3 ; commit 5: mul_a<=cpu_a, mul_b<=idb + ; 8b. romdata / ramdata / busaddr / ramaddr + lda @SS_CX4_CORE_SCRATCH+3 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_CORE_SCRATCH+4 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_CORE_SCRATCH+5 : sta @SS_CX4_WINDOW|$00E2 + lda #$01 : sta @SS_CX4_WINDOW|$00E3 + lda @SS_CX4_CORE_SCRATCH+6 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_CORE_SCRATCH+7 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_CORE_SCRATCH+8 : sta @SS_CX4_WINDOW|$00E2 + lda #$02 : sta @SS_CX4_WINDOW|$00E3 + lda @SS_CX4_CORE_SCRATCH+9 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_CORE_SCRATCH+10 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_CORE_SCRATCH+11 : sta @SS_CX4_WINDOW|$00E2 + lda #$03 : sta @SS_CX4_WINDOW|$00E3 + lda @SS_CX4_CORE_SCRATCH+12 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_CORE_SCRATCH+13 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_CORE_SCRATCH+14 : sta @SS_CX4_WINDOW|$00E2 + lda #$04 : sta @SS_CX4_WINDOW|$00E3 + ; 8c. the REAL cpu_a last (step 8a used it as the mul_a vehicle) + lda @SS_CX4_CORE_SCRATCH+0 : sta @SS_CX4_WINDOW|$00E0 + lda @SS_CX4_CORE_SCRATCH+1 : sta @SS_CX4_WINDOW|$00E1 + lda @SS_CX4_CORE_SCRATCH+2 : sta @SS_CX4_WINDOW|$00E2 + lda #$00 : sta @SS_CX4_WINDOW|$00E3 + ; ---- 9. final normalize ---- + lda #$01 : sta @SS_CX4_WINDOW|$00F0 + rtl ; stays FROZEN until return_align + +; --- bounded wait for the freeze ack ($FE bit0). Exits with C=1 on timeout. --- +ss_cx4_wait_frozen: + .as : .xl + ldx #$0000 +ss_cx4_wf_l: + lda @SS_CX4_WINDOW|$00FE + and #$01 + bne ss_cx4_wf_ok + inx + cpx #$4000 + bne ss_cx4_wf_l + sec + rts +ss_cx4_wf_ok: + clc + rts + +; --- bounded wait for cx4_active ($00:7F53 bit6) to drop ------------------- +ss_cx4_busywait: + .as : .xl + ldx #$0000 +ss_cx4_bw_l: + lda @$007F53 + and #$40 + beq ss_cx4_bw_out + inx + cpx #$2000 + bne ss_cx4_bw_l +ss_cx4_bw_out: + rts + + +; --- ss_wait_bail: the NMI resume wait timed out ------------------------------ +; Only the FPGA can deliver the nested entry the wait needs, and the wait runs with +; the PPU force-blanked and the coprocessor frozen, so a missed re-entry would need +; a MENU_RESET. Build the state the successful path would have built instead, so +; the caller falls into the shared release/restore tail: breadcrumb, interrupts +; off, land on a vblank edge (the caller exits into the game's real NMI vector, and +; entering that ISR mid-frame would run its vblank DMA queue during display), ack +; the NMI, and set CS_STATE to the post-resume cooldown -- leaving it at 1 would +; make the next hook entry take ss_irq_inc outside a nested interrupt. Both halves +; of the edge search are bounded. A 8-bit, X/Y 16-bit. +ss_wait_bail: + .as : .xl + ; Breadcrumb: nonzero = "the resume wait timed out", bit7 = the NMI enable of + ; the RESTORED $4200 (0 there means the wait was unwinnable by construction -- + ; see the ora #$80 note at return_enable_nmi), bit6 = this marker. ss_init + ; clears the byte on every game load. + lda @SRAM_OTH_BANK + ora #$40 + sta @SS_DIAG_BAIL + ; Interrupts off for the edge search (keep auto-joypad exactly as the state had + ; it). Without this the NMI we forced on at return_enable_nmi could fire in the + ; middle of the tail below, and with CS_STATE already at 2 that entry would exit + ; through nmi_exit -- dropping snescmd_unlock, i.e. the $C0-FF identity window + ; this very code is executing from. + lda @SRAM_OTH_BANK + and #$01 + sta @$004200 + ; Wait out the rest of the current vblank ... + ldx #$4000 +ss_wb_active: + lda @$004212 + bpl ss_wb_edge + dex + bne ss_wb_active + bra ss_wb_ack + ; ... then for the start of the next one. +ss_wb_edge: + ldx #$4000 +ss_wb_vbl: + lda @$004212 + bmi ss_wb_ack + dex + bne ss_wb_vbl +ss_wb_ack: + lda @$004210 ; ack the pending NMI flag + lda #$02 ; post-save/load cooldown: ss_start_skip + sta @CS_STATE ; drains 2..15,0 + rtl diff --git a/src/cheat.c b/src/cheat.c index 5d2848f0..457e686e 100644 --- a/src/cheat.c +++ b/src/cheat.c @@ -2,6 +2,7 @@ #include "fileops.h" #include "uart.h" #include "memory.h" +#include "fpga.h" #include "fpga_spi.h" #include "snes.h" #include "cheat.h" @@ -76,6 +77,8 @@ void cheat_program_single(cheat_patch_record_t *cheat) { /* apply cheat to FPGA / NMI hook */ if(is_wram_cheat) { cheat_program_ram_cheat(wram_index++, cheat); + } else if(cheat_rom_psram_mode()) { + /* no comparator slots here: written into the image at deassert_reset() */ } else if(rom_index < 6) { enable_mask |= (1 << rom_index); cheat_program_rom_cheat(rom_index++, cheat); @@ -101,6 +104,82 @@ void cheat_program_ram_cheat(int index, cheat_patch_record_t *cheat) { printf("RAM cheat #%d: %02x%04x %02x\n", index, cheat->fields.patchbank, cheat->fields.patchaddr, cheat->fields.patchvalue); } +/* ROM cheats without FPGA comparators: the Mk.II GSU and CX4 cores had to drop + theirs to fit the savestate machinery, so there a ROM code is written straight + into the image in PSRAM, with the original byte kept in the record's spare tail. + On a coprocessor core that also covers address mirrors and the chip's own + fetches, which never went through the comparators. The translation below uses + each core's static map, so a game that rebanks a patched region at runtime would + diverge; neither CX4 game does and the GSU map is static. */ + +uint8_t cheat_rom_psram_mode(void) { +#ifdef CONFIG_MK2 + return romprops.fpga_conf == FPGA_GSU + || romprops.fpga_conf == FPGA_CX4; +#else + return 0; +#endif +} + +/* Bus address -> offset in the loaded image, or -1 when not ROM-backed. Mirrors + collapse through the ROM size mask, as they do in the FPGA. */ +static int32_t cheat_rom_code_offset(uint32_t addr) { + uint32_t bank = (addr >> 16) & 0xff; + uint32_t ofs = addr & 0xffff; + uint32_t off; + uint32_t mask = romprops.romsize_bytes ? (romprops.romsize_bytes - 1) : 0x3fffff; + if(romprops.fpga_conf == FPGA_GSU) { /* GSU hybrid Lo/Hi map (address.v) */ + if(bank & 0x40) { + /* $40-5F/$C0-DF:0000-FFFF -> SNES_ADDR[21:0]; $60-7D/$E0-FF are SAVERAM */ + if((bank & 0x60) == 0x60) return -1; + off = addr & 0x3fffff; + } else { + /* $00-3F/$80-BF -> SNES_ADDR[14:0] (both halves); $6000-7FFF is SAVERAM */ + if((ofs & 0xe000) == 0x6000) return -1; + off = ((bank & 0x7f) << 15) | (ofs & 0x7fff); + } + } else if(romprops.fpga_conf == FPGA_CX4) { /* CX4: plain LoROM (cx4/address.v) */ + /* offsets < $8000 are never ROM here: CX4 MMIO and SAVERAM live there */ + if(!(ofs & 0x8000)) return -1; + off = ((bank & 0x7f) << 15) | (ofs & 0x7fff); + } else return -1; + return (int32_t)(off & mask); +} + +/* With the codes in the image rather than in comparators, the in-game cheat toggle + no longer affects ROM codes: they follow CFG.enable_cheats as of game load. + WRAM codes are unaffected. */ +void cheat_rom_psram_apply(void) { + if(!cheat_rom_psram_mode()) return; + int count = sram_readshort(SRAM_NUM_CHEATS); + if(count < 0) count = 0; + if(count > 2048) count = 2048; /* records live in banks D0-DF: 2048 slots */ + for(int i = 0; i < count; i++) { + uint32_t rec = SRAM_CHEAT_ADDR + 512u * (uint32_t)i; + uint8_t flags = sram_readbyte(rec); + uint8_t np = sram_readbyte(rec + 255); + if(np > CHEAT_NUM_CODES_PER_CHEAT) np = CHEAT_NUM_CODES_PER_CHEAT; + uint8_t want = CFG.enable_cheats && (flags & CHEAT_FLAG_ENABLE); + for(uint8_t c = 0; c < np; c++) { + uint32_t code; + sram_readblock(&code, rec + 256 + 4u * c, 4); + if(cheat_is_wram_cheat(code)) continue; /* WRAM codes stay hook-based */ + int32_t off = cheat_rom_code_offset(code >> 8); + if(off < 0) continue; + uint32_t tgt = (uint32_t)off; /* the image loads at PSRAM 0 */ + uint8_t applied = sram_readbyte(rec + CHEAT_REC_APPLIED_OFS + c); + if(want && applied != 1) { + sram_writebyte(sram_readbyte(tgt), rec + CHEAT_REC_ORIG_OFS + c); + sram_writebyte(code & 0xff, tgt); + sram_writebyte(1, rec + CHEAT_REC_APPLIED_OFS + c); + } else if(!want && applied == 1) { + sram_writebyte(sram_readbyte(rec + CHEAT_REC_ORIG_OFS + c), tgt); + sram_writebyte(0, rec + CHEAT_REC_APPLIED_OFS + c); + } + } + } +} + void cheat_load_to_menu(int index, cheat_record_t *cheat) { uint32_t offset = SRAM_CHEAT_ADDR + 512 * index; sram_writeblock(cheat, offset, sizeof(cheat_record_t)); @@ -204,6 +283,15 @@ void cheat_yaml_load(uint8_t* romfilename) { } /* a single cheat + codes have been read, put in RAM */ cheat_load_to_menu(cheat_idx, &cheat); + /* The image was just (re)streamed, so nothing in it is patched: clear the + per-code "applied" flags or the apply pass would skip the write and later + restore a stale original over live data. */ + if(cheat_rom_psram_mode()) { + uint8_t zeroes[CHEAT_NUM_CODES_PER_CHEAT]; + memset(zeroes, 0, sizeof(zeroes)); + sram_writeblock(zeroes, SRAM_CHEAT_ADDR + 512u * (uint32_t)cheat_idx + + CHEAT_REC_APPLIED_OFS, sizeof(zeroes)); + } cheat_idx++; } sram_writeshort((uint16_t)cheat_idx, SRAM_NUM_CHEATS); diff --git a/src/cheat.h b/src/cheat.h index 8f1a5a62..17e54240 100644 --- a/src/cheat.h +++ b/src/cheat.h @@ -28,6 +28,18 @@ #define CHEAT_FLAG_ENABLE (0x80) #define CHEAT_NUM_CODES_PER_CHEAT (40) +/* PSRAM-patched ROM cheats: per-record spare tail. A record slot is + flags(1) + desc(254) + numpatches(1) + patches(40*4) = 416 bytes, so the rest of + the 512-byte slot holds, per code, the original ROM byte and an "applied" flag. */ +#define CHEAT_REC_ORIG_OFS (416) +#define CHEAT_REC_APPLIED_OFS (456) + +/* Apply/restore ROM codes in the loaded image. Only on cores whose comparators + had to be dropped (Mk.II GSU and CX4), a no-op elsewhere. Idempotent; called + from deassert_reset(), after every image mutation and before the SNES runs. */ +void cheat_rom_psram_apply(void); +uint8_t cheat_rom_psram_mode(void); + typedef union _cheat_patch_record { struct __attribute__ ((__packed__)) _patch_fields { uint8_t patchvalue; diff --git a/src/memory.c b/src/memory.c index e59b2395..5df46c32 100644 --- a/src/memory.c +++ b/src/memory.c @@ -563,6 +563,8 @@ void init(uint8_t *filename) { } void deassert_reset() { + /* after every image mutation and before the SNES runs; no-op on most cores */ + cheat_rom_psram_apply(); snes_reset(0); fpga_dspx_reset(0); // handle reset loop from hook diff --git a/src/savestate.c b/src/savestate.c index ef3bd218..c0ad33df 100644 --- a/src/savestate.c +++ b/src/savestate.c @@ -17,9 +17,23 @@ extern cfg_t CFG; extern snes_romprops_t romprops; void savestate_program() { - if(romprops.fpga_conf != NULL - && romprops.fpga_conf != FPGA_BASE - /* && romprops.fpga_conf != FPGA_DSP */) { + /* Clear the handler's CS_STATE on every game load: it lives in PSRAM, which + survives resets, and a stale value swallows every save/load combo. */ + sram_writebyte(0, SRAM_SAVESTATE_HANDLER_ADDR + 0x0C); + + /* Cores carrying the machinery the handler needs: the NMI/IRQ hook, the $C0-$FF + identity window it executes from, and a shadow of the write-only $21xx/$42xx + registers at $F90500/$F90700 (from ctx.v on base/DSP/SA-1, from regshadow.v on + the others). SPC7110 and SGB have no hook at all. */ + int core_has_hook = (romprops.fpga_conf == NULL) + || (romprops.fpga_conf == FPGA_BASE) + || (romprops.fpga_conf == FPGA_DSP) + || (romprops.fpga_conf == FPGA_SA1) + || (romprops.fpga_conf == FPGA_OBC1) + || (romprops.fpga_conf == FPGA_SDD1) + || (romprops.fpga_conf == FPGA_CX4) + || (romprops.fpga_conf == FPGA_GSU); + if(!core_has_hook) { savestate_enable_handler(0); return; } @@ -29,12 +43,48 @@ void savestate_program() { * 2C00 "EXE" hook is now left alone so it doesn't clash with USB hook features */ - savestate_enable_handler(CFG.enable_ingame_savestate); - if(CFG.enable_ingame_savestate) { + /* Per-coprocessor capture/restore, each gated on the core implementing it: + DSP1-4 halt + $E8 scan window over data RAM and the register file. The + ST0010 shares this core but its 2 KB data RAM collides with the + window's register-file gap, so it is not covered. + SA-1 halt at an instruction boundary + $E8 state block; IRAM and BW-RAM + are read back over the bus. Mk.III only: the machinery overmaps + the Mk.II Spartan-3 (see verilog/sd2snes_sa1). + GSU run-to-stop freeze + $E8 window; cache and cart RAM come from their + native SNES-visible images. + CX4 run-to-stop freeze + $E8 window for the CPU core; data RAM, MMIO, + vectors and GPRs are already bus-visible and the program cache is + replayed through the native MMIO on restore. + OBC1 purely reactive: snapshot/restore its $7800-$7FFF window over the bus. + S-DD1 never mid-transfer at an NMI boundary, so only the bus-visible + config block $4800-$4807 has to be captured. */ + int dsp_ok = (romprops.fpga_conf == FPGA_DSP) && !romprops.has_st0010; +#ifndef CONFIG_MK2 + int sa1_ok = (romprops.fpga_conf == FPGA_SA1); +#else + int sa1_ok = 0; +#endif + int gsu_ok = (romprops.fpga_conf == FPGA_GSU); + int obc1_ok = (romprops.fpga_conf == FPGA_OBC1); + int sdd1_ok = (romprops.fpga_conf == FPGA_SDD1); + int cx4_ok = (romprops.fpga_conf == FPGA_CX4); + + int savestate_ok = CFG.enable_ingame_savestate + && (romprops.fpga_conf == NULL || romprops.fpga_conf == FPGA_BASE + || dsp_ok || sa1_ok || gsu_ok || obc1_ok || sdd1_ok || cx4_ok); + + savestate_enable_handler(savestate_ok); + if(savestate_ok) { sram_writeshort(0x0101, SS_REQ_ADDR); sram_writebyte(CFG.loadstate_delay, SS_DELAY_ADDR); sram_writebyte(CFG.enable_savestate_slots, SS_SLOTS_ADDR); sram_writebyte(CFG.enable_ingame_savestate, SS_CTRL_ADDR); + sram_writebyte(dsp_ok ? 1 : 0, SS_DSP_GATE_ADDR); + sram_writebyte(sa1_ok ? 1 : 0, SS_SA1_GATE_ADDR); + sram_writebyte(gsu_ok ? 1 : 0, SS_GSU_GATE_ADDR); + sram_writebyte(obc1_ok ? 1 : 0, SS_OBC1_GATE_ADDR); + sram_writebyte(sdd1_ok ? 1 : 0, SS_SDD1_GATE_ADDR); + sram_writebyte(cx4_ok ? 1 : 0, SS_CX4_GATE_ADDR); savestate_set_inputs(); savestate_set_fixes(); load_backup_state(); diff --git a/src/savestate.h b/src/savestate.h index e227289d..d60b351d 100644 --- a/src/savestate.h +++ b/src/savestate.h @@ -15,9 +15,20 @@ #define SS_DELAY_ADDR 0xFE100EL #define SS_SLOTS_ADDR 0xFE100FL #define SS_SLOTS_INPUT_ADDR 0xFE1010L +#define SS_SA1_LOAD_ADDR 0xFE1013L #define SS_CTRL_ADDR 0xFE1012L #define SS_FIXES_ADDR 0xFE1014L +/* Per-coprocessor "this core can capture/restore its chip" flags, armed on every + game load and read by the handler (lockstep with snes/savestate.i65). 0 makes + the handler skip that chip entirely. */ +#define SS_DSP_GATE_ADDR 0xFF0712L +#define SS_SA1_GATE_ADDR 0xFF0713L +#define SS_GSU_GATE_ADDR 0xFF0714L +#define SS_OBC1_GATE_ADDR 0xFF0718L +#define SS_SDD1_GATE_ADDR 0xFF0719L +#define SS_CX4_GATE_ADDR 0xFF071FL + typedef enum { SS_OP_NONE = 0, SS_OP_OR = ASM_ORA_IMM, diff --git a/verilog/sd2snes_base/Makefile b/verilog/sd2snes_base/Makefile index 75e2c37c..d46f1943 100644 --- a/verilog/sd2snes_base/Makefile +++ b/verilog/sd2snes_base/Makefile @@ -1,6 +1,6 @@ CORE = base -VSRC = address.v cheat.v clk_test.v dac.v dcm.v bsx.v rtc.v srtc.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v ctx.v dac.v dcm.v dma.v bsx.v rtc.v srtc.v main.v mcu_cmd.v msu.v sd_dma.v spi.v VHSRC = COMMON_IP = dac_buf msu_databuf snescmd_buf diff --git a/verilog/sd2snes_base/dma.v b/verilog/sd2snes_base/dma.v index 33e0bc5c..10c86893 100644 --- a/verilog/sd2snes_base/dma.v +++ b/verilog/sd2snes_base/dma.v @@ -31,7 +31,7 @@ module dma( input reg_we_rising, output loop_enable, - + input BUS_RDY, output BUS_RRQ, output BUS_WRQ, @@ -42,10 +42,10 @@ module dma( input [15:0] ROM_DATA_IN ); -parameter ST_IDLE = 0; -parameter ST_READ = 1; -parameter ST_WRITE = 2; -parameter ST_DONE = 3; +parameter ST_IDLE = 0; +parameter ST_READ = 1; +parameter ST_WRITE = 2; +parameter ST_DONE = 3; parameter OP_COPY = 0; parameter OP_RESET = 1; @@ -53,7 +53,7 @@ parameter OP_SET = 2; parameter OP_DEBUG = 3; // Register bank -reg [7:0] dma_r[15:0]; +reg [7:0] dma_r[9:0]; reg [2:0] state; initial state = ST_IDLE; @@ -97,7 +97,7 @@ always @(posedge clkin) begin dma_r[reg_addr] <= reg_data_in; end else if (state == ST_DONE) begin - dma_r[9][0] <= 0; + dma_r[9][0] <= 0; // clear single-op trigger end end @@ -123,6 +123,16 @@ reg trig_r; initial trig_r = 0; reg word_mode_r; reg [23:0] src_addr_r, dst_addr_r, length_r, mod_r; +// 1-trigger queue: the savestate handler fires its second op without polling, so a +// trigger arriving while the copier is busy is latched with the op snapshotted and +// started in ST_IDLE. One slot is all it needs; a further trigger while one is +// already pending is dropped, as every trigger during a busy copier was before. +reg pending; initial pending = 0; +reg [23:0] shadow_src, shadow_dst, shadow_len; +reg shadow_word, shadow_dir; +reg [4:0] shadow_op; +wire op_kick = reg_we_rising & enable & (reg_addr == 4'd9) & reg_data_in[0]; + assign BUS_RRQ = BUS_RDY && (state == ST_READ); assign BUS_WRQ = BUS_RDY && (state == ST_WRITE); assign ROM_ADDR = (state == ST_READ) ? src_addr_r : dst_addr_r; @@ -133,32 +143,60 @@ assign ROM_DATA_OUT = (opcode_r == OP_COPY) ? (dst_addr_r[0] ? {ROM_DATA_IN[7 : 0; assign ROM_WORD_ENABLE = word_mode_r; assign loop_enable = loop_r; - wire [23:0] length_next = length_r - (word_mode_r ? 2 : 1); always @(posedge clkin) begin if (reset) begin - loop_r <= 0; - state <= ST_IDLE; - trig_r <= 0; + loop_r <= 0; + state <= ST_IDLE; + trig_r <= 0; + pending <= 0; end else begin - trig_r <= TRIG; - + trig_r <= TRIG; + + // when idle the fresh trigger is handled by the op_kick path below + if (op_kick && (state != ST_IDLE) && !pending) begin + pending <= 1'b1; + shadow_src <= SRC_ADDR; + shadow_dst <= DST_ADDR; + shadow_len <= LEN; + shadow_word <= WORD_MODE; + shadow_op <= reg_data_in[7:3]; // dma_r[9] being written this cycle + shadow_dir <= reg_data_in[1]; + end + case (state) ST_IDLE: begin - if (TRIG && (trig_r ^ TRIG)) begin - src_addr_r <= SRC_ADDR; - dst_addr_r <= DST_ADDR; - length_r <= LEN; - mod_r <= WORD_MODE ? (DIR ? -2 : 2) : (DIR ? -1 : 1); - opcode_r <= OPCODE; - loop_r <= LOOP; - dir_r <= DIR; + if (pending) begin + // start the op snapshotted while the previous one ran + src_addr_r <= shadow_src; + dst_addr_r <= shadow_dst; + length_r <= shadow_len; + mod_r <= shadow_word ? (shadow_dir ? -2 : 2) : (shadow_dir ? -1 : 1); + opcode_r <= shadow_op; + word_mode_r <= shadow_word; + dir_r <= shadow_dir; + loop_r <= 0; + pending <= 0; + if (shadow_op == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; + end + else if (op_kick) begin + // Single-op start, detected by the dma_r[9] write rather than a TRIG + // edge: robust to the bit already being 1, which happens when a colliding + // register write skips the ST_DONE clear. + src_addr_r <= SRC_ADDR; + dst_addr_r <= DST_ADDR; + length_r <= LEN; + mod_r <= WORD_MODE ? (reg_data_in[1] ? -2 : 2) : (reg_data_in[1] ? -1 : 1); + opcode_r <= reg_data_in[7:3]; + loop_r <= reg_data_in[2]; + dir_r <= reg_data_in[1]; word_mode_r <= WORD_MODE; - - if (OPCODE == OP_COPY) state <= ST_READ; - else state <= ST_WRITE; + + if (reg_data_in[7:3] == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; end end ST_READ: begin @@ -179,7 +217,7 @@ always @(posedge clkin) begin end ST_DONE: begin loop_r <= 0; - state <= ST_IDLE; + state <= ST_IDLE; end endcase end diff --git a/verilog/sd2snes_cx4/Makefile b/verilog/sd2snes_cx4/Makefile index 540df1cc..1fad15d0 100644 --- a/verilog/sd2snes_cx4/Makefile +++ b/verilog/sd2snes_cx4/Makefile @@ -1,6 +1,6 @@ CORE = cx4 -VSRC = address.v cheat.v clk_test.v dac.v dcm.v cx4.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v dac.v dcm.v dcm96.v cx4.v main.v mcu_cmd.v msu.v regshadow.v sd_dma.v spi.v VHSRC = COMMON_IP = cx4_datram cx4_datrom cx4_mul cx4_pgmrom dac_buf msu_databuf snescmd_buf diff --git a/verilog/sd2snes_cx4/address.v b/verilog/sd2snes_cx4/address.v index 93bd855f..956c91f3 100644 --- a/verilog/sd2snes_cx4/address.v +++ b/verilog/sd2snes_cx4/address.v @@ -29,11 +29,14 @@ module address( output IS_SAVERAM, // address/CS mapped as SRAM? output IS_ROM, // address mapped as ROM? output IS_WRITABLE, // address somehow mapped as writable area? + output IS_PATCH, // hook identity window active ($C0-FF while unlocked) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, + input snescmd_unlock, // snescmd region unlocked (gates the hook window) output msu_enable, output cx4_enable, output cx4_vect_enable, + output cx4_ss_enable, // savestate scan window ($E8:00xx while unlocked; mk2 AND mk3) output r213f_enable, output r2100_hit, output snescmd_enable, @@ -62,7 +65,15 @@ assign IS_ROM = ~SNES_ROMSEL; assign IS_SAVERAM = |SAVERAM_MASK & (~SNES_ADDR[23] & &SNES_ADDR[22:20] & ~SNES_ADDR[19] & ~SNES_ADDR[15]); -assign SRAM_SNES_ADDR = IS_SAVERAM +// Hook identity window (as in sd2snes_base): while the hook holds the snescmd +// region unlocked, banks $C0-$FF are identity-mapped so the savestate handler runs +// from menu PSRAM with its scratch in $F2-$FF. 0 outside the hook window. +assign IS_PATCH = snescmd_unlock & &SNES_ADDR[23:22]; + +assign SRAM_SNES_ADDR = IS_PATCH + // hook window: identity-map $C0-$FF (handler code + scratch) + ? SNES_ADDR + : IS_SAVERAM ? (24'hE00000 | ({SNES_ADDR[19:16], SNES_ADDR[14:0]} & SAVERAM_MASK)) : ({2'b00, SNES_ADDR[22:16], SNES_ADDR[14:0]} @@ -70,7 +81,7 @@ assign SRAM_SNES_ADDR = IS_SAVERAM assign ROM_ADDR = SRAM_SNES_ADDR; -assign IS_WRITABLE = IS_SAVERAM; +assign IS_WRITABLE = IS_SAVERAM | IS_PATCH; assign ROM_HIT = IS_ROM | IS_WRITABLE; @@ -82,6 +93,13 @@ assign cx4_enable = cx4_enable_w; assign cx4_vect_enable = &SNES_ADDR[15:5]; +// Savestate scan window: $E8:0000-$00FF while unlocked (inside IS_PATCH; the main.v +// data mux gives the window priority over the PSRAM serve, mirroring the GSU core's +// gsu_ss_enable). Collides with nothing: cx4_enable needs ~SNES_ADDR[22] and $E8 has +// bit22=1; cx4_vect_enable needs &SNES_ADDR[15:5] and the window has SNES_ADDR[15:8]==0; +// IS_SAVERAM is $70-$77. Inside cx4.v the window offset is therefore ADDR[7:0]. +assign cx4_ss_enable = snescmd_unlock & (SNES_ADDR[23:16] == 8'hE8) & ~|SNES_ADDR[15:8]; + assign r213f_enable = featurebits[FEAT_213F] & (SNES_PA == 8'h3f); assign r2100_hit = (SNES_PA == 8'h00); diff --git a/verilog/sd2snes_cx4/cheat.v b/verilog/sd2snes_cx4/cheat.v index 3048045f..a762be79 100644 --- a/verilog/sd2snes_cx4/cheat.v +++ b/verilog/sd2snes_cx4/cheat.v @@ -51,6 +51,27 @@ reg irq_enable = 0; reg holdoff_enable = 0; // temp disable hooks after reset reg buttons_enable = 0; reg wram_present = 0; +// Full in-game save/load states run on this core (Mk.II and Mk.III): the CX4 is +// halted through the $E8:00xx savestate window while the handler captures/restores +// it. That needs the base core's force-entry latch: the resume-wait protocol +// requires the NEXT hook entry to bump CS_STATE after the saveinputloop forced a +// button release -- without it the stub branches to nmi_exit and the game parks +// black forever (same lesson as the SA-1/GSU/OBC1 ports). savestate_force_entry +// keeps the nmi_savestate branch routed while a savestate is in flight with the +// buttons RELEASED. Pulse-latched: set at a branch1 fetch with buttons held, +// cleared at the unlock-drop. Only flip-flops, so it builds the same on mk2/mk3. +reg savestate_enable = 0; +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end wire branch_wram = cheat_enable & wram_present; reg auto_nmi_enable = 1; @@ -74,8 +95,16 @@ wire vector_unlock = |vector_unlock_r; reg [1:0] reset_unlock_r = 2'b10; wire reset_unlock = |reset_unlock_r; +// ROM-cheat comparators: Mk.III only. On the Mk.II Spartan-3 the savestate +// machinery does not fit next to them, so they are dropped there and ROM cheats +// are instead patched straight into the PSRAM image by the MCU +// (cheat_rom_psram_apply, src/cheat.c) -- which on this core is strictly better +// anyway: it covers address mirrors and the coprocessor's own fetches, neither +// of which ever went through the comparators. +`ifndef MK2 reg [23:0] cheat_addr[5:0]; reg [7:0] cheat_data[5:0]; +`endif reg [5:0] cheat_enable_mask; reg snescmd_unlock_r = 0; @@ -90,12 +119,16 @@ reg [7:0] branch3_offset = 8'h04; reg [15:0] pad_data = 0; +`ifndef MK2 wire [5:0] cheat_match_bits ={(cheat_enable_mask[5] & (SNES_ADDR == cheat_addr[5])), (cheat_enable_mask[4] & (SNES_ADDR == cheat_addr[4])), (cheat_enable_mask[3] & (SNES_ADDR == cheat_addr[3])), (cheat_enable_mask[2] & (SNES_ADDR == cheat_addr[2])), (cheat_enable_mask[1] & (SNES_ADDR == cheat_addr[1])), (cheat_enable_mask[0] & (SNES_ADDR == cheat_addr[0]))}; +`else +wire [5:0] cheat_match_bits = 6'h00; // mk2: comparators dropped -> folds away +`endif wire cheat_addr_match = |cheat_match_bits; wire [1:0] nmi_match_bits = {SNES_ADDR == 24'h00FFEA, SNES_ADDR == 24'h00FFEB}; @@ -108,13 +141,17 @@ wire rst_addr_match = |rst_match_bits; wire hook_enable = ~|hook_enable_count; -assign data_out = cheat_match_bits[0] ? cheat_data[0] +assign data_out = +`ifndef MK2 + cheat_match_bits[0] ? cheat_data[0] : cheat_match_bits[1] ? cheat_data[1] : cheat_match_bits[2] ? cheat_data[2] : cheat_match_bits[3] ? cheat_data[3] : cheat_match_bits[4] ? cheat_data[4] : cheat_match_bits[5] ? cheat_data[5] - : nmi_match_bits[1] ? 8'h10 + : +`endif + nmi_match_bits[1] ? 8'h10 : irq_match_bits[1] ? 8'h10 : rst_match_bits[1] ? 8'h7D : nmicmd_enable ? nmicmd @@ -196,6 +233,8 @@ always @(posedge clk) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; end else begin + savestate_force_entry_enable_strobe <= 0; + savestate_force_entry_disable_strobe <= 0; if(SNES_rd_strobe) begin // *** GAME -> INGAME HOOK *** if(hook_enable_sync @@ -205,9 +244,20 @@ always @(posedge clk) begin // remember where we came from (IRQ/NMI) for hook exit return_vector <= SNES_ADDR[7:0]; snescmd_unlock_r <= 1; + // clear unlock countdown if we are entering the snescmd region. this is to + // avoid a prior snescmd lock countdown re-locking before we are done + snescmd_unlock_disable <= 0; + snescmd_unlock_disable_countdown <= 0; end - if(rst_match_bits[1] & |reset_unlock_r) begin + else if(rst_match_bits[1] & |reset_unlock_r) begin snescmd_unlock_r <= 1; + snescmd_unlock_disable <= 0; + snescmd_unlock_disable_countdown <= 0; + end + // arm the savestate force-entry latch when the hook redirect is taken with + // buttons held; it holds the nmi_savestate route through the button release. + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; end end // give some time to exit snescmd memory and jump to original vector @@ -221,6 +271,8 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; + // drop the force-entry latch at the same unlock-drop point + savestate_force_entry_disable_strobe <= 1; end end end @@ -296,18 +348,21 @@ always @(posedge clk) begin snescmd_unlock_disable_strobe <= 1'b1; end end else if(pgm_we) begin +`ifndef MK2 if(pgm_idx < 6) begin cheat_addr[pgm_idx] <= pgm_in[31:8]; cheat_data[pgm_idx] <= pgm_in[7:0]; - end else if(pgm_idx == 6) begin // set rom patch enable + end else +`endif + if(pgm_idx == 6) begin // set rom patch enable cheat_enable_mask <= pgm_in[5:0]; end else if(pgm_idx == 7) begin // set/reset global enable / hooks - // pgm_in[13:8] are reset bit flags - // pgm_in[5:0] are set bit flags - {wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - <= ({wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - & ~pgm_in[13:8]) - | pgm_in[5:0]; + // pgm_in[14:8] are reset bit flags + // pgm_in[6:0] are set bit flags + {savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + <= ({savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + & ~pgm_in[14:8]) + | pgm_in[6:0]; end end end @@ -352,7 +407,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & (savestate_force_entry | |pad_data)) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end else begin @@ -370,7 +429,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & |pad_data) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end @@ -381,7 +444,19 @@ always @* begin end else if(branch_wram) begin branch2_offset = 8'h00; // nmi_patches end else begin - branch2_offset = 8'h09; // nmi_exit + if(savestate_enable) begin + branch2_offset = 8'h05; // nmi_savestate + end else begin + branch2_offset = 8'h09; // nmi_exit + end + end +end + +always @* begin + if(savestate_enable) begin + branch3_offset = 8'h00; // nmi_savestate + end else begin + branch3_offset = 8'h04; // nmi_exit end end diff --git a/verilog/sd2snes_cx4/cx4.v b/verilog/sd2snes_cx4/cx4.v index c1930691..51b71bd8 100644 --- a/verilog/sd2snes_cx4/cx4.v +++ b/verilog/sd2snes_cx4/cx4.v @@ -26,6 +26,12 @@ module cx4( input SNES_VECT_EN, input reg_we_rising, input CLK, + input pause, // in-game savestate handler: freeze the coprocessor (cache/DMA/CPU + // FSMs) so it neither drifts nor contends the bus while the SNES + // CPU is frozen in the handler. Transparent -- state is held, + // resumes exactly on release ($202C, driven from main.v). + input SS_EN, // savestate scan window active ($E8:00xx, offset = ADDR[7:0]) + input RST, // SNES reset strobe: drop any pending savestate halt/freeze input [7:0] BUS_DI, output [23:0] BUS_ADDR, output BUS_RRQ, @@ -41,6 +47,38 @@ parameter BUSY_DMA = 2'b01; parameter BUSY_CPU = 2'b10; assign cx4_busy_out = cx4_busy; +/* --------------------------------------------------------------------------- + Savestate window ($E8:00xx) and freeze control. + Declared up here (before the first use, which is the CPU busy latch below) + because XST on mk2 requires declaration before use. The freeze FSM itself + lives further down, where CPU_STATE/CACHE_ST/DMA_ST are already declared. + --------------------------------------------------------------------------- */ +reg ss_halt_r; initial ss_halt_r = 1'b0; +reg ss_frozen_r; initial ss_frozen_r = 1'b0; +reg ss_dirty_r; initial ss_dirty_r = 1'b0; +reg [7:0] ss_pre_r; initial ss_pre_r = 8'h00; // timeout prescaler +reg [13:0] ss_wait_r; initial ss_wait_r = 14'h0000; +reg [3:0] ss_settle_r; initial ss_settle_r = 4'h0; +reg [7:0] ss_ridx_r; initial ss_ridx_r = 8'h00; // $E4 readback index +reg ss_rd_r; initial ss_rd_r = 1'b0; +reg [7:0] SS_DOr; + +// Single CPU-core gate: the pause freezes the core, a requested-but-not-reached +// savestate halt overrides it so a mid-program CX4 can run to its stop point, and +// once frozen the core is held unconditionally. +wire cx4_cpu_en = ~((pause & ~(ss_halt_r & ~ss_frozen_r)) | ss_frozen_r); + +wire ss_win = SS_EN; +wire SS_WR_EN = ss_win & reg_we_rising; +wire ss_norm = SS_WR_EN & (ADDR[7:0] == 8'hf0); // normalize pulse +wire ss_commit = SS_WR_EN & (ADDR[7:0] == 8'he3); // commit staged cpu_idb +wire [2:0] ss_cidx = DI[2:0]; +wire ss_pcs_wr = SS_WR_EN & (ADDR[7:3] == 5'b00100); // $20-$27 pc stack +wire ss_core_wr = SS_WR_EN & ((ADDR[7:0] == 8'h14) | (ADDR[7:0] == 8'h15) + |(ADDR[7:0] == 8'h16) | (ADDR[7:0] == 8'he0) + |(ADDR[7:0] == 8'he1) | (ADDR[7:0] == 8'he2)); +wire ss_core_act = ss_rd_r | ss_norm | ss_commit | ss_pcs_wr | ss_core_wr; + wire datram_enable = CS & (ADDR[11:0] < 12'hc00); wire mmio_enable = CS & (ADDR[12:5] == 8'b11111010) & (ADDR[4:0] < 5'b10011); wire status_enable = CS & (ADDR[12:5] == 8'b11111010) & (ADDR[4:0] >= 5'b10011); @@ -60,6 +98,9 @@ assign DO = datram_enable ? DATRAM_DO : status_enable ? STATUS_DO : vector_enable ? VECTOR_DOr : gpr_enable ? GPR_DOr + // window last: every enable above needs CS (0 in $E8), so this keeps + // the DATRAM_DO path one mux level shorter. + : ss_win ? SS_DOr : 8'h00; /* 0x1f40 - 0x1f52: MMIO @@ -275,6 +316,15 @@ always @(posedge CLK) begin CACHE_TRIG_ENr <= 1'b0; DMA_TRIG_ENr <= 1'b0; cpu_go_en_r <= 1'b0; + // Window restore, in the else arm on purpose: the clears above are one-shot + // strobes that must keep firing every non-MMIO clock, and the two write enables + // are different SNES cycles that never coincide. + if(SS_WR_EN) begin + case(ADDR[7:0]) + 8'h18: cx4_mmio_cachepage <= DI[0]; + 8'h19: cx4_mmio_savepage <= DI[1:0]; + endcase + end end end @@ -314,6 +364,9 @@ reg [9:0] cx4_pgmrom_addr; reg [8:0] cache_count; initial cache_count = 9'b0; +// Not pause-gated: an in-flight fill has to drain, or the cached program is left +// corrupt (caches only refill on a page switch). With the CPU and the SNES frozen +// nothing kicks a new fill. always @(posedge CLK) begin case(CACHE_ST) ST_CACHE_IDLE: begin @@ -365,6 +418,9 @@ always @(posedge CLK) begin end end endcase + // window $17: invalidate the cache so the restore's native replay refills both + // pages. Last in the block so it wins over the chain above. + if(SS_WR_EN & (ADDR[7:0] == 8'h17)) cachevalid <= DI[1:0]; end reg cx4_dma_datram_we; @@ -380,6 +436,7 @@ wire [7:0] cx4_datram_di = cx4_busy[BUSY_DMA] ? BUS_DI : cx4_cpu_datram_di[7:0]; reg [15:0] dma_count; initial dma_count = 16'b0; +// NOT pause-gated: same drain rationale as the cache pipe above. always @(posedge CLK) begin case(DMA_ST) ST_DMA_IDLE: begin @@ -488,12 +545,17 @@ reg op_jump; reg condtrue; reg mul_strobe = 0; +// Normalize ($F0): a dirty freeze can leave BUSY_CPU stuck at 1, which saturates +// every busywait the handler does afterwards. always @(posedge CLK) begin - if(cpu_go_en_r) cx4_busy[BUSY_CPU] <= 1'b1; - else if(op == OP_HLT) cx4_busy[BUSY_CPU] <= 1'b0; + if(ss_norm) cx4_busy[BUSY_CPU] <= 1'b0; + else if(cx4_cpu_en) begin + if(cpu_go_en_r) cx4_busy[BUSY_CPU] <= 1'b1; + else if(op == OP_HLT) cx4_busy[BUSY_CPU] <= 1'b0; + end end -always @(posedge CLK) begin +always @(posedge CLK) if(cx4_cpu_en) begin case(op_sa) 2'b00: cpu_sa <= cpu_a; 2'b01: cpu_sa <= cpu_a << 1; @@ -505,7 +567,69 @@ end reg jp_docache; initial jp_docache = 1'b0; +// One write and one read port, both address-muxed with the window, so the +// distributed-RAM inference survives. Safe because the window only reads while +// frozen, with the native consumer stopped. +wire [2:0] pcs_waddr = ss_pcs_wr ? ADDR[2:0] : cpu_sp; +wire [7:0] pcs_wdata = ss_pcs_wr ? DI : (cpu_pc + 8'd1); +wire [2:0] pcs_raddr = ss_frozen_r ? ADDR[2:0] : (cpu_sp - 3'd1); +wire [7:0] pcs_rdata = cpu_pc_stack[pcs_raddr]; + +// Readback mux ($E4), deliberately small: only the 5 registers the handler asks +// for, and NOT a reuse of the native register-file read. Hoisting that out of the +// clocked body turns the variable-index gpr[]/constrom[] lookups into an +// asynchronous mux worth several hundred LUTs on mk2. The handler reads gpr and +// constrom through their native space anyway. +wire [23:0] ss_idb_rd = (ss_ridx_r == 8'h00) ? cpu_a + : (ss_ridx_r == 8'h08) ? cpu_romdata + : (ss_ridx_r == 8'h0c) ? cpu_ramdata + : (ss_ridx_r == 8'h13) ? cpu_busaddr + : (ss_ridx_r == 8'h1c) ? cpu_ramaddr + : 24'b0; + always @(posedge CLK) begin + if(ss_core_act) begin + /* ---- savestate window arm: runs while the core is FROZEN, so it has to + SUPPRESS the native body rather than live inside it (cx4_cpu_en is 0 + there). Every register touched here is owned by this block. ---- */ + if(ss_rd_r) cpu_idb <= ss_idb_rd; // $E4 readback select + if(ss_pcs_wr) cpu_pc_stack[pcs_waddr] <= pcs_wdata; // $20-$27 + if(ss_core_wr) begin + case(ADDR[7:0]) + 8'h14: cpu_page_stack <= DI; + 8'h15: cpu_sp <= DI[2:0]; + 8'h16: begin fl_n <= DI[0]; fl_z <= DI[1]; fl_c <= DI[2]; end + 8'he0: cpu_idb[7:0] <= DI; // staging lanes + 8'he1: cpu_idb[15:8] <= DI; + 8'he2: cpu_idb[23:16] <= DI; + endcase + end + if(ss_commit) begin + case(ss_cidx) + 3'd0: cpu_a <= cpu_idb; + 3'd1: cpu_romdata <= cpu_idb; + 3'd2: cpu_ramdata <= cpu_idb; + 3'd3: cpu_busaddr <= cpu_idb; + 3'd4: cpu_ramaddr <= cpu_idb; + default: ; // 5 = multiplier latch, owned by the mul block + endcase + end + if(ss_norm) begin + // park at the canonical boundary: with op != OP_HLT the busy bit sticks, + // cpu_cache_en kicks spurious fills and cx4_cpu_datram_we rewrites a data + // RAM byte every clock. + CPU_STATE <= ST_CPU_IDLE; + op <= OP_HLT; + cpu_wait <= 8'h00; + condtrue <= 1'b0; + jp_docache <= 1'b0; + cpu_cache_en <= 1'b0; + mul_strobe <= 1'b0; + cpu_bus_rq <= 1'b0; + cx4_cpu_datram_we <= 1'b0; + end + end + else if(cx4_cpu_en) begin mul_strobe <= 1'b0; case(CPU_STATE) ST_CPU_IDLE: begin @@ -621,7 +745,7 @@ always @(posedge CLK) begin if(condtrue) begin if(op_call) begin cpu_page_stack[cpu_sp] <= cpu_page; - cpu_pc_stack[cpu_sp] <= cpu_pc + 1; + cpu_pc_stack[pcs_waddr] <= pcs_wdata; cpu_sp <= cpu_sp + 1; end cpu_pc <= op_param; @@ -635,7 +759,7 @@ always @(posedge CLK) begin end OP_RT: begin cpu_page <= cpu_page_stack[cpu_sp - 1]; - cpu_pc <= cpu_pc_stack[cpu_sp - 1]; + cpu_pc <= pcs_rdata; cpu_sp <= cpu_sp - 1; end OP_WAI: if(BUS_RDY) cpu_pc <= cpu_pc + 1; @@ -828,6 +952,7 @@ always @(posedge CLK) begin else CPU_STATE <= ST_CPU_0; end endcase + end end reg[2:0] BUSRD_STATE; @@ -838,7 +963,12 @@ initial BUSRD_STATE = ST_BUSRD_IDLE; reg cpu_bus_rq2; always @(posedge CLK) cpu_bus_rq2 <= cpu_bus_rq; -always @(posedge CLK) begin +// No savestate arm here: BUSRD_STATE is stuck-at-END by construction (nothing ever +// leaves ST_BUSRD_END), so cpu_busdata is reloaded from BUS_DI on every enabled +// clock and restoring it would be a no-op. For the same reason BUSRD_STATE is kept +// OUT of the freeze predicate -- including it would stop the freeze from ever +// converging after the game's first OP_BUS. +always @(posedge CLK) if(cx4_cpu_en) begin if(CPU_STATE == ST_CPU_2 && (op == OP_ST || op == OP_SWP) && op_param == 8'h03) @@ -862,24 +992,145 @@ always @(posedge CLK) begin end end -// gpr write, either by CPU or by MMIO +// gpr write, either by CPU or by MMIO. The MMIO arm relaxes the gate so the +// savestate restore can write the 48 GPR bytes while frozen -- it uses the NATIVE +// $00:7F80-$7FAF addresses ($E8 does not decode GPR, CS is 0 there), so no second +// write port is needed. always @(posedge CLK) begin - if(CPU_STATE == ST_CPU_2 + if(cx4_cpu_en & CPU_STATE == ST_CPU_2 && (op == OP_ST || op == OP_SWP) && (op_param[7:4] == 4'h6)) begin gpr[op_param[3:0]*3+2] <= cpu_idb[23:16]; gpr[op_param[3:0]*3+1] <= cpu_idb[15:8]; gpr[op_param[3:0]*3] <= cpu_idb[7:0]; end - else if(GPR_WR_EN) gpr[ADDR[5:0]] <= DI; + else if(GPR_WR_EN & (cx4_cpu_en | ss_frozen_r)) gpr[ADDR[5:0]] <= DI; end // external multiplier always @(posedge CLK) begin - if(mul_strobe) begin + // savestate commit 5 ($E3): this IS the native mul_strobe path, so restoring + // cpu_mul_a/cpu_mul_b costs enable terms only. + if(ss_commit & (ss_cidx == 3'd5)) begin cpu_mul_a <= cpu_a; cpu_mul_b <= cpu_idb; end + else if(cx4_cpu_en) begin + if(mul_strobe) begin + cpu_mul_a <= cpu_a; + cpu_mul_b <= cpu_idb; + end + end +end + +/*************************** + ======== SAVESTATE ======== + ***************************/ +// $E4: latch the readback index and pulse ss_rd_r for one clock; the CPU block +// loads cpu_idb from ss_idb_rd on the next edge, where the SNES then reads it back +// through $00-$02. The index is the NATIVE op_param encoding (see the handler). +always @(posedge CLK) begin + ss_rd_r <= 1'b0; + if(SS_WR_EN & (ADDR[7:0] == 8'he4)) begin + ss_ridx_r <= DI; + ss_rd_r <= 1'b1; + end +end + +// Freeze predicate. Compared with == on purpose: XST/Quartus re-encode FSMs by +// default, so a one-hot bit test would survive compilation while silently meaning +// something else. BUSRD_STATE is deliberately absent (stuck-at-END, see above). +wire ss_idle_w = ~|cx4_busy + & (CPU_STATE == ST_CPU_IDLE) + & (CACHE_ST == ST_CACHE_IDLE) + & (DMA_ST == ST_DMA_IDLE) + & ~CACHE_TRIG_ENr & ~CACHE_TRIG_EN2r & ~DMA_TRIG_ENr & ~cpu_go_en_r; + +// Halt protocol (window $FE bit0, same semantics as the GSU/SA-1 cores): on a halt +// request let the CX4 run to its stop point, then hold it frozen after a 16-cycle +// settle (covers both CACHE_TRIG stages). A saturating timeout -- prescaler 8 bits +// + counter 14 bits = ~45.9ms @80MHz, split in two short carry chains to help +// placement on the nearly full mk2 -- freezes anyway and latches ss_dirty_r (sticky +// until the next halt request): a dirty capture may lose one in-flight CX4 program. +// ss_frozen_r is monotonic while halted, so the restore's cache replay (which wakes +// CACHE_ST again) does not thaw the core. +always @(posedge CLK) begin + if(RST) begin + ss_halt_r <= 1'b0; ss_frozen_r <= 1'b0; ss_dirty_r <= 1'b0; + ss_pre_r <= 8'h00; ss_wait_r <= 14'h0000; ss_settle_r <= 4'h0; + end + else begin + // halt control write: this block is not pause-gated, so the request lands + // while the core still runs + if(SS_WR_EN & (ADDR[7:0] == 8'hfe)) begin + ss_halt_r <= DI[0]; + if(DI[0]) begin + ss_dirty_r <= 1'b0; ss_pre_r <= 8'h00; ss_wait_r <= 14'h0000; ss_settle_r <= 4'h0; + end + end + if(~ss_halt_r) begin + ss_frozen_r <= 1'b0; ss_pre_r <= 8'h00; ss_wait_r <= 14'h0000; ss_settle_r <= 4'h0; + end else if(~ss_frozen_r) begin + if(ss_idle_w) begin + if(&ss_settle_r) ss_frozen_r <= 1'b1; + else ss_settle_r <= ss_settle_r + 1'b1; + end else begin + ss_settle_r <= 4'h0; + if(&ss_wait_r[13:11]) begin + ss_frozen_r <= 1'b1; + ss_dirty_r <= 1'b1; + end else begin + ss_pre_r <= ss_pre_r + 1'b1; + if(&ss_pre_r) ss_wait_r <= ss_wait_r + 1'b1; + end + end + end + end +end + +// Read the arrays through scalar wires so the window mux never indexes a 2D array +// where XST (mk2) chokes; cpu_pc_stack is NOT unrolled this way -- it goes through +// the single pcs_rdata port to keep its RAM inference. +wire [14:0] ss_ctag0 = cachetag[0]; +wire [14:0] ss_ctag1 = cachetag[1]; +wire [14:0] ss_pmem0 = cx4_mmio_pagemem[0]; +wire [14:0] ss_pmem1 = cx4_mmio_pagemem[1]; + +// Window read mux. Registered like MMIO_DOr/VECTOR_DOr/GPR_DOr: SNES_ADDR is +// stable for many CLK cycles before the read strobe, so no new combinational path +// reaches SNES_DATA. Offsets not listed read $00 and ignore writes. +always @(posedge CLK) begin + casex(ADDR[7:0]) + 8'h00: SS_DOr <= cpu_idb[7:0]; // readback of the register picked by $E4 + 8'h01: SS_DOr <= cpu_idb[15:8]; + 8'h02: SS_DOr <= cpu_idb[23:16]; + 8'h04: SS_DOr <= cpu_mul_a[7:0]; + 8'h05: SS_DOr <= cpu_mul_a[15:8]; + 8'h06: SS_DOr <= cpu_mul_a[23:16]; + 8'h08: SS_DOr <= cpu_mul_b[7:0]; + 8'h09: SS_DOr <= cpu_mul_b[15:8]; + 8'h0a: SS_DOr <= cpu_mul_b[23:16]; + 8'h0c: SS_DOr <= ss_ctag0[7:0]; + 8'h0d: SS_DOr <= {1'b0, ss_ctag0[14:8]}; + 8'h0e: SS_DOr <= ss_ctag1[7:0]; + 8'h0f: SS_DOr <= {1'b0, ss_ctag1[14:8]}; + 8'h10: SS_DOr <= ss_pmem0[7:0]; + 8'h11: SS_DOr <= {1'b0, ss_pmem0[14:8]}; + 8'h12: SS_DOr <= ss_pmem1[7:0]; + 8'h13: SS_DOr <= {1'b0, ss_pmem1[14:8]}; + 8'h14: SS_DOr <= cpu_page_stack; + 8'h15: SS_DOr <= {5'b0, cpu_sp}; + 8'h16: SS_DOr <= {5'b0, fl_c, fl_z, fl_n}; + 8'h17: SS_DOr <= {6'b0, cachevalid}; + 8'h18: SS_DOr <= {7'b0, cx4_mmio_cachepage}; + 8'h19: SS_DOr <= {6'b0, cx4_mmio_savepage}; + // diag: bit4 = dirty capture, bits 3:1 = cx4_busy, bit0 = frozen + 8'h1a: SS_DOr <= {3'b0, ss_dirty_r, cx4_busy[2], cx4_busy[1], cx4_busy[0], ss_frozen_r}; + 8'b00100xxx: SS_DOr <= pcs_rdata; // $20-$27 cpu_pc_stack, single port + 8'hfe: SS_DOr <= {7'b0, ss_frozen_r}; // halt status + 8'hff: SS_DOr <= 8'h5c; // magic + default: SS_DOr <= 8'h00; // incl. the $03/$07/$0B/$1B-$1F pad lanes + endcase end /*************************** diff --git a/verilog/sd2snes_cx4/dcm96.v b/verilog/sd2snes_cx4/dcm96.v new file mode 100644 index 00000000..10b33fdd --- /dev/null +++ b/verilog/sd2snes_cx4/dcm96.v @@ -0,0 +1,63 @@ +`timescale 1ns / 1ps +////////////////////////////////////////////////////////////////////////////////// +// Module Name: dcm96 +// +// Second DCM, Mk.II only: 96 MHz snoop clock (CLK96) for the savestate register +// shadow. The primary DCM runs CLK2 at 80 MHz for CX4 cycle fidelity, but the +// snoop capture patterns are calibrated for 96 MHz sampling. On Mk.III the PLL's +// .c1 output provides the same clock. CLKFX = 24 MHz x 4 / 1, the same parameters +// as the base core's DCM. No IBUFG here (nor in dcm.v): CLKIN comes from the +// top-level pad, so one inferred IBUFG feeds both DCMs. +////////////////////////////////////////////////////////////////////////////////// +module my_dcm96 ( + input CLKIN, + output CLKFX, + output LOCKED, + input RST, + output[7:0] STATUS +); + +// DCM: Digital Clock Manager Circuit +// Spartan-3 +// Xilinx HDL Language Template, version 11.1 + + DCM #( + .SIM_MODE("SAFE"), // Simulation: "SAFE" vs. "FAST", see "Synthesis and Simulation Design Guide" for details + .CLKDV_DIVIDE(2.0), // Divide by: 1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5 + // 7.0,7.5,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0 or 16.0 + .CLKFX_DIVIDE(1), // Can be any integer from 1 to 32 + .CLKFX_MULTIPLY(4), // Can be any integer from 2 to 32 + .CLKIN_DIVIDE_BY_2("FALSE"), // TRUE/FALSE to enable CLKIN divide by two feature + .CLKIN_PERIOD(41.667), // Specify period of input clock + .CLKOUT_PHASE_SHIFT("NONE"), // Specify phase shift of NONE, FIXED or VARIABLE + .CLK_FEEDBACK("NONE"), // Specify clock feedback of NONE, 1X or 2X + .DESKEW_ADJUST("SYSTEM_SYNCHRONOUS"), // SOURCE_SYNCHRONOUS, SYSTEM_SYNCHRONOUS or + // an integer from 0 to 15 + .DFS_FREQUENCY_MODE("LOW"), // HIGH or LOW frequency mode for frequency synthesis + .DLL_FREQUENCY_MODE("LOW"), // HIGH or LOW frequency mode for DLL + .DUTY_CYCLE_CORRECTION("TRUE"), // Duty cycle correction, TRUE or FALSE + .FACTORY_JF(16'hFFFF), // FACTORY JF values +// .LOC("DCM_X0Y0"), + .PHASE_SHIFT(0), // Amount of fixed phase shift from -255 to 255 + .STARTUP_WAIT("TRUE") // Delay configuration DONE until DCM LOCK, TRUE/FALSE + ) DCM_inst ( + .CLK0(CLK0), // 0 degree DCM CLK output + .CLK180(CLK180), // 180 degree DCM CLK output + .CLK270(CLK270), // 270 degree DCM CLK output + .CLK2X(CLK2X), // 2X DCM CLK output + .CLK2X180(CLK2X180), // 2X, 180 degree DCM CLK out + .CLK90(CLK90), // 90 degree DCM CLK output + .CLKDV(CLKDV), // Divided DCM CLK out (CLKDV_DIVIDE) + .CLKFX(CLKFX), // DCM CLK synthesis out (M/D) + .CLKFX180(CLKFX180), // 180 degree CLK synthesis out + .LOCKED(LOCKED), // DCM LOCK status output + .PSDONE(PSDONE), // Dynamic phase adjust done output + .STATUS(STATUS), // 8-bit DCM status bits output + .CLKFB(CLKFB), // DCM clock feedback + .CLKIN(CLKIN), // Clock input (from IBUFG, BUFG or DCM) + .PSCLK(PSCLK), // Dynamic phase adjust clock input + .PSEN(PSEN), // Dynamic phase adjust enable input + .PSINCDEC(PSINCDEC), // Dynamic phase adjust increment/decrement + .RST(RST) // DCM asynchronous reset input + ); +endmodule diff --git a/verilog/sd2snes_cx4/ip/mk3/pll.v b/verilog/sd2snes_cx4/ip/mk3/pll.v index fb04152c..7c74b281 100644 --- a/verilog/sd2snes_cx4/ip/mk3/pll.v +++ b/verilog/sd2snes_cx4/ip/mk3/pll.v @@ -41,11 +41,13 @@ module pll ( areset, inclk0, c0, + c1, locked); input areset; input inclk0; output c0; + output c1; output locked; `ifndef ALTERA_RESERVED_QIS // synopsys translate_off @@ -62,6 +64,7 @@ module pll ( wire [1:0] sub_wire1 = {sub_wire2, sub_wire0}; wire [0:0] sub_wire4 = sub_wire3[0:0]; wire c0 = sub_wire4; + wire c1 = sub_wire3[1]; wire locked = sub_wire5; altpll altpll_component ( @@ -108,6 +111,10 @@ module pll ( altpll_component.clk0_duty_cycle = 50, altpll_component.clk0_multiply_by = 10, altpll_component.clk0_phase_shift = "0", + altpll_component.clk1_divide_by = 1, + altpll_component.clk1_duty_cycle = 50, + altpll_component.clk1_multiply_by = 12, + altpll_component.clk1_phase_shift = "0", altpll_component.compensate_clock = "CLK0", altpll_component.inclk0_input_frequency = 125000, altpll_component.intended_device_family = "Cyclone IV E", @@ -141,7 +148,7 @@ module pll ( altpll_component.port_scanread = "PORT_UNUSED", altpll_component.port_scanwrite = "PORT_UNUSED", altpll_component.port_clk0 = "PORT_USED", - altpll_component.port_clk1 = "PORT_UNUSED", + altpll_component.port_clk1 = "PORT_USED", altpll_component.port_clk2 = "PORT_UNUSED", altpll_component.port_clk3 = "PORT_UNUSED", altpll_component.port_clk4 = "PORT_UNUSED", diff --git a/verilog/sd2snes_cx4/main.qsf b/verilog/sd2snes_cx4/main.qsf index 01bbab1f..07b78bde 100644 --- a/verilog/sd2snes_cx4/main.qsf +++ b/verilog/sd2snes_cx4/main.qsf @@ -522,6 +522,7 @@ set_global_assignment -name VERILOG_FILE cx4.v set_global_assignment -name VERILOG_FILE clk_test.v set_global_assignment -name VERILOG_FILE cheat.v set_global_assignment -name VERILOG_FILE address.v +set_global_assignment -name VERILOG_FILE regshadow.v set_global_assignment -name QIP_FILE ip/mk3/dac_buf.qip set_global_assignment -name QIP_FILE ip/mk3/msu_databuf.qip set_global_assignment -name QIP_FILE ip/mk3/cx4_datrom.qip diff --git a/verilog/sd2snes_cx4/main.ucf b/verilog/sd2snes_cx4/main.ucf index 636ed526..ba8e407f 100644 --- a/verilog/sd2snes_cx4/main.ucf +++ b/verilog/sd2snes_cx4/main.ucf @@ -1,5 +1,9 @@ NET "CLKIN" TNM_NET = "CLKIN"; TIMESPEC TS_CLKIN = PERIOD "CLKIN" 24.01 MHz HIGH 50 %; +// Both DCM outputs are auto-constrained by ISE from this source PERIOD, propagated +// through each DCM by its CLKFX_MULTIPLY/DIVIDE ratio: my_dcm -> CLK2 (x10/3, 80 MHz) +// and my_dcm96 -> CLK96 (x4/1, 96.04 MHz / ~10.41 ns) for the regshadow snoop domain. +// No separate PERIOD spec is needed for the derived clocks. //TIMESPEC TS_CLKIN = PERIOD "CLKIN" 21.5 MHz HIGH 50 %; NET "p113_out" IOSTANDARD = LVCMOS33; NET "p113_out" LOC = P113; diff --git a/verilog/sd2snes_cx4/main.v b/verilog/sd2snes_cx4/main.v index 224d1235..db544bbc 100644 --- a/verilog/sd2snes_cx4/main.v +++ b/verilog/sd2snes_cx4/main.v @@ -92,6 +92,7 @@ module main( ); wire CLK2; +wire CLK96; // regshadow snoop domain (see the pll instance comment) wire [7:0] CX4_SNES_DATA_IN; wire [7:0] CX4_SNES_DATA_OUT; @@ -191,6 +192,8 @@ wire SD_DMA_TO_ROM; wire free_slot = (SNES_PULSE_end | free_strobe) & ~SD_DMA_TO_ROM; wire ROM_HIT; +wire IS_PATCH; // hook identity window ($C0-FF while snescmd unlocked) -- savestate +wire cx4_ss_enable; // savestate scan window ($E8:00xx while snescmd unlocked) assign DCM_RST=0; @@ -410,6 +413,8 @@ address snes_addr( .IS_SAVERAM(IS_SAVERAM), .IS_ROM(IS_ROM), .IS_WRITABLE(IS_WRITABLE), + .IS_PATCH(IS_PATCH), + .snescmd_unlock(snescmd_unlock), .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), .featurebits(featurebits), @@ -418,6 +423,7 @@ address snes_addr( //CX4 .cx4_enable(cx4_enable), .cx4_vect_enable(cx4_vect_enable), + .cx4_ss_enable(cx4_ss_enable), //region .r213f_enable(r213f_enable), //brightness fix @@ -440,6 +446,17 @@ reg [7:0] CX4_DINr; wire [23:0] CX4_ADDR; wire [2:0] cx4_busy; +// Hook-side pause ($202C bit0, an address the CX4/PPU/CPU do not use): halts the +// CX4 transparently while the SNES CPU is frozen inside the hook, cleared on reset. +// The savestate handler freezes through the $E8 window instead and does not use it. +reg snapshot_pause; initial snapshot_pause = 1'b0; +always @(posedge CLK2) begin + if(SNES_reset_strobe) + snapshot_pause <= 1'b0; + else if(SNES_WR_end & ~SNES_ADDR[22] & (SNES_ADDR[15:0] == 16'h202C)) + snapshot_pause <= SNES_DATA[0]; +end + cx4 snes_cx4 ( .DI(CX4_SNES_DATA_IN), .DO(CX4_SNES_DATA_OUT), @@ -448,6 +465,9 @@ cx4 snes_cx4 ( .SNES_VECT_EN(cx4_vect_enable), .reg_we_rising(SNES_WR_end), .CLK(CLK2), + .pause(snapshot_pause), + .SS_EN(cx4_ss_enable), + .RST(SNES_reset_strobe), .BUS_DI(CX4_DINr), .BUS_ADDR(CX4_ADDR), .BUS_RRQ(CX4_RRQ), @@ -487,6 +507,77 @@ cheat snes_cheat( .snescmd_unlock(snescmd_unlock) ); +// ---- 96 MHz snoop domain for the savestate register shadow ---- +// Port of the base core's bus-snoop machinery, clocked at the base's 96 MHz: the +// shift patterns and the count==4 capture point are calibrated for that frequency +// and sample the wrong bus phase on this core's 80 MHz CLK2. Everything else stays +// on CLK2 (CX4 cycle fidelity untouched); the shadow BRAM lives in this domain and +// its read path is quasi-static during a SNES read cycle, so the crossing is safe. +reg [7:0] rs96_PAWRr; initial rs96_PAWRr = 8'b11111111; +reg [7:0] rs96_WRITEr; initial rs96_WRITEr = 8'b11111111; +reg [7:0] rs96_PAr [5:0]; +reg [23:0] rs96_ADDRr [5:0]; +reg [7:0] rs96_DATAr; initial rs96_DATAr = 0; +reg [3:0] rs96_pawr_cnt; initial rs96_pawr_cnt = 0; +reg rs96_pawr_end; initial rs96_pawr_end = 0; +reg rs96_pawr_end_r; initial rs96_pawr_end_r = 0; +reg [7:0] rs96_data_r; initial rs96_data_r = 0; + +wire rs96_pawr_start_early = ((rs96_PAWRr[4:1] | rs96_PAWRr[5:2]) == 4'b1110); +wire [7:0] rs96_PA = rs96_PAr[5] & rs96_PAr[4]; +wire [23:0] rs96_ADDR = rs96_ADDRr[5] & rs96_ADDRr[4]; +wire rs96_wr_end = (rs96_WRITEr[6:1] == 6'b000001); + +always @(posedge CLK96) begin + rs96_PAWRr <= {rs96_PAWRr[6:0], SNES_PAWR_IN}; + rs96_WRITEr <= {rs96_WRITEr[6:0], SNES_WRITE_IN}; + rs96_PAr[5] <= rs96_PAr[4]; rs96_PAr[4] <= rs96_PAr[3]; + rs96_PAr[3] <= rs96_PAr[2]; rs96_PAr[2] <= rs96_PAr[1]; + rs96_PAr[1] <= rs96_PAr[0]; rs96_PAr[0] <= SNES_PA_IN; + rs96_ADDRr[5] <= rs96_ADDRr[4]; rs96_ADDRr[4] <= rs96_ADDRr[3]; + rs96_ADDRr[3] <= rs96_ADDRr[2]; rs96_ADDRr[2] <= rs96_ADDRr[1]; + rs96_ADDRr[1] <= rs96_ADDRr[0]; rs96_ADDRr[0] <= SNES_ADDR_IN; + rs96_DATAr <= SNES_DATA; + // /PAWR-low duration counter; fire at count==4 (the base ctx.v capture point) + if (rs96_pawr_end) rs96_pawr_cnt <= 0; + else if (rs96_pawr_start_early) rs96_pawr_cnt <= 1; + else if (|rs96_pawr_cnt) rs96_pawr_cnt <= rs96_pawr_cnt + 1'b1; + rs96_pawr_end <= (rs96_pawr_cnt == 4'd4); + rs96_pawr_end_r <= rs96_pawr_end; // ctx.v registers the strobe once more... + rs96_data_r <= rs96_DATAr; // ...and the data tap with it (2-cyc align) +end +// PPU: the captured byte; CPU ($42xx): raw SNES_DATA, as the native snes_ajr capture. +wire [7:0] rs_data = rs96_pawr_end_r ? rs96_data_r : SNES_DATA; + +// Savestate register shadow (regshadow.v), read through the hook window at +// $F90500 (PPU, stride-2 pairs) and $F90700 (CPU $42xx). IS_PATCH gates the reads. +wire shadow_ppu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF905) & ~SNES_ADDR[7]; // $F90500-7F +wire shadow_cpu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF907) & (SNES_ADDR[7:5] == 3'b000); // $F90700-1F +wire [7:0] regshadow_dout; +// PPU pair at mem[$00-$7F] indexed by SNES_ADDR[6:0], CPU reg at mem[$80-$9F]. +// 1-cycle BRAM read latency (like snescmd_buf); the address is stable for the whole +// ROM cycle. The index carries the same REGSHADOW_1DEEP gate as regshadow.v, whose +// mk2 layout differs -- out of step the serve reads the wrong cells with no build +// error. The SNES side is unaffected: under 1DEEP a stride-2 word serves (v,v). +`ifdef REGSHADOW_1DEEP +wire [8:0] regshadow_raddr = shadow_cpu_hit ? {4'b0010, SNES_ADDR[4:0]} + : {3'b000, SNES_ADDR[6:1]}; +`else +wire [8:0] regshadow_raddr = shadow_cpu_hit ? {4'b0100, SNES_ADDR[4:0]} + : {2'b00, SNES_ADDR[6:0]}; +`endif +regshadow snes_regshadow( + // Entirely in the 96 MHz snoop domain (see above). + .clk(CLK96), + .pawr_end(rs96_pawr_end_r), + .wr_end(rs96_wr_end), + .snes_addr(rs96_ADDR), + .snes_pa(rs96_PA), + .snes_data(rs_data), + .rd_addr(regshadow_raddr), + .rd_data(regshadow_dout) +); + wire [7:0] snescmd_dout; parameter ST_R213F_ARMED = 4'b0001; @@ -512,6 +603,10 @@ wire r2100_patch = featurebits[6]; wire r2100_enable = r2100_hit & (r2100_patch | ~(&r2100_limit)); wire snoop_4200_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04200; +// regshadow CPU-side snoop range ($4200-$421F, any bank with A22=0) +wire snoop_42xx_enable = ~SNES_ADDR[22] & (SNES_ADDR[15:5] == 11'b01000010000); +// regshadow PPU-side snoop window: any B-bus write to a PPU reg ($2100-$213F) +wire rs_snoop_pawr_oe = ~SNES_PAWR & (SNES_PA < 8'h40); wire r4016_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04016; always @(posedge CLK2) begin @@ -543,9 +638,25 @@ assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr & ~(r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE)) ? (msu_enable ? MSU_SNES_DATA_OUT :cx4_enable ? CX4_SNES_DATA_OUT - :(cx4_active & cx4_vect_enable) ? CX4_SNES_DATA_OUT + // savestate scan window ($E8:00xx while unlocked): served by + // the CX4 window mux, ahead of the PSRAM identity serve. + :cx4_ss_enable ? CX4_SNES_DATA_OUT + // The hook redirect wins over the CX4 vector override + // ($FFE0-$FFFF), which would otherwise mask the redirected + // vector and the handler's own fetches near a bank end. + :(cx4_active & cx4_vect_enable & ~IS_PATCH & ~cheat_hit) ? CX4_SNES_DATA_OUT :(cheat_hit & ~feat_cmd_unlock) ? cheat_data_out :(snescmd_unlock | feat_cmd_unlock) & snescmd_enable ? snescmd_dout + // in-game savestate register shadow: $F90500 (PPU, stride-2) / + // $F90700 (CPU $42xx) read-backs. A stride-2 PPU entry is a + // PAIR, not a duplicate: even byte = 1st write (prev), odd byte + // = 2nd write (current), so the handler's double-writing restore + // replays scroll/mode-7 in the right order (ctx.v-style, see + // regshadow.v). Single-write regs store (value, value), so the + // high byte is never $00 (that zeroed BGMODE/TM/INIDISP -> + // backdrop-only screen, seen on SA-1 hw). + :shadow_ppu_hit ? regshadow_dout + :shadow_cpu_hit ? regshadow_dout :(ROM_ADDR0 ? ROM_DATA[7:0] : ROM_DATA[15:8]) ): 8'bZ; @@ -578,6 +689,18 @@ my_dcm snes_dcm( .LOCKED(DCM_LOCKED), .RST(DCM_RST) ); +// Second DCM (Mk.II only) drives the 96 MHz snoop clock for the handler register +// shadow. CLK2 runs at 80 MHz for CX4 cycle fidelity, but the bus-snoop shift +// patterns and the count==4 write-capture point are calibrated for 96 MHz; without +// this the rs96_* machinery and regshadow would be clocked by a dead net and the +// shadow would restore garbage into the PPU/CPU registers on handler exit. On the +// Mk.III this same 96 MHz comes from the PLL's .c1 output (see the pll instance). +my_dcm96 snes_dcm96( + .CLKIN(CLKIN), + .CLKFX(CLK96), + .LOCKED(), + .RST(DCM_RST) +); assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:1] : MCU_HIT ? ROM_ADDRr[23:1] : CX4_HIT ? CX4_ADDRr[23:1] : MAPPED_SNES_ADDR[23:1]; assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : MCU_HIT ? ROM_ADDRr[0] : CX4_HIT ? CX4_ADDRr[0] : MAPPED_SNES_ADDR[0]; assign ROM_CE = 1'b0; @@ -601,6 +724,11 @@ snescmd_buf snescmd ( pll snes_pll( .inclk0(CLKIN), .c0(CLK2), + // 96 MHz snoop clock for the savestate register shadow only. CLK2 runs at 80 MHz + // here for CX4 cycle fidelity, but the bus-snoop capture patterns are calibrated + // for 96 MHz sampling and land on the wrong bus phase at 80. Everything except + // the shadow stays on CLK2. + .c1(CLK96), .locked(DCM_LOCKED), .areset(DCM_RST) ); @@ -677,8 +805,20 @@ always @(posedge CLK2) begin case(STATE) ST_IDLE: begin STATE <= ST_IDLE; - if(cx4_active) begin - if (CX4_RD_PENDr) begin + // Pause semantics ($202C): freeze the CPU, drain the pipes. A pending CX4 + // read is still serviced while paused -- the cache/DMA fill FSMs are not + // pause-gated (see cx4.v), so an in-flight fill completes instead of freezing + // mid-burst and corrupting the cached program. With nothing pending, fall + // through to the MCU/SNES path so USB/MCU access does not starve behind a + // frozen-busy cx4_active. + // + // Under the hook the grant must be free_slot-gated: ST_CX4_RD_ADDR takes the + // PSRAM address bus away from the SNES for most of a page fill, and the + // handler executes from PSRAM through the IS_PATCH window, so an ungated fill + // corrupts its own instruction stream (the same class as the GSU's RON + // starvation). Outside the hook this is bit-for-bit the old behaviour. + if(cx4_active & (CX4_RD_PENDr | ~snapshot_pause)) begin + if (CX4_RD_PENDr & (~snescmd_unlock | free_slot | SNES_DEADr)) begin STATE <= ST_CX4_RD_ADDR; ST_MEM_DELAYr <= 16; end @@ -820,6 +960,13 @@ assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : (cx4_active & cx4_vect_enable) ? 1'b0 : (r213f_enable & ~SNES_PARD) ? 1'b0 : (r2100_enable & ~SNES_PAWR) ? 1'b0 : + // regshadow write snoop: enable the level shifter for PPU + // B-bus and $42xx A-bus writes, which are otherwise not + // cart-mapped and would be snooped as bus float (the base + // core does this via SNES_SNOOPPAWR_DATA_OE). DIR is gated + // alongside, see SNES_DATABUS_DIR. + rs_snoop_pawr_oe ? 1'b0 : + (snoop_42xx_enable & ~SNES_WRITE) ? 1'b0 : snoop_4200_enable ? SNES_WRITE : snescmd_enable ? (~(snescmd_unlock | feat_cmd_unlock) | (SNES_READ_narrow & SNES_WRITE)) : ((IS_ROM & SNES_ROMSEL) @@ -832,7 +979,11 @@ assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : * a) the SNES wants to read * b) we want to force a value on the bus */ -assign SNES_DATABUS_DIR = (~SNES_READ | (~SNES_PARD & (r213f_enable))) +// During a snooped B-bus write the concurrent A-bus read (/RD low on DMA/HDMA) must +// not flip the shifter to drive, unless the FPGA is serving that source itself: +// ROM_HIT for ROM/PSRAM, cx4_enable for DMA out of the CX4 RAM/MMIO. Without the +// latter, a game DMA of CX4-computed sprite data reads float. +assign SNES_DATABUS_DIR = ((~SNES_READ & (~rs_snoop_pawr_oe | ROM_HIT | cx4_enable)) | (~SNES_PARD & (r213f_enable))) ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE)) : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite diff --git a/verilog/sd2snes_cx4/regshadow.v b/verilog/sd2snes_cx4/regshadow.v new file mode 100644 index 00000000..573efad2 --- /dev/null +++ b/verilog/sd2snes_cx4/regshadow.v @@ -0,0 +1,114 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// Company: sd2snes +// Module Name: regshadow +// Description: +// Write-only shadow of the PPU ($2100-$213F) and CPU ($4200-$421F) registers, +// read back by the in-game savestate handler through the hook window: +// $F90500-$F9057F : PPU regs, stride-2 (1st write, 2nd write) +// $F90700-$F9071F : CPU regs, stride-1 +// base/DSP/SA-1 get this from ctx.v, which does not fit the mk2 Spartan-3. +// +// The scroll ($210D-$2114) and mode-7 ($211B-$2120) registers latch 16 bits from +// two consecutive writes, so the pair is stored, not just the last byte (ctx.v +// does the same via rBG/rM7). Non-double regs store (value, value). +// +// Storage (256x8, one RAMB16): +// mem[$00-$7F] PPU pairs, mem[{PA,1'b0}] = 1st write, mem[{PA,1'b1}] = 2nd +// mem[$80-$9F] CPU regs +// REGSHADOW_1DEEP selects the older layout (PPU mem[$00-$3F], CPU mem[$40-$5F]); +// main.v indexes both under the same macro. +// +// Compile gates, mutually exclusive, for mk2 area/timing: +// REGSHADOW_NO_M7 drop the mode-7 tracker, keep the scroll pair (gsu mk2) +// REGSHADOW_1DEEP drop the pair scheme entirely (cx4 mk2) +////////////////////////////////////////////////////////////////////////////////// +module regshadow( + input clk, + input pawr_end, // settled rising edge of /PAWR + input wr_end, // settled rising edge of /WR + input [23:0] snes_addr, + input [7:0] snes_pa, + input [7:0] snes_data, + input [8:0] rd_addr, + output reg [7:0] rd_data +); + +// 1DEEP removes what NO_M7 thins, so defining both is always a mistake and 1DEEP +// would silently win. The bare identifier is illegal Verilog: fail at parse time. +`ifdef REGSHADOW_1DEEP + `ifdef REGSHADOW_NO_M7 + ERROR_REGSHADOW_1DEEP_and_REGSHADOW_NO_M7_are_mutually_exclusive + `endif +`endif + +(* ram_style = "block" *) reg [7:0] mem [0:255]; + +wire ppu_wr = pawr_end & (snes_pa < 8'h40); +// $4200-$421F in any bank with ADDR[22]=0: games write them through FastROM banks, +// so a bank-$00-only decode misses those writes. +wire cpu_wr = wr_end & ~snes_addr[22] + & (snes_addr[15:5] == 11'b01000010000); + +`ifdef REGSHADOW_1DEEP +wire wr_en = ppu_wr | cpu_wr; +wire [7:0] wr_a = ppu_wr ? {2'b00, snes_pa[5:0]} + : {3'b010, snes_addr[4:0]}; + +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= snes_data; + rd_data <= mem[rd_addr[7:0]]; +end +`else +// Previous-byte trackers (ctx.v's rBG/rM7): consumed before being updated. +reg [7:0] prev_bg; initial prev_bg = 0; +`ifndef REGSHADOW_NO_M7 +reg [7:0] prev_m7; initial prev_m7 = 0; +`endif +wire is_bg_dbl = (snes_pa >= 8'h0D) && (snes_pa <= 8'h14); +`ifndef REGSHADOW_NO_M7 +wire is_m7_dbl = ((snes_pa >= 8'h0D) && (snes_pa <= 8'h0E)) + || ((snes_pa >= 8'h1B) && (snes_pa <= 8'h20)); +`endif + +// The strobe cycle stores the current byte (odd offset), the next cycle stores the +// previous one (even offset) out of these defer flops. Bus writes are dozens of +// clocks apart, so the defer can never collide with the next write. +reg wr2_pend; initial wr2_pend = 0; +reg [7:0] wr2_a; initial wr2_a = 0; +reg [7:0] wr2_d; initial wr2_d = 0; + +wire wr_en = ppu_wr | cpu_wr | wr2_pend; +wire [7:0] wr_a = wr2_pend ? wr2_a + : ppu_wr ? {1'b0, snes_pa[5:0], 1'b1} + : {3'b100, snes_addr[4:0]}; +wire [7:0] wr_d = wr2_pend ? wr2_d : snes_data; + +// One write enable and one write address in the process: anything else falls out +// of the XST block-RAM template and the memory is built from flip-flops instead. +// The arm is edge-guarded so a strobe wider than one cycle cannot re-arm with the +// trackers already updated, which would degrade the pair back to (value, value). +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= wr_d; + rd_data <= mem[rd_addr[7:0]]; + if (ppu_wr & ~wr2_pend) begin + wr2_pend <= 1'b1; + wr2_a <= {1'b0, snes_pa[5:0], 1'b0}; +`ifndef REGSHADOW_NO_M7 + wr2_d <= is_bg_dbl ? prev_bg : is_m7_dbl ? prev_m7 : snes_data; +`else + wr2_d <= is_bg_dbl ? prev_bg : snes_data; +`endif + if (is_bg_dbl) prev_bg <= snes_data; +`ifndef REGSHADOW_NO_M7 + if (is_m7_dbl) prev_m7 <= snes_data; +`endif + end else if (~ppu_wr) begin + wr2_pend <= 1'b0; + end +end +`endif + +endmodule diff --git a/verilog/sd2snes_cx4/sd2snes_cx4.xise b/verilog/sd2snes_cx4/sd2snes_cx4.xise index c41ae432..c8b64f95 100644 --- a/verilog/sd2snes_cx4/sd2snes_cx4.xise +++ b/verilog/sd2snes_cx4/sd2snes_cx4.xise @@ -19,6 +19,10 @@ + + + + @@ -31,6 +35,10 @@ + + + + @@ -446,8 +454,8 @@ - - + + @@ -508,7 +516,7 @@ - + diff --git a/verilog/sd2snes_cx4/tb_regshadow_dh.v b/verilog/sd2snes_cx4/tb_regshadow_dh.v new file mode 100644 index 00000000..c471ca9e --- /dev/null +++ b/verilog/sd2snes_cx4/tb_regshadow_dh.v @@ -0,0 +1,123 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// tb_regshadow_dh -- clock-accurate check of the CX4 savestate register-shadow capture +// (RAW-PULSE-ALIGNED shallow capture). The 80 MHz CX4 /PAWR pulse is only ~4 FPGA +// cycles; the deep SNES_PA debounce (PAr[5]&[4], ~5 cyc) lagged past it, a cycle-count +// strobe overshot into the next store, and committing PA at the debounced rising edge +// grabbed the NEXT store's address in a tight BG-setup burst (STA $2107/$2108/$210B). +// Fix: take DATA, PA and the /PAWR gate all from the SAME shallow 1-cycle tap +// (SNES_*r[0]); latch during the pulse and commit at its end, so the current store's +// PA+DATA are captured together, before the next store's pulse. This TB models the +// exact pipeline plus a TIGHT back-to-back burst (small inter-write gap) -- the case +// that corrupted the BG on hardware. +////////////////////////////////////////////////////////////////////////////////// +module tb_regshadow_dh; + reg clk = 0; + always #5 clk = ~clk; + + reg SNES_PAWR_IN = 1'b1; + reg [7:0] SNES_PA_IN = 8'h00; + reg [7:0] SNES_DATA = 8'h00; + + reg [7:0] SNES_PAWRr = 8'b11111111; + reg [7:0] SNES_PAr [6:0]; + reg [7:0] SNES_DATAr [1:0]; + integer k; + initial begin + for(k=0;k<7;k=k+1) SNES_PAr[k]=8'h00; + SNES_DATAr[0]=8'h00; SNES_DATAr[1]=8'h00; + end + + always @(posedge clk) begin + SNES_PAWRr <= {SNES_PAWRr[6:0], SNES_PAWR_IN}; + SNES_PAr[6]<=SNES_PAr[5]; SNES_PAr[5]<=SNES_PAr[4]; SNES_PAr[4]<=SNES_PAr[3]; + SNES_PAr[3]<=SNES_PAr[2]; SNES_PAr[2]<=SNES_PAr[1]; SNES_PAr[1]<=SNES_PAr[0]; + SNES_PAr[0]<=SNES_PA_IN; + SNES_DATAr[1]<=SNES_DATAr[0]; SNES_DATAr[0]<=SNES_DATA; + end + + // ---- raw-pulse-aligned capture under test (verbatim plan for main.v) ---- + reg rs_pawr0_d = 1'b1; + always @(posedge clk) rs_pawr0_d <= SNES_PAWRr[0]; + wire rs_commit = SNES_PAWRr[0] & ~rs_pawr0_d; // /PAWR rising = pulse end + reg [7:0] rs_data_l = 0, rs_pa_l = 0; + always @(posedge clk) if(~SNES_PAWRr[0]) begin // during the pulse (raw low, 1-cyc) + rs_data_l <= SNES_DATAr[0]; + rs_pa_l <= SNES_PAr[0]; + end + + reg [8:0] rd_addr = 0; + wire [7:0] rd_data; + regshadow dut( + .clk(clk), .pawr_end(rs_commit), .wr_end(1'b0), + .snes_addr(24'h000000), .snes_pa(rs_pa_l), .snes_data(rs_data_l), + .rd_addr(rd_addr), .rd_data(rd_data) + ); + + integer errors = 0; + + // single write with address setup/hold, /PAWR low `width`, then `gap` idle cycles + task ppu_write(input [7:0] pa, input [7:0] data, input integer width, input integer gap); + integer i; + begin + SNES_PA_IN = pa; @(posedge clk); @(posedge clk); + SNES_DATA = data; @(posedge clk); + SNES_PAWR_IN = 1'b0; + for(i=0;i PASS: raw-pulse capture correct for short/long/burst pulses"); + else $display("\n==> FAIL: %0d error(s)", errors); + $finish; + end +endmodule diff --git a/verilog/sd2snes_cx4/tb_regshadow_wide.v b/verilog/sd2snes_cx4/tb_regshadow_wide.v new file mode 100644 index 00000000..0df3f41d --- /dev/null +++ b/verilog/sd2snes_cx4/tb_regshadow_wide.v @@ -0,0 +1,113 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// tb_regshadow_wide -- proves the defer arm is edge-guarded against a multi-cycle `pawr_end`. +// All four cores emit a 1-cycle strobe today; this TB asserts that (a) width==1 is +// bit-identical to the unguarded form and (b) widths 2/3/5 still reconstruct the +// (prev, current) pair instead of silently degrading to (value, value). +// Under REGSHADOW_1DEEP there is no defer at all (a single write per strobe cycle, +// idempotent while the strobe is held), so the same stimulus must land the last byte. +////////////////////////////////////////////////////////////////////////////////// +`ifdef REGSHADOW_1DEEP + `define P1(pa) (9'h000 + (pa)) + `define P2(pa) (9'h000 + (pa)) +`else + `define P1(pa) (9'h000 + ((pa)<<1)) + `define P2(pa) (9'h000 + ((pa)<<1) + 1) +`endif +`ifdef REGSHADOW_1DEEP + `define BG1ST(lo,hi) (hi) + `define M71ST(lo,hi) (hi) +`elsif REGSHADOW_NO_M7 + `define BG1ST(lo,hi) (lo) + `define M71ST(lo,hi) (hi) +`else + `define BG1ST(lo,hi) (lo) + `define M71ST(lo,hi) (lo) +`endif + +module tb_regshadow_wide; + reg clk = 0; always #5 clk = ~clk; + reg pawr_end = 0, wr_end = 0; + reg [23:0] snes_addr = 0; reg [7:0] snes_pa = 0, snes_data = 0; + reg [8:0] rd_addr = 0; wire [7:0] rd_data; + integer errors = 0; + + regshadow dut(.clk(clk), .pawr_end(pawr_end), .wr_end(wr_end), .snes_addr(snes_addr), + .snes_pa(snes_pa), .snes_data(snes_data), .rd_addr(rd_addr), .rd_data(rd_data)); + + // PPU write with the strobe held high for `width` module cycles; PA/data are held + // stable for the whole pulse (that is what a real widened strobe would look like, + // since the taps are latched during the /PAWR window), then the bus goes to junk. + task ppuw_w(input [7:0] pa, input [7:0] d, input integer width); + integer i; + begin + @(negedge clk); snes_pa = pa; snes_data = d; pawr_end = 1; + for (i = 0; i < width; i = i + 1) @(negedge clk); + pawr_end = 0; snes_data = 8'hAA; snes_pa = 8'h7F; // junk after the pulse + repeat (8) @(negedge clk); + end + endtask + + task chk(input [8:0] off, input [7:0] exp, input [255:0] name); + begin + rd_addr = off; @(posedge clk); @(posedge clk); + if (rd_data !== exp) begin + errors = errors + 1; + $display(" FAIL %0s mem[$%03h]=$%02h exp $%02h", name, off, rd_data, exp); + end else + $display(" OK %0s mem[$%03h]=$%02h", name, off, rd_data); + end + endtask + + // BG1HOFS ($210D) written low-then-high, the Star Fox scroll case. + task bg_pair_at_width(input integer w); + begin + $display("== strobe width %0d: $210D <- $F8 then $01 ==", w); + ppuw_w(8'h0D, 8'hF8, w); + ppuw_w(8'h0D, 8'h01, w); + chk(`P1(8'h0D), `BG1ST(8'hF8, 8'h01), "BG1HOFS 1st"); + chk(`P2(8'h0D), 8'h01, "BG1HOFS 2nd"); + end + endtask + + initial begin +`ifdef REGSHADOW_1DEEP + $display("### mode: REGSHADOW_1DEEP (no defer; wide strobe = idempotent rewrite) ###"); +`else + `ifdef REGSHADOW_NO_M7 + $display("### mode: REGSHADOW_NO_M7 ###"); + `else + $display("### mode: default ###"); + `endif +`endif + repeat (4) @(negedge clk); + + bg_pair_at_width(1); // the real hardware case -- must stay byte-identical + bg_pair_at_width(2); // degrades to (v,v) without the ~wr2_pend guard + bg_pair_at_width(3); + bg_pair_at_width(5); + + $display("== width 3, M7A ($211B) pair =="); + ppuw_w(8'h1B, 8'h20, 3); ppuw_w(8'h1B, 8'h01, 3); + chk(`P1(8'h1B), `M71ST(8'h20, 8'h01), "M7A 1st"); + chk(`P2(8'h1B), 8'h01, "M7A 2nd"); + + $display("== width 4, non-double $2105 stays (v,v) =="); + ppuw_w(8'h05, 8'h09, 4); + chk(`P1(8'h05), 8'h09, "BGMODE 1st"); chk(`P2(8'h05), 8'h09, "BGMODE 2nd"); + + $display("== width 2 burst across regs: $210D,$210E,$210D,$210E =="); + ppuw_w(8'h0D, 8'h11, 2); ppuw_w(8'h0E, 8'h22, 2); + ppuw_w(8'h0D, 8'h33, 2); ppuw_w(8'h0E, 8'h44, 2); + // prev_bg is a single shared tracker (ctx.v semantics): the 1st-write slot of a + // reg holds whatever byte went to ANY BG-double reg immediately before it. + chk(`P1(8'h0D), `BG1ST(8'h22, 8'h33), "BG1HOFS 1st"); + chk(`P2(8'h0D), 8'h33, "BG1HOFS 2nd"); + chk(`P1(8'h0E), `BG1ST(8'h33, 8'h44), "BG1VOFS 1st"); + chk(`P2(8'h0E), 8'h44, "BG1VOFS 2nd"); + + if (errors) $display("\n==> FAIL: %0d error(s)", errors); + else $display("\n==> PASS: strobe widths 1..5 handled correctly in this mode"); + $finish; + end +endmodule diff --git a/verilog/sd2snes_dsp/Makefile b/verilog/sd2snes_dsp/Makefile index a33e924e..b4386a4d 100644 --- a/verilog/sd2snes_dsp/Makefile +++ b/verilog/sd2snes_dsp/Makefile @@ -1,6 +1,6 @@ CORE = dsp -VSRC = address.v cheat.v clk_test.v dac.v dcm.v upd77c25.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v ctx.v dac.v dcm.v dma.v upd77c25.v main.v mcu_cmd.v msu.v sd_dma.v spi.v VHSRC = COMMON_IP = dac_buf msu_databuf snescmd_buf upd77c25_datram upd77c25_datrom upd77c25_pgmrom diff --git a/verilog/sd2snes_dsp/address.v b/verilog/sd2snes_dsp/address.v index b2382872..a7ea5148 100644 --- a/verilog/sd2snes_dsp/address.v +++ b/verilog/sd2snes_dsp/address.v @@ -204,9 +204,12 @@ assign dspx_enable = ?(SNES_ADDR[22] & SNES_ADDR[21] & ~SNES_ADDR[20] & &(~SNES_ADDR[19:16]) & ~SNES_ADDR[15]) :1'b0; -assign dspx_dp_enable = featurebits[FEAT_ST0010] - &(SNES_ADDR[22:19] == 4'b1101 - && SNES_ADDR[15:11] == 5'b00000); +// Data-RAM / savestate-scan window, offset $0000-$07FF. ST0010: always, at banks +// $68-$6F/$E8-$EF. DSP1-4: only the $E8 window, and only while the handler holds +// the unlock, so it can never alias the game-readable LoROM band. +assign dspx_dp_enable = (SNES_ADDR[15:11] == 5'b00000) + & ( (featurebits[FEAT_ST0010] & (SNES_ADDR[22:19] == 4'b1101)) + | (featurebits[FEAT_DSPX] & (map_unlock | snescmd_unlock) & (SNES_ADDR[23:16] == 8'hE8)) ); assign dspx_a0 = featurebits[FEAT_DSPX] ?((MAPPER_DEC[3'b001]) ? SNES_ADDR[14] diff --git a/verilog/sd2snes_dsp/cheat.v b/verilog/sd2snes_dsp/cheat.v index 17877399..db619ce9 100644 --- a/verilog/sd2snes_dsp/cheat.v +++ b/verilog/sd2snes_dsp/cheat.v @@ -143,6 +143,7 @@ assign data_out = cheat_match_bits[0] ? cheat_data[0] /// BUT MUST NOT apply actual ROM cheat patches during snescmd menu bank /// execution to prevent ROM cheats from patching nonsense into the savestate /// handler. +/// this is caused by C0-FF bank overlay. Probably not a good idea. assign cheat_hit = (snescmd_unlock & hook_enable_sync & (nmicmd_enable | return_vector_enable | branch1_enable | branch2_enable | branch3_enable)) | (reset_unlock & rst_addr_match) | (cheat_enable & cheat_addr_match & ~snescmd_unlock) @@ -211,13 +212,31 @@ reg snescmd_unlock_disable_strobe = 1'b0; reg [6:0] snescmd_unlock_disable_countdown = 0; reg snescmd_unlock_disable = 0; +// force savestate handler entry until savestate handler returns on its own +// (in-game hook must keep jumping to savestate handler until its logic has finished) +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end + always @(posedge clk) begin + savestate_force_entry_disable_strobe <= 0; if(SNES_reset_strobe) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; map_unlock_r <= 0; exe_to_hook_transition_r <= 0; end else begin + if (~nmi_addr_match) exe_to_hook_transition_r <= 0; + else if (map_unlock_r) exe_to_hook_transition_r <= 1; + if(SNES_rd_strobe) begin // *** GAME -> USB HOOK *** if(hook_enable_sync @@ -229,7 +248,7 @@ always @(posedge clk) begin // remember where we came from (IRQ/NMI) for hook exit return_vector <= SNES_ADDR[7:0]; // unlock the address map - map_unlock_r <= 1; + map_unlock_r <= 1; // unlock exe code exe_unlock_r <= 1; end @@ -245,7 +264,7 @@ always @(posedge clk) begin // lock the address map map_unlock_r <= 0; // no longer in exe region - exe_unlock_r <= 0; + exe_unlock_r <= 0; end // *** USB HOOK -> GAME *** else if (exe_unlock & nmi_match_bits[1] @@ -272,7 +291,30 @@ always @(posedge clk) begin if(rst_match_bits[1] & |reset_unlock_r) begin snescmd_unlock_r <= 1; end + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; + end end + +/// TODO unlock disable on hook exit needs rework, there are potential issues: +/// +/// 1. Countdown needs to be short because jumping back to ROM would +/// otherwise yield wrong data (because of bank C0 overlay) +/// +/// 2. HDMA can interrupt the IRQ hook after writing the unlock trigger +/// so the number of countdown cycles needed may be much bigger but can't +/// be predicted, so countdown might be too short and disable nmi hook +/// unlock before the CPU can exit. (this happens on Star Fox (2)) +/// +/// 3. HDMA might access bank $C0 expecting ROM data during unlock but reads +/// menu bank data instead ((( CANNOT FIX --- REVERT C0-FF UNLOCK? ))) +/// +/// Possible solution: +/// 1. arm disable detection after disable trigger has been written +/// 2. wait for CPU to read 2 vector addresses (FFEA, FFEE, FFFC) and capture +/// the data read from those addresses +/// 3. disarm detection and disable unlock when CPU starts reading the address +/// captured in 2. // give some time to exit snescmd memory and jump to original vector // sta @NMI_VECT_DISABLE 1-2 (after effective write) @@ -285,6 +327,7 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; + savestate_force_entry_disable_strobe <= 1; end end end @@ -295,6 +338,7 @@ always @(posedge clk) begin end end + // Only clock the usage timeout when outside of in-game hook // to prevent nested IRQs from jumping to game // (otherwise FPGA might disable hook patching while still inside hook @@ -418,13 +462,13 @@ end always @* begin if(buttons_enable) begin if(snes_ajr) begin - if(nmicmd) begin + if(|nmicmd) begin branch1_offset = 8'h30; // nmi_echocmd end else begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - if(savestate_enable) begin + if(savestate_enable & (savestate_force_entry | |pad_data)) begin branch1_offset = 8'h3f; // nmi_savestate end else begin branch1_offset = 8'h43; // nmi_exit @@ -449,7 +493,7 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - if(savestate_enable) begin + if(savestate_enable & |pad_data) begin branch1_offset = 8'h3f; // nmi_savestate end else begin branch1_offset = 8'h43; // nmi_exit diff --git a/verilog/sd2snes_dsp/dma.v b/verilog/sd2snes_dsp/dma.v index 33e0bc5c..10c86893 100644 --- a/verilog/sd2snes_dsp/dma.v +++ b/verilog/sd2snes_dsp/dma.v @@ -31,7 +31,7 @@ module dma( input reg_we_rising, output loop_enable, - + input BUS_RDY, output BUS_RRQ, output BUS_WRQ, @@ -42,10 +42,10 @@ module dma( input [15:0] ROM_DATA_IN ); -parameter ST_IDLE = 0; -parameter ST_READ = 1; -parameter ST_WRITE = 2; -parameter ST_DONE = 3; +parameter ST_IDLE = 0; +parameter ST_READ = 1; +parameter ST_WRITE = 2; +parameter ST_DONE = 3; parameter OP_COPY = 0; parameter OP_RESET = 1; @@ -53,7 +53,7 @@ parameter OP_SET = 2; parameter OP_DEBUG = 3; // Register bank -reg [7:0] dma_r[15:0]; +reg [7:0] dma_r[9:0]; reg [2:0] state; initial state = ST_IDLE; @@ -97,7 +97,7 @@ always @(posedge clkin) begin dma_r[reg_addr] <= reg_data_in; end else if (state == ST_DONE) begin - dma_r[9][0] <= 0; + dma_r[9][0] <= 0; // clear single-op trigger end end @@ -123,6 +123,16 @@ reg trig_r; initial trig_r = 0; reg word_mode_r; reg [23:0] src_addr_r, dst_addr_r, length_r, mod_r; +// 1-trigger queue: the savestate handler fires its second op without polling, so a +// trigger arriving while the copier is busy is latched with the op snapshotted and +// started in ST_IDLE. One slot is all it needs; a further trigger while one is +// already pending is dropped, as every trigger during a busy copier was before. +reg pending; initial pending = 0; +reg [23:0] shadow_src, shadow_dst, shadow_len; +reg shadow_word, shadow_dir; +reg [4:0] shadow_op; +wire op_kick = reg_we_rising & enable & (reg_addr == 4'd9) & reg_data_in[0]; + assign BUS_RRQ = BUS_RDY && (state == ST_READ); assign BUS_WRQ = BUS_RDY && (state == ST_WRITE); assign ROM_ADDR = (state == ST_READ) ? src_addr_r : dst_addr_r; @@ -133,32 +143,60 @@ assign ROM_DATA_OUT = (opcode_r == OP_COPY) ? (dst_addr_r[0] ? {ROM_DATA_IN[7 : 0; assign ROM_WORD_ENABLE = word_mode_r; assign loop_enable = loop_r; - wire [23:0] length_next = length_r - (word_mode_r ? 2 : 1); always @(posedge clkin) begin if (reset) begin - loop_r <= 0; - state <= ST_IDLE; - trig_r <= 0; + loop_r <= 0; + state <= ST_IDLE; + trig_r <= 0; + pending <= 0; end else begin - trig_r <= TRIG; - + trig_r <= TRIG; + + // when idle the fresh trigger is handled by the op_kick path below + if (op_kick && (state != ST_IDLE) && !pending) begin + pending <= 1'b1; + shadow_src <= SRC_ADDR; + shadow_dst <= DST_ADDR; + shadow_len <= LEN; + shadow_word <= WORD_MODE; + shadow_op <= reg_data_in[7:3]; // dma_r[9] being written this cycle + shadow_dir <= reg_data_in[1]; + end + case (state) ST_IDLE: begin - if (TRIG && (trig_r ^ TRIG)) begin - src_addr_r <= SRC_ADDR; - dst_addr_r <= DST_ADDR; - length_r <= LEN; - mod_r <= WORD_MODE ? (DIR ? -2 : 2) : (DIR ? -1 : 1); - opcode_r <= OPCODE; - loop_r <= LOOP; - dir_r <= DIR; + if (pending) begin + // start the op snapshotted while the previous one ran + src_addr_r <= shadow_src; + dst_addr_r <= shadow_dst; + length_r <= shadow_len; + mod_r <= shadow_word ? (shadow_dir ? -2 : 2) : (shadow_dir ? -1 : 1); + opcode_r <= shadow_op; + word_mode_r <= shadow_word; + dir_r <= shadow_dir; + loop_r <= 0; + pending <= 0; + if (shadow_op == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; + end + else if (op_kick) begin + // Single-op start, detected by the dma_r[9] write rather than a TRIG + // edge: robust to the bit already being 1, which happens when a colliding + // register write skips the ST_DONE clear. + src_addr_r <= SRC_ADDR; + dst_addr_r <= DST_ADDR; + length_r <= LEN; + mod_r <= WORD_MODE ? (reg_data_in[1] ? -2 : 2) : (reg_data_in[1] ? -1 : 1); + opcode_r <= reg_data_in[7:3]; + loop_r <= reg_data_in[2]; + dir_r <= reg_data_in[1]; word_mode_r <= WORD_MODE; - - if (OPCODE == OP_COPY) state <= ST_READ; - else state <= ST_WRITE; + + if (reg_data_in[7:3] == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; end end ST_READ: begin @@ -179,7 +217,7 @@ always @(posedge clkin) begin end ST_DONE: begin loop_r <= 0; - state <= ST_IDLE; + state <= ST_IDLE; end endcase end diff --git a/verilog/sd2snes_dsp/main.qsf b/verilog/sd2snes_dsp/main.qsf index 94c6aee1..9d765cc8 100644 --- a/verilog/sd2snes_dsp/main.qsf +++ b/verilog/sd2snes_dsp/main.qsf @@ -527,4 +527,11 @@ set_global_assignment -name QIP_FILE ip/mk3/snescmd_buf.qip set_global_assignment -name QIP_FILE ip/mk3/msu_databuf.qip set_global_assignment -name QIP_FILE ip/mk3/dac_buf.qip set_global_assignment -name SDC_FILE main.sdc -set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top \ No newline at end of file +set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top + +# Fitter seed. The savestate window write pipeline (upd77c25.v) shifted placement +# just enough that the default seed closed at -0.004 ns on the 96 MHz domain -- 4 ps, +# i.e. placement noise, but `make mk3` (rightly) refuses any negative TNS. Seed 2 +# closes at +0.766 ns; seeds 3 and 5 also close (+0.671 / +0.318), so this is not a +# knife-edge pick. +set_global_assignment -name SEED 2 diff --git a/verilog/sd2snes_dsp/main.v b/verilog/sd2snes_dsp/main.v index ae201fdc..3e11b601 100644 --- a/verilog/sd2snes_dsp/main.v +++ b/verilog/sd2snes_dsp/main.v @@ -147,6 +147,10 @@ wire dspx_dat_we; wire [15:0] featurebits; wire feat_cmd_unlock = featurebits[5]; +wire dspx_ss_halt; +wire dspx_ss_halted; +wire dspx_ss_window_en = featurebits[0]; // FEAT_DSPX: 1 = DSP1-4 (sca handler), 0 = ST0010 + wire r213f_enable; wire [23:0] MAPPED_SNES_ADDR; @@ -544,7 +548,9 @@ upd77c25 snes_dspx ( .DI(DSPX_SNES_DATA_IN), .DO(DSPX_SNES_DATA_OUT), .A0(DSPX_A0), - .enable(dspx_enable), + // The $E8 window must not clock the live DR/SR side effects: the LoROM MMIO + // decode also matches $E0-$EF for <=1MB ROMs. + .enable(dspx_enable & ~dspx_dp_enable), .reg_oe_falling(SNES_RD_start), .reg_oe_rising(SNES_RD_end), .reg_we_rising(SNES_WR_end), @@ -558,6 +564,9 @@ upd77c25 snes_dspx ( .DAT_WR_ADDR(dspx_dat_addr), .DP_enable(dspx_dp_enable), .DP_ADDR(SNES_ADDR[10:0]), + .ss_halt(dspx_ss_halt), + .ss_window_en(dspx_ss_window_en), + .ss_halted(dspx_ss_halted), .dsp_feat(dsp_feat) ); `endif @@ -628,6 +637,7 @@ mcu_cmd snes_mcu_cmd( .dspx_dat_addr_out(dspx_dat_addr), .dspx_dat_we_out(dspx_dat_we), .dspx_reset_out(dspx_reset), + .dspx_ss_halt_out(dspx_ss_halt), .featurebits_out(featurebits), .mcu_rrq(MCU_RRQ), .mcu_wrq(MCU_WRQ), diff --git a/verilog/sd2snes_dsp/mcu_cmd.v b/verilog/sd2snes_dsp/mcu_cmd.v index 89fd1998..c325b572 100644 --- a/verilog/sd2snes_dsp/mcu_cmd.v +++ b/verilog/sd2snes_dsp/mcu_cmd.v @@ -90,6 +90,7 @@ module mcu_cmd( output reg dspx_dat_we_out, output reg dspx_reset_out, + output reg dspx_ss_halt_out = 1'b0, // savestate halt (MCU debug) // feature enable output reg [15:0] featurebits_out, @@ -362,6 +363,8 @@ always @(posedge clk) begin endcase 8'heb: // control DSPx reset dspx_reset_out <= param_data[0]; + 8'hfb: // control DSPx savestate halt (MCU debug; SNES side uses the $E8:07FF scan-window control byte) + dspx_ss_halt_out <= param_data[0]; 8'hec: begin // set DAC properties dac_vol_select_out <= param_data[2:0]; diff --git a/verilog/sd2snes_dsp/sd2snes_dsp.xise b/verilog/sd2snes_dsp/sd2snes_dsp.xise index 76145d1b..aab78a8e 100644 --- a/verilog/sd2snes_dsp/sd2snes_dsp.xise +++ b/verilog/sd2snes_dsp/sd2snes_dsp.xise @@ -394,8 +394,8 @@ - - + + diff --git a/verilog/sd2snes_dsp/upd77c25.v b/verilog/sd2snes_dsp/upd77c25.v index 7ccb74c5..2b731b0c 100644 --- a/verilog/sd2snes_dsp/upd77c25.v +++ b/verilog/sd2snes_dsp/upd77c25.v @@ -42,6 +42,11 @@ module upd77c25( input [15:0] dsp_feat, + // savestate scan port (Phase 1: read-only) + input ss_halt, // MCU debug halt request + input ss_window_en, // 1 = DSP1-4 (sca handler active); 0 = ST0010 (plain RAM) + output ss_halted, // 1 = freeze in effect, safe to snapshot/restore + // debug output [15:0] updDR, output [15:0] updSR, @@ -160,7 +165,12 @@ reg [7:0] DP_DOr; wire [7:0] DP_DO; wire [7:0] UPD_DO; -wire ram_web = reg_we_rising & DP_enable; +// suppress RAM write when the access targets the scan register-file / control +// Declared before first use: XST rejects use-before-declaration, Quartus does not. +wire ss_ctrl; +wire ss_regwin; +reg ss_frozen; initial ss_frozen = 1'b0; +wire ram_web = reg_we_rising & DP_enable & ~ss_regwin & ~ss_ctrl; `ifdef MK2 `ifndef DEBUG @@ -191,7 +201,9 @@ upd77c25_datram datram ( .q_b(DP_DO) // output [7 : 0] doutb ); `endif -assign ram_wea = ((op != I_JP) && op_dst == 4'b1111 && insn_state == STATE_NEXT); +// Gate the core's port-A RAM write while halted so it cannot race the port-B +// restore; a pending write replays on unhalt, since insn_state is preserved. +assign ram_wea = ((op != I_JP) && op_dst == 4'b1111 && insn_state == STATE_NEXT) & ~ss_frozen; assign ram_addra = {regs_dpb, regs_dph | ((|(insn_state & (STATE_ALU1 | STATE_ALU2)) && op_dst == 4'b1100) ? 4'b0100 @@ -240,6 +252,129 @@ assign updB = regs_ab[1]; assign updFL_A = {flags_s1[0],flags_s0[0],flags_c[0],flags_z[0],flags_ov1[0],flags_ov0[0]}; assign updFL_B = {flags_s1[1],flags_s0[1],flags_c[1],flags_z[1],flags_ov1[1],flags_ov0[1]}; +// ---- savestate scan port ------------------------------------------------- +// While halted every always block below holds. The architectural and pipeline +// state is exposed as a flat byte window at DP_ADDR $600-$6FF (a port-B gap DSP1 +// never touches); $7FF is the halt control byte. Offset map below. +// ss_window_en separates DSP1-4 from the ST0010, which shares this core and uses +// the full 2 KB RAM window for the game; there it is 0 and the window stays RAM. +// Two halt sources: ss_halt (MCU) and ss_halt_snes (handler, via $7FF). The +// latter lives in its own block so it stays writable while everything else holds. +reg ss_halt_snes; +initial ss_halt_snes = 1'b0; +wire ss_halt_eff = ss_halt | ss_halt_snes; +assign ss_ctrl = ss_window_en & DP_enable & (DP_ADDR == 11'h7ff); +always @(posedge CLK) begin + if(~RST) ss_halt_snes <= 1'b0; + else if(ss_ctrl & reg_we_rising) ss_halt_snes <= DI[0]; +end + +// Boundary-gated freeze: on a halt request let the DSP finish the current +// transaction and stop at an idle instruction boundary, so the cut stays +// consistent with the SNES CPU and the handshake re-syncs on resume. A counter +// forces the freeze after 256 cycles, so it can never hang. +reg [7:0] ss_wait; initial ss_wait = 8'h00; +wire ss_boundary = (insn_state == STATE_FETCH) & regs_sr[SR_RQM]; +always @(posedge CLK) begin + if(~RST | ~ss_halt_eff) begin + ss_frozen <= 1'b0; + ss_wait <= 8'h00; + end else if(~ss_frozen) begin + ss_wait <= ss_wait + 1'b1; + if(ss_boundary | (&ss_wait)) ss_frozen <= 1'b1; + end +end +assign ss_halted = ss_frozen; + +// $600-$6FF register-file window (live only once actually frozen) +assign ss_regwin = ss_window_en & DP_enable & ss_frozen & (DP_ADDR[10:8] == 3'b110); + +// Window writes are pipelined one cycle through local flops. DP_enable comes from +// address.v, which ANDs the hook unlock (a flop in cheat.v, far away on the die) +// with the address decode; feeding that straight into the register-file write +// demux builds a 7-level path that misses timing on the Mk.II. Latching the +// request first makes those writes flop-to-flop out of local registers, and the +// extra cycle is invisible: the write strobe comes from a SNES bus cycle that +// lasts tens of clocks. +reg ss_wr_r; initial ss_wr_r = 1'b0; +reg [7:0] ss_off_r; initial ss_off_r = 8'h00; +reg [7:0] ss_di_r; initial ss_di_r = 8'h00; +always @(posedge CLK) begin + ss_wr_r <= ss_regwin & reg_we_rising; + ss_off_r <= DP_ADDR[7:0]; + ss_di_r <= DI; +end +wire [3:0] ss_stk_idx_r = (ss_off_r - 8'h34) >> 1; +wire [3:0] ss_stk_idx = (DP_ADDR[7:0] - 8'h34) >> 1; +reg [7:0] ss_reg_do; +// Read the arrays through scalar wires so the combinational readback mux below reads +// scalars, not 2D arrays -- XST (mk2) rejects a memory array in an @(*) sensitivity list +// (Xst:902 "Unexpected ... event"); Quartus (mk3) tolerates it. Behavior-neutral. +wire [15:0] ss_rab0 = regs_ab[0]; +wire [15:0] ss_rab1 = regs_ab[1]; +wire [10:0] ss_stk = stack[ss_stk_idx]; +always @(*) begin + if (DP_ADDR[7:0] >= 8'h34 && DP_ADDR[7:0] <= 8'h53) + ss_reg_do = DP_ADDR[0] ? {5'b0, ss_stk[10:8]} + : ss_stk[7:0]; + else case (DP_ADDR[7:0]) + 8'h00: ss_reg_do = pc[7:0]; + 8'h01: ss_reg_do = {5'b0, pc[10:8]}; + 8'h02: ss_reg_do = ss_rab0[7:0]; + 8'h03: ss_reg_do = ss_rab0[15:8]; + 8'h04: ss_reg_do = ss_rab1[7:0]; + 8'h05: ss_reg_do = ss_rab1[15:8]; + 8'h06: ss_reg_do = regs_tr[7:0]; + 8'h07: ss_reg_do = regs_tr[15:8]; + 8'h08: ss_reg_do = regs_trb[7:0]; + 8'h09: ss_reg_do = regs_trb[15:8]; + 8'h0a: ss_reg_do = regs_dr[7:0]; + 8'h0b: ss_reg_do = regs_dr[15:8]; + 8'h0c: ss_reg_do = regs_sr[7:0]; + 8'h0d: ss_reg_do = regs_sr[15:8]; + 8'h0e: ss_reg_do = regs_rp[7:0]; + 8'h0f: ss_reg_do = {5'b0, regs_rp[10:8]}; + 8'h10: ss_reg_do = regs_k[7:0]; + 8'h11: ss_reg_do = regs_k[15:8]; + 8'h12: ss_reg_do = regs_l[7:0]; + 8'h13: ss_reg_do = regs_l[15:8]; + 8'h14: ss_reg_do = regs_m[7:0]; + 8'h15: ss_reg_do = regs_m[15:8]; + 8'h16: ss_reg_do = regs_n[7:0]; + 8'h17: ss_reg_do = regs_n[15:8]; + 8'h18: ss_reg_do = {regs_dph, regs_dpl}; + 8'h19: ss_reg_do = {6'b0, regs_dpb}; + 8'h1a: ss_reg_do = {4'b0, regs_sp}; + 8'h1b: ss_reg_do = insn_state; + 8'h1c: ss_reg_do = {2'b0, updFL_A}; + 8'h1d: ss_reg_do = {2'b0, updFL_B}; + 8'h1e: ss_reg_do = idb[7:0]; + 8'h1f: ss_reg_do = idb[15:8]; + 8'h20: ss_reg_do = alu_p[7:0]; + 8'h21: ss_reg_do = alu_p[15:8]; + 8'h22: ss_reg_do = alu_q[7:0]; + 8'h23: ss_reg_do = alu_q[15:8]; + 8'h24: ss_reg_do = alu_r[7:0]; + 8'h25: ss_reg_do = alu_r[15:8]; + 8'h26: ss_reg_do = ram_dina_r[7:0]; + 8'h27: ss_reg_do = ram_dina_r[15:8]; + 8'h28: ss_reg_do = ld_id[7:0]; + 8'h29: ss_reg_do = ld_id[15:8]; + 8'h2a: ss_reg_do = {op, op_pselect, op_alu}; + 8'h2b: ss_reg_do = {op_asl, op_dpl, op_dphm, op_rpdcr}; + 8'h2c: ss_reg_do = {op_src, op_dst}; + 8'h2d: ss_reg_do = {ld_dst, 1'b0, alu_store, cond_true}; + 8'h2e: ss_reg_do = jp_brch[7:0]; + 8'h2f: ss_reg_do = {7'b0, jp_brch[8]}; + 8'h30: ss_reg_do = jp_na[7:0]; + 8'h31: ss_reg_do = {5'b0, jp_na[10:8]}; + 8'h32: ss_reg_do = {4'b0, cpu_wait}; + 8'h33: ss_reg_do = 8'hd1; // magic, sanity-check on restore + default: ss_reg_do = 8'h00; + endcase +end +// -------------------------------------------------------------------------- + initial begin alu_store = 2'b11; insn_state = STATE_IDLE1; @@ -266,7 +401,7 @@ initial begin end always @(posedge CLK) begin - if(RST) begin + if(RST & ~ss_frozen) begin if(enable & reg_we_rising & (A0 == 1'b0)) begin if(!regs_sr[SR_DRC]) begin if(regs_sr[SR_DRS] == 1'b1) begin @@ -288,13 +423,16 @@ always @(posedge CLK) begin || (op_dst == 4'b0110 && op != 2'b10 && insn_state == STATE_STORE)) begin regs_sr[SR_RQM] <= 1'b1; end + end else if(RST & ss_frozen) begin + if(ss_wr_r & (ss_off_r == 8'h0d)) + regs_sr[SR_RQM] <= ss_di_r[7]; // restore bit 15 (offset $0d high byte) end else begin regs_sr[SR_RQM] <= 1'b0; end end always @(posedge CLK) begin - if(RST) begin + if(RST & ~ss_frozen) begin if(enable & reg_we_rising & (A0 == 1'b0)) begin if(!regs_sr[SR_DRC]) begin if(regs_sr[SR_DRS] == 1'b0) begin @@ -316,13 +454,16 @@ always @(posedge CLK) begin end endcase end + end else if(RST & ss_frozen) begin + if(ss_wr_r & (ss_off_r == 8'h0d)) + regs_sr[SR_DRS] <= ss_di_r[4]; // restore bit 12 (offset $0d high byte) end else begin regs_sr[SR_DRS] <= 1'b0; end end always @(posedge CLK) begin - if(RST) begin + if(RST & ~ss_frozen) begin if(enable & reg_we_rising & (A0 == 1'b0)) begin if(!regs_sr[SR_DRC]) begin if(regs_sr[SR_DRS] == 1'b0) begin @@ -337,16 +478,23 @@ always @(posedge CLK) begin if (op == I_OP || op == I_RT) regs_dr <= idb; else if (op == I_LD) regs_dr <= ld_id; end + end else if(RST & ss_frozen) begin + if(ss_wr_r) begin + if(ss_off_r == 8'h0a) regs_dr[7:0] <= ss_di_r; // restore low byte + if(ss_off_r == 8'h0b) regs_dr[15:8] <= ss_di_r; // restore high byte + end end else begin regs_dr <= 16'h0000; end end assign UPD_DO = (A0 ? regs_sr[15:8] : (regs_sr[SR_DRC] ? regs_dr[7:0] : (regs_sr[SR_DRS] ? regs_dr[15:8] : regs_dr[7:0]))); -assign DO = DP_enable ? DP_DO : UPD_DO; +assign DO = ss_ctrl ? {7'b0, ss_halted} + : ss_regwin ? ss_reg_do + : (DP_enable ? DP_DO : UPD_DO); always @(posedge CLK) begin - if(RST) begin + if(RST & ~ss_frozen) begin case(insn_state) STATE_FETCH: begin insn_state <= STATE_LOAD; @@ -636,6 +784,71 @@ always @(posedge CLK) begin endcase end endcase + end else if(RST & ss_frozen) begin + // savestate RESTORE (Phase 2): load FSM-owned state from the scan window. + // regs_dr and regs_sr bits 15/12 are restored in their own blocks below; + // everything else is here. Mirrors the read mux offset map exactly. + if(ss_wr_r) begin + if(ss_off_r >= 8'h34 && ss_off_r <= 8'h53) begin + if(ss_off_r[0]) stack[ss_stk_idx_r][10:8] <= ss_di_r[2:0]; + else stack[ss_stk_idx_r][7:0] <= ss_di_r; + end else case(ss_off_r) + 8'h00: pc[7:0] <= ss_di_r; + 8'h01: pc[10:8] <= ss_di_r[2:0]; + 8'h02: regs_ab[0][7:0] <= ss_di_r; + 8'h03: regs_ab[0][15:8] <= ss_di_r; + 8'h04: regs_ab[1][7:0] <= ss_di_r; + 8'h05: regs_ab[1][15:8] <= ss_di_r; + 8'h06: regs_tr[7:0] <= ss_di_r; + 8'h07: regs_tr[15:8] <= ss_di_r; + 8'h08: regs_trb[7:0] <= ss_di_r; + 8'h09: regs_trb[15:8] <= ss_di_r; + // $0a/$0b regs_dr restored in its own block + 8'h0c: begin regs_sr[7] <= ss_di_r[7]; regs_sr[1] <= ss_di_r[1]; regs_sr[0] <= ss_di_r[0]; end + 8'h0d: begin regs_sr[14] <= ss_di_r[6]; regs_sr[13] <= ss_di_r[5]; regs_sr[11] <= ss_di_r[3]; + regs_sr[SR_DRC] <= ss_di_r[2]; regs_sr[9] <= ss_di_r[1]; regs_sr[8] <= ss_di_r[0]; end + 8'h0e: regs_rp[7:0] <= ss_di_r; + 8'h0f: regs_rp[10:8] <= ss_di_r[2:0]; + 8'h10: regs_k[7:0] <= ss_di_r; + 8'h11: regs_k[15:8] <= ss_di_r; + 8'h12: regs_l[7:0] <= ss_di_r; + 8'h13: regs_l[15:8] <= ss_di_r; + 8'h14: regs_m[7:0] <= ss_di_r; + 8'h15: regs_m[15:8] <= ss_di_r; + 8'h16: regs_n[7:0] <= ss_di_r; + 8'h17: regs_n[15:8] <= ss_di_r; + 8'h18: begin regs_dph <= ss_di_r[7:4]; regs_dpl <= ss_di_r[3:0]; end + 8'h19: regs_dpb <= ss_di_r[1:0]; + 8'h1a: regs_sp <= ss_di_r[3:0]; + 8'h1b: insn_state <= ss_di_r; + 8'h1c: begin flags_s1[0] <= ss_di_r[5]; flags_s0[0] <= ss_di_r[4]; flags_c[0] <= ss_di_r[3]; + flags_z[0] <= ss_di_r[2]; flags_ov1[0] <= ss_di_r[1]; flags_ov0[0] <= ss_di_r[0]; end + 8'h1d: begin flags_s1[1] <= ss_di_r[5]; flags_s0[1] <= ss_di_r[4]; flags_c[1] <= ss_di_r[3]; + flags_z[1] <= ss_di_r[2]; flags_ov1[1] <= ss_di_r[1]; flags_ov0[1] <= ss_di_r[0]; end + 8'h1e: idb[7:0] <= ss_di_r; + 8'h1f: idb[15:8] <= ss_di_r; + 8'h20: alu_p[7:0] <= ss_di_r; + 8'h21: alu_p[15:8] <= ss_di_r; + 8'h22: alu_q[7:0] <= ss_di_r; + 8'h23: alu_q[15:8] <= ss_di_r; + 8'h24: alu_r[7:0] <= ss_di_r; + 8'h25: alu_r[15:8] <= ss_di_r; + 8'h26: ram_dina_r[7:0] <= ss_di_r; + 8'h27: ram_dina_r[15:8] <= ss_di_r; + 8'h28: ld_id[7:0] <= ss_di_r; + 8'h29: ld_id[15:8] <= ss_di_r; + 8'h2a: begin op <= ss_di_r[7:6]; op_pselect <= ss_di_r[5:4]; op_alu <= ss_di_r[3:0]; end + 8'h2b: begin op_asl <= ss_di_r[7]; op_dpl <= ss_di_r[6:5]; op_dphm <= ss_di_r[4:1]; op_rpdcr <= ss_di_r[0]; end + 8'h2c: begin op_src <= ss_di_r[7:4]; op_dst <= ss_di_r[3:0]; end + 8'h2d: begin ld_dst <= ss_di_r[7:4]; alu_store <= ss_di_r[2:1]; cond_true <= ss_di_r[0]; end + 8'h2e: jp_brch[7:0] <= ss_di_r; + 8'h2f: jp_brch[8] <= ss_di_r[0]; + 8'h30: jp_na[7:0] <= ss_di_r; + 8'h31: jp_na[10:8] <= ss_di_r[2:0]; + 8'h32: cpu_wait <= ss_di_r[3:0]; + default: ; // $33 magic (read-only) + endcase + end end else begin insn_state <= STATE_IDLE1; pc <= 11'b0; diff --git a/verilog/sd2snes_gsu/Makefile b/verilog/sd2snes_gsu/Makefile index b490f520..cb9839e9 100644 --- a/verilog/sd2snes_gsu/Makefile +++ b/verilog/sd2snes_gsu/Makefile @@ -1,6 +1,6 @@ CORE = gsu -VSRC = address.v cheat.v dac.v dcm.v gsu.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v regshadow.v dac.v dcm.v gsu.v main.v mcu_cmd.v msu.v sd_dma.v spi.v VHSRC = HEADER = config.vh diff --git a/verilog/sd2snes_gsu/address.v b/verilog/sd2snes_gsu/address.v index 49be17be..da403c53 100644 --- a/verilog/sd2snes_gsu/address.v +++ b/verilog/sd2snes_gsu/address.v @@ -29,6 +29,8 @@ module address( output IS_SAVERAM, // address/CS mapped as SRAM? output IS_ROM, // address mapped as ROM? output IS_WRITABLE, // address somehow mapped as writable area? + output IS_PATCH, // hook identity window active ($C0-FF while unlocked) + output gsu_ss_enable, // savestate scan window ($E8:00xx while unlocked; active on mk2 AND mk3) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, output msu_enable, @@ -40,7 +42,8 @@ module address( output branch1_enable, output branch2_enable, output branch3_enable, - output gsu_enable + output gsu_enable, + input snescmd_unlock // snescmd region unlocked (gates the hook window) ); parameter [2:0] @@ -56,7 +59,20 @@ wire [23:0] SRAM_SNES_ADDR; assign IS_ROM = ~SNES_ROMSEL; -assign IS_SAVERAM = SAVERAM_MASK[0] +// In-game hook identity window: while the snescmd region is unlocked, map all of +// $C0-$FF 1:1 to PSRAM (handler code at $C0xxxx, scratch/shadows in $F2-$FF). +assign IS_PATCH = snescmd_unlock & &SNES_ADDR[23:22]; + +// Savestate scan window: $E8:0000-00FF while unlocked (inside IS_PATCH; the +// main.v data mux gives the window priority over the PSRAM serve, mirroring +// the SA-1 core's sa1_ss_enable). +// MEASUREMENT: scan window active on mk2 too (fit probe for full savestate) +assign gsu_ss_enable = snescmd_unlock & (SNES_ADDR[23:16] == 8'hE8) & ~|SNES_ADDR[15:8]; + +// ~IS_PATCH: the GSU map places SAVERAM at 60-7D/E0-FF -- banks $E0-$FF overlap the +// hook window, and without the gate the handler's PSRAM scratch reads/writes would +// hit the GSU cart RAM instead (the identity window must win while unlocked). +assign IS_SAVERAM = ~IS_PATCH & SAVERAM_MASK[0] & ( // 60-7D/E0-FF:0000-FFFF ( &SNES_ADDR[22:21] & ~SNES_ROMSEL @@ -68,11 +84,14 @@ assign IS_SAVERAM = SAVERAM_MASK[0] ) ); -assign IS_WRITABLE = IS_SAVERAM; +assign IS_WRITABLE = IS_SAVERAM | IS_PATCH; // GSU has a weird hybrid of Lo and Hi ROM formats. // TODO: add programmable address map -assign SRAM_SNES_ADDR = (IS_SAVERAM +assign SRAM_SNES_ADDR = IS_PATCH + // hook window: identity-map $C0-$FF (handler code + scratch) + ? SNES_ADDR + : (IS_SAVERAM // 60-7D/E0-FF:0000-FFFF or 00-3F/80-BF:6000-7FFF (first 8K mirror) ? (24'hE00000 + ((SNES_ADDR[22] ? SNES_ADDR[16:0] : SNES_ADDR[12:0]) & SAVERAM_MASK)) // 40-5F/C0-DF:0000-FFFF or 00-3F/80-BF:8000-FFFF diff --git a/verilog/sd2snes_gsu/cheat.v b/verilog/sd2snes_gsu/cheat.v index 317ba49a..50b217b7 100644 --- a/verilog/sd2snes_gsu/cheat.v +++ b/verilog/sd2snes_gsu/cheat.v @@ -38,7 +38,6 @@ module cheat( input [2:0] pgm_idx, input pgm_we, input [31:0] pgm_in, - input gsu_vec_enable, output [7:0] data_out, output cheat_hit, output snescmd_unlock @@ -55,6 +54,27 @@ reg irq_enable = 0; reg holdoff_enable = 0; // temp disable hooks after reset reg buttons_enable = 0; reg wram_present = 0; +// In-game hooks: route the NMI/IRQ hook to the savestate handler (nmi_savestate). +// The GSU supports full save/load states (mk2 and mk3), so the complete +// force-entry latch machinery is required here (see below), same as the base core. +reg savestate_enable = 0; +// savestate_force_entry keeps the nmi_savestate branch routed while a savestate +// is in flight with the buttons RELEASED (the resume-wait protocol requires the +// NEXT hook entry to bump CS_STATE after the saveinputloop forced a release -- +// without it the stub branches to nmi_exit and the game parks black forever; +// same lesson as the SA-1 port). Pulse-latched: set at a branch1 fetch with +// buttons held, cleared at the unlock-drop. +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end wire branch_wram = cheat_enable & wram_present; reg auto_nmi_enable = 1; @@ -78,8 +98,16 @@ wire vector_unlock = |vector_unlock_r; reg [1:0] reset_unlock_r = 2'b10; wire reset_unlock = |reset_unlock_r; +// ROM-cheat comparators: Mk.III only. On the Mk.II Spartan-3 the savestate +// machinery does not fit next to them, so they are dropped there and ROM cheats +// are instead patched straight into the PSRAM image by the MCU +// (cheat_rom_psram_apply, src/cheat.c) -- which on this core is strictly better +// anyway: it covers address mirrors and the coprocessor's own fetches, neither +// of which ever went through the comparators. +`ifndef MK2 reg [23:0] cheat_addr[5:0]; reg [7:0] cheat_data[5:0]; +`endif reg [5:0] cheat_enable_mask; reg snescmd_unlock_r = 0; @@ -90,16 +118,20 @@ reg [7:0] return_vector = 8'hea; reg [7:0] branch1_offset = 8'h00; reg [7:0] branch2_offset = 8'h00; -reg [7:0] branch3_offset = 8'h04; +reg [7:0] branch3_offset; reg [15:0] pad_data = 0; +`ifndef MK2 wire [5:0] cheat_match_bits ={(cheat_enable_mask[5] & (SNES_ADDR == cheat_addr[5])), (cheat_enable_mask[4] & (SNES_ADDR == cheat_addr[4])), (cheat_enable_mask[3] & (SNES_ADDR == cheat_addr[3])), (cheat_enable_mask[2] & (SNES_ADDR == cheat_addr[2])), (cheat_enable_mask[1] & (SNES_ADDR == cheat_addr[1])), (cheat_enable_mask[0] & (SNES_ADDR == cheat_addr[0]))}; +`else +wire [5:0] cheat_match_bits = 6'h00; // mk2: comparators dropped -> folds away +`endif wire cheat_addr_match = |cheat_match_bits; wire [1:0] nmi_match_bits = {SNES_ADDR == 24'h00FFEA, SNES_ADDR == 24'h00FFEB}; @@ -112,13 +144,17 @@ wire rst_addr_match = |rst_match_bits; wire hook_enable = ~|hook_enable_count; -assign data_out = cheat_match_bits[0] ? cheat_data[0] +assign data_out = +`ifndef MK2 + cheat_match_bits[0] ? cheat_data[0] : cheat_match_bits[1] ? cheat_data[1] : cheat_match_bits[2] ? cheat_data[2] : cheat_match_bits[3] ? cheat_data[3] : cheat_match_bits[4] ? cheat_data[4] : cheat_match_bits[5] ? cheat_data[5] - : nmi_match_bits[1] ? 8'h10 + : +`endif + nmi_match_bits[1] ? 8'h10 : irq_match_bits[1] ? 8'h10 : rst_match_bits[1] ? 8'h7D : nmicmd_enable ? nmicmd @@ -200,6 +236,8 @@ always @(posedge clk) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; end else begin + savestate_force_entry_enable_strobe <= 0; + savestate_force_entry_disable_strobe <= 0; if(SNES_rd_strobe) begin // *** GAME -> INGAME HOOK *** if(hook_enable_sync @@ -219,6 +257,9 @@ always @(posedge clk) begin snescmd_unlock_disable <= 0; snescmd_unlock_disable_countdown <= 0; end + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; + end end // give some time to exit snescmd memory and jump to original vector // sta @NMI_VECT_DISABLE 1-2 (after effective write) @@ -234,6 +275,7 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; + savestate_force_entry_disable_strobe <= 1; end end end @@ -309,18 +351,21 @@ always @(posedge clk) begin snescmd_unlock_disable_strobe <= 1'b1; end end else if(pgm_we) begin +`ifndef MK2 if(pgm_idx < 6) begin cheat_addr[pgm_idx] <= pgm_in[31:8]; cheat_data[pgm_idx] <= pgm_in[7:0]; - end else if(pgm_idx == 6) begin // set rom patch enable + end else +`endif + if(pgm_idx == 6) begin // set rom patch enable cheat_enable_mask <= pgm_in[5:0]; end else if(pgm_idx == 7) begin // set/reset global enable / hooks - // pgm_in[13:8] are reset bit flags - // pgm_in[5:0] are set bit flags - {wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - <= ({wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - & ~pgm_in[13:8]) - | pgm_in[5:0]; + // pgm_in[14:8] are reset bit flags + // pgm_in[6:0] are set bit flags + {savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + <= ({savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + & ~pgm_in[14:8]) + | pgm_in[6:0]; end end end @@ -344,7 +389,11 @@ always @(posedge clk) begin end end -always @* begin +// registered (was comb): the branch-offset byte feeds the SNES_DATA serve path +// combinationally; the savestate additions (16-bit |pad_data + mux) made that +// unconstrained I/O path too deep at 85.9 MHz (hook redirect died in HW with +// timing formally met). The offsets are stable long before the fetch. +always @(posedge clk) begin case(pad_data) 16'h3030: nmicmd = 8'h80; 16'h2070: nmicmd = 8'h81; @@ -356,7 +405,11 @@ always @* begin endcase end -always @* begin +// registered (was comb): the branch-offset byte feeds the SNES_DATA serve path +// combinationally; the savestate additions (16-bit |pad_data + mux) made that +// unconstrained I/O path too deep at 85.9 MHz (hook redirect died in HW with +// timing formally met). The offsets are stable long before the fetch. +always @(posedge clk) begin if(buttons_enable) begin if(snes_ajr) begin if(nmicmd) begin @@ -365,7 +418,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & (savestate_force_entry | |pad_data)) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end else begin @@ -383,18 +440,42 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & |pad_data) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end -always @* begin +// registered (was comb): the branch-offset byte feeds the SNES_DATA serve path +// combinationally; the savestate additions (16-bit |pad_data + mux) made that +// unconstrained I/O path too deep at 85.9 MHz (hook redirect died in HW with +// timing formally met). The offsets are stable long before the fetch. +always @(posedge clk) begin if(nmicmd == 8'h81) begin branch2_offset = 8'h14; // nmi_stop end else if(branch_wram) begin branch2_offset = 8'h00; // nmi_patches end else begin - branch2_offset = 8'h09; // nmi_exit + if(savestate_enable) begin + branch2_offset = 8'h05; // nmi_savestate + end else begin + branch2_offset = 8'h09; // nmi_exit + end + end +end + +// registered (was comb): the branch-offset byte feeds the SNES_DATA serve path +// combinationally; the savestate additions (16-bit |pad_data + mux) made that +// unconstrained I/O path too deep at 85.9 MHz (hook redirect died in HW with +// timing formally met). The offsets are stable long before the fetch. +always @(posedge clk) begin + if(savestate_enable) begin + branch3_offset = 8'h00; // nmi_savestate + end else begin + branch3_offset = 8'h04; // nmi_exit end end diff --git a/verilog/sd2snes_gsu/gsu.v b/verilog/sd2snes_gsu/gsu.v index 92005785..5c962908 100644 --- a/verilog/sd2snes_gsu/gsu.v +++ b/verilog/sd2snes_gsu/gsu.v @@ -21,6 +21,10 @@ module gsu( input RST, input CLK, + // freeze the GSU execution (clock enable); the ROM/RAM fetch FSMs free-run and + // drain any in-flight access. + input pause, + input SS_EN, // savestate scan window ($E8:00xx while unlocked; mk3) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, @@ -91,10 +95,13 @@ reg [9:0] addr_in_r; reg enable_r; reg [9:0] pgm_addr_r; +reg ss_en_r; initial ss_en_r = 0; + always @(posedge CLK) begin data_in_r <= DATA_IN; addr_in_r <= SNES_ADDR; enable_r <= ENABLE; + ss_en_r <= SS_EN; pgm_addr_r <= PGM_ADDR; end @@ -335,6 +342,12 @@ reg [31:0] gsu_plot_read_r; initial gsu_plot_read_r = 0; reg [31:0] gsu_plot_writ_r; initial gsu_plot_writ_r = 0; reg [31:0] gsu_plot_dump_r; initial gsu_plot_dump_r = 0; +reg ss_halt_r; initial ss_halt_r = 0; +reg ss_frozen_r; initial ss_frozen_r = 0; +reg ss_dirty_r; initial ss_dirty_r = 0; +reg [21:0] ss_wait_r; initial ss_wait_r = 0; +reg [3:0] ss_settle_r; initial ss_settle_r = 0; + reg [1:0] gsu_cycle_r; initial gsu_cycle_r = 0; reg gsu_clock_en; initial gsu_clock_en = 0; @@ -349,10 +362,18 @@ always @(posedge CLK) begin end else begin gsu_cycle_r <= gsu_cycle_r + 1; - gsu_clock_en <= (gsu_cycle_r == 2'b10); + // pause: hold the execution clock-enable low. The divider keeps counting so + // the GSU resumes in phase, and the memory FSMs are not gated, so in-flight + // fetches drain. Driven by the $202C pause only; the hook window yields the + // ROM arbiter but leaves the GSU running. + // While a halt is requested but not yet frozen, override the pause so a + // mid-program GSU can run to completion; once frozen, hold unconditionally. + gsu_clock_en <= ~((pause & ~(ss_halt_r & ~ss_frozen_r)) | ss_frozen_r) + & (gsu_cycle_r == 2'b10); end end + // Assert clock enable every 4 FPGA clocks. Delays are calculated in // terms of GSU clocks so this is used to align transitions and // operate counters. @@ -553,9 +574,11 @@ wire [8:0] debug_cache_addr; wire [7:0] debug_cache_wrdata; wire [7:0] debug_cache_rddata; -assign cache_wren = SFR_GO ? cache_gsu_wren_r : cache_mmio_wren_r; -assign cache_addr = SFR_GO ? cache_gsu_addr_r : cache_mmio_addr_r; -assign cache_wrdata = SFR_GO ? cache_gsu_wrdata_r : cache_mmio_wrdata_r; +// While frozen the GSU issues nothing, so the cache port goes to the MMIO path even +// after a dirty freeze: $3100 is how the handler captures/restores the cache. +assign cache_wren = (SFR_GO & ~ss_frozen_r) ? cache_gsu_wren_r : cache_mmio_wren_r; +assign cache_addr = (SFR_GO & ~ss_frozen_r) ? cache_gsu_addr_r : cache_mmio_addr_r; +assign cache_wrdata = (SFR_GO & ~ss_frozen_r) ? cache_gsu_wrdata_r : cache_mmio_wrdata_r; assign debug_cache_wren = 0; assign debug_cache_addr = {~pgm_addr_r[8],pgm_addr_r[7:0]}; @@ -619,6 +642,7 @@ reg snes_writereg_r; initial snes_writereg_r = 0; reg snes_writebuf_val_r; initial snes_writebuf_val_r = 0; reg snes_writebuf_reg_r; reg snes_writebuf_gpr_r; +reg snes_writebuf_ss_r; initial snes_writebuf_ss_r = 0; reg [8:0] snes_writebuf_addr_r; reg [7:0] snes_writebuf_data_r; @@ -626,6 +650,59 @@ reg snes_readbuf_val_r; initial snes_readbuf_val_r = 0; reg idle_r; initial idle_r = 0; +// --- relocated for XST (mk2): decls/logic must precede first use --- +reg [7:0] PIXBUF_VALID_r[1:0]; +reg [15:0] PIXBUF_OFFSET_r[1:0]; +reg [7:0] PIXBUF_r[1:0][7:0]; +reg PIXBUF_OBJ_r[1:0]; +reg [1:0] PIXBUF_HT_r[1:0]; +reg [1:0] PIXBUF_MD_r[1:0]; +reg PIXBUF_HEAD_r; + +//------------------------------------------------------------------- +// SAVESTATE FREEZE +//------------------------------------------------------------------- +// Halt protocol (window $FE bit0, same semantics as the SA-1 $7FF): on a request, +// wait until the GSU program ends (GO==0, exe FSM idle, 16-cycle settle so the +// memory pipes drain). Games run GSU programs in sub-frame bursts, so this +// converges even while the S-CPU spins in the hook. A ~44ms saturating timeout +// freezes anyway and latches ss_dirty_r: such a capture may lose an in-flight +// program on load. +always @(posedge CLK) begin + if (RST) begin + ss_halt_r <= 0; ss_frozen_r <= 0; ss_dirty_r <= 0; + ss_wait_r <= 0; ss_settle_r <= 0; + end + else begin + // halt control write (window $FE): committed on the raw clock so the + // request works while the GSU still runs + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en + & (snes_writebuf_addr_r[7:0] == 8'hFE)) begin + ss_halt_r <= snes_writebuf_data_r[0]; + if (snes_writebuf_data_r[0]) begin + ss_dirty_r <= 0; ss_wait_r <= 0; ss_settle_r <= 0; + end + end + if (~ss_halt_r) begin + ss_frozen_r <= 0; ss_wait_r <= 0; ss_settle_r <= 0; + end + else if (~ss_frozen_r) begin + if (~SFR_GO & exe_idle) begin + if (&ss_settle_r) ss_frozen_r <= 1; + else ss_settle_r <= ss_settle_r + 1; + end + else begin + ss_settle_r <= 0; + if (&ss_wait_r) begin + ss_frozen_r <= 1; + ss_dirty_r <= 1; + end + else ss_wait_r <= ss_wait_r + 1; + end + end + end +end + always @(posedge CLK) begin if (RST) begin for (i = 0; i < NUM_GPR; i = i + 1) begin @@ -666,21 +743,50 @@ always @(posedge CLK) begin else begin // True data enable. This assumes we need unmapped read addresses to be openbus. // Register Read - if (enable_r) begin + if (enable_r | ss_en_r) begin if (SNES_RD_start) begin if (~|addr_in_r[9:8]) begin casex (addr_in_r[7:0]) - ADDR_GPRL : begin data_out_r <= REG_r[addr_in_r[4:1]][7:0]; if (~SFR_GO) data_enable_r <= 1; end - ADDR_GPRH : begin data_out_r <= REG_r[addr_in_r[4:1]][15:8]; if (~SFR_GO) data_enable_r <= 1; end + ADDR_GPRL : begin data_out_r <= REG_r[addr_in_r[4:1]][7:0]; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end + ADDR_GPRH : begin data_out_r <= REG_r[addr_in_r[4:1]][15:8]; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end ADDR_SFR : begin data_out_r <= SFR_r[7:0]; data_enable_r <= 1; end ADDR_SFR+1: begin data_out_r <= SFR_r[15:8]; data_enable_r <= 1; end - ADDR_PBR : begin data_out_r <= PBR_r; if (~SFR_GO) data_enable_r <= 1; end - ADDR_ROMBR: begin data_out_r <= ROMBR_r; if (~SFR_GO) data_enable_r <= 1; end + ADDR_PBR : begin data_out_r <= PBR_r; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end + ADDR_ROMBR: begin data_out_r <= ROMBR_r; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end ADDR_VCR : begin data_out_r <= VCR_r; data_enable_r <= 1; end - ADDR_RAMBR: begin data_out_r <= RAMBR_r; if (~SFR_GO) data_enable_r <= 1; end - ADDR_CBR+0: begin data_out_r <= CBR_r[7:0]; if (~SFR_GO) data_enable_r <= 1; end - ADDR_CBR+1: begin data_out_r <= CBR_r[15:8]; if (~SFR_GO) data_enable_r <= 1; end + ADDR_RAMBR: begin data_out_r <= RAMBR_r; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end + ADDR_CBR+0: begin data_out_r <= CBR_r[7:0]; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end + ADDR_CBR+1: begin data_out_r <= CBR_r[15:8]; if (~SFR_GO | ss_frozen_r) data_enable_r <= 1; end + // window extras: expose the write-only config regs and the internal + // plot/pixel state for capture, gated so native MMIO is unchanged. + ADDR_BRAMR: if (ss_en_r) begin data_out_r <= BRAMR_r; data_enable_r <= 1; end + ADDR_CFGR : if (ss_en_r) begin data_out_r <= CFGR_r; data_enable_r <= 1; end + ADDR_SCBR : if (ss_en_r) begin data_out_r <= SCBR_r; data_enable_r <= 1; end + ADDR_CLSR : if (ss_en_r) begin data_out_r <= CLSR_r; data_enable_r <= 1; end + ADDR_SCMR : if (ss_en_r) begin data_out_r <= SCMR_r; data_enable_r <= 1; end + 8'h40 : if (ss_en_r) begin data_out_r <= COLR_r; data_enable_r <= 1; end + 8'h41 : if (ss_en_r) begin data_out_r <= POR_r; data_enable_r <= 1; end + 8'h42 : if (ss_en_r) begin data_out_r <= {4'h0,SREG_r}; data_enable_r <= 1; end + 8'h43 : if (ss_en_r) begin data_out_r <= {4'h0,DREG_r}; data_enable_r <= 1; end + 8'h44 : if (ss_en_r) begin data_out_r <= cache_val_r[7:0]; data_enable_r <= 1; end + 8'h45 : if (ss_en_r) begin data_out_r <= cache_val_r[15:8]; data_enable_r <= 1; end + 8'h46 : if (ss_en_r) begin data_out_r <= cache_val_r[23:16]; data_enable_r <= 1; end + 8'h47 : if (ss_en_r) begin data_out_r <= cache_val_r[31:24]; data_enable_r <= 1; end + 8'h48 : if (ss_en_r) begin data_out_r <= {5'h0,ss_dirty_r,SFR_GO,ss_frozen_r}; data_enable_r <= 1; end + 8'h50 : if (ss_en_r) begin data_out_r <= PIXBUF_VALID_r[0]; data_enable_r <= 1; end + 8'h51 : if (ss_en_r) begin data_out_r <= PIXBUF_OFFSET_r[0][7:0]; data_enable_r <= 1; end + 8'h52 : if (ss_en_r) begin data_out_r <= PIXBUF_OFFSET_r[0][15:8]; data_enable_r <= 1; end + 8'h53 : if (ss_en_r) begin data_out_r <= {3'h0,PIXBUF_MD_r[0],PIXBUF_HT_r[0],PIXBUF_OBJ_r[0]}; data_enable_r <= 1; end + 8'b01011xxx: if (ss_en_r) begin data_out_r <= PIXBUF_r[0][addr_in_r[2:0]]; data_enable_r <= 1; end // $58-$5F + 8'h60 : if (ss_en_r) begin data_out_r <= PIXBUF_VALID_r[1]; data_enable_r <= 1; end + 8'h61 : if (ss_en_r) begin data_out_r <= PIXBUF_OFFSET_r[1][7:0]; data_enable_r <= 1; end + 8'h62 : if (ss_en_r) begin data_out_r <= PIXBUF_OFFSET_r[1][15:8]; data_enable_r <= 1; end + 8'h63 : if (ss_en_r) begin data_out_r <= {3'h0,PIXBUF_MD_r[1],PIXBUF_HT_r[1],PIXBUF_OBJ_r[1]}; data_enable_r <= 1; end + 8'b01101xxx: if (ss_en_r) begin data_out_r <= PIXBUF_r[1][addr_in_r[2:0]]; data_enable_r <= 1; end // $68-$6F + 8'h70 : if (ss_en_r) begin data_out_r <= {7'h0,PIXBUF_HEAD_r}; data_enable_r <= 1; end + 8'hFE : if (ss_en_r) begin data_out_r <= {7'h0,ss_frozen_r}; data_enable_r <= 1; end + 8'hFF : if (ss_en_r) begin data_out_r <= 8'h5B; data_enable_r <= 1; end // magic endcase end else begin @@ -688,7 +794,7 @@ always @(posedge CLK) begin cache_mmio_addr_r <= {~addr_in_r[8],addr_in_r[7:0]}; end end - else if (|addr_in_r[9:8]) begin + else if (|addr_in_r[9:8] & ~ss_en_r) begin data_out_r <= cache_rddata; end end @@ -697,9 +803,13 @@ always @(posedge CLK) begin end // Register Write Buffer. snes writes are sent on non-GSU clocks - if (SNES_WR_end & enable_r) begin + if (SNES_WR_end & (enable_r | ss_en_r)) begin snes_writebuf_val_r <= 1; - snes_writebuf_reg_r <= ~|addr_in_r[9:8]; + snes_writebuf_ss_r <= ss_en_r; + // $00-$3F of the savestate window reuses the native register write + // semantics wholesale (the restore writes SFR last, so the R15-high + // GO side effect is overwritten by the saved GO bit) + snes_writebuf_reg_r <= ~|addr_in_r[9:8] & (~ss_en_r | ~|addr_in_r[7:6]); snes_writebuf_gpr_r <= ~|addr_in_r[9:5]; snes_writebuf_addr_r <= addr_in_r[8:0]; snes_writebuf_data_r <= data_in_r; @@ -708,6 +818,7 @@ always @(posedge CLK) begin snes_writebuf_val_r <= 0; snes_writebuf_reg_r <= 0; snes_writebuf_gpr_r <= 0; + snes_writebuf_ss_r <= 0; end if (SNES_RD_start && enable_r && addr_in_r[9:0] == {2'h0,ADDR_SFR+1}) begin @@ -737,6 +848,16 @@ always @(posedge CLK) begin r2i_clear_r <= 0; end + // savestate normalize ($F0): kill any cache flush pended by the restore's + // native PBR/SFR writes -- on a dirty freeze idle_r may never fire while + // frozen, and the pended flush would zap the restored cache_val/CBR after + // the unfreeze. Runs last in this block so it overrides the chain above. + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en + & (snes_writebuf_addr_r[7:0] == 8'hF0)) begin + r2i_flush_r <= 0; + r2i_clear_r <= 0; + end + // Registers that can be modifed by both if (snes_writebuf_val_r & ~gsu_clock_en) begin if (snes_writebuf_reg_r) begin @@ -753,6 +874,15 @@ always @(posedge CLK) begin ADDR_SCMR : SCMR_r[5:0] <= snes_writebuf_data_r[5:0]; endcase end + else if (snes_writebuf_ss_r) begin + // savestate window-only registers (restore path, mk3) + case (snes_writebuf_addr_r[7:0]) + 8'h40: COLR_r <= snes_writebuf_data_r; + 8'h41: POR_r <= snes_writebuf_data_r; + 8'h42: SREG_r <= snes_writebuf_data_r[3:0]; + 8'h43: DREG_r <= snes_writebuf_data_r[3:0]; + endcase + end else begin cache_mmio_wren_r <= 1; cache_mmio_wrdata_r <= snes_writebuf_data_r; @@ -1074,13 +1204,6 @@ assign RAM_BUS_WRDATA = ram_bus_data_r[7:0]; //------------------------------------------------------------------- // WriteBuffers -reg [7:0] PIXBUF_VALID_r[1:0]; -reg [15:0] PIXBUF_OFFSET_r[1:0]; -reg [7:0] PIXBUF_r[1:0][7:0]; -reg PIXBUF_OBJ_r[1:0]; -reg [1:0] PIXBUF_HT_r[1:0]; -reg [1:0] PIXBUF_MD_r[1:0]; -reg PIXBUF_HEAD_r; reg bmp_mode_r; reg [3:0] bmp_bppm1_r; @@ -1201,6 +1324,15 @@ always @(posedge CLK) begin // swap the buffers PIXBUF_HEAD_r <= ~PIXBUF_HEAD_r; end + + // savestate window restore (mk3): pixel cache valid masks + head + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + case (snes_writebuf_addr_r[7:0]) + 8'h50: PIXBUF_VALID_r[0] <= snes_writebuf_data_r; + 8'h60: PIXBUF_VALID_r[1] <= snes_writebuf_data_r; + 8'h70: PIXBUF_HEAD_r <= snes_writebuf_data_r[0]; + endcase + end end end @@ -1274,17 +1406,53 @@ always @(posedge CLK) begin ST_BMP_END: begin // this state signals completion to EXE //if (bmp_waitcnt_r == 0) begin +`ifndef MK2 if (bmp_mode_r == BMP_MODE_PLOT) begin // write offset and color. valid handled in that pipe PIXBUF_OFFSET_r[PIXBUF_HEAD_r] <= bmp_offset_r; PIXBUF_r[PIXBUF_HEAD_r][bmp_index_r] <= bmp_colr_r; end +`endif // RPIX gets the data directly from the local color register BMP_STATE <= ST_BMP_IDLE; //end end endcase + +`ifdef MK3 + // savestate window restore (mk3): pixel cache contents + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + casex (snes_writebuf_addr_r[7:0]) + 8'h51: PIXBUF_OFFSET_r[0][7:0] <= snes_writebuf_data_r; + 8'h52: PIXBUF_OFFSET_r[0][15:8] <= snes_writebuf_data_r; + 8'h61: PIXBUF_OFFSET_r[1][7:0] <= snes_writebuf_data_r; + 8'h62: PIXBUF_OFFSET_r[1][15:8] <= snes_writebuf_data_r; + 8'b01011xxx: PIXBUF_r[0][snes_writebuf_addr_r[2:0]] <= snes_writebuf_data_r; // $58-$5F + 8'b01101xxx: PIXBUF_r[1][snes_writebuf_addr_r[2:0]] <= snes_writebuf_data_r; // $68-$6F + endcase + end +`endif +`ifdef MK2 + // mk2: the plot pipeline and the savestate window-restore share ONE write + // driver for PIXBUF_r / PIXBUF_OFFSET_r (XST rejects two drivers on a reg + // array). Restore (frozen) has priority over the plot commit (running); + // the two never coincide. Same decode/writes as the mk3 paths above/plot. + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + casex (snes_writebuf_addr_r[7:0]) + 8'h51: PIXBUF_OFFSET_r[0][7:0] <= snes_writebuf_data_r; + 8'h52: PIXBUF_OFFSET_r[0][15:8] <= snes_writebuf_data_r; + 8'h61: PIXBUF_OFFSET_r[1][7:0] <= snes_writebuf_data_r; + 8'h62: PIXBUF_OFFSET_r[1][15:8] <= snes_writebuf_data_r; + 8'b01011xxx: PIXBUF_r[0][snes_writebuf_addr_r[2:0]] <= snes_writebuf_data_r; // $58-$5F + 8'b01101xxx: PIXBUF_r[1][snes_writebuf_addr_r[2:0]] <= snes_writebuf_data_r; // $68-$6F + endcase + end + else if (BMP_STATE == ST_BMP_END & bmp_mode_r == BMP_MODE_PLOT) begin + PIXBUF_OFFSET_r[PIXBUF_HEAD_r] <= bmp_offset_r; + PIXBUF_r[PIXBUF_HEAD_r][bmp_index_r] <= bmp_colr_r; + end +`endif end end @@ -1546,6 +1714,22 @@ always @(posedge CLK) begin cache_val_r[cache_mmio_addr_r[8:4]] <= 1; end +`ifdef MK3 + // savestate window restore (mk3): CBR + exact cache valid mask (written + // AFTER the $3100 cache image, whose line injections set spurious bits) + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + case (snes_writebuf_addr_r[7:0]) + 8'h3E: CBR_r[7:4] <= snes_writebuf_data_r[7:4]; + 8'h3F: CBR_r[15:8] <= snes_writebuf_data_r; + 8'h44: cache_val_r[7:0] <= snes_writebuf_data_r; + 8'h45: cache_val_r[15:8] <= snes_writebuf_data_r; + 8'h46: cache_val_r[23:16] <= snes_writebuf_data_r; + 8'h47: cache_val_r[31:24] <= snes_writebuf_data_r; + endcase + end +`endif + +`ifndef MK2 if (r2i_flush_r & r2i_clear_r & idle_r) begin CBR_r[15:4] <= 0; end @@ -1555,6 +1739,33 @@ always @(posedge CLK) begin gsu_cache_cbr_r <= gsu_cache_cbr_r + 1; end end +`else + // mk2: CBR restore ($3E/$3F) shares ONE write driver with the internal + // cache logic (XST:528). cache_val restore ($44-$47) is a separate driver + // (it did not multi-source). Restore only fires while frozen; the internal + // paths only while running -- mutually exclusive. + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en + & (snes_writebuf_addr_r[7:0] == 8'h3E)) + CBR_r[7:4] <= snes_writebuf_data_r[7:4]; + else if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en + & (snes_writebuf_addr_r[7:0] == 8'h3F)) + CBR_r[15:8] <= snes_writebuf_data_r; + else if (r2i_flush_r & r2i_clear_r & idle_r) + CBR_r[15:4] <= 0; + else if (pipeline_advance & op_complete & e2r_wcbr_r) begin + CBR_r[15:4] <= e2r_cbr_r[15:4]; + gsu_cache_cbr_r <= gsu_cache_cbr_r + 1; + end + + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + case (snes_writebuf_addr_r[7:0]) + 8'h44: cache_val_r[7:0] <= snes_writebuf_data_r; + 8'h45: cache_val_r[15:8] <= snes_writebuf_data_r; + 8'h46: cache_val_r[23:16] <= snes_writebuf_data_r; + 8'h47: cache_val_r[31:24] <= snes_writebuf_data_r; + endcase + end +`endif // PBR is updated by SNES or JMP instructions. fetch_rom_r <= (PBR_r < 8'h60); @@ -2630,6 +2841,26 @@ always @(posedge CLK) begin end endcase + + // savestate window restore + normalize (mk3). ROMBR/RAMBR live in this + // block (native writes only happen via the ROMB/RAMB instructions); the + // $F0 normalize re-idles the exe pipeline so the restored GSU refetches + // cleanly at the restored R15/CBR when the game next kicks it. Placed + // last so it overrides the FSM case. + if (snes_writebuf_val_r & snes_writebuf_ss_r & ~gsu_clock_en) begin + case (snes_writebuf_addr_r[7:0]) + 8'h36: ROMBR_r <= snes_writebuf_data_r; + 8'h3C: RAMBR_r <= snes_writebuf_data_r[0]; + 8'hF0: begin + EXE_STATE <= ST_EXE_IDLE; + exe_opsize_r <= 0; + exe_operand_valid_r <= 0; + exe_branch_r <= 0; + e2r_val_r <= 0; + e2i_flush_r <= 0; + end + endcase + end end end diff --git a/verilog/sd2snes_gsu/main.qsf b/verilog/sd2snes_gsu/main.qsf index cf1bc99e..fe62663a 100644 --- a/verilog/sd2snes_gsu/main.qsf +++ b/verilog/sd2snes_gsu/main.qsf @@ -511,6 +511,7 @@ set_global_assignment -name VERILOG_FILE mcu_cmd.v set_global_assignment -name VERILOG_FILE main.v set_global_assignment -name VERILOG_FILE dac.v set_global_assignment -name VERILOG_FILE cheat.v +set_global_assignment -name VERILOG_FILE regshadow.v set_global_assignment -name VERILOG_FILE address.v set_global_assignment -name QIP_FILE ip/mk3/pll.qip set_global_assignment -name QIP_FILE ip/mk3/snescmd_buf.qip @@ -524,4 +525,6 @@ set_global_assignment -name QIP_FILE ip/mk3/gsu_cache.qip set_global_assignment -name QIP_FILE ip/mk3/gsu_fmult.qip set_global_assignment -name QIP_FILE ip/mk3/gsu_mult.qip set_global_assignment -name QIP_FILE ip/mk3/gsu_umult.qip -set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top \ No newline at end of file +set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top +# Fitter seed pinned for the build with the savestate machinery. +set_global_assignment -name SEED 3 diff --git a/verilog/sd2snes_gsu/main.v b/verilog/sd2snes_gsu/main.v index 0a87b7e9..b43d136c 100644 --- a/verilog/sd2snes_gsu/main.v +++ b/verilog/sd2snes_gsu/main.v @@ -220,6 +220,10 @@ wire SD_DMA_TO_ROM; wire free_slot = (SNES_PULSE_end | free_strobe) & ~SD_DMA_TO_ROM; wire ROM_HIT; +wire IS_PATCH; +wire gsu_ss_enable; +wire gsu_exec_pause; // GSU execution freeze ($202C snapshot pause only) +wire gsu_hook_yield; // ROM-arbiter yield while the hook may fetch from PSRAM assign DCM_RST=0; @@ -243,7 +247,7 @@ end // Provide full bandwidth if snes is not accessing the bus. always @(posedge CLK2) begin - if(GSU_RONr) free_strobe <= 1; + if(GSU_RONr & ~gsu_hook_yield) free_strobe <= 1; // yield to SNES while the hook runs from PSRAM else if (SNES_cycle_start) free_strobe <= ~ROM_HIT | IS_SAVERAM; else free_strobe <= 1'b0; end @@ -404,10 +408,12 @@ wire GSU_RAM_WORD; gsu snes_gsu ( .RST(SNES_reset_strobe), .CLK(CLK2), - + .pause(gsu_exec_pause), + .SS_EN(gsu_ss_enable), + .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), - + // MMIO interface .ENABLE(gsu_enable), .SNES_RD_start(SNES_RD_start), @@ -551,6 +557,9 @@ address snes_addr( .IS_SAVERAM(IS_SAVERAM), .IS_ROM(IS_ROM), .IS_WRITABLE(IS_WRITABLE), + .IS_PATCH(IS_PATCH), + .gsu_ss_enable(gsu_ss_enable), + .snescmd_unlock(snescmd_unlock), .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), //MSU-1 @@ -592,7 +601,6 @@ cheat snes_cheat( .pgm_idx(cheat_pgm_idx), .pgm_we(cheat_pgm_we), .pgm_in(cheat_pgm_data), - .gsu_vec_enable(ROM_HIT & ~IS_SAVERAM & GSU_RONr), .data_out(cheat_data_out), .cheat_hit(cheat_hit), .snescmd_unlock(snescmd_unlock) @@ -623,6 +631,39 @@ wire r2100_patch = featurebits[6]; wire r2100_enable = r2100_hit & (r2100_patch | ~(&r2100_limit)); wire snoop_4200_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04200; +// regshadow write-snoop windows (see SNES_DATABUS_OE/DIR) +wire snoop_42xx_enable = ~SNES_ADDR[22] & (SNES_ADDR[15:5] == 11'b01000010000); +wire rs_snoop_pawr_oe = ~SNES_PAWR & (SNES_PA < 8'h40); + +// Hook-side coprocessor pause register ($202C bit0, same protocol as the SA-1 and +// CX4 cores): freezes the GSU execution clock-enable while memory FSMs drain +// (gsu.v). The savestate handler does not use it (it freezes the GSU through the +// $E8 window, run-to-stop); the in-game hook itself yields the bus but leaves the +// GSU running (see gsu_hook_yield below). +reg snapshot_pause; initial snapshot_pause = 1'b0; +always @(posedge CLK2) begin + if(SNES_reset_strobe) + snapshot_pause <= 1'b0; + else if(SNES_WR_end & ~SNES_ADDR[22] & (SNES_ADDR[15:0] == 16'h202C)) + snapshot_pause <= SNES_DATA[0]; +end + +// While the hook executes (snescmd_unlock) the CPU may fetch the handler from the +// IS_PATCH window, i.e. from PSRAM. With the GSU running under RON the ROM arbiter +// gives it every slot, so an SNES-side PSRAM fetch is never serviced and the handler +// executes garbage. The arbiter is therefore yielded back to the SNES for as long +// as the hook may execute from PSRAM. +// +// The yield is bus priority only: it does NOT stop the GSU, which keeps running +// through our handler exactly as it does through the game's own ISR. Execution is +// frozen separately, by the $202C pause and by the savestate freeze in gsu.v. +reg gsu_hook_psram_r; initial gsu_hook_psram_r = 1'b0; +always @(posedge CLK2) begin + if(~snescmd_unlock) gsu_hook_psram_r <= 1'b0; + else if(IS_PATCH) gsu_hook_psram_r <= 1'b1; +end +assign gsu_hook_yield = snapshot_pause | IS_PATCH | gsu_hook_psram_r; +assign gsu_exec_pause = snapshot_pause; wire r4016_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04016; always @(posedge CLK2) begin @@ -648,6 +689,48 @@ always @(posedge CLK2) begin end end +// PPU-register capture via the base ctx.v counter scheme (fire at count==4 from the +// write start). CLK2 here is 85.9 MHz, close enough to the base core's 96 MHz for +// the base-calibrated shift patterns to apply. Requires the snoop OE/DIR terms below: without them the data-bus level +// shifter stays disabled for B-bus writes to non-cart addresses and every capture +// design reads bus float (the CX4/OBC1/S-DD1 audit lesson). +wire rs_pawr_start_early = ((SNES_PAWRr[4:1] | SNES_PAWRr[5:2]) == 4'b1110); +reg [3:0] rs_pawr_cnt; initial rs_pawr_cnt = 0; +reg rs_pawr_end; initial rs_pawr_end = 0; +reg rs_pawr_end_r; initial rs_pawr_end_r = 0; +reg [7:0] rs_data_r; initial rs_data_r = 0; +always @(posedge CLK2) begin + if (rs_pawr_end) rs_pawr_cnt <= 0; + else if (rs_pawr_start_early) rs_pawr_cnt <= 1; + else if (|rs_pawr_cnt) rs_pawr_cnt <= rs_pawr_cnt + 1'b1; + rs_pawr_end <= (rs_pawr_cnt == 4'd4); + rs_pawr_end_r <= rs_pawr_end; // ctx.v registers the strobe once more... + rs_data_r <= SNES_DATAr[0]; // ...and the data tap with it (2-cyc align) +end +wire [7:0] rs_data = rs_pawr_end_r ? rs_data_r : SNES_DATA; + +// In-game cheat-savestate register shadow (regshadow.v): $F90500 (PPU, stride-2 words +// = the (1st write, 2nd write) pair) / $F90700 (CPU $42xx), read through the hook +// identity window only. +wire shadow_ppu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF905) & ~SNES_ADDR[7]; +wire shadow_cpu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF907) & (SNES_ADDR[7:5] == 3'b000); +wire [7:0] regshadow_dout; +// PPU pair = mem[0x00-0x7F] indexed straight by SNES_ADDR[6:0]: the even byte of a +// stride-2 entry is the 1st write, the odd byte the 2nd (double-write regs are +// reconstructed ctx.v-style inside regshadow.v). CPU reg = mem[0x80-0x9F]. +wire [8:0] regshadow_raddr = shadow_cpu_hit ? {4'b0100, SNES_ADDR[4:0]} + : {2'b00, SNES_ADDR[6:0]}; +regshadow snes_regshadow( + .clk(CLK2), + .pawr_end(rs_pawr_end_r), + .wr_end(SNES_WR_end), + .snes_addr(SNES_ADDR), + .snes_pa(SNES_PA), + .snes_data(rs_data), + .rd_addr(regshadow_raddr), + .rd_data(regshadow_dout) +); + assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr :(r2100_enable & ~SNES_PAWR & r2100_forcewrite) ? r2100r :((~SNES_READ ^ (r213f_forceread & r213f_enable & ~SNES_PARD)) @@ -656,8 +739,19 @@ assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr : gsu_data_enable ? GSU_SNES_DATA_OUT // GSU MMIO read : (cheat_hit & ~feat_cmd_unlock) ? cheat_data_out : ((snescmd_unlock | feat_cmd_unlock) & snescmd_enable) ? snescmd_dout + // in-game savestate register shadow read-backs: a + // stride-2 entry is a PAIR, not a duplicate -- even + // byte = 1st write (prev), odd byte = 2nd write + // (current), reconstructed ctx.v-style in + // regshadow.v. Single-write regs store (value, + // value), so the high byte is never $00. + : shadow_ppu_hit ? regshadow_dout + : shadow_cpu_hit ? regshadow_dout : (ROM_HIT & IS_SAVERAM) ? RAM_DATA - : (ROM_HIT & ~IS_SAVERAM & GSU_RONr) ? (SNES_ADDR[0] ? 8'h01 : {4'h0, (SNES_ADDR[3] & SNES_ADDR[1]), (SNES_ADDR[2] & ~^{SNES_ADDR[3],SNES_ADDR[1]}), 1'b0, SNES_ADDR[0]}) // used for interrupt vectors + // ~IS_PATCH: with RON=1 plain ROM reads serve the + // stub vector, so the hook window has to win or the + // handler's own fetches return the stub. + : (ROM_HIT & ~IS_SAVERAM & GSU_RONr & ~IS_PATCH) ? (SNES_ADDR[0] ? 8'h01 : {4'h0, (SNES_ADDR[3] & SNES_ADDR[1]), (SNES_ADDR[2] & ~^{SNES_ADDR[3],SNES_ADDR[1]}), 1'b0, SNES_ADDR[0]}) // used for interrupt vectors : (ROM_ADDR0 ? ROM_DATA[7:0] : ROM_DATA[15:8]) ) : 8'bZ; @@ -913,9 +1007,14 @@ reg MCU_WRITE_1; always @(posedge CLK2) MCU_WRITE_1<= MCU_WRITE; // odd addresses xxx1 +// SNES->PSRAM writes were dropped whenever the GSU held RON, which also dropped the +// handler's own writes through the IS_PATCH window (CS_STATE/CS_INPUT_* and its +// scratches) in GSU-heavy scenes. Drive SNES_DATA for IS_PATCH writes only: stray +// game writes to ROM stay high-Z, and the arbiter yield above keeps the GSU off the +// bus on those cycles, so the write cannot fight an active GSU access. assign ROM_DATA[7:0] = (ROM_ADDR0) ?(SD_DMA_TO_ROM ? (!MCU_WRITE_1 ? MCU_DOUT : 8'bZ) - : (ROM_HIT & ~IS_SAVERAM & ~SNES_WRITE & ~GSU_RONr) ? SNES_DATA + : (IS_PATCH & ~SNES_WRITE) ? SNES_DATA : MCU_WR_HIT ? MCU_DOUT : 8'bZ ) :8'bZ; @@ -924,14 +1023,14 @@ assign ROM_DATA[7:0] = (ROM_ADDR0) assign ROM_DATA[15:8] = (ROM_ADDR0) ? 8'bZ :(SD_DMA_TO_ROM ? (!MCU_WRITE_1 ? MCU_DOUT : 8'bZ) - : (ROM_HIT & ~IS_SAVERAM & ~SNES_WRITE & ~GSU_RONr) ? SNES_DATA + : (IS_PATCH & ~SNES_WRITE) ? SNES_DATA : MCU_WR_HIT ? MCU_DOUT : 8'bZ ); assign ROM_WE = SD_DMA_TO_ROM ?MCU_WRITE - : (ROM_HIT & IS_WRITABLE & ~IS_SAVERAM & SNES_CPU_CLK & ~GSU_RONr) ? SNES_WRITE + : (ROM_HIT & IS_WRITABLE & ~IS_SAVERAM & SNES_CPU_CLK & (~GSU_RONr | IS_PATCH)) ? SNES_WRITE : MCU_WE_HIT ? 1'b0 : 1'b1; @@ -1114,6 +1213,11 @@ assign SNES_DATABUS_OE = msu_enable ? 1'b0 : snescmd_enable & ~(SNES_READ & SNES_WRITE) ? ~(snescmd_unlock | feat_cmd_unlock) : (r213f_enable & !SNES_PARD) ? 1'b0 : (r2100_enable & ~SNES_PAWR) ? 1'b0 : + // regshadow write snoop: enable the shifter (receive) during + // PPU B-bus writes and $42xx A-bus writes, else the snoop + // reads float (base does this via SNES_SNOOPPAWR_DATA_OE). + rs_snoop_pawr_oe ? 1'b0 : + (snoop_42xx_enable & ~SNES_WRITE) ? 1'b0 : snoop_4200_enable ? SNES_WRITE : ( (IS_ROM & SNES_ROMSEL) | (!IS_ROM & !IS_SAVERAM & !IS_WRITABLE) @@ -1125,7 +1229,11 @@ assign SNES_DATABUS_OE = msu_enable ? 1'b0 : * a) the SNES wants to read * b) we want to force a value on the bus */ -assign SNES_DATABUS_DIR = (~SNES_READ | (~SNES_PARD & (r213f_enable))) +// During a snooped B-bus write the concurrent A-bus read (/RD low on DMA/HDMA) must +// not flip the shifter to drive, unless the FPGA serves that source itself: ROM, RAM +// and the stub via ROM_HIT, the GSU MMIO via gsu_data_enable. A GSU-RAM framebuffer +// DMA to VRAM is exactly that case. +assign SNES_DATABUS_DIR = ((~SNES_READ & (~rs_snoop_pawr_oe | ROM_HIT | gsu_data_enable)) | (~SNES_PARD & (r213f_enable))) ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE)) : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite diff --git a/verilog/sd2snes_gsu/regshadow.v b/verilog/sd2snes_gsu/regshadow.v new file mode 100644 index 00000000..573efad2 --- /dev/null +++ b/verilog/sd2snes_gsu/regshadow.v @@ -0,0 +1,114 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// Company: sd2snes +// Module Name: regshadow +// Description: +// Write-only shadow of the PPU ($2100-$213F) and CPU ($4200-$421F) registers, +// read back by the in-game savestate handler through the hook window: +// $F90500-$F9057F : PPU regs, stride-2 (1st write, 2nd write) +// $F90700-$F9071F : CPU regs, stride-1 +// base/DSP/SA-1 get this from ctx.v, which does not fit the mk2 Spartan-3. +// +// The scroll ($210D-$2114) and mode-7 ($211B-$2120) registers latch 16 bits from +// two consecutive writes, so the pair is stored, not just the last byte (ctx.v +// does the same via rBG/rM7). Non-double regs store (value, value). +// +// Storage (256x8, one RAMB16): +// mem[$00-$7F] PPU pairs, mem[{PA,1'b0}] = 1st write, mem[{PA,1'b1}] = 2nd +// mem[$80-$9F] CPU regs +// REGSHADOW_1DEEP selects the older layout (PPU mem[$00-$3F], CPU mem[$40-$5F]); +// main.v indexes both under the same macro. +// +// Compile gates, mutually exclusive, for mk2 area/timing: +// REGSHADOW_NO_M7 drop the mode-7 tracker, keep the scroll pair (gsu mk2) +// REGSHADOW_1DEEP drop the pair scheme entirely (cx4 mk2) +////////////////////////////////////////////////////////////////////////////////// +module regshadow( + input clk, + input pawr_end, // settled rising edge of /PAWR + input wr_end, // settled rising edge of /WR + input [23:0] snes_addr, + input [7:0] snes_pa, + input [7:0] snes_data, + input [8:0] rd_addr, + output reg [7:0] rd_data +); + +// 1DEEP removes what NO_M7 thins, so defining both is always a mistake and 1DEEP +// would silently win. The bare identifier is illegal Verilog: fail at parse time. +`ifdef REGSHADOW_1DEEP + `ifdef REGSHADOW_NO_M7 + ERROR_REGSHADOW_1DEEP_and_REGSHADOW_NO_M7_are_mutually_exclusive + `endif +`endif + +(* ram_style = "block" *) reg [7:0] mem [0:255]; + +wire ppu_wr = pawr_end & (snes_pa < 8'h40); +// $4200-$421F in any bank with ADDR[22]=0: games write them through FastROM banks, +// so a bank-$00-only decode misses those writes. +wire cpu_wr = wr_end & ~snes_addr[22] + & (snes_addr[15:5] == 11'b01000010000); + +`ifdef REGSHADOW_1DEEP +wire wr_en = ppu_wr | cpu_wr; +wire [7:0] wr_a = ppu_wr ? {2'b00, snes_pa[5:0]} + : {3'b010, snes_addr[4:0]}; + +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= snes_data; + rd_data <= mem[rd_addr[7:0]]; +end +`else +// Previous-byte trackers (ctx.v's rBG/rM7): consumed before being updated. +reg [7:0] prev_bg; initial prev_bg = 0; +`ifndef REGSHADOW_NO_M7 +reg [7:0] prev_m7; initial prev_m7 = 0; +`endif +wire is_bg_dbl = (snes_pa >= 8'h0D) && (snes_pa <= 8'h14); +`ifndef REGSHADOW_NO_M7 +wire is_m7_dbl = ((snes_pa >= 8'h0D) && (snes_pa <= 8'h0E)) + || ((snes_pa >= 8'h1B) && (snes_pa <= 8'h20)); +`endif + +// The strobe cycle stores the current byte (odd offset), the next cycle stores the +// previous one (even offset) out of these defer flops. Bus writes are dozens of +// clocks apart, so the defer can never collide with the next write. +reg wr2_pend; initial wr2_pend = 0; +reg [7:0] wr2_a; initial wr2_a = 0; +reg [7:0] wr2_d; initial wr2_d = 0; + +wire wr_en = ppu_wr | cpu_wr | wr2_pend; +wire [7:0] wr_a = wr2_pend ? wr2_a + : ppu_wr ? {1'b0, snes_pa[5:0], 1'b1} + : {3'b100, snes_addr[4:0]}; +wire [7:0] wr_d = wr2_pend ? wr2_d : snes_data; + +// One write enable and one write address in the process: anything else falls out +// of the XST block-RAM template and the memory is built from flip-flops instead. +// The arm is edge-guarded so a strobe wider than one cycle cannot re-arm with the +// trackers already updated, which would degrade the pair back to (value, value). +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= wr_d; + rd_data <= mem[rd_addr[7:0]]; + if (ppu_wr & ~wr2_pend) begin + wr2_pend <= 1'b1; + wr2_a <= {1'b0, snes_pa[5:0], 1'b0}; +`ifndef REGSHADOW_NO_M7 + wr2_d <= is_bg_dbl ? prev_bg : is_m7_dbl ? prev_m7 : snes_data; +`else + wr2_d <= is_bg_dbl ? prev_bg : snes_data; +`endif + if (is_bg_dbl) prev_bg <= snes_data; +`ifndef REGSHADOW_NO_M7 + if (is_m7_dbl) prev_m7 <= snes_data; +`endif + end else if (~ppu_wr) begin + wr2_pend <= 1'b0; + end +end +`endif + +endmodule diff --git a/verilog/sd2snes_gsu/sd2snes_gsu.xise b/verilog/sd2snes_gsu/sd2snes_gsu.xise index 1b6a7f2b..ed7c05a0 100644 --- a/verilog/sd2snes_gsu/sd2snes_gsu.xise +++ b/verilog/sd2snes_gsu/sd2snes_gsu.xise @@ -72,6 +72,10 @@ + + + + @@ -131,7 +135,7 @@ - + @@ -263,7 +267,7 @@ - + @@ -294,7 +298,7 @@ - + @@ -319,7 +323,7 @@ - + @@ -419,7 +423,7 @@ - + diff --git a/verilog/sd2snes_obc1/Makefile b/verilog/sd2snes_obc1/Makefile index 3b8fd014..38344b13 100644 --- a/verilog/sd2snes_obc1/Makefile +++ b/verilog/sd2snes_obc1/Makefile @@ -1,6 +1,6 @@ CORE = obc1 -VSRC = address.v cheat.v clk_test.v dac.v dcm.v obc1.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v dac.v dcm.v obc1.v main.v mcu_cmd.v msu.v regshadow.v sd_dma.v spi.v VHSRC = COMMON_IP = dac_buf msu_databuf obc_lower obc_upper snescmd_buf diff --git a/verilog/sd2snes_obc1/address.v b/verilog/sd2snes_obc1/address.v index 5ff87cb0..a2f6d721 100644 --- a/verilog/sd2snes_obc1/address.v +++ b/verilog/sd2snes_obc1/address.v @@ -29,8 +29,10 @@ module address( output IS_SAVERAM, // address/CS mapped as SRAM? output IS_ROM, // address mapped as ROM? output IS_WRITABLE, // address somehow mapped as writable area? + output IS_PATCH, // hook identity window active ($C0-FF while unlocked) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, + input snescmd_unlock, // snescmd region unlocked (gates the hook window) output msu_enable, output r213f_enable, output r2100_hit, @@ -79,9 +81,17 @@ assign IS_SAVERAM = SAVERAM_MASK[0] ) : 1'b0)); -assign IS_WRITABLE = IS_SAVERAM; +// Hook identity window (as in sd2snes_base): while the hook holds the snescmd +// region unlocked, banks $C0-$FF are identity-mapped so the savestate handler runs +// from menu PSRAM with its scratch in $F2-$FF. 0 outside the hook window. +assign IS_PATCH = snescmd_unlock & &SNES_ADDR[23:22]; -assign SRAM_SNES_ADDR = ((MAPPER == 3'b000) +assign IS_WRITABLE = IS_SAVERAM | IS_PATCH; + +assign SRAM_SNES_ADDR = IS_PATCH + // hook window: identity-map $C0-$FF (handler code + scratch) + ? SNES_ADDR + : ((MAPPER == 3'b000) ?(IS_SAVERAM ? 24'hE00000 + ({SNES_ADDR[20:16], SNES_ADDR[12:0]} & SAVERAM_MASK) diff --git a/verilog/sd2snes_obc1/cheat.v b/verilog/sd2snes_obc1/cheat.v index 3048045f..777c8349 100644 --- a/verilog/sd2snes_obc1/cheat.v +++ b/verilog/sd2snes_obc1/cheat.v @@ -51,6 +51,24 @@ reg irq_enable = 0; reg holdoff_enable = 0; // temp disable hooks after reset reg buttons_enable = 0; reg wram_present = 0; +// In-game save/load states run on this core. savestate_force_entry keeps the +// nmi_savestate branch routed while a savestate is in flight with the buttons +// RELEASED, which the resume protocol needs: the next hook entry has to bump +// CS_STATE after the handler forced a release, and without it the stub branches to +// nmi_exit instead. Pulse-latched: set at a branch1 fetch with buttons held, +// cleared at the unlock-drop. +reg savestate_enable = 0; +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end wire branch_wram = cheat_enable & wram_present; reg auto_nmi_enable = 1; @@ -196,6 +214,8 @@ always @(posedge clk) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; end else begin + savestate_force_entry_enable_strobe <= 0; + savestate_force_entry_disable_strobe <= 0; if(SNES_rd_strobe) begin // *** GAME -> INGAME HOOK *** if(hook_enable_sync @@ -209,6 +229,11 @@ always @(posedge clk) begin if(rst_match_bits[1] & |reset_unlock_r) begin snescmd_unlock_r <= 1; end + // arm the savestate force-entry latch when the hook redirect is taken with + // buttons held; it holds the nmi_savestate route through the button release. + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; + end end // give some time to exit snescmd memory and jump to original vector // sta @NMI_VECT_DISABLE 1-2 (after effective write) @@ -221,6 +246,8 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; + // drop the force-entry latch at the same unlock-drop point + savestate_force_entry_disable_strobe <= 1; end end end @@ -302,12 +329,12 @@ always @(posedge clk) begin end else if(pgm_idx == 6) begin // set rom patch enable cheat_enable_mask <= pgm_in[5:0]; end else if(pgm_idx == 7) begin // set/reset global enable / hooks - // pgm_in[13:8] are reset bit flags - // pgm_in[5:0] are set bit flags - {wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - <= ({wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - & ~pgm_in[13:8]) - | pgm_in[5:0]; + // pgm_in[14:8] are reset bit flags + // pgm_in[6:0] are set bit flags + {savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + <= ({savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + & ~pgm_in[14:8]) + | pgm_in[6:0]; end end end @@ -352,7 +379,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & (savestate_force_entry | |pad_data)) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end else begin @@ -370,7 +401,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & |pad_data) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end @@ -381,7 +416,19 @@ always @* begin end else if(branch_wram) begin branch2_offset = 8'h00; // nmi_patches end else begin - branch2_offset = 8'h09; // nmi_exit + if(savestate_enable) begin + branch2_offset = 8'h05; // nmi_savestate + end else begin + branch2_offset = 8'h09; // nmi_exit + end + end +end + +always @* begin + if(savestate_enable) begin + branch3_offset = 8'h00; // nmi_savestate + end else begin + branch3_offset = 8'h04; // nmi_exit end end diff --git a/verilog/sd2snes_obc1/main.qsf b/verilog/sd2snes_obc1/main.qsf index 4e5799c4..14551a69 100644 --- a/verilog/sd2snes_obc1/main.qsf +++ b/verilog/sd2snes_obc1/main.qsf @@ -508,6 +508,7 @@ set_global_assignment -name VERILOG_FILE main.v set_global_assignment -name VERILOG_FILE dac.v set_global_assignment -name VERILOG_FILE cheat.v set_global_assignment -name VERILOG_FILE address.v +set_global_assignment -name VERILOG_FILE regshadow.v set_global_assignment -name QIP_FILE ip/mk3/snescmd_buf.qip set_global_assignment -name QIP_FILE ip/mk3/msu_databuf.qip set_global_assignment -name QIP_FILE ip/mk3/dac_buf.qip diff --git a/verilog/sd2snes_obc1/main.v b/verilog/sd2snes_obc1/main.v index c1ce87f4..c04d9e18 100644 --- a/verilog/sd2snes_obc1/main.v +++ b/verilog/sd2snes_obc1/main.v @@ -197,6 +197,7 @@ wire SD_DMA_TO_ROM; wire free_slot = (SNES_PULSE_end | free_strobe) & ~SD_DMA_TO_ROM; wire ROM_HIT; +wire IS_PATCH; // hook identity window ($C0-FF while snescmd unlocked) -- savestate assign DCM_RST=0; @@ -405,6 +406,8 @@ address snes_addr( .IS_SAVERAM(IS_SAVERAM), .IS_ROM(IS_ROM), .IS_WRITABLE(IS_WRITABLE), + .IS_PATCH(IS_PATCH), + .snescmd_unlock(snescmd_unlock), .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), //MSU-1 @@ -466,6 +469,50 @@ cheat snes_cheat( .snescmd_unlock(snescmd_unlock) ); +// PPU-register capture via the base ctx.v counter scheme; this core's CLK2 is the +// base's native 96 MHz, so the shift patterns and the count==4 sample point apply +// unchanged. Needs the snoop OE terms below, or the level shifter stays disabled +// for B-bus writes to non-cart addresses and the shadow captures bus float. +wire rs_pawr_start_early = ((SNES_PAWRr[4:1] | SNES_PAWRr[5:2]) == 4'b1110); +reg [3:0] rs_pawr_cnt; initial rs_pawr_cnt = 0; +reg rs_pawr_end; initial rs_pawr_end = 0; +reg rs_pawr_end_r; initial rs_pawr_end_r = 0; +reg [7:0] rs_data_r; initial rs_data_r = 0; +always @(posedge CLK2) begin + if (rs_pawr_end) rs_pawr_cnt <= 0; + else if (rs_pawr_start_early) rs_pawr_cnt <= 1; + else if (|rs_pawr_cnt) rs_pawr_cnt <= rs_pawr_cnt + 1'b1; + rs_pawr_end <= (rs_pawr_cnt == 4'd4); + rs_pawr_end_r <= rs_pawr_end; // ctx.v registers the strobe once more... + rs_data_r <= SNES_DATAr[0]; // ...and the data tap with it (2-cyc align) +end +// PPU (B-bus) -> the ctx-aligned captured byte; CPU ($42xx, A-bus/SNES_WR_end) uses +// raw SNES_DATA (the native snes_ajr capture pattern). +wire [7:0] rs_data = rs_pawr_end_r ? rs_data_r : SNES_DATA; + +// Savestate register shadow (regshadow.v), read through the hook window at +// $F90500 (PPU, stride-2 pairs) and $F90700 (CPU $42xx). IS_PATCH gates the reads. +wire shadow_ppu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF905) & ~SNES_ADDR[7]; // $F90500-7F +wire shadow_cpu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF907) & (SNES_ADDR[7:5] == 3'b000); // $F90700-1F +wire [7:0] regshadow_dout; +// PPU pair at mem[$00-$7F] indexed by SNES_ADDR[6:0], CPU reg at mem[$80-$9F]. +// 1-cycle BRAM read latency (like snescmd_buf); the address is stable for the whole +// ROM cycle. +wire [8:0] regshadow_raddr = shadow_cpu_hit ? {4'b0100, SNES_ADDR[4:0]} + : {2'b00, SNES_ADDR[6:0]}; +regshadow snes_regshadow( + .clk(CLK2), + // PPU strobe = the ctx-style counter end (count==4 from write start). + .pawr_end(rs_pawr_end_r), + .wr_end(SNES_WR_end), + .snes_addr(SNES_ADDR), + .snes_pa(SNES_PA), + // PPU: ctx-aligned captured byte; CPU: raw SNES_DATA (muxed by rs_pawr_end_r). + .snes_data(rs_data), + .rd_addr(regshadow_raddr), + .rd_data(regshadow_dout) +); + wire [7:0] snescmd_dout; parameter ST_R213F_ARMED = 4'b0001; @@ -491,6 +538,10 @@ wire r2100_patch = featurebits[6]; wire r2100_enable = r2100_hit & (r2100_patch | ~(&r2100_limit)); wire snoop_4200_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04200; +// regshadow write-snoop windows: enable the data-bus level shifter (receive) so the +// snoop sees the actual write byte instead of bus float -- see SNES_DATABUS_OE/DIR. +wire snoop_42xx_enable = ~SNES_ADDR[22] & (SNES_ADDR[15:5] == 11'b01000010000); +wire rs_snoop_pawr_oe = ~SNES_PAWR & (SNES_PA < 8'h40); wire r4016_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04016; always @(posedge CLK2) begin @@ -524,6 +575,11 @@ assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr :obc1_enable ? OBC1_SNES_DATA_OUT :(cheat_hit & ~feat_cmd_unlock) ? cheat_data_out :((snescmd_unlock | feat_cmd_unlock) & snescmd_enable) ? snescmd_dout + // savestate register shadow read-backs; a stride-2 entry is + // the (1st write, 2nd write) pair, so the double-writing restore + // loop replays scroll and mode-7 in the right order. + :shadow_ppu_hit ? regshadow_dout + :shadow_cpu_hit ? regshadow_dout :(ROM_ADDR0 ? ROM_DATA[7:0] : ROM_DATA[15:8])) : 8'bZ; reg [3:0] ST_MEM_DELAYr; @@ -769,13 +825,22 @@ assign SNES_DATABUS_OE = obc1_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : snescmd_enable & ~(SNES_READ_narrow & SNES_WRITE) ? ~(snescmd_unlock | feat_cmd_unlock) : (r213f_enable & !SNES_PARD) ? 1'b0 : (r2100_enable & ~SNES_PAWR) ? 1'b0 : + // regshadow write snoop: enable the shifter (receive) during + // PPU B-bus writes and $42xx A-bus writes, else the snoop + // reads float (base does this via SNES_SNOOPPAWR_DATA_OE). + rs_snoop_pawr_oe ? 1'b0 : + (snoop_42xx_enable & ~SNES_WRITE) ? 1'b0 : snoop_4200_enable ? SNES_WRITE : ((IS_ROM & SNES_ROMSEL) |(!IS_ROM & !IS_SAVERAM & !IS_WRITABLE) |(SNES_READ_narrow & SNES_WRITE) ); -assign SNES_DATABUS_DIR = (~SNES_READ | (~SNES_PARD & (r213f_enable))) +// During a snooped B-bus write the concurrent A-bus read (/RD low on DMA/HDMA) must +// not flip the shifter to drive, unless the FPGA serves that source itself: ROM and +// PSRAM via ROM_HIT, plus the chip's own region. Missing the latter makes those +// DMAs read float. +assign SNES_DATABUS_DIR = ((~SNES_READ & (~rs_snoop_pawr_oe | ROM_HIT | obc1_enable)) | (~SNES_PARD & (r213f_enable))) ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE)) : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite diff --git a/verilog/sd2snes_obc1/regshadow.v b/verilog/sd2snes_obc1/regshadow.v new file mode 100644 index 00000000..573efad2 --- /dev/null +++ b/verilog/sd2snes_obc1/regshadow.v @@ -0,0 +1,114 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// Company: sd2snes +// Module Name: regshadow +// Description: +// Write-only shadow of the PPU ($2100-$213F) and CPU ($4200-$421F) registers, +// read back by the in-game savestate handler through the hook window: +// $F90500-$F9057F : PPU regs, stride-2 (1st write, 2nd write) +// $F90700-$F9071F : CPU regs, stride-1 +// base/DSP/SA-1 get this from ctx.v, which does not fit the mk2 Spartan-3. +// +// The scroll ($210D-$2114) and mode-7 ($211B-$2120) registers latch 16 bits from +// two consecutive writes, so the pair is stored, not just the last byte (ctx.v +// does the same via rBG/rM7). Non-double regs store (value, value). +// +// Storage (256x8, one RAMB16): +// mem[$00-$7F] PPU pairs, mem[{PA,1'b0}] = 1st write, mem[{PA,1'b1}] = 2nd +// mem[$80-$9F] CPU regs +// REGSHADOW_1DEEP selects the older layout (PPU mem[$00-$3F], CPU mem[$40-$5F]); +// main.v indexes both under the same macro. +// +// Compile gates, mutually exclusive, for mk2 area/timing: +// REGSHADOW_NO_M7 drop the mode-7 tracker, keep the scroll pair (gsu mk2) +// REGSHADOW_1DEEP drop the pair scheme entirely (cx4 mk2) +////////////////////////////////////////////////////////////////////////////////// +module regshadow( + input clk, + input pawr_end, // settled rising edge of /PAWR + input wr_end, // settled rising edge of /WR + input [23:0] snes_addr, + input [7:0] snes_pa, + input [7:0] snes_data, + input [8:0] rd_addr, + output reg [7:0] rd_data +); + +// 1DEEP removes what NO_M7 thins, so defining both is always a mistake and 1DEEP +// would silently win. The bare identifier is illegal Verilog: fail at parse time. +`ifdef REGSHADOW_1DEEP + `ifdef REGSHADOW_NO_M7 + ERROR_REGSHADOW_1DEEP_and_REGSHADOW_NO_M7_are_mutually_exclusive + `endif +`endif + +(* ram_style = "block" *) reg [7:0] mem [0:255]; + +wire ppu_wr = pawr_end & (snes_pa < 8'h40); +// $4200-$421F in any bank with ADDR[22]=0: games write them through FastROM banks, +// so a bank-$00-only decode misses those writes. +wire cpu_wr = wr_end & ~snes_addr[22] + & (snes_addr[15:5] == 11'b01000010000); + +`ifdef REGSHADOW_1DEEP +wire wr_en = ppu_wr | cpu_wr; +wire [7:0] wr_a = ppu_wr ? {2'b00, snes_pa[5:0]} + : {3'b010, snes_addr[4:0]}; + +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= snes_data; + rd_data <= mem[rd_addr[7:0]]; +end +`else +// Previous-byte trackers (ctx.v's rBG/rM7): consumed before being updated. +reg [7:0] prev_bg; initial prev_bg = 0; +`ifndef REGSHADOW_NO_M7 +reg [7:0] prev_m7; initial prev_m7 = 0; +`endif +wire is_bg_dbl = (snes_pa >= 8'h0D) && (snes_pa <= 8'h14); +`ifndef REGSHADOW_NO_M7 +wire is_m7_dbl = ((snes_pa >= 8'h0D) && (snes_pa <= 8'h0E)) + || ((snes_pa >= 8'h1B) && (snes_pa <= 8'h20)); +`endif + +// The strobe cycle stores the current byte (odd offset), the next cycle stores the +// previous one (even offset) out of these defer flops. Bus writes are dozens of +// clocks apart, so the defer can never collide with the next write. +reg wr2_pend; initial wr2_pend = 0; +reg [7:0] wr2_a; initial wr2_a = 0; +reg [7:0] wr2_d; initial wr2_d = 0; + +wire wr_en = ppu_wr | cpu_wr | wr2_pend; +wire [7:0] wr_a = wr2_pend ? wr2_a + : ppu_wr ? {1'b0, snes_pa[5:0], 1'b1} + : {3'b100, snes_addr[4:0]}; +wire [7:0] wr_d = wr2_pend ? wr2_d : snes_data; + +// One write enable and one write address in the process: anything else falls out +// of the XST block-RAM template and the memory is built from flip-flops instead. +// The arm is edge-guarded so a strobe wider than one cycle cannot re-arm with the +// trackers already updated, which would degrade the pair back to (value, value). +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= wr_d; + rd_data <= mem[rd_addr[7:0]]; + if (ppu_wr & ~wr2_pend) begin + wr2_pend <= 1'b1; + wr2_a <= {1'b0, snes_pa[5:0], 1'b0}; +`ifndef REGSHADOW_NO_M7 + wr2_d <= is_bg_dbl ? prev_bg : is_m7_dbl ? prev_m7 : snes_data; +`else + wr2_d <= is_bg_dbl ? prev_bg : snes_data; +`endif + if (is_bg_dbl) prev_bg <= snes_data; +`ifndef REGSHADOW_NO_M7 + if (is_m7_dbl) prev_m7 <= snes_data; +`endif + end else if (~ppu_wr) begin + wr2_pend <= 1'b0; + end +end +`endif + +endmodule diff --git a/verilog/sd2snes_obc1/sd2snes_obc1.xise b/verilog/sd2snes_obc1/sd2snes_obc1.xise index f3db7b1c..dfad21bc 100644 --- a/verilog/sd2snes_obc1/sd2snes_obc1.xise +++ b/verilog/sd2snes_obc1/sd2snes_obc1.xise @@ -19,6 +19,10 @@ + + + + diff --git a/verilog/sd2snes_sa1/Makefile b/verilog/sd2snes_sa1/Makefile index c855e16d..1a14a1d9 100644 --- a/verilog/sd2snes_sa1/Makefile +++ b/verilog/sd2snes_sa1/Makefile @@ -1,6 +1,6 @@ CORE = sa1 -VSRC = address.v cheat.v clk_test.v dac.v dcm.v sa1.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v ctx.v dac.v dcm.v dma.v sa1.v main.v mcu_cmd.v msu.v sd_dma.v spi.v VHSRC = COMMON_IP = dec_table sa1_div sa1_iram sa1_mult snescmd_buf diff --git a/verilog/sd2snes_sa1/address.v b/verilog/sd2snes_sa1/address.v index ece97419..3d897ad7 100644 --- a/verilog/sd2snes_sa1/address.v +++ b/verilog/sd2snes_sa1/address.v @@ -17,6 +17,17 @@ // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// + +// SA-1 savestate machinery gate. Mk.III only: on the Mk.II Spartan-3 the +// machinery overmaps the device by ~2,200 LUTs, so it is compiled out there and +// that netlist is unchanged. SA1_SS_MK2 is an opt-in escape for a larger target. +// Derived in every file that needs it: the repo uses no include files. +`ifdef MK3 +`define SA1_SS_ACTIVE +`elsif SA1_SS_MK2 +`define SA1_SS_ACTIVE +`endif + module address( input CLK, input [15:0] featurebits, // peripheral enable/disable @@ -29,9 +40,13 @@ module address( output IS_SAVERAM, // address/CS mapped as SRAM? output IS_ROM, // address mapped as ROM? output IS_WRITABLE, // address somehow mapped as writable area? + output IS_PATCH, // hook identity window active ($C0-FF while unlocked) + output sa1_ss_enable, // savestate scan window ($E8:0000-07FF while unlocked; 0 on mk2) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, + input snescmd_unlock, // snescmd region unlocked (gates the hook window) output msu_enable, + output dma_enable, // SNES-side $2020-$202F copier reg window input [4:0] sa1_bmaps_sbm, input sa1_dma_cc1_en, input [11:0] sa1_xxb, @@ -99,10 +114,33 @@ assign IS_SAVERAM = SAVERAM_MASK_r[0] ) ); -assign IS_WRITABLE = IS_SAVERAM; +// Hook identity window (as in sd2snes_base): while the hook holds the snescmd +// region unlocked, banks $C0-$FF are identity-mapped so the handler runs from menu +// PSRAM with its scratch in $F2-$FF. Without it the handler fetches the game's +// SuperMMC-mapped ROM instead of its own code. +`ifdef SA1_SS_ACTIVE +assign IS_PATCH = snescmd_unlock & &SNES_ADDR[23:22]; +`else +assign IS_PATCH = 1'b0; +`endif + +assign IS_WRITABLE = IS_SAVERAM | IS_PATCH; + +// Savestate scan window: $E8:0000-$07FF while unlocked; $000-$0FF is the state +// block, $7FF the halt control byte. $E8 also falls inside the identity window, so +// main.v gives this window priority in the read mux; the parallel PSRAM write lands +// in unused save-region space. +`ifdef SA1_SS_ACTIVE +assign sa1_ss_enable = snescmd_unlock & (SNES_ADDR[23:16] == 8'hE8) & (SNES_ADDR[15:11] == 5'b00000); +`else +assign sa1_ss_enable = 1'b0; +`endif // TODO: add programmable address map -assign SRAM_SNES_ADDR = (IS_SAVERAM +assign SRAM_SNES_ADDR = (IS_PATCH + // hook window: identity map $C0-$FF (handler code + scratch) + ? SNES_ADDR + : IS_SAVERAM // 40-4F:0000-FFFF or 00-3F/80-BF:6000-7FFF (first 8K mirror). Mask handles mirroring. 60 is sa1-only ? (24'hE00000 + (iram_battery_r ? SNES_ADDR[10:0] : ((SNES_ADDR[22] ? SNES_ADDR[19:0] : {sa1_bmaps_sbm,SNES_ADDR[12:0]}) & SAVERAM_MASK_r))) // C0-FF:0000-FFFF or 00-3F/80-BF:8000-FFFF @@ -114,6 +152,13 @@ assign ROM_ADDR = SRAM_SNES_ADDR; assign ROM_HIT = IS_ROM | IS_WRITABLE; assign msu_enable = featurebits[FEAT_MSU1] & (!SNES_ADDR[22] && ((SNES_ADDR[15:0] & 16'hfff8) == 16'h2000)); +// Copier reg window $2020-$202F (as in sd2snes_base). No map_unlock on this core, +// so gate on the snescmd unlock: the handler is the only user. +`ifdef SA1_SS_ACTIVE +assign dma_enable = snescmd_unlock & (!SNES_ADDR[22] && ((SNES_ADDR[15:0] & 16'hfff0) == 16'h2020)); +`else +assign dma_enable = 1'b0; +`endif assign r213f_enable = featurebits[FEAT_213F] & (SNES_PA == 8'h3f); assign r2100_hit = (SNES_PA == 8'h00); assign snescmd_enable = ({SNES_ADDR[22], SNES_ADDR[15:9]} == 8'b0_0010101); diff --git a/verilog/sd2snes_sa1/cheat.v b/verilog/sd2snes_sa1/cheat.v index dc05b58e..82d96d91 100644 --- a/verilog/sd2snes_sa1/cheat.v +++ b/verilog/sd2snes_sa1/cheat.v @@ -18,6 +18,14 @@ // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// + +// SA-1 savestate machinery gate; Mk.III only (see address.v). +`ifdef MK3 +`define SA1_SS_ACTIVE +`elsif SA1_SS_MK2 +`define SA1_SS_ACTIVE +`endif + module cheat( input clk, input [7:0] SNES_PA, @@ -34,6 +42,7 @@ module cheat( input branch3_enable, input pad_latch, input snes_ajr, + input ss_combo, // FPGA-detected savestate gesture (from the ctx JOY1 capture) input SNES_cycle_start, input [2:0] pgm_idx, input pgm_we, @@ -43,7 +52,13 @@ module cheat( output snescmd_unlock ); -//`define IRQ_HOOK_ENABLE +// IRQ hook: games that run their vblank logic on IRQ with NMI disabled are +// otherwise unreachable. The auto_nmi/auto_irq heuristic picks the vector per +// scene, as on the base core. Enabled only together with the savestate +// machinery, which brings the rate limit and gesture gate below. +`ifdef SA1_SS_ACTIVE +`define IRQ_HOOK_ENABLE +`endif wire snescmd_wr_strobe = snescmd_enable & SNES_wr_strobe; @@ -53,6 +68,29 @@ reg irq_enable = 0; reg holdoff_enable = 0; // temp disable hooks after reset reg buttons_enable = 0; reg wram_present = 0; +// Savestate handler enable (bit 6 of pgm reg 7): routes the hook's branch offsets +// into nmi_savestate. Present on the base core, stripped from this one. +reg savestate_enable = 0; +// Keeps the nmi_savestate branch routed until the handler returns: after a +// save/load it waits for the next hook entry with the buttons already released, +// which |pad_data alone would never deliver. Pulsed rather than latched as on the +// base core, because here it also arms the IRQ redirect below. +`ifdef SA1_SS_ACTIVE +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end +`else +wire savestate_force_entry = 1'b0; +`endif + wire branch_wram = cheat_enable & wram_present; reg auto_nmi_enable = 1; @@ -174,6 +212,31 @@ always @(posedge clk) begin end end +// IRQ hook rate limit and gesture gate. A raster effect fires an H-IRQ every +// scanline, and the handler is far longer than the ~63us gap between them, so +// hooking each one piles up handlers and the game hangs on its own delayed ISR. +// Two defences: after hooking one IRQ, suppress the redirect for ~12ms so a raster +// burst runs unhooked (the game's IRQ still vectors at full speed, only ours is +// skipped), and arm it only while a savestate gesture is physically held, so +// normal play sees no IRQ redirect at all. +// savestate_force_entry keeps it armed while a savestate is in flight and +// bypasses the auto_nmi/auto_irq heuristic: the frozen save has no vector fetches, +// so the usage window would roll over and unarm the very IRQ the resume waits for. +`ifdef IRQ_HOOK_ENABLE +reg [19:0] irq_hold = 0; +wire irq_hold_ok = ~|irq_hold; +wire irq_arm = irq_enable & irq_match_bits[1] & irq_hold_ok + & ((auto_irq_enable_sync & ss_combo) | savestate_force_entry); +always @(posedge clk) begin + if(SNES_reset_strobe) irq_hold <= 0; + else if(SNES_rd_strobe & hook_enable_sync & irq_arm & (cpu_push_cnt == 4)) + irq_hold <= 20'hfffff; + else if(|irq_hold) irq_hold <= irq_hold - 1'b1; +end +`else +wire irq_arm = 1'b0; +`endif + // make patched vectors visible for last cycles of NMI/IRQ handling only always @(posedge clk) begin if(SNES_reset_strobe) begin @@ -182,7 +245,7 @@ always @(posedge clk) begin if(hook_enable_sync & ((auto_nmi_enable_sync & nmi_enable & nmi_match_bits[1]) `ifdef IRQ_HOOK_ENABLE - |(auto_irq_enable_sync & irq_enable & irq_match_bits[1]) + | irq_arm `endif ) & cpu_push_cnt == 4) begin @@ -210,6 +273,10 @@ reg [6:0] snescmd_unlock_disable_countdown = 0; reg snescmd_unlock_disable = 0; always @(posedge clk) begin +`ifdef SA1_SS_ACTIVE + savestate_force_entry_enable_strobe <= 0; + savestate_force_entry_disable_strobe <= 0; +`endif if(SNES_reset_strobe) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; @@ -219,7 +286,7 @@ always @(posedge clk) begin if(hook_enable_sync & ((auto_nmi_enable_sync & nmi_enable & nmi_match_bits[1]) `ifdef IRQ_HOOK_ENABLE - |(auto_irq_enable_sync & irq_enable & irq_match_bits[1]) + | irq_arm `endif ) & cpu_push_cnt == 4) begin @@ -230,6 +297,13 @@ always @(posedge clk) begin if(rst_match_bits[1] & |reset_unlock_r) begin snescmd_unlock_r <= 1; end +`ifdef SA1_SS_ACTIVE + // arm the savestate force-entry latch when the hook redirect is taken with + // buttons held; it holds the nmi_savestate route through the button release. + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; + end +`endif end // give some time to exit snescmd memory and jump to original vector // sta @NMI_VECT_DISABLE 1-2 (after effective write) @@ -242,6 +316,10 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; +`ifdef SA1_SS_ACTIVE + // drop the force-entry latch at the same unlock-drop point + savestate_force_entry_disable_strobe <= 1; +`endif end end end @@ -338,12 +416,12 @@ always @(posedge clk) begin end else if(pgm_idx == 6) begin // set rom patch enable cheat_enable_mask <= pgm_in[5:0]; end else if(pgm_idx == 7) begin // set/reset global enable / hooks - // pgm_in[13:8] are reset bit flags - // pgm_in[5:0] are set bit flags - {wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - <= ({wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - & ~pgm_in[13:8]) - | pgm_in[5:0]; + // pgm_in[14:8] are reset bit flags + // pgm_in[6:0] are set bit flags + {savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + <= ({savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + & ~pgm_in[14:8]) + | pgm_in[6:0]; end end end @@ -388,7 +466,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & (savestate_force_entry | |pad_data)) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end else begin @@ -406,7 +488,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & |pad_data) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end @@ -417,7 +503,19 @@ always @* begin end else if(branch_wram) begin branch2_offset = 8'h00; // nmi_patches end else begin - branch2_offset = 8'h09; // nmi_exit + if(savestate_enable) begin + branch2_offset = 8'h05; // nmi_savestate + end else begin + branch2_offset = 8'h09; // nmi_exit + end + end +end + +always @* begin + if(savestate_enable) begin + branch3_offset = 8'h00; // nmi_savestate + end else begin + branch3_offset = 8'h04; // nmi_exit end end diff --git a/verilog/sd2snes_sa1/ctx.v b/verilog/sd2snes_sa1/ctx.v new file mode 100644 index 00000000..b824076c --- /dev/null +++ b/verilog/sd2snes_sa1/ctx.v @@ -0,0 +1,599 @@ +`timescale 1ns / 1ps +////////////////////////////////////////////////////////////////////////////////// +// Company: +// Engineer: +// +// Create Date: 06:25:58 08/12/2017 +// Design Name: +// Module Name: ctx +// Project Name: +// Target Devices: +// Tool versions: +// Description: +// +// Dependencies: +// +// Revision: +// Revision 0.01 - File Created +// Additional Comments: +// +////////////////////////////////////////////////////////////////////////////////// +module ctx( + input clkin, + input reset, + input [23:0] SNES_ADDR, // requested address from SNES + input [7:0] SNES_PA, // peripheral address from SNES + input SNES_RD_end_PRE, // READ from SNES + input SNES_WR_end_PRE, // WRITE from SNES + input SNES_PARD_end_PRE, // PARD from SNES + input SNES_PAWR_end_PRE, // PAWR from SNES + input [7:0] SNES_DATA_IN_PRE, + + //output OE_RD_ENABLE, + output OE_WR_ENABLE, + output OE_PAWR_ENABLE, + output OE_PARD_ENABLE, + + output BUS_WRQ, + input BUS_RDY, + + input snescmd_unlock, + + output [23:0] ROM_ADDR, // Address to request from SRAM0 + output [15:0] ROM_DATA, // Data to write to SRAM0 + output ROM_WORD_ENABLE, + + output DBG, + output [15:0] PAD1_OUT // captured JOY1 ($4218/$4219) from the CPUREG store-sniffing FSM +); + +`define CTX_APU_ENABLE + +reg [7:0] SNES_DATA_IN; always @(posedge clkin) SNES_DATA_IN <= SNES_DATA_IN_PRE; +reg SNES_RD_end; always @(posedge clkin) SNES_RD_end <= SNES_RD_end_PRE; +reg SNES_WR_end; always @(posedge clkin) SNES_WR_end <= SNES_WR_end_PRE; +reg SNES_PARD_end; always @(posedge clkin) SNES_PARD_end <= SNES_PARD_end_PRE; +reg SNES_PAWR_end; always @(posedge clkin) SNES_PAWR_end <= SNES_PAWR_end_PRE; + +//------------------- +// handle WRAM writes - upper 7b ignored +//------------------- +reg [23:0] WRAM_ADDR; + +// bank $00-$3F,$80-$BF +reg IS_WRAM_SHADOW_ADDR_r; initial IS_WRAM_SHADOW_ADDR_r = 0; +reg IS_WRAM_BANK_ADDR_r; initial IS_WRAM_BANK_ADDR_r = 0; +reg IS_WRAM_PA_ADDR_r; initial IS_WRAM_PA_ADDR_r = 0; +assign IS_WRAM_SHADOW_ADDR = !SNES_ADDR[22] && (SNES_ADDR[15:13] == 3'h0); +assign IS_WRAM_SHADOW = SNES_WR_end && IS_WRAM_SHADOW_ADDR_r; +assign IS_WRAM_BANK_ADDR = ({SNES_ADDR[23:17],1'b0} == 8'h7E); +assign IS_WRAM_BANK = SNES_WR_end && IS_WRAM_BANK_ADDR_r; +assign IS_WRAM_PA_ADDR = (SNES_PA == 8'h80); +assign IS_WRAM_PA = SNES_PAWR_end && IS_WRAM_PA_ADDR_r; + +assign IS_WRAM_ADDR = IS_WRAM_SHADOW_ADDR_r | IS_WRAM_BANK_ADDR_r | IS_WRAM_PA_ADDR_r; +assign IS_WRAM = IS_WRAM_SHADOW | IS_WRAM_BANK | IS_WRAM_PA; + +// flop register state +always @(posedge clkin) begin + if (reset) begin + WRAM_ADDR <= 0; + + IS_WRAM_SHADOW_ADDR_r <= 0; + IS_WRAM_BANK_ADDR_r <= 0; + IS_WRAM_PA_ADDR_r <= 0; + end + else begin + IS_WRAM_SHADOW_ADDR_r <= IS_WRAM_SHADOW_ADDR; + IS_WRAM_BANK_ADDR_r <= IS_WRAM_BANK_ADDR; + IS_WRAM_PA_ADDR_r <= IS_WRAM_PA_ADDR; + + if (SNES_PAWR_end | SNES_PARD_end) begin + if (SNES_PA == 8'h80) WRAM_ADDR[16: 0] <= WRAM_ADDR[16:0] + 1'b1; + end + if (SNES_PAWR_end) begin + if (SNES_PA == 8'h81) WRAM_ADDR[ 7: 0] <= SNES_DATA_IN; + else if (SNES_PA == 8'h82) WRAM_ADDR[15: 8] <= SNES_DATA_IN; + else if (SNES_PA == 8'h83) WRAM_ADDR[23:16] <= SNES_DATA_IN; + end + end +end + +//------------------- +// handle VRAM writes +//------------------- +reg [7:0] r2115; +reg [15:0] VRAM_ADDR; + +reg VRAM_ADDR_r; initial VRAM_ADDR_r = 0; +assign IS_VRAM_ADDR = VRAM_ADDR_r; +assign IS_VRAM = SNES_PAWR_end && IS_VRAM_ADDR; + +// flop register state +always @(posedge clkin) begin + if (reset) begin + VRAM_ADDR_r <= 0; + end + else begin + VRAM_ADDR_r <= (SNES_PA == 8'h18 || SNES_PA == 8'h19); + end + + if (SNES_PARD_end) begin + if (SNES_PA == 8'h39 && ~r2115[7]) VRAM_ADDR[15:0] <= VRAM_ADDR[15:0] + ({r2115[1],1'b0,(~r2115[1] & r2115[0]),4'b0000,(~r2115[1] & ~r2115[0])}); + else if (SNES_PA == 8'h3A && r2115[7]) VRAM_ADDR[15:0] <= VRAM_ADDR[15:0] + ({r2115[1],1'b0,(~r2115[1] & r2115[0]),4'b0000,(~r2115[1] & ~r2115[0])}); + end + else if (SNES_PAWR_end) begin + if (SNES_PA == 8'h15) r2115 [ 7: 0] <= SNES_DATA_IN; + else if (SNES_PA == 8'h16) VRAM_ADDR[ 7: 0] <= SNES_DATA_IN; + else if (SNES_PA == 8'h17) VRAM_ADDR[15: 8] <= SNES_DATA_IN; + else if (SNES_PA == 8'h18 && ~r2115[7]) VRAM_ADDR[15:0] <= VRAM_ADDR[15:0] + ({r2115[1],1'b0,(~r2115[1] & r2115[0]),4'b0000,(~r2115[1] & ~r2115[0])}); + else if (SNES_PA == 8'h19 && r2115[7]) VRAM_ADDR[15:0] <= VRAM_ADDR[15:0] + ({r2115[1],1'b0,(~r2115[1] & r2115[0]),4'b0000,(~r2115[1] & ~r2115[0])}); + end +end + +//------------------- +// handle APU writes +//------------------- +reg [7:0] r214x[3:0]; +reg [15:0] APU_ADDR; +initial APU_ADDR = 0; +reg [2:0] APU_STATE; +initial APU_STATE = 0; +reg IS_APU_RAM_r; initial IS_APU_RAM_r = 0; +reg IS_APU_PORT_r; initial IS_APU_PORT_r = 0; + +wire [7:0] APU_STATUS_COMPARE_PRE = (r214x[0] + 1) - SNES_DATA_IN; +wire [7:0] APU_SNES_PA_PRE = ({SNES_PA[7:6],4'b0000,SNES_PA[1:0]}); + +reg [7:0] APU_STATUS_COMPARE; always @(posedge clkin) APU_STATUS_COMPARE <= APU_STATUS_COMPARE_PRE; +reg [7:0] APU_SNES_PA; always @(posedge clkin) APU_SNES_PA <= APU_SNES_PA_PRE; + +parameter APU_STATE_INIT = 0; +parameter APU_STATE_INIT_BB = 1; +parameter APU_STATE_INIT_AA = 2; +parameter APU_STATE_INIT_IDLE = 3; +parameter APU_STATE_IDLE = 4; +parameter APU_STATE_DATA_INIT = 5; +parameter APU_STATE_DATA = 6; +parameter APU_STATE_DONE = 7; + +assign IS_APU_RAM = ((APU_STATE == APU_STATE_DATA_INIT) && (SNES_PAWR_end && (APU_SNES_PA == 8'h40) && (SNES_DATA_IN == 0))) + | ((APU_STATE == APU_STATE_DATA) && (SNES_PAWR_end && (APU_SNES_PA == 8'h40) && (APU_STATUS_COMPARE == 0))); +// ignore writes when in done state +// ignore $2140 writes since we don't need that state and it frees up a slot for RAM writes +assign IS_APU_PORT = SNES_PAWR_end && ({SNES_PA[7:6],6'b000000} == 8'h40) && (|SNES_PA[1:0]) && (APU_STATE != APU_STATE_DONE); +assign IS_APU = IS_APU_RAM_r | IS_APU_PORT_r; + +assign IS_APU_PORT_ADDR = {SNES_PA[7:6],6'b000000} == 8'h40; + +reg IS_APU_PORT_ADDR_r = 0; + +`ifdef CTX_APU_ENABLE +always @(posedge clkin) begin + IS_APU_PORT_ADDR_r <= IS_APU_PORT_ADDR; + + if (reset) begin + APU_STATE <= 0; + APU_ADDR <= 0; + + r214x[0] <= 0; + r214x[1] <= 0; + r214x[2] <= 0; + r214x[3] <= 0; + + IS_APU_RAM_r <= 0; + IS_APU_PORT_r <= 0; + end + else begin + IS_APU_RAM_r <= IS_APU_RAM; + IS_APU_PORT_r <= IS_APU_PORT; + + // update register state on register write + if (SNES_PAWR_end && IS_APU_PORT_ADDR_r) r214x[SNES_PA[1:0]] <= SNES_DATA_IN; + + // increment addresses on ram write + if (IS_APU_RAM_r) APU_ADDR <= APU_ADDR + 1; + + case (APU_STATE) + APU_STATE_INIT: begin + // INIT wait for read of AA or BB + if (SNES_PARD_end && (APU_SNES_PA == 8'h40)) begin + if (SNES_DATA_IN == 8'hAA) APU_STATE <= APU_STATE_INIT_BB; + end + else if (SNES_PARD_end && (APU_SNES_PA == 8'h41)) begin + if (SNES_DATA_IN == 8'hBB) APU_STATE <= APU_STATE_INIT_AA; + end + end + APU_STATE_INIT_BB: begin + if (SNES_PARD_end && (APU_SNES_PA == 8'h41)) begin + // INIT wait for read of BB + if (SNES_DATA_IN == 8'hBB) APU_STATE <= APU_STATE_INIT_IDLE; + end + end + APU_STATE_INIT_AA: begin + if (SNES_PARD_end && (APU_SNES_PA == 8'h40)) begin + // INIT wait for read of AA + if (SNES_DATA_IN == 8'hAA) APU_STATE <= APU_STATE_INIT_IDLE; + end + end + APU_STATE_INIT_IDLE: begin + if (SNES_PAWR_end && (APU_SNES_PA == 8'h40) && (SNES_DATA_IN == 8'hCC)) begin + // exiting init, wait for write of CC + if (r214x[1] == 8'h00) APU_STATE <= APU_STATE_DONE; + else APU_STATE <= APU_STATE_DATA_INIT; + + APU_ADDR <= {r214x[3],r214x[2]}; + end + end + APU_STATE_DATA_INIT: begin + if (SNES_PAWR_end && (APU_SNES_PA == 8'h40) && (SNES_DATA_IN == 8'h00)) begin + // wait for init of the offset field. this is also the first write + APU_STATE <= APU_STATE_DATA; + end + end + APU_STATE_DATA: begin + // check for write that isn't +1 + if (SNES_PAWR_end && (APU_SNES_PA == 8'h40) && APU_STATUS_COMPARE[7]) begin + if (r214x[1] == 8'h00) APU_STATE <= APU_STATE_DONE; + else APU_STATE <= APU_STATE_DATA_INIT; + + APU_ADDR <= {r214x[3],r214x[2]}; + end + end + APU_STATE_DONE: begin + // nothing to do here. wait for reset + end + endcase + + end +end +`endif + +//------------------- +// handle CGRAM writes +//------------------- +// FIXME: need to model the internal flop to handle mid-word address changes +reg [8:0] CGRAM_ADDR; + +reg IS_CGRAM_ADDR_r; initial IS_CGRAM_ADDR_r = 0; +assign IS_CGRAM_ADDR = (SNES_PA == 8'h22); +assign IS_CGRAM = SNES_PAWR_end && IS_CGRAM_ADDR_r; + +// flop register state +always @(posedge clkin) begin + IS_CGRAM_ADDR_r <= IS_CGRAM_ADDR; + + if (SNES_PARD_end) begin + if (SNES_PA == 8'h3B) CGRAM_ADDR[8:0] <= CGRAM_ADDR[8:0] + 1'b1; + end + else if (SNES_PAWR_end) begin + if (SNES_PA == 8'h21) CGRAM_ADDR[8:0] <= {SNES_DATA_IN,1'b0}; + else if (SNES_PA == 8'h22) CGRAM_ADDR[8:0] <= CGRAM_ADDR[8:0] + 1'b1; + end +end + +//------------------- +// handle OAM writes +//------------------- +// FIXME: need to model the internal flop to handle mid-word address changes +reg [9:0] OAM_ADDR; + +reg IS_OAM_ADDR_r; initial IS_OAM_ADDR_r = 0; +assign IS_OAM_ADDR = (SNES_PA == 8'h04); +assign IS_OAM = SNES_PAWR_end && IS_OAM_ADDR_r; + +// flop register state +always @(posedge clkin) begin + IS_OAM_ADDR_r <= IS_OAM_ADDR; + + if (SNES_PARD_end) begin + if (SNES_PA == 8'h38) OAM_ADDR[9:0] <= OAM_ADDR[9:0] + 1'b1; + end + else if (SNES_PAWR_end) begin + if (SNES_PA == 8'h02) OAM_ADDR[9:0] <= {OAM_ADDR[9],SNES_DATA_IN,1'b0}; + else if (SNES_PA == 8'h03) OAM_ADDR[9:0] <= {SNES_DATA_IN[0],OAM_ADDR[8:1],1'b0}; + else if (SNES_PA == 8'h04) OAM_ADDR[9:0] <= OAM_ADDR[9:0] + 1'b1; + end +end + +//------------------- +// handle $21XX accesses +//------------------- + +// WRITES +// $00-$03 // skip data $04 +// $05-$17 // skip data $18-$19 +// $1A-$21 // skip data $22 +// $23-$33 +// $81-$83 // skip data $80 + +// READ +// $34-$36 // skip latch and data registers +// $3C-$3F +reg [7:0] rBG, rM7; +reg PPUREG_WRITE_ADDR_r; initial PPUREG_WRITE_ADDR_r = 0; +reg PPUREG_READ_ADDR_r; initial PPUREG_READ_ADDR_r = 0; +reg IS_PAWR_r; + +assign IS_PAWR = (SNES_PA <= 8'h33); + +assign IS_PPUREG_WRITE_ADDR = PPUREG_WRITE_ADDR_r; +assign IS_PPUREG_WRITE = SNES_PAWR_end && IS_PPUREG_WRITE_ADDR; + +assign IS_PPUREG_READ_ADDR = PPUREG_READ_ADDR_r; +assign IS_PPUREG_READ = SNES_PARD_end && IS_PPUREG_READ_ADDR; + +assign IS_PPUREG_ADDR = IS_PPUREG_WRITE_ADDR | IS_PPUREG_READ_ADDR; +assign IS_PPUREG = IS_PPUREG_WRITE | IS_PPUREG_READ; + +// double +assign IS_BG0_DOUBLE_ADDR = (SNES_PA >= 8'h0D && SNES_PA <= 8'h0E); +assign IS_BGN_DOUBLE_ADDR = (SNES_PA >= 8'h0F && SNES_PA <= 8'h14); +assign IS_M7_DOUBLE_ADDR = (SNES_PA >= 8'h1B && SNES_PA <= 8'h20); + +assign IS_BG_DOUBLE = SNES_PAWR_end && (IS_BG0_DOUBLE_ADDR || IS_BGN_DOUBLE_ADDR); +assign IS_M7_DOUBLE = SNES_PAWR_end && (IS_BG0_DOUBLE_ADDR || IS_M7_DOUBLE_ADDR); + +always @(posedge clkin) begin + IS_PAWR_r <= IS_PAWR; + + if (reset) begin + rBG <= 0; + rM7 <= 0; + + PPUREG_WRITE_ADDR_r <= 0; + PPUREG_READ_ADDR_r <= 0; + end + else begin + if (IS_BG_DOUBLE) rBG <= SNES_DATA_IN; + if (IS_M7_DOUBLE) rM7 <= SNES_DATA_IN; + + // <= 33, ignore data registers or 81,82,83 (not 80) + PPUREG_WRITE_ADDR_r <= ((SNES_PA <= 8'h33) && (SNES_PA != 8'h04) && (SNES_PA != 8'h18) && (SNES_PA != 8'h19) && (SNES_PA != 8'h22)) || ((SNES_PA != 8'h80) && ({SNES_PA[7:2],2'b00} == 8'h80)); + // 34-36, 3C-3F + PPUREG_READ_ADDR_r <= ((SNES_PA > 8'h33) && (SNES_PA <= 8'h3F)) && (SNES_PA != 8'h37) && (SNES_PA != 8'h38) && (SNES_PA != 8'h39) && (SNES_PA != 8'h3A) && (SNES_PA != 8'h3B); + end +end + +// handle double registers + +//------------------- +// handle $42XX accesses. Covers DMA +//------------------- +// WRITES +// $4200-$420F +// $4300-$43FF + +// READ +// $4210-$421F +reg [7:0] r421x[15:0]; +reg [2:0] CPUREG_STATE; +reg [3:0] CPUREG_ADDR; +reg CPUREG_DOUBLE; +reg [7:0] CPUREG_INST[3:0]; +reg [27:0] CPUREG_COUNTER; + +integer i; +initial for (i = 0; i < 16; i = i + 1) r421x[i] = 0; +initial CPUREG_STATE = 0; + +reg CPUREG_WRITE_ADDR_r; initial CPUREG_WRITE_ADDR_r = 0; +reg CPUREG_READ_ADDR_r; initial CPUREG_READ_ADDR_r = 0; + +assign IS_CPUREG_WRITE_ADDR = CPUREG_WRITE_ADDR_r; +assign IS_CPUREG_WRITE = SNES_WR_end && IS_CPUREG_WRITE_ADDR; + +assign IS_CPUREG_READ_ADDR = CPUREG_READ_ADDR_r; +assign IS_CPUREG_READ = SNES_RD_end && IS_CPUREG_READ_ADDR; + +assign IS_CPUREG_ADDR = IS_CPUREG_WRITE_ADDR | IS_CPUREG_READ_ADDR; +assign IS_CPUREG = IS_CPUREG_WRITE | IS_CPUREG_READ; + +always @(posedge clkin) begin + if (reset) begin + CPUREG_WRITE_ADDR_r <= 0; + CPUREG_READ_ADDR_r <= 0; + end + else begin + // $43x0-$43xA (x < 8) + CPUREG_WRITE_ADDR_r <= (({1'b0,SNES_ADDR[22],6'b000000, SNES_ADDR[15:4], 4'b0000} == 24'h04200) && (SNES_ADDR[3:0] <= 4'hD)) || (({1'b0,SNES_ADDR[22],6'b000000, SNES_ADDR[15:7], 7'b0000000} == 24'h04300)) && (SNES_ADDR[3:0] <= 4'hA); + // this isn't used for level shifter enable so ok to get larger regions. + CPUREG_READ_ADDR_r <= {1'b0,SNES_ADDR[22],6'b000000, SNES_ADDR[15:4], 4'b0000} == 24'h04210; + end +end + +// FIXME: This is broken because it's testing for reads and associated data to find ST to memory. RDs from WRAM are not visible right now. +always @(posedge clkin) begin + if (reset || (~CPUREG_COUNTER[25])) begin + // don't zero out the non-counter state outside of reset + if (reset) for (i = 0; i < 8; i = i + 1) r421x[i] <= 0; + for (i = 8; i < 16; i = i + 1) r421x[i] <= 0; + CPUREG_STATE <= 0; + CPUREG_COUNTER <= 28'h3000000; + end + else begin + // reset counter for zeroing out stale writes. Only allow controller writes to do this. + if (CPUREG_ADDR[3] && CPUREG_STATE == 3 && SNES_WR_end) begin + CPUREG_COUNTER <= 28'h3000000; + end + else if (CPUREG_COUNTER[25]) begin + CPUREG_COUNTER <= CPUREG_COUNTER - 1; + end + + if (SNES_RD_end) begin + CPUREG_INST[3] <= CPUREG_INST[2]; + CPUREG_INST[2] <= CPUREG_INST[1]; + CPUREG_INST[1] <= CPUREG_INST[0]; + CPUREG_INST[0] <= SNES_DATA_IN; + end + + case (CPUREG_STATE) + 0: begin + // watch for read of $18 + // ignore read during NMI hook since it usually gets prelatched data (00 or other) + // ignore AND in capcom games which is trying to capture differences from the previous frame + if (!snescmd_unlock && IS_CPUREG_READ && ({CPUREG_INST[2][7:5],1'h0,CPUREG_INST[2][3:0],CPUREG_INST[1][7:4],4'h0,CPUREG_INST[0]} != 24'h2D1042)) begin + CPUREG_STATE <= 1; + CPUREG_ADDR <= SNES_ADDR[3:0]; + CPUREG_DOUBLE <= 0; + end + end + 1: begin + // watch for read of +1 + if (IS_CPUREG_READ && (SNES_ADDR[3:0] == CPUREG_ADDR[3:0] + 1)) begin + CPUREG_STATE <= 2; + CPUREG_DOUBLE <= 1; + end + else if (SNES_RD_end && ({SNES_DATA_IN[7:5],1'b0} == 4'h8) && ({1'b0,SNES_DATA_IN[2],1'b0,SNES_DATA_IN[0]} != 4'h0) && (SNES_DATA_IN[3:0] != 4'hB) && (SNES_DATA_IN[7:0] != 8'h99)) begin + // single + CPUREG_STATE <= 3; + end + else if (SNES_RD_end | SNES_WR_end | SNES_PARD_end | SNES_PAWR_end) begin + // reset + CPUREG_STATE <= 0; + end + end + 2: begin + // watch for immediate ST opcode + if (SNES_RD_end && ({SNES_DATA_IN[7:5],1'b0} == 4'h8) && ({1'b0,SNES_DATA_IN[2],1'b0,SNES_DATA_IN[0]} != 4'h0) && (SNES_DATA_IN[3:0] != 4'hB) && (SNES_DATA_IN[7:0] != 8'h99)) begin + CPUREG_STATE <= 3; + end + else if (SNES_RD_end | SNES_WR_end | SNES_PARD_end | SNES_PAWR_end) begin + // reset + CPUREG_STATE <= 0; + end + end + 3: begin + // watch for data write low + if (SNES_RD_end) begin + // ok to read rest of opcode + CPUREG_STATE <= 3; + end + else if (SNES_WR_end) begin + // capture data + r421x[CPUREG_ADDR] <= SNES_DATA_IN; + CPUREG_STATE <= CPUREG_DOUBLE ? 4 : 0; + end + else if (SNES_RD_end | SNES_WR_end | SNES_PARD_end | SNES_PAWR_end) begin + // reset + CPUREG_STATE <= 0; + end + end + 4: begin + // watch for data write high + if (SNES_WR_end) begin + // capture data + r421x[CPUREG_ADDR + 1] <= SNES_DATA_IN; + CPUREG_STATE <= 0; + end + else if (SNES_RD_end | SNES_WR_end | SNES_PARD_end | SNES_PAWR_end) begin + // reset + CPUREG_STATE <= 0; + end + end + endcase + end +end + +//------------------- +// handle MISC accesses. +//------------------- +// This space has $E0 bytes for random crap + +// gamepad is a hack. the read happens at the start of the NMI which is likely to get invalid data. it needs to be filtered. +reg IS_GAMEPAD_WRITE_ADDR_r; initial IS_GAMEPAD_WRITE_ADDR_r = 0; +assign IS_GAMEPAD_WRITE_ADDR = ({SNES_ADDR[23:1],1'b0} == 24'h002BF0); +assign IS_GAMEPAD_WRITE = SNES_WR_end && IS_GAMEPAD_WRITE_ADDR_r; + +assign IS_MISC_ADDR = IS_GAMEPAD_WRITE_ADDR; +assign IS_MISC = IS_GAMEPAD_WRITE; + +reg IS_MISC_ADDR_r; + +always @(posedge clkin) begin + IS_GAMEPAD_WRITE_ADDR_r <= IS_GAMEPAD_WRITE_ADDR; + IS_MISC_ADDR_r <= IS_MISC_ADDR; +end + +//------------------- +// generate address +//------------------- +wire [23:0] SRAM_SNES_ADDR; +assign SRAM_SNES_ADDR[23:0] = IS_WRAM + ? (24'hF50000 + ( IS_WRAM_SHADOW ? SNES_ADDR[12:0] + : IS_WRAM_BANK ? SNES_ADDR[16:0] + : WRAM_ADDR[16:0])) + : IS_VRAM + ? (24'hF70000 + ( (r2115[3:2] == 2'h0) ? ({VRAM_ADDR[14: 0], SNES_PA[0]}) + : (r2115[3:2] == 2'h1) ? ({VRAM_ADDR[14: 8],VRAM_ADDR[4:0],VRAM_ADDR[7:5],SNES_PA[0]}) + : (r2115[3:2] == 2'h2) ? ({VRAM_ADDR[14: 9],VRAM_ADDR[5:0],VRAM_ADDR[8:6],SNES_PA[0]}) + : ({VRAM_ADDR[14:10],VRAM_ADDR[6:0],VRAM_ADDR[9:7],SNES_PA[0]}))) + : IS_CGRAM + ? (24'hF90000 + CGRAM_ADDR[8:0]) + : IS_OAM + ? (24'hF90200 + ( OAM_ADDR[9] ? (OAM_ADDR[9:0] & 10'h21F) + : (OAM_ADDR[9:0] ))) + : IS_PPUREG + ? (24'hF90500 + {SNES_PA[7:0],1'b0}) + : IS_CPUREG + ? (24'hF90700 + SNES_ADDR[8:0]) + : IS_MISC + ? (24'hF90420 + ( IS_GAMEPAD_WRITE ? ({7'h00,SNES_ADDR[0]}) + : (8'hDF ))) + : IS_APU + ? (24'hF80000 + ( IS_APU_RAM_r ? (APU_ADDR[15:0] ) + : (8'hF4 + SNES_PA[1:0]))) + : 24'hF98000; + +assign IS_WRITE = IS_WRAM | IS_VRAM | IS_CGRAM | IS_OAM | IS_APU | IS_PPUREG | IS_CPUREG | IS_MISC; // | IS_SNESCAST_NMI; // NMI for SNESCAST +assign IS_WORD = IS_PPUREG; + +// flop request +reg REQ; +initial REQ = 1'b0; +reg [23:0] ADDR; +initial ADDR = 24'h0; +reg [15:0] DATA; +initial DATA = 16'h0000; +reg WORD; +initial WORD = 0; + +// doubles +wire [7:0] DATA_SINGLE_IN = IS_CPUREG_READ ? r421x[SNES_ADDR[3:0]] : IS_APU_RAM_r ? r214x[1] : SNES_DATA_IN[7:0]; + +always @(posedge clkin) begin + if (IS_WRITE) begin + // this is only asserted once as the main code flops it + REQ <= 1; + ADDR[23:0] <= SRAM_SNES_ADDR[23:0]; + // The following handles double writes. This approximates the data value used to assign + // the register by assigning the lower byte based on a past write. Note that M7 double + // overlaps with BG so BG should have priority when assigning data + DATA[15:0] <= { DATA_SINGLE_IN, (IS_BG_DOUBLE ? rBG : IS_M7_DOUBLE ? rM7 : DATA_SINGLE_IN) }; + WORD <= IS_WORD; + end + else begin + REQ <= 0; + end +end + +// assign outputs +assign BUS_WRQ = REQ & BUS_RDY; +assign ROM_ADDR[23:0] = ADDR[23:0]; +assign ROM_DATA[15:0] = DATA[15:0]; +assign ROM_WORD_ENABLE = WORD; + +// TODO: figure out if we need WRAM and other non-PA reads +//assign OE_RD_ENABLE = IS_CPUREG_READ_ADDR; +assign OE_WR_ENABLE = (IS_WRAM_SHADOW_ADDR_r || IS_WRAM_BANK_ADDR_r || CPUREG_WRITE_ADDR_r || IS_MISC_ADDR_r); +assign OE_PAWR_ENABLE = (IS_WRAM_PA_ADDR_r || IS_PAWR_r || IS_APU_PORT_ADDR_r || PPUREG_WRITE_ADDR_r); +assign OE_PARD_ENABLE = (IS_APU_PORT_ADDR_r || PPUREG_READ_ADDR_r); +assign DBG = |CPUREG_STATE; + +// JOY1 as seen by the game. $4218/$4219 are internal CPU registers: the read +// cycle appears on the cartridge bus but the data is not driven externally, so the +// cartridge cannot snoop the read itself. The CPUREG machine above already sniffs +// the LDA/STA pair and captures the value of the store, so export what it latched. +assign PAD1_OUT = {r421x[9], r421x[8]}; + +endmodule diff --git a/verilog/sd2snes_sa1/dma.v b/verilog/sd2snes_sa1/dma.v new file mode 100644 index 00000000..10c86893 --- /dev/null +++ b/verilog/sd2snes_sa1/dma.v @@ -0,0 +1,226 @@ +`timescale 1ns / 1ps +////////////////////////////////////////////////////////////////////////////////// +// Company: +// Engineer: +// +// Create Date: 19:31:43 08/22/2017 +// Design Name: +// Module Name: dma +// Project Name: +// Target Devices: +// Tool versions: +// Description: +// +// Dependencies: +// +// Revision: +// Revision 0.01 - File Created +// Additional Comments: +// +////////////////////////////////////////////////////////////////////////////////// +module dma( + input clkin, + input reset, + input enable, + + input [3:0] reg_addr, + input [7:0] reg_data_in, + output [7:0] reg_data_out, + + input reg_oe_falling, + input reg_we_rising, + + output loop_enable, + + input BUS_RDY, + output BUS_RRQ, + output BUS_WRQ, + + output [23:0] ROM_ADDR, + output [15:0] ROM_DATA_OUT, + output ROM_WORD_ENABLE, + input [15:0] ROM_DATA_IN +); + +parameter ST_IDLE = 0; +parameter ST_READ = 1; +parameter ST_WRITE = 2; +parameter ST_DONE = 3; + +parameter OP_COPY = 0; +parameter OP_RESET = 1; +parameter OP_SET = 2; +parameter OP_DEBUG = 3; + +// Register bank +reg [7:0] dma_r[9:0]; + +reg [2:0] state; initial state = ST_IDLE; + +initial begin + dma_r[0] = 8'h00; // dst bank + dma_r[1] = 8'h00; // src bank + dma_r[2] = 8'h00; // dst[7:0] + dma_r[3] = 8'h00; // dst[15:8] + dma_r[4] = 8'h00; // src[7:0] + dma_r[5] = 8'h00; // src[15:0] + dma_r[6] = 8'h00; // len[7:0] + dma_r[7] = 8'h00; // len[15:8] + dma_r[8] = 8'h00; // len[23:16] + dma_r[9] = 8'h00; // opcode[7:3], loop, direction, trigger +end + +reg [7:0] data_out_r; +assign reg_data_out = data_out_r; + +integer i; +always @(posedge clkin) begin + if(reg_oe_falling & enable) begin + case(reg_addr) + 4'h0: data_out_r <= 8'h53; + 4'h1: data_out_r <= 8'h2D; + 4'h2: data_out_r <= 8'h44; + 4'h3: data_out_r <= 8'h4D; + 4'h4: data_out_r <= 8'h41; + 4'h5: data_out_r <= 8'h31; + 4'h9: data_out_r <= dma_r[reg_addr]; + default: data_out_r <= 8'h00; + endcase + end +end + +always @(posedge clkin) begin + if (reset) begin + for (i = 0; i < 10; i = i + 1) dma_r[i] <= 0; + end + else if(reg_we_rising & enable) begin + dma_r[reg_addr] <= reg_data_in; + end + else if (state == ST_DONE) begin + dma_r[9][0] <= 0; // clear single-op trigger + end +end + +wire [23:0] SRC_ADDR, DST_ADDR, LEN; +wire [4:0] OPCODE; +wire LOOP, DIR, TRIG, WORD_MODE; + +assign SRC_ADDR = {dma_r[1], dma_r[3], dma_r[2]}; +assign DST_ADDR = {dma_r[0], dma_r[5], dma_r[4]}; +assign LEN = {dma_r[8], dma_r[7], dma_r[6]}; +assign OPCODE = dma_r[9][7:3]; +assign LOOP = dma_r[9][2]; +assign DIR = dma_r[9][1]; +assign TRIG = dma_r[9][0]; +// this covers misaligned addresses, misaligned length, as well as byte overlaps +assign WORD_MODE = !SRC_ADDR[0] && !DST_ADDR[0] && !LEN[0]; + +reg [15:0] data; +reg [4:0] opcode_r; +reg loop_r; initial loop_r = 0; +reg dir_r; +reg trig_r; initial trig_r = 0; +reg word_mode_r; +reg [23:0] src_addr_r, dst_addr_r, length_r, mod_r; + +// 1-trigger queue: the savestate handler fires its second op without polling, so a +// trigger arriving while the copier is busy is latched with the op snapshotted and +// started in ST_IDLE. One slot is all it needs; a further trigger while one is +// already pending is dropped, as every trigger during a busy copier was before. +reg pending; initial pending = 0; +reg [23:0] shadow_src, shadow_dst, shadow_len; +reg shadow_word, shadow_dir; +reg [4:0] shadow_op; +wire op_kick = reg_we_rising & enable & (reg_addr == 4'd9) & reg_data_in[0]; + +assign BUS_RRQ = BUS_RDY && (state == ST_READ); +assign BUS_WRQ = BUS_RDY && (state == ST_WRITE); +assign ROM_ADDR = (state == ST_READ) ? src_addr_r : dst_addr_r; +assign ROM_DATA_OUT = (opcode_r == OP_COPY) ? (dst_addr_r[0] ? {ROM_DATA_IN[7:0],ROM_DATA_IN[15:8]} : ROM_DATA_IN) + : (opcode_r == OP_RESET) ? 16'h0000 + : (opcode_r == OP_SET) ? 16'hFFFF + : (opcode_r == OP_DEBUG) ? {dst_addr_r[3:0],loop_enable,ROM_WORD_ENABLE,mod_r[1:0],dst_addr_r[3:0],loop_enable,ROM_WORD_ENABLE,mod_r[1:0]} + : 0; +assign ROM_WORD_ENABLE = word_mode_r; +assign loop_enable = loop_r; +wire [23:0] length_next = length_r - (word_mode_r ? 2 : 1); + +always @(posedge clkin) begin + if (reset) begin + loop_r <= 0; + state <= ST_IDLE; + trig_r <= 0; + pending <= 0; + end + else begin + trig_r <= TRIG; + + // when idle the fresh trigger is handled by the op_kick path below + if (op_kick && (state != ST_IDLE) && !pending) begin + pending <= 1'b1; + shadow_src <= SRC_ADDR; + shadow_dst <= DST_ADDR; + shadow_len <= LEN; + shadow_word <= WORD_MODE; + shadow_op <= reg_data_in[7:3]; // dma_r[9] being written this cycle + shadow_dir <= reg_data_in[1]; + end + + case (state) + ST_IDLE: begin + if (pending) begin + // start the op snapshotted while the previous one ran + src_addr_r <= shadow_src; + dst_addr_r <= shadow_dst; + length_r <= shadow_len; + mod_r <= shadow_word ? (shadow_dir ? -2 : 2) : (shadow_dir ? -1 : 1); + opcode_r <= shadow_op; + word_mode_r <= shadow_word; + dir_r <= shadow_dir; + loop_r <= 0; + pending <= 0; + if (shadow_op == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; + end + else if (op_kick) begin + // Single-op start, detected by the dma_r[9] write rather than a TRIG + // edge: robust to the bit already being 1, which happens when a colliding + // register write skips the ST_DONE clear. + src_addr_r <= SRC_ADDR; + dst_addr_r <= DST_ADDR; + length_r <= LEN; + mod_r <= WORD_MODE ? (reg_data_in[1] ? -2 : 2) : (reg_data_in[1] ? -1 : 1); + opcode_r <= reg_data_in[7:3]; + loop_r <= reg_data_in[2]; + dir_r <= reg_data_in[1]; + word_mode_r <= WORD_MODE; + + if (reg_data_in[7:3] == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; + end + end + ST_READ: begin + if (BUS_RDY) begin + src_addr_r <= src_addr_r + mod_r; + state <= ST_WRITE; + end + end + ST_WRITE: begin + if (BUS_RDY) begin + dst_addr_r <= dst_addr_r + mod_r; + length_r <= length_next; + + if (length_next == 0) state <= ST_DONE; + else if (opcode_r == OP_COPY) state <= ST_READ; + else state <= ST_WRITE; + end + end + ST_DONE: begin + loop_r <= 0; + state <= ST_IDLE; + end + endcase + end +end + +endmodule diff --git a/verilog/sd2snes_sa1/main.qsf b/verilog/sd2snes_sa1/main.qsf index 9c384158..abd4fe50 100644 --- a/verilog/sd2snes_sa1/main.qsf +++ b/verilog/sd2snes_sa1/main.qsf @@ -977,6 +977,8 @@ set_global_assignment -name VERILOG_FILE dac.v set_global_assignment -name VERILOG_FILE cheat.v set_global_assignment -name SDC_FILE main.sdc set_global_assignment -name VERILOG_FILE address.v +set_global_assignment -name VERILOG_FILE ctx.v +set_global_assignment -name VERILOG_FILE dma.v set_global_assignment -name QIP_FILE ip/mk3/sa1_div.qip set_global_assignment -name QIP_FILE ip/mk3/sa1_mult.qip set_global_assignment -name QIP_FILE ip/mk3/sa1_iram.qip diff --git a/verilog/sd2snes_sa1/main.v b/verilog/sd2snes_sa1/main.v index 3e217cc0..754dab85 100644 --- a/verilog/sd2snes_sa1/main.v +++ b/verilog/sd2snes_sa1/main.v @@ -92,6 +92,14 @@ module main( inout SD_CLK ); +// SA-1 savestate machinery gate; Mk.III only (see address.v). Derived in each +// file that needs it: address.v, sa1.v, cheat.v, mcu_cmd.v. +`ifdef MK3 +`define SA1_SS_ACTIVE +`elsif SA1_SS_MK2 +`define SA1_SS_ACTIVE +`endif + wire CLK2; wire dspx_dp_enable; @@ -142,6 +150,23 @@ wire feat_cmd_unlock = featurebits[5]; wire [23:0] MAPPED_SNES_ADDR; wire ROM_ADDR0; +// $2020-$202F copier reg window (address.v); declared before its consumers so XST +// does not build an implicit net. +wire dma_enable; + +// Hook identity window, from address.v. +wire IS_PATCH; + +// Savestate scan window (address.v); wins the SNES_DATA read mux. 0 on mk2. +wire sa1_ss_enable; +wire [7:0] SA1_SS_DATA_OUT; +wire [15:0] sa1_iram_pad; +// MCU-side halt request (mcu_cmd 0xfb). +wire sa1_ss_halt; + +// Hook-side SA-1 pause, written through the $202C decode next to the copier. +reg snapshot_pause; initial snapshot_pause = 0; + wire [13:0] DBG_msu_address; wire DBG_msu_reg_oe_rising; wire DBG_msu_reg_oe_falling; @@ -191,12 +216,13 @@ wire SNES_PULSE_IN = SNES_READ_IN & SNES_WRITE_IN & ~SNES_CPU_CLK_IN; wire SNES_PULSE_end = (SNES_PULSEr[6:1] == 6'b000001); wire SNES_PARD_start = (SNES_PARDr[6:1] == 6'b111110); -// wire SNES_PARD_end = (SNES_PARDr[6:1] == 6'b000001); +wire SNES_PARD_end = (SNES_PARDr[6:1] == 6'b000001); // Sample PAWR data earlier on CPU accesses, later on DMA accesses... wire SNES_PAWR_start = (SNES_PAWRr[6:1] == (({SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h02100) ? 6'b111000 : 6'b100000)); wire SNES_PAWR_end = (SNES_PAWRr[6:1] == 6'b000001); wire SNES_RD_start = (SNES_READr[6:1] == 6'b111110); wire SNES_RD_end = (SNES_READr[6:1] == 6'b000001); +wire SNES_WR_start = (SNES_WRITEr[6:1] == 6'b111000); wire SNES_WR_end = (SNES_WRITEr[6:1] == 6'b000001); wire SNES_cycle_start = (SNES_CPU_CLKr[6:1] == 6'b000001); wire SNES_cycle_end = (SNES_CPU_CLKr[6:1] == 6'b111110); @@ -212,6 +238,93 @@ wire SNES_PAWR = SNES_PAWRr[2] & SNES_PAWRr[1]; wire SNES_ROMSEL = (SNES_ROMSELr[0]); +`ifdef SA1_SS_ACTIVE +// --------------------------------------------------------------------------- +// ctx bus-snoop plumbing, ported from sd2snes_base; Mk.III only. The savestate +// handler reads the $21xx/$42xx register shadows out of the mirror (VRAM, CGRAM, +// OAM and WRAM it reads back from the hardware instead). +// +// The base samples the late, glitch-filtered SNES_ADDR/PA while this core runs off +// the early SNES_ADDRr[0], so the settled taps are rebuilt here: the 4-tick end +// strobes are calibrated against the late address. +wire [23:0] CTX_SNES_ADDR = (SNES_ADDRr[5] & SNES_ADDRr[4]); +wire [7:0] CTX_SNES_PA = (SNES_PAr[5] & SNES_PAr[4]); + +// early snoop-arm strobe (control lines only, address/PA independent) +wire SNES_PAWR_start_early = ((SNES_PAWRr[4:1] | SNES_PAWRr[5:2]) == 4'b1110); + +reg SNES_SNOOPWR_DATA_OE = 0; +reg SNES_SNOOPPAWR_DATA_OE = 0; +reg SNES_SNOOPPARD_DATA_OE = 0; +reg [3:0] SNES_SNOOPWR_count; +reg [3:0] SNES_SNOOPPAWR_count; +reg [3:0] SNES_SNOOPPARD_count; +reg SNES_SNOOPWR_end; +reg SNES_SNOOPPAWR_end; +reg SNES_SNOOPPARD_end; +reg [7:0] CTX_DINr; +reg CTX_DIRr; +wire [15:0] ctx_pad1; // JOY1 captured by ctx's CPUREG store-sniffing FSM +wire [7:0] CTX_SNES_DATA_IN = CTX_DIRr ? CTX_DINr : SNES_DATAr[0]; + +// 4-tick snoop end strobes, as in sd2snes_base. +always @(posedge CLK2) begin + if (SNES_reset_strobe) begin + SNES_SNOOPPARD_end <= 0; + SNES_SNOOPPAWR_end <= 0; + SNES_SNOOPWR_end <= 0; + end + else begin + SNES_SNOOPPARD_end <= SNES_SNOOPPARD_count == 4; + SNES_SNOOPPAWR_end <= SNES_SNOOPPAWR_count == 4; + SNES_SNOOPWR_end <= SNES_SNOOPWR_count == 4; + end +end + +// snoop low-strobe counters + data-OE (level-shifter enable during capture) +always @(posedge CLK2) begin + // PA write + if (SNES_reset_strobe | SNES_SNOOPPAWR_end) begin + SNES_SNOOPPAWR_count <= 0; + SNES_SNOOPPAWR_DATA_OE <= 0; + end + else if (SNES_PAWR_start_early) begin + SNES_SNOOPPAWR_count <= 1; + SNES_SNOOPPAWR_DATA_OE <= 1; + end + else if (|SNES_SNOOPPAWR_count) begin + SNES_SNOOPPAWR_count <= SNES_SNOOPPAWR_count + 1; + end + + // PA read -- prioritize writes when there is a DMA from a PPU reg; avoid + // r213f (region override) and the external B-bus (>=$2184, e.g. Satellaview) + if (SNES_reset_strobe | SNES_SNOOPPARD_end | ~SNES_WRITE) begin + SNES_SNOOPPARD_count <= 0; + SNES_SNOOPPARD_DATA_OE <= 0; + end + else if (SNES_PARD_start & ~r213f_enable & (CTX_SNES_PA < 8'h84)) begin + SNES_SNOOPPARD_count <= 1; + SNES_SNOOPPARD_DATA_OE <= 1; + end + else if (|SNES_SNOOPPARD_count) begin + SNES_SNOOPPARD_count <= SNES_SNOOPPARD_count + 1; + end + + // main-bus write + if (SNES_reset_strobe | SNES_SNOOPWR_end) begin + SNES_SNOOPWR_count <= 0; + SNES_SNOOPWR_DATA_OE <= 0; + end + else if (SNES_WR_start) begin + SNES_SNOOPWR_count <= 1; + SNES_SNOOPWR_DATA_OE <= 1; + end + else if (|SNES_SNOOPWR_count) begin + SNES_SNOOPWR_count <= SNES_SNOOPWR_count + 1; + end +end +`endif + reg [7:0] BUS_DATA; always @(posedge CLK2) begin @@ -266,17 +379,24 @@ always @(posedge CLK2) begin SNES_DATAr[0] <= SNES_DATA; end -parameter ST_IDLE = 11'b00000000001; -parameter ST_MCU_RD_ADDR = 11'b00000000010; -parameter ST_MCU_RD_END = 11'b00000000100; -parameter ST_MCU_WR_ADDR = 11'b00000001000; -parameter ST_MCU_WR_END = 11'b00000010000; -parameter ST_SA1_ROM_RD_ADDR = 11'b00000100000; -parameter ST_SA1_ROM_RD_END = 11'b00001000000; -parameter ST_SA1_RAM_RD_ADDR = 11'b00010000000; -parameter ST_SA1_RAM_RD_END = 11'b00100000000; -parameter ST_SA1_RAM_WR_ADDR = 11'b01000000000; -parameter ST_SA1_RAM_WR_END = 11'b10000000000; +// One-hot ROM-side arbiter state, widened 11 -> 13 bits for the two CTX write +// states; every original bit is in use, so they are appended, not shared. +parameter ST_IDLE = 13'b0000000000001; +parameter ST_MCU_RD_ADDR = 13'b0000000000010; +parameter ST_MCU_RD_END = 13'b0000000000100; +parameter ST_MCU_WR_ADDR = 13'b0000000001000; +parameter ST_MCU_WR_END = 13'b0000000010000; +parameter ST_SA1_ROM_RD_ADDR = 13'b0000000100000; +parameter ST_SA1_ROM_RD_END = 13'b0000001000000; +// The ST_SA1_RAM_* bits were dead here (SA-1 RAM uses the separate ST_RAM_* FSM), +// so they are reused for the copier. +parameter ST_DMA_RD_ADDR = 13'b0000010000000; +parameter ST_DMA_RD_END = 13'b0000100000000; +parameter ST_DMA_WR_ADDR = 13'b0001000000000; +parameter ST_DMA_WR_END = 13'b0010000000000; +// ctx (context/PPU-mirror) write port -- top priority; mk3 only. +parameter ST_CTX_WR_ADDR = 13'b0100000000000; +parameter ST_CTX_WR_END = 13'b1000000000000; `ifdef MK2 parameter SNES_DEAD_TIMEOUT = 17'd85714; // 1ms @@ -288,7 +408,7 @@ parameter SNES_DEAD_TIMEOUT = 17'd85867; // 1ms parameter ROM_CYCLE_LEN = 4'd6; `endif -reg [10:0] STATE; +reg [12:0] STATE; initial STATE = ST_IDLE; assign MSU_SNES_DATA_IN = BUS_DATA; @@ -446,7 +566,15 @@ sa1 snes_sa1 ( .IRQ(SA1_IRQ), .SPEED(dsp_feat[0]), - + // transparent FPGA-side halt for savestate snapshots (mk3; 0 on mk2) + .snapshot_pause(snapshot_pause), + + // Savestate scan window (mk3; tied off on mk2) + .ss_window_en(sa1_ss_enable), + .ss_halt(sa1_ss_halt), + .ss_dout(SA1_SS_DATA_OUT), + .ss_iram_pad(sa1_iram_pad), + // State debug read interface .PGM_ADDR(SA1_PGM_ADDR), // [11:0] .PGM_DATA(SA1_PGM_DATA), // [7:0] @@ -463,6 +591,54 @@ sa1 snes_sa1 ( .DBG(DBG_SA1) ); +`ifdef SA1_SS_ACTIVE +// -------------------------------------------------------------------------- +// ctx (context / PPU-mirror) engine -- Mk.III only. Instantiated exactly as in +// sd2snes_base, wired to the ported snoop signals + the settled SNES taps. +// Produces CTX write requests (CTX_WRQ/CTX_ADDR/CTX_DOUT/CTX_WORD) granted by +// the ROM arbiter, and the ctx_*_enable data-OE terms for the databus logic. +// -------------------------------------------------------------------------- +wire [23:0] CTX_ADDR; +wire [15:0] CTX_DOUT; +wire CTX_WORD; +wire CTX_WRQ; +wire CTX_RDY; +wire CTX_DBG; +wire ctx_wr_enable; +wire ctx_pawr_enable; +wire ctx_pard_enable; + +ctx snes_ctx ( + .clkin(CLK2), + .reset(SNES_reset_strobe), + + .SNES_ADDR(CTX_SNES_ADDR), + .SNES_PA(CTX_SNES_PA), + .SNES_RD_end_PRE(SNES_RD_end), + .SNES_WR_end_PRE(SNES_SNOOPWR_end), + .SNES_PARD_end_PRE(SNES_SNOOPPARD_end), + .SNES_PAWR_end_PRE(SNES_SNOOPPAWR_end), + .SNES_DATA_IN_PRE(CTX_SNES_DATA_IN), // needs to handle PA accesses, too + + //.OE_RD_ENABLE(ctx_rd_enable), + .OE_WR_ENABLE(ctx_wr_enable), + .OE_PAWR_ENABLE(ctx_pawr_enable), + .OE_PARD_ENABLE(ctx_pard_enable), + + .BUS_WRQ(CTX_WRQ), + .BUS_RDY(CTX_RDY), + + .snescmd_unlock(snescmd_unlock), + + .ROM_ADDR(CTX_ADDR), + .ROM_DATA(CTX_DOUT), + .ROM_WORD_ENABLE(CTX_WORD), + + .DBG(CTX_DBG), + .PAD1_OUT(ctx_pad1) +); +`endif + reg [7:0] MCU_DINr; reg [7:0] MCU_ROM_DINr; reg [7:0] MCU_RAM_DINr; @@ -471,6 +647,35 @@ wire [31:0] cheat_pgm_data; wire [7:0] cheat_data_out; wire [2:0] cheat_pgm_idx; +// ---- SNES-side copier ($2020-$202F) ------------------------------------------- +// Ported from sd2snes_base: the savestate handler programs the copier over the +// bus (inside its hook window) to stage the WRAM/APU-RAM mirrors into the state +// buffer. The copier requests PSRAM via BUS_RRQ/WRQ; the ROM-side STATE machine +// grants it cycles through the ST_DMA_* states (which reuse the dead SA1_RAM +// one-hot bits), exactly like the MCU/SA1 requesters. +`ifdef SA1_SS_ACTIVE +wire [7:0] DMA_SNES_DATA_IN; +wire [7:0] DMA_SNES_DATA_OUT; +assign DMA_SNES_DATA_IN = BUS_DATA; +`endif +wire [23:0] DMA_ADDR; +wire [15:0] DMA_DOUT; +wire DMA_WORD; +reg [15:0] DMA_DINr; +wire DMA_RRQ; +wire DMA_WRQ; +reg DMA_RD_PENDr = 0; +reg DMA_WR_PENDr = 0; +reg [23:0] DMA_ROM_ADDRr; +reg [15:0] DMA_ROM_DATAr; +reg DMA_ROM_WORDr; +reg RQ_DMA_RDYr = 1'b1; +wire DMA_RDY = RQ_DMA_RDYr; +wire DMA_WE_HIT = |(STATE & ST_DMA_WR_ADDR); +wire DMA_WR_HIT = |(STATE & (ST_DMA_WR_ADDR | ST_DMA_WR_END)); +wire DMA_RD_HIT = |(STATE & (ST_DMA_RD_ADDR | ST_DMA_RD_END)); +wire DMA_HIT = DMA_WR_HIT | DMA_RD_HIT; + mcu_cmd snes_mcu_cmd( .clk(CLK2), .snes_sysclk(SNES_SYSCLK), @@ -533,6 +738,7 @@ mcu_cmd snes_mcu_cmd( .mcu_wrq(MCU_WRQ), .mcu_rq_rdy(MCU_RDY), .region_out(mcu_region), + .sa1_ss_halt_out(sa1_ss_halt), .snescmd_addr_out(snescmd_addr_mcu), .snescmd_we_out(snescmd_we_mcu), .snescmd_data_out(snescmd_data_out_mcu), @@ -543,6 +749,44 @@ mcu_cmd snes_mcu_cmd( .dsp_feat_out(dsp_feat) ); +`ifdef SA1_SS_ACTIVE +dma snes_dma ( + .clkin(CLK2), + .reset(SNES_reset_strobe), + // Driven by the live SNES bus when it writes/reads $2020-$202F (dma_enable) -- + // used by the in-game savestate snapshot. + .enable(dma_enable), + .reg_addr(SNES_ADDR[3:0]), + .reg_data_in(DMA_SNES_DATA_IN), + .reg_data_out(DMA_SNES_DATA_OUT), + .reg_oe_falling(SNES_RD_start), + .reg_we_rising(SNES_WR_end), + .loop_enable(), + .BUS_RDY(DMA_RDY), + .BUS_RRQ(DMA_RRQ), + .BUS_WRQ(DMA_WRQ), + .ROM_ADDR(DMA_ADDR), + .ROM_DATA_OUT(DMA_DOUT), + .ROM_DATA_IN(DMA_DINr), + .ROM_WORD_ENABLE(DMA_WORD) +); +`endif + +// Hook-side SA-1 pause register. $202C (SNES_ADDR[3:0]==C) sits in the same +// 16-address dma_enable window; bit0 of the write sets/clears a TRANSPARENT halt +// (CCNT_r untouched, so the SA-1 resumes exactly where it stopped), cleared on +// SNES reset so a reset can never leave it stuck paused. The savestate handler +// does not use it -- it freezes the SA-1 at an instruction boundary through the +// $E8 window instead -- but it is the cheap way for a hook to stop the +// coprocessor for a moment. dma_enable is 0 outside the savestate build, so the +// whole decode folds away there. +always @(posedge CLK2) begin + if (SNES_reset_strobe) + snapshot_pause <= 1'b0; + else if (dma_enable & SNES_WR_end & (SNES_ADDR[3:0] == 4'hC)) + snapshot_pause <= SNES_DATA_IN[0]; +end + address snes_addr( .CLK(CLK2), .MAPPER(MAPPER), @@ -555,10 +799,15 @@ address snes_addr( .IS_SAVERAM(IS_SAVERAM), .IS_ROM(IS_ROM), .IS_WRITABLE(IS_WRITABLE), + .IS_PATCH(IS_PATCH), + .sa1_ss_enable(sa1_ss_enable), .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), + .snescmd_unlock(snescmd_unlock), //MSU-1 .msu_enable(msu_enable), + //DMA-1 / SNES-side $2020 copier (mk3 snapshot) + .dma_enable(dma_enable), // sa1 .sa1_bmaps_sbm(SA1_BMAPS_SBM), .sa1_dma_cc1_en(SA1_DMA_CC1_EN), @@ -579,6 +828,11 @@ reg [4:0] pad_cnt = 0; reg snes_ajr = 0; +// ss_combo is declared HERE (before its use in the instance below) and assigned +// further down. XST (mk2) otherwise makes the port connection an implicit net and +// rejects the later declaration as an illegal redeclaration (Quartus tolerates it). +wire ss_combo; + cheat snes_cheat( .clk(CLK2), .SNES_ADDR(SNES_ADDR), @@ -595,6 +849,7 @@ cheat snes_cheat( .branch3_enable(branch3_enable), .pad_latch(pad_latch), .snes_ajr(snes_ajr), + .ss_combo(ss_combo), .SNES_cycle_start(SNES_cycle_start), .pgm_idx(cheat_pgm_idx), .pgm_we(cheat_pgm_we), @@ -631,6 +886,33 @@ wire r2100_enable = r2100_hit & (r2100_patch | ~(&r2100_limit)); wire snoop_4200_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04200; wire r4016_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04016; +// Savestate gesture detect, used by cheat.v to arm the IRQ hook only while the +// user is actually holding one (see the rate-limit/gesture-gate note there). +// Reads of $4218/$4219 pulse address+/RD on the cart bus but the DATA of a +// CPU-INTERNAL register read is not driven externally, so a plain read snoop +// captures garbage (verified in hardware). ctx.v's CPUREG FSM solves this the +// proven way: it watches the instruction stream for the game's LDA $4218(/19) -> +// STA idiom and captures the value from the STORE (writes ARE bus-visible), with +// stale-value expiry, and exports it as PAD1_OUT. +// +// The gestures are the savestate DEFAULT inputs: Start+R (save), Start+L (load) +// and Select+d-pad (slot load). Custom inputs from savestate_inputs.yml are not +// known to the FPGA, so those only trigger in NMI-driven scenes. +// +// Two pad sources, matched independently: the ctx $4218 store-sniff (scene +// dependent -- some Super Mario RPG areas never use the LDA/STA idiom) and the +// IRAM $3010 pad-forwarding snoop from sa1.v (which every SMRPG scene does). +`ifdef SA1_SS_ACTIVE +assign ss_combo = ((ctx_pad1 & 16'h1010) == 16'h1010) + | ((ctx_pad1 & 16'h1020) == 16'h1020) + | (ctx_pad1[13] & |ctx_pad1[11:8]) + | ((sa1_iram_pad & 16'h1010) == 16'h1010) + | ((sa1_iram_pad & 16'h1020) == 16'h1020) + | (sa1_iram_pad[13] & |sa1_iram_pad[11:8]); +`else +assign ss_combo = 1'b0; +`endif + always @(posedge CLK2) begin r2100_forcewrite <= r2100_forcewrite_pre; end @@ -665,6 +947,16 @@ assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr :((~SNES_READ ^ (r213f_forceread & r213f_enable & ~SNES_PARD)) & ~(r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE & ~sa1_data_enable)) ? ( msu_enable ? MSU_SNES_DATA_OUT +`ifdef SA1_SS_ACTIVE + // SA-1 savestate scan window ($E8:0000-07FF): must + // win over the IS_PATCH/PSRAM fall-through and the + // cheat arm below (address.v aliases $E8 under + // unlock). Registered byte from sa1.v. + : sa1_ss_enable ? SA1_SS_DATA_OUT +`endif +`ifdef SA1_SS_ACTIVE + : dma_enable ? DMA_SNES_DATA_OUT // $2020 copier read-back +`endif : sa1_data_enable ? SA1_SNES_DATA_OUT // SA1 MMIO read : (cheat_hit & ~feat_cmd_unlock) ? cheat_data_out : ((snescmd_unlock | feat_cmd_unlock) & snescmd_enable) ? snescmd_dout @@ -702,6 +994,21 @@ assign SA1_ROM_RDY = RQ_SA1_ROM_RDYr; wire SA1_ROM_RD_HIT = |(STATE & ST_SA1_ROM_RD_ADDR); wire SA1_ROM_HIT = SA1_ROM_RD_HIT; +// CTX (context/PPU-mirror) write port -- top-priority ROM requester (mk3). +// Declared unconditionally because the ROM muxes reference them; on mk2 CTX_HIT +// is constant 0 (STATE never enters ST_CTX_*) and the terms fold away. +reg [23:0] CTX_ROM_ADDRr; initial CTX_ROM_ADDRr = 24'h0; +reg [15:0] CTX_ROM_DATAr; initial CTX_ROM_DATAr = 16'h0000; +reg CTX_ROM_WORDr; initial CTX_ROM_WORDr = 1'b0; +wire CTX_WE_HIT = |(STATE & ST_CTX_WR_ADDR); +wire CTX_WR_HIT = |(STATE & (ST_CTX_WR_ADDR | ST_CTX_WR_END)); +wire CTX_HIT = CTX_WR_HIT; +`ifdef SA1_SS_ACTIVE +reg CTX_WR_PENDr; initial CTX_WR_PENDr = 0; +reg RQ_CTX_RDYr; initial RQ_CTX_RDYr = 1'b1; +assign CTX_RDY = RQ_CTX_RDYr; +`endif + `ifdef MK2 my_dcm snes_dcm( .CLKIN(CLKIN), @@ -710,8 +1017,8 @@ my_dcm snes_dcm( .RST(DCM_RST) ); -assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:1] : SA1_ROM_HIT ? SA1_ROM_ADDRr[23:1] : MCU_HIT ? ROM_ADDRr[23:1] : MAPPED_SNES_ADDR[23:1]; -assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : SA1_ROM_HIT ? SA1_ROM_ADDRr[0] : MCU_HIT ? ROM_ADDRr[0] : MAPPED_SNES_ADDR[0]; +assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:1] : CTX_HIT ? CTX_ROM_ADDRr[23:1] : SA1_ROM_HIT ? SA1_ROM_ADDRr[23:1] : DMA_HIT ? DMA_ROM_ADDRr[23:1] : MCU_HIT ? ROM_ADDRr[23:1] : MAPPED_SNES_ADDR[23:1]; +assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : CTX_HIT ? CTX_ROM_ADDRr[0] : SA1_ROM_HIT ? SA1_ROM_ADDRr[0] : DMA_HIT ? DMA_ROM_ADDRr[0] : MCU_HIT ? ROM_ADDRr[0] : MAPPED_SNES_ADDR[0]; assign ROM_CE = 1'b0; @@ -740,9 +1047,9 @@ pll snes_pll( ); wire ROM_ADDR22; -assign ROM_ADDR22 = (SD_DMA_TO_ROM) ? MCU_ADDR[1] : SA1_ROM_HIT ? SA1_ROM_ADDRr[1] : MCU_HIT ? ROM_ADDRr[1] : MAPPED_SNES_ADDR[1]; -assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:2] : SA1_ROM_HIT ? SA1_ROM_ADDRr[23:2] : MCU_HIT ? ROM_ADDRr[23:2] : MAPPED_SNES_ADDR[23:2]; -assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : SA1_ROM_HIT ? SA1_ROM_ADDRr[0] : MCU_HIT ? ROM_ADDRr[0] : MAPPED_SNES_ADDR[0]; +assign ROM_ADDR22 = (SD_DMA_TO_ROM) ? MCU_ADDR[1] : CTX_HIT ? CTX_ROM_ADDRr[1] : SA1_ROM_HIT ? SA1_ROM_ADDRr[1] : DMA_HIT ? DMA_ROM_ADDRr[1] : MCU_HIT ? ROM_ADDRr[1] : MAPPED_SNES_ADDR[1]; +assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:2] : CTX_HIT ? CTX_ROM_ADDRr[23:2] : SA1_ROM_HIT ? SA1_ROM_ADDRr[23:2] : DMA_HIT ? DMA_ROM_ADDRr[23:2] : MCU_HIT ? ROM_ADDRr[23:2] : MAPPED_SNES_ADDR[23:2]; +assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : CTX_HIT ? CTX_ROM_ADDRr[0] : SA1_ROM_HIT ? SA1_ROM_ADDRr[0] : DMA_HIT ? DMA_ROM_ADDRr[0] : MCU_HIT ? ROM_ADDRr[0] : MAPPED_SNES_ADDR[0]; assign ROM_ZZ = 1'b1; assign ROM_1CE = ROM_ADDR22; @@ -811,6 +1118,54 @@ always @(posedge CLK2) begin end end +`ifdef SA1_SS_ACTIVE +// Copier r/w request -- latch the requested PSRAM access +always @(posedge CLK2) begin + if(DMA_RRQ) begin + DMA_RD_PENDr <= 1'b1; + RQ_DMA_RDYr <= 1'b0; + DMA_ROM_ADDRr <= DMA_ADDR; + DMA_ROM_WORDr <= DMA_WORD; + end else if(DMA_WRQ) begin + DMA_WR_PENDr <= 1'b1; + RQ_DMA_RDYr <= 1'b0; + DMA_ROM_ADDRr <= DMA_ADDR; + DMA_ROM_DATAr <= DMA_DOUT; + DMA_ROM_WORDr <= DMA_WORD; + end else if(STATE & (ST_DMA_RD_END | ST_DMA_WR_END)) begin + DMA_RD_PENDr <= 1'b0; + DMA_WR_PENDr <= 1'b0; + RQ_DMA_RDYr <= 1'b1; + end +end + +// ctx (context/PPU-mirror) write request -- latch the requested PSRAM write. +always @(posedge CLK2) begin + if(CTX_WRQ) begin + CTX_WR_PENDr <= 1'b1; + RQ_CTX_RDYr <= 1'b0; + CTX_ROM_ADDRr <= CTX_ADDR; + CTX_ROM_DATAr <= CTX_DOUT; + CTX_ROM_WORDr <= CTX_WORD; + end + else if(STATE & ST_CTX_WR_END) begin + CTX_WR_PENDr <= 1'b0; + RQ_CTX_RDYr <= 1'b1; + end +end + +// ctx read-back: the byte the FPGA is serving plus the bus direction. This core +// serves reads from three sources (SA-1 IRAM/MMIO, BW-RAM and PSRAM), and a DMA +// out of IRAM or BW-RAM is snooped with the FPGA driving, so capturing ROM_DATA +// alone would leave holes in the mirror. Mirror the SNES_DATA serve mux. +always @(posedge CLK2) begin + CTX_DINr <= sa1_data_enable ? SA1_SNES_DATA_OUT + : (ROM_HIT & IS_SAVERAM) ? RAM_DATA + : (ROM_ADDR0 ? ROM_DATA[7:0] : ROM_DATA[15:8]); + CTX_DIRr <= SNES_DATABUS_DIR; +end +`endif + always @(posedge CLK2) begin if(~SNES_CPU_CLKr[1]) SNES_DEAD_CNTr <= SNES_DEAD_CNTr + 1; else SNES_DEAD_CNTr <= 17'h0; @@ -833,6 +1188,14 @@ always @(posedge CLK2) begin STATE <= ST_IDLE; if(free_slot | SNES_DEADr) begin +`ifdef SA1_SS_ACTIVE + // ctx write, top priority; the trailing `end else` chains into the SA1 + // `if` below on mk3 and vanishes on mk2. + if (CTX_WR_PENDr) begin + STATE <= ST_CTX_WR_ADDR; + ST_MEM_DELAYr <= ROM_CYCLE_LEN; + end else +`endif // early notify from SA1 to save a clock if (SA1_ROM_RD_PENDr | SA1_ROM_RRQ) begin STATE <= ST_SA1_ROM_RD_ADDR; @@ -846,6 +1209,15 @@ always @(posedge CLK2) begin STATE <= ST_MCU_WR_ADDR; ST_MEM_DELAYr <= ROM_CYCLE_LEN; end +`ifdef SA1_SS_ACTIVE + else if(DMA_RD_PENDr) begin + STATE <= ST_DMA_RD_ADDR; + ST_MEM_DELAYr <= ROM_CYCLE_LEN; + end else if(DMA_WR_PENDr) begin + STATE <= ST_DMA_WR_ADDR; + ST_MEM_DELAYr <= ROM_CYCLE_LEN; + end +`endif end end ST_MCU_RD_ADDR: begin @@ -865,7 +1237,25 @@ always @(posedge CLK2) begin if(ST_MEM_DELAYr == 0) STATE <= ST_SA1_ROM_RD_END; SA1_ROM_DINr <= (ROM_ADDR0_r ? ROM_DATA[15:0] : {ROM_DATA[7:0],ROM_DATA[15:8]}); end - ST_MCU_RD_END, ST_MCU_WR_END, ST_SA1_ROM_RD_END: begin +`ifdef SA1_SS_ACTIVE + ST_DMA_RD_ADDR: begin + STATE <= ST_DMA_RD_ADDR; + ST_MEM_DELAYr <= ST_MEM_DELAYr - 1; + if(ST_MEM_DELAYr == 0) STATE <= ST_DMA_RD_END; + DMA_DINr <= (ROM_ADDR0 ? ROM_DATA[15:0] : {ROM_DATA[7:0],ROM_DATA[15:8]}); + end + ST_DMA_WR_ADDR: begin + STATE <= ST_DMA_WR_ADDR; + ST_MEM_DELAYr <= ST_MEM_DELAYr - 1; + if(ST_MEM_DELAYr == 0) STATE <= ST_DMA_WR_END; + end + ST_CTX_WR_ADDR: begin + STATE <= ST_CTX_WR_ADDR; + ST_MEM_DELAYr <= ST_MEM_DELAYr - 1; + if(ST_MEM_DELAYr == 0) STATE <= ST_CTX_WR_END; + end +`endif + ST_MCU_RD_END, ST_MCU_WR_END, ST_SA1_ROM_RD_END, ST_DMA_RD_END, ST_DMA_WR_END, ST_CTX_WR_END: begin STATE <= ST_IDLE; end endcase @@ -942,11 +1332,15 @@ reg MCU_WRITE_1; always @(posedge CLK2) MCU_WRITE_1<= MCU_WRITE; // odd addresses xxx1 -assign ROM_DATA[7:0] = ROM_ADDR0 +assign ROM_DATA[7:0] = (ROM_ADDR0 || (!SD_DMA_TO_ROM && CTX_HIT && CTX_ROM_WORDr) || (!SD_DMA_TO_ROM && DMA_HIT && DMA_ROM_WORDr)) ?(SD_DMA_TO_ROM ? (!MCU_WRITE_1 ? MCU_DOUT : 8'bZ) -// : (ROM_HIT -// & ~IS_SAVERAM -// & ~SNES_WRITE) ? SNES_DATA + : CTX_WR_HIT ? CTX_ROM_DATAr[15:8] // ctx snapshot write (mk3) + : DMA_WR_HIT ? DMA_ROM_DATAr[15:8] + // The handler writes its scratches through the ROM bus, so the + // SNES->PSRAM drive is kept for IS_PATCH writes only; stray game + // writes to ROM stay high-Z. + : (IS_PATCH + & ~SNES_WRITE) ? SNES_DATA : MCU_WR_HIT ? MCU_DOUT : 8'bZ ) :8'bZ; @@ -955,24 +1349,28 @@ assign ROM_DATA[7:0] = ROM_ADDR0 assign ROM_DATA[15:8] = ROM_ADDR0 ? 8'bZ :(SD_DMA_TO_ROM ? (!MCU_WRITE_1 ? MCU_DOUT : 8'bZ) -// : (ROM_HIT -// & ~IS_SAVERAM -// & ~SNES_WRITE) ? SNES_DATA + : CTX_WR_HIT ? CTX_ROM_DATAr[7:0] // ctx snapshot write (mk3) + : DMA_WR_HIT ? DMA_ROM_DATAr[7:0] + // See the IS_PATCH note on the odd-byte mux above. + : (IS_PATCH + & ~SNES_WRITE) ? SNES_DATA : MCU_WR_HIT ? MCU_DOUT : 8'bZ ); assign ROM_WE = SD_DMA_TO_ROM ?MCU_WRITE + : CTX_WE_HIT ? 1'b0 // ctx snapshot write (mk3) + : DMA_WE_HIT ? 1'b0 // MCU-driven copier write : (ROM_HIT & IS_WRITABLE & ~IS_SAVERAM & SNES_CPU_CLK) ? SNES_WRITE : MCU_WE_HIT ? 1'b0 : 1'b1; -// force word enable for SA1 -assign ROM_BHE = ROM_ADDR0 && !(!SD_DMA_TO_ROM && SA1_ROM_HIT && SA1_ROM_WORDr); -assign ROM_BLE = !ROM_ADDR0 && !(!SD_DMA_TO_ROM && SA1_ROM_HIT && SA1_ROM_WORDr); +// force word enable for SA1, the MCU-driven copier and ctx (word mode) +assign ROM_BHE = ROM_ADDR0 && !(!SD_DMA_TO_ROM && CTX_HIT && CTX_ROM_WORDr) && !(!SD_DMA_TO_ROM && SA1_ROM_HIT && SA1_ROM_WORDr) && !(!SD_DMA_TO_ROM && DMA_HIT && DMA_ROM_WORDr); +assign ROM_BLE = !ROM_ADDR0 && !(!SD_DMA_TO_ROM && CTX_HIT && CTX_ROM_WORDr) && !(!SD_DMA_TO_ROM && SA1_ROM_HIT && SA1_ROM_WORDr) && !(!SD_DMA_TO_ROM && DMA_HIT && DMA_ROM_WORDr); //-------------- // RAM Pipeline @@ -1148,11 +1546,22 @@ assign MCU_RDY = RQ_MCU_RDYr & RQ_RAM_MCU_RDYr; //-------------- assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : +`ifdef SA1_SS_ACTIVE + sa1_ss_enable ? 1'b0 : // savestate window read/write +`endif sa1_data_enable ? 1'b0 : // accounts for read/write snescmd_enable & ~(SNES_READ_narrow & SNES_WRITE) ? ~(snescmd_unlock | feat_cmd_unlock) : (r213f_enable & ~SNES_PARD) ? 1'b0 : (r2100_enable & ~SNES_PAWR) ? 1'b0 : snoop_4200_enable & ~SNES_WRITE ? 1'b0 : +`ifdef SA1_SS_ACTIVE + // enable the level shifter for the $2020 copier + // (read-back) and while the ctx engine captures a snooped access. + dma_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : + (ctx_wr_enable & SNES_SNOOPWR_DATA_OE) ? 1'b0 : + (ctx_pawr_enable & SNES_SNOOPPAWR_DATA_OE) ? 1'b0 : + (ctx_pard_enable & SNES_SNOOPPARD_DATA_OE) ? 1'b0 : +`endif ( (IS_ROM & SNES_ROMSEL) | (!IS_ROM & !IS_SAVERAM & !IS_WRITABLE) | (SNES_READ_narrow & SNES_WRITE) @@ -1163,11 +1572,27 @@ assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : * a) the SNES wants to read * b) we want to force a value on the bus */ +`ifdef SA1_SS_ACTIVE +// Keep the bus SNES -> FPGA while the ctx engine snoops a PA access, so the FPGA +// captures the byte instead of fighting the PPU/APU for it. The served-source +// terms (ROM_HIT, sa1_data_enable) are what makes that safe: a concurrent A-bus +// read on DMA/HDMA must not flip the shifter to drive unless the FPGA is the one +// serving that source, or the DMA reads float. The assign head is duplicated per +// platform so the mk2 output stays identical to the original. +assign SNES_DATABUS_DIR = ((~SNES_READ & ((~SNES_SNOOPPAWR_DATA_OE & ~SNES_SNOOPPARD_DATA_OE) | ROM_HIT | sa1_data_enable + | sa1_ss_enable // savestate window read -> drive FPGA->SNES; write falls through (DIR=0) + )) | (~SNES_PARD & (r213f_enable))) + ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) + ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE & ~sa1_data_enable)) + : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite + : 1'b0); +`else assign SNES_DATABUS_DIR = (~SNES_READ | (~SNES_PARD & (r213f_enable))) ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE & ~sa1_data_enable)) : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite : 1'b0); +`endif assign SNES_IRQ = SA1_IRQ; diff --git a/verilog/sd2snes_sa1/mcu_cmd.v b/verilog/sd2snes_sa1/mcu_cmd.v index 0b041e39..00d27081 100644 --- a/verilog/sd2snes_sa1/mcu_cmd.v +++ b/verilog/sd2snes_sa1/mcu_cmd.v @@ -18,6 +18,14 @@ // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// + +// SA-1 savestate machinery gate -- see the note in address.v. +`ifdef MK3 +`define SA1_SS_ACTIVE +`elsif SA1_SS_MK2 +`define SA1_SS_ACTIVE +`endif + module mcu_cmd( input clk, input cmd_ready, @@ -91,6 +99,10 @@ module mcu_cmd( output reg [15:0] featurebits_out, output reg region_out, + + // savestate halt (MCU-side debug handle; the SNES side uses the $E8:07FF + // scan-window control byte). Never written on mk2, so it folds away there. + output reg sa1_ss_halt_out = 1'b0, // SNES sync/clk input snes_sysclk, @@ -345,6 +357,10 @@ always @(posedge clk) begin endcase 8'hee: region_out <= param_data[0]; +`ifdef SA1_SS_ACTIVE + 8'hfb: // control SA-1 savestate halt (MCU debug; SNES side uses $E8:07FF) + sa1_ss_halt_out <= param_data[0]; +`endif `ifdef DEBUG 8'hfa: // handles all group, index, value, invmask writes. unit is responsible for decoding group for match case (spi_byte_cnt) diff --git a/verilog/sd2snes_sa1/sa1.v b/verilog/sd2snes_sa1/sa1.v index f5476d0a..959d679a 100644 --- a/verilog/sd2snes_sa1/sa1.v +++ b/verilog/sd2snes_sa1/sa1.v @@ -19,6 +19,14 @@ // ////////////////////////////////////////////////////////////////////////////////// +// SA-1 savestate machinery gate; Mk.III only (see address.v). Derived per file: +// the repo uses no include files. +`ifdef MK3 +`define SA1_SS_ACTIVE +`elsif SA1_SS_MK2 +`define SA1_SS_ACTIVE +`endif + module sa1( input RST, input CLK, @@ -76,10 +84,21 @@ module sa1( input SPEED, + // Transparent halt: the CPU stops advancing, CCNT_r and the registers are kept, + // so it resumes where it paused when the bit clears. 0 on mk2. + input snapshot_pause, + // State debug read interface input [11:0] PGM_ADDR, // [11:0] output [7:0] PGM_DATA, // [7:0] + // Savestate scan window (mk3; tied off on mk2). ss_window_en from address.v, + // ss_halt from mcu_cmd 0xfb, ss_dout served to SNES_DATA. + input ss_window_en, + input ss_halt, + output [7:0] ss_dout, + output [15:0] ss_iram_pad, // pad word snooped from the game's IRAM $3010 forwarding (mk3) + // config interface input [7:0] reg_group_in, input [7:0] reg_index_in, @@ -150,6 +169,24 @@ module sa1( integer i; wire pipeline_advance; +`ifdef SA1_SS_MK2 +// XST rejects the data-dependent loop bound below (Xst:2634). Same algorithm with +// a static bound: 32 iterations guarded by the original condition, so the result is +// identical. Quartus accepts the original, kept in the `else arm. +function integer clog2; + input integer value; + integer i; + begin + value = value-1; + clog2 = 0; + for (i=0; i<32; i=i+1) + if (value>0) begin + clog2 = clog2+1; + value = value>>1; + end + end +endfunction +`else function integer clog2; input integer value; begin @@ -158,6 +195,7 @@ function integer clog2; value = value>>1; end endfunction +`endif `define BCD_A_CARRY(m,c,s) (~m & (c | (s[3] & (s[2] | s[1])))) // add 6 `define BCD_S_CARRY(m,c,s) (m & ~c) // sub 6 @@ -608,6 +646,36 @@ always @(*) begin end assign xxb_en = {FXB_r[`FXB_FBMODE], EXB_r[`EXB_EBMODE], DXB_r[`DXB_DBMODE], CXB_r[`CXB_CBMODE]}; +// Savestate freeze control, declared early so the guards below bind; the FSM that +// drives them is in the SAVESTATE SCAN WINDOW section at the end of the file. +// Constant 0 on mk2, so every guard folds back to the original logic. +`ifdef SA1_SS_ACTIVE +reg ss_halt_snes; initial ss_halt_snes = 1'b0; +reg ss_frozen; initial ss_frozen = 1'b0; +reg [19:0] ss_wait; initial ss_wait = 20'h0; // ~12 ms saturating timeout +reg ss_window_en_r; initial ss_window_en_r = 1'b0; +reg [7:0] ss_dout_r; initial ss_dout_r = 8'h00; + +wire ss_halt_req_eff = ss_halt | ss_halt_snes; +// Window write strobe, aligned with addr_in_r/data_in_r like the MMIO write path. +wire ss_win_wr = ss_window_en_r & SNES_WR_end; +wire [10:0] ss_off = addr_in_r[10:0]; +wire [7:0] ss_wr_data = data_in_r; +// State-block restore write ($000-$0FF); $7FF (control) is handled by the halt flop. +wire ss_block_wr = ss_frozen & ss_win_wr & (ss_off[10:8] == 3'b000); +// A restored WAI parks in IDLE until the interrupt block clears WAI_r. +wire ss_idle_wai_park = WAI_r; +assign ss_dout = ss_dout_r; +`else +wire ss_frozen = 1'b0; +wire ss_halt_req_eff = 1'b0; +wire ss_idle_wai_park = 1'b0; +wire ss_block_wr = 1'b0; +wire [10:0] ss_off = 11'h000; +wire [7:0] ss_wr_data = 8'h00; +assign ss_dout = 8'h00; +`endif + //------------------------------------------------------------------- // PIPELINE IO //------------------------------------------------------------------- @@ -934,6 +1002,27 @@ always @(posedge CLK) begin ADDR_DTC : DTC_r[7:0] <= snes_writebuf_data_r; // 8'h38, // $2 ADDR_DTC+1 : DTC_r[15:8] <= snes_writebuf_data_r; // 8'h38, // $2 ADDR_BBF : BBF_r[`BBF_BBF] <= snes_writebuf_data_r[`BBF_BBF]; // 8'h3F, +`ifdef SA1_SS_MK2 + // Same assignment as the `else arm, minus the part-select: mixing that form + // with the whole-element writes of the savestate restore below makes XST + // split the memory and call every bit multi-driven (Xst:528). + ADDR_BRF+0 : BRF_r[0] <= snes_writebuf_data_r; // 8'h40, + ADDR_BRF+1 : BRF_r[1] <= snes_writebuf_data_r; // 8'h41, + ADDR_BRF+2 : BRF_r[2] <= snes_writebuf_data_r; // 8'h42, + ADDR_BRF+3 : BRF_r[3] <= snes_writebuf_data_r; // 8'h43, + ADDR_BRF+4 : BRF_r[4] <= snes_writebuf_data_r; // 8'h44, + ADDR_BRF+5 : BRF_r[5] <= snes_writebuf_data_r; // 8'h45, + ADDR_BRF+6 : BRF_r[6] <= snes_writebuf_data_r; // 8'h46, + ADDR_BRF+7 : BRF_r[7] <= snes_writebuf_data_r; // 8'h47, + ADDR_BRF+8 : BRF_r[8] <= snes_writebuf_data_r; // 8'h48, + ADDR_BRF+9 : BRF_r[9] <= snes_writebuf_data_r; // 8'h49, + ADDR_BRF+10: BRF_r[10] <= snes_writebuf_data_r; // 8'h4A, + ADDR_BRF+11: BRF_r[11] <= snes_writebuf_data_r; // 8'h4B, + ADDR_BRF+12: BRF_r[12] <= snes_writebuf_data_r; // 8'h4C, + ADDR_BRF+13: BRF_r[13] <= snes_writebuf_data_r; // 8'h4D, + ADDR_BRF+14: BRF_r[14] <= snes_writebuf_data_r; // 8'h4E, + ADDR_BRF+15: BRF_r[15] <= snes_writebuf_data_r; // 8'h4F, +`else ADDR_BRF+0 : BRF_r[0][7:0] <= snes_writebuf_data_r; // 8'h40, ADDR_BRF+1 : BRF_r[1][7:0] <= snes_writebuf_data_r; // 8'h41, ADDR_BRF+2 : BRF_r[2][7:0] <= snes_writebuf_data_r; // 8'h42, @@ -950,6 +1039,7 @@ always @(posedge CLK) begin ADDR_BRF+13: BRF_r[13][7:0] <= snes_writebuf_data_r; // 8'h4D, ADDR_BRF+14: BRF_r[14][7:0] <= snes_writebuf_data_r; // 8'h4E, ADDR_BRF+15: BRF_r[15][7:0] <= snes_writebuf_data_r; // 8'h4F, +`endif ADDR_MCNT : {MCNT_r[`MCNT_ACM],MCNT_r[`MCNT_MD]} <= {snes_writebuf_data_r[`MCNT_ACM],snes_writebuf_data_r[`MCNT_MD]}; // 8'h50, ADDR_MA : MA_r[7:0] <= snes_writebuf_data_r; // 8'h51, // $2 ADDR_MA+1 : MA_r[15:8] <= snes_writebuf_data_r; // 8'h51, // $2 @@ -962,6 +1052,80 @@ always @(posedge CLK) begin default: begin end endcase end + + // ---- Savestate restore: MMIO config regs plus this block's interrupt-flag + // bits (SFR_r[7]/CFR_r[7,6,4]); VDA, MR/OF and SFR[5]/CFR[5] belong to the VBD, + // math and DMA blocks. Frozen only, so it cannot collide with a normal write. +`ifdef SA1_SS_ACTIVE + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h10: SFR_r[`SFR_CPU_IRQFL] <= ss_wr_data[7]; + 8'h11: begin CFR_r[`CFR_SA1_IRQFL] <= ss_wr_data[7]; CFR_r[`CFR_TMR_IRQFL] <= ss_wr_data[6]; CFR_r[`CFR_SA1_NMIFL] <= ss_wr_data[4]; end + 8'h20: CCNT_r <= ss_wr_data; + 8'h21: SIE_r <= ss_wr_data; + 8'h22: SIC_r <= ss_wr_data; + 8'h23: CRV_r[7:0] <= ss_wr_data; + 8'h24: CRV_r[15:8] <= ss_wr_data; + 8'h25: CNV_r[7:0] <= ss_wr_data; + 8'h26: CNV_r[15:8] <= ss_wr_data; + 8'h27: CIV_r[7:0] <= ss_wr_data; + 8'h28: CIV_r[15:8] <= ss_wr_data; + 8'h29: SCNT_r <= ss_wr_data; + 8'h2A: CIE_r <= ss_wr_data; + 8'h2B: CIC_r <= ss_wr_data; + 8'h2C: SNV_r[7:0] <= ss_wr_data; + 8'h2D: SNV_r[15:8] <= ss_wr_data; + 8'h2E: SIV_r[7:0] <= ss_wr_data; + 8'h2F: SIV_r[15:8] <= ss_wr_data; + 8'h30: TMC_r <= ss_wr_data; + 8'h36: CXB_r <= ss_wr_data; + 8'h37: DXB_r <= ss_wr_data; + 8'h38: EXB_r <= ss_wr_data; + 8'h39: FXB_r <= ss_wr_data; + 8'h3A: BMAPS_r <= ss_wr_data; + 8'h3B: BMAP_r <= ss_wr_data; + 8'h3C: SWBE_r <= ss_wr_data; + 8'h3D: CWBE_r <= ss_wr_data; + 8'h3E: BWPA_r <= ss_wr_data; + 8'h3F: SIWP_r <= ss_wr_data; + 8'h40: CIWP_r <= ss_wr_data; + 8'h41: DCNT_r <= ss_wr_data; + 8'h42: CDMA_r <= ss_wr_data; + 8'h43: DSA_r[7:0] <= ss_wr_data; + 8'h44: DSA_r[15:8] <= ss_wr_data; + 8'h45: DSA_r[23:16] <= ss_wr_data; + 8'h46: DDA_r[7:0] <= ss_wr_data; + 8'h47: DDA_r[15:8] <= ss_wr_data; + 8'h48: DDA_r[23:16] <= ss_wr_data; + 8'h49: DTC_r[7:0] <= ss_wr_data; + 8'h4A: DTC_r[15:8] <= ss_wr_data; + 8'h4B: BBF_r <= ss_wr_data; + 8'h4C: BRF_r[0] <= ss_wr_data; + 8'h4D: BRF_r[1] <= ss_wr_data; + 8'h4E: BRF_r[2] <= ss_wr_data; + 8'h4F: BRF_r[3] <= ss_wr_data; + 8'h50: BRF_r[4] <= ss_wr_data; + 8'h51: BRF_r[5] <= ss_wr_data; + 8'h52: BRF_r[6] <= ss_wr_data; + 8'h53: BRF_r[7] <= ss_wr_data; + 8'h54: BRF_r[8] <= ss_wr_data; + 8'h55: BRF_r[9] <= ss_wr_data; + 8'h56: BRF_r[10] <= ss_wr_data; + 8'h57: BRF_r[11] <= ss_wr_data; + 8'h58: BRF_r[12] <= ss_wr_data; + 8'h59: BRF_r[13] <= ss_wr_data; + 8'h5A: BRF_r[14] <= ss_wr_data; + 8'h5B: BRF_r[15] <= ss_wr_data; + 8'h5C: MCNT_r <= ss_wr_data; + 8'h5D: MA_r[7:0] <= ss_wr_data; + 8'h5E: MA_r[15:8] <= ss_wr_data; + 8'h5F: MB_r[7:0] <= ss_wr_data; + 8'h60: MB_r[15:8] <= ss_wr_data; + 8'h61: VBD_r <= ss_wr_data; + default: ; + endcase + end +`endif end end @@ -1029,6 +1193,34 @@ always @(posedge CLK) begin MR_r <= 0; OF_r <= 0; end + // Savestate freeze: hold MR_r (otherwise recomputed every clock) and restore + // MR/OF plus the flopped operands, handshake forced idle for the release. +`ifdef SA1_SS_ACTIVE + else if (ss_frozen) begin + // Pure hold while frozen; the $F0 write forces the handshake idle on a load. + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h68: MR_r[7:0] <= ss_wr_data; + 8'h69: MR_r[15:8] <= ss_wr_data; + 8'h6A: MR_r[23:16] <= ss_wr_data; + 8'h6B: MR_r[31:24] <= ss_wr_data; + 8'h6C: MR_r[39:32] <= ss_wr_data; + 8'h6D: OF_r <= ss_wr_data; + 8'h78: math_md_r <= ss_wr_data[1:0]; + 8'h79: math_ma_r[7:0] <= ss_wr_data; + 8'h7A: math_ma_r[15:8] <= ss_wr_data; + 8'h7B: math_mb_r[7:0] <= ss_wr_data; + 8'h7C: math_mb_r[15:8] <= ss_wr_data; + 8'hF0: begin + math_val_r <= 0; + math_acm_r <= 0; + math_init_r <= 0; + end + default: ; + endcase + end + end +`endif else begin if (snes_writebuf_val_r) begin if (snes_writebuf_addr_r[8:0] == ADDR_MB+1) begin @@ -1723,7 +1915,8 @@ assign RAM_BUS_WORD = 1'b0; assign RAM_BUS_ADDR = {4'h0,ram_bus_addr_r}; assign RAM_BUS_WRDATA = ram_bus_data_r; -assign iram_wren = MMC_STATE[clog2(ST_MMC_IRAM)] & mmc_wr_r; +// Keep the SA-1-side IRAM write off while frozen so it cannot race the restore. +assign iram_wren = MMC_STATE[clog2(ST_MMC_IRAM)] & mmc_wr_r & ~ss_frozen; assign iram_addr = mmc_addr_r[10:0]; assign iram_din = mmc_wrdata_r[7:0]; @@ -1844,6 +2037,69 @@ always @(posedge CLK) begin SFR_r[`SFR_DMA_IRQFL] <= 0; CFR_r[`CFR_DMA_IRQFL] <= 0; end + // Savestate freeze: hold the DMA state, force the transients to a clean idle so + // the release resumes from ST_DMA_IDLE, and restore the persistent CC1 config and + // the DMA interrupt flags. The derived CC1 config recomputes from CDMA_r. +`ifdef SA1_SS_ACTIVE + else if (ss_frozen) begin + // Pure HOLD while frozen (DSP-core semantics): the normal logic is suspended, + // so the handler's own bus activity (e.g. the BW-RAM capture reads at + // $40:xxxx) cannot pulse the CC1 trigger, and a timeout-forced freeze parks + // any in-flight CC1/DMA state exactly. Unconditionally zeroing the + // transients here corrupted a LIVE CC1 session on a plain SAVE (seen in + // hardware: the SMRPG menu loses its CC1-converted digits and the game hangs + // on the SA-1 interlock). The LOAD path normalizes the FSM/transients + // explicitly via the $F0 resume-shape write below. + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h10: SFR_r[`SFR_DMA_IRQFL] <= ss_wr_data[5]; + 8'h11: CFR_r[`CFR_DMA_IRQFL] <= ss_wr_data[5]; + 8'h80: dma_cc1_en_r <= ss_wr_data[0]; + 8'h81: dma_cc1_active_r <= ss_wr_data[0]; + 8'h82: dma_cc1_char_num_r <= ss_wr_data[4:0]; + 8'h83: dma_cc1_addr_char_base_r[7:0] <= ss_wr_data; + 8'h84: dma_cc1_addr_char_base_r[15:8] <= ss_wr_data; + 8'h85: dma_cc1_addr_char_base_r[23:16] <= ss_wr_data; + 8'h86: dma_cc1_addr_row_base_r[7:0] <= ss_wr_data; + 8'h87: dma_cc1_addr_row_base_r[15:8] <= ss_wr_data; + 8'h88: dma_cc1_addr_row_base_r[23:16] <= ss_wr_data; + 8'h89: dma_cc1_addr_rd_r[7:0] <= ss_wr_data; + 8'h8A: dma_cc1_addr_rd_r[15:8] <= ss_wr_data; + 8'h8B: dma_cc1_addr_rd_r[23:16] <= ss_wr_data; + 8'h8C: dma_cc1_addr_wr_r[7:0] <= ss_wr_data; + 8'h8D: dma_cc1_addr_wr_r[10:8] <= ss_wr_data[2:0]; + 8'h8E: dma_cc1_data_r[0] <= ss_wr_data; + 8'h8F: dma_cc1_data_r[1] <= ss_wr_data; + 8'h90: dma_cc1_data_r[2] <= ss_wr_data; + 8'h91: dma_cc1_data_r[3] <= ss_wr_data; + 8'h92: dma_cc1_data_r[4] <= ss_wr_data; + 8'h93: dma_cc1_data_r[5] <= ss_wr_data; + 8'h94: dma_cc1_data_r[6] <= ss_wr_data; + 8'h95: dma_cc1_data_r[7] <= ss_wr_data; + 8'h96: dma_cc2_line_r <= ss_wr_data[3:0]; + 8'hF0: begin + // resume-shape write (LOAD): normalize the engine for a clean restart + DMA_STATE <= ST_DMA_IDLE; + dma_mmc_rd_rom_r <= 0; + dma_mmc_rd_bram_r <= 0; + dma_mmc_wr_bram_r <= 0; + dma_mmc_rd_iram_r <= 0; + dma_mmc_wr_iram_r <= 0; + dma_trigger_normal_r <= 0; + dma_start_type1_r <= 0; + dma_trigger_type1_r <= 0; + dma_trigger_type2_r <= 0; + dma_cc1_int_r <= 0; + dma_normal_int_r <= 0; + dma_normal_pri_active_r <= 0; + dma_normal_prefetch_val_r <= 0; + dma_normal_state_r <= 0; + end + default: ; + endcase + end + end +`endif else begin // watch for triggers dma_trigger_normal_r <= ( dma_dcnt_r[`DCNT_DMAEN] @@ -2207,6 +2463,38 @@ always @(posedge CLK) begin VBD_STATE <= ST_VBD_IDLE; end + // Savestate freeze: hold the VBD engine idle and restore VDA/VDP/vbit/data. VDA_r + // is owned by this block (not the MMIO block), so it is restored here. The FSM is + // forced idle (the freeze boundary already requires VBD idle). Constant-folds on + // mk2 (ss_frozen == 0). +`ifdef SA1_SS_ACTIVE + else if (ss_frozen) begin + // Pure HOLD while frozen (see the DMA block note); the $F0 resume-shape + // write normalizes the engine on the LOAD path. + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h62: VDA_r[7:0] <= ss_wr_data; + 8'h63: VDA_r[15:8] <= ss_wr_data; + 8'h64: VDA_r[23:16] <= ss_wr_data; + 8'h6E: VDP_r[7:0] <= ss_wr_data; + 8'h6F: VDP_r[15:8] <= ss_wr_data; + 8'h70: vbd_vbit_r <= ss_wr_data[3:0]; + 8'h71: vbd_data_r[7:0] <= ss_wr_data; + 8'h72: vbd_data_r[15:8] <= ss_wr_data; + 8'h73: vbd_data_r[23:16] <= ss_wr_data; + 8'h74: vbd_data_r[31:24] <= ss_wr_data; + 8'hF0: begin + vbd_mmc_rd_r <= 0; + vbd_trigger_r <= 0; + vbd_update_r <= 0; + vbd_active_r <= 0; + VBD_STATE <= ST_VBD_IDLE; + end + default: ; + endcase + end + end +`endif else begin // watch for triggers // HL=0 trigger on VBA+2 and every VBD write. HL=1 trigger on VDP+1 data read and every VBD write. @@ -2330,15 +2618,34 @@ wire int_wai; // - WAI write from execute and clear from interrupt edge (while in WAI state). Should have common support in mmc for interrupt active. // - Set/Clear interrupt flag in register state. always @(posedge CLK) begin - int_rti_r <= exe_dec_grp == `GRP_SPC && exe_dec_add_stk && !exe_dec_store; - if (RST) begin int_pending_r <= 0; int_nmi_r <= 0; WAI_r <= 0; + int_rti_r <= 0; end + // Savestate freeze: hold the interrupt state and restore it from the window. + // WAI_r is restored via the $F0 resume-shape byte so it lands after the FSMs are + // normalized. Constant-folds on mk2 (ss_frozen == 0). +`ifdef SA1_SS_ACTIVE + else if (ss_frozen) begin + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h12: int_pending_r <= ss_wr_data[0]; + 8'h13: int_nmi_r <= ss_wr_data[0]; + 8'h14: int_vector_r[7:0] <= ss_wr_data; + 8'h15: int_vector_r[15:8] <= ss_wr_data; + 8'h16: int_rti_r <= ss_wr_data[0]; + 8'hF0: WAI_r <= ss_wr_data[0]; + default: ; + endcase + end + end +`endif else begin + int_rti_r <= exe_dec_grp == `GRP_SPC && exe_dec_add_stk && !exe_dec_store; + // WAI_r can only be set in EXE_WAIT for WAI if (EXE_STATE[clog2(ST_EXE_WAIT)] & pipeline_advance) begin // check current pending. taking an interrupt will block nmi and avoid duplicate irq. @@ -2561,6 +2868,42 @@ always @(posedge CLK) begin e2c_waitcnt_r <= 0; end + // Savestate freeze: hold the execution pipeline and restore the committed + // architectural registers. The $F0 resume-shape write normalizes the pipeline + // to a clean IDLE boundary and reloads the fetch pointer from the restored + // {PBR,PC} (offsets $0A-$0C, written earlier in the ascending block copy). On + // release the IDLE fetch reloads the exe_* working copies from *_r (FETCH_END), + // so only the committed regs need restoring. Constant-folds on mk2. +`ifdef SA1_SS_ACTIVE + else if (ss_frozen) begin + if (ss_block_wr) begin + case (ss_off[7:0]) + 8'h00: A_r[7:0] <= ss_wr_data; + 8'h01: A_r[15:8] <= ss_wr_data; + 8'h02: X_r[7:0] <= ss_wr_data; + 8'h03: X_r[15:8] <= ss_wr_data; + 8'h04: Y_r[7:0] <= ss_wr_data; + 8'h05: Y_r[15:8] <= ss_wr_data; + 8'h06: S_r[7:0] <= ss_wr_data; + 8'h07: S_r[15:8] <= ss_wr_data; + 8'h08: D_r[7:0] <= ss_wr_data; + 8'h09: D_r[15:8] <= ss_wr_data; + 8'h0A: PC_r[7:0] <= ss_wr_data; + 8'h0B: PC_r[15:8] <= ss_wr_data; + 8'h0C: PBR_r <= ss_wr_data; + 8'h0D: DBR_r <= ss_wr_data; + 8'h0E: P_r <= ss_wr_data; + 8'h0F: E_r <= ss_wr_data[0]; + 8'hF0: begin + EXE_STATE <= ST_EXE_IDLE; + exe_active_r <= 1'b0; + exe_fetch_addr_r <= {PBR_r, PC_r}; + end + default: ; + endcase + end + end +`endif else begin case (EXE_STATE) ST_EXE_IDLE: begin @@ -2569,7 +2912,12 @@ always @(posedge CLK) begin exe_fetch_addr_r <= {8'h00,CRV_r}; end - if (~(CCNT_r[`CCNT_SA1_RESB] | CCNT_r[`CCNT_SA1_RDYB]) & sa1_clock_en) begin + // Savestate halt: ss_halt_req_eff parks the CPU here at an IDLE + // instruction boundary (both 0 on mk2). ss_idle_wai_park (=WAI_r on mk3) + // keeps a RESTORED WAI parked in IDLE until the interrupt block clears + // WAI_r on a pending interrupt; in normal operation IDLE never coexists + // with WAI_r=1 so it is a no-op. + if (~(CCNT_r[`CCNT_SA1_RESB] | CCNT_r[`CCNT_SA1_RDYB] | snapshot_pause | ss_halt_req_eff | ss_idle_wai_park) & sa1_clock_en) begin exe_fetch_size_r <= 0; exe_mmc_byte_total_r <= 1; exe_data_word_r <= 0; @@ -3229,7 +3577,7 @@ always @(posedge CLK) begin // reset internal PCs to help with debugging exe_nextpc_r <= 0; - EXE_STATE <= (exe_active_r & ~(CCNT_r[`CCNT_SA1_RESB] | CCNT_r[`CCNT_SA1_RDYB])) ? ST_EXE_FETCH : ST_EXE_IDLE; + EXE_STATE <= (exe_active_r & ~(CCNT_r[`CCNT_SA1_RESB] | CCNT_r[`CCNT_SA1_RDYB] | snapshot_pause | ss_halt_req_eff)) ? ST_EXE_FETCH : ST_EXE_IDLE; end end endcase @@ -3243,6 +3591,229 @@ assign exe_fetch_move = exe_move_val_r; assign exe_fetch_byte = exe_prefetch_r; assign exe_fetch_data = exe_fetch_data_r[7:0]; +//------------------------------------------------------------------- +// SAVESTATE SCAN WINDOW (mk3 only) +//------------------------------------------------------------------- +// A flat 256-byte state block plus a halt-control byte are exposed to the SNES at +// $E8:0000-$E8:07FF while the handler holds the snescmd region unlocked +// (ss_window_en from address.v). The handler halts the SA-1 at an instruction +// boundary, DMAs the block out (save) or in (load), then releases the halt. +// +// $7FF control : WRITE bit0=1 request halt / bit0=0 release. +// READ bit0 = frozen, bit1 = frozen-at-WAI (debug), rest 0. +// $000-$0FF state block (RESTORE writes are applied ascending, so $F0 lands late): +// $00-$0F arch: A X Y S D (16b lo/hi), PC(16), PBR, DBR, P, E{b0}. +// At the WAI boundary the committed *_r may not hold post-WAI values, +// so the READ returns the EFFECTIVE next-instruction values (the exe_* +// working copies + next PC). RESTORE writes the committed *_r; the +// IDLE fetch reloads exe_* from *_r on release, so that is sufficient. +// $10 SFR $11 CFR $12 int_pending{b0} $13 int_nmi{b0} +// $14-$15 int_vector(16) $16 int_rti{b0} +// $20 CCNT $21 SIE $22 SIC $23-$24 CRV $25-$26 CNV $27-$28 CIV +// $29 SCNT $2A CIE $2B CIC $2C-$2D SNV $2E-$2F SIV +// $30 TMC $31 CTR $32-$33 HCNT $34-$35 VCNT (last three read-only/static) +// $36 CXB $37 DXB $38 EXB $39 FXB $3A BMAPS $3B BMAP $3C SWBE $3D CWBE +// $3E BWPA $3F SIWP $40 CIWP $41 DCNT $42 CDMA +// $43-$45 DSA(24) $46-$48 DDA(24) $49-$4A DTC(16) $4B BBF +// $4C-$5B BRF[0..15] $5C MCNT $5D-$5E MA $5F-$60 MB $61 VBD $62-$64 VDA(24) +// $68-$6C MR(40) $6D OF $6E-$6F VDP(16) +// $70 vbd_vbit{[3:0]} $71-$74 vbd_data(32) +// $78 math_md{[1:0]} $79-$7A math_ma $7B-$7C math_mb $7D math flags (read-only) +// $80 cc1_en{b0} $81 cc1_active{b0} $82 cc1_char_num{[4:0]} +// $83-$85 cc1_addr_char_base(24) $86-$88 cc1_addr_row_base(24) +// $89-$8B cc1_addr_rd(24) $8C-$8D cc1_addr_wr(11) $8E-$95 cc1_line_buf[0..7] +// $96 cc2_line{[3:0]} +// $97 cc1_bpp $98-$99 cc1_bpl $9A cc1_size_mask $9B cc1_mask $9C cc1_imask +// (the last six are derived and read-only: they recompute from CDMA_r) +// $F0 resume-shape: READ bit0=WAI_r; WRITE bit0->WAI_r + normalize FSMs to idle +// $FF magic, READ-only, returns $5A +// The restore demux lives inside each owning always block (search "Savestate +// freeze" / "Savestate RESTORE"); this section holds the freeze FSM + read mux. +// The freeze CONTROL signals (ss_frozen, ss_block_wr, ss_off, ss_wr_data, +// ss_halt_req_eff, ss_idle_wai_park) are declared EARLY (near the state flops) so +// the guards above bind cleanly; the FSM + capture that produce them live here. +// Everything here is gated on SA1_SS_ACTIVE (mk3 always; mk2 only with the +// experimental SA1_SS_MK2): without it those control signals are constant 0 +// (tied off early) and this whole section is compiled out, so the guards fold to +// the original SA-1 logic. +`ifdef SA1_SS_ACTIVE +// Frozen at the WAI boundary: exposes the effective (post-WAI) arch state on READ. +wire ss_at_wai = (EXE_STATE == ST_EXE_WAIT) & WAI_r; + +// Freeze boundary: an IDLE (or a parked WAI) instruction boundary with every +// SA-1-side engine quiesced, OR the saturating timeout (never hangs). +wire ss_boundary = (EXE_STATE[clog2(ST_EXE_IDLE)] | ss_at_wai) + & DMA_STATE[clog2(ST_DMA_IDLE)] + & MMC_STATE[clog2(ST_MMC_IDLE)] + & (MMC_RAM_STATE == ST_MMC_RAM_IDLE) + & VBD_STATE[clog2(ST_VBD_IDLE)] + & ~math_val_r & ~math_acm_r & ~math_init_r + // pending 1-cycle pulses: freezing on the cycle one is + // latched would discard it. They can only delay the freeze + // by a cycle or two, never starve it. + & ~vbd_trigger_r & ~vbd_update_r + & ~dma_trigger_normal_r & ~dma_start_type1_r + & ~dma_trigger_type1_r & ~dma_trigger_type2_r; + +// Effective (post-WAI) architectural values for the capture mux. +wire [15:0] ss_eff_pc = exe_control_r ? exe_target_r[15:0] : exe_nextpc_r; +wire [7:0] ss_eff_pbr = exe_control_r ? exe_target_r[23:16] : exe_pbr_r; +wire [15:0] ss_a = ss_at_wai ? exe_a_r : A_r; +wire [15:0] ss_x = ss_at_wai ? exe_x_r : X_r; +wire [15:0] ss_y = ss_at_wai ? exe_y_r : Y_r; +wire [15:0] ss_s = ss_at_wai ? exe_s_r : S_r; +wire [15:0] ss_d = ss_at_wai ? exe_d_r : D_r; +wire [15:0] ss_pc = ss_at_wai ? ss_eff_pc : PC_r; +wire [7:0] ss_pbr = ss_at_wai ? ss_eff_pbr : PBR_r; +wire [7:0] ss_dbr = ss_at_wai ? exe_dbr_r : DBR_r; +wire [7:0] ss_p = ss_at_wai ? exe_p_r : P_r; +wire ss_e = ss_at_wai ? exe_e_r : E_r; + +// Halt-request flop: settable/clearable even while everything else is frozen. +always @(posedge CLK) begin + if (RST) ss_halt_snes <= 1'b0; + else if (ss_win_wr & (ss_off == 11'h7ff)) ss_halt_snes <= ss_wr_data[0]; +end + +// Boundary-gated freeze with saturating timeout. ss_dbg_froze_to_r is sticky for +// the halt session: set when the freeze came from the timeout, not a boundary. +reg ss_dbg_froze_to_r; initial ss_dbg_froze_to_r = 1'b0; +always @(posedge CLK) begin + if (RST | ~ss_halt_req_eff) begin + ss_frozen <= 1'b0; + ss_wait <= 20'h0; + end else if (~ss_frozen) begin + ss_wait <= ss_wait + 1'b1; + if (ss_boundary | (&ss_wait)) begin + ss_frozen <= 1'b1; + if (~ss_boundary) ss_dbg_froze_to_r <= 1'b1; + end + end +end + +// Align the window-enable with addr_in_r for the write path. +always @(posedge CLK) ss_window_en_r <= ss_window_en; + +// Registered read serve (GSU lesson: never an unregistered deep mux). +`ifdef SA1_SS_MK2 +// XST refuses an unpacked-array element in the implicit sensitivity list of an +// always @(*) (Xst:902). Tap the words into nets outside the block so the mux +// reads nets only; Quartus accepts the array read, kept in the `else arm. +wire [7:0] ss_brf00 = BRF_r[0]; wire [7:0] ss_brf01 = BRF_r[1]; +wire [7:0] ss_brf02 = BRF_r[2]; wire [7:0] ss_brf03 = BRF_r[3]; +wire [7:0] ss_brf04 = BRF_r[4]; wire [7:0] ss_brf05 = BRF_r[5]; +wire [7:0] ss_brf06 = BRF_r[6]; wire [7:0] ss_brf07 = BRF_r[7]; +wire [7:0] ss_brf08 = BRF_r[8]; wire [7:0] ss_brf09 = BRF_r[9]; +wire [7:0] ss_brf10 = BRF_r[10]; wire [7:0] ss_brf11 = BRF_r[11]; +wire [7:0] ss_brf12 = BRF_r[12]; wire [7:0] ss_brf13 = BRF_r[13]; +wire [7:0] ss_brf14 = BRF_r[14]; wire [7:0] ss_brf15 = BRF_r[15]; +// Same treatment for the other unpacked array the mux reads. +wire [7:0] ss_cc1d0 = dma_cc1_data_r[0]; wire [7:0] ss_cc1d1 = dma_cc1_data_r[1]; +wire [7:0] ss_cc1d2 = dma_cc1_data_r[2]; wire [7:0] ss_cc1d3 = dma_cc1_data_r[3]; +wire [7:0] ss_cc1d4 = dma_cc1_data_r[4]; wire [7:0] ss_cc1d5 = dma_cc1_data_r[5]; +wire [7:0] ss_cc1d6 = dma_cc1_data_r[6]; wire [7:0] ss_cc1d7 = dma_cc1_data_r[7]; +`endif +reg [7:0] ss_cap; +always @(*) begin + ss_cap = 8'h00; + if (SNES_ADDR[10:0] == 11'h7ff) + ss_cap = {6'h0, (ss_at_wai & ss_frozen), ss_frozen}; + else if (SNES_ADDR[10:8] == 3'b000) begin + case (SNES_ADDR[7:0]) + 8'h00: ss_cap = ss_a[7:0]; 8'h01: ss_cap = ss_a[15:8]; + 8'h02: ss_cap = ss_x[7:0]; 8'h03: ss_cap = ss_x[15:8]; + 8'h04: ss_cap = ss_y[7:0]; 8'h05: ss_cap = ss_y[15:8]; + 8'h06: ss_cap = ss_s[7:0]; 8'h07: ss_cap = ss_s[15:8]; + 8'h08: ss_cap = ss_d[7:0]; 8'h09: ss_cap = ss_d[15:8]; + 8'h0A: ss_cap = ss_pc[7:0]; 8'h0B: ss_cap = ss_pc[15:8]; + 8'h0C: ss_cap = ss_pbr; 8'h0D: ss_cap = ss_dbr; + 8'h0E: ss_cap = ss_p; 8'h0F: ss_cap = {7'h0, ss_e}; + 8'h10: ss_cap = SFR_r; 8'h11: ss_cap = CFR_r; + 8'h12: ss_cap = {7'h0, int_pending_r}; + 8'h13: ss_cap = {7'h0, int_nmi_r}; + 8'h14: ss_cap = int_vector_r[7:0]; 8'h15: ss_cap = int_vector_r[15:8]; + 8'h16: ss_cap = {7'h0, int_rti_r}; + 8'h20: ss_cap = CCNT_r; 8'h21: ss_cap = SIE_r; 8'h22: ss_cap = SIC_r; + 8'h23: ss_cap = CRV_r[7:0]; 8'h24: ss_cap = CRV_r[15:8]; + 8'h25: ss_cap = CNV_r[7:0]; 8'h26: ss_cap = CNV_r[15:8]; + 8'h27: ss_cap = CIV_r[7:0]; 8'h28: ss_cap = CIV_r[15:8]; + 8'h29: ss_cap = SCNT_r; 8'h2A: ss_cap = CIE_r; 8'h2B: ss_cap = CIC_r; + 8'h2C: ss_cap = SNV_r[7:0]; 8'h2D: ss_cap = SNV_r[15:8]; + 8'h2E: ss_cap = SIV_r[7:0]; 8'h2F: ss_cap = SIV_r[15:8]; + 8'h30: ss_cap = TMC_r; 8'h31: ss_cap = CTR_r; + 8'h32: ss_cap = HCNT_r[7:0]; 8'h33: ss_cap = HCNT_r[15:8]; + 8'h34: ss_cap = VCNT_r[7:0]; 8'h35: ss_cap = VCNT_r[15:8]; + 8'h36: ss_cap = CXB_r; 8'h37: ss_cap = DXB_r; + 8'h38: ss_cap = EXB_r; 8'h39: ss_cap = FXB_r; + 8'h3A: ss_cap = BMAPS_r; 8'h3B: ss_cap = BMAP_r; + 8'h3C: ss_cap = SWBE_r; 8'h3D: ss_cap = CWBE_r; + 8'h3E: ss_cap = BWPA_r; 8'h3F: ss_cap = SIWP_r; 8'h40: ss_cap = CIWP_r; + 8'h41: ss_cap = DCNT_r; 8'h42: ss_cap = CDMA_r; + 8'h43: ss_cap = DSA_r[7:0]; 8'h44: ss_cap = DSA_r[15:8]; 8'h45: ss_cap = DSA_r[23:16]; + 8'h46: ss_cap = DDA_r[7:0]; 8'h47: ss_cap = DDA_r[15:8]; 8'h48: ss_cap = DDA_r[23:16]; + 8'h49: ss_cap = DTC_r[7:0]; 8'h4A: ss_cap = DTC_r[15:8]; + 8'h4B: ss_cap = BBF_r; +`ifdef SA1_SS_MK2 + 8'h4C: ss_cap = ss_brf00; 8'h4D: ss_cap = ss_brf01; 8'h4E: ss_cap = ss_brf02; 8'h4F: ss_cap = ss_brf03; + 8'h50: ss_cap = ss_brf04; 8'h51: ss_cap = ss_brf05; 8'h52: ss_cap = ss_brf06; 8'h53: ss_cap = ss_brf07; + 8'h54: ss_cap = ss_brf08; 8'h55: ss_cap = ss_brf09; 8'h56: ss_cap = ss_brf10; 8'h57: ss_cap = ss_brf11; + 8'h58: ss_cap = ss_brf12; 8'h59: ss_cap = ss_brf13; 8'h5A: ss_cap = ss_brf14; 8'h5B: ss_cap = ss_brf15; +`else + 8'h4C: ss_cap = BRF_r[0]; 8'h4D: ss_cap = BRF_r[1]; 8'h4E: ss_cap = BRF_r[2]; 8'h4F: ss_cap = BRF_r[3]; + 8'h50: ss_cap = BRF_r[4]; 8'h51: ss_cap = BRF_r[5]; 8'h52: ss_cap = BRF_r[6]; 8'h53: ss_cap = BRF_r[7]; + 8'h54: ss_cap = BRF_r[8]; 8'h55: ss_cap = BRF_r[9]; 8'h56: ss_cap = BRF_r[10]; 8'h57: ss_cap = BRF_r[11]; + 8'h58: ss_cap = BRF_r[12]; 8'h59: ss_cap = BRF_r[13]; 8'h5A: ss_cap = BRF_r[14]; 8'h5B: ss_cap = BRF_r[15]; +`endif + 8'h5C: ss_cap = MCNT_r; + 8'h5D: ss_cap = MA_r[7:0]; 8'h5E: ss_cap = MA_r[15:8]; + 8'h5F: ss_cap = MB_r[7:0]; 8'h60: ss_cap = MB_r[15:8]; + 8'h61: ss_cap = VBD_r; + 8'h62: ss_cap = VDA_r[7:0]; 8'h63: ss_cap = VDA_r[15:8]; 8'h64: ss_cap = VDA_r[23:16]; + 8'h68: ss_cap = MR_r[7:0]; 8'h69: ss_cap = MR_r[15:8]; 8'h6A: ss_cap = MR_r[23:16]; + 8'h6B: ss_cap = MR_r[31:24];8'h6C: ss_cap = MR_r[39:32]; + 8'h6D: ss_cap = OF_r; + 8'h6E: ss_cap = VDP_r[7:0]; 8'h6F: ss_cap = VDP_r[15:8]; + 8'h70: ss_cap = {4'h0, vbd_vbit_r}; + 8'h71: ss_cap = vbd_data_r[7:0]; 8'h72: ss_cap = vbd_data_r[15:8]; + 8'h73: ss_cap = vbd_data_r[23:16]; 8'h74: ss_cap = vbd_data_r[31:24]; + 8'h78: ss_cap = {6'h0, math_md_r}; + 8'h79: ss_cap = math_ma_r[7:0]; 8'h7A: ss_cap = math_ma_r[15:8]; + 8'h7B: ss_cap = math_mb_r[7:0]; 8'h7C: ss_cap = math_mb_r[15:8]; + 8'h7D: ss_cap = {5'h0, math_init_r, math_acm_r, math_val_r}; + 8'h80: ss_cap = {7'h0, dma_cc1_en_r}; + 8'h81: ss_cap = {7'h0, dma_cc1_active_r}; + 8'h82: ss_cap = {3'h0, dma_cc1_char_num_r}; + 8'h83: ss_cap = dma_cc1_addr_char_base_r[7:0]; 8'h84: ss_cap = dma_cc1_addr_char_base_r[15:8]; 8'h85: ss_cap = dma_cc1_addr_char_base_r[23:16]; + 8'h86: ss_cap = dma_cc1_addr_row_base_r[7:0]; 8'h87: ss_cap = dma_cc1_addr_row_base_r[15:8]; 8'h88: ss_cap = dma_cc1_addr_row_base_r[23:16]; + 8'h89: ss_cap = dma_cc1_addr_rd_r[7:0]; 8'h8A: ss_cap = dma_cc1_addr_rd_r[15:8]; 8'h8B: ss_cap = dma_cc1_addr_rd_r[23:16]; + 8'h8C: ss_cap = dma_cc1_addr_wr_r[7:0]; 8'h8D: ss_cap = {5'h0, dma_cc1_addr_wr_r[10:8]}; +`ifdef SA1_SS_MK2 + 8'h8E: ss_cap = ss_cc1d0; 8'h8F: ss_cap = ss_cc1d1; 8'h90: ss_cap = ss_cc1d2; 8'h91: ss_cap = ss_cc1d3; + 8'h92: ss_cap = ss_cc1d4; 8'h93: ss_cap = ss_cc1d5; 8'h94: ss_cap = ss_cc1d6; 8'h95: ss_cap = ss_cc1d7; +`else + 8'h8E: ss_cap = dma_cc1_data_r[0]; 8'h8F: ss_cap = dma_cc1_data_r[1]; 8'h90: ss_cap = dma_cc1_data_r[2]; 8'h91: ss_cap = dma_cc1_data_r[3]; + 8'h92: ss_cap = dma_cc1_data_r[4]; 8'h93: ss_cap = dma_cc1_data_r[5]; 8'h94: ss_cap = dma_cc1_data_r[6]; 8'h95: ss_cap = dma_cc1_data_r[7]; +`endif + 8'h96: ss_cap = {4'h0, dma_cc2_line_r}; + 8'h97: ss_cap = {4'h0, dma_cc1_bpp_r}; + 8'h98: ss_cap = dma_cc1_bpl_r[7:0]; 8'h99: ss_cap = {7'h0, dma_cc1_bpl_r[8]}; + 8'h9A: ss_cap = {3'h0, dma_cc1_size_mask_r}; + 8'h9B: ss_cap = {2'h0, dma_cc1_mask_r}; + 8'h9C: ss_cap = {1'h0, dma_cc1_imask_r}; + 8'hF0: ss_cap = {7'h0, WAI_r}; + // $FF must stay read-only and serve $5A unconditionally, NOT gated on + // ss_frozen: the handler probes it before the freeze to tell a core with + // this window from one without, and the captured copy is what proves a + // .state really holds SA-1 state. + 8'hFF: ss_cap = 8'h5A; + default: ss_cap = 8'h00; + endcase + end +end + +always @(posedge CLK) ss_dout_r <= ss_cap; +`endif + `ifdef DEBUG // breakpoints reg brk_inst_rd_rom_m1; @@ -3607,7 +4178,31 @@ assign DBG = 0; `ifdef DEBUG assign PGM_DATA = pgmdata_out; `else +`ifdef SA1_SS_ACTIVE +// Joypad capture from the game's own IRAM forwarding ($3010/$3011): those writes +// are bus-visible, so this sees the pad in scenes where the ctx $4218 store-sniff +// does not fire. main.v ORs both to arm the IRQ hook only while a gesture is +// held; the ~33ms expiry drops stale values across scene changes. +reg [15:0] ss_iram_pad_r; initial ss_iram_pad_r = 0; +reg [21:0] ss_iram_pad_to_r; initial ss_iram_pad_to_r = 0; +always @(posedge CLK) begin + if (snes_writebuf_iram_r & (snes_iram_addr_r == 11'h010)) begin + ss_iram_pad_r[7:0] <= snes_writebuf_iram_data_r; + ss_iram_pad_to_r <= 22'h2FFFFF; + end + else if (snes_writebuf_iram_r & (snes_iram_addr_r == 11'h011)) begin + ss_iram_pad_r[15:8] <= snes_writebuf_iram_data_r; + ss_iram_pad_to_r <= 22'h2FFFFF; + end + else if (|ss_iram_pad_to_r) ss_iram_pad_to_r <= ss_iram_pad_to_r - 1'b1; + else ss_iram_pad_r <= 16'h0000; +end +assign PGM_DATA = 0; +assign ss_iram_pad = ss_iram_pad_r; +`else assign PGM_DATA = 0; +assign ss_iram_pad = 16'h0000; +`endif `endif assign DATA_ENABLE = snes_data_enable_r; diff --git a/verilog/sd2snes_sdd1/Makefile b/verilog/sd2snes_sdd1/Makefile index 03ce87ea..8c76f6b2 100644 --- a/verilog/sd2snes_sdd1/Makefile +++ b/verilog/sd2snes_sdd1/Makefile @@ -1,6 +1,6 @@ CORE = sdd1 -VSRC = address.v cheat.v clk_test.v dac.v DCM_Scope.v main.v mcu_cmd.v msu.v sd_dma.v spi.v +VSRC = address.v cheat.v clk_test.v dac.v DCM_Scope.v main.v mcu_cmd.v msu.v regshadow.v sd_dma.v spi.v VHSRC = FIFO_B2B.vhd FIFO_AXIS.vhd Golomb_0_Decoder.vhd Golomb_N_Decoder.vhd Input_Manager.vhd Output_Manager.vhd Probability_Estimator.vhd SDD1.vhd Serializer.vhd COMMON_IP = dac_buf msu_databuf snescmd_buf diff --git a/verilog/sd2snes_sdd1/address.v b/verilog/sd2snes_sdd1/address.v index c6f509e3..267e9614 100644 --- a/verilog/sd2snes_sdd1/address.v +++ b/verilog/sd2snes_sdd1/address.v @@ -27,8 +27,10 @@ module address( output IS_SAVERAM, // address/CS mapped as SRAM? output IS_ROM, // address mapped as ROM? output IS_WRITABLE, // address somehow mapped as writable area? + output IS_PATCH, // hook identity window active ($C0-FF while unlocked) input [23:0] SAVERAM_MASK, input [23:0] ROM_MASK, + input snescmd_unlock, // snescmd region unlocked (gates the hook window) output msu_enable, output r213f_enable, output r2100_hit, @@ -89,10 +91,18 @@ wire [12:0] SAVERAM_OFFSET_SMALL = SNES_ADDR[12:0]; assign IS_SAVERAM = IS_SAVERAM_BIGBANKS | IS_SAVERAM_SMALLBANKS; +// Hook identity window (as in sd2snes_base): while the hook holds the snescmd +// region unlocked, banks $C0-$FF are identity-mapped so the savestate handler runs +// from menu PSRAM with its scratch in $F2-$FF. 0 outside the hook window. +assign IS_PATCH = snescmd_unlock & &SNES_ADDR[23:22]; + // '1' to signal access to cartrigde writable range (Backup RAM or BS-X RAM) -assign IS_WRITABLE = IS_SAVERAM; +assign IS_WRITABLE = IS_SAVERAM | IS_PATCH; -assign SRAM_SNES_ADDR = IS_SAVERAM_BIGBANKS ? 24'hE00000 + ({SAVERAM_BANK_BIG, SAVERAM_OFFSET_BIG} & SAVERAM_MASK) +assign SRAM_SNES_ADDR = IS_PATCH + // hook window: identity-map $C0-$FF (handler code + scratch) + ? SNES_ADDR + : IS_SAVERAM_BIGBANKS ? 24'hE00000 + ({SAVERAM_BANK_BIG, SAVERAM_OFFSET_BIG} & SAVERAM_MASK) : IS_SAVERAM_SMALLBANKS ? 24'hE00000 + ({SAVERAM_BANK_SMALL, SAVERAM_OFFSET_SMALL} & SAVERAM_MASK) : ({1'b0, !SNES_ADDR[23], SNES_ADDR[21:0]} & ROM_MASK); diff --git a/verilog/sd2snes_sdd1/cheat.v b/verilog/sd2snes_sdd1/cheat.v index 3048045f..524421f2 100644 --- a/verilog/sd2snes_sdd1/cheat.v +++ b/verilog/sd2snes_sdd1/cheat.v @@ -51,6 +51,29 @@ reg irq_enable = 0; reg holdoff_enable = 0; // temp disable hooks after reset reg buttons_enable = 0; reg wram_present = 0; +// Full in-game save/load states run on this core (Mk.II and Mk.III): the S-DD1's +// SNES-visible state is its bus-facing config block ($4800-$4807), which the handler +// snapshots/restores directly over the bus -- the decompressor FSM is never mid- +// transfer at an NMI boundary (the GP-DMA that drives it is atomic), so no chip halt +// is needed. That still needs the base core's force-entry latch: the resume-wait +// protocol requires the NEXT hook entry to bump CS_STATE after the saveinputloop +// forced a button release -- without it the stub branches to nmi_exit and the game +// parks black forever (same lesson as the SA-1/GSU ports). savestate_force_entry +// keeps the nmi_savestate branch routed while a savestate is in flight with the +// buttons RELEASED. Pulse-latched: set at a branch1 fetch with buttons held, cleared +// at the unlock-drop. Only flip-flops, so it builds identically on Mk.II and Mk.III. +reg savestate_enable = 0; +reg savestate_force_entry_enable_strobe = 0; +reg savestate_force_entry_disable_strobe = 0; +reg savestate_force_entry = 0; + +always @(posedge clk) begin + if(savestate_force_entry_enable_strobe) begin + savestate_force_entry <= 1'b1; + end else if(savestate_force_entry_disable_strobe) begin + savestate_force_entry <= 1'b0; + end +end wire branch_wram = cheat_enable & wram_present; reg auto_nmi_enable = 1; @@ -196,6 +219,8 @@ always @(posedge clk) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; end else begin + savestate_force_entry_enable_strobe <= 0; + savestate_force_entry_disable_strobe <= 0; if(SNES_rd_strobe) begin // *** GAME -> INGAME HOOK *** if(hook_enable_sync @@ -209,6 +234,11 @@ always @(posedge clk) begin if(rst_match_bits[1] & |reset_unlock_r) begin snescmd_unlock_r <= 1; end + // arm the savestate force-entry latch when the hook redirect is taken with + // buttons held; it holds the nmi_savestate route through the button release. + if(branch1_enable & savestate_enable & |pad_data) begin + savestate_force_entry_enable_strobe <= 1; + end end // give some time to exit snescmd memory and jump to original vector // sta @NMI_VECT_DISABLE 1-2 (after effective write) @@ -221,6 +251,8 @@ always @(posedge clk) begin end else if(snescmd_unlock_disable_countdown == 0) begin snescmd_unlock_r <= 0; snescmd_unlock_disable <= 0; + // drop the force-entry latch at the same unlock-drop point + savestate_force_entry_disable_strobe <= 1; end end end @@ -302,12 +334,12 @@ always @(posedge clk) begin end else if(pgm_idx == 6) begin // set rom patch enable cheat_enable_mask <= pgm_in[5:0]; end else if(pgm_idx == 7) begin // set/reset global enable / hooks - // pgm_in[13:8] are reset bit flags - // pgm_in[5:0] are set bit flags - {wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - <= ({wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} - & ~pgm_in[13:8]) - | pgm_in[5:0]; + // pgm_in[14:8] are reset bit flags + // pgm_in[6:0] are set bit flags + {savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + <= ({savestate_enable, wram_present, buttons_enable, holdoff_enable, irq_enable, nmi_enable, cheat_enable} + & ~pgm_in[14:8]) + | pgm_in[6:0]; end end end @@ -352,7 +384,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & (savestate_force_entry | |pad_data)) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end else begin @@ -370,7 +406,11 @@ always @* begin if(branch_wram) begin branch1_offset = 8'h3a; // nmi_patches end else begin - branch1_offset = 8'h43; // nmi_exit + if(savestate_enable & |pad_data) begin + branch1_offset = 8'h3f; // nmi_savestate + end else begin + branch1_offset = 8'h43; // nmi_exit + end end end end @@ -381,7 +421,19 @@ always @* begin end else if(branch_wram) begin branch2_offset = 8'h00; // nmi_patches end else begin - branch2_offset = 8'h09; // nmi_exit + if(savestate_enable) begin + branch2_offset = 8'h05; // nmi_savestate + end else begin + branch2_offset = 8'h09; // nmi_exit + end + end +end + +always @* begin + if(savestate_enable) begin + branch3_offset = 8'h00; // nmi_savestate + end else begin + branch3_offset = 8'h04; // nmi_exit end end diff --git a/verilog/sd2snes_sdd1/main.qsf b/verilog/sd2snes_sdd1/main.qsf index 2b53ff1a..6abc985c 100644 --- a/verilog/sd2snes_sdd1/main.qsf +++ b/verilog/sd2snes_sdd1/main.qsf @@ -514,6 +514,7 @@ set_global_assignment -name VERILOG_FILE dac.v set_global_assignment -name VERILOG_FILE clk_test.v set_global_assignment -name VERILOG_FILE cheat.v set_global_assignment -name VERILOG_FILE address.v +set_global_assignment -name VERILOG_FILE regshadow.v set_global_assignment -name SDC_FILE main.sdc set_global_assignment -name VHDL_FILE Serializer.vhd set_global_assignment -name VHDL_FILE SDD1_Core.vhd diff --git a/verilog/sd2snes_sdd1/main.v b/verilog/sd2snes_sdd1/main.v index daa0b38c..4e586d1d 100644 --- a/verilog/sd2snes_sdd1/main.v +++ b/verilog/sd2snes_sdd1/main.v @@ -194,6 +194,7 @@ wire SD_DMA_TO_ROM; wire free_slot = (SNES_PULSE_end | free_strobe) & ~SD_DMA_TO_ROM; wire ROM_HIT; +wire IS_PATCH; // hook identity window ($C0-FF while snescmd unlocked) -- savestate assign DCM_RST=0; @@ -501,6 +502,8 @@ address snes_addr( .IS_ROM(IS_ROM), // '1' when SNES request to access to PSRAM writable range (Backup RAM or BS-X RAM) .IS_WRITABLE(IS_WRITABLE), + .IS_PATCH(IS_PATCH), + .snescmd_unlock(snescmd_unlock), .SAVERAM_MASK(SAVERAM_MASK), .ROM_MASK(ROM_MASK), //MSU-1 @@ -545,6 +548,50 @@ cheat snes_cheat( .snescmd_unlock(snescmd_unlock) ); +// PPU-register capture via the base ctx.v counter scheme; this core's CLK2 is the +// base's native 96 MHz, so the shift patterns and the count==4 sample point apply +// unchanged. Needs the snoop OE terms below, or the level shifter stays disabled +// for B-bus writes to non-cart addresses and the shadow captures bus float. +wire rs_pawr_start_early = ((SNES_PAWRr[4:1] | SNES_PAWRr[5:2]) == 4'b1110); +reg [3:0] rs_pawr_cnt; initial rs_pawr_cnt = 0; +reg rs_pawr_end; initial rs_pawr_end = 0; +reg rs_pawr_end_r; initial rs_pawr_end_r = 0; +reg [7:0] rs_data_r; initial rs_data_r = 0; +always @(posedge CLK2) begin + if (rs_pawr_end) rs_pawr_cnt <= 0; + else if (rs_pawr_start_early) rs_pawr_cnt <= 1; + else if (|rs_pawr_cnt) rs_pawr_cnt <= rs_pawr_cnt + 1'b1; + rs_pawr_end <= (rs_pawr_cnt == 4'd4); + rs_pawr_end_r <= rs_pawr_end; // ctx.v registers the strobe once more... + rs_data_r <= SNES_DATAr[0]; // ...and the data tap with it (2-cyc align) +end +// PPU (B-bus) -> the ctx-aligned captured byte; CPU ($42xx, A-bus/SNES_WR_end) uses +// raw SNES_DATA (the native snes_ajr capture pattern). +wire [7:0] rs_data = rs_pawr_end_r ? rs_data_r : SNES_DATA; + +// Savestate register shadow (regshadow.v), read through the hook window at +// $F90500 (PPU, stride-2 pairs) and $F90700 (CPU $42xx). IS_PATCH gates the reads. +wire shadow_ppu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF905) & ~SNES_ADDR[7]; // $F90500-7F +wire shadow_cpu_hit = IS_PATCH & (SNES_ADDR[23:8] == 16'hF907) & (SNES_ADDR[7:5] == 3'b000); // $F90700-1F +wire [7:0] regshadow_dout; +// PPU pair at mem[$00-$7F] indexed by SNES_ADDR[6:0], CPU reg at mem[$80-$9F]. +// 1-cycle BRAM read latency (like snescmd_buf); the address is stable for the whole +// ROM cycle. +wire [8:0] regshadow_raddr = shadow_cpu_hit ? {4'b0100, SNES_ADDR[4:0]} + : {2'b00, SNES_ADDR[6:0]}; +regshadow snes_regshadow( + .clk(CLK2), + // PPU strobe = the ctx-style counter end (count==4 from write start). + .pawr_end(rs_pawr_end_r), + .wr_end(SNES_WR_end), + .snes_addr(SNES_ADDR), + .snes_pa(SNES_PA), + // PPU: ctx-aligned captured byte; CPU: raw SNES_DATA (muxed by rs_pawr_end_r). + .snes_data(rs_data), + .rd_addr(regshadow_raddr), + .rd_data(regshadow_dout) +); + wire [7:0] snescmd_dout; parameter ST_R213F_ARMED = 4'b0001; @@ -570,6 +617,10 @@ wire r2100_patch = featurebits[6]; wire r2100_enable = r2100_hit & (r2100_patch | ~(&r2100_limit)); wire snoop_4200_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04200; +// regshadow write-snoop windows: enable the data-bus level shifter (receive) so the +// snoop sees the actual write byte instead of bus float -- see SNES_DATABUS_OE/DIR. +wire snoop_42xx_enable = ~SNES_ADDR[22] & (SNES_ADDR[15:5] == 11'b01000010000); +wire rs_snoop_pawr_oe = ~SNES_PAWR & (SNES_PA < 8'h40); wire r4016_enable = {SNES_ADDR[22], SNES_ADDR[15:0]} == 17'h04016; always @(posedge CLK2) begin @@ -602,8 +653,19 @@ assign SNES_DATA = (r213f_enable & ~SNES_PARD & ~r213f_forceread) ? r213fr ( msu_enable ? MSU_SNES_DATA_OUT :(cheat_hit & ~feat_cmd_unlock) ? cheat_data_out :((snescmd_unlock | feat_cmd_unlock) & snescmd_enable) ? snescmd_dout + // in-game savestate register shadow: $F90500 (PPU, stride-2) / + // $F90700 (CPU $42xx) read-backs. A stride-2 PPU entry is a PAIR, + // not a duplicate: even byte = 1st write (prev), odd byte = 2nd + // write (current), so the double-writing restore loop replays + // scroll/mode-7 in the right order (ctx.v-style, see regshadow.v). + // Single-write regs store (value, value), so the high byte is never + // $00 (that zeroed BGMODE/TM/INIDISP -> backdrop-only screen, hw). + :shadow_ppu_hit ? regshadow_dout + :shadow_cpu_hit ? regshadow_dout // RG S-DD1 will drive data on normal ROM and RAM reads, during a decompression DMA, and when a $480X register is read. - :(sdd1_enable & (~SDD1_RAM_CE | ~SDD1_ROM_CE | FSM_DMA_Transferring | sdd1_reg_enable)) ? SDD1_SNES_DATA_OUT + // gated by ~IS_PATCH: yield to the hook identity window so the handler + // reads PSRAM ($C0 code / $F9 shadows), not decompressed S-DD1 output. + :(~IS_PATCH & sdd1_enable & (~SDD1_RAM_CE | ~SDD1_ROM_CE | FSM_DMA_Transferring | sdd1_reg_enable)) ? SDD1_SNES_DATA_OUT :(ROM_ADDR0 ? ROM_DATA[7:0] : ROM_DATA[15:8])) : 8'bZ; @@ -634,8 +696,8 @@ DCM_Scope snes_dcm( ); assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:1] : MCU_HIT ? ROM_ADDRr[23:1] // keep MCU above sdd1 to allow it to use the free slot during normal SNES accesses - : (sdd1_enable & ~SDD1_ROM_CE)?({1'b0, SDD1_ROM_ADDR} & ROM_MASK[23:1]) - : (sdd1_enable & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[23:1] + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE)?({1'b0, SDD1_ROM_ADDR} & ROM_MASK[23:1]) + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[23:1] : MAPPED_SNES_ADDR[23:1]; @@ -666,14 +728,14 @@ pll snes_pll( assign ROM_ADDR22 = (SD_DMA_TO_ROM) ? MCU_ADDR[1] : MCU_HIT ? ROM_ADDRr[1] // keep MCU above sdd1 to allow it to use the free slot during normal SNES accesses - : (sdd1_enable & ~SDD1_ROM_CE)?SDD1_ROM_ADDR[0] // SDD1_ROM_ADDR is a word address! - : (sdd1_enable & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[1] + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE)?SDD1_ROM_ADDR[0] // SDD1_ROM_ADDR is a word address! + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[1] : MAPPED_SNES_ADDR[1]; assign ROM_ADDR = (SD_DMA_TO_ROM) ? MCU_ADDR[23:2] : MCU_HIT ? ROM_ADDRr[23:2] // keep MCU above sdd1 to allow it to use the free slot during normal SNES accesses - : (sdd1_enable & ~SDD1_ROM_CE)?({1'b0, SDD1_ROM_ADDR[21:1] & ROM_MASK[22:2]}) - : (sdd1_enable & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[23:2] + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE)?({1'b0, SDD1_ROM_ADDR[21:1] & ROM_MASK[22:2]}) + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE)?SDD1_RAM_ADDR[23:2] : MAPPED_SNES_ADDR[23:2]; assign ROM_ZZ = 1'b1; @@ -699,8 +761,8 @@ assign ROM_OE = 1'b0; // lower address bit to select [7:0] (ROM_ADDR0 = '1') or [15:8] (ROM_ADDR0 = '0') byte in the 16-bit word read from PSRAM assign ROM_ADDR0 = (SD_DMA_TO_ROM) ? MCU_ADDR[0] : MCU_HIT ? ROM_ADDRr[0] // keep MCU above sdd1 to allow it to use the free slot during normal SNES accesses - : (sdd1_enable & ~SDD1_ROM_CE) ? 1'b0 - : (sdd1_enable & ~SDD1_RAM_CE) ? SDD1_RAM_ADDR[0] + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE) ? 1'b0 + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE) ? SDD1_RAM_ADDR[0] : MAPPED_SNES_ADDR[0]; reg[17:0] SNES_DEAD_CNTr; @@ -851,8 +913,8 @@ assign ROM_DATA[7:0] = ROM_ADDR0 ? : MCU_WR_HIT ? MCU_DOUT // if S-DD1 is present, only writes to PSRAM if game is storing in backup SRAM; // if reading from PSRAM, the bus is tri-state - : (sdd1_enable & ~SDD1_RAM_CE) ? ((~SDD1_RAM_WE) ? SNES_DATA : 8'bZ ) - : (sdd1_enable & ~SDD1_ROM_CE) ? 8'bZ + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE) ? ((~SDD1_RAM_WE) ? SNES_DATA : 8'bZ ) + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE) ? 8'bZ // if writing to ROM, backup RAM or BS-X RAM (all stored in PSRAM) : (ROM_HIT & ~SNES_WRITE) ? SNES_DATA : 8'bZ ) @@ -865,8 +927,8 @@ assign ROM_DATA[15:8] = ROM_ADDR0 ? 8'bZ : MCU_WR_HIT ? MCU_DOUT // if S-DD1 is present, only writes to PSRAM if game is storing in backup SRAM // if reading from PSRAM, the bus is tri-state - : (sdd1_enable & ~SDD1_RAM_CE) ? ((~SDD1_RAM_WE) ? SNES_DATA : 8'bZ ) - : (sdd1_enable & ~SDD1_ROM_CE) ? 8'bZ + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE) ? ((~SDD1_RAM_WE) ? SNES_DATA : 8'bZ ) + : (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE) ? 8'bZ // if writing to ROM, backup RAM or BS-X RAM (all stored in PSRAM) : (ROM_HIT & ~SNES_WRITE) ? SNES_DATA : 8'bZ ); @@ -875,15 +937,15 @@ assign ROM_DATA[15:8] = ROM_ADDR0 ? 8'bZ // write enable for PSRAM; for S-DD1, enabled when accessing backup SRAM for writing assign ROM_WE = SD_DMA_TO_ROM ? MCU_WRITE : MCU_WE_HIT ? 1'b0 - : (sdd1_enable & ~SDD1_RAM_CE & SNES_CPU_CLK) ? SDD1_RAM_WE + : (sdd1_enable & ~IS_PATCH & ~SDD1_RAM_CE & SNES_CPU_CLK) ? SDD1_RAM_WE : (ROM_HIT & IS_WRITABLE & SNES_CPU_CLK) ? SNES_WRITE : 1'b1; // byte selector for PSRAM output; when S-DD1 is reading from ROM (PSRAM), access is 16bit wide // '0' when accessing high byte -assign ROM_BHE = (sdd1_enable & ~SDD1_ROM_CE & ~MCU_HIT)?1'b0:ROM_ADDR0; +assign ROM_BHE = (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE & ~MCU_HIT)?1'b0:ROM_ADDR0; // '0' when accessing low byte -assign ROM_BLE = (sdd1_enable & ~SDD1_ROM_CE & ~MCU_HIT)?1'b0:!ROM_ADDR0; +assign ROM_BLE = (sdd1_enable & ~IS_PATCH & ~SDD1_ROM_CE & ~MCU_HIT)?1'b0:!ROM_ADDR0; // active low signal to enable level converters' output; it enables output in both sides of the chip assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : @@ -891,6 +953,11 @@ assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : (sdd1_reg_enable | (sdd1_snoop_enable & ~SNES_WRITE)) ? 1'b0 : (r213f_enable & ~SNES_PARD) ? 1'b0 : (r2100_enable & ~SNES_PAWR) ? 1'b0 : + // regshadow write snoop: enable the shifter (receive) during + // PPU B-bus writes and $42xx A-bus writes, else the snoop + // reads float (base does this via SNES_SNOOPPAWR_DATA_OE). + rs_snoop_pawr_oe ? 1'b0 : + (snoop_42xx_enable & ~SNES_WRITE) ? 1'b0 : snoop_4200_enable ? SNES_WRITE : ((IS_ROM & SNES_ROMSEL) | (!IS_ROM & !IS_SAVERAM & !IS_WRITABLE) | (SNES_READ_narrow & SNES_WRITE) ); @@ -900,7 +967,11 @@ assign SNES_DATABUS_OE = msu_enable & ~(SNES_READ_narrow & SNES_WRITE) ? 1'b0 : * a) the SNES wants to read * b) we want to force a value on the bus */ -assign SNES_DATABUS_DIR = (~SNES_READ | (~SNES_PARD & (r213f_enable))) ? +// During a snooped B-bus write the concurrent A-bus read (/RD low on DMA/HDMA) must +// not flip the shifter to drive, unless the FPGA serves that source itself: ROM and +// PSRAM via ROM_HIT, plus the chip's own region. Missing the latter makes those +// DMAs read float. +assign SNES_DATABUS_DIR = ((~SNES_READ & (~rs_snoop_pawr_oe | ROM_HIT | (~IS_PATCH & sdd1_enable & (~SDD1_RAM_CE | ~SDD1_ROM_CE | FSM_DMA_Transferring | sdd1_reg_enable)))) | (~SNES_PARD & (r213f_enable))) ? (1'b1 ^ (r213f_forceread & r213f_enable & ~SNES_PARD) ^ (r2100_enable & ~SNES_PAWR & ~r2100_forcewrite & ~IS_ROM & ~IS_WRITABLE)) : ((~SNES_PAWR & r2100_enable) ? r2100_forcewrite diff --git a/verilog/sd2snes_sdd1/regshadow.v b/verilog/sd2snes_sdd1/regshadow.v new file mode 100644 index 00000000..573efad2 --- /dev/null +++ b/verilog/sd2snes_sdd1/regshadow.v @@ -0,0 +1,114 @@ +`timescale 1 ns / 1 ns +////////////////////////////////////////////////////////////////////////////////// +// Company: sd2snes +// Module Name: regshadow +// Description: +// Write-only shadow of the PPU ($2100-$213F) and CPU ($4200-$421F) registers, +// read back by the in-game savestate handler through the hook window: +// $F90500-$F9057F : PPU regs, stride-2 (1st write, 2nd write) +// $F90700-$F9071F : CPU regs, stride-1 +// base/DSP/SA-1 get this from ctx.v, which does not fit the mk2 Spartan-3. +// +// The scroll ($210D-$2114) and mode-7 ($211B-$2120) registers latch 16 bits from +// two consecutive writes, so the pair is stored, not just the last byte (ctx.v +// does the same via rBG/rM7). Non-double regs store (value, value). +// +// Storage (256x8, one RAMB16): +// mem[$00-$7F] PPU pairs, mem[{PA,1'b0}] = 1st write, mem[{PA,1'b1}] = 2nd +// mem[$80-$9F] CPU regs +// REGSHADOW_1DEEP selects the older layout (PPU mem[$00-$3F], CPU mem[$40-$5F]); +// main.v indexes both under the same macro. +// +// Compile gates, mutually exclusive, for mk2 area/timing: +// REGSHADOW_NO_M7 drop the mode-7 tracker, keep the scroll pair (gsu mk2) +// REGSHADOW_1DEEP drop the pair scheme entirely (cx4 mk2) +////////////////////////////////////////////////////////////////////////////////// +module regshadow( + input clk, + input pawr_end, // settled rising edge of /PAWR + input wr_end, // settled rising edge of /WR + input [23:0] snes_addr, + input [7:0] snes_pa, + input [7:0] snes_data, + input [8:0] rd_addr, + output reg [7:0] rd_data +); + +// 1DEEP removes what NO_M7 thins, so defining both is always a mistake and 1DEEP +// would silently win. The bare identifier is illegal Verilog: fail at parse time. +`ifdef REGSHADOW_1DEEP + `ifdef REGSHADOW_NO_M7 + ERROR_REGSHADOW_1DEEP_and_REGSHADOW_NO_M7_are_mutually_exclusive + `endif +`endif + +(* ram_style = "block" *) reg [7:0] mem [0:255]; + +wire ppu_wr = pawr_end & (snes_pa < 8'h40); +// $4200-$421F in any bank with ADDR[22]=0: games write them through FastROM banks, +// so a bank-$00-only decode misses those writes. +wire cpu_wr = wr_end & ~snes_addr[22] + & (snes_addr[15:5] == 11'b01000010000); + +`ifdef REGSHADOW_1DEEP +wire wr_en = ppu_wr | cpu_wr; +wire [7:0] wr_a = ppu_wr ? {2'b00, snes_pa[5:0]} + : {3'b010, snes_addr[4:0]}; + +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= snes_data; + rd_data <= mem[rd_addr[7:0]]; +end +`else +// Previous-byte trackers (ctx.v's rBG/rM7): consumed before being updated. +reg [7:0] prev_bg; initial prev_bg = 0; +`ifndef REGSHADOW_NO_M7 +reg [7:0] prev_m7; initial prev_m7 = 0; +`endif +wire is_bg_dbl = (snes_pa >= 8'h0D) && (snes_pa <= 8'h14); +`ifndef REGSHADOW_NO_M7 +wire is_m7_dbl = ((snes_pa >= 8'h0D) && (snes_pa <= 8'h0E)) + || ((snes_pa >= 8'h1B) && (snes_pa <= 8'h20)); +`endif + +// The strobe cycle stores the current byte (odd offset), the next cycle stores the +// previous one (even offset) out of these defer flops. Bus writes are dozens of +// clocks apart, so the defer can never collide with the next write. +reg wr2_pend; initial wr2_pend = 0; +reg [7:0] wr2_a; initial wr2_a = 0; +reg [7:0] wr2_d; initial wr2_d = 0; + +wire wr_en = ppu_wr | cpu_wr | wr2_pend; +wire [7:0] wr_a = wr2_pend ? wr2_a + : ppu_wr ? {1'b0, snes_pa[5:0], 1'b1} + : {3'b100, snes_addr[4:0]}; +wire [7:0] wr_d = wr2_pend ? wr2_d : snes_data; + +// One write enable and one write address in the process: anything else falls out +// of the XST block-RAM template and the memory is built from flip-flops instead. +// The arm is edge-guarded so a strobe wider than one cycle cannot re-arm with the +// trackers already updated, which would degrade the pair back to (value, value). +always @(posedge clk) begin + if (wr_en) + mem[wr_a] <= wr_d; + rd_data <= mem[rd_addr[7:0]]; + if (ppu_wr & ~wr2_pend) begin + wr2_pend <= 1'b1; + wr2_a <= {1'b0, snes_pa[5:0], 1'b0}; +`ifndef REGSHADOW_NO_M7 + wr2_d <= is_bg_dbl ? prev_bg : is_m7_dbl ? prev_m7 : snes_data; +`else + wr2_d <= is_bg_dbl ? prev_bg : snes_data; +`endif + if (is_bg_dbl) prev_bg <= snes_data; +`ifndef REGSHADOW_NO_M7 + if (is_m7_dbl) prev_m7 <= snes_data; +`endif + end else if (~ppu_wr) begin + wr2_pend <= 1'b0; + end +end +`endif + +endmodule diff --git a/verilog/sd2snes_sdd1/sd2snes_sdd1.xise b/verilog/sd2snes_sdd1/sd2snes_sdd1.xise index 7f554d4e..bc8b4230 100644 --- a/verilog/sd2snes_sdd1/sd2snes_sdd1.xise +++ b/verilog/sd2snes_sdd1/sd2snes_sdd1.xise @@ -19,6 +19,10 @@ + + + +