Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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:
- Moving a C variable into a CPU register —
MOV R0, val - Reading a special register into a C variable —
MRS control_reg, CONTROL - 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:
| Syntax | Meaning |
|---|---|
%0 | First operand (output or input) |
%1 | Second operand |
%[name] | Named operand (clearer for many operands) |
%% | Literal % character |
2. Output Operands
Format: "constraint"(C_variable). Multiple outputs separated by commas.
| Constraint | Meaning |
|---|---|
"=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.
| Constraint | Meaning |
|---|---|
"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.
| Clobber | Meaning |
|---|---|
"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
__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.
| Bit | Field | Reset | Description |
|---|---|---|---|
| [31:3] | — | 0 | Reserved, RAZ (Read As Zero) |
| 2 | FPCA | 0 | Floating-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. |
| 1 | SPSEL | 0 | Stack 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. |
| 0 | nPRIV | 0 | Not-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 */
6. ARM Special Registers Accessible via MRS/MSR
| Register | MRS/MSR Name | Description |
|---|---|---|
| XPSR | XPSR | Composite PSR: APSR + IPSR + EPSR |
| APSR | APSR | Application PSR: condition flags (N,Z,C,V,Q) |
| IPSR | IPSR | Interrupt PSR: current exception number |
| EPSR | EPSR | Execution PSR: T-bit, IT state |
| MSP | MSP | Main Stack Pointer |
| PSP | PSP | Process Stack Pointer |
| CONTROL | CONTROL | Access level + SP selection + FP context |
| PRIMASK | PRIMASK | Priority mask: bit0=1 disables all exceptions except NMI and HardFault |
| FAULTMASK | FAULTMASK | Fault mask: bit0=1 disables all exceptions except NMI |
| BASEPRI | BASEPRI | Base 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?
• 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?
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?
Syntax:
MRS Rd, <special_reg>Example:
MRS R0, CONTROL → R0 = CONTROL register valueMSR (Move from general-purpose register to Special Register): Writes a GP register value to a special register.
Syntax:
MSR <special_reg>, RnExample:
MSR CONTROL, R0 → CONTROL = R0 valueThese 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?
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?
__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 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?
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?
"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