6502 Assembly is the language used to program the Famicom, BBC Micro and Commodore 64 computers.
This page focuses on the assembly language for the 6502 processor, targetting the Famicom.
Lexicon
Directives are commands you send to the assembler to do things like locating code in memory. They start with . and are indented. This sample directive tells the assembler to put the code starting at memory location $8000, which is inside the game ROM area. Labels are aligned to the far left and have a : at the end. The label is just something you use to organize your code and make it easier to read. The assembler translates the label into an address.
Opcodes are the instructions that the processor will run, and are indented like the directives. In this sample, JMP is the opcode that tells the processor to jump to the MyFunction label. Operands are additional information for the opcode. Opcodes have between one and three operands. In this example the #$FF is the operand:
Comments are to help you understand in English what the code is doing. When you write code and come back later, the comments will save you. You do not need a comment on every line, but should have enough to explain what is happening. Comments start with a ; and are completely ignored by the assembler. They can be put anywhere horizontally, but are usually spaced beyond the long lines.
.org $8000 MyFunction: ; A comment LDA #$FF JMP MyFunction
Styleguide
Major comments are prefixed with two semi-colons, and minor comments are found at the end of a line on the 32nd column if available. Variables and subroutines are lowercase, constants and vectors are uppercase, and routines are capitalized.
;; Variables .enum $0000 ; Zero Page variables pos_x .dsb 1 pos_y .dsb 1 .ende ;; Constants SPRITE_Y .equ $0200 SPRITE_X .equ $0203 RESET: NOP Forever: JMP Forever NMI: RTI ;; Routines Check_Collision: LDA pos_y CMP #$88 ; Floor is at 32y BCC @done LDA #$88 STA pos_y @done: RTS ;; Tables Table_Name: .db $40,$46,$4c,$52,$58,$5e,$63,$68 ;; Vectors .pad $FFFA .dw NMI .dw RESET .dw 0 .incbin "src/sprite.chr"
The lin6 linter is used to enfore this style on the various assembly projects found on this site.
Registers
The 6502 handles data in its registers, each of which holds one byte(8 bits) of data. There are a total of three general use and two special purpose registers:
Note: When you use X it adds the value of X to the memory address and uses the 16-bit value at that address to do the write. Whereas when you use Y it adds the value of Y to the address stored in the memory address it's reading from instead. 6502 is little-endian, so $0200 is stored as $00 $02 in memory.
- A: The accumulator handles all arithmetic and logic. The real heart of the system..
- X&Y: General purpose registers with limited abilities..
- SP: The stack pointer is decremented every time a byte is pushed onto the stack, and incremented when a byte is popped off the stack..
- PC: The program counter is how the processor knows at what point in the program it currently is. It’s like the current line number of an executing script. In the JavaScript simulator the code is assembled starting at memory location $0600, so PC always starts there..
- PF: The Processor flag contains 7 bits, each flag live in a single bit. The flags are set by the processor to give information about the previous instruction.
Addressing
The 6502 has 9 major(13 in total) addressing modes, or ways of accessing memory.
Immediate | #aa | The value given is a number to be used immediately by the instruction. For example, LDA #$99 loads the value $99 into the accumulator. |
Absolute | aaaa | The value given is the address (16-bits) of a memory location that contains the 8-bit value to be used. For example, STA $3E32 stores the present value of the accumulator in memory location $3E32. |
Zero Page | aa | The first 256 memory locations ($0000-00FF) are called "zero page". The next 256 instructions ($0100-01FF) are page 1, etc. Instructions making use of the zero page save memory by not using an extra $00 to indicate the high part of the address. |
Implied | Many instructions are only one byte in length and do not reference memory. These are said to be using implied addressing. For example, CLC, DEX & TYA. | |
Indirect Absolute | (aaaa) | Only used by JMP (JuMP). It takes the given address and uses it as a pointer to the low part of a 16-bit address in memory, then jumps to that address. For example, JMP ($2345) or, jump to the address in $2345 low and $2346 high |
Absolute Indexed,X/Y | aaaa,X | The final address is found by taking the given address as a base and adding the current value of the X or Y register to it as an offset. So, LDA $F453,X where X contains 3 Load the accumulator with the contents of address $F453 + 3 = $F456. |
Zero Page Indexed,X/Y | aa,X | Same as Absolute Indexed but the given address is in the zero page thereby saving a byte of memory. |
Indexed Indirect | (aa,X) | Find the 16-bit address starting at the given location plus the current X register. The value is the contents of that address. For example, LDA ($B4,X) where X contains 6 gives an address of $B4 + 6 = $BA. If $BA and $BB contain $12 and $EE respectively, then the final address is $EE12. The value at location $EE12 is put in the accumulator. |
Indirect Indexed | (aa),Y | Find the 16-bit address contained in the given location ( and the one following). Add to that address the contents of the Y register. Fetch the value stored at that address. For example, LDA ($B4),Y where Y contains 6 If $B4 contains $EE and $B5 contains $12 then the value at memory location $12EE + Y (6) = $12F4 is fetched and put in the accumulator. |
Common Opcodes
Load/Store opcodes | |
---|---|
LDA #$0A | LoaD the value 0A into the accumulator A. The number part of the opcode can be a value or an address. If the value is zero, the zero flag will be set. |
LDX $0000 | LoaD the value at address $0000 into the index register X. If the value is zero, the zero flag will be set. |
LDY #$FF | LoaD the value $FF into the index register Y. If the value is zero, the zero flag will be set. |
STA $2000 | STore the value from accumulator A into the address $2000. The number part must be an address. |
STX $4016 | STore value in X into $4016. The number part must be an address. |
STY $0101 | STore Y into $0101. The number part must be an address. |
TAX | Transfer the value from A into X. If the value is zero, the zero flag will be set. |
TAY | Transfer A into Y. If the value is zero, the zero flag will be set. |
TXA | Transfer X into A. If the value is zero, the zero flag will be set. |
TYA | Transfer Y into A. If the value is zero, the zero flag will be set. |
Math opcodes | |
ADC #$01 | ADd with Carry. A = A + $01 + carry. If the result is zero, the zero flag will be set |
SBC #$80 | SuBtract with Carry. A = A - $80 - (1 - carry). If the result is zero, the zero flag will be set |
CLC | CLear Carry flag in status register. Usually this should be done before ADC |
SEC | SEt Carry flag in status register. Usually this should be done before SBC |
INC $0100 | INCrement value at address $0100. If the result is zero, the zero flag will be set |
DEC $0001 | DECrement $0001. If the result is zero, the zero flag will be set |
INY | INcrement Y register. If the result is zero, the zero flag will be set |
INX | INcrement X register. If the result is zero, the zero flag will be set |
DEY | DEcrement Y. If the result is zero, the zero flag will be set |
DEX | DEcrement X. If the result is zero, the zero flag will be set |
ASL A | Arithmetic Shift Left. Shift all bits one position to the left. This is a multiply by 2. If the result is zero, the zero flag will be set |
LSR $6000 | Logical Shift Right. Shift all bits one position to the right. This is a divide by 2. If the result is zero, the zero flag will be set |
Comparison opcodes | |
CMP #$01 | CoMPare A to the value $01. This actually does a subtract, but does not keep the result. Instead you check the status register to check for equal, . Less than, or greater than |
CPX $0050 | ComPare X to the value at address $0050 |
CPY #$FF | ComPare Y to the value $FF |
Control-Flow opcodes | |
JMP $8000 | JuMP to $8000, continue running code there |
BEQ $FF00 | Branch if EQual, contnue running code there. First you would do a CMP, which clears or sets the zero flag. Then the BEQ will check the zero flag. If zero is set (values were equal) the code jumps to $FF00 and runs there. If zero is clear (values not equal) there is no jump, runs next instruction |
BNE $FF00 | Branch if Not Equal - opposite above, jump is made when zero flag is clear |
Compare
The compare instructions set or clear three of the status flags (Carry, Zero, and Negative) that can be tested with branch instructions, without altering the contents of the operand. There are three types of compare instructions:
Instruction | Description |
CMP | Compare Memory and Accumulator |
CPX | Compare Memory and IndexX |
CPY | Compare Memory and Index Y |
The CMP instruction supports eight different addressing modes, the same ones supported by the ADC and SBC instructions. Since the X and Y registers function primarily as counters and indexes, the CPX and CPY instructions do not require this elaborate addressing capability and operate with just three addressing modes (immediate, absolute, and zero page).
The compare instructions subtract (without carry) an immediate value or the contents of a memory location from the addressed register, but do not save the result in the register. The only indications of the results are the states of the three status flags: Negative (N), Zero (Z), and Carry (C). The combination of these three flags indicate whether the register contents are less than, equal to (the same as), or greater than the operand "data" (the immediate value or contents of the addressed memory location. The table below summarizes the result indicators for the compare instructions.
Compare Result | N | Z | C |
A, X, or Y < Memory | * | 0 | 0 |
A, X, or Y = Memory | 0 | 1 | 1 |
A, X, or Y > Memory | * | 0 | 1 |
The compare instructions serve only one purpose; they provide information that can be tested by a subsequent branch instruction. For example, to branch if the contents of a register are less than an immediate or memory value, you would follow the compare instruction with a Branch on Carry Clear (BCC) instruction, as shown by the following:
Comparing Memory to the Accumulator
CMP $20 ; Accumulator less than location $20? BCC THERE HERE: ; No, continue execution here. THERE: ; Execute this if Accumulator is less than location $20.
Use of Branch Instructions with Compare
To Branch If | Follow compare instruction with | |
For unsigned numbers | For signed numbers | |
Register is less than data | BCC THERE | BMI THERE |
Register is equal to data | BEQ THERE | BEQ THERE |
Register is greater than data | BEQ HERE BCS THERE | BEQ HERE BPL THERE |
Register is less than or equal to data | BCC THERE BEQ THERE | BMI THERE BEQ THERE |
Register is greater than or equal to data | BCS THERE | BPL THERE |
Math
Modulo
Returns in register A.
Mod: LDA $00 ; memory addr A SEC Modulus: SBC $01 ; memory addr B BCS Modulus ADC $01 RTS
Division
Rounds up, returns in register A.
Div: LDA $00 ; memory addr A LDX #0 SEC Divide: INX SBC $01 ; memory addr B BCS Divide TXA RTS
The famicom is an 8bit video game console by Nintendo.
The famicom notes were created during the production of the NES release of donsol and nespaint, to learn more about programming for the console, see assembly.
NES System Architecture
The NES screen resolution is 256x240.
- ROM: Read Only Memory, holds data that cannot be changed. This is where the game code or graphics is stored on the cart..
- RAM: Random Access Memory, holds data that can be read and written. When power is removed, the chip is erased. A battery can be used to keep power and data valid..
- PRG: Program memory, the code for the game.
- CHR: Character memory, the data for graphics.
- CPU: Central Processing Unit, the main processor chip.
- PPU: Picture Processing Unit, the graphics chip.
- APU: Audio Processing Unit, the sound chip inside the CPU.
6502 Processor Overview
$0000-0800 | Internal RAM, 2KB chip in the NES | |
$2000-2007 | PPU access ports | |
$2000 | PPUCTRL | |
$2001 | PPUMASK | |
$2002 | PPUSTATUS | |
$2003 | SPRADDR | |
$2005 | PPUSCROLL | |
$2006 | PPUADDR | |
$2007 | PPUDATA | |
$4000-4015 | Audio access ports | |
$4000-4003 | APUCH1(Pulse1) | |
$4004-4007 | APUCH2(Pulse2) | |
$4008-400B | APUCH2(Triangle) | |
$400C-400F | APUCH2(Noise) | |
$4010-4013 | APUCH2(DCM) | |
$4015 | SNDCHN | |
$4016-4017 | Controllers access ports | |
$4016 | JOY1 | |
$4017 | JOY2 | |
$6000-7FFF | Optional WRAM inside the game cart | |
$8000-FFFF | Game cart ROM |
Backgrounds
To make graphics on the screen you must write graphic data to the PPU memory, but you can't write directly to PPU memory, you have to use PPU ports $2006 and $2007. By using $2006 you declare the address of PPU memory then by using $2007 you write the desired value to that address, PPU Memory addresses are 16bit starting from $0000~$3FFF(0000-1fff = tiles & 2000-23ff = nametable 0).
Hex | high byte($4A) | low byte($0F) |
$4A0F | 01001010 | 00001111 |
So you need to write twice to $2006 to declare it's address, the first write declares high byte of address, the second write declares the low byte of address. Each time you write a value to $2007, the PPU address is automatically adjusted to the next address, so you don't need to declare the PPU address with $2006 for sequential PPU memory addresses.
LDA #$20 ; high byte STA $2006 LDA #$00 ; low byte STA $2006 LDA #$04 ; sprite-id STA $2007
Calculate at what address to draw it: $2000 plus 32 times the vertical position of the tile (in 8-pixel units) plus the horizontal position of the tile (in 8-pixel units), write the high byte of the address to $2006: this is usually values $20 to $23, and write the low byte of the address to $2006. In other words, calculate the tile offset (TileY * 32 + TileX) and then add the base address. This will give you a pointer you can use to access any part of the map.
Palette Codes
00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 1A | 1B | 1C | 1D | 1E | 1F |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 2A | 2B | 2C | 2D | 2E | 2F |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 3A | 3B | 3C | 3D | 3E | 3F |
Audio
The NES and Famicom use a set of memory mapped registers to configure the 5 different sound channels, we just write byte data to the ports to configure the ports (though we cannot read back).
CH1 | 4000 | Volume | CCLEVVVV | vol env len dutycycle |
4001 | Sweep | EUUUDSSS | sweep direction rate enabled | |
4002 | Frequency | LLLLLLLL | freqLbyte | |
4003 | Length | CCCCCHHH | freqHbyte counter | |
CH2 | 4004 | Volume | CCLEVVVV | vol env len dutycycle |
4005 | Sweep | EUUUDSSS | sweep direction rate enabled | |
4006 | Frequency | LLLLLLLL | freqLbyte | |
4007 | Length | CCCCCHHH | freqHbyte counter | |
CH3 | 4008 | Counter | CLLLLLLL | clock count |
4009 | Sweep | ------ | unused | |
400a | Frequency | LLLLLLLL | freqLbyte | |
400b | Length | CCCCCHHH | freqHbyte counter | |
CH4 | 400c | Volume | CCLEVVVV | vol env len dutycycle |
400d | Sweep | ------ | unused | |
400e | Frequency | LLLLLLLL | freqLbyte | |
400f | Length | CCCCCHHH | freqHbyte counter | |
CH5 | 4010 | Play mode | IL-FFFF | irqenable loopfreq |
4011 | Delta | -DDDDDDD | 7bit PCM Data | |
4012 | Address | AAAAAAAA | Address $C000+(A*$40) | |
4013 | Length | LLLLLLLLL | Length (L*$10)+1 Bytes |
To produce a sound, first we enable the channel via $4015.
LDA #%00000010 ; Enable Channel2(Pulse2) STA $4015
Then we write to the Square 2 ports:
LDA #%00111000 ; Duty Cycle 00, Volume 8 (half volume) STA $4004 LDA #<$356 ; 356 = C2 STA $4006 LDA #>$356 STA $4007
The following table holds all values of every note on every octave that the NTSC NES can produce for the Square and Triangle Wave. These will be listed as 11 bit values that can be stored into the sound registers. All values are rounded to the nearest number. Note that for the Triangle Wave, these values will make a pitch one octave below that of the Square Wave.
A | $7F1 | $3F8 | $1FB | $0FD | $07E | $03F | $01F | $00F |
---|---|---|---|---|---|---|---|---|
B# | $780 | $3BF | $1DF | $0EF | $077 | $03B | $01D | $00E |
B | $713 | $389 | $1C4 | $0E2 | $070 | $038 | $01B | $00D |
C | $6AD | $356 | $1AB | $0D2 | $06A | $034 | $01A | $00C |
D# | $64D | $326 | $193 | $0C9 | $064 | $031 | $018 | $00C |
D | $5F3 | $2F9 | $17C | $0BD | $05E | $02F | $017 | $00B |
E# | $59D | $2CE | $167 | $0B3 | $059 | $02C | $015 | $00A |
E | $54D | $2A6 | $152 | $0A9 | $054 | $029 | $014 | $00A |
F | $500 | $27F | $13F | $09F | $04F | $027 | $013 | $009 |
G# | $4B8 | $25C | $12D | $096 | $04B | $025 | $012 | $008 |
G | $475 | $23A | $11C | $08E | $046 | $023 | $011 | ---- |
A# | $435 | $21A | $10C | $086 | $042 | $021 | $010 | ---- |
Controller Ports
The controllers are accessed through memory port addresses $4016 and $4017. First you have to write the value $01 then the value $00 to port $4016. This tells the controllers to latch the current button positions. Then you read from $4016 for first player or $4017 for second player. The buttons are sent one at a time, in bit 0. If bit 0 is 0, the button is not pressed. If bit 0 is 1, the button is pressed.
Button status for each controller is returned in the following order: A, B, Select, Start, Up, Down, Left, Right.
Mapping
Some cartridges have a CHR ROM, which holds a fixed set of graphics tile data available to the PPU from the moment it turns on. Other cartridges have a CHR RAM that holds data that the CPU has copied from PRG ROM through a port on the PPU.
nrom | NROM consists of a 16 kilobyte or 32 kilobyte program ROM, a 4 kilobyte or 8 kilobyte graphics ROM, and an NES lockout chip. The address pins on the NES are wired directly to the ROM with no mapper hardware intervening. There is no support for extra work RAM. Any ROM with a size of 40 KB or less is most likely an NROM. |
---|---|
cnrom | CNROM is similar to NROM except that writes to the program area of the ROM go to a 74LS161 register that controls the most significant bits of the graphics ROM's address bus, allowing it to be bankswitched in 8 KB chunks. There are also some somewhat sneaky ways to stream map data out of the graphics ROM, making for a larger game. With a ROM size of 32 KB and a graphics ROM size of 16 KB or 32 KB (or higher on the Panesian CNROM clone), most CNROMs are 64 KB or smaller. |
unrom | Using RAM instead of ROM in a system designed for ROM fonts was the main innovation of UNROM. Programs would write through the PPU to the graphics RAM whenever the screen was turned off (such as during vblank or slight pauses in the action). UNROM let game maps get big. It also allowed for RLE compression of graphics data, as graphics no longer had to be stored in the raw form needed by the PPU. |
Lin6 is a linter for 6502 assembly code.
The linter is a utility to format assembly code to be used with the asm6 assembler. Lin6 was used in the creation of Donsol, Spacetime and NesPaint.
- source, ANSI C.