Such Programming

Tinkerings and Ramblings

6502 Reasons to Learn Assembly

Learning the language of a processor can be a daunting task, but understanding the machine you're working with makes it easier to write great code for it. Modern systems are so complex that directly writing assembly is laborious. However, modern open source tooling allows us to travel back in time 40 years and explore foundational software and hardware concepts in ways unimaginable during the lifetime of those systems.

One of the systems I plan to explore quite a bit in this blog series is the NES, my all time favorite retrocomputer. In this post I want to warm you up to some of the shenanigans to come and show you that tinkering with assembly on the NES may be a lot easier than you think!

Simpler Times

Compared to writing assembly for a desktop computer, the NES has no operating sytem, no MMU and no memory protection. These complex systems are valuable to understand but are also more difficult to appreciate without a context. The lack of these modern features means the code you write for a 6502-based machine runs in a very raw and simple environment that is very close to hardware.

A lot of what I'd like to share about why and how to get started with 6502 assembly development is already well covered in Easy 6502, which offers a nice getting started tutorial and provides an in-browser assembler and simple debugger. The main aspect I want to focus on for this post are the "APIs" of the processor.

There are only 56 operations the 6502 can perform, most of which are trivial to understand. These are the building blocks used to make great stuff and are the internal API for your software.

Via the pins connecting the 6502 to whatever system it's placed in, the CPU has a 16-bit address bus, an 8-bit data bus and a control bus that lets the processor interact with the outside world. This is the external interface that the CPU will use to interact with RAMs, ROMs and devices.

Pointers for the Hardware

Before exploring the internal API more, let's look at the external side of things. Since the 6502 has a 16-bit address bus and an 8-bit data bus, it can read/write 8 bits of data from/to 2^16 different addresses. The CPU controls what address is being referred to on this address bus and uses the control bus to notify other connected devices if the data bus should be used for a read or a write operation.

To compare this to something like JavaScript, imagine an array of 65536 [Object]s, some which may support a .read() and/or .write() operation. What type of device exists at each address varies greatly by system, though for 6502 you can comfortably rely on the lowest addresses being system RAM. For the NES, here's how describes the CPU memory map.

Would you like to read the first byte of RAM? that's effectively someting like this:

var first_byte = memory_map[0].read();

What about something that sounds more difficult, like reading the status of the Picture Processing Unit? Well that's pretty much the same thing but instead of reading from 0 we'd want to read from $2002

var ppu_status = memory_map[0x2002].read();

We could even use our old friend, the pointer!

const PPU_STATUS = 0x2002;

var pointer = PPU_STATUS;
var ppu_status = memory_map[pointer].read();

This is really the main concept to understand for the hardware-facing side of the interface. There are interesting details in the implementation and the devices you would communicate to but hopefully this give a good functional base for the exploration of the internal API, coming up after the break.

Operating the 6502

This section summarizes the controls available within the code you run on a 6502. Don't worry if some or many of these terms sound entirely unfamiliar, they are concepts learned best by tinkering with in their own experimentation.

As I mentioned before there are only 56 Opcodes for the 6502. Many of these have similarities and can be grouped together. The other big element to be aware of is that the 6502 has 3 primary registers: the accumulator (A) and 2 index registers (X and Y). Internally, there is also a Program Counter PC that points to the memory address of the next location that should be fetched from and executed as code, as well as a Stack Pointer (SP) that is used for stack based operations. There are also a few processor status flags: Carry, Zero, Interrupt, Decimal, Overflow and Negative encoded into a Status register.

I would broadly categorize these instructions into 4 groups:

  • Load/Store/Transfer Instructions

    • LDA - Load A Register
    • LDX - Load X Register
    • LDY - Load Y Register
    • PHA - Push Accumulator
    • PHP - Push processor status
    • PLA - Pull Accumulator
    • PLP - Pull processor status
    • STA - Store A Register
    • STX - Store X Register
    • STY - Store Y Register
    • TAX - Transfer A to X
    • TAY - Transfer A to Y
    • TSX - Transfer stack pointer to X
    • TXA - Transfer X to A
    • TXS - Transfer X to stack pointer
    • TYA - Transfer Y to A
  • Control Flow

    • BCC - Branch "Carry" clear
    • BCS - Branch "Carry" set
    • BEQ - Branch equal
    • BMI - Branch "minus"
    • BNE - Branch not equal
    • BPL - Branch "plus"
    • BRK - Break
    • BVC - Branch "Overflow" clear
    • BVS - Branch "Overflow" set
    • CMP - Compare A Register
    • CPX - Compare X Register
    • CPY - Compary Y Register
    • JMP - Jump
    • JSR - Jump to subroutine
    • NOP - No-operation
    • RTI - Return from interrupt
    • RTS - Return from subroutine
  • Math/Logic

    • ADC - Add with Carry
    • AND - Bitwise AND
    • ASL - Arithmetic shift left
    • DEC - Decrement A register
    • DEX - Decrement X register
    • DEY - Decrement Y register
    • EOR - Exclusive OR
    • INC - Increment A register
    • INX - Increment X register
    • INY - Increment Y register
    • LSR - Logical shift right
    • ORA - Bitwise OR
    • ROL - Rotate Left
    • ROR - Rotate Right
    • SBC - Subtract with Carry
  • Flags

    • BIT - Test BITs
    • CLC - Clear "Carry" Flag
    • CLD - Clear "Decimal" Flag
    • CLI - Clear "Interrupt" Flag
    • CLV - Clear "Overflow" Flag
    • SEC - Set "Carry" Flag
    • SED - Set "Decimal" Flag
    • SEI - Set "Instruction" Flag

This is not a exactly a small amount of instructions, but there are 987 in x86_64 so by comparison it is quite tiny!

The Learning Journey

Generally speaking I think it's fair to say that understanding 6502 assembly is easy. What is much less easy is seeing and understanding how you can put these tools together to do something useful, like letting me jump on the head of a Koopa.

Generally speaking this is a situation that get