← AS/A Level

CS · Chapter 28

Cambridge AS & A Level · Part 4 · Chapter 28

Low-level Programming

Turning everyday pseudocode into accumulator instructions, one tiny step at a time. Every table, task and exam question from the chapter is here, with a simulator to run each program, an addressing-mode lab and a queue you can watch move through memory.

Guru Jazzy
Chapter 28 opener: hexadecimal digits, the language of memory addresses

🎯 Learning objective

By the end of this chapter you should be able to:

  • write low-level code that uses the addressing modes immediate, direct, indirect, indexed and relative
Where this fits: Chapter 6 introduced machine code, the assembler and the addressing modes. This chapter is the A Level follow-up: you now have to write programs with them, not just trace them.

📋 28.01 The processor instruction set Table 28.01

Every program in this chapter uses the instruction set below. It belongs to a simple imaginary processor with one general-purpose register, the accumulator (ACC), and an index register (IX).

LabelOp codeOperandExplanation
Data movement
LDM#nImmediate addressing. Load the denary number n into ACC.
LDD<address>Direct addressing. Load the contents of the given address into ACC.
LDI<address>Indirect addressing. The given address holds the address to use; load the contents of that second address into ACC.
LDX<address>Indexed addressing. Form the address from <address> + IX and copy its contents into ACC.
LDR#nImmediate addressing. Load the denary number n into IX.
MOV<register>Move the contents of ACC into the given register (IX).
STO<address>Store the contents of ACC at the given address.
STX<address>Indexed addressing. Form the address from <address> + IX and copy ACC to it.*
STI<address>Indirect addressing. The given address holds the address to use; store ACC at that second address.*
Arithmetic
ADD<address>Add the contents of the given address to ACC.
ADD#nAdd the denary number n to ACC.
SUB<address>Subtract the contents of the given address from ACC.
SUB#nSubtract the denary number n from ACC.
INC<register>Add 1 to the register (ACC or IX).
DEC<register>Subtract 1 from the register (ACC or IX).
Comparison and jumps
JMP<address>Jump to the given address.
CMP<address>Compare ACC with the contents of <address>.
CMP#nCompare ACC with the denary number n.
CMI<address>Indirect addressing. The given address holds the address to use; compare ACC with the contents of that second address.
JPE<address>After a compare, jump to <address> if the compare was True.
JPN<address>After a compare, jump to <address> if the compare was False.
Input and output
INKey in a character and store its ASCII value in ACC.
OUTOutput to the screen the character whose ASCII value is in ACC.
Bit manipulation
AND#nBitwise AND of ACC with the operand.
AND<address>Bitwise AND of ACC with the contents of <address>.
XOR#nBitwise XOR of ACC with the operand.
XOR<address>Bitwise XOR of ACC with the contents of <address>.
OR#nBitwise OR of ACC with the operand.
OR<address>Bitwise OR of ACC with the contents of <address>.
LSL#nShift the bits in ACC logically n places left. Zeros come in at the right-hand end.
LSR#nShift the bits in ACC logically n places right. Zeros come in at the left-hand end.
Other
ENDReturn control to the operating system.
<label>:<opcode><operand>Labels an instruction.
<label>:<data>Gives the symbolic address <label> to the memory location holding <data>.

* STX and STI are not in the 9618 syllabus instruction set. The book uses them to show what becomes possible; in the exam you only get the others.

Notation used in the code

SymbolMeansExample
ACCthe accumulatorINC ACC
IXthe index registerMOV IX
#immediate addressing: the operand is the valueLDM #34
Ba binary numberB01001010
&a hexadecimal number&4A
<address>an absolute address (a number) or a symbolic address (a label)LDD 200 · LDD TOTAL

🎮 Name the instruction

Which op code does the job described? Watch for the difference between a value and an address.

🏷️ 28.02 Symbolic addresses

Label (symbolic address): a name for the memory location it represents. You can treat it like a variable name.

You could write every program with absolute addresses such as LDD 210, but that is very restrictive: add one instruction near the top and every address below it moves, so every operand that refers to them must be rewritten by hand.

Writing LDD TOTAL instead lets you think at a higher level. The assembler works out the absolute address of each label during assembly (the two-pass assembler in Chapter 6, section 6.03).

Absolute

200  LDD 205
201  ADD 206
202  STO 205
203  END
204
205  0
206  7

Symbolic

       LDD TOTAL
       ADD STEP
       STO TOTAL
       END

TOTAL: 0
STEP:  7

Insert an instruction at the top of the absolute version and 205/206 are now wrong. The symbolic version still works: the assembler simply gives TOTAL and STEP new addresses.

🧩 28.03 Problem-solving and assembly-language programs

To solve a problem in low-level code, break it down into steps small enough for the instruction set. A good way in is to think in the same basic constructs you use in a high-level language: assignment, selection, repetition and input/output. The tables below are design patterns: learn the shape and reuse it.

The golden rule: nothing can be processed until it is in the accumulator. Almost every pattern is load → work on ACC → store.

➡️ Assignment Table 28.02

Pseudocode
A ← 34
Assembly
LDM #34
STO A
Why

To store a value in memory it must first be generated in the accumulator.

B ← B + 1
LDD B
INC ACC
STO B

Load the value into ACC, increment it, then store ACC back to the same location.

B ← B + A
LDD B
ADD A
STO B

Load the first value, add the value at the second location, store the result where it is needed.

A ← -A
LDD A
XOR #&FF
INC ACC
STO A

; or
LDM #0
SUB A
STO A

Assuming 8 bits: XOR with 1111 1111 flips every bit (the one's complement); adding 1 gives the two's complement, which is −A. The alternative sets ACC to 0 and subtracts A.

Task 28.01 Write assembly code for: A ← 2, B ← 10, C ← A + B, D ← A − B.
     LDM #2
     STO A       ; A ← 2
     LDM #10
     STO B       ; B ← 10
     LDD A
     ADD B
     STO C       ; C ← 12
     LDD A
     SUB B
     STO D       ; D ← -8
     END
A:   0
B:   0
C:   0
D:   0

Run it in the simulator (Task 28.01). D ends as −8, which in 8-bit two's complement is 1111 1000.

🔀 Selection Table 28.03

Pseudocode
IF A = 0
  THEN
    B ← B + 1
ENDIF
Assembly
        LDD A
        CMP #0
        JPN ENDIF
THEN:   LDD B
        INC ACC
        STO B
ENDIF:  ...
Why

Load the value to test and compare it with the value you need. If the comparison is False (A ≠ 0), jump over the THEN part. If it is True, fall through into the THEN instructions. The labels THEN and ENDIF are only there to make it readable.

IF A = B
  THEN
    OUTPUT "Y"
  ELSE
    OUTPUT "N"
ENDIF
        LDD A
        SUB B
        CMP #0
        JPN ELSE
THEN:   LDM #89
        OUT
        JMP ENDIF
ELSE:   LDM #78
        OUT
ENDIF:  ...

A − B is zero exactly when A = B. If the compare is False, jump to ELSE; if True, the THEN part runs. 89 and 78 are the ASCII codes of Y and N. Note the JMP ENDIF: without it the THEN part would run straight on into the ELSE part.

(LDD A then CMP B would do the same job in one instruction fewer.)

Task 28.02 Write assembly code for: IF A <> 0 THEN B ← A ELSE B ← B − 1 ENDIF
        LDD A
        CMP #0
        JPE ELSE     ; A = 0, so the condition A <> 0 is False
THEN:   STO B        ; ACC still holds A, so B ← A
        JMP ENDIF
ELSE:   LDD B
        DEC ACC
        STO B        ; B ← B - 1
ENDIF:  END
A:      5
B:      9

The test is not equal, so this time the jump to ELSE uses JPE. CMP does not change ACC, so A is still there for STO B. Try A = 5 and A = 0 in the simulator.

🔁 Repetition Table 28.04

Pseudocode
A ← 0
REPEAT
  OUTPUT "*"
  A ← A + 1
UNTIL A = 5
Assembly
        LDM #0
        STO A
LOOP:   LDM #42
        OUT
        LDD A
        INC ACC
        STO A
        CMP #5
        JPN LOOP
Why

Store the counter's start value. Inside the loop, generate the ASCII code of * (42) and output it; load the counter, increment it, save it, and test for the final value. If it has not been reached, jump back to the start of the loop. A REPEAT loop tests at the bottom, so the body always runs at least once.

Task 28.03 Write assembly code for:
Number ← 1
Total ← 0
Max ← 5
REPEAT
  Total ← Total + Number
  Number ← Number + 1
UNTIL Number = Max
        LDM #1
        STO NUMBER
        LDM #0
        STO TOTAL
        LDM #5
        STO MAX
LOOP:   LDD TOTAL
        ADD NUMBER
        STO TOTAL     ; Total ← Total + Number
        LDD NUMBER
        INC ACC
        STO NUMBER    ; Number ← Number + 1
        CMP MAX
        JPN LOOP      ; UNTIL Number = Max
        END
NUMBER: 0
TOTAL:  0
MAX:    0

Total finishes as 1 + 2 + 3 + 4 = 10: when Number reaches 5 the loop stops before adding it. CMP MAX compares with the contents of MAX (direct), not the number 5 itself.

⌨️ Input and output Table 28.05

Pseudocode
INPUT A
Assembly
IN
STO A
Why

Store a character typed at the keyboard at memory location A. ACC holds its ASCII code.

OUTPUT B
; B is a string
        LDM #-1
        MOV IX
LOOP:   INC IX
        LDX B
        OUT
        CMP #13
        JPN LOOP

To output a string of characters stored in consecutive locations starting at B, use a loop and indexed addressing. IX starts at −1, so the first time round INC IX makes it 0 and the character at B itself is loaded. After each OUT, check for the end of the string: the carriage-return character, ASCII 13. If it is not the end, jump back.

To output a number, it must first be converted into its string of digit characters, stored in consecutive locations, and then output this way.

INPUT A
; A is a string
        LDM #-1
        MOV IX
LOOP:   INC IX
        IN
        STX A
        CMP #13
        JPN LOOP

Store a string typed at the keyboard in consecutive locations starting at A. Note: STX is not in the 9618 instruction set, so with the exam set a string cannot be stored like this.

🔎 Look closely: both string loops test for 13 after handling the character, so the carriage return itself is output (or stored) too. That is harmless on a screen and actually useful in memory, because it marks the end of the stored string.
Task 28.04 Write assembly code for:
Count ← 0
REPEAT
  Count ← Count + 1
  INPUT Character
UNTIL Character = "N"
        LDM #0
        STO COUNT
LOOP:   LDD COUNT
        INC ACC
        STO COUNT       ; Count ← Count + 1
        IN
        STO CHARACTER   ; INPUT Character
        CMP #78         ; 78 is the ASCII code of "N"
        JPN LOOP
        END
COUNT:     0
CHARACTER: 0
Task 28.05 Modify your Task 28.04 answer so that it also does OUTPUT Count after the loop.

Replace the final END with:

        LDD COUNT
        ADD #48         ; digit value → ASCII code ("0" is 48)
        OUT
        END

OUT sends a character, so the count must become the ASCII code of its digit. This works for counts 0–9. For a count of 10 or more you would have to split it into tens and units digits and output each one, exactly as the book says about outputting numbers.

🎮 Which code matches?

Fresh pseudocode every time. Pick the assembly code that does exactly what it says. The wrong ones contain the mistakes students really make.

📍 28.04 Absolute and relative addressing

Absolute address: the actual numeric address of a memory location. A program written with absolute addresses cannot be loaded anywhere else in memory.
Relative address: an address relative to a base address, such as the first instruction of the program. Some assemblers produce them so the program can be loaded anywhere.

When the program is loaded, its base address is stored in the base register (BR). Instructions then use the base register's value plus an offset. For example STO [BR] + 10 stores ACC at the address (contents of BR) + 10.

The same program three ways Table 28.06

SymbolicOffset from STARTRelative (BR holds base)Absolute

Change the base address and only the absolute column changes. The relative code is identical wherever it is loaded, which is the whole point.

📕 Book slip: the printed Table 28.06 shows the first absolute address as 100; with the base at 200 it should be 200 (every other row is 201–210).
Always END. At the end of the program, control must go back to the operating system. Otherwise the processor fetches the bit pattern in the next memory location and tries to run it as an instruction. If that data is not a valid instruction, the processor crashes. END marks the end of the program's instructions.

Note: the 9618 instruction set does not include relative (base register) addressing.

🧮 Relative → absolute

👉 28.05 Indirect addressing

Indirect addressing is useful when the address an instruction should use changes while the program runs. One example is a subroutine whose parameters are passed by reference (beyond this book). Another is a pointer variable: a memory location whose contents is an address.

🔬 Lab: the four ways to load ACC

Fresh memory every time. Pick an instruction and see which locations it touches. The amber cell is the operand's address; the green cell is where the value finally comes from.

IX =   BR = 100
Choose an instruction.

Your turn

🚶 Worked Example 28.01 · A program for a simple queue

At the top level the problem can be written in structured English:

Add a character to the queue:
1  Store the contents of the accumulator
   in the memory location pointed to by
   the tail pointer.
2  Increment the tail pointer.
Remove a character from the queue:
1  Load the contents of the memory
   location at the head of the queue.
2  Increment the head pointer.

Queue processing Table 28.07

LabelOp codeOperandExplanation
JOINQ:STITAILPTRStore ACC in the location pointed to by the tail pointer
LDDTAILPTRIncrement the tail pointer
INCACC
STOTAILPTR
JMPENDQ
LEAVEQ:LDIHEADPTRLoad the contents of the location at the head of the queue
OUTOutput the character
LDDHEADPTRIncrement the head pointer
INCACC
STOHEADPTR
JMPENDQ
ENDQ:
HEADPTR:QSTARTPointer to the start of the queue
TAILPTR:QSTARTPointer to the next free location in the queue
QSTART:" "Start of the memory reserved for the queue, currently empty

HEADPTR and TAILPTR hold the address QSTART, not a character: they are pointers. As characters join, TAILPTR increases to point just past the end of the queue data; as characters leave, HEADPTR increases to point at the new head. STI TAILPTR and LDI HEADPTR are indirect, because the address they use is whatever the pointer holds at that moment.

Note: the 9618 instruction set does not include STI, so with the exam set a character cannot be added to the queue.

🚶‍♀️ Watch the queue move

QSTART is at address 50. Join puts the character in ACC into the queue with STI TAILPTR; Leave runs LDI HEADPTR and OUT.

HEADPTR =   TAILPTR =   Output:
The queue is empty: HEADPTR = TAILPTR = 50.
🧠 Think about it: the book's code has no checks. What happens if LEAVEQ runs when HEADPTR = TAILPTR (the queue is empty)? And because the pointers only ever increase, the queue creeps through memory. A real queue would need an "empty" test (LDD HEADPTR, CMP TAILPTR, JPE …) and would wrap round as a circular queue (Chapter 13).

🔄 Task 28.06 · Reverse a word with a stack

Task 28.06 Write instructions to reverse a word entered at the keyboard. This requires access to an area of memory treated as a stack.

Push each character onto a stack until Enter (ASCII 13) is pressed, then pop them off, so the last character typed comes out first. IX is the stack pointer; COUNT remembers how many characters are on the stack, because nothing can compare IX directly.

        LDM #0
        STO COUNT
        LDR #0          ; IX = 0: stack empty
PUSH:   IN
        CMP #13         ; Enter ends the word
        JPE POP
        STX STACK       ; push: store at STACK + IX
        INC IX
        LDD COUNT
        INC ACC
        STO COUNT
        JMP PUSH
POP:    LDD COUNT
        CMP #0          ; stack empty?
        JPE DONE
        DEC ACC
        STO COUNT
        DEC IX          ; IX back to the top item
        LDX STACK       ; pop: load from STACK + IX
        OUT
        JMP POP
DONE:   END
COUNT:  0
STACK:  0               ; the stack grows from here

Load Task 28.06 in the simulator and type a word: STRESSED comes out as DESSERTS.

🖥️ The accumulator machine

This runs the whole of Table 28.01, including STX, STI and relative addressing. Choose one of the chapter's programs or a model answer, or write your own, then Step through it. Memory, ACC, IX, BR and the program counter update as you go, and every step is added to the trace table.

  • One instruction or data value per line. A line can start with an address (200 LDM #0, always followed by something) or a label (LOOP: OUT), or both. Lines without an address follow on from the line before.
  • A line holding just a number is a data value, so a string can be listed one code per line. A data line can also be a label name, which stores that label's address (HEADPTR: QSTART): a pointer.
  • Values can be denary 48, binary B0011 or hex &30; immediate operands start with #.
  • Relative operands are written [BR] + 10. Set the base register with .BR 200.
  • ; starts a comment. .ACC n, .IX n and .BR n set registers before the run; .START a sets where it begins.
  • IN takes from the Input box: each key's ASCII code, the same followed by Enter (13), or whole numbers.

ACC
IX
PC
BR
Compare
Last instr.
AddrLabelContents
StepAddrInstructionACCIXMemoryOutput

🛠️ Challenge: write it yourself

Write each one in the simulator, then compare with the model.

        IN
        STO FIRST
        IN
        STO SECOND
        SUB FIRST       ; SECOND - FIRST
        AND #B10000000  ; sign bit set if the result is negative
        CMP #0
        JPN FIRSTBIG
        LDD SECOND
        JMP SHOW
FIRSTBIG: LDD FIRST
SHOW:   OUT
        END
FIRST:  0
SECOND: 0

There is no "jump if greater" instruction, so this looks at the sign bit of SECOND − FIRST (8-bit two's complement). Digits are ASCII codes 48–57, so the difference is small and the sign bit is reliable.

        LDM #57         ; "9"
LOOP:   OUT
        DEC ACC
        CMP #47         ; one below "0"
        JPN LOOP
        END
        LDM #0
        STO COUNT
LOOP:   IN
        CMP #13
        JPE DONE
        LDD COUNT
        INC ACC
        STO COUNT
        JMP LOOP
DONE:   LDD COUNT
        ADD #48
        OUT
        END
COUNT:  0

This is a WHILE loop: it tests before counting, so pressing Enter straight away gives 0. Choose "… then Enter (13) at the end" for the input.

🪞 Reflection point

These are the addressing modes you should be able to use when writing low-level code. Tick the ones you can use easily; cross the ones you find difficult, and revisit them.

Addressing modeExampleMe

📌 Summary

  • A problem must be broken down into simple steps that can be programmed using the processor's instruction set.
  • A value must be copied into the accumulator before it can be processed.
  • Processing includes: arithmetic (adding, incrementing, decrementing); comparison (equal or not equal); bitwise operations (AND, OR, XOR, shifting); and output to the screen.
  • A value can be put in the accumulator by input from the keyboard, by immediate addressing, or by loading it from memory using direct, relative, indirect or indexed addressing.
  • An address can be absolute (a number) or symbolic (a label).

🃏 Key-term flashcards

Tap a card to flip it.

✅ Quick quiz

📝 Exam-style questions

Write your answer first, then open the model answer. These are my mark-scheme-style points, not official mark schemes.

Question 1 uses this instruction set for a processor with one general-purpose register, the accumulator:

LabelOp codeOperandExplanation
LDD<address>Direct addressing. Load the contents of the given address into ACC
STO<address>Store the contents of ACC at the given address
ADD<address>Add the contents of the given address to ACC
INKey in a character and store its ASCII value in ACC
AND<address>Bitwise AND of ACC with the contents of <address>
LSL#nShift the bits in ACC logically n places left; zeros come in at the right
ENDReturn control to the operating system
<label>:<data>Gives the symbolic address <label> to the location holding <data>

Each bit of ACC is ANDed with the bit in the same position of the contents of the given address; a result bit is 1 only if both bits are 1, and the result is left in ACC.

AND MASK, where MASK: B00001111 (&0F). The mask keeps the low nibble (the digit's value) and clears the 0011 in the high nibble. For example '7' = 0011 0111 AND 0000 1111 = 0000 0111 = 7.

        IN              ; input first digit
        AND MASK        ; convert from ASCII to its digit value
        LSL #4          ; move to upper nibble
        STO RESULT      ; store in location Result
        IN              ; input second digit
        AND MASK        ; convert from ASCII to its digit value
        ADD RESULT      ; combine the two values
        STO RESULT      ; store result
        END             ; end of program
MASK:   B00001111       ; mask to convert from ASCII to digit
RESULT: &00             ; memory location for result

Only ADD can combine the nibbles with this instruction set (there is no OR here). That is safe because the low nibble of RESULT is 0000 after the shift. Keying in 4 then 7 gives 0100 0111 = &47. Load Exam question 1c in the simulator to check.

Question 2 is for a processor with one general-purpose register, ACC, and one index register, IX. Part of its instruction set: LDM #n, LDD, LDX (indexed), LDR #n, STO, STX, ADD, INC, JMP, CMP <address>, CMP #n, JPE, JPN, IN, END, plus labels.

        LDR #0          ; set index register to zero
LOOP:   LDX STRING      ; load ACC with the character at STRING + IX
        INC IX          ; increment index register
        OUT             ; output character
        CMP #33         ; is this character the ! key?
        JPN LOOP        ; no: jump to beginning of loop
        END             ; end of program
STRING: 72              ; H
        69              ; E
        76              ; L
        80              ; P
        33              ; !

The output is HELP!. The '!' is output before the test, so it appears too.

📕 Book slip: the instruction table printed for this question leaves out OUT, but the program cannot be written without it ("Output character"). Assume OUT is available, as in Table 28.01.

Load Exam question 2 in the simulator to run it.

Guru Jazzy · Cambridge AS & A Level Computer Science · Chapter 28 Low-level Programming · Opener image from the Cambridge coursebook (Langfield & Duddell)