kobold-wyx

vtuber and magical rogue

evil nonbinary kobold vtuber whomst gender is queer adventurer

...

please gently the kobolds

...

email:
wyx [[AT]] koboldinteractive [[DOT]] com

...
Irregularly streaming on Twitch @kobold_wyx!

...

avi by @keinga
header by @ultchimi


typhlosion
@typhlosion

previous | next

it's been a little bit, huh! sorry. i got distracted by life. you know how it is. but now we're back and making code for a 40-year-old game console, so let's hop right in

by the end of this episode: the same thing as before, but better organized


where we left off

last time on nesdevlog i struggled to present a whole code file in a good way. shortly after i published that post someone reminded me that github gists are a thing. so here you go! it's even syntax-highlighted so it's easier to read. all it does is turn the screen white, but if you run it through our tools:

cl65 hello.s -t nes -o hello.nes

it will produce a valid NES rom, which you can run in mesen or any other NES emulator, or (i'm pretty sure) even a physical console if you have a flashcart, and it turns the screen white just like i promised

most of the contents of this file are stuff i went over in the first two episodes, but i wanted to talk a bit more about the _main routine i threw in at the end, because there's some interesting stuff going on that will be important to understand. for easier reference:

.segment "CODE"
_main:
    ; lets try to set background color (in this case white is $30)
    ldx #$3f
    ldy #$00
    stx $2006  ; write $3f00 to the PPU address register
    sty $2006  ; i.e. the VRAM address we're gonna change is at $3f00
    lda #$30
    sta $2007  ; write $30 to the PPU data register (white)

    lda #%00011000  ; turn on bg and sprite rendering
    sta $2001    ; so we can see the change
:
    jmp :-  ; loop forever

the PPU is the picture processing unit, essentially the NES' graphics hardware. it has its own RAM, which we call VRAM, and the CPU can't access it directly - all the CPU sees is a small handful of memory addresses that are mapped to different controls and ports on the PPU. we saw a few of these in episode 2, like address $2002 to check whether the PPU is doing stuff or not

in this routine we write to $2006 and $2007, which are the VRAM address and data registers respectively - this is how we can write data into the PPU's memory to change things like color palettes and where tiles go on screen. here is the nesdev wiki article describing how the PPU's memory is laid out, but the only thing we're interested in right this minute is the address $3f00 - the beginning of the palette data, where the "universal background color" lives

in order to put stuff into that address in VRAM, we need to write it into the address register, which is what stx $2006 ("store X register into address $2006") and sty $2006 ("store Y register into etc.") are doing - because addresses are two bytes long, we need to write to it twice. now we can write whatever we want to put in that address into $2007 - in this case that's hex value $30, which is the byte that corresponds to the color white. (you can see the palette table if you scroll down the PPU palette data page, if you want to tweak this code to make the screen be a different color instead - for instance, if you wanted to make the screen purple, you could do sta #$13 instead)

after that's done, we write %00011000 (that's a binary number in assemblerese) to the PPU mask register at $2001 to turn on background and sprite rendering, so that we can actually see the change we've made, and then loop forever

hey kassy these memory addresses are getting kind of hard to keep track of

you're right, convenient and timely section header. thankfully, we can give them names and then refer to the names in our code rather than having to put the address everywhere, which makes the code much easier to read

here are all the ones we've used so far, plus a couple extras that will be handy later:

PPU_CTRL     = $2000  ; miscellaneous PPU-related flags
PPU_MASK     = $2001  ; for controlling which stuff the PPU draws
PPU_STATUS     = $2002  ; for figuring out how the PPU is doing
PPU_OAM_ADDR = $2003  ; OAM is object attribute memory, where sprite info lives
PPU_OAM_DATA = $2004
PPU_SCROLL     = $2005  ; for controlling the screen scroll
PPU_ADDR     = $2006
PPU_DATA     = $2007
PPU_OAM_DMA     = $4014  ; DMA is a way to copy a bunch of data into the OAM fast

APU_FRAMECNT = $4017  ; APU frame counter
APU_DMC_FREQ     = $4010  ; APU DMC (it has to do with sample playback)

CTRL_PORT1     = $4016  ; for reading controller data
CTRL_PORT2     = $4017

you might have noticed that $4017 is on here twice. that's because some of the addresses have different functions depending on whether you're reading to them or writing from them! when you write to $4017 you're talking to the APU, but when you read from it you're getting the player 2 controller data, so to make the code more legible we name that address after both of its functions

anyway, with these names, the code gets a lot nicer to read. we could just stick them straight into the hello.s file, but i'm going to put them in a separate file - let's call it "addrs.inc" to indicate that it's meant to be included into other assembly files. we do that by sticking this line at the beginning of the file:

.include "addrs.inc"

and the assembler knows to basically splice that file's contents in before doing much else, so that it knows what all the names we're referencing are

dividing and conquering

let's split up our project a little bit more. the main non-startup code might get pretty long, so it makes sense to put that in a separate file. just gotta take the main routine and pull it out into, let's call it, main.s, remembering to include the addrs file again:

.include "addrs.inc"

.segment "CODE"
_main:
    ; lets try to set background color (in this case white is $30)
    ldx #$3f
    ldy #$00
    stx PPU_ADDR  ; write $3f00 to the PPU address register
    sty PPU_ADDR  ; i.e. the VRAM address we're gonna change is at $3f00
    lda #$30
    sta PPU_DATA  ; write $30 to the PPU data register (white)

    lda #%00011000  ; turn on bg and sprite rendering
    sta PPU_MASK    ; so we can see the change
:
    jmp :-  ; loop forever

and then in order to compile it, we just have to tell cl65 it needs to use both files:

$ cl65 hello.s main.s -t nes -o hello.nes
hello.s:63: Error: Symbol '_main' is undefined

...huh. oh, right! any symbol we define in one file and use in another has to be exported from the file that defines it and imported anywhere that uses it. so we just gotta .import _main in hello.s and .export _main in main.s, and then the assembler knows what to do. now we should get... a rom that does exactly the same thing as it did before, but with better project management. oops, we didn't really do much this time around, huh

i'm going to rename hello.s to crt0.s, for reasons that will become apparent much later on (those who know a lot about how C works might be nodding along already), and add a couple other bits and bobs, and now it's ready to put on github - go check it out to see how my version of this project looks, and compare it to your project if you've been following along. there will be other branches for subsequent episodes. please don't be mean at me for my bad commit discipline i am doing my best

join me in episode 4 where we start making graphics happen!


You must log in to comment.

in reply to @typhlosion's post:

I never knew about that thing with $4017 until now, but it does reinforce my view that having names for memory addresses makes all the difference in the world.