Tiny Binary Computer Programming Patterns
As of Summer, 2026, this page is a work in progress.
Programming in assembly language is different from programming in a high-level language like Python. Something as simple as displaying "Hello World!" on the screen can take over a dozen assembler statements and a hundred or so instruction times. This page explains how to make TBC's assembly language do many of those things that are built in to high-level languages.
Clicking the copy icon
in a code block will copy that code block to the clipboard. You can then paste it into the assembler source pane or into a program file.
Table of Contents
- Input and Output
- Comparison
- Arrays, Strings, and Looping
- Subroutines
- Utilities
Shortcut I/O for Numbers and Strings
The IN and OUTx instructions provide shortcuts for output. Using the shortcuts will let you focus on other parts of assembly programming without dealing with the details of input and output. To learn one way that real computers handle input and output, see the PIO instruction.
The IN instruction reads a number and places it in the accumulator. Numbers are limited to −2048 to +2047. Any other input causes a machine check.
The OUT instruction displays the contents of the accumulator as an integer followed by a new line. The OUTN instruction is identical but does not follow the number with a new line. The OUTC instruction displays the contents of the accumulator as a character. The accumulator must be between 0 and 0xFF. The character displayed is the corresponding character from the first 256 code points of Unicode. To move to a new line, display 0x0a.
The OUTS instruction displays all characters of a null-terminated string. In the case of OUTC, the accumulator must contain the address of the string. The string ends when a 0, or null, is encountered. If a new line is wanted, include 0x0a before the terminating null.
- ldi hello // get address of string
- outs // output string
- hlt // Done!
- hello dat "Hello World from OUTS",0x0a,0
- Line 1 is a Load Immediate instruction. The address of string hello is placed in the instruction when the code is assembled, so a second memory reference is not needed at runtime.
- Line 2 uses OUTS to output the string to the display.
- The HLT instruction on the third line stops the program.
- The last line defines the label "hello" to be the address of the string to be displayed. The 0x0a is the new line character, and the final 0 ends the string. The 0 is not displayed by OUTS.
Use PIO to Display a Character
- // Output using programmed I/O.
- // This program displays "A" on the screen, then halts.
- // For more than one character, see "Loop through a string"
- ldi 0 // Output is device 0
- sta ioDev // Store 0 in device register
- lda A // Get data for output
- sta ioData // Store it in data register
- pio // Start the I/O operation
- loop lda ioStat // Read I/O status
- sub one // Check for 1
- brz done // If one, I/O is complete
- br loop // Otherwise, keep checking
- done hlt // Halt
- ioData equ 0xff // EQU lets us give a name to a value
- ioDev equ 0xfd
- ioStat equ 0xfe
- A dat "A" // Data to display
- one dat 1 // Constant 1
Use PIO to Read Keystrokes
- // Input using programmed I/O.
- // This program displays reads characters and stores them
- // in memory until "enter" is pressed. The "enter" is not stored.
- // A null (0x0) is added at the end of the buffer.
- // This also shows how to use "store indirect accumulator."
- ldi buffer // Gets ADDRESS of "buffer"
- sta ptr // Store buffer address in PTR
- ldi 1 // Input is device 1
- sta ioDev // Store 1 in device register
- in pio // Start the I/O operation
- loop lda ioStat // Read I/O status
- sub one // Check for 1
- brz store // If one, I/O is complete
- br loop // Otherwise, keep checking
- store lda ioReg // get character
- sub enter // was it enter key?
- brz finish
- lda ioReg // Not enter, get character back
- sia ptr // Store in buffer pointed by ptr
- lda ptr // get the pointer
- inc // point to next location
- sta ptr
- br in // Get next
- finish ldi 0 // Null for end of string
- sia ptr // Add to end of buffer
- hlt // Halt
- ioReg equ 0xff // EQU lets us give a name to a hex address
- ioDev equ 0xfd
- ioStat equ 0xfe
- enter dat 0x0a // Code for enter key
- one dat 1
- ptr dat // pointer for store indirect
- buffer dat // data will go here
- // Anything below here could get overwritten by the input!
Using PIO to Read Numbers
- ldi 0
- sta number // initialize number
- ldi 1 // Input is device 1
- sta ioDev // Store 1 in device register
- in pio // Start the I/O operation
- loop lda ioStat // Read I/O status
- sub one // Check for 1
- brz save // If one, I/O is complete
- br loop // Otherwise, keep checking
- save lda ioReg // get character
- sub enter // was it enter key?
- brz finish // Yes. Done
- lda number
- push
- call mult10 // shift result by multiplying by 10
- pop
- sta number
- lda ioReg // Not enter, get character back
- sub offset // convert symbol to number
- // error checking here
- add number // add current digit to number
- sta number // and save the sum
- br in // Get next
- finish ldi msg
- outs
- lda number
- out
- hlt // Halt
- ioReg equ 0xff // EQU lets us give a name to a hex address
- ioDev equ 0xfd
- ioStat equ 0xfe
- enter dat 0x0a // Code for enter key
- offset dat 0x30
- one dat 1 // Constant 1
- number dat
- msg dat "The number entered was ",0
Comparison Operations with TBC
Programming in a high-level language like Python, it is common to write something like if (a > b) followed by a block of code to be executed if the expression in parentheses evaluates to true. The assembly/machine languages for many computer families have comparison instructions that more or less directly correspond to the high-level language statements. The very simplest machines, like TBC, do not. Instead, comparison is accomplished by subtracting the comparands and evaluating the result with a conditional branch.
In this section, we present three patterns, comparison for equality, greater than or equal, and strictly greater than. "Less than" comparisons are accomplished by reversing the order of the comparands. Note that a < b is the same as b > a.
Comparison for Equality
In the example below, all the work happens in lines 2 – 4. In line 2, the first comparand is loaded into the accumulator. In line 3, the second comparand is subtracted from the accumulator, and in line 4, a branch is taken if the result was zero, that is, if the two comparands were equal. The rest of the example is scaffolding to produce something that will assemble and run.
After the subtract instruction, the value of the accumulator is no longer equal to the variable a, however the stored values of a and b are unchanged.
- // if A = B
- lda a // First comparand
- sub b // subtract second comparand
- brz equal // branch if equal
- ldi neqmsg // report not equal
- outs
- hlt
- equal ldi eqmsg // report equal
- outs
- hlt
- a dat 60
- b dat 50
- neqmsg dat "Not equal",0x0a,0
- eqmsg dat "Equal",0x0a,0
Greater than or Equal To
- // if A >= B
- lda a // Load first comparand
- sub b // Subtract second comparand
- bnn gteq // Branch if A >= B
- ldi bmsg
- outs
- hlt
- gteq ldi amsg
- outs
- hlt
- a dat 7
- b dat 6
- amsg dat "A >= B",0x0a,0
- bmsg dat "A < B",0x0a,0
Strictly Greater Than
- // if A > B
- lda a // Load first comparand
- sub b // Subtract second comparand
- brz not // Equal, so not strictly >
- bnn gteq // Branch if A >= B
- not ldi bmsg
- outs
- hlt
- gteq ldi amsg
- outs
- hlt
- a dat 9
- b dat 8
- amsg dat "A > B",0x0a,0
- bmsg dat "A !> B",0x0a,0
Loop Through a String
In this example we process a string, loading each character of the string into the accumulator and sending it to the screen with the outc command. The same looping technique could be applied to an array of numbers.
Suppose you have a string like this:
msg dat "Hello, World!",0x0a,0
That 0x0a is the new line character. It will move the display to the next line. The 0 marks the end of the string. It assembles to a word of all zeros, also called a null. Here is code to display a string:
- ldi msg // get address of msg
- sta ptr // Save address in "ptr"
- loop lia ptr // value loaded from address in "ptr"
- brz done // null means done
- outc // Output character
- lda ptr // pointer back in Acc
- inc // Increment to next character
- sta ptr // Save it
- br loop // Go around
- done hlt
- ptr dat
- newline equ 0x0a // new line character
- msg dat "Hello, World!",newline,0
- Line 1 is a Load Immediate instruction. The address of variable msg is placed in the instruction when the code is assembled, so a second memory reference is not needed at runtime. If we had used lda instead, that would have loaded the letter "H" into the accumulator.
- Line 2 stores that address in a pointer variable called ptr.
- Line 3 is Load Indirect Accumulator. "Indirect" means to get the operand address from someplace other than the instruction; in this case, the operand address is in the accumulator itself. So, that lia loads the accumulator using the address that was in the accumulator, namely the pointer into the string. After lia completes, the accumulator has a character from msg in it.
- Line 5 sends the character in the accumulator to the screen.
- Lines 6, 7, and 8 get the pointer back, increment it, and save it.
- The brz at line 4 says that if a zero value is loaded, the end of the string has been reached. That's why the message at line 14 ends with a zero.
- Line 13 defines newline as 0x0a, the code for a new line.
- The last line defines the message, followed by a newline character and a zero.
Simple Subroutine Calls
Subroutines provide a way to have only one copy of code that would be needed at two or more places. Instead of duplicating the code, the program calls the subroutine each time it is needed. When complete, the subroutine returns control to the instruction immediately after the call. The TBC instructions to do this are call and ret The program below just prints 1-2-3-4-5, but it uses subroutines to print the 2, 3 and 4 to show how subroutine calls work.
- ldi 1 // a number to display
- out // display it
- call two // subroutine call
- ldi 5
- out
- hlt
- two ldi 2
- out
- call three // a call within a call
- ldi 4 // "two" continues here
- out
- ret // returns to main program
- three ldi 3
- out
- ret // returns to "two"
- Lines 1 and 2 use Load Immediate (ldi) and out to get a one into the accumulator and display it.
- Line 3 is the first subroutine call; it calls the subroutine called two. When two completes, it will return control to the main program at the next instruction, namely the Load Immediate on line 4.
- We skip ahead to line 8, the first line of subroutine two. It loads a 2 into the accumulator and displays it.
- Skip to line 15 and subprogram three. It gets and displays a 3, then executes a ret (return) instruction. Control will pass back to subroutine two at line 11, the line right after the call instruction.
- Subroutine two does more work, namely displaying a 4, then executes its own ret instruction, which returns to the main program at line 4, the line right after the call to two.
- The main program displays that last digit, 5, and halts.
Subroutines have the same problem Hansel and Gretel had; getting there is easy because the subroutine name comes right after the call. The problem is how to get back. The call and ret instructions use a stack that grows downward from address 0x07e to keep track of how to get back. A call puts the proper return address on the stack and a ret gets the address at the "top" of the stack and loads it into the program counter. Since the program counter holds the address of the next instruction, execution continues at that address.
Click the "copy" icon to copy this snippet, paste it into the assembler pane, assemble and run it. Watch what happens at the high memory addresses when the program runs.
A Subroutine with One Argument
If you have a subroutine that needs one argument that will fit in a 12-bit word, you can pass the argument on the stack.
A Subroutine with Two or More Arguments
The design of TBC makes it impractical to pass more than one argument to a subroutine using the stack. Instead, the calling program builds a parameter list and passed the address of that parameter list using the stack. Subroutines can pass the number of parameters as the first item of the list. Note: It is possible to return more than one result using the stack.
- ldi 5 // Some value for first argument
- sta a1
- ldi 4 // And a value for the second
- sta a2
- ldi a1 // Address of the "list"
- push // Param list address on stack
- call gfam // Multiply the two arguments
- pop
- sta result
- out // Display the result
- hlt
- a1 dat // the parameters must be together
- a2 dat
- result dat
Note: to run this, you must also copy the gfam subroutine and add it to the source pane in the assembler.
- Lines 1 through 4 store two numbers at addresses a1 and a2. These are the two arguments.
- Line 5 loads the address of the argument list into the accumulator and line 6 pushes it onto the stack.
- Line 7 calls the subroutine.
- This subroutine returns only one result on the stack. Lines 8 and 9 pop the result and store it.
- The result is still in the accumulator. Line 10 displays it.
Multiplication
Like most early, small computers, TBC does not have a multiply instruction. Instead, multiplication is performed by repeated addition. That's somewhat slow, but it works. Here is the gfam (go forth and multiply) subroutine. Call it with an argument list of two words. See Call a Subroutine with Two or More Arguments for help with that. The product is returned on the stack. A pop instruction right after the call will get the product into the accumulator.
- gfam sswp temp // Go Forth And Multiply
- pop // address of parameter list
- sta params
- lia params // get first param
- sta mcand // store multiplicand
- sta product // product now multiplicand times 1
- lda params
- inc // increment to next param
- lia params // get second param
- sub one // product already times 1
- sta mplier // store multiplier
- loop lda product
- add mcand
- sta product
- lda mplier
- sub one
- sta mplier
- brz return
- br loop
- return lda product
- push
- sswp temp
- ret
- temp dat 0,0
- params dat // address of caller's argument list
- mplier dat
- mcand dat
- product dat
- one dat 1
- The address of the parameter list was pushed before the call instruction, so the stack items are out of order. The sswp at line 1 exchanges them so that address of the parameter list is the first stack item.
Multiplication by Ten (Special Case)
Multiplication by ten happens frequently with decimal numbers, and
- mult10 sswp temp // Exchange param and return address on stack
- pop // Get number to be multiplied
- brz retzero // Special case: 0 x 10 = 0
- sta temp // Already times 1
- add temp // Now times 2
- add temp // times 3
- add temp // times 4
- add temp // times 5
- sta temp // Save the "times 5" value
- add temp // And add it back. Now times 10
- push // Save the return value
- sswp temp // Fix the stack
- ret // And return to caller
- retzero ldi 0
- push
- sswp temp
- ret // return zero
- temp dat
Sorting
Sorting a list is a big job for an imaginary computer with only 128 words of memory, but it can be done using the bubble sort algorithm. Bubble sort has time complexity of O(n2) which is poor, but it needs only one extra word of storage and the algorithm itself is compact. The example below uses 125 of the 128 words. There's a main program with a list of items to sort, the sort algorithm itself, and a small print subroutine to list the data before and after sorting. The actual sorting begins at line 15 and ends with the ret instruction at line 69.
Lines 18 to 27 just loop through the list, counting the number of items. Lines 28 to 30 set up index variables and limits. The actual sorting happens between lines 31 and 68.
- // Bubble sort
- ldi data // Get the ADDRESS of the list to be sorted
- push // Put address on stack
- call print // print unsorted data
- ldi data // Get address again
- push // On stack
- call sort // Call the sort subroutine
- ldi data // Address of data, not sorted
- push
- call print // Print it again
- hlt // and stop
- data dat 34, 90, 12, 11, 25, 64, 22, 0 // 7 items plus null
- // The null above signals end of list; it isn't part of the data
- sort sswp temp
- pop // address of data
- sta start // save it
- sta ptr // save it again
- getlen lia ptr // Loop through the data, count the items
- brz gotlen // if this was the last one
- lda ptr
- inc
- sta ptr
- ldi 1
- add len
- sta len
- br getlen
- gotlen lda len // we now have the length of the list to sort
- sub one
- sta ilimit // Limit of outer loop
- outer ldi pass // Display a progress message
- outs
- lda i
- out // Pass number in progress message
- inc
- sta i
- sub ilimit // Have we reached limit of outer loop?
- brz endouter
- // This initializes the inner loop
- lda len
- sub i // I has been incremented already
- sta jlimit
- ldi 0
- sta j
- inner lda j
- // This is the actual compare and exchange
- lda start // start of list
- add j
- sta a1 // address of item at J
- add one
- sta a2
- lia a1 // get actual data
- sta temp
- lia a2
- sub temp // compare
- bnn continu // If they do not need to be exchanged
- lia a2 // Otherwise, exchange the two items
- sia a1
- lda temp
- sia a2
- continu lda j
- inc
- sta j
- sub jlimit // Have we reached the inner loop limit?
- brz endinner
- ldi j
- br inner
- endinner br outer // at end of inner loop go back to outer
- endouter ret // The list is now sorted!
- a1 dat
- a2 dat
- i dat 0 // index for outer loop
- ilimit dat // Ending value for outer loop
- j dat 0 // index for inner loop
- jlimit dat // ending value for inner loop
- len dat 0
- one dat 1
- pass dat "Pass ",0
- ptr dat
- start dat
- temp dat 0,0
- print sswp temp // Routine to print list of items before/after
- pop // Address of string
- sta nbr // load acc with actual number
- ploop lia nbr
- brz pdone
- outn // out with No new line
- ldi psep // print the separator
- outs
- lda nbr
- inc
- sta nbr
- br ploop
- pdone ldi 0x0a
- outc
- ret
- psep dat " ",0
- nbr dat