This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Why does this RAM initialization code work?

Why does this RAM initialization code work?
by on (#201374)
Today I noticed a little issue with my RAM initialization code (the code that sets all RAM values to 0 at the start).
Since I use a pointer variable to iterate through the RAM addresses, this means the pointer variable itself is eventually also overwritten. A side effect I haven't considered until now.

So, I checked the RAM in fceux: When resetting the game and after fceux itself sets everything to $00 and $FF, I simply fill everything with the character X. Then I check which parts are overwritten with 0 by my code.

For some reason, the whole RAM still gets set to 0. There isn't a chunk of Xs anywhere in the code because of pointer misalignment or something like that, nor does the game run into an infinite loop because of the pointer's high byte value always being set back to 0.
I can set the pointer to any arbitrary address and it always worked.

Why is that the case? Why doesn't the below code produce chunks of RAM that remain untouched?

Code:
   ; The value to
   ; write into RAM.
   LDA #0

   ; The X and Y registers are
   ; used as counters for the
   ; loops and initialized with 0.
   TAX
   TAY

   ; Pointer + 0 always remains 0.
   ; The address is calculated
   ; by Pointer + 1 and by Y.
   STA Pointer + 0
   STA Pointer + 1

@initializeRamLoop:

   ; Set the value of 0
   ; to the current address.
   STA (Pointer), Y

   ; Increment the low byte part
   ; of the address.
   INY

   ; If it is set back to 0,
   ; increment the high byte part
   ; and, of course, the counter.
   ; Otherwise, continue with
   ; the inner loop.
   BNE @initializeRamLoop
   INC Pointer + 1
   INX

   ; The outer loop ends
   ; just before address $0800.
   CPX #$08
   BNE @initializeRamLoop

   ; Set the pointer itself
   ; to 0 as well.
   ; Pointer + 0
   ; is always 0. So, we only
   ; need to set
   ; Pointer + 1.
   ; A is still 0 from above.
   STA Pointer + 1

   ; The RAM has now been
   ; initialized with all zeroes.
Re: Why does this RAM initialization code work?
by on (#201375)
You actually have the answer in your own comments:
DRW wrote:
; Pointer + 0
; is always 0.


The writes go
Y=0, $0=0, $1=0 thus 0→$0
Y=1, $0=0, $1=0 thus 0→$1

And after that, nothing writes to the pointer memory again.

Since the pointer has to be stored in zero page, and you're initializing RAM with 0, it won't miss anything.
Re: Why does this RAM initialization code work?
by on (#201376)
Why such a complicated RAM-clearing routine?

Adapted from the wiki:

Code:
   ldx #0
   txa $0

@clrmem_top:
   sta $000, x
   sta $100, x
   ; Reserving $200 for OAM display list, sta $200, x if you want
   sta $300, x
   sta $400, x
   sta $500, x
   sta $600, x
   sta $700, x
   inx
   bne @clrmem_top
Re: Why does this RAM initialization code work?
by on (#201377)
@lidnariq:

Oh, that's right.

Let me reiterate:

Pointer + 0 is always 0, so that one doesn't matter since Y is the actual low byte value.

The value in Pointer + 1 will go from $00 to $07 during the algorithm.

But since the pointer is in zeropage, its own location in memory will always be guaranteed to be $00xx.
And this means Pointer + 1 is only overwritten with 0 when when Pointer + 1 has the value 0 anyway.

Or to say it like this:
The loop cannot suddenly jump from $1234 to $0034 due to Pointer + 1 being overwritten with a 0 because the pointer will never be in a memory location $12xx, only $00xx.

Is that correct?


mikejmoffitt wrote:
Why such a complicated RAM-clearing routine?

Two reasons:

1. I find a loop more elegant than manually writing 0, 100, 300, 400, 500, 600, 700.

2. There's an additional piece of code in my initialization routine that doesn't have anything to do with my original question, so I left it out, but it's required if you want to keep, for example, the highscore when pressing reset:
Code:
   ; There is a certain RAM area that
   ; shall not be initialized with zeroes
   ; because this area shall be persistent
   ; when the Reset button is pressed.
   ; We check if we reached that RAM area.

   ; The high byte of the address is checked.
   LDA Pointer + 1
   CMP #>__NO_RESET_LOAD__
   BNE @noNoResetSkip

   ; The low byte is checked.
   CPY #<__NO_RESET_LOAD__
   BNE @noNoResetSkip

   ; If we reached the
   ; reset-persistent area,
   ; we change the address to
   ; the first value after the area.
   ; This way, the reset-persistent
   ; area doesn't get changed.
   LDY #<(__NO_RESET_LOAD__ + __NO_RESET_SIZE__)
   LDA #>(__NO_RESET_LOAD__ + __NO_RESET_SIZE__)
   STA Pointer + 1

@noNoResetSkip:

   ; Set the value of 0
   ; to the current address.
   LDA #0
   STA (Pointer), Y
Re: Why does this RAM initialization code work?
by on (#201378)
I just noticed: The "reset-persistent area" code is missing the adjustment of X, am I right?

In the usual code, these values get updated in pairs:
Code:
   INC CommonPointer1AsVar + 1
   INX


So, this needs to be done here as well, right?

Code:
LDA #>(__NO_RESET_LOAD__ + __NO_RESET_SIZE__)
   STA CommonPointer1AsVar + 1

New code after the STA: TAX, right? (Or I guess I could completely do without the X.)
Re: Why does this RAM initialization code work?
by on (#201385)
I have no idea what your last question is asking.

How about this...the memory persistent area is zeropage 00-03

Start your loop with LDY #4.

EDIT-Alternatively, you could use $7fc-7ff as the memory persistent area, and reverse your loop, starting at $7fb, going down to $000
Re: Why does this RAM initialization code work?
by on (#201395)
The last question was just a discovery of mine: X and Pointer + 1 always need to have the same value. Because X counts the loop and Pointer accesses the RAM. And I just discovered that I missed to set X accordingly in one situation.
And I wanted to check if other people confirm this.

It didn't make a practical difference in the past. Because the reset-persistent area is only a few bytes big, so I was lucky that the high byte didn't change between before and after the reset-persistent area. But it could happen, so I'll have to fix it.

Setting this area to address $0000 wouldn't be good. This means I waste a bunch of my precious zeropage variables.

$07FF might work though. I'll have to declare the reset-persistent segment at the end of RAM and then I move the software stack some bytes to the front.
Since I use a CFG file for this, accidental overlapping shouldn't be possible.
Then I'll simply end the loop as soon as the reset-persistent area is reached.
Re: Why does this RAM initialization code work?
by on (#201396)
The code will take a while, a long while to run, all interrupts/nmis are disabled while it runs ?
Re: Why does this RAM initialization code work?
by on (#201397)
Yes.
Re: Why does this RAM initialization code work?
by on (#201398)
Yes in this case you will need X and Pointer+1 to be equal otherwise you will always write $0800 bytes, and since the NES repeats RAM again after $0800 you end up writing to $0000 again.

so for example $0200 + $0700 = $0900 which is the same as $0100 which will explain why you don't see any X's in the case where you try to only clear part of it.
Re: Why does this RAM initialization code work?
by on (#201399)
I was lucky that it works in my game "City Trouble" simply because the reset-persitent area is only 13 bytes big and it isn't positioned in a location where the low byte overflows and the high byte gets incremented. So, Pointer + 1 remains the same before and after that check anyway.
But programming-wise it's of course a bug and I'll correct it for the next game.
Re: Why does this RAM initialization code work?
by on (#201405)
Using a pointer variable means that everything was cleared to zeroes, except for the pointer itself.
Re: Why does this RAM initialization code work?
by on (#201407)
Huh? What are you referring to in this specific context?
Re: Why does this RAM initialization code work?
by on (#201409)
Because read-modify-write operations (ASL, LSR, ROL, ROR, DEC, INC) still set the NZ flags, your RAM clearing routine can be golfed by using the pointer's high byte as the loop counter:
Code:
   LDA #0  ; The value to write to RAM, and the start of a page
   LDY #$07  ; The first page to clear, in reverse order
   STA Pointer+0  ; Write the pointer
   STY Pointer+1

   TAY  ; Y is also the loop counter
   ; If you have reset-persistent data at, say, $700-$70F,
   ; do LDY #$10 instead

@initializeRamLoop:

   STA (Pointer), Y  ; Write 0 to to the current address.
   INY  ; Move to the next address in the page
   BNE @initializeRamLoop  ; If page not done, continue
   
   DEC Pointer+1  ; Go to previous page
   BPL @initializeRamLoop  ; If not wrapped around into ROM, continue
   
   ; Pointer+0 is always 0, Pointer+1 is now $FF
   STA Pointer+1  ; So write 0 there
Re: Why does this RAM initialization code work?
by on (#201417)
mikejmoffitt wrote:
Why such a complicated RAM-clearing routine?

Adapted from the wiki:

Code:
   ldx #0
   txa $0

@clrmem_top:
   sta $000, x
   sta $100, x
   ; Reserving $200 for OAM display list, sta $200, x if you want
   sta $300, x
   sta $400, x
   sta $500, x
   sta $600, x
   sta $700, x
   inx
   bne @clrmem_top

His version is not very complicated, in fact it's pretty much standard, and note the fact that while your version is faster, his version is more compact: your algorithm takes 27 bytes, his takes 22 bytes, and could take only 20 bytes if we didn't care of the pointer value at the end. For some code initializing things, ran once at the beginning, speed is not important, but size is.
Re: Why does this RAM initialization code work?
by on (#201418)
Jarhmander wrote:
His version is not very complicated, in fact it's pretty much standard...

It does seem to be the standard for commercial era games to initialize memory with an indirect Y addressed loop. Here's the memory clear from Super Mario Bros. (doppelganger's disassembly):
Code:
;$06 - RAM address low
;$07 - RAM address high

InitializeMemory:
              ldx #$07          ;set initial high byte to $0700-$07ff
              lda #$00          ;set initial low byte to start of page (at $00 of page)
              sta $06
InitPageLoop: stx $07
InitByteLoop: cpx #$01          ;check to see if we're on the stack ($0100-$01ff)
              bne InitByte      ;if not, go ahead anyway
              cpy #$60          ;otherwise, check to see if we're at $0160-$01ff
              bcs SkipByte      ;if so, skip write
InitByte:     sta ($06),y       ;otherwise, initialize byte with current low byte in Y
SkipByte:     dey
              cpy #$ff          ;do this until all bytes in page have been erased
              bne InitByteLoop
              dex               ;go onto the next page
              bpl InitPageLoop  ;do this until all pages of memory have been erased
              rts

Do any commercial games do the "list of absolute X" style initialization? I see it all the time in homebrew ROMs, but we have kind of an insular code culture here, and a lot of our popular examples are not derived from commercial games at all.

Jarhmander wrote:
your algorithm takes 27 bytes, his takes 22 bytes

Tiny nitpick, but technically 26 on ca65, 27 on NESASM as written (i.e. whether that STA $000, X gets turned into a ZP or ABS instruction... though you can force it with < on NESASM).
Re: Why does this RAM initialization code work?
by on (#201419)
dougeff wrote:
I have no idea what your last question is asking.

How about this...the memory persistent area is zeropage 00-03

Start your loop with LDY #4.

EDIT-Alternatively, you could use $7fc-7ff as the memory persistent area, and reverse your loop, starting at $7fb, going down to $000

Yet alternative way would be to start the memory clearing loop from the end of the persistent area, and loop for 2048 bytes minus the size of the persistent area. Mirrored memory at $800..$FFF makes this possible. Might not be very convenient to implement though because of the non-page-aligned start/end addresses.
Re: Why does this RAM initialization code work?
by on (#201427)
I'm posting my final version of the function for completeness.

I decided against putting the reset-persistent area at the end of RAM after the C stack. Because in this case, I would have to declare an absolute size for it in the cfg file. But I prefer the reset-persistent area to be just a RAM segment that grows and shrinks according to the global variables I actually declared in the program.

Of course, if you can suggest me a way to declare the NO_RESET part after the stack, but without manually declaring a size for it (i.e. the stack location automatically gets shifted to the left when I declare more variables in NO_RESET), I might change it.
This is the relevant part of my config file (I removed all the ROM- and zeropage-related stuff):
Code:
SYMBOLS
{
   __STACKSIZE__  = $0100;
   __RAMSTART__   = $0200;
   __RAMSIZE__    = $0800 - __RAMSTART__ - __STACKSIZE__;
   __STACKSTART__ = __RAMSTART__ + __RAMSIZE__;
}

MEMORY
{
   RAM:     type = rw, start = __RAMSTART__,   size = __RAMSIZE__,   file = "";
   STACK:   type = rw, start = __STACKSTART__, size = __STACKSIZE__, file = "", define = yes;
}

SEGMENTS
{
   SPRITES:  load = RAM, type = bss, align = $0100;
   FAMITONE: load = RAM, type = bss, align = $0100;
   BSS:      load = RAM, type = bss;
   NO_RESET: load = RAM, type = bss,                define = yes;
}



Alright, and here the actual code.
The code is put between the two wait for vblank calls.
X was already set to 0 before.
Code:
   ; In the meantime, set the
   ; whole RAM memory (i.e. all
   ; variables) to 0.
   ; The RAM goes from address
   ; $0000 to $07FF.
   ; The current address value
   ; goes into a two bytes pointer.
   ; The values are set in two loops.

   ; The X and Y registers are
   ; used as counters for the
   ; loops. A is used for
   ; overwriting the RAM values.
   ; X is still 0 from above.
   ; A and Y are set to 0 now.
   TXA
   TAY

   ; A, X and Y all have
   ; the value 0 now.

   ; Pointer + 0
   ; always remains 0.
   ; The address is calculated
   ; by Pointer + 1
   ; and by Y.
   ; X, which acts as a
   ; counter, always has to be
   ; kept in sync with
   ; Pointer + 1.
   STY Pointer + 0
   STX Pointer + 1

   ; The fact that the pointer
   ; will overwrite itself in
   ; the following process is
   ; not an issue:
   ; Pointer + 0 is
   ; always 0 anyway, so overwriting
   ; it with 0 makes no problems.
   ; And since the pointer is in the
   ; zeropage, Pointer + 1
   ; only gets overwritten with 0
   ; when it's still 0 anyway.

@initializeRamLoop:

   ; There is a certain RAM area that
   ; shall not be initialized with zeroes
   ; because this area shall be persistent
   ; when the Reset button is pressed.
   ; We check if we reached that RAM area.

   ; The high byte of the
   ; address is checked.
   CPX #>__NO_RESET_LOAD__
   BNE @noNoResetSkip

   ; The low byte is checked.
   CPY #<__NO_RESET_LOAD__
   BNE @noNoResetSkip

   ; If we reach the reset-
   ; persistent area, we change
   ; the address and our counters
   ; (Pointer + 1, X and Y)
   ; to the first value after the area.
   ; This way, the reset-persistent
   ; area doesn't get overwritten.
   LDY #<(__NO_RESET_LOAD__ + __NO_RESET_SIZE__)
   LDX #>(__NO_RESET_LOAD__ + __NO_RESET_SIZE__)
   STX Pointer + 1

   ; Since the C stack always comes
   ; after the regular RAM variables,
   ; no global variables will ever
   ; be right at the end of the RAM.
   ; So, we don't need to check here
   ; whether writing to the next
   ; memory location is even allowed
   ; anymore. We can be sure that we
   ; haven't reached $0800 yet.

@noNoResetSkip:

   ; Set the value of 0,
   ; which is still in A,
   ; to the current address.
   STA (Pointer), Y

   ; Increment the low byte
   ; part of the address.
   INY

   ; If it is set back to 0,
   ; increment the high byte
   ; part in the pointer and
   ; in the counter.
   ; Otherwise, continue
   ; with the inner loop.
   BNE @initializeRamLoop
   INC Pointer + 1
   INX

   ; The outer loop ends
   ; just before address
   ; $0800.
   CPX #$08
   BNE @initializeRamLoop

   ; Set the pointer itself
   ; to 0 as well.
   ; Pointer + 0
   ; is always 0. So, we only
   ; need to set
   ; Pointer + 1.
   ; A is still 0 from above.
   STA Pointer + 1

   ; The RAM has now been
   ; initialized with all zeroes.