Memory Access Notes
March 12, 2025 ยท View on GitHub
Endianness
Nominally, the architecture supports little-endian and big-endian modes; any single CPU that implements the architecture may use either mode, and may optionally be able to support both with some implementation-defined method to switch between the two.
In practice, most ARM Cortex-M0+ and M4 are used in little-endian mode, and hand-written assembly commonly assumes little-endian.
ARM Cortex-M0+
The ARM Cortex-M0+ supports 8-bit, 16-bit and 32-bit accesses. Each memory access (read or write) is 2 cycles. All accesses must be aligned (i.e. accesses to 16-bit slots must be at even addresses; accesses to 32-bit slots must be at addresses multiple of 4).
The "load multiple" (ldm, pop) and "store multiple" (stm, push)
opcodes can read or write N words in N+1 cycles, which is better; they
are also size-efficient since a single 16-bit instruction can be used to
load or store multiple registers. In ARMv6-M, ldm and stm
necessarily increment the used index register (unless it is also used as
destination in ldm). ldm and stm can only load and store the low
registers (r0 to r7).
push and pop use the stack pointer (register r13, also known as
sp). push can save r14 (lr); pop can restore r15 (pc). A
function that needs to call another function, or that needs r14 as a
temporary register, will typically push lr on entry, and pop it back
into pc when exiting. If lr does not need to be saved, then it is
faster to not push it on entry, and to use bx lr to return.
Loads and store for a word (ldr, str) can use as index:
-
a general-purpose low register (
r0tor7) with a small immediate non-negative offset (0 to 124 bytes, necessarily a multiple of 4); -
the stack pointer (
sp) or the program counter (pc) with a small non-negative offset (0 to 1020 bytes, necessarily a multiple of 4); -
the sum of two arbitrary low registers.
Since the immediate offset is non-negative, PC-relative accesses used for constants require that the loaded value is located after the code that loads it, not before. The range is relatively small; thus, these constants are usually immediately after the function that uses them.
For accesses to single bytes (ldrb, strb) and to halfwords (ldrh,
strh), addressing modes using sp or pc are not available. These
accesses are unsigned, i.e. loading a byte or halfword fills the upper
bits of the destination register with zeros. Sign-extending loads of
bytes (ldrsb) and of halfwords (ldrsh) are available, but they
support only the "sum of two low registers" syntax. Sign extension can
otherwise be achieved with extra opcodes (sxtb, sxth), but of course
with the corresponding overhead (1 extra instruction and 1 clock cycle).
ARM Cortex-M4
The ARM Cortex-M4 supports all that the M0+ supports, but has also more options:
-
Unaligned accesses (for a single-word or single-halfword) are tolerated. Penalty is 1 cycle for a misaligned halfword (16-bit chunk at an odd address). Penalty is 1 or 2 cycles for a misaligned word (32-bit word at address equal to 2 mod 4 is 1-cycle penalty; penalty is 2 cycles if the address is odd). Opcodes that load or store multiple words must still do only aligned accesses.
-
Addressing modes for
ldr,ldrb,ldrh,ldrsbandldrshare extended and harmonized:-
all registers can be used, not just low registers;
-
immediate offsets have a larger range and can be negative;
-
when an immediate value is added to an index register, that register value can be updated as well, either before (pre-indexed) or after (post-indexed) the access;
-
when two register values are added to compute the address, the second register can optionally be multiplied by 2, 4 or 8.
-
-
The
ldm,stm,pushandpopopcodes can use all registers, not just the low registers. Theldmdbandstmdbopcodes work likeldmandstmbut use pre-decrement instead of post-increment for the index register (i.e. they load/store values at descending addresses). -
The
ldrdandstrdcan access two words at consecutive addresses, with arbitrary registers as destination or source (whereasldmandstmuse registers in increasing order only). These opcodes can use addressing with immediate offsets, including the variants that update the index register before or after the access. However, the "sum of two registers" addressing mode is not available forldrdandstrd.
Memory access timings are more complicated than in the M0+:
-
In general, a load (
ldr) is 2 cycles, but successive loads will be pipelined, so that a sequence of Nldropcodes will use 1+N cycles, provided that all loads are aligned and none of the loads uses for its address calculation the destination register of the immediately previous load. -
A lone store (
str) is 1 cycle, but if the next opcode performs a memory access then an extra 1-cycle penalty is applied. This is because the write uses an asynchronous buffer, which can operate in parallel with purely computational operations, but not with other memory accesses. -
ldrdandstrdare 3 cycles each. They do not pipeline with other instructions. If two words must be written at successive addresses, it is often preferable to use twostropcodes, separated by a computational instruction, so that the writes can be done with a cost of 2 cycles in total. -
There can be contention with the instruction fetch unit. The exact conditions are not documented and may depend on how the CPU core is integrated by the microcontroller vendor. Experimentally, extra penalties appear in routines that perform memory accesses and in which instructions are not aligned. This means that in a time-critical routine which mixes computations and memory accesses, then all instructions that use a 32-bit encoding should appear only at addresses which are multiple of 4. In practice, this means:
-
the function should be 32-bit aligned (
.align 2directive); -
if instructions with 16-bit encoding are used, then they should be paired together;
-
use of explicit
.nand.winstruction suffixes can help in ensuring that the instruction encoded size is as expected.
-