Completely misunderstood how banks and addresses work?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Completely misunderstood how banks and addresses work?
by on (#192998)
Hey, everyone

I've been following Bunnyboy's Nerdy Nights series for a couple of weeks now. Assembler used is NESASM3.
I was moving onto the sound-section of the series, lead by Metal Slime.
Now, in this tutorial, Metal Slime has added another 16KB of PRG code memory, adding up to a total of 32KB (max. without using mappers). So far so good.

However, when I add another 16KB of memory for the code to be stored in,
Code:
  .inesprg 2   ; 2x 16KB PRG code

Then the code simply breaks entirely.
I've tried debugging it myself, and had this line of logic in my head:
Quote:
Obviously, now that there's twice as much PRG code memory, the banks have shifted, too.
0-1 used to be for PRG code, bank 2 for graphics. Double the PRG banks, and we've got 0-3 for PRG code, and bank 4 for graphics.

And thus I've changed this line,
Code:
  .bank 2 ;<---- this one
  .org $0000
  .incbin "mario.chr"   ;includes 8KB graphics file from SMB1

  .bank 4 ;<---- to this one
  .org $0000
  .incbin "mario.chr"   ;includes 8KB graphics file from SMB1

But it doesn't work, regardless. Could it be that the addresses shift by adding the 16 KB?
The same thing happens when applied to this example file.

I feel I might have mixed up a couple of things there.
TL;DR: Can someone help me figure out where my logic went wrong?
Re: Completely misunderstood how banks and addresses work?
by on (#192999)
After you've expanded your project's PRG ROM to four 8192-byte banks, here are two things to try:
  • The reset and NMI vectors need to go at the end of bank 3, not bank 1.
  • Have you tried putting at least some data in all four banks (0-3)?
Re: Completely misunderstood how banks and addresses work?
by on (#193009)
I've tried switching to bank 3 for the vector-configuration with no luck - I'm sure it's because I misunderstood what I was supposed to try out.
I'm nearly 100% this issue is a thing solely because I didn't quite understand how banks work.

For instance, I am more than confused as to why, in the Nerdy Nights series, they start bank 0 at $C000, instead of $8000, where PRG code starts.
Of course my code does that as well, but I have no idea why. To me it seems more of a hassle having it start so far out. I reckon putting each bank next to each other ($8000-A000, $A000-C000, etc.)

From Nerdy Nights alone I wasn't able to figure out exactly how it all works. Is there a good read on that, if possible in relation to 6502 NES assembly?
Re: Completely misunderstood how banks and addresses work?
by on (#193011)
PRG banks for NROM-128, which has 16384 bytes of PRG ROM, are located at $C000 and $E000. They are also mirrored into $8000 and $A000, but their canonical locations are $C000 and $E000.

PRG banks for NROM-256, which has 32768 bytes of PRG ROM, are located at $8000, $A000, $C000, and $E000.

Could you attach your code so that others can look at it to help you determine what's going wrong?
Re: Completely misunderstood how banks and addresses work?
by on (#193013)
Sure, I can. I just thought it wouldn't make much sense before, since it's so close to the original example by Bunnyboy.
After reading up on NROM again, I immediately realized you said is correct - when only 16 KB are set to be used as PRG code, it'll be mirrored to fill in the rest.

Anyhow, down below you can find the code, unaltered, working and ready for use of 16 KB, not 32 KB. When run, it should display the setup Bunnyboy made in this tutorial.
Re: Completely misunderstood how banks and addresses work?
by on (#193015)
Currently you have:
Code:
  .bank 0
  ;(stuff mapped to CPU $C000-$DFFF)
  .bank 1
  ;(stuff mapped to CPU $E000-$FFFF)
  .bank 2
  ;(stuff mapped to PPU $0000-$1FFF)

To expand this to 32KB of PRG-ROM it has to become:
Code:
  .bank 0
  ;(stuff mapped to CPU $8000-$9FFF)
  .bank 1
  ;(stuff mapped to CPU $A000-$BFFF)
  .bank 2
  ;(stuff mapped to CPU $C000-$DFFF)
  .bank 3
  ;(stuff mapped to CPU $E000-$FFFF)
  .bank 4
  ;(stuff mapped to PPU $0000-$1FFF)

Is this what you're doing?
Re: Completely misunderstood how banks and addresses work?
by on (#193018)
Yes, I have. It still has the same issues as before.
In case I am doing it wrong, here is the code with all banks assigned.
Re: Completely misunderstood how banks and addresses work?
by on (#193021)
Your vectors are in the wrong bank. You're trying to put stuff that goes at $FFFA in the bank that contains $A000-$BFFF.

Because of the vectors (and hardwired banks in case there's bankswitching), when expanding ROMs we often add new banks to the beginning, not the end.
Re: Completely misunderstood how banks and addresses work?
by on (#193046)
And with that, you've solved a lot of questions I had starting with Assembly and the 2A03.

Code:
  .bank 3
  .org $FFFA     ;first of the three vectors starts here
  .dw NMI        ;when an NMI happens (once per frame if enabled) the
                   ;processor will jump to the label NMI:
  .dw RESET      ;when the processor first turns on or is reset, it will jump
                   ;to the label RESET:
  .dw 0          ;external interrupt IRQ is not used in this tutorial

  .bank 2
  .org $C000

  .bank 3
  .org $E000


That fixed it in a pinch, indeed!

Since I've had trouble with this before, I hope you don't mind if I ask for some verification on this before I call it "understood":
Code:
  .org

This directive simply points at the specified address, correct? If so, why would I do that, since most of the stuff I can do is done by directing, for example, the opcode LDA to a specific address anyway.
As mentioned in this thread, and I thought that wasnt the case before, the banks have pre-defined locations in memory. ".org" doesn't define their starting point (what I originally thought).

So in other words:
.bank only switches the 8kb bank I've selected. .org only points to a specific memory-address for further (for me unknown) processing.
Meaning, that the code above does absolutely nothing, other than switching through the banks and pointing at their respective starting-memory.
It isn't necessary to go through them all once, then? I could have 4 banks but only need 3 without having to ever switch to the 4th?

Yeah I tried getting the gist of it before, so I'm glad I am asking here, despite feeling pretty dumb now, heh.
Re: Completely misunderstood how banks and addresses work?
by on (#193049)
Skelpolu wrote:
Code:
  .org

This directive simply points at the specified address, correct?

Actually, it tells the assembler where in the address space of the target machine the code that follows will be mapped. The assembler needs this in order to calculate the addresses of labels, variables, and such. It doesn't CAUSE code to be mapped at the specified address though - the console and the mapper decide that, you're merely informing the assembler where the code will be.

Anyway, the first .org simply lets the assembler know where the code that follow will be mapped, but subsequent .org commands will pad the ROM from the current address until the specified address so that whatever comes next is guaranteed to be at that address. This is what happens with the vectors, for example.

Quote:
As mentioned in this thread, and I thought that wasnt the case before, the banks have pre-defined locations in memory. ".org" doesn't define their starting point (what I originally thought).

I'm not very familiar with NESASM, but it's possible that you don't need an .org at the beginning of every bank (maybe the program counter automatically rolls over to the next bank, IDK, you have to try), but you definitely need the first one, or the assembler won't be able to translate labels and such into addresses.

Quote:
.bank only switches the 8kb bank I've selected.

.bank is a stupid directive that needlessly complicates things​, but there's no way around it if you're using NESASM. NESASM forces you to create ROMs composed of 8KB banks regardless of the mapper you're using, if any, so you must explicitly insert these "breaks" every 8KB.
Re: Completely misunderstood how banks and addresses work?
by on (#193105)
Thanks for the detailed reply, Tokumaru. :)
I get the gist of things much more now.

So wait, you're saying that NESASM is the sole reason we need to split the memory into 8KB segments?
I actually thought that the 2A03 internally works like this to begin with. If it doesn't, or if there's a more convenient, well supported assembler, which one would you recommend?
Heard about CC65 and ASM6, but it's hard for me to decide on one of them, especially since the Nerdy Nights tutorials use the NESASM syntax and I'm afraid I won't keep up with it.
Re: Completely misunderstood how banks and addresses work?
by on (#193108)
Skelpolu wrote:
So wait, you're saying that NESASM is the sole reason we need to split the memory into 8KB segments?

If you're doing NROM, this is true. It's also true of CNROM, BNROM, AOROM, GNROM, Color Dreams, UNROM, MMC1, Action 53, and other relatively simple mappers. But MMC2, MIMIC-1, MMC3, FME-7, RAMBO-1, VRC2, VRC4, VRC7, and several other mappers use the same 8 KiB bank size as NESASM.

Quote:
I actually thought that the 2A03 internally works like this to begin with.

NESASM is ultimately based on an assembler that targeted the PC Engine console, which was called TurboGrafx-16 outside Asia. The TG16's CPU is a 65C02 with a few more instructions and an integrated memory controller that divides the address space into eight 8192-byte windows. (A "bank" is an area of memory of a particular size, and a "window" is a piece of address space that can be mapped to a particular bank.)

PRG ROM windows on the NES, by contrast, are implemented by mapper circuitry inside the cartridge. The vast majority of NES games use PRG ROM windows 8 KiB, 16 KiB, or 32 KiB in size, and some mappers (VRC6 and MMC5) even support a mix of two different window sizes. From the point of view of an NES programmer, the 8 KiB bank size of NESASM is because that's the smallest window size of any common NES mapper. You can have your assembler's banks being the same size as the mapper's windows or smaller but not bigger. A mapper's windows are as big as one, two, or four NESASM banks.

Quote:
If it doesn't, or if there's a more convenient, well supported assembler, which one would you recommend?

For simplicity comparable to that of NESASM, use ASM6. For flexibility particular with larger projects, use ca65.
Re: Completely misunderstood how banks and addresses work?
by on (#193110)
Yeah, the 8KB Bankai things os exclusive to NESASM. I always suggest ASM6 for beginners and ca65 for more experienced coders. These are the main points where ASM6 differs from NESASM:

-It uses < and > instead of LOW() and HIGH();
-it uses () instead of [] for indirection;
-It uses ENUM for declaring variables instead of RSSET;
-It doesn't create an NES header automatically, so you either have to create one yourself using DB statements or using a macro pack;
-It doesn't have any banking bullshit - if you're not using bankswitching at all, just use ORG, if you do have multiple banks, use BASE to rollback the PC whenever a new bank starts;

That's all I can think of right now. If you keep these things in mind, it's possible to use ASM6 and still file the Nerdy Nights tutorials. The code itself is largely the same.
Re: Completely misunderstood how banks and addresses work?
by on (#193157)
It could be a good exercise to adapt the NESASM syntax to ASM6 syntax as you go. If it feels too difficult, some people have already adapted the code for ASM6, you can use it as a reference: https://forums.nesdev.com/viewtopic.php?f=10&t=12097.

ca65 adaptation here: https://bitbucket.org/ddribin/nerdy-nights/src. (There might be others.)
Re: Completely misunderstood how banks and addresses work?
by on (#193206)
At this point, it seems like a reasonable choice to jump straight to CA65 for multiple reasons.
One being that I eventually want to go deeper into 2A03 programming and proper optimizing the code, which, if I understood this correctly, is not quite as possible with NESASM.
Then, of course, it's easier to stick with an assembler that suits my needs from the start rather than having to switch to another assembler once I need to.

Anyway, thanks to everybody who's replied. I'll definitely look into it more now, but I guess the problem I had is resolved. :)