Programming Fundamentals

Why This Matters

Hardware without software is an expensive paperweight. A computer built from logic gates can do nothing until someone tells it what to do, step by step, in a language it understands. Programming is the bridge between raw digital hardware and useful computation. This article teaches you how to write instructions for a processor, from raw binary machine code through assembly language to the first higher-level languages.

What You Need

For writing and running programs:

  • A working computer (even a minimal 8-bit homebrew system with basic computing capabilities)
  • A way to input programs (toggle switches, keyboard, or paper tape reader)
  • A way to see output (LEDs, 7-segment displays, or a serial terminal)
  • Paper and pencil (essential for hand-assembly and debugging)
  • Reference card for your processor’s instruction set
  • Patience --- early programming is meticulous and unforgiving

If using salvaged hardware:

  • Any 8-bit microprocessor board (Z80, 6502, 8080, or similar)
  • ROM programmer or monitor program in ROM
  • RAM (at least 256 bytes to start, ideally 32-64 KB)
  • Serial terminal or display for output

What a Processor Understands

A processor is a machine built from the logic gates described in Boolean Logic and Gates. It executes a fixed set of operations, each represented by a specific binary number. This set of operations is the instruction set architecture (ISA).

The Fetch-Execute Cycle

Every processor, no matter how simple or complex, follows the same loop endlessly:

  1. Fetch: Read the instruction at the address pointed to by the Program Counter (PC)
  2. Decode: Determine what operation the instruction specifies
  3. Execute: Perform the operation (add numbers, move data, jump to a new address)
  4. Advance: Increment the PC to point to the next instruction (unless the instruction was a jump)
  5. Repeat

This cycle runs millions of times per second in modern processors, but even a 1 MHz 8-bit processor executes roughly 500,000 instructions per second.

Instruction Format

Each instruction consists of an opcode (operation code) and zero or more operands (data or addresses the operation acts on).

For a simple 8-bit processor:

Byte 1: Opcode (what to do)
Byte 2: Operand (what to do it with/to)

Example:
  00000001 00001010  =  LOAD 10  (load the value 10 into the accumulator)
  00000010 00000011  =  ADD  3   (add 3 to the accumulator)
  00000011 00100000  =  STORE 32 (store the accumulator to memory address 32)

Registers

Registers are tiny, fast storage locations inside the processor. A minimal processor has:

RegisterPurpose
Accumulator (A)Holds the current working value, used in arithmetic
Program Counter (PC)Points to the next instruction to execute
Stack Pointer (SP)Points to the top of the stack in memory
Flags/StatusIndividual bits indicating: zero result, carry, negative, overflow
General purpose (B, C, etc.)Additional working storage

Operations typically work between registers and memory: load a value from memory into a register, perform arithmetic on it, store the result back.


Machine Code: The Raw Language

Machine code is the actual binary that the processor executes. Writing machine code means choosing the correct binary number for each instruction and its operands.

A Minimal Instruction Set

Here is a realistic minimal instruction set for an 8-bit processor (similar to early microprocessors):

Opcode (hex)MnemonicOperation
01LDA addrLoad accumulator from memory address
02STA addrStore accumulator to memory address
03ADD addrAdd memory value to accumulator
04SUB addrSubtract memory value from accumulator
05AND addrBitwise AND with memory value
06OR addrBitwise OR with memory value
07NOTInvert all bits in accumulator
08JMP addrJump to address (unconditional)
09JZ addrJump to address if accumulator is zero
0AJNZ addrJump to address if accumulator is not zero
0BIN portRead input port into accumulator
0COUT portWrite accumulator to output port
0DCALL addrPush PC to stack, jump to subroutine
0ERETPop PC from stack, return from subroutine
0FHLTHalt processor

This is only 15 instructions, but it is Turing-complete --- capable of computing anything any computer can compute, given enough time and memory.

Your First Machine Code Program

Task: Add two numbers (5 and 3) and output the result.

Assume numbers are stored at addresses 20 and 21, and the output port is 0.

Address  Hex Code  Assembly       Comment
00       01 20     LDA 20         Load first number (5) into accumulator
02       03 21     ADD 21         Add second number (3)
04       0C 00     OUT 0          Output result (8) to display
06       0F        HLT            Stop

Data:
20       05                       First number = 5
21       03                       Second number = 3

To enter this on a minimal computer with toggle switches:

  1. Set the address switches to 00, set the data switches to 01, press DEPOSIT
  2. Set address to 01, data to 20, press DEPOSIT
  3. Set address to 02, data to 03, press DEPOSIT
  4. Continue for all bytes…
  5. Set address to 00, press RUN

The output display should show 8 (00001000 in binary).

Tip

Write every program on paper first. List each instruction, its address, its hex code, and a comment explaining what it does. This discipline prevents errors and makes debugging possible. Machine code bugs are extremely difficult to find without documentation.


Assembly Language: Human-Readable Machine Code

Writing raw hex codes is error-prone and tedious. Assembly language replaces numeric opcodes with memorable abbreviations (mnemonics) and numeric addresses with descriptive labels.

From Machine Code to Assembly

The same program in assembly language:

        ORG 00          ; Program starts at address 0

START:  LDA NUM1        ; Load first number
        ADD NUM2        ; Add second number
        OUT 0           ; Display result
        HLT             ; Stop

NUM1:   DB 5            ; First number
NUM2:   DB 3            ; Second number

Improvements over raw machine code:

  • LDA NUM1 is clearer than 01 20
  • If you add instructions before NUM1, the label automatically adjusts --- no need to manually recalculate addresses
  • Comments (after ;) document the program’s intent
  • ORG directives tell the assembler where to place the code

The Assembler Program

An assembler is a program that converts assembly language into machine code. It performs two main tasks:

Pass 1 --- Symbol table: Scan through the code, count bytes, and record the address of every label.

Pass 2 --- Code generation: Go through again, replacing each mnemonic with its opcode and each label with its resolved address.

For your first system, you will likely hand-assemble: manually look up each opcode and calculate each address. This is tedious but educational. Once you have a working computer with enough memory, writing an assembler (in assembly language) is one of the first programs to create.

Addressing Modes

Most processors support several ways to specify operands:

ModeExampleMeaning
ImmediateLDA #5Load the literal value 5
DirectLDA 20Load from memory address 20
IndirectLDA (20)Address 20 contains the actual address; load from there
RegisterMOV A,BCopy register B to register A
IndexedLDA 20,XLoad from address (20 + register X)

Indexed addressing is essential for working with arrays: set X to 0, then increment X to step through sequential memory locations.


Control Flow: Making Decisions and Repeating

Conditional Jumps

The processor’s flags register is updated after every arithmetic or logic operation:

FlagSet When
Zero (Z)Result is zero
Carry (C)Result overflowed the register width
Negative (N)Result has its highest bit set (negative in two’s complement)

Conditional jump instructions test these flags:

        LDA SCORE       ; Load score
        SUB THRESHOLD    ; Subtract threshold
        JZ  EQUAL        ; Jump if score == threshold
        JNZ NOTEQUAL     ; Jump if score != threshold
        ; Use carry flag for less-than/greater-than

Loops

A loop repeats a block of code a specific number of times or until a condition is met.

Counting loop (print numbers 1 to 10):

        LDA #1          ; Start with 1
LOOP:   OUT 0           ; Output current number
        ADD #1          ; Increment
        SUB #11         ; Compare with 11 (subtract 11)
        JZ  DONE        ; If result is 0, we reached 11, stop
        ADD #11         ; Restore the value (undo the subtract)
        JMP LOOP        ; Repeat
DONE:   HLT

Tip

The pattern of “subtract and check zero flag” is how comparison works on simple processors. To compare A with B: compute A - B. If the result is zero, they are equal. If the carry flag is set, A was less than B. If neither, A was greater than B.

Subroutines

A subroutine is a reusable block of code that can be called from multiple places in a program. The CALL instruction pushes the return address onto the stack, and RET pops it back to resume execution.

        LDA #5
        CALL DOUBLE     ; Call subroutine, A = 5
        OUT 0           ; Output: 10
        LDA #7
        CALL DOUBLE     ; Call again, A = 7
        OUT 0           ; Output: 14
        HLT

DOUBLE: ADD A           ; A = A + A (double it)
        RET             ; Return to caller

The Stack

The stack is a region of memory that grows and shrinks as data is pushed and popped. It operates in LIFO (Last In, First Out) order.

Uses of the stack:

  • Return addresses: CALL pushes the return address, RET pops it
  • Local variables: Subroutines push temporary values, pop them when done
  • Saving registers: Before a subroutine modifies registers, push their values; restore them before returning
Stack grows downward in memory:

Before CALL:     After CALL:      After RET:
SP -> [empty]    [empty]          SP -> [empty]
                 SP -> [ret addr]

Higher-Level Languages

Assembly language is powerful but tedious. Higher-level languages express ideas more naturally and let one line of source code generate many machine instructions.

Interpreters vs Compilers

Interpreter: Reads source code one line at a time, translates it, and executes it immediately. Slow but simple. No separate compilation step.

Compiler: Reads the entire source program, translates it all into machine code, and produces an executable file. Faster execution but more complex to build.

Forth: The Bootstrap Language

Forth is uniquely suited to a post-collapse computing rebuild because:

  • The entire language and compiler fit in 2-4 KB of memory
  • It is self-extending: you define new words (commands) in terms of existing ones
  • It runs on the smallest processors with minimal memory
  • It can be written in assembly language in a few hundred lines

Basic Forth concepts:

\ This is a comment
5 3 +           \ Push 5, push 3, add them -> result 8 on stack
.                \ Print top of stack -> displays 8

: DOUBLE  2 * ;  \ Define a new word: DOUBLE multiplies by 2
7 DOUBLE .       \ Push 7, double it, print -> displays 14

: SQUARE  DUP * ;  \ DUP duplicates top of stack
5 SQUARE .       \ 5 * 5 = 25

Forth uses postfix notation (also called Reverse Polish Notation): operands come before the operator. This maps directly to stack operations and requires no parentheses or operator precedence rules.

BASIC: The Accessible Language

BASIC (Beginner’s All-purpose Symbolic Instruction Code) is easier to read and learn:

10 LET A = 5
20 LET B = 3
30 LET C = A + B
40 PRINT C
50 END

A minimal BASIC interpreter requires 4-8 KB of memory. It processes one line at a time, making it slow but interactive: you type a command and see the result immediately.

Tip

For a bootstrap sequence: first write an assembler in machine code. Then write a Forth interpreter in assembly. Then use Forth to write more sophisticated tools. Finally, write a BASIC interpreter in Forth or assembly for general-purpose use by non-programmers. Each layer makes the next easier to build.


Data Structures

Arrays

An array is a contiguous block of memory locations holding elements of the same type. The address of any element can be calculated: base_address + (index * element_size).

; Array of 5 bytes starting at address 100
; Values: 10, 20, 30, 40, 50

        LDA #2          ; Index 2
        ADD #100        ; Base address
        ; Accumulator now holds 102
        LDA (A)         ; Load indirectly: gets value 30

Character Strings

Text is stored as sequences of numeric character codes. The most common encoding is ASCII (American Standard Code for Information Interchange):

CharacterASCII Code (decimal)ASCII Code (hex)
‘0’4830
’9’5739
’A’6541
’Z’905A
’a’9761
space3220

Strings are typically terminated with a zero byte (null terminator) so the program knows where the string ends.

Linked Lists

When you need a collection that can grow and shrink dynamically, use a linked list. Each element contains the data plus a pointer (address) to the next element.

Address 100: [Data: 42] [Next: 110]
Address 110: [Data: 17] [Next: 130]
Address 130: [Data: 99] [Next: 0]   (0 = end of list)

Linked lists use more memory (extra pointer per element) but allow insertion and deletion without moving other elements.


Debugging

Common Bug Types

Bug TypeSymptomExample
Off-by-oneLoop runs one too many or too few timesLoop counter starts at 1 instead of 0
OverflowArithmetic result exceeds register capacity200 + 100 = 44 (on 8-bit: 300 mod 256 = 44)
Uninitialized variableUnpredictable behavior, different each runReading memory that was never written
Wrong addressing modeLoads address instead of value, or vice versaLDA 5 (load from addr 5) vs LDA #5 (load value 5)
Stack imbalanceSubroutine returns to wrong address, crashesPUSH without matching POP, or vice versa
Infinite loopProgram hangs, never completesMissing or wrong exit condition

Debugging Techniques

Single-stepping: If your system has a single-step mode, execute one instruction at a time and check register values after each step. This is slow but infallible.

Breakpoints: Insert a HLT instruction at a suspect location. When the program stops, examine registers and memory. Replace HLT with the original instruction when done.

Memory dump: Display a range of memory addresses and their contents. Compare with your expected values.

Trace output: Insert OUT instructions at key points to display intermediate values. Remove them once the bug is found.

Tip

The best debugging technique is prevention: write small, test immediately, then add more. Never write 100 lines and then try to test. Write 5 lines, verify they work, then write 5 more. In machine code and assembly, every untested line is a potential bug that becomes exponentially harder to find as the program grows.


Common Mistakes

MistakeWhy It’s DangerousWhat to Do Instead
Not documenting machine codeImpossible to understand or modify laterComment every instruction, keep a paper listing
Wrong byte order for multi-byte valuesAddresses and 16-bit numbers come out garbledKnow your processor’s endianness (big vs little)
Forgetting to save registers in subroutinesCalling code’s data is destroyedPUSH all modified registers at subroutine entry, POP before RET
Stack overflowStack grows into program code, corrupts everythingMonitor stack pointer, allocate generous stack space at top of memory
Branch target off by oneJump lands one byte into the middle of a 2-byte instructionDouble-check all branch target addresses after any code changes
Confusing signed and unsigned arithmeticNegative numbers misinterpreted as large positivesBe consistent: choose signed or unsigned and use appropriate comparison flags
No input validationBad data causes crashes or wrong resultsCheck inputs before processing: range limits, null pointers, buffer sizes
Writing self-modifying codeNearly impossible to debug, unpredictable on cached processorsAvoid except in extreme memory-constrained situations

What’s Next

With programming, you can make your computer do useful work. The next frontier is connecting computers together:

  • Internet Infrastructure --- networked computers sharing data and resources across distances, multiplying the value of each individual machine

Quick Reference Card

Programming Fundamentals --- At a Glance

Fetch-Execute cycle: Fetch instruction, decode, execute, advance PC, repeat

Instruction format: Opcode (what to do) + operand (what to do it with)

Key registers: Accumulator (working value), Program Counter (next instruction), Stack Pointer (top of stack), Flags (zero, carry, negative)

Assembly language: Mnemonics (LDA, ADD, JMP) replace numeric opcodes; labels replace addresses

Addressing modes: Immediate (#5 = value), Direct (20 = address), Indirect ((20) = pointer), Indexed (20,X = base+offset)

Comparison: Subtract and check flags. Zero = equal, Carry = less than.

Subroutines: CALL pushes return address to stack, RET pops it

Bootstrap path: Machine code Assembler Forth BASIC higher languages

Forth: Fits in 2-4 KB, stack-based, self-extending, ideal first high-level language

ASCII: ‘A’=65, ‘a’=97, ‘0’=48, space=32, strings terminated with 0

Debug rule: Write 5 lines, test, then write 5 more. Never write 100 untested lines.