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:
- Fetch: Read the instruction at the address pointed to by the Program Counter (PC)
- Decode: Determine what operation the instruction specifies
- Execute: Perform the operation (add numbers, move data, jump to a new address)
- Advance: Increment the PC to point to the next instruction (unless the instruction was a jump)
- 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:
| Register | Purpose |
|---|---|
| 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/Status | Individual 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) | Mnemonic | Operation |
|---|---|---|
| 01 | LDA addr | Load accumulator from memory address |
| 02 | STA addr | Store accumulator to memory address |
| 03 | ADD addr | Add memory value to accumulator |
| 04 | SUB addr | Subtract memory value from accumulator |
| 05 | AND addr | Bitwise AND with memory value |
| 06 | OR addr | Bitwise OR with memory value |
| 07 | NOT | Invert all bits in accumulator |
| 08 | JMP addr | Jump to address (unconditional) |
| 09 | JZ addr | Jump to address if accumulator is zero |
| 0A | JNZ addr | Jump to address if accumulator is not zero |
| 0B | IN port | Read input port into accumulator |
| 0C | OUT port | Write accumulator to output port |
| 0D | CALL addr | Push PC to stack, jump to subroutine |
| 0E | RET | Pop PC from stack, return from subroutine |
| 0F | HLT | Halt 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:
- Set the address switches to 00, set the data switches to 01, press DEPOSIT
- Set address to 01, data to 20, press DEPOSIT
- Set address to 02, data to 03, press DEPOSIT
- Continue for all bytes…
- 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 NUM1is clearer than01 20- If you add instructions before NUM1, the label automatically adjusts --- no need to manually recalculate addresses
- Comments (after
;) document the program’s intent ORGdirectives 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:
| Mode | Example | Meaning |
|---|---|---|
| Immediate | LDA #5 | Load the literal value 5 |
| Direct | LDA 20 | Load from memory address 20 |
| Indirect | LDA (20) | Address 20 contains the actual address; load from there |
| Register | MOV A,B | Copy register B to register A |
| Indexed | LDA 20,X | Load 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:
| Flag | Set 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):
| Character | ASCII Code (decimal) | ASCII Code (hex) |
|---|---|---|
| ‘0’ | 48 | 30 |
| ’9’ | 57 | 39 |
| ’A’ | 65 | 41 |
| ’Z’ | 90 | 5A |
| ’a’ | 97 | 61 |
| space | 32 | 20 |
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 Type | Symptom | Example |
|---|---|---|
| Off-by-one | Loop runs one too many or too few times | Loop counter starts at 1 instead of 0 |
| Overflow | Arithmetic result exceeds register capacity | 200 + 100 = 44 (on 8-bit: 300 mod 256 = 44) |
| Uninitialized variable | Unpredictable behavior, different each run | Reading memory that was never written |
| Wrong addressing mode | Loads address instead of value, or vice versa | LDA 5 (load from addr 5) vs LDA #5 (load value 5) |
| Stack imbalance | Subroutine returns to wrong address, crashes | PUSH without matching POP, or vice versa |
| Infinite loop | Program hangs, never completes | Missing 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
| Mistake | Why It’s Dangerous | What to Do Instead |
|---|---|---|
| Not documenting machine code | Impossible to understand or modify later | Comment every instruction, keep a paper listing |
| Wrong byte order for multi-byte values | Addresses and 16-bit numbers come out garbled | Know your processor’s endianness (big vs little) |
| Forgetting to save registers in subroutines | Calling code’s data is destroyed | PUSH all modified registers at subroutine entry, POP before RET |
| Stack overflow | Stack grows into program code, corrupts everything | Monitor stack pointer, allocate generous stack space at top of memory |
| Branch target off by one | Jump lands one byte into the middle of a 2-byte instruction | Double-check all branch target addresses after any code changes |
| Confusing signed and unsigned arithmetic | Negative numbers misinterpreted as large positives | Be consistent: choose signed or unsigned and use appropriate comparison flags |
| No input validation | Bad data causes crashes or wrong results | Check inputs before processing: range limits, null pointers, buffer sizes |
| Writing self-modifying code | Nearly impossible to debug, unpredictable on cached processors | Avoid 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.