Need Some Direction

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Need Some Direction
by on (#15197)
OK, so I got this crazy ambition to write my own NES game (I love the NES and have some light programming experience), problem being I don't know where to go. I basically have this so far:

- Need to learn 6502 ASM
- Need to learn NES hardware, processors, etc.

I've been to nesdev.com, but I don't have any clue as to where I should start. Its like a bunch of documents with no intro page.

Someone really needs to make one...

Can you suggest any documentation I need to consult first (everything seems so abstract right now)?

Many thanks.

by on (#15200)
The big thing (besides learning 6502 like you mentioned), is understanding the PPU. Try to get an idea of how the name tables and pattern tables work (viewing them in games running in an emulator's debugger can help, tho not many emus display the nametables). I just find that it's a lot more fun when you can actually put results on the screen. But stepping through code in a debugger might help (and you definitely will want to do that, when you have any code doing something you don't expect).

Another thing is to learn how to understand hex and binary numbering.

The programming section of the old NES Tech FAQ has some pretty decent introductory info. http://nesdev.com/NESTechFAQ.htm#programming

There doesn't seem to be really a whole lot of basic info about how to get started with the NES. A lot of it overlaps with doing microprocessor-type stuff in general, and with things like the PPU the really detailed docs are about the really obscure aspects of it (that probably almost none of the original NES game developers knew about, or needed to know about).

I've always thought this doc is good, for reference about the registers, memory map, etc.
http://nesdev.com/ndox200.zip

by on (#15202)
Learn a bit about tiles, nametables and attribute tables before learning coding by playing with Nesticle or FCEUltra would definitely be a good thing. I remember I got interested in NESdev by playing a lot with FF3 tiles in Nesticle. Nesticle is a totally outdated and innacurate emulator, but it allow the player to learn how the game deals with tiles, and this makes it a worthy emulator for newbies.

Memblers : You definitely have to refresh the NESdev main page. I know it is annoying to refresh a website, my page about Chrono Trigger havent be updated since march 2005 and still show a notice for Chrono Trigger's ten years anniversary, even if the game now have almost 11.5 years. Anyway, I think my page is much less visited than yours.

by on (#15209)
Nintendulator, in addition to being several times more accurate than Nesticle, is pretty good for watching pattern tables and nametables nowadays.

by on (#15220)
Bregalad wrote:
and this makes it a worthy emulator for newbies.


You must never have used FCEUXD


There is no reason to suggest NESticle for debugging over FCEUXD -- FCEUXD has everything. The only thing it lacks is a runtime CHR-ROM editor (though it does have a runtime hex editor).

by on (#15223)
That was just why I say Nesticle is good to discover the PPU : It allow you to see tiles and edit them. FCEUXD, VirtuaNES and possibly Nintendulator allow pattern table viewing, but the pattern tables are too small to allow someone to really explore the tileset, and you don't get the tile # when clicking on one, and you cannot edit it.

I think FCEUXD is good to for this purpose, but it doesn't really replace Nesticle.

by on (#15224)
Disch wrote:
(though it does have a runtime hex editor)

It has a runtime assembler... that's awesome!

FCEUXD has way more stuff than Nesticle. The only thing Nesticle has that FCEUXD doesn't is the CHR editing, but that is pretty useless when it comes to debuging IMO... you can still easily spot a tile without having to draw an "X" over it...

Anyway, i don't think this is such an inportant feature. beeing able to edit memory is much more usefull for someone to understand how things work.

by on (#15263)
Some questions now...

How big can A be? For instance if I wanted to perform larger (> 8-bits) calculations.

I'm kind of hazy on how 6502 handles signed and unsigned numbers, explanation would be helpful. I'm getting the impression that it only has signed ones.

How do you end a branch? The examples I'm reading go something like this:

lda some_variable
cmp some_other_variable
beq exe_this_code
(continue if false)

exe_this_code
(execute if true)

But what if I want multiple branches?

Seems like I'm asking a lot... Thanks again for answers.

by on (#15268)
random wrote:
How big can A be?


8-bits. That's all. No more, no less.

Quote:
For instance if I wanted to perform larger (> 8-bits) calculations.


This is what the C flag (carry flag) in the processor status register is for. for example, if you want to add 2 16-bit numbers together, you'd add the low 8-bits of each together first, and then if the result was >= $100, the C flag will be set, which will allow you to add it into the addition of the high 8-bits of each number.

That explaination wasn't bery good. Remeber that if C is set at the time of ADC, an extra 1 will be added to the sum:

Code:
CLC ; clear carry initially

; add low bytes together
LDA valueA_low
ADC valueB_low
STA sum_low   ; store the sum -- note that if the sum was > $FF, C is set

LDA valueA_hi
ADC valueB_hi
STA sum_hi


Consider you have the following 2 16-bit numbers that you want to add:
$0362
$04F3

first you clear C (CLC) so the extra 1 won't be in your addition... then you add the low bytes of the numbers together:
$62 + $F3 = $55 <-- note that the sum is $55 and not $155 (because it can only be 8-bits). However because it was greater than $FF, C is now set.

So now you add the high bytes together: $03 + $04 = $08 <--- note that it's $08 because C is set by the previous addition, so the extra 1 got added.

therefore... $0362 + $04F3 = $0855.


C works in a similar fashion for multi-byte ASL/ROL/LSR/ROR commands. And works "backwards" for SBC, but the same logic applies.[/quote]


Quote:
I'm kind of hazy on how 6502 handles signed and unsigned numbers, explanation would be helpful. I'm getting the impression that it only has signed ones.


Well, it doesn't handle them at all really. In fact, if anything I'd say the numbers are all unsigned.

What really makes a number signed or unsigned is how your program treats it. The processor doesn't care one way or the other. $FF can be either 255 or -1.... the processor will always treat it the same regardless -- what matters is how your program works with the number.

There is a "N" flag in the processor status reg, which will be set if the result of the last operation was negative and cleared if the result was positive. All "negative" really means here is "the high bit is set". For this purpose, $00-7F are all "positive" numbers, and $80-FF are all "negative". However, again, the processor doesn't treat them any differently if they're positive or negative.... it kind of treats them all like they're positive ($80 + $80 will give you 256, not -256)


Quote:
How do you end a branch?


Branches behave just like jumps, only they will only jump under certain conditions. The condition is the current status of a status flag. Usually you will work with BNE, BEQ, BCC, and BCS.

BNE will jump if and only if the Z status flag is clear
BEQ will jump if and only if the Z status flag is set
BCC will jump if C is clear
BCS will jump is C is set

HOW these flags gets set is determined by the instruction(s) before the branch.

Therefore in your examples:

Quote:
lda some_variable
cmp some_other_variable
beq exe_this_code



that CMP instruction is comparing two values. CMP will set Z if the values equal each other, and will clear Z if the values do not equal each other. If you want to get into details, CMP actually performs a subtraction, and Z is set to the result of that subtraction -- and if the two numbers equal each other, the result is Zero, so Z is set. If the result is nonzero (the numbers don't equal each other), Z will be clear.

Code:
LDA #$03   ; Z=clear
CMP #$03  ; Z=set (3-3 = 0 -- a result of 0 = Z set)
BEQ somewhere  ; jump to 'somewhere' only if Z is set (which it was set by last instruction)

LDA #$00  ; value of $00 = Z set
BNE somewhere  ; this will not jump anywhere because Z is set by above LDA
BEQ somewhere  ; this WILL jump because Z is set by previous LDA



Quote:
But what if I want multiple branches?


You can branch as many times as you want, there's no limit. Remember that all the branches look at is the status flags, so to really understand branches, you have to understand how the status flags are set.

by on (#15269)
If you want to check for multiple cases you could do a CMP and a branch for each one. Preferably with the most probable cases first, so that the program doesn't get too slow.

To work with numbers larger than 8 bits, you make use of the carry flag. This code does a 16-bit addition:
Code:
   clc      ;clear the carry so that you don't add an unwanted "1"
   lda VarLo   ;take the low byte of a variable
   adc #<657   ;add the low byte of the number "657"
   sta VarLo   ;the low byte is ready to be stored back, but if the result was more than 255, the carry will be set
   lda VarHi   ;load the high byte of the variable
   adc #>657   ;add the high byte of the number "657" and a possible "1" if there was an overflow in the low byte
   sta VarHi   ;store the final high byte


You just have to clear the carry (or set it, if you are subtracting) before adding the first pair of bytes, and then add each of the following pairs at a time, from the least significant one to the most significant one. The carry flag will take care of propagating any carry, and you'll get the correct answer at the end.

The 6502 can handle signed and unsigned numbers. Although there is no difference between them until you perform some sort of calculation or comparison on them. The hex value $FF can be a decimal 255 or a -1. It's up to how you interpret the number.

Addition and subtraction of signed and unsigned numbers is the same, there is not a single difference, but depending on whether you consider the numbers signed or unsigned, you have to interpret the result differently. BPL and BMI are used to check for a negative or positive result, of course, if you are using signed numbers. BCC and BCS can be used to find wich of two unsigned numbers is larger.

When you compare 2 numbers, the rule is: If they are signed, use BPL and BMI, if they are unsigned, use BCC and BCS.

Also, when working with signed numbers larger than 1 byte, only the last (most significant) byte should be considered signed. You could get a little confused with this. I was when I first started studying ASM.

by on (#15271)
So if I understand correctly, signed and unsigned numbers are interpreted by you and determined with the N and C flags, right?

Pretty much understanding the other stuff. Any source I can get my hands on (probably help a lot in the learning process)?

Thanks for the explanations.

by on (#15275)
tokumaru wrote:
If you want to check for multiple cases you could do a CMP and a branch for each one. Preferably with the most probable cases first, so that the program doesn't get too slow.

Or you can divide them in half with BCC/BCS and make what is essentially a binary search tree. Or you can use a jump table like Super Mario Bros., Tetris, and the Apple IIGS ROM do.

by on (#15279)
tepples wrote:
Or you can divide them in half with BCC/BCS and make what is essentially a binary search tree. Or you can use a jump table like Super Mario Bros., Tetris, and the Apple IIGS ROM do.

I like jump tables myself, but the binary search is a nice option too. I never ran into a case where I needed to perform a huge lot of compares anyway. When I said what I did I was thinking about 3 or 4 cases... simple stuff.

random wrote:
So if I understand correctly, signed and unsigned numbers are interpreted by you and determined with the N and C flags, right?

Yes, the processor doen't care whether the numbers are signed or unsined, 8-bit, 16-bit, 24-bit or whatever... it just does the operations it's supposed to and sets the flags (N and C) accordingly. If you are gonna store the results or not, if you are gonna use the flags (and how) or not, it's all up to you.

EDIT: About the source code, http://www.6502.org/ has plenty of general 6502 examples. The main page of NESDEV has some good NES-specific code.

by on (#15280)
tokumaru wrote:
I like jump tables myself, but the binary search is a nice option too. I never ran into a case where I needed to perform a huge lot of compares anyway. When I said what I did I was thinking about 3 or 4 cases... simple stuff.

It just called to mind the horror of the CMP linear search that I found in a disassembly of the control-character processing in Higher Text II, a text rendering library for Apple II.

by on (#15289)
I'm having trouble understanding how the program counter and relative addressing works in relation with JMP and the various branch functions. Any help, is again appreciated.

Many thanks.

by on (#15294)
random wrote:
I'm having trouble understanding how the program counter and relative addressing works in relation with JMP and the various branch functions.

Well, you don't have to understand that. Unless you are programming an emulator, then you have to.

If you want to make NES games, you can trust that the 6502 will do the right thing, it always does. When branching, you don't have to worry about relative displacement, you just use a label. It's up to the assembler to convert the labels to the signed value that represents the displacement.

by on (#15308)
Yet more questions...

How exactly does the stack pointer work and what does it do? I'm getting different definitions from different docs.

While looking through some homebrewed games for the NES I'm noticing headers, ".org" seems to be common to a lot of them. The 6502 stuff I'm reading doesn't mention it at all. Naturally, I'm asking: what do they do and how do they work? Also limited information on these.

Additionally while looking through the same games, I'm seeing things like the following:

1) SOME_NAME = SOME_MEMORY_ADDRESS_OR_NUMBER

2) SOME_NAME dc.b
3) SOME_NAME ds.b

And strange (well I haven't seen it in the docs) subroutine stuff like:

4) SOME_NAME:
5) .SOME_NAME
6) SOME_NAME subroutine (following this would sometimes include the .SOME_NAME form)

Of course, again, none of the docs I'm reading explain stuff like this. I can only guess that the different subrountine forms are personal preference or something (different syntax maybe?).

Thank you for any help.

by on (#15312)
tokumaru wrote:
If you want to make NES games, you can trust that the 6502 will do the right thing, it always does. When branching, you don't have to worry about relative displacement, you just use a label.

Unless you're trying to troubleshoot "branch too far" errors and your assembler doesn't automatically convert a BNE to a BEQ around a JMP, or if you're trying to troubleshoot cycle timing errors in your raster effect.

random:
The "stack" is an area of memory from $0100 to $01FF. When you "push" something, the byte is written to the address at the current value of the stack pointer, and then the stack pointer goes down by 1. (There are only 8 changeable bits in the 6502's stack pointer; if it is currently at $0100, it wraps to $01FF.) When you "pull" something, the stack pointer goes up by 1 (wrapping from $01FF to $0100), and then the byte is read.
  • PHA and PHP are one push; PLA and PLP are one pull.
  • JSR is two pushes, and RTS is two pulls.
The .org pseudoinstruction sets the current address. This tells the assembler where your code is supposed to be loaded into memory, so that labels will be assigned correctly.

The next question (examples 1 through 6) discusses the various ways of assigning labels, or compile-time constant values. Much of this depends on the specifics of your assembler; only your examples 1 and 5 are valid in CA65.
  • name = expression
    Assigns the label 'name' to equal the value of expression. Assemblers have different limitations as to what in the expression is allowed to be unknown at compile time (e.g. labels found in other modules).
  • name:
    Assigns the label 'name' to equal the current address.
  • name: lda #0
    Assigns the label 'name' to equal the address at the start of the instruction.
  • name: .byte 1, 2, 3, 4, 5
    Assigns the label 'name' to equal the current address, and then places bytes of value $01, $02, $03, $04, and $05 here. Some assemblers use .dcb (define constant byte) instead of .byte.
  • name: .res 8
    Assigns the label 'name' to equal the current address, and then places 8 bytes of "unimportant" value. Useful when the current address is in RAM and you're trying to knock out space for a variable.
Read the CA65 manual for details.