DMA examples: Difference between revisions

From SNESdev Wiki
Jump to navigationJump to search
(Filling VRAM with a word value)
m (Move MemClear and FillVRAM examples into subsections of Fixed address DMA transfers)
Line 110: Line 110:
</pre>
</pre>


== WRAM clear ==
 
== Fixed address DMA transfers ==
 
The ''fixed address'' DMA mode is useful for clearing and filling blocks of memory.
 
 
=== 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.
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.


Line 156: Line 162:




== Filling VRAM ==
=== Filling VRAM ===




Line 233: Line 239:




== Filling VRAM with a word value ==
=== Filling VRAM with a word value ===


Filling VRAM with a word value is a bit more complicated.  The ''fixed address'' DMA mode only allows for byte fills.  Fortunately the VMAIN register provides a method of writing to the low and high bytes of VRAM separately, allowing for a VRAM word fill to be preformed in two DMA transfers.
Filling VRAM with a word value is a bit more complicated.  The ''fixed address'' DMA mode only allows for byte fills.  Fortunately the VMAIN register provides a method of writing to the low and high bytes of VRAM separately, allowing for a VRAM word fill to be preformed in two DMA transfers.

Revision as of 02:15, 30 May 2022

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:

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

; Configuration for $43n0
; OR these together to get the desired effect
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

; These defines are meant for a 16-bit write to $43n0 and $43n1
; and they set up the channel for several common cases.
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

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
  :


Fixed address DMA transfers

The fixed address DMA mode is useful for clearing and filling blocks of memory.


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

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


Filling VRAM

A fixed byte DMA transfer can be used to clear a block of VRAM. Unlike the clear Work-RAM routine above, the source of the DMA can be a Work-RAM memory address. This allows us to fill VRAM with a byte value of our choosing.


; 8 bit A
; 16 bit Index
; DB access registers
; DP = 0
;
; Uses a single zeropage byte variable (zpTmpByte)


; Clears all of VRAM (using DMA)
.proc ResetVram
    ldx #0
    ldy #0
; fallthrough
.endproc



; Clear a block of VRAM (using DMA)
;
; IN: X - VRAM word address
; IN: Y - size (in bytes)
.proc ClearVram
    lda #0
; fallthrough
.endproc



; Fill a block of VRAM with a byte value (using DMA)
;
; IN: X - VRAM word address
; IN: Y - size in bytes (if 0 then 64KiB of VRAM is filled)
; IN: A - byte value
.proc FillVRAM
    ; Store value to fill in zeropage
    sta zpTmpByte


    ; VRAM word addressing
    lda #$80
    sta VMAIN

    ; Set VRAM word address
    stx VMADD


    ; Length of the DMA transfer
    sty DMALEN

    ; Fixed byte transfer to word register VMDATA
    ldx #DMA_01 | DMA_CONST | ((VMDATA & 0xff) << 8)
    stx DMAMODE                                         ; also sets B Bus Address

    ; Set DMA source address
    ldx #zpTmpByte
    stx DMAADDR
    stz DMAADDRBANK             ; zeropage bank is always 0

    ; Disable HDMA (prevents the model-1 HDMA/DMA crash)
    stz HDMAEN

    ; Start DMA transfer
    lda #1
    sta MDMAEN

    rts
.endproc


Filling VRAM with a word value

Filling VRAM with a word value is a bit more complicated. The fixed address DMA mode only allows for byte fills. Fortunately the VMAIN register provides a method of writing to the low and high bytes of VRAM separately, allowing for a VRAM word fill to be preformed in two DMA transfers.


; 8 bit A
; 16 bit Index
; DB access registers
; DP = 0
;
; Uses a single zeropage word variable (zpTmpWord)


; Fills a 32x32 tilemap in VRAM with a given word value (using DMA)
; IN: X = VRAM word address
; IN: Y = word value
.proc FillVramTilemap
    ; Store value to fill in zeropage
    sty zpTmpWord

    ; Must not modify X, it is still required after the first DMA transfer


    ; Disable HDMA (prevents the model-1 HDMA/DMA crash)
    stz HDMAEN


    ;
    ; Transfer the low byte of zpTmpWord to VMDATAL `32*32` times
    ;

    ; VRAM byte addressing to VMDATAL
    stz VMAIN

    ; Set VRAM word address
    stx VMADD

    ; Fixed byte transfer to byte register VMDATAL
    ldy #DMA_LINEAR | DMA_CONST | ((VMDATAL & 0xff) << 8)
    sty DMAMODE                                             ; also sets B Bus Address

    ; Set DMA source address to the low byte of zpTmpWord
    ldy #zpTmpWord
    sty DMAADDR
    stz DMAADDRBANK             ; zeropage bank is always 0

    ; Length of the DMA transfer
    ldy #32 * 32
    sty DMALEN

    ; Start DMA transfer
    lda #1
    sta MDMAEN


    ;
    ; Transfer the high byte of zpTmpWord to VMDATAH `32*32` times
    ;

    ; VRAM byte addressing to VMDATAH
    lda #$80
    sta VMAIN

    ; Set VRAM word address
    stx VMADD

    ; Change DMA B-Bus address to VMDATAH
    lda #VMDATAH & 0xff
    sta DMAPPUREG
    ; DMAMODE is already set

    ; Set DMA source address to the high byte of zpTmpWord
    lda #zpTmpWord + 1
    sta DMAADDR
    ; DMAADDRHI and DMAADDRBANK is already set

    ; Length of the DMA transfer
    ldy #32 * 32
    sty DMALEN

    ; Start DMA transfer
    lda #1
    sta MDMAEN

    rts
.endproc