Now in this part of the series, we'll actually be.. Working with VRAM. :)
What We're Covering
Now we're going to actually upload data to the Video RAM. Now, there's 2 things that need to go to VRAM, and that's the tile data itself, and the tile map, which says where specific tiles will be displayed on the screen. Now let's do it.
!!VERY IMPORTANT!! Each address in VRAM is A WORD IN SIZE. I misunderstood this at one point and it screwed everything I thought I knew. SO REMEMBER, $0000
holds 2 bytes, low and high.
What we need to first is write a routine and macro that will upload a block of data to VRAM. We will use DMA to do this as well, so at least now you have some knowledge under your belt of what we're going to do :). Time to write our VRAM upload code.
;============================================================================
; LoadBlockToVRAM -- Macro that simplifies calling LoadVRAM to copy data to VRAM
;----------------------------------------------------------------------------
; In: SRC_ADDR -- 24 bit address of source data
; DEST -- VRAM address to write to (WORD address!!)
; SIZE -- number of BYTEs to copy
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: A, X, Y
;----------------------------------------------------------------------------
;LoadBlockToVRAM SRC_ADDRESS, DEST, SIZE
; requires: mem/A = 8 bit, X/Y = 16 bit
.MACRO LoadBlockToVRAM
lda #$80
sta $2115 ; Set VRAM transfer mode to word-access, increment by 1
ldx #\2 ; DEST
stx $2116 ; $2116: Word address for accessing VRAM.
lda #:\1 ; SRCBANK
ldx #\1 ; SRCOFFSET
ldy #\3 ; SIZE
jsr LoadVRAM
.ENDM
;============================================================================
; LoadVRAM -- Load data into VRAM
;----------------------------------------------------------------------------
; In: A:X -- points to the data
; Y -- Number of bytes to copy (0 to 65535) (assumes 16-bit index)
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: none
;----------------------------------------------------------------------------
; Notes: Assumes VRAM address has been previously set!!
;----------------------------------------------------------------------------
LoadVRAM:
phb
php ; Preserve Registers
stx $4302 ; Store Data offset into DMA source offset
sta $4304 ; Store data Bank into DMA source bank
sty $4305 ; Store size of data block
lda #$01
sta $4300 ; Set DMA mode (word, normal increment)
lda #$18 ; Set the destination register (VRAM write register)
sta $4301
lda #$01 ; Initiate DMA transfer (channel 1)
sta $420B
plp ; restore registers
plb
rts ; return
Ok most of these registers we used in the palette routine. Following what register holds what parameter may be a little confusing. Just suck it up and remember. It's just memorization. :P
Let me go over the new registers we just used.
Register $2116 (VMADDL)
VRAM Address: This is just the register that is used to set the initial WORD address for a VRAM upload or download. Note: when reading from VRAM, a dummy read must be performed after writing to this register, just to let you know :)
Register $2118 (VMDATAL)
VRAM Data Write:
Well we kinda used it, we told register $4301
that in the DMA we would be writing to $2118
. When you write to this register, a byte or word is written to VRAM. The VRAM address is incremented or not depending on the settings of register $2115
.
Register $2115 (VMAIN)
Video Port Control: It greatly affects how data is written to VRAM, so it is initialized with #$80
, or #%10000000
.
i---ffrr i: 0=increment when $2118 or $2139 is accessed
1=increment when $2119 or $2139 is accessed
f: full graphic (?) - Not sure, Yoshi's doc loosely describes it.
r: increment rate: 00=increment by 2 bytes (1x1)
01=increment by 64 bytes (32x32)
10=increment by 128 bytes (64x64)
11=increment by 256 bytes (128x128)
Since $2115
was initialized with #%10000000
, the VRAM address is incremented 1 word after $2119
is accessed. This enables us to write an entire word to the VRAM address before it is incremented.
Also, the DMA transfer type was set to 2 regs (001), so the DMA alternates writing between $2118
and $2119
. See how it all connects? cool.. :)
Now we need to utilize the macro and routine we made. What we'll do next is load the block of tile data to VRAM, and tell the SNES what addresses will be the starting addresses for the BG1 character (tile) data, and the BG1 tile map. Then we'll display the tile on the screen. cool.. :)
Loading the tiles to VRAM is simple now. Just call the LoadBlockToVRAM routine in your program code. But I hear you ask, "How do I determine the size of the tile data?" Easy! just use the following calculation:
8 * color_depth(in bits) * number_of_characters
What I mean by this is if the BG can have 4 colors a tile, it is 2 bits in size (00000011 = 4 decimal). so instead of using 4, you use 2. So, if we had 20 2bit color tiles. in order to find the size of all of them, we would do
(8*2*20) = $140 bytes (hex)
Now you could load the tile data.
\|/ address here is just an example
LoadBlockToVRAM Tiles, $5000, $0140 ; ta da
Ok, in our case we only have 2 tiles, but how do we figure out the color depth? Well, we need to choose our screen mode first. Let's check out register $2105
in Qwertie's doc:
Register $2105 (BGMODE)
Screen mode register (1b/W)
dcbapmmm d: BG4 tile size c: BG3 tile size b: BG2 tile size
a: BG1 tile size Sizes are: 0=8x8; 1=16x16. (See reg. $2107)
p: order of BG priorities m: General screen mode
This register determines the size of the tile represented by 1 entry in the tile map array, the order that BGs are drawn on the screen, and the screen mode. The screen modes are:
MODE # of BGs Max Colors/Tile Palettes Colors Total
0 4 4 32 (8 per BG) 128 (32 per BG*4 BGs)
1 3 BG1/BG2:16 BG3:4 8 BG1/BG2:128 BG3:32
2 2 16 8 128
3 2 BG1:256 BG2:16 BG1:1 BG2:8 BG1:256 BG2:128
4 2 BG1:256 BG2:4 BG1:1 BG2:8 BG1:256 BG2:32
5 2 BG1:16 BG2:4 8 BG1:128 BG2:32(Interlaced)
6 1 16 8 128 (Interlaced mode)
7 1 256 1 256
Ah, that's enough copying. As you can see, each mode has special properties. We'll just use MODE 0, display the tile on a 32x32 BG1, and use 8x8 tiles. Note: That reference to $2107
will be talked about soon. Like.. Right now :) We need to tell the SNES where our tile maps and tile data will be stored. Registers $2107-$210A
, and $210B-$210C
, will help us do that.
Register $2107 (BG1SC)
BG1 Tile Map Location (1B/W)
aaaaaass a: Tile map address s: Screen size in tiles: 00=32x32 01=64x32 10=32x64 11=64x64
The a bits set the starting tilemap address. This can be set in intervals of $0400
words. So setting the a bits to 1 sets the address to $0400
, incrementing that would set it to $0800
, etc. If you want to convert the bits into the address itself to see, shift them left by 10. (1 left shift 10 = $0400
) Since there is only 64K of VRAM, the Most Significant Bit (bit 7) must be ZERO. $2108-$210A
are the same exact thing as $2107, only for BG2-BG4, respectively.
Register $210B (BG12NBA)
BG1 & BG2 Character location (1b/W)
aaaabbbb a: Base address for BG2.
b: Base address for BG1.
Register $210C (BG34NBA)
BG3 & BG4 Character Location (1b/W)
aaaabbbb a: Base address for BG4.
b: Base address for BG3.
The starting address here can be set in intervals of $1000
words. So 0 would be $0000
, 1 would be $1000
, etc. Because of the limited size of VRAM, the MSB must be 0 (you can't go over address $8000
). You can convert the bits to the address value by shifting them left by 12.
Got it? I hope so. Since we're only using BG1, we're going to tell $2107
where our tile map will be, and $210B
where our tile data is. Simple enough right?
lda #$xx
sta $2107
lda #$xx
sta $210B
Ok, let's put our tile data right at the beginning of VRAM, $0000
. Let's get the size of our tile data. (8*2*2 = 32, or $20
. We're going to use a 32x32 tile map, so that's 1024 bytes. But wait, each entry in the tile map is 2 bytes long, so now we have a 2048 byte long map. That's $0800
bytes. We'll put our Tile Map at $0400
(We can't put it lower than that, try shifting 1 left by 10. $0400
is as low as you can go). After swirling all that information around in your head, you may start to see how the code would look like:
LoadBlockToVRAM Tiles, $000, $0020
stz $2105 ; Set Mode to 0
lda #$04
sta $2107 ; Set BG1's Tile Map VRAM offset to $0400 (word address) and the Tile Map size to 32x32
stz $210B ; Set BG1's Character VRAM offset to $0000 (word address)
Remember that the $800
byte-long Tile Map goes from $0400-$0800
, cause it makes $400
words, and each address in VRAM is a word in size. There's only one thing to do after that. Turn the BG on! We do that with
Register $212C (TM)
Main screen designation (1b/W)
---abcde a: Sprites disable/enable.
b: Disable/enable BG4.
c: Disable/enable BG3.
d: Disable/enable BG2.
e: Disable/enable BG1.
So all we need to do is:
lda #$01
sta $212C
THAT'S IT! Well, almost, hehe. The last thing you need to do is tell the Tile Map to display a character. Let's do that.
Each entry in the Tile Map is worth 2 bytes. Here's the format:
High Low v: vertical flip h: horizontal flip
vhopppcc cccccccc o: priority bit p: palette number
c: Starting character (tile) number
Please read Qwertie's section "The SNES PPU Graphics Organization" for info on how the palette bits determine what colors the tile will use in CGRAM, the horizontal and vertical flip bits (which are kind of obvious), and the priority bit. We'll just be setting the character #.
Your tile # is 1, so all we need to is write to the low byte of some location in the Tile Map. Let's just display tile on the top-left of the screen (0,0). Here's the code:
ldx #$0400
stx $2116
lda #$01
sta $2118
And that IS IT. All that's left to do, is write all of this into 1 entire source file. The source is in the next part.
Some Important Notes
Remember that the SNES screen is 256x224 pixels in size by default. Because of this, a screen full of tiles would not be fully visible. 256 pixels is just enough for 32 8-pixel-long tiles, so how many lines of tiles are we missing with 224 vertical lines? 224/8 = 28 pixel lines. 32-28 = 4 vertical lines of tiles. We're missing 4 lines of TILES (not pixels) on the BOTTOM of the screen. Remember that. You may not have understood my logic, and if so, just remember the facts.
Also, for some odd reason, the top line of PIXELS is also cut off for tiles on the first vertical line, like or tile. If we added color to the top line of our tile, it would not show. You will need to learn how to scroll the BG down a pixel in order to make the line visible. You will learn this shortly.
;------------------------------------------------------------------------
;- Written by Bazz
;- This code demonstrates displaying a simple tile on the screen.
;- This can be expanded on easily, and will be used as a base for
;- later examples.
;-
;- All I want is feedback, so please tell me if my tutorials suck,
;- are decent, or whatever. I'd also like to know what needs
;- better explanations and so on. I must improve the tutorials!
;------------------------------------------------------------------------
;============================================================================
; Includes
;============================================================================
;== Include MemoryMap, Vector Table, and HeaderInfo ==
.INCLUDE "header.inc"
;== Include SNES Initialization routines ==
.INCLUDE "InitSNES.asm"
;============================================================================
; Macros
;============================================================================
;============================================================================
;LoadPalette - Macro that loads palette information into CGRAM
;----------------------------------------------------------------------------
; In: SRC_ADDR -- 24 bit address of source data,
; START -- Color # to start on,
; SIZE -- # of COLORS to copy
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: A,X,Y
; Requires: mem/A = 8 bit, X/Y = 16 bit
;----------------------------------------------------------------------------
.MACRO LoadPalette
lda #\2
sta $2121 ; Start at START color
lda #:\1 ; Using : before the parameter gets its bank.
ldx #\1 ; Not using : gets the offset address.
ldy #(\3 * 2) ; 2 bytes for every color
jsr DMAPalette
.ENDM
;============================================================================
; LoadBlockToVRAM -- Macro that simplifies calling LoadVRAM to copy data to VRAM
;----------------------------------------------------------------------------
; In: SRC_ADDR -- 24 bit address of source data
; DEST -- VRAM address to write to (WORD address!!)
; SIZE -- number of BYTEs to copy
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: A, X, Y
;----------------------------------------------------------------------------
;LoadBlockToVRAM SRC_ADDRESS, DEST, SIZE
; requires: mem/A = 8 bit, X/Y = 16 bit
.MACRO LoadBlockToVRAM
lda #$80
sta $2115
ldx #\2 ; DEST
stx $2116 ; $2116: Word address for accessing VRAM.
lda #:\1 ; SRCBANK
ldx #\1 ; SRCOFFSET
ldy #\3 ; SIZE
jsr LoadVRAM
.ENDM
;============================================================================
; Main Code
;============================================================================
.BANK 0 SLOT 0
.ORG 0
.SECTION "MainCode"
Start:
InitSNES ; Clear registers, etc.
; Load Palette for our tiles
LoadPalette BG_Palette, 0, 4
; Load Tile data to VRAM
LoadBlockToVRAM Tiles, $0000, $0020 ; 2 tiles, 2bpp, = 32 bytes
; Now, load up some data into our tile map
; (If you had an full map, you could use LoadBlockToVRAM)
; Remember that in the default map, all entries point to tile #0
lda #$80
sta $2115
ldx #$0400
stx $2116
lda #$01
sta $2118
; Setup Video modes and other stuff, then turn on the screen
jsr SetupVideo
Infinity:
jmp Infinity ; bwa hahahahaha
;============================================================================
; SetupVideo -- Sets up the video mode and tile-related registers
;----------------------------------------------------------------------------
; In: None
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
SetupVideo:
php
lda #$00
sta $2105 ; Set Video mode 0, 8x8 tiles, 4 color BG1/BG2/BG3/BG4
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 #$FF
sta $210E
sta $210E
lda #$0F
sta $2100 ; Turn on screen, full Brightness
plp
rts
;============================================================================
;============================================================================
; LoadVRAM -- Load data into VRAM
;----------------------------------------------------------------------------
; In: A:X -- points to the data
; Y -- Number of bytes to copy (0 to 65535) (assumes 16-bit index)
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: none
;----------------------------------------------------------------------------
; Notes: Assumes VRAM address has been previously set!!
;----------------------------------------------------------------------------
LoadVRAM:
php ; Preserve Registers
stx $4302 ; Store Data offset into DMA source offset
sta $4304 ; Store data Bank into DMA source bank
sty $4305 ; Store size of data block
lda #$01
sta $4300 ; Set DMA mode (word, normal increment)
lda #$18 ; Set the destination register (VRAM write register)
sta $4301
lda #$01 ; Initiate DMA transfer (channel 1)
sta $420B
plp ; restore registers
rts ; return
;============================================================================
;============================================================================
; DMAPalette -- Load entire palette using DMA
;----------------------------------------------------------------------------
; In: A:X -- points to the data
; Y -- Size of data
;----------------------------------------------------------------------------
; Out: None
;----------------------------------------------------------------------------
; Modifies: none
;----------------------------------------------------------------------------
DMAPalette:
php ; Preserve Registers
stx $4302 ; Store data offset into DMA source offset
sta $4304 ; Store data bank into DMA source bank
sty $4305 ; Store size of data block
stz $4300 ; Set DMA Mode (byte, normal increment)
lda #$22 ; Set destination register ($2122 - CGRAM Write)
sta $4301
lda #$01 ; Initiate DMA transfer
sta $420B
plp
rts ; return from subroutine
.ENDS
;============================================================================
; Character Data
;============================================================================
.BANK 1 SLOT 0
.ORG 0
.SECTION "CharacterData"
.INCLUDE "tiles.inc"
.ENDS
Complete Source Code VRAM-SNES-Program.7z
On to Learning the GFX Format - 2BPP Gameboy and SNES!
Tutorial by bazz