Skip to content

Lesson 01: Number Systems โ€” Binary, Hexadecimal, and You โ€‹

Part 0: Foundations ยท Estimated time: 45 minutes Prerequisites: None โ€” this is where it all begins! You will learn: Binary, hexadecimal, decimal conversion, bit/nibble/byte terminology, bitmasks You will build: A demo ROM that visualizes hex color values in Stella

Introduction โ€‹

Before we write a single line of assembly code, we need to learn the language that computers actually speak: numbers. Not decimal numbers like we humans prefer, but binary and hexadecimal โ€” the two number systems that every assembly programmer must think in fluently.

Why does this matter? The Atari 2600 has no strings, no floating-point math, no objects โ€” just bytes. Every color, every sprite pixel, every sound, every joystick direction is encoded as a pattern of 8 bits. To program the 2600, you need to see $1A and immediately know what color that is. You need to look at %10110100 and know which bits are set. This lesson gives you that superpower.

By the end of this lesson, you'll convert between binary, hex, and decimal in your head, understand why programmers prefer hex over decimal, and know what a "nibble" is (hint: it's not a snack).

TypeScript Equivalent

If you've written TypeScript or JavaScript, you've already used hex โ€” you just might not have noticed:

typescript
// Hex in TypeScript โ€” the 0x prefix
const red = 0xFF0000;     // 16,711,680 in decimal
const mask = 0x0F;        // 15 in decimal โ€” lower nibble mask
const color = 0x1A;       // 26 in decimal

// Bitwise operators โ€” same concept, different syntax
const upper = (0xAB >> 4) & 0x0F;  // Extract upper nibble โ†’ 0x0A
const lower = 0xAB & 0x0F;         // Extract lower nibble โ†’ 0x0B

// parseInt for base conversion
parseInt('FF', 16);  // โ†’ 255
parseInt('11111111', 2);  // โ†’ 255
(255).toString(16);  // โ†’ "ff"
(255).toString(2);   // โ†’ "11111111"

In 6502 assembly, the syntax is different but the concepts are identical:

  • 0xFF becomes $FF
  • 0b11111111 becomes %11111111
  • & (AND) becomes the AND instruction
  • | (OR) becomes the ORA instruction
  • ^ (XOR) becomes the EOR instruction

Theory โ€‹

Section 1: Why Not Decimal? โ€‹

You already know decimal โ€” base 10, ten digits (0-9), what you've used your whole life. So why learn anything else?

The answer is alignment with hardware. Computers work in binary: every wire is either on or off, every memory cell stores a 0 or a 1. A single binary digit is called a bit. The Atari 2600's processor, the 6507, works with groups of 8 bits at a time โ€” and we call that group a byte.

Here's the problem with decimal: the number 255 doesn't look like anything special. But in binary it's 11111111 โ€” every bit is set. In hex it's FF โ€” the maximum value for a byte. Hex makes the bit patterns visible in a way that decimal never can.

DecimalBinaryHexWhat It Means
000000000$00All bits off
1500001111$0FLower nibble full
1600010000$10One hex digit rolls over
12810000000$80Only the high bit set
25511111111$FFAll bits on โ€” maximum byte value

Section 2: Binary โ€” Base 2 โ€‹

Binary has only two digits: 0 and 1. Each digit is a bit (short for "binary digit"). In 6502 assembly, we write binary numbers with a % prefix: %10110100.

Each bit position represents a power of 2, counted from right to left:

Bit position:    7     6     5     4     3     2     1     0
Power of 2:    128    64    32    16     8     4     2     1

To convert binary to decimal, add up the powers of 2 where the bit is 1:

%10110100  =  128 + 32 + 16 + 4  =  180
 โ”‚ โ”‚โ”‚ โ”‚
 โ”‚ โ”‚โ”‚ โ””โ”€โ”€ bit 2 = 4
 โ”‚ โ”‚โ””โ”€โ”€โ”€โ”€ bit 4 = 16
 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€ bit 5 = 32
 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€ bit 7 = 128

Key Binary Terminology โ€‹

TermSizeRangeExample
Bit1 bit0โ€“1A single 0 or 1
Nibble4 bits0โ€“15%1010 = 10 decimal
Byte8 bits0โ€“255%10110100 = 180 decimal

A byte is two nibbles side by side. The upper nibble is bits 7-4, and the lower nibble is bits 3-0:

  Byte: %1011 0100
         ^^^^โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Upper nibble = %1011 = 11 = $B
              ^^^^โ”€โ”€ Lower nibble = %0100 =  4 = $4
  
  Combined in hex: $B4 = 180 decimal

This is why hex is so perfect โ€” each hex digit maps exactly to one nibble.

Section 3: Hexadecimal โ€” Base 16 โ€‹

Hexadecimal (hex) uses sixteen digits. Since we only have ten digit symbols (0-9), we borrow six letters:

HexDecimalBinary
000000
110001
220010
330011
440100
550101
660110
770111
881000
991001
A101010
B111011
C121100
D131101
E141110
F151111

In 6502 assembly, hex numbers use a $ prefix: $FF, $1A, $80.

Converting Hex to Decimal โ€‹

Each hex position is a power of 16:

$B4  =  (B ร— 16) + (4 ร— 1)
     =  (11 ร— 16) + (4 ร— 1)
     =   176 + 4
     =   180

Converting Hex to Binary (the easy way) โ€‹

Just expand each hex digit to its 4-bit binary equivalent:

$B4  โ†’  $B    $4
     โ†’  1011  0100
     โ†’  %10110100

This is the magic of hex: no math needed โ€” just a simple table lookup per digit.

TypeScript Equivalent
typescript
// In TypeScript, the conversions look like this:
const hex = 0xB4;
console.log(hex);              // โ†’ 180 (decimal)
console.log(hex.toString(2));  // โ†’ "10110100" (binary)
console.log(hex.toString(16)); // โ†’ "b4" (hex)

// Going the other way:
parseInt('B4', 16);   // โ†’ 180
parseInt('10110100', 2); // โ†’ 180

Section 4: Hex on the Atari 2600 โ€‹

The 2600 uses hex everywhere. Here are some examples you'll see throughout this masterclass:

ContextValueMeaning
Colors$1AOrange, medium brightness
RAM addresses$80First byte of RAM
TIA registers$09COLUBK โ€” background color
ROM start$F000Beginning of cartridge ROM
Vectors$FFFCReset vector address

Colors on the 2600 are especially interesting. The color byte encodes two things:

Color byte:  CCCC LLL0
             โ”‚โ”‚โ”‚โ”‚ โ”‚โ”‚โ”‚โ””โ”€โ”€ Bit 0: unused (always 0)
             โ”‚โ”‚โ”‚โ”‚ โ””โ””โ””โ”€โ”€โ”€ Bits 1-3: Luminance (brightness, 0-7)
             โ””โ””โ””โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Bits 4-7: Hue (color, 0-15)

So the color $1A:

$1A  =  %0001 1010
         ^^^^โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Hue = $1 = 1 (yellow-orange family)
              ^^^โ”€โ”€โ”€โ”€ Luminance = %101 = 5 (medium-bright)
                 ^โ”€โ”€โ”€ Bit 0 = 0 (unused)

PAL Note

NTSC systems have 128 colors (16 hues ร— 8 luminances). PAL systems have 104 colors with a different hue mapping. The hex notation is the same, but the actual color you see on screen may differ. Throughout this masterclass, we'll use NTSC color values. PAL equivalents will be noted where they differ significantly.

Section 5: Bitmasks โ€” The Power of AND, OR, and XOR โ€‹

Now that you understand bits, here's where it gets powerful. Bitmasks let you inspect, set, or clear individual bits within a byte. This is how the 2600 packs multiple pieces of information into a single byte.

AND โ€” Keep only the bits you want โ€‹

AND compares each bit position: the result is 1 only if both inputs are 1.

  %10110100   โ† original value
AND %00001111   โ† mask: keep lower nibble only
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  %00000100   โ† result: upper nibble cleared, lower nibble preserved

Use case: Extracting the lower nibble of a color to get the luminance:

asm
LDA #$1A        ; Color value
AND #$0F        ; Mask off upper nibble โ†’ result: $0A (luminance bits)

OR โ€” Set specific bits โ€‹

OR compares each bit position: the result is 1 if either input is 1.

  %10110000   โ† original value
ORA %00000100   โ† mask: set bit 2
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  %10110100   โ† result: bit 2 is now set, everything else unchanged

Use case: Setting a specific flag in a status byte.

EOR (XOR) โ€” Toggle specific bits โ€‹

EOR (exclusive OR) compares each bit position: the result is 1 if the inputs are different.

  %10110100   โ† original value
EOR %11111111   โ† mask: flip all bits
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  %01001011   โ† result: every bit toggled

Use case: Toggling a flag on/off โ€” XOR with the same mask twice returns to the original value.

TypeScript Equivalent
typescript
// Bitmask operations โ€” identical concepts in TypeScript
const color = 0x1A;

// AND โ€” extract lower nibble
const luminance = color & 0x0F;     // โ†’ 0x0A

// OR โ€” set bit 2
const withFlag = 0xB0 | 0x04;      // โ†’ 0xB4

// XOR โ€” toggle bits
const flipped = 0xB4 ^ 0xFF;       // โ†’ 0x4B
const backAgain = 0x4B ^ 0xFF;     // โ†’ 0xB4 (back to original!)

Section 6: Two's Complement โ€” Signed Numbers โ€‹

The 6502 can also work with signed numbers โ€” values that can be negative. It uses a system called two's complement, where the highest bit (bit 7) indicates the sign:

  • Bit 7 = 0 โ†’ positive number (0 to 127)
  • Bit 7 = 1 โ†’ negative number (-128 to -1)
$00 =   0     %00000000
$01 =   1     %00000001
$7F = 127     %01111111  โ† largest positive value
$80 = -128    %10000000  โ† most negative value
$FF =  -1     %11111111
$FE =  -2     %11111110

You'll encounter two's complement when working with horizontal motion registers (HMPx) โ€” they use a 4-bit signed value to move sprites left or right:

Hex (upper nibble)Signed ValueDirection
$70+77 pixels left
$10+11 pixel left
$000No movement
$F0-11 pixel right
$80-88 pixels right

Don't worry about memorizing this now โ€” we'll use it extensively in the sprite lessons.

The Code โ€‹

Building the Demo โ€‹

bash
cd lessons/part0-foundations/01-number-systems
acme -f plain -o ../../../build/lesson01.bin 01-number-systems.asm

Or use the Makefile:

bash
make lesson01

Full Source โ€‹

The main lesson ROM stores hex values in RAM and uses AND/OR masks โ€” you can modify values and observe results in the Stella debugger.

asm
; =============================================================================
; LESSON 01: Number Systems โ€” Binary, Hexadecimal, and You
; =============================================================================
; Part 0: Foundations
;
; What this program does:
;   Stores hex values in RAM, applies AND/OR/EOR bitmasks, and displays
;   the results as background colors. Each scanline zone shows a different
;   value so you can SEE the connection between hex numbers and colors.
;
; What you'll learn:
;   - How hex values map to Atari 2600 colors
;   - How AND masks extract parts of a byte
;   - How OR and EOR modify bytes
;
; Build: acme -f plain -o build/lesson01.bin lessons/part0-foundations/01-number-systems/01-number-systems.asm
; Run:   stella build/lesson01.bin
; =============================================================================

    !cpu 6502

; --- Include hardware definitions and macros ---
    !source "include/vcs.asm"
    !source "include/macro.asm"

; =============================================================================
; RAM VARIABLES
; =============================================================================

OriginalValue   = $80    ; The value we start with
MaskedLower     = $81    ; After AND #$0F โ€” lower nibble only
MaskedUpper     = $82    ; After AND #$F0 โ€” upper nibble only (shifted)
OrResult        = $83    ; After ORA โ€” bits set
EorResult       = $84    ; After EOR โ€” bits toggled
FrameCount      = $85    ; Frame counter

; =============================================================================
; ROM START
; =============================================================================

    * = $F000

; =============================================================================
; ENTRY POINT
; =============================================================================
Reset:
    +clean_start

    ; --- Initialize our demo values ---
    ; Store a recognizable hex value in RAM
    LDA #$1A             ; $1A = orange (hue 1, luminance 5)
    STA OriginalValue

    ; AND mask: extract lower nibble
    AND #$0F             ; $1A AND $0F = $0A
    STA MaskedLower      ; Store result: $0A

    ; AND mask: extract upper nibble  
    LDA OriginalValue
    AND #$F0             ; $1A AND $F0 = $10
    STA MaskedUpper      ; Store result: $10

    ; OR: set additional bits
    LDA OriginalValue
    ORA #$44             ; $1A ORA $44 = $5E
    STA OrResult         ; Store result: $5E

    ; EOR: toggle bits
    LDA OriginalValue
    EOR #$FF             ; $1A EOR $FF = $E5 (all bits flipped)
    STA EorResult        ; Store result: $E5

; =============================================================================
; MAIN LOOP
; =============================================================================

StartFrame:

; --- VSYNC (3 scanlines) ---
    +vsync

; --- VBLANK (37 scanlines) ---
    LDA #$02
    STA VBLANK
    +set_timer 43

    ; === GAME LOGIC ===
    INC FrameCount
    ; === END GAME LOGIC ===

    +wait_timer
    LDA #$00
    STA VBLANK

; --- KERNEL (192 visible scanlines) ---
; We divide the screen into 5 zones of ~38 lines each,
; displaying each of our computed values as a background color.

; #region kernel

    ; --- Zone 1: Original value $1A (38 lines) ---
    LDA OriginalValue    ; $1A = orange
    STA COLUBK
    LDX #38
-   STA WSYNC
    DEX
    BNE -

    ; --- Zone 2: AND lower nibble $0A (38 lines) ---
    LDA MaskedLower      ; $0A = very dark yellow/gold
    STA COLUBK
    LDX #38
-   STA WSYNC
    DEX
    BNE -

    ; --- Zone 3: AND upper nibble $10 (38 lines) ---
    LDA MaskedUpper      ; $10 = dark yellow-orange
    STA COLUBK
    LDX #38
-   STA WSYNC
    DEX
    BNE -

    ; --- Zone 4: OR result $5E (40 lines) ---
    LDA OrResult         ; $5E = bright pink/magenta
    STA COLUBK
    LDX #40
-   STA WSYNC
    DEX
    BNE -

    ; --- Zone 5: EOR result $E5 (38 lines) ---
    LDA EorResult        ; $E5 = light blue-green
    STA COLUBK
    LDX #38
-   STA WSYNC
    DEX
    BNE -

; #endregion kernel

; --- OVERSCAN (30 scanlines) ---
    LDA #$02
    STA VBLANK
    +set_timer 35

    +wait_timer

    JMP StartFrame

; =============================================================================
; VECTORS
; =============================================================================

    * = $FFFA
    !word Reset     ; NMI
    !word Reset     ; RESET
    !word Reset     ; IRQ

Code Walkthrough โ€‹

Let's walk through the key parts of this program.

Initialization โ€” Setting Up Our Values โ€‹

When the program starts, we store $1A (orange) in RAM, then compute four bitmask operations:

OperationExpressionResultColor on Screen
Original$1A$1AOrange
AND lower$1A AND $0F$0ADark gold
AND upper$1A AND $F0$10Dark yellow
OR$1A ORA $44$5EBright pink
EOR$1A EOR $FF$E5Light blue

Each result is stored in a separate RAM address so you can inspect them in the Stella debugger.

The Kernel โ€” Seeing the Results โ€‹

The kernel divides the 192 visible scanlines into 5 horizontal zones. Each zone loads one of our computed values and sets it as the background color (COLUBK). The result is 5 colored bands on screen โ€” each band is the visual representation of a bitmask operation.

What You Should See โ€‹

When you run this ROM in Stella, you'll see five horizontal color bands filling the screen from top to bottom:

  1. Top band โ€” Orange ($1A): the original value
  2. Second band โ€” Dark gold ($0A): lower nibble extracted with AND
  3. Third band โ€” Dark yellow ($10): upper nibble extracted with AND
  4. Fourth band โ€” Bright pink ($5E): bits added with OR
  5. Bottom band โ€” Light blue ($E5): all bits flipped with EOR

Bitmask demo showing five colored bands โ€” each band represents a different bitmask operation on the hex value $1A

โฌ‡ Download ROM (.bin)

Try this in the Stella debugger (press the backtick key `):

  • Look at RAM addresses $80 through $85
  • You should see: 1A 0A 10 5E E5 โ€” the exact values from our bitmask operations
  • Change $80 to a different value and watch how all the other values (and colors) would change on the next reset

The Demo ROM โ€‹

The demo ROM below creates a visual "hex color chart" โ€” 16 rows of colors where each row shows a different hue at 8 luminance levels. This is a practical reference for the Atari 2600's NTSC color palette.

Hex color chart showing all 128 NTSC colors organized by hue and luminance

โฌ‡ Download ROM (.bin)

The source code for this demo is at lessons/part0-foundations/01-number-systems/demo-01.asm.

Exercises โ€‹

Exercise 1: Base Conversion Practice โ€‹

Challenge: Convert each of these values between all three bases. Do it on paper first โ€” no calculators!

GivenFind DecimalFind BinaryFind Hex
$2F??โ€”
%11001010?โ€”?
200โ€”??
$80??โ€”
%00110011?โ€”?

Hints:

  1. For hex to decimal: multiply each digit by its power of 16 (upper digit ร— 16, lower digit ร— 1)
  2. For binary to hex: split into groups of 4 bits from the right, convert each group
  3. For decimal to binary: repeatedly divide by 2, the remainders (bottom to top) are your bits

Expected Result:

GivenDecimalBinaryHex
$2F47%00101111$2F
%11001010202%11001010$CA
200200%11001000$C8
$80128%10000000$80
%0011001151%00110011$33

Exercise 2: Predict the Bitmask โ€‹

Challenge: Predict the result of each bitmask operation without running the code. Then modify the ROM to verify your answers.

asm
; What is the result of each operation?
LDA #$A7
AND #$0F        ; Result = ?

LDA #$30
ORA #$0C        ; Result = ?

LDA #$55
EOR #$FF        ; Result = ?

LDA #$FF
AND #$F0        ; Result = ?

LDA #$00
ORA #$1A        ; Result = ?

Hints:

  1. AND keeps only bits that are 1 in both the value and the mask
  2. ORA sets bits that are 1 in either the value or the mask
  3. EOR flips bits that are 1 in the mask
  4. Write out the binary for both values and apply the operation bit-by-bit

Expected Result:

  • $A7 AND $0F = $07 (extracted lower nibble)
  • $30 ORA $0C = $3C (combined the bits)
  • $55 EOR $FF = $AA (all bits toggled โ€” neat pattern!)
  • $FF AND $F0 = $F0 (cleared lower nibble)
  • $00 ORA $1A = $1A (set bits from mask into zero)

Exercise 3: Design Your Own Color Palette (Stretch Goal) โ€‹

Challenge: Using what you know about the 2600 color byte format (CCCC LLL0), design a 5-color palette for a simple game. Pick colors for: sky, grass, player, enemy, and score text. Write the hex values and explain your choices.

Hints:

  1. Remember: upper nibble = hue (0-F), lower nibble = luminance (even values only: 0, 2, 4, 6, 8, A, C, E)
  2. Higher luminance = brighter. $x0 is darkest, $xE is brightest
  3. Test your colors by modifying the ROM โ€” put each color in a zone

Expected Result: There's no single correct answer โ€” this is a design exercise. But your hex values should produce visually distinct, appropriate colors when tested in Stella. For example:

  • Sky: $96 (blue, medium brightness)
  • Grass: $C6 (green, medium brightness)
  • Player: $0E (white/bright gray)
  • Enemy: $46 (red, medium brightness)
  • Score: $0A (light gray)

Key Takeaways โ€‹

  • โœ… Binary (%) has 2 digits (0, 1). Each position is a power of 2. Eight bits = one byte (0โ€“255)
  • โœ… Hexadecimal ($) has 16 digits (0โ€“F). Each hex digit = exactly 4 bits (one nibble). Two hex digits = one byte
  • โœ… AND masks extract bits (keep only what you want). OR masks set bits. EOR masks toggle bits
  • โœ… The Atari 2600 encodes colors as CCCC LLL0 โ€” hue in the upper nibble, luminance in bits 1-3
  • โœ… Hex makes bit patterns visible โ€” $FF is obviously "all bits on" while 255 is not

What's Next โ€‹

In Lesson 02: Meet Stella โ€” Your Debugger and Best Friend, you'll learn to use the Stella emulator's built-in debugger. You'll step through code instruction by instruction, watch registers change in real-time, and inspect the exact RAM values that this lesson's bitmask operations produce. The debugger is where hex knowledge becomes truly powerful โ€” you'll see every byte in its natural habitat.

Released under the MIT License.