Memory-mapped input-output 2026-10-24
Memory-mapped input-output exposes device registers through addresses accessed by load and store operations. A target-specific C programming language port maps the documented registers to suitably qualified objects and implements any required access barriers. Register layout, access width and acknowledgment semantics belong to the device specification; a register's name alone does not establish its numerical address.
Use the AAPCS32 convention:
r0 holds the input address, r1 the length, and r0 receives the result. The following Thumb instruction set assembly language uses only scratch processor registers, so no callee-saved registers need saving. Assume the array is valid, halfword-aligned, and has nonnegative length; for the mathematical sum assume it fits a signed 32-bit two's complement integer..syntax unified
.thumb
.thumb_func
sum16:
movs r2, #0 @ accumulated sum
cmp r1, #0
ble .Ldone @ empty array; also returns 0 if n < 0
.Lloop:
movs r3, #0 @ Thumb signed load needs a register offset
ldrsh r3, [r0, r3] @ signed halfword at a; offset read before r3 changes
adds r2, r2, r3 @ include this signed value
adds r0, r0, #2 @ advance by one short, not one byte
subs r1, r1, #1 @ one fewer element remains
bne .Lloop
.Ldone:
movs r0, r2 @ result register
bx lr @ leaf routine: lr was not overwrittenAfter iterations, the loop invariant is that
r2 contains the first elements' sum, r0 points to element , and r1 is . The sign extension in ldrsh supplies the correct signed addend. One iteration preserves this loop invariant, and the decreasing remaining length proves algorithm termination. For the seven-element calls, the sum lies between and , so it fits. For longer arrays that overflow, these instructions compute the sum modulo ; that is not a claim about defined signed C programming language overflow behavior. The operand forms are those of the paper's reference sheet.[ref]Thumb 16-bit instruction set quick reference by Arm. Load: signed halfword; Add/Subtract; Branch
Keep
total in the callee-saved register r4, and the first returned sum in the callee-saved register r5. Suppose the surrounding function has made an aligned stack frame with the local seven-halfword array at the current sp, occupying fourteen bytes in a sixteen-byte slot. One possible frame setup is push {r4,r5,r6,lr} followed by sub sp,sp,#16; the earlier code initializes total, regular, and special before this fragment. ldr r0, =regular @ assembler literal load of the global array address
movs r1, #7
bl sum16
movs r5, r0 @ first sum must survive the next call
add r0, sp, #0 @ current frame's local special array
movs r1, #7
bl sum16
muls r0, r5, r0 @ second sum times first sum
adds r4, r4, r0 @ update totalThe AAPCS32 permits each call to overwrite
r0–r3, r12, and condition flags, but requires preservation of r4–r8, r10, r11, and sp. Thus both r4 and r5 remain valid even if sum16 is replaced by another conforming implementation. bl changes the link processor register lr, so the enclosing function saves its own return address in its prologue. Its epilogue can use add sp,sp,#16 and pop {r4,r5,r6,pc}. The frame uses thirty-two bytes in total, preserving the required eight-byte stack pointer alignment at both calls.[ref]The two calls may be evaluated in this order. Assume the product and final addition fit the signed 32-bit result expected by the source program; unlike each individual seven-element sum, their product need not fit. The assembly language operations retain their low 32 bits if this assumption fails, whereas signed integer overflow in the C programming language has different semantics.
Maintain the latest five temperature readings in a circular buffer, together with their running sum. The following C programming language code spells out the message passing interface rather than assuming a particular library:
send(dst,&m) is a blocking synchronous message passing send; receive(src,&m) receives from the specified sender or ANY and returns the sender's identity; sendrec(dst,&m) atomically sends the request and waits for that sender's reply in the same message object. Successful, reliable operations are assumed. Message contents are copied by the interface.enum { ANY = -1, TIMER = 1, TEMP = 2 };
enum { PULSE, PING, REQUEST, REPLY };
typedef struct {
int type;
int period_ms;
double value;
} Message;
extern void send(int dst, const Message *m);
extern int receive(int src, Message *m);
extern void sendrec(int dst, Message *m);
void smooth(void) {
double history[5] = {0};
double sum = 0;
int count = 0, next = 0;
Message m = { .type = PULSE, .period_ms = 1000 };
send(TIMER, &m); /* subscribe once */
for (;;) {
int sender = receive(count < 5 ? TIMER : ANY, &m);
if (sender == TIMER && m.type == PING) {
Message t = { .type = REQUEST };
sendrec(TEMP, &t); /* matching REPLY contains t.value */
if (count == 5)
sum -= history[next];
else
++count;
history[next] = t.value;
sum += t.value;
next = (next + 1) % 5;
} else if (count == 5 && m.type == REQUEST) {
Message answer = { .type = REPLY, .value = sum / 5.0 };
send(sender, &answer);
}
}
}Before the fifth reading, selective reception from
TIMER prevents premature client requests from being accepted. Such clients wait at their synchronous sends; they are not given a partial arithmetic mean. Thereafter the operating-system process receives both timer messages and client requests. Each accepted PING initiates one measurement; each accepted client request returns the arithmetic mean of the five most recently completed measurements, without initiating another measurement or changing the circular buffer.The loop invariant is that
count is the number of valid readings, capped at five, sum is their sum, and next is the next unused slot during startup or the oldest slot afterwards. Removing the overwritten reading and adding the new one preserves this loop invariant. Each update and reply uses time complexity and storage, since the window length is fixed. Under timely sensor replies, timer delivery and process scheduling, measurements are initiated once per second. The next parts explain failures of these timing assumptions; the code does not promise one-second sampling when the sensor itself cannot sustain it.Use
r0 for the argument and result. With 32-bit integers and pointers, the three fields have byte offsets 0, 4 and 8. Assume an acyclic finite binary tree, no concurrent mutation, a valid aligned call stack, and no signed integer overflow in any evaluated sum. These are representation and arithmetic assumptions, rather than properties of arbitrary C programming language implementations..syntax unified
.cpu cortex-m0
.thumb
.global sum_rec
.thumb_func
sum_rec:
cmp r0, #0
beq .Lempty
push {r4, r5, r6, lr} @ 16-byte frame; r6 is alignment padding
movs r4, r0 @ retain this invocation's node
ldr r5, [r4, #0] @ running value begins with its label
ldr r0, [r4, #4]
bl sum_rec @ r0 = left subtree sum
adds r5, r5, r0
ldr r0, [r4, #8]
bl sum_rec @ r0 = right subtree sum
adds r0, r0, r5
pop {r4, r5, r6, pc} @ restore caller state and return
.Lempty:
movs r0, #0
bx lrThe node and partial sum are held in callee-saved registers
r4,r5. Each recursive invocation saves its caller's values before replacing them, so a child returns with the parent's values intact. Saving lr preserves the return address across the two bl instructions. Caller-saved registers may be overwritten by a child; no parent value is left live only in them. Pushing four words maintains eight-byte stack alignment at recursive calls.[ref]Procedure Call Standard for the Arm Architecture by ARM Limited. AAPCS32, base standard: core-register roles, stack constraints and result return
Correctness follows by structural induction. An empty subtree returns zero. For a nonempty subtree, assume both recursive calls return their respective label sums. The final addition then combines the node's label with exactly those two sums, so
r0 returns the sum of all labels. The instruction forms and field offsets fit the short load and arithmetic encodings.[ref]Thumb 16-bit instruction set quick reference by Arm. Move, Add, Load, Push/Pop and Branch tables
Use an explicit-stack depth-first tree traversal. Descend the left branch immediately and put each nonempty right subtree on a stack for later. For the same node declarations as in the paper, an original C programming language implementation is:
int sum_iter(tree t)
{
tree pending[32];
unsigned top = 0;
int total = 0;
while (t != NULL || top != 0) {
if (t == NULL) {
t = pending[--top];
} else {
total += t->data;
if (t->right != NULL)
pending[top++] = t->right;
t = t->left;
}
}
return total;
}The loop invariant says that the accumulated labels plus the sums of the current subtree and all pending subtrees equal the original tree sum, with the unvisited subtrees disjoint. Visiting a node adds its label and replaces its subtree by its two children. Popping merely changes which pending subtree is current. When both current and pending subtrees are absent, the invariant proves the result.
At most one unvisited right sibling is pending at each level of the current ancestor path. If depth means the number of nonempty nodes on a longest root-to-leaf path, depth at most 32 requires at most 31 pending right siblings. A 32-word array is therefore sufficient; it also covers the convention that depth counts at most 32 edges. The depth promise makes unchecked pushes valid here. For arbitrary input, a production implementation should check capacity rather than overwrite its frame.
A corresponding Thumb instruction set implementation keeps the total in
r4, the array base in r5, and its next-free address in r6:.global sum_iter_asm
.thumb_func
sum_iter_asm:
push {r4, r5, r6, lr}
sub sp, sp, #128 @ 32 pending pointers
mov r5, sp @ base of pending array
mov r6, sp @ empty stack: next-free equals base
movs r4, #0
.Lvisit:
cmp r0, #0
beq .Ltake_pending
ldr r1, [r0, #0]
adds r4, r4, r1 @ consume this node's label
ldr r1, [r0, #8]
cmp r1, #0
beq .Lleft
str r1, [r6, #0] @ defer the nonempty right subtree
adds r6, r6, #4
.Lleft:
ldr r0, [r0, #4] @ continue down the left branch
b .Lvisit
.Ltake_pending:
cmp r6, r5
beq .Ldone
subs r6, r6, #4
ldr r0, [r6, #0]
b .Lvisit
.Ldone:
movs r0, r4
add sp, sp, #128
pop {r4, r5, r6, pc}The stack pointer stays fixed while the array is used;
r6 is a separate cursor inside that array. All callee-saved registers touched by the function are restored. Each nonempty node contributes its label exactly once. As in part (a), require that every partial total fits the signed result type; a representable final answer alone does not prevent overflow in a different summation order.Keep successive timestamps in raw unsigned ticks, and convert their modular difference to milliseconds only after subtraction. The first pulse establishes a timestamp; it does not create a fictitious interval from startup. Store valid intervals in a five-element ring buffer. During startup, average the one to four intervals actually available; return zero until there is at least one valid interval. Here zero is explicitly an unavailable-data sentinel.
The following C programming language code uses the supplied register names through an abstract memory-mapped input-output register interface. The board port supplies the actual register mapping and interrupt vector; no undocumented addresses are assumed. The two interrupt-mask hooks have the exact semantics described below and are interface requirements, not names asserted to exist in an unspecified device library.
#include <stdint.h>
#include <stdbool.h>
struct gpio_registers { volatile uint32_t EVENT; };
struct timer_registers { volatile uint32_t COUNT; };
extern struct gpio_registers GPIO;
extern struct timer_registers TIMER;
uint32_t irq_save_disable(void);
void irq_restore(uint32_t old_mask);
static volatile uint32_t interval_ms[5];
static volatile uint32_t next_slot = 0;
static volatile uint32_t valid = 0;
static volatile uint32_t previous_tick = 0;
static volatile bool have_previous = false;
void gpio_handler(void)
{
if (GPIO.EVENT == 0)
return;
uint32_t now = TIMER.COUNT;
GPIO.EVENT = 0; /* acknowledge with the specified write */
if (have_previous) {
uint32_t ticks = now - previous_tick;
interval_ms[next_slot] = ticks / UINT32_C(16000);
next_slot = (next_slot + 1) % 5;
if (valid < 5)
++valid;
} else {
have_previous = true;
}
previous_tick = now;
}
uint32_t get_pulse(void)
{
uint32_t local[5];
uint32_t mask = irq_save_disable();
uint32_t n = valid;
for (uint32_t i = 0; i < n; ++i)
local[i] = interval_ms[i];
irq_restore(mask); /* restore the earlier mask, not just enable */
if (n == 0)
return 0;
uint64_t total = 0;
for (uint32_t i = 0; i < n; ++i)
total += local[i];
if (total == 0)
return 0;
/* Mean duration is total/n milliseconds; round bpm to nearest integer. */
return (uint32_t)((UINT64_C(60000) * n + total / 2) / total);
}Before the buffer fills, its valid entries occupy positions . Once full, all five positions are valid, so their order is irrelevant to their sum. The conversion uses 16,000 ticks per millisecond. Truncating each interval introduces less than one millisecond of error per interval. The desired conversion is beats per minute, the reciprocal of the mean interval; averaging the individual values is generally different.
irq_save_disable saves the previous interrupt mask and prevents the GPIO handler from running until irq_restore restores that mask. Both hooks must include compiler memory barriers. On an appropriate privileged Cortex-M target, the implementation can use MRS PRIMASK, CPSID i and MSR PRIMASK, with a compiler memory clobber. These target operations implement interrupt masking; they are not expressible as portable ISO C alone. One concrete GCC/Clang-compatible privileged Cortex-M port for those hooks is:uint32_t irq_save_disable(void)
{
uint32_t old_mask;
__asm__ volatile ("mrs %0, primask\n\tcpsid i"
: "=r" (old_mask) : : "memory");
return old_mask;
}
void irq_restore(uint32_t old_mask)
{
__asm__ volatile ("msr primask, %0"
: : "r" (old_mask) : "memory");
}The compiler memory clobbers prevent access motion across the mask changes. Saving and restoring the entire prior mask is intentional; an unconditional enable would break an enclosing critical section.[ref]
CMSIS-Core register access documentation by Arm. PRIMASK, __get_PRIMASK, __disable_irq and __set_PRIMASK
All state is initialized before enabling the peripheral interrupt. Assume the timer read is coherent, the handler is not reentered, and the device follows the stated write-zero acknowledgment semantics.
Unsigned integer arithmetic in C 2026-10-24
For a -bit unsigned integer type without padding, C programming language arithmetic on that type reduces results modulo . In particular, subtracting two
uint32_t timestamps gives their modular difference when that subtraction is evaluated in the 32-bit unsigned type. Integer promotions must be checked for narrower types or unusual implementations. Signed integer overflow has different rules and must not be treated as the same operation.[ref]