SNES programming tutorial. Example 2.
What we want to do is fill the palette. We could set up a loop that writes to the CGDATA register 512 times (register $2122). But there is a much faster way to do this called DMA (direct memory access).
Ignore the HDMA stuff for now (which use the same registers). The main use for DMA is to write to $2104, $2118, and $2122 (OAM data, VRAM data, and CG data). DMA is just a hardware copy loop for transferring data from the CPU bus (ROM and RAM) to the PPU bus (VRAM, palette, and OAM). You should use a DMA when you are transferring more than a dozen bytes to any of these RAMs.
What it can’t do… It can’t copy from CPU bus to the CPU bus (such as WRAM to WRAM, or ROM to WRAM). You need to use a MVN or MVP block move operation to do that.
The example code is for palette DMA. DMA needs to happen during *forced blank or during **v-blank. First you set up the transfer. There are 8 channels, but let’s focus on channel 0. All of these are 8 bit values.
* forced blank is when the PPU is off, register $2100 upper bit set.
** v-blank or vertical blank, happens once per frame when the the PPU is on. First the PPU will draw the entire screen, line by line, then it will pause slightly before it jumps back to the top. This pause is the vertical blank period, and the PPU is idle, so you can send new data to the VRAM and OAM and CGRAM during this time.
$4300 to set up the transfer mode.
$4301 is the destination register $21xx. So, $04 = $2104. $18 = $2118. Etc.
$4302 is the source address, low byte
$4303 is the source address, high byte
$4304 is the source address, bank byte
$4305 is the number of bytes, low byte
$4306 is the number of bytes, high byte
Then, for channel 0, you write #1 to $420b to start the transfer. This locks up the CPU until the transfer is complete.
If you were using channel 1, the registers would be 4310,4311,4312,etc and you would write #2 to 420b. If you were using channel 2, the registers would be 4320,4321,4322,etc and you would write #4 to 420b. And so forth.
We are writing to the palette, we need to first zero the palette address, then send to $2122, the CG data register.
; DMA from BG_Palette to CGRAM A8 XY16 stz pal_addr ; $2121 cg address = zero stz $4300 ; transfer mode 0 = 1 register write once lda #$22 ; $2122 sta $4301 ; destination, pal data ldx #.loword(BG_Palette) stx $4302 ; source lda #^BG_Palette sta $4304 ; bank ldx #256 ; BG_Palette only has 128 colors stx $4305 ; length lda #1 sta $420b ; start dma, channel 0
Note, I only transferred 256 bytes (128 colors). Let’s look at why.
I just used the default palette from the M1TE editor. This was designed for editing BG tiles, so the palette only has 128 entries. I thought having a ROM that was just a black screen would be dull, so I changed color #0 (the top left = the background color) to blue. Palette / Save.
You can include a binary file using the .incbin directive (see bottom of main.asm). There is a label here BG_Palette: which we can reference in our code. I put it in an entirely different bank (RODATA1 segment is bank $81), just to show that it can be done easily.
Then I ran the DMA code twice to copy the same 256 bytes to both the BG palette (first 128 colors) and the Sprite palette (last 128 colors). It would be nice if you could just write #1 to $420b again, but the transfer changes some of the DMA registers (transfer length counted down to zero and the source address will be adjusted upward). So I had to rewrite the those and THEN write #1 to $420b.
When I run the ROM in MESEN-S, I can open the Palette Viewer tool, and see that our palette has been copied twice, as expected.
Well… we haven’t copied any actual tiles yet, so we can’t show anything but a plain screen. Try changing the color in M1TE to some other color, and reassembling and see if you can get it to work. This is what it looked like for me.
For reference, I will post the example code for DMA to VRAM and DMA to OAM. See also…
;DMA from Tiles to VRAM A8 XY16 lda #$80 sta $2115 ; set the increment mode +1 ldx #$0000 stx $2116 ; set an address in the vram of $0000 ; (and $2117) lda #1 sta $4300 ; transfer mode, 2 registers 1 write ; $2118 and $2119 are a pair Low/High lda #$18 ; $2118 vram data sta $4301 ; destination ldx #.loword(Tiles) stx $4302 ; source lda #^Tiles sta $4304 ; bank ldx #$2000 stx $4305 ; length lda #1 sta $420b ; start dma, channel 0
;DMA from OAM_BUFFER to OAM A8 XY16 ldx #$0000 stx $2102 ; oam address (and $2103) stz $4300 ; transfer mode 0 = 1 register write once lda #4 ;$2104 oam data sta $4301 ; destination, oam data ldx #.loword(OAM_BUFFER) stx $4302 ; source lda #^OAM_BUFFER sta $4304 ; bank ldx #544 stx $4305 ; length lda #1 sta $420b ; start dma, channel 0
I will be covering these a little later.
On a side note. HMDA is a different thing altogether (but uses the same registers). It is for changing register values midscreen and it can change lots of different registers, such as mode 7 parameters, scroll registers, colors, mosaic, windowing, etc. You should be aware that there is a bug which happens if HDMA and DMA happen at the same time. It can crash the game (on the early revision SNES model). If you are using both, you might want to write #0 to $420c (the HDMA enable register) before performing a DMA, to disable HDMA.