Earlier we made an interrupt table which assigned addresses for the SNES interrupts. We assigned the address to an empty handler that just returned for every interrupt except the NMI (VBLANK) interrupt. This interrupt occurs whenever the T.V.'s tracer is not tracing to the screen, making it safe to edit data in VRAM and update the screen. Not waiting for VBLANK can create awful results on the screen, such as flicker. To illustrate how this interrupt works, we'll increment the palette bits of a tile to make it flash different colors. This is a cool effect :).
Notes: You should preserve most of your main registers in your NMI
routine. Also, you really shouldn't do anything not graphic-related; it's a waste of precious time. VBLANK doesn't last forever, so you only have ample time to do what you need to do. It's still plenty of time though, believe me.
The NMI interrupt is disabled at first, so we need to enable it. Enable it through register $4200
:
Register $4200: Counter Enable
n-vh---j n: NMI interrupt enable v: vertical counter enable
h: horizontal counter enable j: joypad enable
We're only conscerned with the n bit, so just write $80
to this register to enable the NMI
.
Register $4210 is also of importance (I think):
Register $4210: NMI Register
x---vvvv: x: NMI V-BLANK flag.
v: Version # ($5A22 (???))
Bit 7 can be reset to 0 by reading this register. When "1" is written to "NMI enable" of register $4200, bit 7 will show NMI status. Status is 0 for "NMI has not occurred" and 1 for "NMI has occurred".
WAI
This instruction waits for an interrupt to occur. Use this at the end of your main routine to wait for VBLANK. This instruction also reduces the processors power consumption (which is handy on portable systems).
Note: In this code, the Stall macro I made WAI
's 7 times. This has the main loop run every 7 VBLANKS. This was my lazy way of slowing the fps down, so normally you would only WAI
once, remember that.
Let's Code!
;------------------------------------------------------------------------
;- Written by: Bazz
;- This code introduces the programmer to the VBLANK interrupt, and
;- teaches him/her its powers. This should be pretty simple & easy
;- to go through.
;------------------------------------------------------------------------
;== Include MemoryMap, HeaderInfo, and interrupt Vector table ==
.INCLUDE "header.inc"
;== Include Library Routines ==
.INCLUDE "InitSNES.asm"
.INCLUDE "LoadGraphics.asm"
;== EQUates ==
.EQU PalNum $0000 ; Use some RAM
;==========================================
; Main Code
;==========================================
.MACRO Stall
.REPT 7
WAI
.ENDR
.ENDM
.BANK 0 SLOT 0
.ORG 0
.SECTION "MainCode"
Start:
InitSNES
rep #$10
sep #$20
stz PalNum
LoadPalette BG_Palette, 0, 14
LoadBlockToVRAM Tiles, $0000, $0020
lda #$80
sta $2115
ldx #$0400
stx $2116
lda #$01
sta $2118
jsr SetupVideo
lda #$80
sta $4200 ; Enable NMI
Infinity:
Stall
lda PalNum
clc
adc #$04
and #$1C ; If palette starting color > 28 (00011100), make 0
sta PalNum
_done:
JMP Infinity
;============================================================================
VBlank:
rep #$10 ; X/Y=16 bits
sep #$20 ; A/mem=8 bit
stz $2115 ; Setup VRAM
ldx #$0400
stx $2116 ; Set VRAM address
lda PalNum
sta $2119 ; Write to VRAM
lda $4210 ; Clear NMI flag
RTI
;============================================================================
;============================================================================
; SetupVideo -- Sets up the video mode and tile-related registers
;----------------------------------------------------------------------------
; In: None
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
SetupVideo:
lda #$00
sta $2105 ; Set Video mode 1, 8x8 tiles, 16 color BG1/2, 4 color BG3
lda #$04 ; Set BG1's Tile Map offset to $0400 (Word address)
sta $2107 ; And the Tile Map size to 32x32
stz $210B ; Set BG1's Character VRAM offset to $0000 (word address)
lda #$01 ; Enable BG1
sta $212C
lda #$0F
sta $2100 ; Turn on screen, full Brightness
rts
.ENDS
;============================================================================
.BANK 1 SLOT 0
.ORG 0
.SECTION "CharacterData"
Tiles:
.DW $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000
.DB $00,$00,$24,$00,$24,$00,$24,$00
.DB $00,$00,$81,$00,$FF,$00,$00,$00
BG_Palette:
.DB $00, $00, $FF, $03
.DW $0000, $0000, $0000
.DB $1F, $00
.DW $0000, $0000, $0000
.DB $E0, $5D
.DW $0000, $0000, $0000
.DB $E0, $02
.ENDS
See how you don't even have to call VBlank.. The interrupt take cares of all that, because we told the SNES where to go when the NMI interrupt occurred back in the header file.
I want to explain some things before this section ends
-
The SetupVideo routine is the same as in the previous tutorial.
-
I'm 98% sure that modification of the Processor Status Register in the interrupt routine stays in the interrupt routine.
-
The palette-changing code:
The high byte of an entry in the tile map uses bits 2-4 as its palette bits. Here's how the palette bits work: the # formed by the palette bits is multiplied by the number of colors in the BG to get the starting index in the color palette. We're in 16 color mode, so if the palette bits were 001, then the tile's colors will range from palette entries 16-31. Thanks to Qwertie for that explanation :).
Now, I had to remember that I was writing to bits 2-4, not 0-2,so I just made bits 0 and 1 0. Since we're only using 1 tile, we won't ever use those 2 bits anyways. So, if the first palette bit is set, we have 100, or 4. if were to go up by one, we then have 1000, or 8. I kept doing this, and found out that the number increases by 4 every time until 28. I created an automatic counter that resets by using the AND instruction. A good man Duo from #gameboy showed me how to do that :). "How does it work?" you ask. I'll tell you.
-
A logical AND takes two values, and compares them in a certain way. If both bits in a certain spot are set, then the result is 1. Any other combination results in an output of 0. Here's an example:
10011011
AND 11001001
10001001
If you set all the bits you want to compare with to 1, you create a sort of mask, that will only output 1's when the value compared with has bits set in those placeholders. In our case:
00011100
AND 00001000
-------------
00001000
So this will output the same value every time, UNTIL it goes over that value and has no bits set in the masked place holders. So when I go over 28 (00011100
), I'll be in 32, because I'm incrementing by 4. Watch what happens when I AND
the mask to 32.
00011100
AND 00100000
-------------
00000000
It resets! So it 'auto-checks' itself. Pretty cool, especially if you want some nice, fast and efficient code (we're coding in ASM anyways, which is fast in its own right). If you ever want to fashion something in this way, just message me and I'll try to help you out.
I could have just CMP #xx
, BNE
or BEQ
'ed to some place, but I thought of doing this by accident actually. Hell, now you you know another way to speed up your code.
Be sure to update header.inc and change the following lines so a NMI interrupt will go to the VBlank label:
.SNESNATIVEVECTOR
COP EmptyHandler
BRK EmptyHandler
ABORT EmptyHandler
NMI VBlank
IRQ EmptyHandler
.ENDNATIVEVECTOR
Well, besides seeing that way of using AND
, this code should have been easy to follow. Enjoy.
Complete Source Code: vblank-tutorial.7z
On to SNES Sprites!
Tutorial by bazz