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

last time on this devlog, i talked way too much about how compilers and linkers work and then made a do-nothing binary file that mesen recognized as a NES rom. today i'm gonna start writing some code!

by the end of this episode: we'll make graphics appear on the screen! strap in, this is a long one


when we left off, this is what was in my file:

.segment "HEADER"  ; 16-byte iNES header
ines_header:       ; you need it for an emulator to recognize the file
    .byte $4e, $45, $53, $1a  ; "NES" and then ms-dos eof character
    .byte $01                 ; size of prg rom (x 16kb)
    .byte $01                 ; size of chr rom (x 8kb)
    .byte $00                 ; flags 6
    .byte $00                 ; flags 7
    .byte $00                 ; flags 8
    .byte $00                 ; flags 9
    .byte $00                 ; flags 10
    .res 5, $00               ; five bytes of zero padding

.segment "STARTUP"  ; cc65 puts startup-related code here when building from C
startup:
    ; code will go here

.segment "VECTORS"  ; the 6502 looks here to figure out where to jump to when certain things happen 
rom_vectors:
    .word $0000  ; nmi vector
    .word $0000  ; reset vector
    .word $0000  ; irq/brk vector

.segment "CHARS"  ; 8kb (0x2000 bytes) of character (graphics) data
.res $2000, $ab

i figured i should actually talk about the syntax here, since we're gonna be seeing a lot of it going forward

the ; symbols you see are the beginnings of comments, which make the assembler ignore whatever comes after them on that line. great for reminding yourself what your code does

words starting with . are basically commands to the assembler, which tell it to insert something or treat the following data in a certain way. for instance, .segment tells it that everything until the next .segment (or the end of the file) should be in the segment with the given name (the linker uses this info later to put things in the right spots). .byte and .word tell the assembler to insert a certain byte, or a 16-bit word, at that location. .res x, b fills an area x bytes long with byte b. we'll see more of these later and talk about them when they come up

words ending with : are labels, which indicate to the assembler and linker what addresses it should keep track of. for instance, if i want to jump to the startup routine, i can just write jmp startup and the assembler and linker calculate whatever address that label ends up at so i don't have to

the init code

now it's time to clap our hands twice, bow once, and start reciting the ancient words of power. that is to say, it's time to start writing assembly code. there's a lot of work we have to do in order to get the NES to a state where we can start making things happen. luckily for us, the nesdev wiki has a pretty good page describing all of the things we need to do in this init code. i'm gonna basically use this code verbatim, but in case you want better insight on what's going on, you might be interested to step through it

so, let's step through it the first thing we have to do right away is prevent anything that could interrupt the startup sequence - `sei` disables maskable interrupts. interrupts are signals that other bits of hardware can send to the CPU to make it take care of something urgent before returning to whatever it was doing before. the 6502 recognizes three different kinds: NMI (non-maskable interrupts), reset, and IRQ. the vector table is there so the 6502 knows where to execute code from when it receives one of these interrupts! anyway we need to turn this off for now - `cld` disables decimal mode. the ricoh 2a03 (the version of the 6502 that the nes uses) doesn't actually *have* a decimal mode, but some 6502 debuggers complain if they don't see this, so it's good practice - `ldx #$40` puts into the X register an immediate (that's what `#` means) hexadecimal (that's what `$` means) value of 0x40, or 0100 0000 in binary. we then store this value into a special memory location with `stx $4017`. a good chunk of the memory addresses in the NES are used for communication between the CPU and other hardware; $4017 in particular is used for some audio stuff, and setting byte 7 ($40) prevents it from asking for interrupts - `ldx #$ff` and `txs` put the stack pointer into the expected place. then `inx` adds 1 to the value in x, wrapping the value around from 255 ($ff) to 0 - then we store that 0 into three other special memory addresses in order to prevent NMIs, keep the NES from drawing anything to the screen, and disable more audio-related IRQs.

at this point, we've turned off most things that can interrupt the rest of startup. if there's a mapper or some other fancy extra hardware on the cartridge, this is where we'd set that up, but for now we can move on

the other problem we have to contend with is that, when the NES first boots up, the PPU (the bit of hardware that controls what gets drawn to the screen and how) is in an unknown state. this sorts itself out eventually, but we have to wait for about two frames in order to make sure things are stable. that's what the @vblankwait1 and @vblankwait2 loops are for

in between the two wait loops, we fill all the RAM addresses with zeroes; that's what the @clrmem loop does. this isn't strictly super necessary, but it's handy to know that nothing is in a potentially unknown state

where are we now

okay. so. we've covered a lot of ground! now we can write a little bit of extra code to turn the screen background white - i'm not gonna step through the new code just yet in this episode, we'll go into it in more detail in episode 3 when we learn how to split our code across multiple files (keep an eye out!), but here's a view of the full hello.s file as we have it now:

...

hey kasran where's the code preview

well i'm having trouble getting cohost's code blocks working. you can't put markdown inside html, but if i try to put <pre><code> inside a <details> tag then for some reason the code still gets line breaks and paragraph tags automatically inserted into it and it ends up looking busted. i don't want to just vomit a long uncollapsible code block at you, so i'll have to find another solution for embedding large pieces of code in a nice way

tell you what: episode 3 will also be the debut of the nesdev github repo, and i'll make branches or flags or something for the repo as it exists at the end of each episode starting with 3. you can look at the full code then. for now here's a rundown of the changes i made in order to make it work

  • changed the "size of prg rom" byte of the ines header to 2, because i had it wrong earlier
  • changed the byte the CHARS segment is filled with to 00 so the chars wouldn't get in the way of the white background
  • added jmp _main to the end of the startup code
  • added this block:
.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

join me for episode 3 when i figure out how to show you code on cohost in a way that doesnt suck and then we start doing project management


You must log in to comment.