Ramblings of an NSF enthusiast (guide to writing an NSF player)

Pages: 1234
closed account (Dy7SLyTq)
update: i found a list of opcodes and their hex values ( http://www.akk.org/~flo/6502%20OpCode%20Disass.pdf ). which commands do i need? should i only use immediate ones?
closed account (N36fSL3A)
#defines are ugly as hell... Use constants inside namespaces.

I already started on a NES emulator, BHX, I can work on one wit you if you'd like. I'm using SDL and OpenGL (for video speed) to work on that.

Finding specs on the NES pretty hard, they're different on the sites I checked... can you post the correct ones here?
update: i found a list of opcodes and their hex values ( http://www.akk.org/~flo/6502%20OpCode%20Disass.pdf ).


The single best reference site I've ever seen for 6502 opcodes is the obelisk one I linked previously:

http://www.obelisk.demon.co.uk/6502/reference.html

For DETAILED TIMINGS this doc is pretty much the best, as it tells you what the CPU is doing for every single cycle of the instruction:

http://nesdev.com/6502_cpu.txt

Though that's more of an advanced doc and the details there is not needed for a casual emu. And certainly not for an NSF player.

which commands do i need? should i only use immediate ones?


You'll need most/all of them if you want to run any NSFs. The only one that you don't really have to worry about is BRK. You can treat BRK like a NOP (do nothing). Nothing uses it.

Just an uninformed suggestion for anyone attempting this, either use something like #define LDA 0xA9 or map the instructions or something to make the opcodes more readable for yourself. But if you try to memorize the instruction set first you'll never get anything done.


Defining each opcode is a waste, IMO. Especially since there are 8 different opcodes for LDA (one for each addressing mode).

You definitely do not need to memorize the opcodes... that'd just be silly. But putting them in a switch is pretty easy. Mine typically looks like this:

1
2
3
4
5
6
7
8
9
switch(...)
{
//...
  /* ADC  */
case 0x69:  ADC( Mode_Im() );   break;  // immediate mode
case 0x65:  ADC( Mode_Zp() );   break;  // zero page
case 0x75:  ADC( Mode_Zx() );   break;  // zero page, X
case 0x6D:  ADC( Mode_Ab() );   break;  // absolute
   // ... etc  etc 



Finding specs on the NES pretty hard, they're different on the sites I checked


The nesdev wiki is the final word for any kind of technical information.

http://wiki.nesdev.com/w/index.php/NES_reference_guide

Other pages might be good for explaining things... but if any technical data conflicts with what it says on the wiki... always trust the wiki.
closed account (Dy7SLyTq)
sorry my mistake. i saw the link previously but didnt follow it because i was out. and thanks ill start on it

edit: and one last question for now, so is this going to be reading in a binary file?
Last edited on
closed account (N36fSL3A)
I started writing one earlier but got frustrated and I don't know where I moved it to... I'm just going to restart, but I realized that it's difficult to find reliable resources on the specs of the NES... different websites have different specs, and the last thing I want is a runtime error.
the last thing I want is a runtime error.

Yeah! Only noobs get runtime errors. I can't even remember the last time I had one. /flex
closed account (N36fSL3A)
Yea, well compile errors > runtime errors.
Disch, how about writing a series of articles, and posting those in the... article section? I know I would read the shit out of them.
and one last question for now, so is this going to be reading in a binary file?


Yes. The NSF spec is here:

http://wiki.nesdev.com/w/index.php/NSF

What I typically do... is have some kind of "driver" code and stick it somewhere unused (like at $3000). So at that place you'd have this:

1
2
3
4
$3000:   JSR <init_address>
$3003:   JSR <play_address>
$3006:   HLT   (opcode $F2)
$3007:   JMP $3003


HLT is an "unofficial" opcode which deadlocks the processor. Normally it would crash the game, so no nsf/game will ever use it. You can do that to mark the "end of a frame".

Then when you load an NSF, put the init address and play address in that driver code so it'll JSR to the right places. Set the PC to $3000... and let it run.

Once you hit the HLT, stop and output a frame's worth of audio. Then next frame, simply un-halt and run until you hit HLT again.


different websites have different specs, and the last thing I want is a runtime error.


You will have runtime errors. I've made about a dozen 6502 emus and I have yet to write one that didn't require some debugging.


Disch, how about writing a series of articles, and posting those in the... article section? I know I would read the shit out of them.


I'm too lazy. It's much easier to just post in the Lounge.



EDIT:

Anyway.. this guide is mostly done. The only things that it doesn't really explain are the Frame Sequencer and the other sound channels... but the technical docs explain that once you get the basic idea.
Last edited on
closed account (9wqjE3v7)
I [love] this kind of stuff too, I am working on an i8085 emulator at the moment. Does the 6502 support paged addressing, or does it only support it in a very limited way (i.e referencing 8 bit addresses- 0-page you mentioned?).
The 6502 does not have paged addressing built in. On the NES that was accomplished by hardware on the cartridge.
closed account (N36fSL3A)
So all CPU memory is in 1 array? How do I open ROM files?
Last edited on
Kinda sorta. The "array" is an oversimplified illustration. Really, different addresses go to different arrays. Some addresses go nowhere, and some addresses go to hardware registers.

All reads and writes should go through a read/write function which determines which address you're accessing. It could be something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint8_t read(uint16_t addr)
{
    if(addr < 0x0800)
    {
        // ... RAM  ...
    }
    if(addr >= 0x8000)
    {
        // ... ROM ...
    }
    if((addr & 0xF000) == 0x4000)
    {
        // ... APU registers ...
    }
    // etc
}


Or... if you want to get really fancy... what I usually do is set up callbacks... one for each 4K ($1000 bytes) page... then when you read from a certain page, you just call the callback for that page. Then the reads/writes go where you need without having to check the address:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef uint8_t (*reader)(uint16_t); // typedef a function pointer

reader readproc[0x10];  // 0x10 pages, one for each 0x1000 addresses

readproc[0] = &readRAM;  // page $0xxx reads from RAM
readproc[1] = &readRAM;  // page $1xxx also reads from [mirrored] RAM
readproc[4] = &readAPU;  // page $4xxx reads from APU regs
readproc[8] = &readROM;  // page $8xxx reads from ROM
// etc

//...

// then for you general "read" function, you just extract the page from the address:
uint8_t read(uint16_t addr)
{
    return readproc[ addr >> 12 ]( addr );
}


// then the individual read/write functions are simple:

uint8_t readRAM(uint16_t addr)
{
    return RAM[ addr & 0x07FF ];
}



EDIT:

In other news... I'm going to start my own NSF player for funsies... Weeeeeeeee
Last edited on
closed account (N36fSL3A)
Is there a specific reason why different addresses go to different arrays? Because I was planning on having the CPU class have all the memory and the other processors (like PPU) taking a pointer to the array.
Last edited on
Yes.

Here's a more detailed memory map of the entire NES memory map:


$0000-$1FFF - System RAM
$2000-$3FFF - PPU registers
$4000-$401F - CPU/APU registers
$4020-$FFFF - forwarded through to the cartridge


Examining the 'System RAM' block a little closer... you'll get this:


$0000-$07FF - System RAM
$0800-$0FFF - The same system RAM (mirrored)
$1000-$17FF - The same system RAM (mirrored again)
$1800-$1FFF - The same system RAM (mirrored yet again)


What this means is that accessing address $0200 and address $0A00 will access the same byte of memory. Like both addresses point to the same area of RAM.

But that seems stupid, right? Why does it do that?


The addressing allows for 8K ($2000 bytes) of RAM... but way back then RAM was expensive... so it only had 2K ($0800) of physical RAM. The mirroring happens because the high bits of the address get "chopped off" when accessing the actual RAM chip.

This is easily simulated by masking out the low bits:

1
2
3
4
5
// assume addr is in the RAM area ($0000-1FFF)
uint8_t readRAM(uint16_t addr)
{
    return RAM[ addr & 0x07FF ];  // <- this handles the mirroring
}



If you treat all of addressing space as a singular big array, doing this is not so easy.

And this doesn't even touch on 'bankswitching' which allows for more than $10000 bytes to be accessed by swapping out memory pages.

So yeah... put reads/writes through a function. The array thing was just conceptual... and I probably should have illustrated it differently in hindsight.
closed account (N36fSL3A)
So I would subtract the actual address the assembler references by the place where the memory bank starts?
That's one way to do it. Or you can just mask out the appropriate bits and a binary AND operation.
closed account (N36fSL3A)
So:

1
2
3
4
5
// assume addr is in the RAM area ($0000-1FFF)
uint8_t readRAM(uint16_t addr)
{
    return RAM[ addr & 0x07FF ];  // <- this handles the mirroring
}


Doesn't require me to do what I've said above?
Correct... you would not have to subtract anything from that. That masking with the & ensures the result will always be between 0x0000 and 0x07FF.

Do you know how & works?
closed account (N36fSL3A)
No, not really... never really got into bitwise operators. Is VRAM the same way?
Last edited on
Pages: 1234