gbdk-2020 | GameBoy Development Kit |
| download: https://git.y1.nz/archives/gbdk.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 5acc7cfbaedea25ae33c6fd6344735fefcf424d2 parent b1e1dc04a24ea1e8f981c6c8b220d2372bff850e Author: Michel Iwaniec <46843052+michel-iwaniec@users.noreply.github.com> Date: Tue, 6 May 2025 11:40:17 +0100 NES: Improve TIMER interrupt emulation (#774) - Move TIM ISR emulation to dedicated function .tim_emulation and dedicated file timer_isr.s - Make TIM ISR emulation save entirety of ALL_REGTEMPS_BEGIN to ALL_REGTEMPS_END - Improve emulation to allow average TIM rate to be faster compared to vblank rate - Add support for consistent average TIM rate on PAL, for a less system-dependent timer - Add support for different base dividers defined by GB-like TAC_REG variable - Add defines values for "vblank parity mode" to global.s and nes.h - Update crt0 init code to set TMA_REG to vblank parity mode correctly for PAL/Dendy - Update documentation, describing the improved GB TIM emulation and vblank parity mode - Update cross-platform IRQ example to better showcase the emulation Diffstat:
| M | docs/pages/06b_supported_consoles.md | 17 | ++++++++++++++++- |
| M | gbdk-lib/examples/cross-platform/irq/src/irq.c | 30 | +++++++++++++++++++++++------- |
| M | gbdk-lib/include/nes/nes.h | 3 | +++ |
| M | gbdk-lib/libc/targets/mos6502/nes/Makefile | 2 | +- |
| M | gbdk-lib/libc/targets/mos6502/nes/crt0.s | 47 | +++++++++-------------------------------------- |
| M | gbdk-lib/libc/targets/mos6502/nes/global.s | 4 | ++++ |
| A | gbdk-lib/libc/targets/mos6502/nes/timer_isr.s | 116 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 172 insertions(+), 47 deletions(-)
diff --git a/docs/pages/06b_supported_consoles.md b/docs/pages/06b_supported_consoles.md @@ -515,12 +515,27 @@ For graphics updates this is the behaviour you usually want. But for non-graphic 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). +The TMA_REG and TAC_REG hardware registers are emulated via RAM variable with the same names. The timer emulation matches the values of these registers for a GBC running in double-speed mode. But there will be a small variation in exact frequency compared to real GBC hardware, and the nature of the timer emulation via vblank means the execution is not as evenly paced as on GBC hardware. + +The TIMA_REG should not be written or read in gbdk-nes, as the emulation does not handle its contents exactly as on GB. + +At reset, TAC_REG is set to clock rate 00 and timer enabled, and TMA_REG is set to either of two values, depending on the detected system: + +* TIMER_VBLANK_PARITY_MODE_SYSTEM_60HZ for a Famicom / US NES +* TIMER_VBLANK_PARITY_MODE_SYSTEM_50Hz for a PAL NES / PAL Famiclone + +These default values ensure that the TIM emulation will always call the TIM handler exactly once for every vblank, resulting in 60Hz vs 50Hz depending on the system. + +Changing TMA_REG allows setting a slower or faster frequency for this emulated timer overflow interrupt if your GB game uses the timer overflow hardware for regular events like music that are slower or faster 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. +Tweaking the playback rate by setting TMA_REG / TAC_REG is a decent way to achieve the same average playback rate as on a GB game that uses a different rate than the vblank tick rate and allow similar music speed for different regions. + +However, it is recommended to use the default vblank parity mode whenever remaking the music specifically for 60Hz / 50Hz is an option, as keeping the music tick rate steady will give more pleasant sound playback for rates that are already close to native vblank rate of 60Hz / 50Hz. + @anchor docs_nes_tim_overlay @note Because the TIM handler will be called from the vblank NMI, this function and all functions it calls need to use the `#pragma nooverlay` command. This makes memory for local variables and function parameters unique to this function instead of being shared with other functions' allocations in the reusable overlay segment. diff --git a/gbdk-lib/examples/cross-platform/irq/src/irq.c b/gbdk-lib/examples/cross-platform/irq/src/irq.c @@ -51,6 +51,21 @@ void print_counter(void) void main(void) { + uint8_t double_speed_mode = 0; +#if defined(NINTENDO) + set_default_palette(); + if (_cpu == CGB_TYPE) { + // Use double-speed mode for GBC + cpu_fast(); + double_speed_mode = 1; + } +#endif + +#if defined(NINTENDO_NES) + // NES TIM emulation is based on GBC's double-speed mode + double_speed_mode = 1; +#endif + // Ensure mutual exclusion CRITICAL { vbl_cnt = tim_cnt = 0; @@ -58,12 +73,12 @@ void main(void) add_TIM(tim); } -#if defined(NINTENDO) - // Set TMA to divide clock by 0x100 - TMA_REG = 0x00U; - // Set clock to 4096 Hertz +#if defined(NINTENDO) || defined(NINTENDO_NES) + // Set TMA to divide clock by 0x80 (0x100 for GBC) + TMA_REG = double_speed_mode ? 0x00U : 0x80; + // Set clock to 4096 Hertz (8192 Hertz for GBC) TAC_REG = 0x04U; -#elif defined(SEGA) || defined(NINTENDO_NES) +#elif defined(SEGA) TMA_REG = 0xFCU; #endif @@ -71,9 +86,10 @@ void main(void) set_interrupts(VBL_IFLAG | TIM_IFLAG); for(;;) { + uint8_t num_frames = (get_system() == SYSTEM_50HZ) ? 50 : 60; print_counter(); - // Loop for 60 frames (1 second) - for(int i = 0; i < 60; i++) { + // Loop for 1 second + for(int i = 0; i < num_frames; i++) { vsync(); } } diff --git a/gbdk-lib/include/nes/nes.h b/gbdk-lib/include/nes/nes.h @@ -33,6 +33,9 @@ extern const uint8_t _SYSTEM; #define SYSTEM_60HZ 0x00 #define SYSTEM_50HZ 0x01 +#define TIMER_VBLANK_PARITY_MODE_SYSTEM_60HZ 0x78 +#define TIMER_VBLANK_PARITY_MODE_SYSTEM_50HZ 0x5D + #define RGB(r,g,b) RGB_TO_NES(((r) | ((g) << 2) | ((b) << 4))) #define RGB8(r,g,b) RGB_TO_NES((((r) >> 6) | (((g) >> 6) << 2) | (((b) >> 6) << 4))) #define RGBHTML(RGB24bit) RGB_TO_NES((((RGB24bit) >> 22) | ((((RGB24bit) & 0xFFFF) >> 14) << 2) | ((((RGB24bit) & 0xFF) >> 6) << 4))) diff --git a/gbdk-lib/libc/targets/mos6502/nes/Makefile b/gbdk-lib/libc/targets/mos6502/nes/Makefile @@ -20,7 +20,7 @@ ASSRC = f_ibm_full.s f_ibm_sh.s f_italic.s f_min.s f_spect.s \ delay.s \ rle_decompress.s \ far_ptr.s sdcc_bcall.s mapper.s \ - lcd.s deferred_isr.s \ + lcd.s deferred_isr.s timer_isr.s \ crt0.s CRT0 = crt0.s diff --git a/gbdk-lib/libc/targets/mos6502/nes/crt0.s b/gbdk-lib/libc/targets/mos6502/nes/crt0.s @@ -76,14 +76,6 @@ __hblank_writes_index:: .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 @@ -93,7 +85,6 @@ __lcd_isr_scroll_x:: .ds (2*.MAX_DEFERRED_ISR_CALLS) __lcd_isr_scroll_y:: .ds (2*.MAX_DEFERRED_ISR_CALLS) __lcd_isr_delay_num_scanlines:: .ds (2*.MAX_DEFERRED_ISR_CALLS) __lcd_isr_ppuaddr_lo:: .ds (2*.MAX_DEFERRED_ISR_CALLS) -_TAC_REG:: .ds 1 ; Unused, for GB compatibility .area _CODE @@ -291,35 +282,8 @@ __crt0_NMI: jsr .do_hblank_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$ - ; Skip if disabled (check for RTS) - lda .jmp_to_TIM_isr - cmp #0x60 - beq 6$ - ; Save REGTEMP to stack - ldx #7 -4$: - lda *REGTEMP,x - pha - dex - bpl 4$ - ; Call handler - jsr .jmp_to_TIM_isr - ; Restore REGTEMP from stack - ldx #0 -5$: - pla - sta *REGTEMP,x - inx - cpx #8 - bne 5$ - ; -6$: - lda _TMA_REG - sta _TIMA_REG -3$: + + jsr .tim_emulation ; Update frame counter lda *_sys_time @@ -570,6 +534,13 @@ __crt0_RESET_bankSwitchValue: ldx #>s__DATA jsr ___memcpy + ; For a PAL / Dendy system, override _TMA_REG to be correctly initialized for a 50Hz vblank rate + lda *__SYSTEM + beq 1$ + lda #.TIMER_VBLANK_PARITY_MODE_SYSTEM_50HZ + sta _TMA_REG +1$: + ; Set bank to first lda #0x00 sta *__current_bank diff --git a/gbdk-lib/libc/targets/mos6502/nes/global.s b/gbdk-lib/libc/targets/mos6502/nes/global.s @@ -113,6 +113,10 @@ ;; Table of routines for modes .MODE_TABLE = 0xFFE0 + ;; Values for vblank parity mode when using timer emulation + .TIMER_VBLANK_PARITY_MODE_SYSTEM_60HZ = 0x78 + .TIMER_VBLANK_PARITY_MODE_SYSTEM_50HZ = 0x5D + ;; C related ;; Overheap of a banked call. Used for parameters ;; = ret + real ret + bank diff --git a/gbdk-lib/libc/targets/mos6502/nes/timer_isr.s b/gbdk-lib/libc/targets/mos6502/nes/timer_isr.s @@ -0,0 +1,116 @@ +; +; Emulation of GB's timer interrupt (TIM) via calls from NES's 60Hz / 50Hz vblank NMI +; +; Timer values are based on GBC running in double-speed mode +; + +.include "global.s" + +.area _DATA +_TIMA_REG:: .ds 2 +_TMA_REG:: .ds 1 +_TAC_REG:: .ds 1 + +.area _XINIT +.db 0x00 +.db 0x00 +.db .TIMER_VBLANK_PARITY_MODE_SYSTEM_60HZ +.db 0x04 + +.area _HOME + +; Lookup tables for adjusted rates based on GB TAC register +.TAC_lookup_lo: +; PAL +.db <163 ; 8192 / 50 = 163.84 ~= 164 +.db <10486 ; 524288 / 50 = 10485.76 ~= 10486 +.db <2621 ; 131072 / 50 = 2621.44 ~= 2621 +.db <655 ; 32768 / 50 = 655.36 ~= 655 +; NTSC +.db <136 ; 8192 / 60 = 136.5333... ~= 136 +.db <8738 ; 524288 / 60 = 8738.1333... ~= 8378 +.db <2184 ; 131072 / 60 = 2184.5333... ~= 2184 +.db <546 ; 32768 / 60 = 546.1333... ~= 546 +; +.TAC_lookup_hi: +; PAL +.db >163 ; 8192 / 50 = 163.84 ~= 164 +.db >10486 ; 524288 / 50 = 10485.76 ~= 10486 +.db >2621 ; 131072 / 50 = 2621.44 ~= 2621 +.db >655 ; 32768 / 50 = 655.36 ~= 655 +; NTSC +.db >136 ; 8192 / 60 = 136.5333... ~= 136 +.db >8738 ; 524288 / 60 = 8738.1333... ~= 8378 +.db >2184 ; 131072 / 60 = 2184.5333... ~= 2184 +.db >546 ; 32768 / 60 = 546.1333... ~= 546 + +; +; Call timer interrupt repeatedly to emulate desired GB playback rate via vblank NMI. +; +.tim_emulation:: + lda _TAC_REG + and #7 + ; Early-out if timer enabled bit not set + cmp #4 + bcs 1$ + rts +1$: + and #3 + bit *__SYSTEM + bmi .tim_emulation_dendy + bvs .tim_emulation_pal + ; +4 to index NTSC table + ora #0x04 +.tim_emulation_dendy: +.tim_emulation_pal: + pha ; Save TAC lookup index to stack +.tim_emulation_loop: + ; Finished when TIMA_REG <= 0 + bit _TIMA_REG+1 + bmi .tim_emulation_calls_done + lda _TIMA_REG + beq .tim_emulation_calls_done + ; Skip if handler disabled (check for RTS) + lda .jmp_to_TIM_isr + cmp #0x60 + beq .tim_emulation_isr_disabled + ; Save REGTEMP to stack + ldx #17 +4$: + lda *ALL_REGTEMPS_BEGIN,x + pha + dex + bpl 4$ + ; Call handler + jsr .jmp_to_TIM_isr + ; Restore REGTEMP from stack + ldx #0 +5$: + pla + sta *ALL_REGTEMPS_BEGIN,x + inx + cpx #18 + bne 5$ +.tim_emulation_isr_disabled: + ; TIMA_REG += TMA_REG + lda _TIMA_REG + clc + adc _TMA_REG + sta _TIMA_REG + lda _TIMA_REG+1 + adc #0xFF + sta _TIMA_REG+1 + jmp .tim_emulation_loop +; +.tim_emulation_calls_done: + pla ; Restore TAC lookup index from stack + tay + ; TIMA_REG += initial value + lda _TIMA_REG + clc + adc .TAC_lookup_lo,y + sta _TIMA_REG + lda _TIMA_REG+1 + adc .TAC_lookup_hi,y + sta _TIMA_REG+1 + 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.