DMA examples

This page provides examples on how to use DMA registers to do fast copies on the SNES.

These examples use the following defines to make the code clearer:

DMAMODE     = $4300 ; DMAPn DMAPPUREG   = $4301 ; BBADn DMAADDR     = $4302 ; A1TnL DMAADDRHI   = $4303 ; A1TnH DMAADDRBANK = $4304 ; A1Bn DMALEN      = $4305 ; DASnL DMALENHI    = $4306 ; DASnH
 * Registers           Also known as...

DMA_LINEAR  = $00 DMA_01      = $01 DMA_00      = $02 DMA_0011    = $03 DMA_0123    = $04 DMA_0101    = $05 DMA_FORWARD = $00 DMA_CONST   = $08 DMA_BACKWARD = $10 DMA_INDIRECT = $40 DMA_READPPU = $80
 * Configuration for $43n0
 * OR these together to get the desired effect

DMAMODE_PPULOFILL = (<VMDATAL << 8) | DMA_LINEAR | DMA_CONST DMAMODE_PPUHIFILL = (<VMDATAH << 8) | DMA_LINEAR | DMA_CONST DMAMODE_PPUFILL  = (<VMDATAL << 8) | DMA_01     | DMA_CONST DMAMODE_RAMFILL  = (<WMDATA  << 8) | DMA_LINEAR | DMA_CONST DMAMODE_PPULODATA = (<VMDATAL << 8) | DMA_LINEAR | DMA_FORWARD DMAMODE_PPUHIDATA = (<VMDATAH << 8) | DMA_LINEAR | DMA_FORWARD DMAMODE_PPUDATA  = (<VMDATAL << 8) | DMA_01     | DMA_FORWARD DMAMODE_CGDATA   = (<CGDATA  << 8) | DMA_00     | DMA_FORWARD DMAMODE_OAMDATA  = (<OAMDATA << 8) | DMA_00     | DMA_FORWARD
 * These defines are meant for a 16-bit write to $43n0 and $43n1
 * and they set up the channel for several common cases.

Copying OAM
This is a simple DMA example that sends an OAM buffer to the PPU.

.proc CopyOAM php

rep #$20 ; Set A to 16-bit

lda #DMAMODE_OAMDATA sta DMAMODE lda #OAM   ; Copy from OAM buffer in RAM sta DMAADDR lda #544   ; 512 bytes + 32 bytes = 544 sta DMALEN

sep #$20 ; Set A to 8-bit

lda #^OAM  ; Set the bank byte of the source address sta DMAADDRBANK

; Start the DMA lda #1 sta MDMAEN

plp rtl .endproc

DMA as part of a scrolling update
This example demonstrates using DMA to write to video RAM while scrolling a large map, and it might make sense to put something like it in a game's vblank handler. Notice the writes to PPU registers - the DMA unit only handles writing the actual data, so anything else (such as setting the destination address) has to be done normally. This also demonstrates using VMAIN to write downwards through a tilemap.

.a16 ; Does a column need to be updated? lda ColumnUpdateAddress beq :+ stz ColumnUpdateAddress sta VMADDL

; Write to VMDATAL and VMDATAH lda #DMAMODE_PPUDATA sta DMAMODE

; Copy from the buffer lda #.loword(ColumnUpdateBuffer) sta DMAADDR

; A tilemap column is 32 tiles long, and each tile is 2 bytes lda #32*2 sta DMALEN

sep #$20 ; 8-bit accumulator

lda #^ColumnUpdateBuffer sta <DMAADDRBANK lda #$81  ; Increment on VMDATAH write, increment by 32 sta VMAIN

lda #1    ; Start the DMA sta MDMAEN

lda #$80  ; Increment on VMDATAH write, increment by 1 sta VMAIN

rep #$20  ; 16-bit accumulator :

WRAM clear
These are reusable subroutines for clearing out sections of RAM, which a program might want to do at the start of a level, or in Init code. Both routines take a start address in X, and a size in Y.

Note: Due to a hardware bug on early SNES consoles it's not recommended to do this while HDMA is enabled.

.i16 ; 16-bit index registers assumed .proc MemClear php sep #$20  ; 8-bit accumulator stz WMADDH ; Set high bit of WRAM address to zero - meaning the first 64KB of RAM UseHighHalf: stx WMADDL ; WRAM address, bottom 16 bits sty DMALEN

; Configure DMA to write to WMDATA, and keep the source address constant ldx #DMAMODE_RAMFILL ZeroSource: stx DMAMODE

; The zero byte used as the source needs to come from somewhere in ROM ; here it's taken from the second byte of a "STX $4300" ldx #.loword(ZeroSource+1) stx DMAADDR ; Set the bank byte of the source address too lda #^MemClear sta DMAADDRBANK

; Start the DMA lda #1 sta MDMAEN plp rtl .endproc

.proc MemClear7F php sep #$20 lda #1    ; Use the second 64KB of RAM sta WMADDH bra MemClear::UseHighHalf .endproc
 * Clear a section of bank 7F instead