Category:6502 Assembly: Difference between revisions

From Rosetta Code
Content added Content deleted
Line 159: Line 159:


===Absolute Offset By X/Y===
===Absolute Offset By X/Y===
An absolute memory address offset by X or Y. This works similar to the zero page version. However, not all commands work with this mode. For example, the LDX and LDY commands work with this mode, but STX and STY do not. (LDA and STA work with all addressing modes)
An absolute memory address offset by X or Y. This works similar to the zero page version. However, not all commands work with this mode. For example, the LDX and LDY commands work with this mode, but STX and STY do not. (LDA and STA work with all addressing modes except Zero Page Offset By Y.)
<lang 6502asm>LDX #$15
<lang 6502asm>LDX #$15
LDY #$20
LDY #$20

Revision as of 01:54, 28 August 2021


This page is a stub. It needs more information! You can help Rosetta Code by filling it in!
Language
6502 Assembly
This programming language may be used to instruct a computer to perform a task.
Official website
See Also:


Listed below are all of the tasks on Rosetta Code which have been solved using 6502 Assembly.

The MOS Technology 6502 is an 8-bit microprocessor that was designed by Chuck Peddle and Bill Mensch for MOS Technology in 1975. When it was introduced, it was the least expensive full-featured microprocessor on the market by a considerable margin, costing less than one-sixth the price of competing designs from larger companies such as Motorola and Intel. It was nevertheless fully comparable with them, and, along with the Zilog Z80, sparked a series of computer projects that would eventually result in the home computer revolution of the 1980s. The 6502 design, with about 4,000 transistors, was originally second-sourced by Rockwell and Synertek and later licensed to a number of companies. It is still made for embedded systems.

One of the first "public" uses for the design was the Apple I computer, introduced in 1976. The 6502 was next used in the Commodore PET and the Apple II. It was later used in the Atari home computers, the BBC Micro family, the Commodore VIC-20 and a large number of other designs both for home computers and business, such as Ohio Scientific and Oric.

The Ricoh 2A03/2A07 was created from a second source 6502 and used in the Nintendo Entertainment System in the US. The Ricoh 2A07 was used in the European model. Both use the same instructions as the 6502 but the Binary Coded Decimal mode does not function on either. The Decimal flag can still be set and cleared, but it will have no effect on any calculations. The functionality was replicated in software, as most NES games keep track of score and display it as decimal digits.

Registers

The 6502 has three main data registers: A (the accumulator), X, and Y. Most mathematical operations can only be done with the accumulator. X and Y are often limited to loop counters and offsets for indirect addressing. It also has the system flags, the stack pointer, and the program counter.

RAM

The first 256 bytes of the 6502's address space is known as "zero page RAM" and can be accessed more quickly than other sections of RAM. If a 8 bit address is used as an instruction parameter, it is actually referring to the zero page. (The high byte equals $00 and is thus omitted). This saves space, as the $00 high byte is not actually included in the bytecode, so a load/store to/from zero page takes one less byte than a load/store to/from anywhere else.

A True 8-Bit Computer

The 6502 is an 8-bit computer in the purest sense. Unlike the Z80, the 6502 is not capable of 16 bit operations within a single register. To work with a 16 bit number you will need to split it in two and work with each half individually.

Processor Flags

The status flags of the 6502 are what allow it to branch to different areas of code, among other things. This is an 8-bit register whose value is updated after certain instructions. It doesn't need to be read from or written to in most cases - but its value can be preserved on the stack for later use. The flags are often displayed as such: NV-BDIZC. Each letter represents a bit in an 8-bit binary value. (The dash means that this value is unused.) The flags are typically updated after a "math instruction" takes place. These include but are not limited to:

  • Loading a value into a register
  • Adding or subtracting
  • Bit shifts/rotates



Storing values into memory, jumping, or returning from subroutines will not set the flags. Furthermore, each command sets the flags differently, and some don't set the flags at all!

Flag terminology: A bit or flag is "clear" if it equals 0 and "set" if it equals 1.

Negative

Denoted with the letter N.

This bit equals 1 if the last math operation resulted in a number that was negative (i.e. between #$80 and #$FF, inclusive).

  • Set with: N/A (There is no explicit command for this but you can do it by loading a value #$80 or greater into a register, or with BIT $addr where $addr is a zero-page or absolute address containing a value #$80 or greater)
  • Cleared with: N/A (There is no explicit command for this but you can do it by loading a value #$7F or less into a register, or with BIT $addr where $addr is a zero-page or absolute address containing a value #$7F or less)
  • BMI branches if this flag is clear.
  • BPL branches if this flag is set.

Overflow

Denoted with the letter V.

If a number crosses the "boundary" between #$7F and #$80, this flag is set. Note that adding #1 to #$FF does not set this flag. On certain systems, this flag is also set by external hardware. The BIT command also sets this flag if the specified address's value has bit 6 set.

  • Set with: N/A (There is no explicit command for this but you can do it with BIT $addr where $addr is a zero-page or absolute address containing a value where bit 6 is set, i.e. the left hex digit equals 4, 5, 6, 7, C, D, E, or F)
  • Cleared with: CLV
  • BVC branches if this flag is clear.
  • BVS branches if this flag is set.



Break

Denoted with the letter B.

This flag is set if the BRK command was executed. The BRK basically does the same thing as JMP ($FFFE), and is mainly used for debugging.

  • Set with: BRK
  • Cleared with: RTI
  • There are no branches associated with this command.



Decimal

Denoted with the letter D.

This flag is set if Decimal Mode is active. There are no explicit branches based on its status. CLD clears this flag and SED sets it. A proper reset routine should clear this flag at the start.

  • Set with: SED
  • Cleared with: CLD
  • There are no branches associated with this command.



Interrupt

Denoted with the letter I.

This flag is set if Interrupts are disabled. Note that this only disables the IRQ (Interrupt Request) line and has no effect on the NMI (Non-Maskable Interrupt.) A proper reset routine should set this flag at the start, but some systems like the NES do so automatically. CLI clears this flag and SEI sets it. There are no explicit branches on this condition.

  • Set with: SEI
  • Cleared with: CLI
  • There are no branches associated with this command.



Zero

Denoted with the letter Z. This flag is set if the last math operation resulted in zero, or if zero was loaded into A, X, or Y. This is mostly used for testing the equality of two values, using the CMP, CPX, or CPY instructions.

  • Set with: N/A (there is no explicit command for this but you can easily do it by loading #0 into a register.)
  • Cleared with: N/A (there is no explicit command for this but you can easily do it by loading a nonzero value into a register)
  • BNE branches if this flag is clear.
  • BEQ branches if this flag is set.



Carry

Denoted with the letter C.

This flag is set under the following conditions:

  • A CMP/CPX/CPY operation was performed and the value in the register is greater than or equal to the operand. If the register was strictly less than the operand, the carry flag will be clear.
  • A bit shift or rotate caused a value of 1 to be "pushed out" of the operand.
  • An ADC or SBC operation resulted in a "wraparound" from #$FF to #$00.



The carry has an effect on math operations:

  • If the carry flag is set, ADC will add an additional 1 to the result.
  • If the carry flag is clear, SBC will subtract an additional 1 to the result.



The carry is incredibly useful for 16-bit math, among other things. There are no ADD or SUB commands on the 6502, but you can achieve the same result with CLC ADC and SEC SBC, respectively.

  • Set with: SEC
  • Cleared with: CLC
  • BCC branches if this flag is clear.
  • BCS branches if this flag is set.



Decimal Mode

The 8086, 68000, and z80 have special commands for Binary Coded Decimal math, where hex values are used to represent decimal numbers (the base 10 system we use, not to be confused with floating point.) The 6502 has a special Decimal Flag as part of its status register. If the Decimal Flag is set, instructions such as ADC and SBC will produce a result that is a valid decimal number (i.e. not containing digits A through F). The Decimal Flag is only affected by the two commands responsible for setting and clearing it, as well as interrupts on certain 6502 revisions. <lang 6502>sed ;set the decimal flag, enabling decimal mode lda #$19 clc adc #$01 ;now the value in the accumulator equals #$20 rather than #$1A cld ;resume normal operations</lang>

A few notes on Decimal Mode:

  • If a register already contains a value that has an A,B,C,D,E or F digit, setting or clearing the decimal flag will not change that.
  • In your assembler, your values need to be encoded as hexadecimal, like the example above. Using decimal numbers in your assembly in decimal mode will result in inaccurate values. This is because the numbers are internally adjusted to only show digits below 9, rather than a true decimal output. The value is still technically stored in hexadecimal.
  • The Decimal Mode does not function on the Nintendo Entertainment System or its derivatives (i.e. the Famicom, Vs. System, or Play Choice 10). The flag can be set or cleared, but has no effect on the calculation. If you are programming for those systems you will have to re-create its functionality with your own code (which isn't too difficult considering almost every game kept score).

Addressing Modes

Implied

Some commands have no operands at all, or if none is given, the operand is assumed to be the accumulator. <lang 6502asm>RTS ;return from subroutine, no operand needed. ASL ;if no operand supplied, the accumulator is used. Some assemblers require you to type "ASL A" but others do not.</lang>

Immediate

A constant value is directly used as the argument for a command. <lang 6502asm>LDA #3 ;load the number 3 into the accumulator AND #%10000000 ;bitwise AND the binary value 1000 0000 with the value in the accumulator SBC #$30 ;subtract hexadecimal 0x30 from the accumulator. If the carry flag is clear, also subtract 1 after that.</lang>

Zero Page

A zero page memory address is supplied as the argument for a command, and the actual operation is performed using the value stored within. This is similar to the dereference operator in C/C++. This is faster than other addressing modes that work with memory, and takes up fewer bytes in your program. Furthermore, certain commands work with zero page but not longer addresses.

For these examples, assume that the zero page memory address $05 contains #$40 (hexadecimal 0x40). <lang 6502>LDA $05 ;dereferences to whatever is stored at $05, in this case, #$40. #$40 is loaded into the accumulator. ADC $05 ;add the value stored at address $05 to whatever is stored in the accumulator. If the carry flag is set, add 1 to the result. ROR $05 ;rotate right the bits of the value stored at memory address $05. The value stored there changes from #$40 to #$20.</lang>

Absolute

A memory address stored outside the zero page is used as the argument for a command. This is slower and takes longer than the zero page. However, there are still certain things that absolute addressing is needed to do, such as jumping and reading/writing to or from memory-mapped ports.

<lang 6502asm>JMP $8000 ;move the program counter to address $8000. Execution resumes there. STA $2007 ;store the value in the accumulator into address $2007 (this is the memory-mapped port on the NES for background graphics)</lang>

Zero Page Offset By X/Y

A zero page memory address offset by X or Y. The value in X or Y is added to the supplied address, and the resulting address is used as the operand. Only the X register can use the "Zero Page Offset by Y" mode. If you want to store the accumulator in a zero page address offset by Y, you'll need to use the absolute address by padding the front of the address with 00. Some assemblers do this automatically, which is why I got this wrong!

<lang 6502asm>LDX #$05 ;load 5 into X LDA $02,x ;load the value stored in $07 into the accumulator. (2 + 5 = 7) LDY #$04 ;load 4 into Y LDX $12,y ;load the value stored in $16 into X. ($12 + $4 = $16)</lang>

Absolute Offset By X/Y

An absolute memory address offset by X or Y. This works similar to the zero page version. However, not all commands work with this mode. For example, the LDX and LDY commands work with this mode, but STX and STY do not. (LDA and STA work with all addressing modes except Zero Page Offset By Y.) <lang 6502asm>LDX #$15 LDY #$20 LDA $4000,x ;evaluates to LDA $4015 SBC $7000,y ;the accumulator is reduced by the value stored at $7020. If the carry is clear, 1 is subtracted from the result</lang>

Zero Page Indirect With Y

This one's a bit confusing. The values at a pair of consecutive zero page memory addresses are dereferenced, their order is swapped, the two values are concatenated into a 16-bit memory address, THEN the value of y is added to that address, and the value at that address is used as the operand. Whew! Let's break it up into steps.

<lang 6502asm>LDA #$40 STA $02  ; $02 contains #$40

LDA #$20 STA $03  ; $03 contains #$20, $02 contains #$40

LDY #$06  ; Y contains #$06

LDA ($02),y ; load the value at address $2040+y = load the value at address $2046</lang>

Note that for this mode, you are required to offset by Y. If you really don't want to offset by Y, load #0 into Y first.

Zero Page Indirect With X

This is similar to the one above. In fact, the only difference besides the register we use is the order of operations. Rather than adding Y after the dereference and concatenation, X is added BEFORE that step. X is placed inside the parentheses to show this. This mode is useful for writing to non-consecutive memory addresses in quick succession, by storing the addresses at consecutive zero page locations. Once again, let's break it down:

<lang 6502asm>LDA #$40 STA $06 LDA #$20 STA $07  ;$07 contains #$20, $06 contains #$40

LDX #$06 ;X contains #$06

LDA ($00,x) ;adds x to $00. Then the same thing happens as LDA ($06),y where y=0. This evaluates to LDA $2040, loading the accumulator

           ;with whatever value happens to be stored there.</lang>

Like before, you are required to use X in this mode. If you don't want to offset, just have X equal zero. In fact, when x and y both equal zero, ($HH,x) = ($HH),y for all 8-bit hexadecimal values $HH.

Quirks and Tricks For Efficient Coding

Looping Backwards Is Faster

Looping is generally faster if the loop counter goes down rather than up. This is because DEX and DEY set the zero and negative flags if their value is zero or #$80 or greater. Generally speaking, this means that when your loop counter goes down, you don't have to use the CMP command to determine if the end of the loop is reached. <lang 6502asm> LDX #3 ;set loop counter to 3. loop:

whatever you want to do in a loop goes here

DEX ;this statement basically has CPX #0 built-in at no additional cost BNE loop</lang>

compared to: <lang 6502asm> LDX #0 ;set loop counter to 0. loop:

whatever you want to do in a loop goes here

INX CPX #3 BCC loop</lang>

The second version takes an additional command per loop for no added benefit. Sometimes you may need X to represent something else in addition to the loop counter, or you may have a large amount of data from an external source, which would take a lot of time to manually reverse the order of the entries. In those cases it may be better to take the "branch penalty" as-is.

Know Your Opcodes

Of course you should know what each instruction does, but it's also very handy on the 6502 to know what their hex values are, as well as how many bytes they take in memory, and their execution time. For example, look at the subroutine below. In the comments, the byte length of the opcode is listed. For brevity the majority of the subroutine will be omitted, but imagine that the subroutine is more than 128 bytes long and thus a short branch is impossible.

<lang 6502asm>loop: lda $2000,x ;3 bytes bne continue ;2 bytes jmp end ;3 bytes continue:

imagine this is a very long subroutine where branching isn't possible


end: rts ;1 byte</lang>

It would take fewer bytes to do this: <lang 6502asm>loop: lda $2000,x ;3 bytes bne continue ;2 bytes rts ;1 byte continue:

imagine this is a very long subroutine where branching isn't possible


end: rts ;1 byte</lang>

Unfortunately, almost all optimizations in assembly languages are at the expense of readability (probably the biggest reason why assembly isn't used as much these days), and this one is no exception. When programming in 6502 in particular you will find yourself committing all the taboos of modern programming, such as BREAK, GOTO, and sometimes even the dreaded, forbidden, SELF-MODIFYING CODE!

Citations

  1. Wikipedia: MOS Technology 6502
  2. Wikipedia: Ricoh 2A03
  3. [6502 assembler in JavaScript]

See Also

Subcategories

This category has the following 3 subcategories, out of 3 total.

Pages in category "6502 Assembly"

The following 129 pages are in this category, out of 129 total.