I'm not new to 6502 Assembly but I am new to the Commodore 64. I'm used to the NES where you have to write everything yourself, such as the NMI/RESET/IRQ vectors, etc, but from what I understand the BIOS takes care of all that for you. If I'm programming a disk image program on the c64, how can I wait for vBlank? I'm trying to figure out how to slow down an animation. This question isn't about the built-in Commodore BASIC, I'm not using that at the moment.
-
1Have you read any C64 graphics programming tutorials yet?– JustmeSep 7, 2021 at 6:12
-
3(1) Ask Godot to watch the vBlank. (2) Wait for Godot.– Jon CusterSep 7, 2021 at 18:34
-
Have a read of c64-wiki.com/wiki/Raster_interrupt It will tell you how to raise an interrupt based on the screen rastering– JeremyPOct 8, 2021 at 7:54
-
1BIOS? KERNAL. As long as you haven't paged it out.– Alan BOct 8, 2021 at 9:00
2 Answers
I'm used to the NES where you have to write everything yourself, such as the NMI/RESET/IRQ vectors, etc, but from what I understand the BIOS takes care of all that for you
The "BIOS" in the Commodore 64 is the Kernal. The Kernal does handle IRQs and NMIs, but before it does, it indirect jumps through a RAM vector (different than the hardware 6502 vectors in the upper 6 bytes of ROM), so you can hook in your own routine.
Unlike the NES, the VIC video chip does not generate an NMI on each frame. It can generate an IRQ but this is not enabled by default.
There's quite a few things that can generate IRQs and NMIs on the C64. The usual situation: IRQs on the C64 are normally driven by the CIA 1 timer (CIA 2 generates NMIs) which is set by the Kernal on power up to fire every 1/60th of a second. The background tasks done are updating the jiffy timer and scanning the keyboard.
You can turn that off and switch to the VIC though.
Having the VIC generate IRQs is very useful for games to sync for VBlank, so what you need to do is:
SEI
Update RAM vector ($0314-$0315 - look for
CINV
in the Commodore 64 Programmer's Reference Guide) to your IRQ routine.At the time the ROM does its
JMP ($0314)
, it has saved the registers on the stack and established that the IRQ is not caused due to theBRK
instruction.Keep the old value (which is probably
$FF48
) because your IRQ routine should jump to it when yours is done. That will take care of restoring CPU context back to pre-IRQ state.
Then
Disable CIA 1 interrupts by writing 0 to it's interrupt enable bit.
Program the VIC register that tells which scanline to trigger the IRQ on.
Write to the VIC interrupt enable bit.
CLI
Also, in your IRQ routine you need to acknowledge the VIC's IRQ by writing to $D019 - towards the end of your IRQ routine might be best. VIC won't generate another IRQ until you do that. The linked example above explains.
The CIA needs it's IRQ acknowledged too - keep in mind the Kernal will still write to $D0CD to do that, but will be harmless since it's ability to generate IRQs is disabled.
Something to keep in mind - you could hook into the IRQ, not disable the CIA, then implement this logic in your handler:
check the IRQ flag on the VIC
if VIC caused IRQ
do what you want to do during VBlank.
check CIA IRQ flag, and if set, JMP to $FF48 [might be needed in case CIA and VIC try to IRQ at or near the same time]
either way after checking JMP to $EA81--point in Kernal ROM that simply restores registers and RTI (CIA did not cause IRQ).
else
- JMP to $FF48 for normal IRQ processing (CIA caused IRQ).
If you want to do it NES style that's the general process.
-
1If one doesn't mind having to write different code for different models of C64, using the CIA may in some cases be superior to using the 6502. Among other things, the CIA would allow code to determine how many cycles elapsed between when an interrupt fired and when it was taken, so an IRQ that uses a "cmp/nop" slide could easily align itself with a precise known beam position.– supercatOct 19, 2021 at 17:51
Here is one way to do it. Note that this approach is quite wasteful; it just wastes time by going round and round in stupid loops until the next frame is reached. It would probably be nicer to do some kind of useful work in the meantime. You could use interrupts to achieve that, but well, it depends on how you want to structure your code.
On to the subroutine that waits for the next frame.
wait_for_next_frame:
bit $d011
bpl wait_for_next_frame
lda $d012
f: cmp $d012
bmi f
rts
So the first instruction, the bit
, checks bit 9 (!) of the raster counter. If this bit is not set, then the raster is still quite high in the screen, so the routine will branch back and check again.
Otherwise, we'll proceed to lda $d012
, or load the (lower 8 bits of the) raster counter into the accumulator. By continously comparing with the the current value, the program determines when the raster goes back to the start.
Hope that's clear, just do jsr wait_for_next_frame
and you should be good.
-
1There are issue with this code. First, comparing equal values resets the N flag and the second loop doesn’t, well, loop. Second, If the scan line counter wraps between reading $d011 and reading $d012 then this routine returns at a raster index around 128 (barring the other issue). It is easier to detect that the msb of $d011 goes from 1 to 0.– WimCSep 7, 2021 at 16:35
-