memory

This module exposes waitstate and DMA control registers, constants for relocating code/data, as well as access to ROM and SRAM as arrays.

Section Constants

These are to be used with the codegenDecl pragma to place your variables and procedures into certain regions of memory. They cannot be used for local variables, as those are always on the stack which is in IWRAM.

Example:

# Define an array which is statically allocated in EWRAM.
var collisionGrid {.codegenDecl: DataInEwram.}: array[2048, uint16]
const DataInIwram = "__attribute__((section(\".iwram.data\"))) $# $#"

Put variable in IWRAM (default).

const DataInEwram = "__attribute__((section(\".ewram.data\"))) $# $#"

Put variable in EWRAM

const ArmCodeInIwram = "__attribute__((section(\".iwram.text\"), target(\"arm\"), long_call)) $# $#$#"

Put procedure in IWRAM.

const ThumbCodeInEwram = "__attribute__((section(\".ewram.text\"), target(\"thumb\"), long_call)) $# $#$#"

Put procedure in EWRAM.

Tip

You may wish create shorthands for these in your project, like so:

{.pragma: ewdata, codegenDecl: DataInEwram.}
{.pragma: iwdata, codegenDecl: DataInIwram.}
{.pragma: ewcode, codegenDecl: ThumbCodeInEwram.}
{.pragma: iwcode, codegenDecl: ArmCodeInIwram.}

You could save this in a prelude.nim file along with commonly-used imports which could then be included at the start of each module in your game.


Memory-mapped arrays

let romMem: array[0x1000000, uint16]

Access to ROM as an array of halfwords.

The max ROM size is 32MiB.

var sramMem: array[0x10000, uint8]

Access to SRAM as an array of bytes.

This has a maximum size of 64KiB, though many carts don’t actually have that much available.


Waitstate Control

var waitcnt: WaitCnt

Waitstate control register.

This controls the number of CPU cycles taken to access cart memory (ROM and SRAM).

The “standard” setting (used by most commercial games) is as follows:

waitcnt.init(
  sram = WsSram.N8_S8,   # 8 cycles to access SRAM.
  rom0 = WsRom0.N3_S1,   # 3 cycles to access ROM, or 1 cycle for sequential access.
  rom2 = WsRom2.N8_S8,   # 8 cycles to access ROM (mirror #2) which may be used for flash storage.
  prefetch = true        # prefetch buffer enabled.
)

In this example, waitcnt.rom0 determines the access time for the default view into ROM.

Warning

The preferred access time of “3,1” works on every flashcart except for the SuperCard SD and its derivatives. If you want to support the SuperCard without compromising performance for all, use the slowGamePak proc.

There are two additional ROM mirrors located at 0x0A000000 and 0x0C000000, which have access times determined by waitcnt.rom1 and waitcnt.rom2.

The mirrors may be useful for carts containing multiple ROM chips. For example on some carts the upper 128KiB of ROM is mapped to flash storage which would require different access timings.

type WaitCnt = distinct uint16

Field

Type

Bits

Description

sram

WsSram

0-1

SRAM access time.

rom0

WsRom0

2-4

ROM mirror 0 access time.

rom1

WsRom1

5-7

ROM mirror 1 access time.

rom2

WsRom2

8-10

ROM mirror 2 access time.

phi

WsPhi

11-12

Cart clock (don’t touch!)

prefetch

bool

14

Game Pak prefetch. If enabled, the GBA’s prefetch unit will fetch upcoming instructions from ROM, when ROM is not being accessed by the CPU, generally leading to a performance boost.

gb

bool

15

True if a Game Boy (Color) cartridge is currently inserted. (Read only!)

type WsSram = enum

SRAM access timings

  • N4_S4SRAM access takes 4 cycles
  • N3_S3SRAM access takes 3 cycles
  • N2_S2SRAM access takes 2 cycles
  • N8_S8SRAM access takes 8 cycles
type WsRom0 = enum

ROM access timings.

Initial access to ROM takes N cycles, sequential access takes S cycles.

For more information on N cycles and S cycles, see the asm chapter of Tonc.

  • N4_S2
  • N3_S2
  • N2_S2
  • N8_S2
  • N4_S1
  • N3_S1
  • N2_S1
  • N8_S1

Note

On Nim 1.6 and later, you can omit the enum prefixes as long as you enable overloadable enums.

In other words, you can write N3_S1 instead of WsRom0.N3_S1.

type WsRom1 = enum

Access timings for ROM mirror starting at 0x0A000000.

  • N4_S4
  • N3_S4
  • N2_S4
  • N8_S4
  • N4_S1
  • N3_S1
  • N2_S1
  • N8_S1
type WsRom2 = enum

Access timings for ROM mirror starting at 0x0C000000.

  • N4_S8
  • N3_S8
  • N2_S8
  • N8_S8
  • N4_S1
  • N3_S1
  • N2_S1
  • N8_S1
type WsPhi = enum

PHI Terminal Output. This allows the GBA to supply a clock signal to the cartridge hardware, but is not used in practise.

  • phiOffDisabled (recommended)
  • phi4MHz4.19MHz
  • phi8MHz8.38MHz
  • phi17MHz16.78MHz

Direct Memory Access

Direct Memory Access (DMA) is a hardware feature that allows the GBA to quickly copy data from one location to another at a given moment.

Channels 0 and 1 are required for audio (maxmod will take care of these), while 2 and 3 may be used for anything you like. Note that DMA comes with some caveats that make it dangerous to use (and it’s not much faster than an optimised copy routine like memcpy32 anyways), so I wouldn’t recommend it for general purposes.

DMA can be applied on HBlank (known as HDMA) for some neat scanline-based effects. The HBlank tnterrupt could also be used for such effects, but HDMA

Example:

# Create a table of horizontal scroll offsets:
var hofsTable: array[ScreenHeight, int16]

# Populate the table with a sine curve:
for i in 0 ..< ScreenHeight:
  hofsTable[i] = (luSin(i * 100) shr 13).int16

proc onVBlank() =
  # Cancel previous DMA.
  dmach[3].stop()

  # Setup new DMA: Each scanline, copy a subsequent value from the table
  #                into BG3's horizontal scroll register.
  dmach[3].start(
    dst = addr bgofs[3].x,
    src = addr hofsTable[0],
    count = 1,
    dstMode = Fix,
    srcMode = Inc,
    repeat = true,
    size = Halfwords,
    time = AtHBlank,
  )

Danger

If you cancel a DMA transfer within a few CPU cycles of it starting internally, the whole GBA will lock up and possibly trash all the memory (including save data!)

For this reason, you should make sure to stop DMA at a safe moment when no transfer is about to occur, for example HDMA could be cancelled at the start of your VBlank handler.

var dmach: array[4, DmaChannel]

Direct memory access channels.

type DmaChannel = object

A group of registers for a single DMA channel.

Note, all DmaCnt fields can be set directly via the channel too, e.g. dmach[3].

    Field    

    Type    

    Description    

src pointer

Source address.

dst pointer

Destination address.

count uint16

Number of transfers.

cnt DmaCnt

DMA control register.

type DmaCnt = distinct uint16

DMA control register. (Write only!)

Field

Type

Bits

Description

dstMode

DmaDstMode

5..6

Type of increment applied to destination address.

srcMode

DmaSrcMode

7..8

Type of increment applied to source address.

repeat

bool

9

Repeat the transfer for each occurrence specified by time.

size

DmaSize

10

Whether to transfer 16 or 32 bits at a time.

time

DmaTime

12..13

Timing mode, determines when the transfer should occur.

irq

bool

14

If true, an interrupt will be raised when finished.

enable

bool

15

Enable DMA transfer for this channel. (Invoking start() will do this for you.)

type DmaDstMode = enum
  • IncIncrement after each copy.
  • DecDecrement after each copy.
  • FixRemain unchanged.
  • ReloadLike Inc but resets to its initial value after all transfers have been completed.
type DmaSrcMode = enum
  • IncIncrement after each copy.
  • DecDecrement after each copy.
  • FixRemain unchanged.
type DmaSize = enum
  • HalfwordsCopy 16 bits.
  • WordsCopy 32 bits.
type DmaTime = enum
  • AtNowTransfer immediately.
  • AtVBlankTransfer on VBlank.
  • AtHBlankTransfer on HBlank. Note: HBlank DMA does not occur during VBlank (unlike HBlank interrupts).
  • AtSpecialChannels 0/1: start on FIFO empty. Channel 2: start on VCount = 2
template start(d: DmaChannel, dst, src: pointer, count: uint16, args: varargs[untyped])

Activate DMA on the given channel.

template stop(d: DmaChannel)

Cancels any transfer that may be happening on the given DMA channel.


Procedures

proc slowGamePak(): bool

Check if the cartridge does not support fast ROM access, which might be the case if the game is running on the SuperCard MiniSD.

Example:

if slowGamePak():
  waitcnt.init(rom0 = WsRom0.N4_S2)
else:
  waitcnt.init(rom0 = WsRom0.N3_S1)