gbdk-2020 | GameBoy Development Kit |
| download: https://git.y1.nz/archives/gbdk.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 2997e027c606f2254f433b0d3283d4d03dbbc8a1 parent 3fb3b2046e9109af9c7842b70aeb1d2c52e413e3 Author: Toxa <56631470+untoxa@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:17:34 +0300 Merge pull request #747 from michel-iwaniec/nes_add_TIM_support-v2 NES: Timer interrupt emulation + support for set_interrupts(...) Diffstat:
| M | docs/pages/06b_supported_consoles.md | 18 | ++++++++++++++++++ |
| M | gbdk-lib/examples/cross-platform/irq/Makefile | 2 | +- |
| M | gbdk-lib/examples/cross-platform/irq/src/irq.c | 7 | +++++-- |
| M | gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c | 2 | +- |
| M | gbdk-lib/include/nes/hardware.h | 4 | ++++ |
| M | gbdk-lib/include/nes/nes.h | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
| M | gbdk-lib/libc/targets/mos6502/nes/crt0.s | 23 | +++++++++++++++++++---- |
| M | gbdk-lib/libc/targets/mos6502/nes/global.s | 5 | +++++ |
| M | gbdk-lib/libc/targets/mos6502/nes/lcd.s | 102 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- |
9 files changed, 175 insertions(+), 34 deletions(-)
diff --git a/docs/pages/06b_supported_consoles.md b/docs/pages/06b_supported_consoles.md @@ -438,6 +438,10 @@ During direct mode, all graphics routines will write directly to the PPUADDR / P Direct mode is typically used for initializing large amounts of tile data at boot and/or level loading time. Unless you plan to have an animated loading screen and decompress a lot of data, it makes more sense to just fade the screen to black and allow direct mode to write data as fast as possible. +Direct mode also affects how (fake) interrupt handlers are processed. As long as vsync() is called on each frame, the VBL and LCD handlers will still be executed in direct mode - but no graphics registers will be written. + +The TIM handler will still be executed as normal. + #### Caveat: Make sure the transfer buffer is emptied before switching to direct mode Because the switch to direct mode is instant and doesn't wait for the next invocation of the vblank, it is possible to create situations where there is still remaining data in the transfer buffer that would only get written once the system is switched back to buffered mode. @@ -507,6 +511,20 @@ If you are using LCD handlers to achieve a top-screen stationary status bar, it In short: Ensuring that the last called LCD handler sets the scroll back to the original value means the PPU rendering keeps rendering the background from the same scrolling position even when the NMI handling was missed. +### Implementation of timer handler + +The nature of the deferred handling for fake VBL and LCD handlers in gbdk-nes means that lag frames will cause these handlers to be called at delayed irregular times. + +For graphics updates this is the behaviour you usually want. But for non-graphics tasks like music playback it will cause distracting stutter. + +The timer overflow handler (TIM) provides an alternative method that is guaranteed to be stutter-free. The TIM handler is always called if timer overflow occurs at the end of the NMI handler in both buffered and direct mode. + +The TMA_REG hardware register is emulated via a RAM variable with the same name. TMA_REG is set to 0xFF at reset, corresponding to 60Hz for a Famicom / US NES, and 50Hz for a PAL NES / PAL Famiclone. Changing TMA_REG allows setting a slower frequency for this emulated timer overflow interrupt if your GB game uses the timer overflow hardware for regular events like music that are slower than 60Hz (50Hz for PAL). + +If you are porting a GB game to gbdk-nes where the music handler is called in the VBL or LCD handler then it is advisable to move this call to the timer handler, in order to achieve reliable music playback at 60Hz (50Hz for PAL). + +Keep in mind that you should NOT write any graphics registers in the TIM handler. This will likely not do what you want, and it may result in bad graphical glitches. + ### Tile Data and Tile Map loading #### Tile and Map Data in 2bpp Game Boy Format diff --git a/gbdk-lib/examples/cross-platform/irq/Makefile b/gbdk-lib/examples/cross-platform/irq/Makefile @@ -9,7 +9,7 @@ LCC = $(GBDK_HOME)bin/lcc # Set platforms to build here, spaced separated. (These are in the separate Makefile.targets) # They can also be built/cleaned individually: "make gg" and "make gg-clean" # Possible are: gb gbc pocket megaduck sms gg -TARGETS=gb sms +TARGETS=gb sms nes # Configure platform specific LCC flags here: LCCFLAGS_gb = -Wl-yt0x1B # Set an MBC for banking (1B-ROM+MBC5+RAM+BATT) diff --git a/gbdk-lib/examples/cross-platform/irq/src/irq.c b/gbdk-lib/examples/cross-platform/irq/src/irq.c @@ -54,7 +54,7 @@ void main(void) TMA_REG = 0x00U; // Set clock to 4096 Hertz TAC_REG = 0x04U; -#elif defined(SEGA) +#elif defined(SEGA) || defined(NINTENDO_NES) TMA_REG = 0xFCU; #endif @@ -63,6 +63,9 @@ void main(void) for(;;) { print_counter(); - delay(1000UL); + // Loop for 60 frames (1 second) + for(int i = 0; i < 60; i++) { + vsync(); + } } } diff --git a/gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c b/gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c @@ -83,7 +83,7 @@ void main(void) { #if defined(SEGA) __WRITE_VDP_REG(VDP_R10, 0x07); #endif -#if defined(NINTENDO) || defined(SEGA) +#if defined(NINTENDO) || defined(NINTENDO_NES) || defined(SEGA) set_interrupts(VBL_IFLAG | LCD_IFLAG); #endif HIDE_LEFT_COLUMN; diff --git a/gbdk-lib/include/nes/hardware.h b/gbdk-lib/include/nes/hardware.h @@ -56,4 +56,8 @@ __REG(0x4014) OAMDMA; __SHADOW_REG bkg_scroll_x; __SHADOW_REG bkg_scroll_y; +extern volatile UBYTE TIMA_REG; +extern volatile UBYTE TMA_REG; +extern volatile UBYTE TAC_REG; + #endif diff --git a/gbdk-lib/include/nes/nes.h b/gbdk-lib/include/nes/nes.h @@ -132,6 +132,25 @@ void set_sprite_palette_entry(uint8_t palette, uint8_t entry, palette_color_t rg */ #define S_PAL(n) n +/* Interrupt flags */ +/** Disable calling of interrupt service routines + */ +#define EMPTY_IFLAG 0x00U +/** VBlank Interrupt occurs at the start of the vertical blank. + + During this period the video ram may be freely accessed. + @see set_interrupts(), @see add_VBL + */ +#define VBL_IFLAG 0x01U +/** LCD Interrupt when triggered by the STAT register. + @see set_interrupts(), @see add_LCD +*/ +#define LCD_IFLAG 0x02U +/** Timer Interrupt when the timer @ref TIMA_REG overflows. + @see set_interrupts(), @see add_TIM + */ +#define TIM_IFLAG 0x04U + /* DMG Palettes */ #define DMG_BLACK 0x03 #define DMG_DARK_GRAY 0x02 @@ -184,6 +203,11 @@ void remove_VBL(int_handler h) NO_OVERLAY_LOCALS; */ void remove_LCD(int_handler h) NO_OVERLAY_LOCALS; +/** Removes the TIM interrupt handler. + @see add_TIM(), remove_VBL() +*/ +void remove_TIM(int_handler h) NO_OVERLAY_LOCALS; + /** Adds a Vertical Blanking interrupt handler. @param h The handler to be called whenever a V-blank @@ -248,6 +272,21 @@ void add_VBL(int_handler h) NO_OVERLAY_LOCALS; */ void add_LCD(int_handler h) NO_OVERLAY_LOCALS; +/** Adds a timer interrupt handler. + + Can not be used together with @ref add_low_priority_TIM + + This interrupt handler is invoked at the end of the NMI handler + for gbdk-nes, after first processing the registers writes done + by the VBL and and LCD handlers. + It is therefore currently limited to 60Hz / 50Hz + (depending on system). + + @see add_VBL + @see set_interrupts() with TIM_IFLAG, ISR_VECTOR() +*/ +void add_TIM(int_handler h) NO_OVERLAY_LOCALS; + /** The maximum number of times the LCD handler will be called per frame. */ #define MAX_LCD_ISR_CALLS 4 @@ -467,6 +506,13 @@ inline void disable_interrupts(void) { __asm__("sei"); } +/** Sets the interrupt mask to flags. + @param flags A logical OR of *_IFLAGS + + @see VBL_IFLAG, LCD_IFLAG, TIM_IFLAG +*/ +void set_interrupts(uint8_t flags) NO_OVERLAY_LOCALS; + /** Performs a soft reset. For the Game Boy and related it does this by jumping to address 0x0150 diff --git a/gbdk-lib/libc/targets/mos6502/nes/crt0.s b/gbdk-lib/libc/targets/mos6502/nes/crt0.s @@ -77,6 +77,14 @@ __SYSTEM:: .ds 1 .define __crt0_NMITEMP "___SDCC_m6502_ret4" +.area _DATA +_TIMA_REG:: .ds 1 +_TMA_REG:: .ds 1 + +.area _XINIT +.db 0xFF +.db 0xFF + .area _BSS __crt0_paletteShadow:: .ds 25 .mode:: .ds 1 @@ -86,6 +94,7 @@ __lcd_isr_scroll_x: .ds .MAX_LCD_ISR_CALLS __lcd_isr_scroll_y: .ds .MAX_LCD_ISR_CALLS __lcd_isr_delay_num_scanlines: .ds .MAX_LCD_ISR_CALLS __lcd_isr_num_calls: .ds 1 +_TAC_REG:: .ds 1 ; Unused, for GB compatibility .area _CODE @@ -215,11 +224,9 @@ ProcessDrawList: sta *.acc ; -> 13.666 NTSC cycles / 13.5625 PAL cycles rts ; -> 6 cycles for RTS, 6 cycles for JSR = 12 cycles -__crt0_NMI_earlyout: - rti __crt0_NMI: bit *__crt0_disableNMI - bmi __crt0_NMI_earlyout + bmi __crt0_NMI_skip pha txa pha @@ -251,7 +258,7 @@ __crt0_NMI: lda *_shadow_PPUMASK sta PPUMASK - ; Call fake LCD isr if present (0x60 = RTS means no LCD) and + ; Call fake LCD isr if present (0x60 = RTS means no LCD) lda .jmp_to_LCD_isr cmp #0x60 beq __crt0_NMI_skip @@ -279,6 +286,14 @@ __crt0_NMI: jsr .do_lcd_ppu_reg_writes __crt0_NMI_skip: + ; Call the timer interrupt for non-graphics events if timer register overflow, and reload TIMA_REG + inc _TIMA_REG + bne 3$ + jsr .jmp_to_TIM_isr + lda _TMA_REG + sta _TIMA_REG +3$: + ; Update frame counter lda *_sys_time clc diff --git a/gbdk-lib/libc/targets/mos6502/nes/global.s b/gbdk-lib/libc/targets/mos6502/nes/global.s @@ -77,6 +77,11 @@ ; JOY2 = 0x4017 + ; Interrupt handler flags + .VBL_IFLAG = 0x01 + .LCD_IFLAG = 0x02 + .TIM_IFLAG = 0x04 + ;; OAM related constants OAM_COUNT = 64 ; number of OAM entries in OAM RAM diff --git a/gbdk-lib/libc/targets/mos6502/nes/lcd.s b/gbdk-lib/libc/targets/mos6502/nes/lcd.s @@ -6,52 +6,102 @@ __lcd_scanline:: .ds 1 .area _DATA -.jmp_to_VBL_isr:: .ds 3 +.jmp_to_xyz_isr:: +.jmp_to_TIM_isr:: .ds 3 .jmp_to_LCD_isr:: .ds 3 +.jmp_to_VBL_isr:: .ds 3 .area _XINIT -; .jmp_to_VBL_isr -rts -nop -nop -; .jmp_to_LCD_isr -rts -nop -nop +; .jmp_to_TIM_isr - default to OFF +.db 0x60 +.dw .rts_instruction +; .jmp_to_LCD_isr - default to OFF +.db 0x60 +.dw .rts_instruction +; .jmp_to_VBL_isr - default to ON +jmp .rts_instruction .area _CODE - -_add_VBL:: -.add_VBL:: +_add_TIM:: +.add_TIM:: ldy #0 beq .add_common _add_LCD:: .add_LCD:: ldy #3 + bne .add_common +_add_VBL:: +.add_VBL:: + ldy #6 .add_common: + pha + stx *.tmp ; Remove old handler - jsr .remove_common + jsr .irq_call_disable ; Store new handler address - sta .jmp_to_VBL_isr+1,y - txa - sta .jmp_to_VBL_isr+2,y - ; Re-enable handler by replacing RTS with JMP instruction - lda #0x4C - sta .jmp_to_VBL_isr,y + pla + sta .jmp_to_xyz_isr+1,y + lda *.tmp + sta .jmp_to_xyz_isr+2,y + jsr .irq_call_restore +.rts_instruction: rts -_remove_VBL:: -.remove_VBL:: +_remove_TIM:: +.remove_TIM:: ldy #0 beq .remove_common _remove_LCD:: .remove_LCD:: ldy #3 + bne .remove_common +_remove_VBL:: +.remove_VBL:: + ldy #6 .remove_common: - pha - ; Replace jump instruction with RTS instruction to disable handler - lda #0x60 - sta .jmp_to_VBL_isr,y - pla + ; Replace handler address with dummy address pointing to RTS instruction + jsr .irq_call_disable + lda .rts_instruction + sta .jmp_to_xyz_isr+1,y + lda .rts_instruction+1 + sta .jmp_to_xyz_isr+2,y + jsr .irq_call_restore .return_instruction: rts + +.irq_call_disable: + ; Temporarily replace JMP instruction with RTS + ldx .jmp_to_xyz_isr,y + lda #0x60 + sta .jmp_to_xyz_isr,y + rts + +.irq_call_restore: + ; Rewrite old instruction (unknown - could be JMP or RTS) + txa + sta .jmp_to_xyz_isr,y + rts + +_set_interrupts:: + ; TIM isr + lsr + ldx #0x60 + bcc 0$ + ldx #0x4C +0$: + stx .jmp_to_VBL_isr + ; LCD isr + lsr + ldx #0x60 + bcc 1$ + ldx #0x4C +1$: + stx .jmp_to_LCD_isr + ; VBL isr + lsr + ldx #0x60 + bcc 2$ + ldx #0x4C +2$: + stx .jmp_to_TIM_isr + rts
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.