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-6 – Numbers and Math

Posted on February 8, 2025February 18, 2025 By William Jojo
AsmBook

(Updated February 18, 2025)

Table of Contents

    Overview
    Printing Numbers – Part I
    Bigger Numbers
    Bigger Math
    Printing Numbers – Part II

Overview

In previous chapters, we’ve glossed over details on numbers and mathematics. This is simply because math on this CPU is hard. Remember that all we can do is add. Even subtraction is nothing more than changing signs and adding.

Now, we will discuss the finer details of more significant numbers, bigger math, and how we can print numbers, which have notably gone unmentioned until now.


Printing Numbers – Part I

So, let’s start with basic printing! There are many algorithms for printing numbers. One such algorithm involves alternating modulus and division, thereby retrieving the digits in reverse order. Thrown on a stack, it’s a simple matter to pop them off and print them in succession, thereby giving you the number as an ASCII string.

However, we don’t yet have the luxury of division. We do, however, have the next best thing: subtraction. We can setup a table that starts at the largest multiple of ten and that becomes the starting point for printing the number.

But now we’re getting a little ahead of ourselves. For this initial printing discussion, we will focus on the code below.

signed8bit.asm
; This is the start of the 8-bit library.

define CHROUT $ffd2
define LZF  $30

; This is test code for the 8-bit printing
    LDX  #0
    LDY  #0
NUMLOOP:
    LDA  NUMBERS,Y
    STA  $31    ; LSB
    PHA         ; SAVE
    JSR  S8OUT ; PRINT AS SIGNED
    LDA  #7     ; '\t'
    JSR  CHROUT
    PLA         ; RESTORE
    STA  $31
    JSR  U8OUT ; PRINT AS UNSIGNED
    LDA  #13    ; '\n'
    JSR  CHROUT
    INY
    INX
    CPX  #5
    BNE NUMLOOP
    BRK

NUMBERS:
    dcb  -128, 127, -1, 1, 0


; S8OUT - print   signed 8bit value in $31
; U8OUT - print unsigned 8bit value in $31
; Based on code by Leo Scanlon in
; 6502 Software Design, 1980
; $31 LSB (clobbered)
; $30 LEADING ZERO FLAG (LZF, clobbered)
; CLOBBERS A, PRESERVES X & Y

S8OUT:
    JSR  S8OUTNEG
U8OUT:
    ; SAVE X & Y
    TXA
    PHA
    TYA
    PHA
    ; begin
    LDY  #0     ; INIT TABLE POINTER
    STY  LZF    ; CLEAR LZF
S8NEXTDIG:
    LDX  #0
S8DOSUBTRACT:
    LDA  $31
    SEC
    SBC  S8SUBTABLE,Y
    BCC  S8ADDBACK
    STA  $31
    INX
    JMP  S8DOSUBTRACT
    
S8ADDBACK:
    ; there's no actual add back for 8-bit
    TXA
    BNE  S8SETLZF
    BIT  LZF
    BMI  S8CNV2ASCII
    BPL  S8MOVUPTBL
S8SETLZF:
    LDX  #$80
    STX  LZF
S8CNV2ASCII:
    ORA  #$30
    JSR  CHROUT
S8MOVUPTBL:
    INY
    CPY  #$02   ; END OF TABLE?
    BCC  S8NEXTDIG
    LDA  $31    ; LAST DIGIT
    ORA  #$30
    JSR  CHROUT
    ; RESTORE Y & X
    PLA
    TAY
    PLA
    TAX
    RTS
; END S8OUT

; This routine inverts 8-bit sign and
; prints a '-'. Clobbers A.
S8OUTNEG:
    BIT  $31
    BPL  S8OUTEND
    JSR  S8INVSGN
    LDA  #$2D   ; '-'
    JSR  CHROUT 
S8OUTEND:
    RTS
; END S8OUTNEG

; Inverts sign of $31
; Clobbers A.
S8INVSGN:
    LDA  $31    ; LOAD LSB
    EOR  #$FF   ; FLIP BITS
    CLC
    ADC  #1     ; ADD ONE
    STA  $31    ; STORE
    RTS         ; DONE

S8SUBTABLE:
    dcb  100
    dcb  10

Entry Point

That’s a lot to digest all at once, but we’ll discuss most of what’s happening. Let’s begin by discussing the dual entry point for this code.

S8OUT:
    JSR  S8OUTNEG
U8OUT:
    ; SAVE X & Y

We’ve stacked the labels on how to start this subroutine. Its purpose is to print out an 8-bit number, which may be signed. Therefore, we have two entry points: S8OUT if you want to print a signed number and U8OUT to print an unsigned one. The only difference is that the signed version first calls S8OUTNEG, which calls S8INVSGN to invert the sign and then print out a minus sign.

Inverting the Sign

So, let’s now talk about inverting the sign.

; Inverts sign of $31
; Clobbers A.
S8INVSGN:
    LDA  $31    ; LOAD LSB
    EOR  #$FF   ; FLIP BITS
    CLC
    ADC  #1     ; ADD ONE
    STA  $31    ; STORE
    RTS         ; DONE

The expectation is that the number to be printed is first moved to the zero-page address of $31. We proceed to flip the bits using exclusive or, then, add 1. That’s it. The sign is now inverted.

Remember that this is only necessary if we treat the number as a signed value. Inverting the sign makes it easier to get the digits for the negative number. Consider the following:

255 is 11111111 as an unsigned number
 -1 is 11111111 as a signed number

It’s important to notice what’s happening here. When applied to our algorithm, the same bit pattern will give vastly differing results. This is why we have two entry points. If you treat the value as a signed quantity, call S8OUT; otherwise, call U8OUT.

Testing for Negative

Incidentally, it may be interesting to note how S8OUTNEG works.

; This routine inverts 8-bit sign and
; prints a '-'. Clobbers A.
S8OUTNEG:
    BIT  $31
    BPL  S8OUTEND
    JSR  S8INVSGN
    LDA  #$2D   ; '-'
    JSR  CHROUT 
S8OUTEND:
    RTS
; END S8OUTNEG

Note the use of BIT. This instruction does several things at once, but we’re only interested in copying bit 7 of the operand into the N flag. If the result of N is set, the number tested is negative. This means we need to invert the sign and print a minus sign. The BPL reacts to a positive number and jumps to the end of the subroutine.

Subtraction Table

Now for the magic of how this subroutine works. As you’ve likely guessed, it’s not magic but a series of subtractions starting with the largest possible and working our way down the powers of ten. Take a look at the following.

S8SUBTABLE:
    dcb  100
    dcb  10

This is a data byte table with 100 and 10 in descending order. Since the range of unsigned numbers is 0 to 255 and the range of signed is -128 to 127, we can see that we never get above the hundreds. Therefore, we don’t need anything more significant in our table.

Let’s go over the details. Remember that the number to be printed must first be moved into the zero page address $31.

First, there is some housekeeping. We’ve seen the beginning of this subroutine earlier, but revealing a bit more, we see some values placed on the stack. We take X and Y each and move them to the accumulator, then push them onto the stack. This allows us to preserve their values. Otherwise, we’d clobber them, and the calling subroutine would have to take additional action. It’s a small price for us to provide peace of mind that using this subroutine is safe and only the accumulator is affected.

S8OUT:
    JSR  S8OUTNEG
U8OUT:
    ; SAVE X & Y
    TXA
    PHA
    TYA
    PHA
    ; begin
    LDY  #0     ; INIT TABLE POINTER
    STY  LZF    ; CLEAR LZF

This is also reversed at the end of the subroutine. We pull the values off the stack and restore Y and X to their previous states just before we return.

    ; RESTORE Y & X
    PLA
    TAY
    PLA
    TAX
    RTS
; END S8OUT

Now, let’s dig into how the table works. The Y register will index the table, while the X register will keep track of the number of times we’ve subtracted. Of course, this value can never exceed 9.

There is also one other handy memory location. Zero page address $30 is our LZF or Leading Zero Flag. Since it’s possible we won’t use the hundreds or even the tens, we want to ensure that 7 isn’t printed as 007. When LZF is set, there are no more leading zeros.

    ; begin
    LDY  #0     ; INIT TABLE POINTER
    STY  LZF    ; CLEAR LZF
S8NEXTDIG:
    LDX  #0
S8DOSUBTRACT:
    LDA  $31
    SEC
    SBC  S8SUBTABLE,Y
    BCC  S8ADDBACK
    STA  $31
    INX
    JMP  S8DOSUBTRACT

We subtract the current table value. If the carry clears, we’ve borrowed, so we’re done counting the hundreds or tens, or whatever we’re on.

Otherwise, we keep subtracting, incrementing X along the way. Here’s what happens when we’re done subtracting.

S8ADDBACK:
    ; there's no actual add back for 8-bit
    TXA
    BNE  S8SETLZF
    BIT  LZF
    BMI  S8CNV2ASCII
    BPL  S8MOVUPTBL

The nature of the S8ADDBACK is taken from the more significant numbers (there’s a chapter with 16- and 32-bit examples.) We kept the naming the same. We move X to the accumulator and test it. A lot is happening in that small bit of code.

  • If A’s not zero, set LZF.
  • If A’s zero and LZF is set, print the digit.
  • Else, move up the table to the smaller tens.

This brings us to the following code. Setting the high bit of LZF indicates that we’ve seen our first non-zero digit.

S8SETLZF:
    LDX  #$80
    STX  LZF
S8CNV2ASCII:
    ORA  #$30
    JSR  CHROUT
S8MOVUPTBL:
    INY
    CPY  #$02   ; END OF TABLE?
    BCC  S8NEXTDIG
    LDA  $31    ; LAST DIGIT
    ORA  #$30
    JSR  CHROUT

Converting to ASCII is easy. Or-ing with #$30 is the same as adding #$30 to the number to make it a printable digit. If this is unclear, please consult an ASCII table with hexadecimal as one of the column options.

Finally, moving up the table involves increasing Y and checking to see if the table’s been exhausted. If it has, we’ve reached the last digit, so we convert to ASCII and print it.

The complete number has now been printed.

Printing the Number

Interestingly enough, we’ve already printed it! The previous mini-sections define how the pieces fit together to produce a printed number. However, we can still discuss how we tested the algorithm.

First, direct your attention to the fact that we’re using Y to control the testing of the S8OUT and U8OUT subroutines. Hopefully, you can now appreciate why the subroutine takes the time to preserve the registers and make it easier for programmers to use them.

; This is test code for the 8-bit printing
    LDY  #0
NUMLOOP:
    LDA  NUMBERS,Y
    STA  $31    ; LSB
    PHA         ; SAVE
    JSR  S8OUT  ; PRINT AS SIGNED
    LDA  #7     ; '\t'
    JSR  CHROUT
    PLA         ; RESTORE
    STA  $31
    JSR  U8OUT  ; PRINT AS UNSIGNED
    LDA  #13    ; '\n'
    JSR  CHROUT
    INY
    CPY  #5
    BNE NUMLOOP
    BRK

NUMBERS:
    dcb  -128, 127, -1, 1, 0

The label NUMLOOP marks the top of the loop controlled by Y. We use absolute addressing (indexed by Y) to load a number into the accumulator. This is stored in $31 and the stack.

We then print the signed version of that number, pull the copy from the stack, and store it back into $31 to print as an unsigned value.

We continue incrementing Y until all the numbers have been printed.


Bigger Numbers

As this section suggests, we need more significant numbers. Using 8 bits just won’t do. So, we need to consider 16- and 32-bit values. Anything more substantial on this CPU is unrealistic.

We will be storing the values as little-endian. We will use the numbers $89, $89AB, $89ABCDEF to demonstrate how this will work.

8-bit
$89 is 10001001, which is -119 signed and 137 unsigned
This is stored in memory as $89.

16-bit
$89AB is 1000100110101011, which is -30293 signed and 35243 unsigned.
This is stored in memory as $AB, $89.

32-bit
$89ABCDEF is 10001001101010111100110111101111, which is -1985229329 signed and 2309737967 unsigned.
This is stored in memory as $EF, $CD, $AB, $89.

Given the nature of this CPU, little-endian is incredibly smart. Adding two numbers together allows us to start at low memory and have the carry extend into the next byte.

Let’s see what this looks like.


Bigger Math

When we discussed printing 8-bit numbers, we also mentioned needing a facility for more significant numbers. One of the appendices contains code for printing 32-bit numbers. However, that code is beyond the scope of this topic, so for now, we will refer to it as S32OUT. It prints the little-endian value stored in $31 through $34.

Consider the following piece of code.

define CHROUT  $FFD2

    ldx  #0
    clc
addloop:
    lda  num1,x
    adc  num2,x
    sta  $31,x
    cpx  #3
    beq  adddone
    inx
    jmp  addloop
adddone:
    jsr  S32OUT
    brk

num1:
    dcq  -238729586  ; $8e, $46, $c5, $f1
num2:
    dcq  1279846486  ; $56, $e8, $48, $4c

Like adding numbers on paper, we start at the least significant end, work toward the most significant, and carry as we go.

We are doing the same thing here but with a loop. Since these are stored as little-endian, the little end is stored first. Using X, we can treat num1 and num2 as arrays of bytes.

Treating the range $31-$34 the same way (and setting it up for printing), we simply read a byte from num1, add the byte from num2, and store the result in page zero. After we iterate over all 4 bytes, we have the complete sum with carries. We then print the number and end.

One frequently asked question is, “If I’m adding two numbers, how do I know that a single-bit carry is enough?”

That’s a great question! Let’s do some mental gymnastics first, and then we’ll examine the bit pattern.

Let’s take this from an unsigned perspective because this explains it best. Remember that 8 bits have the largest possible value of 255. If we add one, we need one more bit to get us to 256 – this is our carry. Effectively, bit 9

Not convinced?

Ok, take 255 and 255 and add them together. It’s 510, correct? So, if bit 9 allows us to break into 256 plus the original 8 bits, we are still shy of bit 10 which is 512.

Still not convinced? Ok.


            C
Carry ->    1   1111111
                11111111 (255)
              + 11111111 (255)
                --------
            1   11111110 (510, if you include the set carry)

The individual bit carries are shown. The singular carry bit (C) for the overall operation is also shown. As you can see, no matter how big we make the numbers for the sum, we never need more than one bit to make this work.

So, to make bigger numbers, we just need one carry bit to carry the extra into the next byte as we add.


Printing Numbers – Part II

This chapter introduced the basic concept of printing numbers. They were small, simple, and fast. Now, with 4 bytes for a 32-bit number, you can imagine that printing more significant numbers involves much more subtraction, which takes longer.

In Appendix A, we have a lot of code examples that show how to accomplish 8-, 16-, and 32-bit printing. Remember that this all ran on a 1.0MHz MOS 6502 CPU. So, it’s going to be a little pokey, especially in an emulator/simulator trying to mimic that clock speed.

Post navigation

❮ Previous Post: Bowling Regex
Next Post: Chapter 6502-A – 8-, 16-, and 32-bit Printing. ❯

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

Copyright © 2018 – 2025 Programming by Design.