Embedded Systems Architecture: ARM
Cortex-M4 Programming
Reading Packet: Instruction Encoding, Memory Mapping, and Assembly Optimization
Section 1: The Hardware-Software Interface
When developing embedded software for small spacecraft and payload subsystems, developers
cannot rely on the abstractions provided by high-level desktop programming or standard
operating systems. Every clock cycle and byte of memory is a critical resource.
The ARM Cortex-M4 is a 32-bit RISC processor designed specifically for highly deterministic,
real-time embedded applications. Unlike older, general-purpose ARM processors, the
Cortex-M4 operates exclusively in the Thumb-2 execution state.
The Thumb-2 Advantage
Historically, ARM processors required developers to manually switch between two operating
states:
● ARM State: 32-bit instructions. Powerful, but consumed excessive Flash memory.
● Thumb State: 16-bit instructions. Highly memory-efficient, but lacked access to
advanced arithmetic and higher registers.
Thumb-2 Technology merges these concepts into a single, variable-length instruction set.
16-bit and 32-bit instructions are freely intermixed without requiring the CPU to switch states.
For satellite telemetry systems where Flash memory is limited, this provides the processing
power of a 32-bit core with the code density of a 16-bit architecture.
Section 2: Assembler Directives and Program Structure
Assembly code contains more than just CPU instructions. It relies heavily on Assembler
Directives—commands that tell the assembler software (like arm-none-eabi-as) how to
translate, organize, and format the binary output. They do not execute on the CPU; they build
the environment.
1. Hardware and Syntax Directives
At the very top of a source file, the assembler must be told what hardware it is compiling for:
● .syntax unified: Instructs the assembler to use the Unified Assembler Language (UAL).
This modern syntax allows the same code structure to be used for both 16-bit and 32-bit
Thumb instructions.
● .cpu cortex-m4: Restricts the assembler to the M4 instruction set, preventing the
accidental use of instructions the physical chip does not support.
● .thumb: Forces the assembler to generate Thumb-2 code. (If omitted, the assembler
might attempt to generate standard ARM code, which will instantly crash a Cortex-M4).
2. Organizing Memory: The .section Directive
Programs are divided into distinct "neighborhoods" called sections. The linker uses these names
to place the code in the correct physical memory hardware.
● .section .text: This is where your actual executable instructions (code) live. It is usually
placed in Read-Only Memory (Flash).
● .section .data: This section holds initialized global and static variables.
● .section .vectors: A highly specific section placed at the very beginning of memory
(0x00000000). It contains the Vector Table, which holds the Stack Pointer address and
the Reset Vector (the address of the first instruction to execute on power-up).
3. Data Allocation and Alignment Directives
● .word: Allocates exactly 32 bits (4 bytes) of space in memory and populates it with a
value. (e.g., .word 0x20001000 sets the Stack Pointer).
● .align 2: This is critical for ARM architectures. The Cortex-M4 expects 32-bit instructions
and data to be aligned on 4-byte boundaries. .align 2 tells the assembler to pad the
memory with zeros until the address is a multiple of $2^2$ (4). Misaligned memory
access can cause a processor HardFault.
4. Visibility and Processor State Directives
● .global Reset_Handler: Makes the label Reset_Handler visible to the Linker. Without
this, the Linker cannot connect the Vector Table to your actual code.
● .thumb_func: This directive is placed immediately before a function label. The
Cortex-M4 requires that the addresses of all Thumb functions have their Least
Significant Bit (LSB) set to 1. If a function starts at 0x08000040, the CPU must be
given the address 0x08000041 to remain in Thumb state. .thumb_func ensures the
assembler and linker automatically handle this $+1$ offset.
Section 3: The Linker Script ([Link])
If the assembly code tells the CPU what to do, the Linker Script tells the binary where to live.
Bare-metal programming requires explicit mapping of the physical silicon.
The MEMORY Command
The linker script begins by defining the physical realities of the microcontroller chip:
Code snippet
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
This tells the linker that Flash memory is read/execute (rx) and starts at address 0x0, while
SRAM is read/write/execute (rwx) and starts at 0x20000000. If your code grows larger than
256K, the linker will throw an error based on these bounds.
The SECTIONS Command
This command maps the .section directives from your assembly code into the physical
MEMORY regions.
Code snippet
SECTIONS
{
.text : {
KEEP(*(.vectors))
*(.text)
} > FLASH
The KEEP command prevents the linker's garbage collector from discarding the Vector Table,
ensuring it remains at the absolute beginning of Flash.
VMA vs. LMA (Virtual vs. Load Memory Address)
The most complex job of the linker script is handling the .data section. Initialized variables (like
an array of sensor calibrations) must live in RAM to be modified. However, RAM is volatile and
is erased when the payload loses power.
To solve this, the linker uses dual addressing:
Code snippet
.data : {
_sdata = .; /* Mark VMA start */
*(.data)
_edata = .; /* Mark VMA end */
} > RAM AT > FLASH
● > RAM (VMA): The Virtual Memory Address. This is where the code expects the data to
be during execution (e.g., 0x20000000).
● AT > FLASH (LMA): The Load Memory Address. This is where the initial values are
physically burned into the non-volatile ROM.
● Symbols (_sdata, _edata): The . (dot) represents the current memory address. By
assigning _sdata = ., the linker creates a symbol that your assembly code can use to find
the data.
The Startup Copy Loop
Because the hardware does not magically move data from Flash to RAM on startup, the
programmer must write a loop at the beginning of the Reset_Handler to perform the copy
manually using the symbols generated by [Link]:
Code snippet
LDR R0, =_sidata /* Source: Start of Data in FLASH (LMA) */
LDR R1, =_sdata /* Destination: Start of Data in RAM (VMA) */
LDR R2, =_edata /* End: End of Data in RAM */
copy_loop:
CMP R1, R2
BGE main_code
LDR R3, [R0], #4 /* Read from Flash */
STR R3, [R1], #4 /* Write to RAM */
B copy_loop
Section 4: The Register File, APSR, and Instruction
Encoding
The Register File
The Cortex-M4 contains 16 core registers (R0 through R15), each 32 bits wide:
● Low Registers (R0 - R7): Accessible by all 16-bit instructions. Using these reduces
binary size.
● High Registers (R8 - R12): Typically require 32-bit instructions.
● Special Registers: R13 is the Stack Pointer (SP), R14 is the Link Register (LR) for
subroutine returns, and R15 is the Program Counter (PC).
The APSR and Condition Flags
The Application Program Status Register (APSR) holds the CPU's condition flags. When an
instruction with an S suffix is executed (e.g., SUBS), the ALU updates four flags:
● N (Negative): Result was < 0.
● Z (Zero): Result was exactly 0.
● C (Carry): Unsigned overflow.
● V (Overflow): Signed overflow.
All conditional branches (BEQ, BGT) rely strictly on these bits.
Decoding Instruction Length and the Modified Immediate
The hardware fetch unit determines instruction length by reading the top 5 bits of the first
fetched half-word. If they are 11101, 11110, or 11111, it is a 32-bit instruction; otherwise, it is
16-bit.
When writing a 32-bit instruction, you cannot fit a 32-bit constant, a register, and an opcode into
32 bits. ARM uses the Modified Immediate value: an 8-bit base value and a 4-bit rotation
value. The hardware barrel shifter rotates the 8-bit value to the correct 32-bit position on the fly
(e.g., allowing #0xFF000000).
Section 5: Advanced Addressing Modes
Addressing modes define how an instruction finds its operands. Mastering these is essential for
efficient memory and sensor buffer manipulation.
1. Immediate & Register Addressing: Data is built into the instruction or already in a
register. (e.g., MOV R0, #25, ADD R0, R1, R2).
2. PC-Relative Addressing (The Literal Pool): If a constant (like 0x20000000) cannot be
generated by the Modified Immediate barrel shifter, MOV fails. You must use LDR R0,
=0x20000000. The assembler places the 32-bit constant in Flash memory (the Literal
Pool) and calculates the offset from the Program Counter to load it.
3. Offset Addressing: Reads from a memory address calculated by adding a base register
and an offset. The base register is unmodified. (LDR R1, [R0, #4]).
4. Pre-Indexed Addressing: The base register is updated with the offset before the
memory access. (LDR R1, [R0, #4]!).
5. Post-Indexed Addressing: Memory is accessed at the current base address, and then
the base is updated. (LDR R1, [R0], #4). This combines an array read and a pointer
increment into a single clock cycle.
Section 6: Control Flow, Pipeline Optimization, and DSP
For embedded space systems, execution timing must be highly deterministic. Traditional
branching clears the processor's instruction pipeline, which wastes 2 to 3 clock cycles while the
CPU fetches the new instruction path.
The IT (If-Then) Block
The Cortex-M4 mitigates branch penalties using the IT block, which makes up to four
subsequent instructions conditional without requiring a branch instruction.
Code snippet
CMP R0, R1 /* Compare values */
ITE GT /* If-Then-Else (Greater Than) */
MOVGT R2, R0 /* THEN: R2 = R0 (executes if R0 > R1) */
MOVLE R2, R1 /* ELSE: R2 = R1 (executes if R0 <= R1) */
Saturating Math (DSP Extensions)
When aggregating raw sensor data, standard arithmetic is dangerous. If a 32-bit accumulator
reaches 0x7FFFFFFF and 1 is added, standard math wraps around to 0x80000000 (a massive
negative number). In a closed-loop control system, this sign inversion is catastrophic.
The Cortex-M4 provides Saturating Arithmetic:
● SSAT (Signed Saturate): Clamps a value to a specified bit-depth limit. If the value
exceeds the limit, it stays at the maximum positive (or negative) value instead of
overflowing.
● USAT (Unsigned Saturate): Clamps a value at zero and a maximum positive limit.
By utilizing saturating math natively in the ALU, developers eliminate the need for extensive
software limit-checking, ensuring robust payload stability.