The LC-3, or Little Computer 3, click for more is far more than a simplified CPU simulation. For thousands of students each year, it is a rite of passage—a gateway to understanding how a computer truly operates beneath layers of high-level abstraction. Originally introduced in the textbook Introduction to Computing Systems: From Bits and Gates to C and Beyond by Yale Patt and Sanjay Patel, the LC-3 provides a complete, self-contained educational architecture that distills the essence of von Neumann machines into a learnable form. This article serves as a deep-dive guide to the LC-3 architecture, offering the help you need to master its instruction set, programming model, and the foundational concepts that will stick with you long after the course ends.
At its heart, the LC-3 is a 16-bit computer. Every instruction, every memory address, and every data word is exactly 16 bits wide. This uniform word size eliminates a lot of the messy edge cases found in real-world 8/32/64-bit hybrids, letting you focus on the mechanics of the machine. The LC-3’s address space spans 65,536 memory locations (from x0000 to xFFFF), but in practice only a portion is available for user programs. The architecture cleanly separates the hardware into a processing unit, a control unit, memory, and input/output, giving you a genuine feel for the fetch-decode-execute cycle that all modern processors follow.
The register set is intentionally minimal. There are eight general-purpose registers named R0 through R7. Each can hold one 16-bit value. Having just eight registers forces you to manage variable storage carefully, but it also makes assembly programming wonderfully transparent. Beyond the general-purpose registers, there are a handful of special-purpose registers essential for execution. The Program Counter (PC) holds the address of the next instruction to be fetched. The Instruction Register (IR) stores the current instruction. The Processor Status Register (PSR) contains privilege level information and, critically, three condition codes—N (negative), Z (zero), and P (positive)—that are set every time a value is written to a general-purpose register. These condition codes drive the branching logic that gives programs their decision-making ability.
One of the most elegant aspects of the LC-3 is its lean instruction set architecture. The machine recognizes only 16 opcodes, each represented by a 4-bit field at the top of the instruction word. The remaining 12 bits specify operands and addressing modes. This regular format makes it possible to manually decode instructions quickly, a skill that rapidly builds your intuition about machine code. The instructions group into four categories: operate, data movement, control, and service calls.
The operate instructions are ADD, AND, and NOT. ADD and AND each come in two flavors. In register mode, both source operands are registers; in immediate mode, the second source operand is a 5-bit sign-extended constant. For example, ADD R0, R1, #5 adds 5 to the contents of R1 and stores the result in R0, while also setting the condition codes based on the result. The NOT instruction performs a bitwise complement. From these three logical building blocks, you can synthesize subtraction, multiplication, and all other arithmetic, underscoring the universal power of a tiny instruction set.
Data movement is where the LC-3 shines as a teaching tool because it introduces three distinct addressing modes. LD (load) and ST (store) use PC-relative addressing. An instruction like LD R2, VALUE assembles to a 9-bit offset that is added to the incremented PC, allowing you to reach data stored near the instruction. LDR (load register) and STR (store register) use base-plus-offset addressing: LDR R3, R4, #2 loads a word from the memory address formed by adding 2 to the contents of R4. This is perfect for array access and pointer manipulation. The third mode, indirect, is implemented by LDI and STI. These instructions first load a pointer from a PC-relative address and then access the target data. Indirect addressing, while potentially confusing at first, teaches the concept of pointer dereferencing that maps directly onto C’s pointer semantics. A final data-movement instruction, LEA (load effective address), computes a PC-relative address and stores that address—not the memory content—into a register, giving you the ability to build pointers to labels.
Control flow in LC-3 revolves around the BR (branch) instruction and the JMP/JSR pairs. The BR opcode uses a 9-bit PC-relative offset, but the branch is only taken if the condition codes specified in the instruction’s three status bits match the current PSR values. You can branch on negative, zero, positive, or any combination. For example, her response BRz DONE will jump to DONE if the last written register value was zero. To create unconditional branches, you simply test all conditions: BRnzp LOOP. The JMP instruction jumps to an address contained in a register, enabling computed gotos and return from subroutines. Subroutine calls use JSR and JSRR. JSR employs an 11-bit PC-relative offset, while JSRR jumps to a register-held address. Both automatically save the return address in R7, a convention that links back to the RET instruction (which is simply a JMP to R7). This linkage mechanism beautifully demonstrates the stack-less calling convention that underlies assembly-level function calls before you introduce a stack pointer.
No real computer can function without input and output, and the LC-3 provides a brilliantly straightforward mechanism: TRAP routines. The TRAP instruction vectors to a fixed set of service routines stored in the operating system’s portion of memory. The 8-bit trap vector identifies which routine to call. Common traps include TRAP x20 (GETC, read a character from the keyboard), TRAP x21 (OUT, write a character to the console), TRAP x22 (PUTS, output a null-terminated string), and TRAP x25 (HALT, stop the processor). Memory-mapped I/O lies beneath these traps. The keyboard status register (KBSR) at address xFE00 and keyboard data register (KBDR) at xFE02, along with the display status register (DSR) at xFE04 and display data register (DDR) at xFE06, are polled or used with interrupts in more advanced exercises. Understanding the relationship between memory-mapped hardware and TRAP calls gives you a direct line of sight into how operating systems abstract hardware.
When you sit down to write LC-3 assembly, the workflow closely mirrors professional low-level development. You create a source file with labels, pseudo-ops like .ORIG (to set the program’s starting address) and .FILL (to allocate and initialize a word of data), and .STRINGZ (for null-terminated strings), then run it through an assembler that produces a text-based machine code image. The simulator—LC-3 Edit, PennSim, or web-based variants—loads this image into memory, and you can step through execution one cycle at a time, inspect registers and memory, set breakpoints, and watch the condition codes dance. This visibility transforms abstract concepts into concrete, tactile experience. You quickly learn why a missing RET causes the PC to sail into unknown memory, or why forgetting to add a null terminator to a string causes PUTS to spew garbage across the screen.
As you work through exercises—multiplication by repeated addition, Fibonacci sequences, keyboard echo routines—you inevitably bump up against a handful of common stumbling blocks. The limited range of immediate fields (5 bits for ADD/AND, 6 bits for STR/LDR offset) forces you to use multiple instructions to load larger constants. The lack of a hardware stack means recursion and deep subroutine nesting must be manually implemented by saving R7 and any caller-saved registers to a software stack that you build in memory. The PC-relative offsets are always computed from the incremented PC (the address of the next instruction plus the offset), so calculating the offset when hand-assembling requires careful counting. Labels in an assembler hide this arithmetic, but knowing what happens underneath is invaluable. Another point of confusion is the difference between LEA and LD: LEA gives you a pointer to a label, while LD gives you the contents stored at that label. Mixing these up leads to data corruption that can be surprisingly illuminating to debug.
The educational value of the LC-3 extends far beyond the ability to write a few trivial assembly programs. By working with a register-memory architecture that is pure von Neumann—instructions and data share the same memory—you internalize the stored-program concept at a visceral level. You see how a bit pattern can simultaneously be an instruction, a memory address, an integer, or an ASCII character, and that meaning is imposed solely by context. The fetch-decode-evaluate-address-fetch-operands-execute-store-result cycle becomes a mental model that makes pipelining, cache behavior, and out-of-order execution far easier to grasp in later architecture courses. The LC-3’s interrupt mechanism (covered in advanced chapters) parallels real-life I/O handling and paves the way for understanding kernel-application privilege separation.
For anyone struggling to get LC-3 programs to work, the best help is to slow down and simulate the machine in your own mind. Draw a map of memory. Track the value of each register after every instruction on paper. Write down what each trap expects—GETC stores the input character in R0, OUT expects the character to be displayed in R0, and PUTS expects R0 to hold the starting address of a string. Using comments liberally is not optional; it is the only way to keep your intentions clear when the assembler has no type system. Common idioms like clearing a register with AND R0, R0, #0 or copying one register to another with ADD R1, R2, #0 become second nature and reinforce the understanding that there are many ways to achieve the same result in a reduced instruction set.
Ultimately, the LC-3 succeeds as an educational architecture because it is complete enough to teach all the big ideas—data paths, addressing modes, subroutines, traps, memory-mapped I/O—yet simple enough that a student can fully comprehend the whole machine in a matter of weeks. There are no x86 segmentation quirks, no ARM Thumb mode interleaving, no paging structures to obscure the core execution model. When you finish your LC-3 journey, you will not just be able to write assembly; you will understand how a computer fetches, decodes, and executes, and that knowledge is a permanent part of your engineering toolkit. Whether you are debugging an off-by-one offset, puzzling out why your branch never triggers, or celebrating the moment your first ASCII character appears on the simulated console, the LC-3 gives you the foundational empathy with the machine that separates competent programmers from truly great ones. So fire up the simulator, load a small program, step through the cycles, Check This Out and let the Little Computer 3 reveal its secrets one 16-bit word at a time.