Device register 2026-10-24
A device register exposes a device's status, control or data to software. Its access semantics can include side effects: a status flag may need an explicit acknowledgement operation before another event is detectable. A device register is not necessarily a processor register. Memory-mapped input-output input-output is one way to access it.
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.
Give the hardware to a single device driver process. Other processes request a conversion through message passing and never touch its memory-mapped input-output registers. The following is protocol pseudocode for Phōs: its operation names specify their required semantics, rather than asserting an unverified spelling of a particular 2019 C interface. The hardware actions and the request/reply design are fully specified.
Use a FIFO queue of accepted requests, each containing its sender and a per-call token. Bind the device's interrupt to a persistent notification addressed to the driver before any conversion is started. A notification must remain pending if it arrives before the driver next waits; this is the lost-wakeup avoidance requirement. The driver receives both client requests and interrupt notifications in its event loop.
configure sensor and connect its interrupt notification to this process
TEMP_DATARDY := 0
clear stale pending interrupt notification
enable sensor interrupt and interrupt-controller delivery
pending := empty FIFO
active := none
forever:
if active is none and pending is not empty:
active := remove oldest request from pending
TEMP_DATARDY := 0
clear stale pending interrupt notification
TEMP_START := 1
event := wait for a request or persistent interrupt notification
if event is REQUEST(sender, token):
append (sender, token) to pending
if event is sensor interrupt and active is not none:
if TEMP_DATARDY != 0:
value := TEMP_TEMP
TEMP_DATARDY := 0
acknowledge the interrupt through the OS interrupt mechanism
send RESULT(active.token, value) to active.sender
active := noneAfter receiving an idle-time request, the next loop iteration starts its conversion before waiting again. While a conversion is in progress, additional requests join the queue without starting another conversion. A completed reading is copied before its ready flag is cleared, and the next request cannot start until the current result has been delivered. Spurious or stale notifications do not complete a request unless the device's ready register confirms completion.
Clearing old status and notification precedes
TEMP_START; doing so afterwards could erase a very fast completion. The operating system interrupt binding and persistent notification cover completion in the interval between starting the device and entering the wait. Register accesses must use the platform's device-access semantics, including required volatile-qualified objects in C or ordering primitives. Exact initialization and interrupt-connection call names remain an implementation binding, not invented Phōs API calls. The later teaching site's named system is documented separately; it is not used as proof of an identical historical API.[ref]Digital Systems course archive of the University of Oxford. 2018–2019 course record and linked teaching-site description of process/message-based device drivers
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.