Writing
GBA Assembly: instruction and syntax reference
Look up ARM7TDMI instructions, registers, flags, conditions, addressing, the stack and directives, with examples and links to the series.
This article is a reference to use alongside the From hardware to a game series and the assembly Tower Defense tutorial. It explains what instructions read, what they change, and how syntax connects to the CPU and memory.
The target is the Game Boy Advance, with an ARM7TDMI CPU, ARMv4T architecture, and instructions in the ARM state, using GNU assembler. The original Game Boy and Game Boy Color use a different CPU: these commands are not a reference for those consoles. The GBA also supports Thumb, but its formats and restrictions differ from the ARM examples here.
This reference covers every instruction family and directive used in the six examples and the teaching Tower Defense. It also introduces some additional operations. Special variants and complete encoding details belong in the manuals linked at the end. Unless stated otherwise, examples use ordinary registers rather than the special behavior of instructions writing to pc.
How to use this reference
Each link below opens the relevant section. Other articles place reference links next to the concept or code that uses them. Open the reference in another tab if you want to keep your position in the tutorial.
| What you found in the code | Where to look |
|---|---|
#, =, brackets, braces, comments | Syntax |
r0 through r15, sp, lr, pc, CPSR | Registers |
| Hexadecimal, negative values, large constants | Numbers and immediates |
mov, moveq, movne | MOV |
ldr, str, ldrh, strh, and ldr =name | LDR and STR |
[r0], [r0, #4], [r0], #4, ! | Addressing |
add, sub, rsb, subs | ADD, SUB, and RSB |
mul | MUL |
and, orr, eor, bic, mvn, orrs | Bit operations |
lsl, lsr, asr, ror | Shifts |
N, Z, C, V, and suffix s | Flags |
cmp, cmpeq, tst | CMP and TST |
eq, ne, lo, hs, lt, ge, hi, le | Conditions |
b, beq, bne, 1b, 1f | B and labels |
bl, blne, bx, bxne, bx lr | BL and BX |
push, pop, the stack, and preservation | PUSH and POP |
msr cpsr_c, #0xdf | MRS and MSR |
.equ, .space, .section, .if, .ltorg | Assembler directives |
MEMORY, SECTIONS, .bss, header | Linker and header |
as, ld, objcopy, nm, objdump | Build tools |
| BIOS calls and other families | SWI and BIOS, other instructions |
The short blocks in this article are reading and syntax examples, not complete ROMs. Symbols such as coins must be defined and laid out by the program using them. To run a complete program, use the six complete examples or the step-by-step first ROM.
Syntax
A line can contain a label, an operation, its operands, and a comment. This is a real line that could appear inside a routine:
paint: strh r1, [r0], #2 @ Write a pixel and advance two bytes.
| Part | Meaning |
|---|---|
paint: | Names this code position; it does not execute an operation |
strh | Instruction name, also called a mnemonic |
r1 | Register containing the value to store |
[r0] | Memory access at the address held in r0 |
, #2 after the brackets | Updates the address after storing |
@ ... | A comment in GNU assembler ARM syntax |
#20 is the immediate value 20. In ldr r0, =coins, =coins asks the assembler to load the symbol's value; it does not mean reading the variable. {r4, lr} is a register list used by operations such as push. A dot in .equ or .section marks a tool directive rather than a CPU instruction.
There is no single operand-count rule. add normally has a destination and two inputs; cmp has two inputs and no numeric destination; b takes an execution destination. Do not treat every first operand as a destination.
ARM suffixes add information: in strh, h means halfword; in subne, ne is a condition; in subs, s requests flag updates. These are different jobs. For the commands here, GNU assembler accepts forms such as addeq and addseq: the second adds under condition EQ and updates the flags.
Registers
CPU registers hold 32-bit values. Their names do not determine whether a value is a position, an address, or a color: the program gives the bits meaning.
| Name | Role |
|---|---|
r0 through r12 | Working values and addresses |
r13, or sp | Stack pointer |
r14, or lr | Return address produced by a bl call |
r15, or pc | Register associated with execution flow |
cpsr | Flags, ARM/Thumb state, CPU mode, and interrupt control |
Reading a RAM word requires placing an address in a register and using a load. An ordinary addition operates on registers; this instruction set has no add [address], #1 form that directly increments a memory variable.
In ARM state, the usual instruction read of pc reflects the pipeline and corresponds to the current instruction address plus eight bytes. A disassembly may therefore show a PC-relative load. That does not mean the program skipped two instructions. For branches and returns, use the patterns under B and labels and BL and BX.
Do not confuse CPU registers with KEYINPUT, VCOUNT, or DISPCNT: those are peripheral hardware registers, accessed through I/O addresses. r0 can hold the address of DISPCNT without becoming DISPCNT itself.
Numbers and immediates
20 and 0x14 represent the same number. 0x indicates hexadecimal, whose digits run from 0 through 9 and A through F. A byte is eight bits, a halfword 16 bits, and an ARM word 32 bits.
A word containing 0xFFFFFFFF represents 4,294,967,295 as unsigned or -1 as signed two's complement. Registers have no type label: the operation and condition determine the interpretation.
Ordinary calculations produce 32-bit results. Adding one to 0xFFFFFFFF leaves zero in the destination's 32 bits; requested flags help identify carry and overflow. Signed overflow V is not the same as unsigned carry C.
Not every 32-bit constant fits an ARM instruction's immediate field. In many data-processing operations, the immediate is formed from eight bits rotated by an even number of positions. Merely fitting in a register does not guarantee a direct encoding.
mov r0, #20
ldr r1, =0x12345678
The second line uses a pseudoinstruction so the assembler can choose a suitable representation. It may emit an equivalent move or read a literal near the code. See LDR and STR and .ltorg.
MOV
mov destination, source copies the source value into the destination. The source can be another register or an immediate the tool can encode.
mov r0, #20
mov r1, r0
mov r2, r1, lsl #1
At the end, r0 and r1 contain 20; r2 contains 40. The shift contributes to the value written to r2 without changing r1.
Ordinary mov neither accesses a RAM variable nor updates flags. movs updates the flags applicable to the logical operation. moveq and movne perform the move only when their condition holds. They use existing flags rather than implicitly comparing the move's source.
GNU assembler may rewrite some constants into an equivalent form such as mvn. Inspect the ELF disassembly to see what was emitted.
LDR and STR
ldr destination, [address] loads a word from memory. str source, [address] stores a word. Direction matters: str takes the source value first, with memory as the destination.
ldr r0, =coins
ldr r1, [r0]
add r1, r1, #5
str r1, [r0]
The first line puts the address represented by coins into r0. The second reads the balance. The third calculates a new balance. Only the fourth changes RAM. If the balance was 20, it becomes 25.
ldr r0, =coins is an assembler pseudoinstruction. The equals sign distinguishes this request from a bracketed memory read. If the symbol represents an address, we load an address; if it represents an .equ constant, we load that constant.
| Instructions | Width | Load behavior |
|---|---|---|
ldr / str | 32 bits | Load or store a word |
ldrh / strh | 16 bits | ldrh fills the destination's upper bits with zeros |
ldrb / strb | 8 bits | ldrb fills the destination's upper bits with zeros |
ldrsh | 16 bits | Sign-extends to 32 bits |
ldrsb | 8 bits | Sign-extends to 32 bits |
If r1 contains 0x12345678, strh r1, [r0] stores only 0x5678. There is no strsh: a narrower store discards upper bits regardless of signed interpretation.
Our RAM word accesses are aligned to four bytes and halfword accesses to two. Do not assume an unaligned access behaves like an ordinary read. The GBA region matters too: VRAM has its own restrictions, and a byte store does not behave like an isolated RAM byte store. The examples draw using strh.
The GBA is little-endian: 0x12345678 occupies bytes 78 56 34 12 in ascending addresses. That describes byte organization, not a reversed arithmetic value. The load/store forms used here do not update NZCV.
Addressing
Addresses count bytes. Incrementing a pointer by one does not automatically advance by one word or pixel.
Consider each row independently, starting with r0 = 0x02000000:
| Form | Address accessed | r0 afterward |
|---|---|---|
str r1, [r0] | 0x02000000 | 0x02000000 |
str r1, [r0, #4] | 0x02000004 | 0x02000000 |
str r1, [r0, #4]! | 0x02000004 | 0x02000004 |
str r1, [r0], #4 | 0x02000000 | 0x02000004 |
! requests base-register writeback in the pre-indexed form. In the post-indexed form, after the brackets, the base updates after the access. This changes the base register, not a variable that happened to live at the old address.
For words, some forms also allow a shifted register index:
ldr r2, [r0, r1, lsl #2]
strh r4, [r5], #2
The first line multiplies the index by four to select a word. The second stores a pixel and advances two bytes. Addressing forms are not identical for all widths: on ARM7TDMI, do not copy a scaled ldr index directly into an ldrh or strh form.
You use these patterns when drawing rectangles: the pointer traverses consecutive pixels, then skips the remaining distance to the next row.
ADD, SUB, and RSB
| Operation | Calculation |
|---|---|
add r0, r1, r2 | r0 = r1 + r2 |
sub r0, r1, r2 | r0 = r1 - r2 |
rsb r0, r1, r2 | r0 = r2 - r1 |
rsb reverses the subtraction order. It is useful for 240 - width or negating a value with 0 - value.
mov r1, #12
add r0, r1, #4
sub r2, r1, #4
rsb r3, r1, #0
The results are 16 in r0, 8 in r2, and the 32-bit representation of -12 in r3. r1 remains 12.
Without s, these operations do not update flags. adds, subs, and rsbs update NZCV. A loop can use subs counter, counter, #1 followed by bne to repeat while the result is nonzero. The condition suffix in subne does a different job: it determines whether subtraction executes.
In Tower Defense, rsblt r2, r2, #0 negates a difference only when it is negative according to the preceding comparison. For the small position values used there, this calculates an absolute value. The smallest signed 32-bit integer is a special case: its positive counterpart does not fit the same representation.
MUL
mul destination, input1, input2 multiplies two registers and keeps the product's lower 32 bits. Multiplicands cannot be immediate operands.
mov r1, #80
mov r5, #240
mul r6, r1, r5
r6 becomes 19,200, the first part of locating an image row.
ARM7TDMI operand restrictions must not be replaced with rules for a newer ARM CPU. In the ARM form here, keep the destination different from the first multiplicand: mul r6, r1, r5 meets that requirement. Our examples use mul without flag updates.
For a full 64-bit product, instructions such as umull and smull exist; see additional operations. A 32-bit product overflowing does not automatically cause a game exception.
AND, ORR, EOR, BIC, and MVN
These operations work on individual bit positions. They do not interpret a value as a button, state, or color.
| Instruction | Result | Typical use |
|---|---|---|
and r0, r1, r2 | r1 AND r2 | Keep bits selected by a mask |
orr r0, r1, r2 | r1 OR r2 | Combine options or bit fields |
eor r0, r1, r2 | r1 XOR r2 | Toggle selected bits |
bic r0, r1, r2 | r1 AND (NOT r2) | Clear selected bits |
mvn r0, r1 | NOT r1 | Invert all 32 bits |
Consider just the lowest four bits:
value = 1100 mask = 1010
AND = 1000
OR = 1110
XOR = 0110
BIC = 0100
In the button example, mvn turns the active-low convention into set bits for pressed buttons. An and with 0x03FF removes bits outside the ten controls. bic then removes buttons that were already pressed in the previous frame.
Ordinary forms do not update flags. Forms with s, such as orrs, update N and Z, may update C according to the operand shift, and preserve V. For example, orrs r0, r0, r1 lets you check whether both values were zero: only then is the result zero. Tower Defense uses this pattern when checking that a wave is over.
Shifts
A shift can be part of an instruction's second operand. ARM includes shifting circuitry associated with these operations; the source register does not need to be changed first.
| Operation | Effect |
|---|---|
lsl #n | Shift left, inserting zeros on the right |
lsr #n | Shift right, inserting zeros on the left |
asr #n | Shift right, repeating the sign bit on the left |
ror #n | Rotate: bits leaving the right reenter on the left |
mov r0, #3
mov r1, r0, lsl #4
add r2, r1, r0, lsl #1
r1 receives 48. r2 receives 48 + 6, or 54. r0 stays 3. For our coordinate values, lsl #4 is equivalent to multiplying by 16, provided the result fits the available width.
For unsigned values, lsr can represent division by a power of two with the remainder discarded. For negatives, asr preserves the sign and does not always match division in a language that rounds toward zero. rrx is a one-bit rotation involving flag C.
Amounts zero and 32 have special encoding and behavior. Introductory examples use small positive shifts; check the instruction and format in the manual for other amounts.
Flags and the S suffix
The main flags live in CPSR:
| Flag | Meaning |
|---|---|
| N | Result bit 31 is one |
| Z | Result is zero |
| C | Carry for addition or no borrow for subtraction; in logical operations it may come from the shift |
| V | Arithmetic result does not fit the signed 32-bit range |
N alone does not mean a signed comparison is “less than”: overflow must also be considered. That is why LT tests N != V.
mov r0, #0
subs r0, r0, #1
After subtraction, r0 = 0xFFFFFFFF, N is one, Z is zero, C is zero because borrowing occurred, and V is zero because signed -1 fits.
cmp and tst update flags without requiring an explicit final s. sub and subs, however, are different forms. Ordinary ldr, str, and mov do not automatically erase a previous comparison result.
A condition always observes the most recent flags, not necessarily the nearest comparison you see in the text. An instruction with s between cmp and bne can change the branch's meaning. A function call may also change flags; do not rely on them across a call without an explicit convention.
CMP and TST
cmp a, b updates NZCV as if calculating a - b but discards the numeric result. Both operands stay unchanged.
cmp r1, #20
blo sem_dinheiro
sub r1, r1, #20
If r1 is below 20 unsigned, execution goes to sem_dinheiro. Otherwise it continues and subtracts the cost. Comparison alone does not implement an if block: a branch or conditional instruction uses the result.
tst a, mask updates logical flags as if performing AND without saving the AND result. We mainly use Z to detect whether any selected bit was set:
tst r1, #16
bne direita_pressionada
This checks bit 4. With a mask containing multiple set bits, a nonzero result means at least one selected bit is set, not necessarily all of them. To test all, store an AND result and compare it to the mask.
cmpeq is conditional comparison. This pattern checks whether both coordinates match a point:
cmp r0, #6
cmpeq r1, #2
bne fora_do_ponto
If X is not 6, the second comparison does not execute and the first unequal result remains. If X is 6, the second comparison decides whether Y is 2. The branch is therefore taken if either coordinate differs.
Conditions
In ARM state, the instructions here can take condition codes. The same condition can appear in bne, movne, strne, or bxne: the instruction's effect occurs only when flags satisfy it.
| Suffix | Flag test | Meaning after cmp a, b |
|---|---|---|
eq | Z = 1 | Equal |
ne | Z = 0 | Not equal |
cs / hs | C = 1 | a >= b, unsigned |
cc / lo | C = 0 | a < b, unsigned |
hi | C = 1 and Z = 0 | a > b, unsigned |
ls | C = 0 or Z = 1 | a <= b, unsigned |
ge | N = V | a >= b, signed |
lt | N != V | a < b, signed |
gt | Z = 0 and N = V | a > b, signed |
le | Z = 1 or N != V | a <= b, signed |
mi / pl | N = 1 / N = 0 | Sign bit one / zero |
vs / vc | V = 1 / V = 0 | Signed overflow occurred / did not occur |
al | Always | Usual form without a condition suffix |
For r0 = 0xFFFFFFFF and cmp r0, #0, bhi would be taken because the unsigned value is greater, and blt would also be taken because the signed value is -1. There is no contradiction: the same bits have different interpretations.
Movement examples use signed conditions for temporarily negative coordinates. Some Tower Defense bounds checks instead use an unsigned comparison that also rejects -1, since it appears enormous under that interpretation.
A false condition prevents the instruction's effect but does not imply zero execution cost. Do not estimate a frame's timing by counting only commands that change values.
B and labels
b destination transfers execution to the label without creating a return address in lr. A conditional form such as bne branches only when its condition holds. Without a taken branch, execution continues at the next instruction.
mov r2, #3
repeat:
subs r2, r2, #1
bne repeat
The decrement occurs three times. The program recognizes no special loop structure: it recognizes subtraction, flags, and execution-flow changes.
GNU assembler also offers reusable numeric labels:
b 1f
1: mov r0, #3
2: subs r0, r0, #1
bne 2b
1f finds the next 1: forward; 2b finds the most recent 2: backward. The b in 2b means backward, not button B. Tools resolve those names before execution.
A named label does not automatically create function scope. Without a branch or return, execution may continue through whatever follows the label. See GNU assembler's local-label rules.
BL and BX
bl routine branches and puts the address for continuing after the call in lr. In the ARM state used here, that return points to the next instruction after bl. Routines commonly return using bx lr.
double_value:
add r0, r0, r0
bx lr
This doubles r0 and returns to the caller. No function object protects registers: the contract must specify inputs, outputs, and what may change.
bx register jumps to the address in the register and may change ARM/Thumb state according to destination bit 0. It does not create a return in lr. Code addresses must satisfy the selected state's alignment requirements.
A routine making another bl call overwrites its own lr. To return to its original caller, it must preserve that value, usually on the stack. In contrast, b fill_rect at the end of paint_hero forwards execution without replacing lr: fill_rect returns directly to the caller of paint_hero.
blne new_game is a conditional call. bxne lr is a conditional early return. Neither implicitly compares values; both use existing flags.
PUSH and POP
The stack occupies RAM and is controlled by sp. In our examples it grows toward lower addresses. push reserves space and stores registers; pop reads them back and releases that space.
outer:
push {r4, lr}
mov r4, #20
bl inner
pop {r4, lr}
bx lr
If sp was 0x03007F00, pushing two registers moves it to 0x03007EF8. The list occupies eight bytes, four per register. In ascending addresses, the saved values are r4, then lr. Register numbers determine the order, not the textual idea of copying a list backward.
On ARM these forms use multiple-store/load operations equivalent to stmdb sp! and ldmia sp!. {r5-r8, lr} means five registers and therefore 20 bytes.
Every return path must restore anything pushed. Returning before pop leaves the stack unbalanced. The list must also preserve the registers required by the routine's contract, not just lr.
The examples use local conventions, including color in r4. This is not the standard contract for an arbitrary C function. When mixing languages, respect the chosen ABI, preserved registers, and AAPCS eight-byte stack alignment at public interfaces. Do not assume every internal call here was written for that integration.
MRS and MSR
mrs reads a status register into an ordinary register. msr writes selected status fields. They operate on CPU state, unlike a memory-addressed video-register write.
mrs r0, cpsr
msr cpsr_c, #0xdf
In example startup, msr cpsr_c, #0xdf sets the control field: System mode, ARM state, and masked IRQ/FIQ. The lowest five bits of 0xDF are 11111, the System-mode code; I and F are one, and T is zero.
_c selects the control field rather than NZCV. Writing it requires the appropriate privilege context. These programs poll and install no interrupt handlers; copying this startup into an IRQ-dependent program may stop it working.
Do not use msr to casually alternate between ARM and Thumb. State changes use the appropriate mechanisms such as bx. The instruction above is initial configuration while already in ARM state.
Assembler directives
Directives guide GNU assembler. Some select syntax; others define symbols or place data into sections. They are not CPU operations like additions or loads.
| Directive | Job in this project |
|---|---|
.syntax unified | Select GNU assembler's unified syntax |
.cpu arm7tdmi | Select the target and restrict available instructions |
.arm | Emit ARM-state code; it does not switch CPU state at runtime by itself |
.equ NAME, value | Define a value symbol without reserving a RAM variable |
.section name, "ax" | Select a section; a means allocation and x executable code |
.global name | Make a symbol visible for linking and other tools |
.space n | Reserve n bytes, interpreted according to section and linking |
.balign n | Advance to alignment on a multiple of n bytes |
.word value | Emit 32-bit data |
.hword value | Emit 16-bit data |
.byte value | Emit eight-bit data |
.ltorg | Emit the section's pending literal pool at this point |
.if expression / .endif | Include code based on an assembly-time expression |
.ifndef NAME | Include a block if the symbol is not defined |
.error "message" | Fail assembly with an error |
A hardware constant and a variable play different roles:
.equ KEYINPUT, 0x04000130
.section .bss
.balign 4
.global coins
coins: .space 4
KEYINPUT names an address fixed by hardware. coins marks four bytes the linker will place in RAM. The project uses NOLOAD BSS; reserving that space does not make the CPU zero it. Startup must clear the variables.
.ltorg matters when ldr =value depends on a reachable literal near the code. Literals are data: do not let execution fall directly into them. In the examples, a return or branch ends the execution path before the emitted area.
Assembly-time conditions differ from game conditions. .if LESSON >= 2 selects what enters the file before the ROM exists. cmp followed by bne decides at runtime. Consult ARM directives and the general assembler manual for other forms.
Linker and header
The assembler creates instructions, data, and references. The linker organizes them and resolves addresses between sections and files.
gba.ld item | Meaning |
|---|---|
OUTPUT_FORMAT / OUTPUT_ARCH | Output ELF format and architecture |
ENTRY(_start) | Entry point declared in ELF |
MEMORY, ORIGIN, LENGTH | Available regions and sizes |
SECTIONS | Rules placing input sections |
KEEP(*(.header)) | Preserve our reserved header section |
.text / .rodata | Instructions and constants arranged in ROM |
.bss (NOLOAD) | State space without corresponding initial ROM bytes |
ALIGN(4) | Align the position to four bytes |
__bss_start, __bss_end | Bounds used by the clearing loop |
ASSERT | Fail linking when a layout condition is not met |
The linker script's dot, ., represents its layout position. It is not the same as the dot beginning an assembler directive, despite their similar notation.
The header is a GBA cartridge-format requirement. We reserve bytes at the ROM's beginning, place a branch instruction at entry, and use rom.py to fill fields and checksum. ENTRY(_start) alone does not make the console search for a textual symbol: the binary presents bytes and instructions at specific positions.
.header is a project naming convention; the linker places it correctly. Header Python runs on the development computer. No linker or Python command automatically becomes a GBA CPU instruction. GNU ld's SECTIONS documentation details the layout.
Build tools
These are terminal commands, not assembly-language instructions:
| Command | Job |
|---|---|
arm-none-eabi-as | Assemble .s text into an .o object |
arm-none-eabi-ld | Link objects and organize ELF addresses |
arm-none-eabi-objcopy -O binary | Extract ROM binary contents |
python3 rom.py file.gba | Fill and verify the example's header |
arm-none-eabi-nm -n file.elf | List symbols in address order |
arm-none-eabi-objdump -d file.elf | Disassemble instructions |
arm-none-eabi-objdump -h file.elf | Show sections |
-mcpu=arm7tdmi selects the target. -g includes debugging information. -o chooses the output filename. -T gba.ld selects the linker script. In Tower Defense, --defsym LESSON=2 defines an assembly-time symbol to select a stage.
The .elf preserves information useful for inspecting addresses and names. The .gba holds the cartridge opened in the emulator. Changing .s without rebuilding does not change an old ROM on disk.
SWI and BIOS
swi causes a software exception. GBA BIOS services can be called through that mechanism, following the appropriate service identifier, argument convention, and ARM or Thumb state. It is not a personal computer operating-system call.
None of the six examples depends on swi: video waits poll VCOUNT. A BIOS service waiting on interrupts requires the relevant configuration to be prepared. Do not replace polling with a BIOS call without checking those requirements.
Begin BIOS experiments with a small program, a documented function convention, and an observable outcome. The instruction reference explains how an exception is requested; platform documentation explains the service handling it.
Other ARM7TDMI instructions
These families complement the tutorial instructions. This table is a guide for later study, not a recommendation to add all of them to your first game.
| Family | Job |
|---|---|
adc | Add operands and the current carry |
sbc | Subtract the operand and the borrow represented by 1 - C |
rsc | Reverse subtraction with the same borrow treatment |
cmn | Set flags as for addition, discarding the result |
teq | Set logical flags as for XOR, discarding the result |
mla | Multiply and add an accumulator, producing 32 bits |
umull / smull | Unsigned / signed long multiplication into two result registers |
umlal / smlal | Long multiplication with 64-bit accumulation |
ldm / stm | Load / store a register list, with ordering and writeback variants |
swp / swpb | Swap a register and memory value, as a word / byte |
ARM7TDMI has no sdiv, udiv, movw, movt, blx, or later architectures' it blocks. Division needs a routine, an appropriate BIOS service, or a mathematically valid transformation. Calling across ARM and Thumb on this CPU requires ARMv4T interworking patterns.
ARMv4T also lacks a dedicated instruction with the modern NOP encoding. The assembler may accept nop and emit an equivalent operation, such as moving a register into itself. Checking the target and disassembly distinguishes accepted tool syntax from new CPU capabilities.
A worked example
Read this fragment assuming coins is defined in RAM and contains 20:
spend_once:
ldr r0, =coins
ldr r1, [r0]
cmp r1, #5
bxlo lr
sub r1, r1, #5
str r1, [r0]
bx lr
| Step | What changes |
|---|---|
Load =coins | r0 receives an address; the balance has not been read |
Read [r0] | r1 receives 20 |
| Compare with 5 | Flags describe subtraction; r1 remains 20 |
| Conditional return | Does not return because 20 is not below 5 unsigned |
| Subtract | r1 becomes 15; RAM still holds 20 |
| Store | RAM now holds 15 |
| Return | Execution continues through the address in lr |
With an initial balance of 3, the routine would return before subtracting. It preserves neither r0, r1, nor flags, and reports no separate success value: those choices are part of this fragment's contract.
This exercise combines the series' essential distinction: a constant may represent an address, a register may hold that address, and a bracketed instruction accesses memory contents. At an I/O address, the same operation interacts with hardware.
Common mistakes
| Symptom or misunderstanding | What to check |
|---|---|
“I changed r1, but the balance did not change” | Did you forget str? |
“ldr =coins gave me a huge number” | You loaded an address, not the balance |
“bne uses the wrong comparison” | Did an instruction or call change the flags? |
“-1 seems greater than zero” | Was the comparison unsigned? |
| “The next pixel appears in the wrong place” | Did the address advance two bytes, and was the row boundary handled? |
| “The function returns to the wrong place” | Did a nested call overwrite lr? |
| “Restart repeats while I hold Start” | Is the rule using current state instead of a new press? |
“.space 4 did not start at zero” | Reserving space does not initialize RAM at runtime |
| “An instruction from the internet will not assemble” | Does it belong to ARM7TDMI and the selected ARM state? |
| “It assembled but does not run correctly” | Check layout, entry, header, CPU state, and hardware accesses as well as syntax |
Official references
The ARM7TDMI Technical Reference Manual, DDI 0210C describes the CPU model and summarizes instructions. Use the ARM-instruction and condition sections while respecting the target architecture. Newer ARM reference cards can contain operations absent from the GBA.
The GNU assembler ARM manual defines the tool syntax used here. Its directive and symbol and local-label pages help interpret program text. The GNU ld script manual covers linking.
To continue with runnable experiments, return to the nine-chapter index. To apply these operations in a round, follow the main Tower Defense tutorial.