Capstone — Writing a Complete LC-3 Program
Assembly Fundamentals
Chapter 10 · Capstone — Writing a Complete LC-3 Program
Nine chapters have each built one working piece of LC-3 — the fetch-decode-execute cycle, addressing modes, registers and condition codes, the instruction encodings, branching, subroutines and the stack, TRAP-based I/O, and finally the assembler mechanics that turn all of it into bits. This capstone combines every one of those pieces into a single, real, working program: a vowel counter that reads a short line of typed lowercase letters, counts how many are vowels, and prints the result.
The Program
Reads up to 8 characters from the keyboard (stopping early if Enter is pressed), echoing each one as it's typed. Once input stops, it walks back through what was typed, uses a subroutine to test each character, and prints the final vowel count as a single digit.
.ORIG x3000 ; === Vowel Counter === AND R3, R3, #0 ; R3 = number of characters read so far = 0 LEA R4, BUFFER ; R4 = pointer to the next free buffer slot READLOOP TRAP x20 ; GETC — read one character into R0 (no echo) ADD R1, R0, #-13 ; is it Enter (ASCII 13)? BRz READDONE ; if so, stop reading TRAP x21 ; OUT — echo the character back to the screen STR R0, R4, #0 ; store the character into the buffer ADD R4, R4, #1 ; advance the buffer pointer ADD R3, R3, #1 ; count++ ADD R1, R3, #-8 ; is the buffer full (8 characters)? BRz READDONE ; if so, stop reading BRnzp READLOOP READDONE AND R2, R2, #0 ; R2 = vowel count = 0 LEA R4, BUFFER ; R4 = pointer, reset to the start of the buffer ADD R3, R3, #0 ; refresh condition codes on R3 (assembly1-4 discipline) BRz PRINTRESULT ; nothing was typed at all — skip straight to printing 0 COUNTLOOP LDR R0, R4, #0 ; load the next character from the buffer JSR ISVOWEL ; R0 in, R0 out: 1 if vowel, else 0 ADD R2, R2, R0 ; vowel count += result ADD R4, R4, #1 ; advance the pointer ADD R3, R3, #-1 ; remaining-- BRp COUNTLOOP ; loop while characters remain PRINTRESULT LEA R0, MSG TRAP x22 ; PUTS — "Vowels found: " LD R1, ASCII0 ; '0' won't fit as a 5-bit immediate (assembly1-5) — load it from memory ADD R0, R2, R1 ; R0 = vowel count + '0' = the correct ASCII digit TRAP x21 ; OUT — print the digit AND R0, R0, #0 ADD R0, R0, #10 ; x0A newline — 10 DOES fit as a 5-bit immediate, no memory constant needed TRAP x21 ; OUT — print the newline TRAP x25 ; HALT ; --- Subroutine: ISVOWEL --- ; Input: R0 = a character. Output: R0 = 1 if a lowercase vowel, else 0. Clobbers R1. ; Uses assembly1-5's own NOT-then-add-1 idiom to negate a value, since LC-3 ; has no direct "compare register to register" instruction — only ADD. ISVOWEL LD R1, VOWEL_A NOT R1, R1 ADD R1, R1, #1 ; R1 = -'a' (two's complement negation) ADD R1, R0, R1 ; R1 = R0 - 'a' BRz ISVOWEL_YES LD R1, VOWEL_E NOT R1, R1 ADD R1, R1, #1 ADD R1, R0, R1 BRz ISVOWEL_YES LD R1, VOWEL_I NOT R1, R1 ADD R1, R1, #1 ADD R1, R0, R1 BRz ISVOWEL_YES LD R1, VOWEL_O NOT R1, R1 ADD R1, R1, #1 ADD R1, R0, R1 BRz ISVOWEL_YES LD R1, VOWEL_U NOT R1, R1 ADD R1, R1, #1 ADD R1, R0, R1 BRz ISVOWEL_YES AND R0, R0, #0 ; not a vowel RET ISVOWEL_YES AND R0, R0, #0 ADD R0, R0, #1 RET BUFFER .BLKW #8 MSG .STRINGZ "Vowels found: " ASCII0 .FILL x0030 VOWEL_A .FILL x0061 VOWEL_E .FILL x0065 VOWEL_I .FILL x0069 VOWEL_O .FILL x006F VOWEL_U .FILL x0075 .END
assembly1-7 established push-before-call, pop-before-return as the rule for safe nesting. ISVOWEL never itself executes a JSR or TRAP, so nothing inside it can overwrite the return address the calling JSR ISVOWEL placed in R7 — its own RET is safe to use directly. The save/restore dance from assembly1-7 is only necessary when a subroutine's own body might overwrite R7 before it returns; recognizing when it's not needed is just as important as applying it correctly when it is.
Chapter Attribution
| Program piece | Concept | From |
|---|---|---|
| The whole program's execution | Fetch-decode-execute cycle running every instruction | assembly1-2 |
| STR/LDR into BUFFER, LEA R4 | Base+offset addressing, PC-relative LEA for the buffer's address | assembly1-3 |
| R0–R4 usage, condition-code refresh on R3 | The register file, and reusing/re-establishing N/Z/P deliberately | assembly1-4 |
| ADD/AND/NOT, immediate vs. register operands, ASCII0/VOWEL_* constants | Arithmetic instructions and the 5-bit immediate range limit | assembly1-5 |
| BRz, BRp, BRnzp throughout | Condition-code-driven branching for both loops and the vowel test | assembly1-6 |
| JSR ISVOWEL / RET | Subroutine call and return via R7 | assembly1-7 |
| TRAP x20/x21/x22/x25 | Keyboard input, echoing, string output, and halting | assembly1-8 |
| .ORIG/.END/.BLKW/.STRINGZ/.FILL, labels throughout | Assembler directives and the two-pass symbol resolution behind every label used here | assembly1-9 |
assembly1-8 only previewed, not the interrupt-driven alternative. None of these are bugs — they're honest boundaries of a Fundamentals course, and several of them (multi-digit output, real hardware I/O timing) are exactly the kind of ground cpu8bit1 and the future x86-64 course pick up from here.
Hands-On Exercises
Trace this program's own COUNTLOOP by hand for the input "bee" (3 characters, all read before Enter). Show the value of R2 (vowel count) after each pass through the loop, and the final digit character printed.
📄 View solutionExplain, using this chapter's own tip-box and assembly1-7's calling convention, exactly what WOULD go wrong if ISVOWEL itself called a nested subroutine (say, to log each character) without first saving R7 — trace the effect on the main program's own COUNTLOOP.
📄 View solutionThis chapter's own warn-box names "no case-insensitivity" as an honest limitation. Using assembly1-5's ISVOWEL comparison technique as a model, describe (in words or pseudocode, not full assembly) the minimum change needed to also recognize uppercase vowels A/E/I/O/U — without writing ten separate comparisons.
📄 View solutionChapter 10 Quick Reference — Course Recap
- assembly1-1 — machine code vs. assembly, the assemble-link-run pipeline, why LC-3
- assembly1-2 — CPU components, the six-phase fetch-decode-execute cycle
- assembly1-3 — memory as a word-addressable array, the four addressing modes
- assembly1-4 — R0–R7, the PC, N/Z/P condition codes, load/store architecture
- assembly1-5 — ADD/AND/NOT, register vs. immediate modes, opcode bit encoding
- assembly1-6 — BR and condition-code-driven if/else and loops
- assembly1-7 — JSR/JSRR/RET, why nesting needs a stack, manual PUSH/POP
- assembly1-8 — TRAP, the OS/hardware boundary, GETC/OUT/PUTS/HALT
- assembly1-9 — the two-pass assembler, symbol tables, directives, linking
- assembly1-10 — all of the above, combined into one real, working program
- Next stop:
cpu8bit1(6502/Z80) applies these exact concepts to real, historically quirky hardware