Inline assembly allows inserting assembly language code directly into C/C++ code. This can be useful for Cortex-M0/M0+ programming when you need more control than C/C++ alone provides, such as for low-level hardware access, performance optimizations, and access to specialized processor instructions.
When to use inline assembly
Typical situations where you would want to use inline assembly on Cortex-M0/M0+ include:
- Implementing time-critical routines
- Optimizing performance by using special registers or instructions
- Accessing features not available from C/C++
- Controlling hardware registers and peripherals
- Writing interrupt service routines and exception handlers
Inline assembly should be used judiciously. Only use it when necessary as it breaks portability and can hamper code comprehension. Prefer to use C/C++ if performance is not critical.
Cortex-M0/M0+ inline assembly syntax
The basic syntax for inline assembly in C code is: asm(“assembly instructions”);
For example: asm(“mov r0, #1”); // Move immediate value 1 into register r0
The assembly code can include almost any valid Cortex-M0/M0+ assembly instruction. Global labels are not allowed but you can use local labels prefixed with a ‘.’.
To interface the inline assembly with the C code, you need to map C variables to assembly registers. This is done by adding modifiers before the registers: int x = 1; int y; asm(“mov r0, %[input]” : [output] “=r” (y) : [input] “r” (x)); // Move x into r0, and output r0 into y
“r” indicates a general purpose register, “=” that it is written to, and the names in [] map the C variables to these registers.
Accessing Cortex-M0/M0+ registers
To access registers like the program status register in inline assembly, the CMSIS headers define register aliases: #include “cmsis.h” asm(“mov %[result], #0” : [result] “=r” (result) : ); asm(“mrs %[result], primask” : [result] “=r” (result) :: );
This allows reading the PRIMASK register into the C variable result. Other common registers like CONTROL, MSP and PSP are accessible in the same way.
Memory access
The Cortex-M0/M0+ has byte, halfword and word memory access. To load/store memory in inline assembly use: asm(“ldrb r0, [%[addr]]” : : [addr] “r” (&data)); // Load byte asm(“strh r0, [%[addr]]” : : [addr] “r” (&data)); // Store halfword
The square brackets are used to dereference the address. You can also use offsets: asm(“ldr r0, [%[addr], #8]” : : [addr] “r” (&data)); // Load word offset
Conditional execution
Assembly conditions can be used like: int x = 10; asm(“cmp %[input], #0” : : [input] “r” (x)); asm(“ite eq” : : : “cc”); asm(“movgt r0, #1” : : : “cc”); asm(“moveq r0, #0” : : : “cc”);
This compares x to 0, then conditionally moves 1 or 0 into r0 based on the result. “cc” indicates the instructions use condition flags.
Loops and branches
Branches and loops are also supported in inline assembly. Local labels prefixed with ‘.’ can be used with branches: int count; asm(“mov r0, #0”); asm(“loop:”); asm(“add r0, r0, #1”); asm(“cmp r0, %[input]” : : [input] “r” (count)); asm(“bne .loop” : : : “cc”);
This is a simple loop that increments r0 count times. “cc” indicates it branches based on condition flags.
Function calls
You can call C functions within inline assembly using: void foo(int x) { // … } asm(“mov r0, %[val]” : : [val] “r” (x)); asm(“bl foo” : : : “r0”);
This passes x in r0 and calls foo(). Any registers used for parameter passing need to be listed after the asm statement.
Similarly, assembly functions can be called from C by declaring them as: extern void asm_func(int x); int result; asm(“mov r0, %[val]” : : [val] “r” (x)); asm(“bl asm_func” : “=r” (result) : : “r0”);
This stores the return value from asm_func() into result. Parameter and return registers need to be declared.
Inserting assembly comments
Assembly comments inside inline ASM code are inserted by escaping them with \n: asm(“mov r0, #1 \n\t” “// Load immediate value \n\t” “bx lr”);
Mixing C and assembly
With careful planning, it is possible to mix C statements and assembly within the same asm() statement: int x = 10; int y; asm(“mov r0, %[input] \n\t” “mov r1, #3 \n\t” “mul r0, r1 \n\t” “str r0, %[output]” : [output] “=m” (y) : [input] “r” (x)); // x * 3 stored in y
This allows creating inline functions that combine C and assembly.
Guidelines for efficient inline assembly
Some guidelines for writing efficient inline assembly code:
- Minimize exchanging data between C and assembly. This can be slow.
- If using floating point, ensure VFP is enabled and aware of context switching.
- Understand assembly effects on condition flags and registers used by C code.
- Use registers r0-r3, r12 for scratch to avoid corrupting C values.
- For critical routines, use assembly only to maximize performance.
- Prefer word and halfword memory access over byte access.
- Take advantage of load/store multiple instructions for efficiency.
Also ensure any assembly code is commented well to aid comprehension and maintenance.
Debugging inline assembly
Debugging inline assembly code can be challenging. Some tips:
- Use a debugger that understands mixed C/assembly like GDB.
- Single step through assembly while examining register/memory contents.
- Break on the asm() statement and step into the assembly.
- Print register values before and after inline assembly to check logic.
- Compare expected vs actual register values for mismatches.
- Add liberal comments explaining the purpose of each instruction.
- Break complex routines into smaller helper assemblies to isolate bugs.
Testing inline assembly thoroughly before integration is highly recommended. Bugs can easily corrupt memory contents and registers.
Conclusion
Inline assembly is a powerful tool for Cortex-M0/M0+ programming when used properly. Insert it sparingly where C/C++ alone is insufficient. Focus on the minimal assembly needed to achieve your goal. Rigorously validate your inline asm code, add ample comments, and design it to interoperate cleanly with the C code.