Skip to content

Programming by Design

If you're not prepared to be wrong, you'll never come up with anything original. – Sir Ken Robinson

  • About
  • Java-PbD
  • C-PbD
  • ASM-PbD
  • Algorithms
  • Other

Chapter 6502-5 – Beyond the Basics

Posted on August 4, 2024February 15, 2025 By William Jojo
AsmBook

(Updated February 15, 2025)

Table of Contents

    Multi-way If Test
    Arrays
    Subroutines
    Self-modifying Code

Multi-way If Test

We will use the following code to demonstrate several features once used on the 6502.

Example1.asm
define  CHROUT  $FFD2

start:
    jsr  printres
    lda  #$65
    sec
    sbc  #$84
    beq  printzero
    bvs  printovf
    bmi  printneg
    bpl  printpos
    
end:
    brk

; Yay! self-modifying code!
printneg:
    lda  #<negtxt
    sta  prints+1
    lda  #>negtxt
    sta  prints+2
    jmp  print
printpos:
    lda  #<postxt
    sta  prints+1
    lda  #>postxt
    sta  prints+2
    jmp  print
printzero:
    lda  #<zerotxt
    sta  prints+1
    lda  #>zerotxt
    sta  prints+2
    jmp  print
printovf:
    lda  #<ovftxt
    sta  prints+1
    lda  #>ovftxt
    sta  prints+2

; main print loop for all categories
print:
    ldx  #0
prints:
    lda  $0000,x  ; gets replaced by addr
    beq  printdone
    jsr  CHROUT
    inx
    jmp  prints
printdone:
    brk

; print leading text
printres:
    ldx  #0
prloop:
    lda  restxt,x
    beq  resdone
    jsr  CHROUT
    inx
    jmp  prloop
resdone:
    rts
    
restxt:
    txt  "The result is "
    dcb  0
negtxt:
    txt  "negative.\n"
    dcb  0
postxt:
    txt  "positive.\n"
    dcb  0
zerotxt:
    txt  "zero.\n"
    dcb  0
ovftxt:
    txt  "OVERFLOWED!\n"
    dcb  0

The first thing we will cover is the multi-way if-test or multi-way branch. Since any comparison performs a subtraction that doesn’t affect the overflow flag, we will use subtraction to test for that as well. Based on the results, we can make multiple branches.

    lda  #$65
    sec
    sbc  #$84
    beq  printzero
    bvs  printovf
    bmi  printneg
    bpl  printpos

The first part of the code performs a basic subtraction. In this case, we are subtracting -124 from 101. Of course, this is the same as adding 124 to 101. It will overflow. If we care about that, we should act early on, as we will also set the negative flag because it overflowed.

However, this section is more about performing a multi-way branch based on the subtraction results. This is where branching really shines. As long as the destination is = 127 bytes forward or = 128 bytes backward, this works perfectly.

printneg:
    lda  #<negtxt
    sta  prints+1
    lda  #>negtxt
    sta  prints+2
    jmp  print
printpos:
    lda  #<postxt
    sta  prints+1
    lda  #>postxt
    sta  prints+2
    jmp  print
printzero:
    lda  #<zerotxt
    sta  prints+1
    lda  #>zerotxt
    sta  prints+2
    jmp  print
printovf:
    lda  #<ovftxt
    sta  prints+1
    lda  #>ovftxt
    sta  prints+2

All branches are nearby and accessible to the set of branch instructions. The code above uses the entry points for each branch to set up what will be printed and then jumps to the routine to print it. Overall, this is a simple and elegant model for reducing repetitive code.


Arrays

We’ve already seen arrays in action in some examples in other chapters. Here, we will explain precisely how this works.

; print leading text
printres:
    ldx  0
prloop:
    lda  restxt,x
    beq  resdone
    jsr  CHROUT
    inx
    jmp  prloop
resdone:
    rts

; ...

restxt:
    txt  "The result is "
    dcb  0

Recall the example from Chapter 2, where we compared some Java code with assembly language. We were trying to impress upon you that the X register was acting as a loop control variable much the same way as the x variable in the Java for loop.

The restxt label indicates the memory location where the string of characters begins. We set the X register to zero. Then, we load the accumulator (LDA) with the character at restxt plus X. This is a form of absolute addressing. It takes the absolute address of restxt and adds X to it. The resulting address is where we get the character from. As X increases with the INX instruction, we eventually visit every character in the string.

When we load the zero at the end of the string, the zero flag is set and we do the BEQ, branching to resdone.


Subroutines

Subroutines make use of the JSR and RTS instructions. JSR is Jump to SubRoutine. The CPU places the return address on the stack and jumps to the location noted after the JSR instruction.

Incidentally, the example code from the array example also demonstrates how we set up subroutines.

; print leading text
printres:
    ldx  0
prloop:
    lda  restxt,x
    beq  resdone
    jsr  CHROUT
    inx
    jmp  prloop
resdone:
    rts

Note the RTS as the last instruction. This is the ReTurn from Subroutine instruction. This is how the CPU knows we’ve reached the end of the subroutine. It pops the saved return address from the stack and jumps back to that address to continue execution.


Self-modifying Code

A particularly useful feature of assembly language programming is also dangerous, but it yields significant returns when used correctly.

Self-modifying code is precisely what the name suggests. The program is assembled into its finished form and ready for execution. However, part of the program is designed to intentionally change part of the program after it has been assembled.

printovf:
    lda  #<ovftxt
    sta  prints+1
    lda  #>ovftxt
    sta  prints+2

; main print loop for all categories
print:
    ldx  0
prints:
    lda  $0000,x  ; gets replaced by addr
    beq  printdone
    jsr  CHROUT
    inx
    jmp  prints
printdone:
    brk

; ...

ovftxt:
    txt  "OVERFLOWED!\n"
    dcb  0

The above code shows one of the four branch targets, which prints the subtraction result as zero, overflowed, positive, or negative. This snippet is the last in the original code’s list and is designed to handle the possibility of overflow.

At the memory location identified by prints, we have

    lda $0000,x

At location prints+0, we have the LDA instruction. At prints+1 is the low-order byte of the address, and at prints+2 is the high-order byte.

The following code replaces the $0000 with the address of ovftxt.

    lda  #<ovftxt
    sta  prints+1
    lda  #>ovftxt
    sta  prints+2

It does this by extracting the low-order byte of ovftxt and placing it in prints+1. The #<ovftxt means low-order byte of the address ovftxt.

Then, it does this again for the high-order byte and places it in prints+2. At the time of this writing, the assembled form of this portion of the program looks like the following.

    LDA #$8c
    STA $0645
    LDA #$06
    STA $0646

This means that the address of ovftxt is $068c. This also means the low-order byte is #$8c, and the high-order byte is #$06.

Further, prints+1 is addressed at $0645, and prints+2 is addressed at $0646. This is shown below.

Original Source Code Assembled Code
    lda  #<ovftxt
    sta  prints+1
    lda  #>ovftxt
    sta  prints+2
    LDA #$8c
    STA $0645
    LDA #$06
    STA $0646

So, when all is said and done, the code above transforms the code like so:

Original Source Code Assembled Code
print:
    ldx  #0
prints:
    lda  $0000,x  ; gets replaced by addr
    beq  printdone
    jsr  CHROUT
    inx
    jmp  prints
printdone:
    brk

    LDX #$00

    LDA $068c,X
    BEQ $0650
    JSR $ffd2
    INX 
    JMP $0644

    BRK 

Hopefully, this chapter has brought many of the details together in a more cohesive way. While understanding assembly language involves a number of moving parts, you should begin to appreciate its power, even though it’s rather primitive.

Post navigation

❮ Previous Post: Chapter 6502-4 – The Basics
Next Post: Variations on a theme – 111 – Project 1 ❯

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright © 2018 – 2025 Programming by Design.