GCC Inline Assembly for ARM Cortex-M4-Embedded Systems Course for Freshers in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Example 04 — Source Code Deep-Dive

GCC Inline Assembly for ARM Cortex-M4

The complete guide to __asm volatile — syntax, constraint strings, clobber lists, reading/writing special registers, and why the compiler needs to know what the assembly does.

============================================================

1. Overview — What This Example Does

The 004_inline_1 project demonstrates three practical uses of GCC inline assembly in embedded C:

  1. Moving a C variable into a CPU register — MOV R0, val
  2. Reading a special register into a C variable — MRS control_reg, CONTROL
  3. Loading a memory value via a pointer into a C variable — LDR p1, [p2]

It also shows (in the #if 0 block) how to perform arithmetic on fixed SRAM addresses — a pattern used in bootloaders and memory tests.

============================================================

2. Source Code — Full Annotated Walkthrough

int main(void)
{
/* ---- BLOCK 1 (disabled with #if 0): raw address arithmetic ---- */
#if 0
    /* Load the VALUE at address 0x20001000 into R1 */
    __asm volatile("LDR R1,=#0x20001000");
    /* Load the VALUE at address 0x20001004 into R2 */
    __asm volatile("LDR R2,=#0x20001004");
    /* Dereference R1: load the 32-bit word stored at address 0x20001000 */
    __asm volatile("LDR R0,[R1]");
    /* Dereference R2: load the 32-bit word stored at address 0x20001004 */
    __asm volatile("LDR R1,[R2]");
    /* Add both values together */
    __asm volatile("ADD R0,R0,R1");
    /* Store the result back to address 0x20001004 */
    __asm volatile("STR R0,[R2]");
#endif

/* ---- BLOCK 2: Move C variable into register R0 ---- */
    int val = 50;
    /* Output constraint "r": compiler chooses a register (here it will be R0)
     * Input constraint  "r": val is passed in via a register
     * %0 is the output operand (first in the list, index 0) */
    __asm volatile("MOV R0,%0" : : "r"(val));
    /* After this: R0 = 50 */

/* ---- BLOCK 3: Read CONTROL register into C variable ---- */
    int control_reg;
    /* "=r" means: write to a C variable via a register
     *  MRS = Move from special Register to general-purpose Register
     *  CONTROL = the CONTROL special register */
    __asm volatile("MRS %0,CONTROL" : "=r"(control_reg));
    /* control_reg now holds the current value of CONTROL register */

/* ---- BLOCK 4: Load from pointer into C variable ---- */
    int p1, *p2;
    p2 = (int*)0x20000008;  /* pointer to SRAM address */

    /* "=r"(p1): output — result stored in p1
     *  "r"(p2): input  — p2 value passed in as register
     * LDR p1, [p2] means: load 32-bit word from address stored in p2 */
    __asm volatile("LDR %0,[%1]" : "=r"(p1) : "r"(p2));
    /* p1 = *(int*)0x20000008 */

    for(;;);
}
============================================================

3. GCC Inline Assembly Syntax — Complete Reference

The full syntax of a GCC inline assembly statement:

__asm volatile (
    "assembly_template"           /* 1. The assembly instruction(s) */
    : output_operands             /* 2. Output constraints (optional) */
    : input_operands              /* 3. Input constraints (optional) */
    : clobber_list                /* 4. Clobber list (optional) */
);

1. Assembly Template

A string literal containing one or more ARM assembly instructions. Multiple instructions are separated by \n\t.

__asm volatile (
    "MOV R0, #5\n\t"
    "ADD R0, R0, R0\n\t"   /* R0 = 10 */
    "MOV R1, R0"
);

Operand references in the template:

SyntaxMeaning
%0First operand (output or input)
%1Second operand
%[name]Named operand (clearer for many operands)
%%Literal % character

2. Output Operands

Format: "constraint"(C_variable). Multiple outputs separated by commas.

ConstraintMeaning
"=r"Write only — compiler assigns a general-purpose register; value written to C variable after asm
"+r"Read-write — register is both read (input) and written (output)
"=m"Write to memory location (for memory output)

3. Input Operands

Format: "constraint"(C_expression). Read-only values passed into the asm.

ConstraintMeaning
"r"Any general-purpose register — compiler loads value into a register before asm
"i"Immediate integer constant (value known at compile time)
"m"Memory operand — address passed to asm
"0"Same register as operand 0 (constraint tying)

4. Clobber List

Tells the compiler which registers are modified by the asm code but are not in the output list. The compiler will not store live values in those registers across the asm block.

ClobberMeaning
"R0", "R1"…Specific registers are clobbered
"memory"Asm reads/writes memory — forces compiler to flush cached values to memory (memory barrier)
"cc"Condition flags (APSR) are modified by the asm

5. volatile Keyword

The volatile in __asm volatile tells the compiler:

  • Do not optimise away this asm block (even if outputs appear unused)
  • Do not reorder this asm relative to adjacent operations
  • Execute exactly once — no loop fusion or code motion
Always use __asm volatile when reading/writing hardware registers or CPU special registers. Without volatile, the compiler may move, remove, or duplicate the asm at its discretion.
============================================================

4. Each Instruction — Deep Explanation

Example 1: MOV R0, val

int val = 50;
__asm volatile("MOV R0,%0" : : "r"(val));

/* What the compiler generates (roughly):
 *   LDR R4, [SP, #offset]    ; load val from stack into R4
 *   MOV R0, R4               ; asm instruction: move to R0
 *
 * The "r" constraint asked for a register.
 * The compiler chose R4 for val.
 * %0 in the template was replaced with R4.
 * The final instruction is "MOV R0, R4". */

Example 2: MRS — Read Special Register

int control_reg;
__asm volatile("MRS %0,CONTROL" : "=r"(control_reg));

/* MRS = Move from Special Register to general-purpose register.
 * "CONTROL" names the CONTROL special register (not directly addressable).
 * "=r" means: compiler allocates a register, MRS writes to it,
 * then compiler stores it into control_reg.
 *
 * After this: control_reg = current CONTROL register value.
 * CONTROL register fields:
 *   bit2 = FPCA (FP context active — set by FPU instruction)
 *   bit1 = SPSEL (0=MSP, 1=PSP in Thread Mode)
 *   bit0 = nPRIV (0=Privileged, 1=Unprivileged in Thread Mode) */

Example 3: LDR %0, [%1] — Dereference Pointer

int p1, *p2;
p2 = (int*)0x20000008;
__asm volatile("LDR %0,[%1]" : "=r"(p1) : "r"(p2));

/* LDR Rd, [Rn] = Load Register: Rd = *(uint32_t*)Rn
 * %0 = output register (p1 written here)
 * %1 = input register  (p2 = 0x20000008)
 * After this: p1 = value at SRAM address 0x20000008
 * Same as C: p1 = *p2;
 * But using assembly lets you control the exact instruction generated
 * (useful when the address must be in a specific register for LDREX/STREX). */

Example 4: STR — Store via Pointer (from #if 0 block)

/* STR Rd, [Rn] = Store Register: *(uint32_t*)Rn = Rd */
int result = 42;
int *addr = (int*)0x20001004;
__asm volatile("STR %0,[%1]" : : "r"(result), "r"(addr));
/* Stores result (42) to SRAM address 0x20001004 */
============================================================

5. CONTROL Register — Deep Dive

The CONTROL register is a special-purpose register in the Cortex-M4. It can only be accessed via MRS/MSR instructions — it has no memory address.

BitFieldResetDescription
[31:3]—0Reserved, RAZ (Read As Zero)
2FPCA0Floating-Point Context Active. Set automatically by the hardware when an FPU instruction executes. When 1, the exception entry will also save FPU registers (S0–S15, FPSCR). Read-only in some contexts.
1SPSEL0Stack Pointer Selection (Thread Mode only).
0 = MSP (Main Stack Pointer) used in Thread Mode
1 = PSP (Process Stack Pointer) used in Thread Mode
In Handler Mode, always MSP regardless of this bit.
0nPRIV0Not-Privileged bit (Thread Mode only).
0 = Thread Mode is privileged (can access all registers/memory)
1 = Thread Mode is unprivileged (limited register access)
Always 0 in Handler Mode.
/* Reading CONTROL */
uint32_t ctrl;
__asm volatile ("MRS %0, CONTROL" : "=r"(ctrl));

uint8_t npriv = ctrl & 0x1;   /* bit0: 0=priv, 1=unpriv */
uint8_t spsel = (ctrl >> 1) & 0x1;  /* bit1: 0=MSP, 1=PSP */
uint8_t fpca  = (ctrl >> 2) & 0x1;  /* bit2: FP context */

/* Writing CONTROL — switch to unprivileged Thread Mode */
__asm volatile ("MRS R0, CONTROL");
__asm volatile ("ORR R0, R0, #0x01");   /* set nPRIV bit */
__asm volatile ("MSR CONTROL, R0");
__asm volatile ("ISB");   /* Instruction Sync Barrier — required after MSR CONTROL */
ISB after MSR CONTROL: ARM documentation requires an ISB (Instruction Synchronisation Barrier) instruction after writing to the CONTROL register. This ensures the pipeline is flushed and the new privilege level takes effect before any subsequent instructions execute.
============================================================

6. ARM Special Registers Accessible via MRS/MSR

RegisterMRS/MSR NameDescription
XPSRXPSRComposite PSR: APSR + IPSR + EPSR
APSRAPSRApplication PSR: condition flags (N,Z,C,V,Q)
IPSRIPSRInterrupt PSR: current exception number
EPSREPSRExecution PSR: T-bit, IT state
MSPMSPMain Stack Pointer
PSPPSPProcess Stack Pointer
CONTROLCONTROLAccess level + SP selection + FP context
PRIMASKPRIMASKPriority mask: bit0=1 disables all exceptions except NMI and HardFault
FAULTMASKFAULTMASKFault mask: bit0=1 disables all exceptions except NMI
BASEPRIBASEPRIBase priority: exceptions with lower priority (higher number) are masked

Practical Examples for Each

/* 1. Disable all interrupts (critical section entry) */
__asm volatile ("CPSID I");  /* same as: MOV R0,#1; MSR PRIMASK,R0 */

/* 2. Enable all interrupts (critical section exit) */
__asm volatile ("CPSIE I");

/* 3. Get current MSP */
uint32_t msp;
__asm volatile ("MRS %0, MSP" : "=r"(msp));

/* 4. Set BASEPRI to mask interrupts below priority 0x60 */
uint32_t pri = 0x60;
__asm volatile ("MSR BASEPRI, %0" : : "r"(pri));

/* 5. Read APSR flags */
uint32_t flags;
__asm volatile ("MRS %0, APSR" : "=r"(flags));
int zero_flag = (flags >> 30) & 1;    /* Z bit */
int negative_flag = (flags >> 31) & 1; /* N bit */

/* 6. Data Synchronisation Barrier */
__asm volatile ("DSB");

/* 7. Instruction Synchronisation Barrier */
__asm volatile ("ISB");
============================================================

7. Extended Examples

Atomic Read-Modify-Write with LDREX/STREX

/* Atomic increment of a shared variable using exclusive load/store */
uint32_t atomic_increment(volatile uint32_t *addr)
{
    uint32_t result, new_val, status;
    do {
        __asm volatile (
            "LDREX %0, [%2]\n\t"   /* exclusive load from *addr → result */
            "ADD   %1, %0, #1\n\t" /* new_val = result + 1 */
            "STREX %0, %1, [%2]"   /* exclusive store: status=0 if OK */
            : "=&r"(status), "=&r"(new_val)
            : "r"(addr)
            : "memory"
        );
    } while (status != 0);  /* retry if another thread/ISR changed the value */
    return new_val;
}

Multi-Instruction Block

/* Swap two 32-bit values using ARM registers */
void swap(uint32_t *a, uint32_t *b)
{
    __asm volatile (
        "LDR R0, [%0]\n\t"    /* R0 = *a */
        "LDR R1, [%1]\n\t"    /* R1 = *b */
        "STR R1, [%0]\n\t"    /* *a = R1 */
        "STR R0, [%1]"        /* *b = R0 */
        :
        : "r"(a), "r"(b)
        : "R0", "R1", "memory"
    );
}

Named Constraints (Cleaner Syntax)

/* Using named operands for readability */
uint32_t x = 5, y = 7, result;
__asm volatile (
    "ADD %[res], %[a], %[b]"
    : [res] "=r"(result)        /* output: named "res" */
    : [a]   "r"(x), [b] "r"(y) /* inputs: named "a" and "b" */
);
/* result = x + y = 12 */
============================================================

8. Interview Questions & Answers

Q1. What is the purpose of inline assembly in embedded C?
Inline assembly allows embedding processor-specific instructions directly within C code. It is needed for:
• Accessing CPU special registers that C cannot express (CONTROL, PRIMASK, MSP, PSP)
• Writing precise atomic operations (LDREX/STREX)
• Inserting barriers (DSB, ISB, DMB) for memory ordering
• Critical path optimisation when the compiler generates suboptimal code
• Context switch code in RTOS kernels (STMDB, LDMIA for register save/restore)
• Inserting specific instructions (SVC, BKPT, WFI, WFE) that have no C equivalent

For most peripheral access, you should use volatile pointer casts. Inline assembly is reserved for CPU-level operations that cannot be expressed in standard C.
Q2. What is the difference between __asm and __asm volatile?
__asm without volatile allows the compiler to optimise the asm block: it can move it, remove it if the output appears unused, or merge multiple identical blocks. __asm volatile prevents all of these optimisations. The compiler treats a volatile asm as having unknown side effects and preserves it exactly as written.

For register access, peripheral writes, and timing-critical code, always use __asm volatile. The only time you might omit volatile is for pure computational asm with no side effects, where you want the compiler to optimise normally.
Q3. Explain the GCC inline assembly constraint “=r” and “r”.
"=r" is an output constraint. The = means write-only (the C variable is only written, not read, by this asm). The r means use any general-purpose register (R0–R12 on ARM). The compiler allocates a register, the asm writes to it, and the compiler stores the register value into the C variable after the asm block.

"r" is an input constraint (no =). The compiler loads the C variable’s value into a general-purpose register before the asm block executes, making it available as %N in the template.

The difference: output operands provide data TO the C program FROM the asm. Input operands provide data TO the asm FROM the C program.
Q4. What is the clobber list and when do you need it?
The clobber list tells GCC which registers are modified by the asm block but not listed as outputs. This is critical for correctness: if the compiler stores an important value in R2, and your asm overwrites R2 without declaring it as clobbered, the compiler won’t know to save and restore R2, causing data corruption.

Example: : : : "R0", "R1", "memory" means R0, R1, and all memory are modified.

The special clobbers:
• "memory": forces a complete memory barrier — all cached values are flushed to memory before the asm, and reloaded after. Use when the asm reads/writes memory not listed in operands.
• "cc": the condition flags (NZCV in APSR) are modified — compiler won’t rely on flag state after this asm.
Q5. What are MRS and MSR instructions?
MRS (Move from special Register to general-purpose register): Reads a Cortex-M special register into a GP register.
Syntax: MRS Rd, <special_reg>
Example: MRS R0, CONTROL → R0 = CONTROL register value

MSR (Move from general-purpose register to Special Register): Writes a GP register value to a special register.
Syntax: MSR <special_reg>, Rn
Example: MSR CONTROL, R0 → CONTROL = R0 value

These are the only ways to access special registers (CONTROL, PRIMASK, MSP, PSP, BASEPRI, FAULTMASK, XPSR etc.) from software. They are privileged for most registers (unprivileged code can only read APSR).
Q6. What is the CONTROL register and what does each bit control?
The CONTROL register is a 3-bit special register in ARM Cortex-M4 (bits 2:0 are significant):

Bit 0 — nPRIV: Not-Privileged. 0 = Thread Mode is privileged (full register/MPU access). 1 = Thread Mode is unprivileged (restricted access). Only applies in Thread Mode — Handler Mode is always privileged.

Bit 1 — SPSEL: Stack Pointer Select. 0 = MSP used in Thread Mode. 1 = PSP used in Thread Mode. Only applies in Thread Mode — Handler Mode always uses MSP.

Bit 2 — FPCA: Floating-Point Context Active. Set by hardware when FPU instructions execute. When 1, exception entry pushes extra FPU registers (S0–S15, FPSCR) onto the stack.

Important: can only be written from privileged code. An ISB instruction is required after writing CONTROL.
Q7. What does the “naked” attribute do on a function?
The __attribute__((naked)) function attribute tells GCC to generate no function prologue or epilogue. Normally, GCC inserts: PUSH {R4,LR} at the start and POP {R4,PC} at the end to save/restore registers used by the function. With naked, none of this is generated — the function body is exactly the assembly you write and nothing else.

Usage: required for interrupt handlers and context switch code where you must completely control the stack frame. The function must contain ONLY inline assembly — no C statements (C statements require the prologue/epilogue that naked removes). A naked function must explicitly manage its own return (e.g., BX LR).
Q8. What is the difference between LDR and MOV in ARM assembly?
MOV: Move — loads an immediate value or register value into a register. Limited to certain immediate ranges (12-bit or rotated 8-bit). MOV R0, #5 loads the constant 5. MOV R0, R1 copies R1 to R0.

LDR with immediate: the assembler pseudo-instruction LDR R0, =0x20000000 loads any 32-bit constant. The assembler places the constant in a literal pool (nearby in Flash) and generates a PC-relative load. This is how large constants (like peripheral addresses) are loaded.

LDR with register base: LDR R0, [R1] is a memory load — loads the 32-bit value at the address stored in R1. This is a memory access, not just a register copy. This is what the example uses to dereference pointer p2.
Q9. What are DSB, ISB, and DMB instructions and when are they used?
DSB (Data Synchronisation Barrier): Ensures all memory accesses (reads and writes) before the DSB complete before any subsequent instructions execute. Used before entering low-power modes (WFI needs all data written to memory first) and after writing to peripheral registers that affect hardware state.

ISB (Instruction Synchronisation Barrier): Flushes the processor pipeline. Any instructions fetched before the ISB are discarded; the pipeline is refilled from the new instruction stream. Required after: writing to CONTROL register (privilege change), writing to VTOR (vector table change), enabling/disabling cache, cache maintenance operations.

DMB (Data Memory Barrier): Weaker than DSB — ensures memory accesses before the DMB appear in order relative to those after it, but doesn’t stop instruction execution. Used in multi-core or multi-bus systems to enforce memory ordering for inter-core communication.
Q10. How do you pass a struct or large value through inline assembly?
For structs and large values, use the "m" (memory) constraint, which passes the address of the variable:
typedef struct { uint32_t x, y; } Point;
Point p = {10, 20};
uint32_t sum;
__asm volatile (
    "LDR %0, [%1]\n\t"    /* R0 = p.x */
    "LDR R1, [%1, #4]\n\t" /* R1 = p.y */
    "ADD %0, %0, R1"       /* sum = x+y */
    : "=r"(sum)
    : "r"(&p)
    : "R1"
);
For the common case of passing a pointer, the “r” constraint with & is the typical approach.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *