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
ISVOWEL doesn't need to save R7 — and that's worth noticing
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 pieceConceptFrom
The whole program's executionFetch-decode-execute cycle running every instructionassembly1-2
STR/LDR into BUFFER, LEA R4Base+offset addressing, PC-relative LEA for the buffer's addressassembly1-3
R0–R4 usage, condition-code refresh on R3The register file, and reusing/re-establishing N/Z/P deliberatelyassembly1-4
ADD/AND/NOT, immediate vs. register operands, ASCII0/VOWEL_* constantsArithmetic instructions and the 5-bit immediate range limitassembly1-5
BRz, BRp, BRnzp throughoutCondition-code-driven branching for both loops and the vowel testassembly1-6
JSR ISVOWEL / RETSubroutine call and return via R7assembly1-7
TRAP x20/x21/x22/x25Keyboard input, echoing, string output, and haltingassembly1-8
.ORIG/.END/.BLKW/.STRINGZ/.FILL, labels throughoutAssembler directives and the two-pass symbol resolution behind every label used hereassembly1-9
Honest scope note
This program deliberately stays within what this course actually taught, which means real limitations: it only recognizes lowercase vowels (no case-insensitivity logic); a typed line longer than 8 characters is silently cut off rather than erroring or growing the buffer; the vowel count is printed as a single digit because converting a general multi-digit number to decimal text is a genuinely separate technique this course never covered; and keyboard input still relies on the polling 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

Exercise 1

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 solution
Exercise 2

Explain, 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 solution
Exercise 3

This 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 solution

Chapter 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