Writing
GBA Assembly — 2. Bits, registers and memory
Follow values through ARM assembly: bits, addresses, loads, stores, comparisons and masks, with step-by-step exercises.
In the first chapter, we saw that writing to an address can change a variable, configure video, or change an image. Now we will understand what is being written and how the CPU arrives at that result.
The goal is to trace a short assembly fragment with a pencil and paper. The examples in this chapter are reading exercises, not complete files to assemble. The first executable ROM appears in the next chapter.
Your route through this chapter
Before starting: you should be able to distinguish CPU, ROM, RAM, and video memory after chapter 1. This chapter still uses paper exercises, not an executable program.
- Use the number table to compare different spellings of the same value. You do not need to convert an entire address by hand.
- Trace the two arithmetic instructions: write
r1 = 20, then cross out 20 and write 15 after the subtraction. - Follow the RAM example one line at a time. Keep separate notes for
r0,r1, andcoins; compare your notes with the table. - Study the comparison and button-mask examples only after that distinction is clear. Look up an unfamiliar instruction through the nearby reference link.
Checkpoint: after loading 20, subtracting 5, and storing, coins is 15. If you omit the store, RAM remains 20. Explain why before continuing to chapter 3. No terminal output is expected in this chapter.
One number, three ways to write it
The reference explains numbers, signed values, and immediate constants.
A bit holds one of two values: zero or one. A byte contains eight bits. Eight bits represent 256 combinations, which range from 0 to 255 when interpreted as unsigned numbers.
| Decimal | Eight-bit binary | Hexadecimal |
|---|---|---|
| 0 | 00000000 | 0x00 |
| 1 | 00000001 | 0x01 |
| 15 | 00001111 | 0x0F |
| 16 | 00010000 | 0x10 |
| 255 | 11111111 | 0xFF |
These are not three kinds of number. They are three ways to write the same value. Hexadecimal uses digits 0 through 9 and letters A through F; each digit corresponds to four bits. Two digits therefore describe one byte. This makes bit masks and addresses easier to inspect.
In the ARM CPU used by this series, a word is 32 bits, or four bytes. A halfword is 16 bits, or two bytes. Whenever someone says “write to memory,” also ask how many bytes will be written.
Registers: values the CPU has at hand
Look up register names, MOV, and ADD, SUB, and RSB while following this fragment.
We will use r0 through r12 for temporary values. Other important names are sp, the stack pointer; lr, which receives a call's return address; and pc, which relates to the execution position.
Imagine this high-level code:
int moedas = 20;
moedas = moedas - 5;
To study just the arithmetic, we can keep the balance in r1:
mov r1, #20
sub r1, r1, #5
mov places a value in its destination. sub destination, source, value calculates the subtraction and stores its result in the destination. # marks an immediate value written in the instruction. After these two lines, r1 contains 15.
Calling a register r1 does not make its contents a coin balance. If another routine overwrites r1, that balance disappears from the register. To preserve a game variable, we will reserve space in RAM.
Holding an address is different from reading its contents
Use LDR and STR to compare the forms with = and brackets. Memory reservations are covered under assembler directives.
This is one of the most important details in the course:
ldr r0, =coins
ldr r1, [r0]
sub r1, r1, #5
str r1, [r0]
In the GNU assembler syntax we use, ldr r0, =coins is a pseudoinstruction: the assembler chooses how to place the address represented by coins in r0. It does not read the balance. The next line, with brackets, reads the word stored at that address.
We then calculate in r1 and store the result back. Without the final str, RAM would still contain the old balance.
| After this line | r0 | r1 | Contents of coins in RAM |
|---|---|---|---|
| Before | Unspecified for this exercise | Unspecified | 20 |
ldr r0, =coins | Address of coins | Unspecified | 20 |
ldr r1, [r0] | Same address | 20 | 20 |
sub r1, r1, #5 | Same address | 15 | 20 |
str r1, [r0] | Same address | 15 | 15 |
In the game, the reservation looks like this:
.section .bss
.balign 4
coins: .space 4
Lines beginning with a dot are directives for the build tools. They are not CPU instructions. .space 4 reserves four bytes; .balign 4 requests alignment to a multiple of four. The linker decides the actual address of coins. Reserving BSS space does not make the hardware initialize it to zero: our startup code must do that.
The width of each access matters
The load/store width table explains ldrb, ldrh, ldr, and the corresponding stores.
| Operation | Load | Store | Use in this series |
|---|---|---|---|
| 8 bits | ldrb | strb | Byte concept; not used for drawing |
| 16 bits | ldrh | strh | Video control, buttons, and pixels |
| 32 bits | ldr | str | Game variables |
Alignment requires care on ARM7TDMI: our words use multiples of four, and halfwords use multiples of two. Do not use an unaligned access as a shortcut to combine bytes.
GBA memory regions also have their own rules. A byte write to VRAM does not behave like an ordinary byte write to RAM. We will write pixels with strh, respecting the expected format and width.
The GBA uses little-endian byte order. If a word contains 0x12345678, its bytes appear as 78 56 34 12, from the lowest address to the highest. The value has not been reversed: that is the byte order in memory. Reading the word at the correct width reconstructs the value.
How a comparison becomes a branch
For the conditions, consult CMP and TST, flags and the S suffix, and the signed and unsigned condition table.
In a high-level language, you would write “if the balance is less than 20, do not buy.” In our assembly:
cmp r1, #20
blo sem_dinheiro
sub r1, r1, #20
cmp compares as if subtracting, but does not store the result in r1. It updates CPU indicators called flags, in the status register. blo examines those indicators and branches if the first value is lower, interpreting both values as unsigned.
sem_dinheiro is a label: a name for a position in the code. A label is not inherently a function; it just marks a location. The fragment above is incomplete and is intended to explain the condition.
| Condition | Meaning we will use |
|---|---|
eq | Equal |
ne | Not equal |
lo / hs | Lower / higher or same, unsigned |
lt / ge | Less than / greater than or equal, signed |
The same bits can represent a signed or unsigned number. The selected condition determines how the comparison is interpreted. A 32-bit value with every bit set can represent signed -1 or unsigned 4294967295.
Some instructions take a condition suffix: subne subtracts only when the “not equal” condition holds. Others take s, such as subs, to update the flags. Before relying on flags, identify which instruction most recently updated them.
Bits can also represent independent choices
The reference describes AND, ORR, EOR, BIC, and MVN and shifts.
A variable can hold a whole balance. A hardware register often combines several independent options in separate bits. We use a mask to inspect one option.
tst r1, #16
bne direita_pressionada
tst performs a logical AND only to update flags. Mask 16 has a single set bit: bit 4. If that bit is also set in r1, the result is nonzero and the branch is taken. In the input chapter, this bit will represent Right after we normalize the hardware reading.
AND selects bits, OR combines bits, and NOT inverts bits. Shifting a value four positions left with lsl #4 multiplies it by 16 when the result fits the available width. We will use this to convert board cells into pixels.
A short tracing exercise
Register and return-address preservation are explained in BL and BX and PUSH and POP.
Suppose coins = 20 in RAM. The CPU loads it into r1, adds 5, and calls another routine before storing. That routine changes r1 to 3. What will str r1, [r0] store now, assuming r0 was preserved?
It will store 3. Registers do not automatically belong to a variable. We must establish which registers a routine may change and which it must preserve. The stack helps save values and return addresses; we will see that when drawing rectangles.
For syntax, consult the GNU assembler ARM directives. The Arm ARM7TDMI reference describes the CPU; you do not need to read it all to follow the course.
Previous: how the hardware works. Next: setting up and running your first ROM.