Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

ARM Control Flow: CMP, TST, and Branch Instructions

Recommended resources

Hello! Welcome back to your ARM assembly preparation.

In our last lesson, we covered the foundational data processing instructions: MOV for moving data, LDR/STR for memory access, and ADD/SUB/AND/ORR for basic computation. You learned how to perform individual operations on data held in registers.

Now, we'll give your programs intelligence. This lesson is all about control flow. We'll move beyond a simple, linear execution of instructions and learn how to make decisions, create loops, and call functions. This is fundamental to translating the logic you're familiar with in C—like if-else statements, for loops, and function calls—into the language of the processor.

We'll focus on the instructions that make this possible:

  • Compare and Test: CMP and TST to set up conditions.
  • Branching: B (Branch), BL (Branch with Link), and BX (Branch Exchange) to change the program's execution path.

1. The Two-Step Process of Decision Making

In a high-level language like C, a statement like if (a == b) feels like a single operation. In assembly, it's a two-step process:

  1. Compare: You first perform an operation to compare the values. This operation doesn't store a result in a general-purpose register; instead, it updates special status flags in the Current Program Status Register (CPSR).
  2. Act: You then use a conditional instruction (like a branch) that reads these flags and decides whether to alter the program's flow.

Let's look at the instructions that perform the first step.

The CMP (Compare) Instruction

The CMP instruction compares two operands. Under the hood, it performs a subtraction (Operand1 - Operand2) but discards the result. Its only purpose is to set the condition flags in the CPSR based on the outcome of that subtraction.

The two most important flags for now are:

  • Z (Zero flag): Set to 1 if the result of the subtraction is zero (i.e., the operands are equal).
  • N (Negative flag): Set to 1 if the result of the subtraction is negative (i.e., Operand1 < Operand2 for signed numbers).

The following video provides an excellent introduction to this concept, using C's if-else as a familiar analogy.

ARM Assembly Branch Instructions

The video 'ARM Assembly Branch Instructions' from Jonathan Muckell clearly explains how comparison works in ARM assembly and its effect on the CPSR flags.

Please watch from the beginning to 07:13. Pay close attention to how the CMP instruction is analogous to a subtraction and how the N and Z flags are set based on its result.

The TST (Test) Instruction

The TST instruction is similar to CMP, but it performs a bitwise AND operation instead of a subtraction. Like CMP, it discards the result and only updates the flags.

Its primary use is to check if one or more specific bits are set in a register. The Z flag is set if the AND operation results in zero, meaning none of the tested bits were set.

Syntax: TST <Register>, <Operand>

Example: To check if bit 4 is set in register R1.

; In C, this is like: if (r1 & (1 << 4))
TST R1, #0x10   ; Test bit 4. (0x10 is 1 shifted left by 4)
; If bit 4 was set, Z flag will be 0.
; If bit 4 was clear, Z flag will be 1.

This is extremely useful in driver development for checking status flags in peripheral registers.

For a quick reference on both CMP and TST, you can consult this document.

Q1. Explain briefly the data processing instructions for ARM ...

The document 'Q1. Explain briefly the data processing instructions for ARM ...' provides a concise summary of comparison instructions.

Read the first part of the document, focusing on the section explaining the syntax for CMP and TST. Notice that they are listed as instructions that update the CPSR without affecting other registers.


2. Changing the Flow: Branch Instructions

Once the CPSR flags are set by CMP or TST, you can use branch instructions to conditionally jump to another part of your code.

Conditional Branching (B<cond>)

The basic branch instruction is B, but it's almost always used with a condition code suffix. For example:

  • BEQ: Branch if Equal (checks if Z flag is 1)
  • BNE: Branch if Not Equal (checks if Z flag is 0)
  • BGT: Branch if Greater Than (for signed numbers)
  • BLT: Branch if Less Than (for signed numbers)
  • BGE: Branch if Greater Than or Equal (for signed numbers)
  • BLE: Branch if Less Than or Equal (for signed numbers)
  • BHI: Branch if Higher (for unsigned numbers)
  • BLS: Branch if Lower or Same (for unsigned numbers)

The "Inverted Logic" of if statements

A crucial concept when translating from C to assembly is that you typically branch on the opposite of the condition you're testing. Why? Because if the C condition is true, you want to execute the code immediately following the comparison. If it's false, you want to branch away to skip that code.

Let's see this in practice.

ARM Assembly Branch Instructions

Let's continue with the 'ARM Assembly Branch Instructions' video. This next segment clearly demonstrates the counter-intuitive but essential logic of branching to skip code blocks.

Watch from 07:13 to 14:48. This is a critical section. Pay close attention to why a C condition of != translates to a BEQ in assembly, and a C == translates to a BNE.

Unconditional Branching (B)

The B instruction without a condition code is an unconditional branch. It always jumps to the specified label. This is essential for building if-else structures. After the if block executes, you need an unconditional branch to jump over the else block.

    CMP R0, #5
    BNE else_block    ; If R0 != 5, branch to the else part

if_block:
    MOV R1, #1        ; This is the 'if' part.
    B end_if          ; Unconditionally branch over the else block

else_block:
    MOV R1, #2        ; This is the 'else' part.

end_if:
    ; Execution continues here.

The Muckell video also briefly covers this point.

ARM Assembly Branch Instructions

This short clip explains the necessity of the unconditional branch.

Watch from 14:48 to 16:17 to see why the B instruction is needed to prevent 'falling through' into the else block.

Test your understanding!

Translate the following C code into ARM assembly. Assume x is in R0 and y is in R1. The result should be stored in R2.

if (x > y) {
    y = y + 1;
}
x = y;
Show answer
    CMP R0, R1      ; Compare x and y
    BLE end_if      ; Branch if Less than or Equal (opposite of >) to skip the 'if' block
    ADD R1, R1, #1  ; if_block: y = y + 1
end_if:
    MOV R0, R1      ; x = y

Notice we branch on BLE (the opposite of GT) to skip the ADD instruction when the condition x > y is false.


3. Function Calls: BL and BX

While B is for jumps within a function, making a function call requires two additional steps:

  1. Saving the address of where to return after the function is finished.
  2. Branching to the function's code.

BL (Branch with Link)

The BL instruction is designed for this. When you execute BL myFunction, the processor does two things simultaneously:

  1. It copies the address of the next instruction into the Link Register (LR, or R14). This is the return address.
  2. It sets the Program Counter (PC, or R15) to the address of myFunction, effectively jumping to it.

BX (Branch and Exchange)

To return from a function, you need to jump back to the address stored in the LR. The BX instruction does this. It branches to the address specified in a register.

BX LR reads the return address from the Link Register and sets the Program Counter to that address, resuming execution in the caller.

The "Exchange" part refers to its ability to switch the processor's execution state between ARM and Thumb, which is important for interoperability but not our main focus right now.

The following video provides a fantastic walkthrough of the entire function call mechanism.

ARM Assembly: Lesson 10 (Function Calls)

The video 'ARM Assembly: Lesson 10 (Function Calls)' by LaurieWired demonstrates a complete function call, including passing arguments and returning.

Watch from 03:34 to 06:09. This covers the exact mechanics of using BL to call a function and BX LR to return.


4. Code Scenario: A Practical Loop

Let's combine these concepts to implement a simple loop. In assembly, decrementing loops are often more efficient than incrementing ones because you can combine the subtraction and the check for zero into a single instruction. The SUBS instruction, for example, performs a subtraction and updates the flags.

Scenario: Write an ARM assembly function that finds the absolute value of a signed integer.
C equivalent:

int my_abs(int value) {
    if (value < 0) {
        return -value;
    } else {
        return value;
    }
}

Assembly Implementation:
Following the AAPCS, the value argument is in R0 and the return value should also be in R0.

.global my_abs

my_abs:
    CMP R0, #0       ; Compare value with 0
    BGE positive     ; If value >= 0, branch to the end
    
    ; If we are here, value is negative
    RSB R0, R0, #0   ; R0 = 0 - R0 (Reverse Subtract to negate)
    
positive:
    BX LR            ; Return. R0 holds the absolute value.
  • CMP R0, #0 sets the flags.
  • BGE positive checks the flags. If the N flag is 0 (not negative) and the Z flag is 1 (zero), it means the value is non-negative, so we skip the negation.
  • RSB R0, R0, #0 is a handy instruction for negation. It calculates 0 - R0.
  • BX LR returns to the caller.

Interview Preparation

Control flow is a very common topic in technical interviews for embedded roles. Be ready to answer questions like these:

  • Question: What is the difference between the CMP and SUB instructions?
    • Answer: Both perform a subtraction. SUB stores the result in a destination register. CMP discards the result and only updates the CPSR condition flags (N, Z, C, V).
  • Question: How would you implement a for loop that counts from 10 down to 1 in assembly?
    • Answer: You'd initialize a register to 10. Inside the loop, you'd perform your logic, then use SUBS reg, reg, #1. This instruction both decrements the register and sets the Z flag when it reaches zero. You would end the loop with BNE loop_label, which continues looping as long as the result is not zero. This is more efficient than using a separate SUB and CMP.
  • Question: Explain the roles of BL and BX LR in a function call.
    • Answer: BL (Branch with Link) is used to call a subroutine. It stores the return address (the next instruction's address) in the Link Register (LR) and then jumps to the subroutine's label. BX LR is used to return. It jumps to the address stored in the LR, effectively resuming execution where the caller left off.

Conclusion

You've now learned how to control the flow of an ARM program. This is the glue that holds all the data processing instructions together, allowing you to build complex logic.

Key Takeaways:

  • Decision-making is a two-step process: set flags (CMP, TST), then act conditionally (B<cond>).
  • When translating if statements, you typically branch on the opposite condition to skip the if block.
  • An unconditional branch (B) is necessary to properly structure if-else blocks.
  • Function calls are handled by BL (which saves the return address in LR) and returns are handled by BX LR (which jumps to the address in LR).
  • TST is a powerful tool for checking specific bits in a register, a common task in driver development.

Next Lesson Preview:

So far, we've stayed within the world of pure assembly. However, in most real-world embedded projects, you'll be working primarily in C, dropping down to assembly only when necessary for performance or specific hardware access. In the next lesson, we will bridge this gap by learning how to write and debug a simple C function with inline assembly using the __asm keyword. This is a practical and powerful skill for any embedded C developer.