Nes ASM question: Wait/Delay?

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
sotriuj
Posts: 1
Joined: Mon Mar 18, 2019 4:44 pm

Nes ASM question: Wait/Delay?

Post by sotriuj »

Hello! I'll like some clarifications about some code I wrote right now.

I followed the tutorial of the site https://book.famicom.party/chapters/05- ... embly.html , and I copy pasted the program that simply changed the background and figured I'll get my hands dirty and decided to write a loop that will go over a bunch of different colors.

So anyway, this is the relevant piece of code:

Code: Select all

.proc main
  LDA #$00
background:
  LDX PPUSTATUS
  ADC #$01
  LDX #$3f
  STX PPUADDR
  LDX #$00
  STX PPUADDR
  STA PPUDATA
  LDY #%00011110
  STY PPUMASK
  LDX #$FF  
loop:
  LDY #$FF
wait:
  NOP
  DEY
  CPY #$00
  BNE wait
  NOP
  DEX 
  CPX #$00
  BNE loop
  JMP background
The reason I have loop: and wait: is because, when I assembled the rom without those, I got a mess of colors in the emulator (Nestopia if its relevant):
Image
I figured this is because the color is changing too fast, so I add the two nested loops to add a bunch of NOPS just to see if that was the reason and sure enough, now I get a nice, solid background that last for a few seconds.

So my question is: This feel incredibly hacky, how does one exactly wait a certain ammount of time for events to happen? I guess Ideally you'll be able to wait for certain ammount of frames to have passed? This is my first time programming for a console and coding with ASM, so I'm a little bit lost here.
lidnariq
Posts: 11432
Joined: Sun Apr 13, 2008 11:12 am

Re: Nes ASM question: Wait/Delay?

Post by lidnariq »

The NES itself only provides you with four total possible clock sources:

1- The CPU itself runs at a certain speed. Each instruction takes a certain amount of time. However, this method is a total pain to do anything with, so few people have bothered unless they need it in addition to one of the following.

2- The PPU can provide a signal every vertical redraw. Almost everything ever written for the NES uses this. In NTSC territory, this signal happens at 60Hz; in PAL it happens at 50Hz.

3- The CPU's sound engine provides another signal that's almost the same as #2, but slightly different. I don't think anything really used it.

4- The CPU's sound engine can provide a signal every time its audio hardware needs more data for its sample playback mechanism. Although there(Bee 52) are things that used this for non-audio purposes, most of the time this is just used for sample playback.

(5- Some cartridges provide the ability to request specific times more precisely. But not all!)
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Nes ASM question: Wait/Delay?

Post by koitsu »

If that's literally all your code is doing, then yes, the behaviour you get is (more or less) correct. You're just spewing data at the PPU constantly, with no concept of when it's proper to do so. You need to learn what VBlank/NMI is, which should answer all your questions (and ensure you can do something at a set rate of time, i.e. 60 times a second for NTSC, or 50 times a second for PAL). It's partially (but not entirely) covered in the next chapter of what you're reading. Otherwise, refer to the wiki, else ask questions w/ code here.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Nes ASM question: Wait/Delay?

Post by tokumaru »

Basically you shouldn't be writing to the PPU in the main loop. The CPU and PPU run in parallel, so if you have the CPU change colors whenever, without any regard for what the PPU is doing, you're gonna get corrupted results during the entire frame. You may get away with writing to the PPU in the main loop during initialization when the screen is off, but other than that, you shouldn't do it.

The absolute reference of time on the NES is the vertical blank (vblank), which's the little bit of time that comes after the PPU is done drawing a frame and lasts until it starts drawing the next frame. It's during this time that you can make changes to the screen, because the PPU itself isn't using the video memory then. To help programmers know when vblank starts, the engineers of the NES made it so that an NMI fires when vblank starts, causing the CPU to stop whatever it's doing and jump to the NMI handler... And it's via the NMI handler that you keep track of time, because we know that vblanks happen at the rate of the video signal (60fps on NTSC, 50fps on PAL).

What you have to do is limit the rate at which your main loop runs to once per frame, by having it wait for the next frame before looping back. A common way to do this is the following:

Code: Select all

NMI:
  ;(...)
  inc FrameCounter ;change the frame counter
  rti ;return from the NMI

Code: Select all

Main:
  ;(...)
  lda FrameCounter ;get the frame counter
Wait:
  cmp FrameCounter ;check if it changed
  beq Wait ;go check again if it didn't
  jmp Main ;now you can process the next frame
This just increments a variable once every vblank, so the main loop can simply wait for this variable to change before processing another frame, and since this variable only changes 60 times per second, the main loop will also run 60 times per second, and your game logic is now synced to the video.

Like I said before, you cannot change the video memory (which includes the palette) while the PPU is drawing a picture, so you need to put the actual write to the palette in the NMI handler, to make sure it happens during vblank:

Code: Select all

NMI:
  lda #$3F
  sta PPUADDR
  lda #$00
  sta PPUADDR
  lda BackgroundColor
  sta PPUDATA
  inc FrameCounter
  rti
This is a simple NMI handler that just writes the current background color to the palette every frame. It's up to the main loop to change that value periodically:

Code: Select all

  lda #1 ;make it so the delay expires immediately
  sta Delay
  lda #$00
  sta BackgroundColor

Main:

  dec Delay ;decrement the delay
  bne ColorReady ;don't do anything if it didn't reach 0 yet
  lda #60 ;prepare to wait 60 frames (1 second)
  sta Delay
  inc BackgroundColor ;change the color
ColorReady:

  lda FrameCounter
Wait:
  cmp FrameCounter
  beq Wait

  jmp Main
Something along these lines.
Post Reply