Device register Created 2026-10-24 Updated 2026-10-31
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 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.
A store buffer holds the memory address and value of a store instruction while the memory cache or backing computer memory completes the write. The in-order pipeline can continue after enqueueing the write, rather than waiting for a possibly long memory access latency. A full store buffer eventually causes a pipeline stall; buffering hides finite delays, not an indefinitely inadequate service rate.
Without a memory management unit, the comparison can use the actual physical addresses; there is no address translation or synonym problem to solve. Suppose x initially contains zero and these operations execute in order:
STORE [x], 7
LOAD  r1, [x]
If the write is still in the store buffer, reading only computer memory could incorrectly return zero. The load instruction searches older buffered store instructions for an overlapping memory address. If the newest matching write covers the requested bytes and its data are ready, store-to-load forwarding returns seven directly. With several pending writes to x, the youngest older matching write wins. If the relevant data are not ready, the load instruction waits; partial overlaps require merging the covered bytes or conservatively waiting for the writes to drain.
A load instruction from an unrelated address y can bypass those buffered writes, subject to the architecture's memory model. Buffer writes to overlap them with useful execution, but preserve each load's dependence on older writes to the same bytes. Device accesses through memory-mapped input-output need their specified ordering and should not be treated as ordinary cacheable computer memory.
In IF, fetch PREFETCH normally. In ID, recognize its opcode, read the base processor register and immediate offset, and set controls selecting a prefetch request rather than a load instruction or store instruction. Ordinary register forwarding and data hazard interlocks still apply to that base processor register. In EX, use the arithmetic logic unit to form the effective memory address.
Latch that memory address and a prefetch-valid control into EX/MEM. In MEM, put the memory address and any hint information on the dedicated request computer bus, or enqueue them in a small prefetch-request queue. The cache prefetching engine handles lookup, allocation and the later cache line return independently. Do not wait for the fill or select the returned data as a register file input. In WB, disable architectural processor register and processor condition flag writes; the instruction simply leaves the in-order pipeline.
If the request path is busy, dropping the hint is compatible with the contract in part (ii), and preserves its nonblocking purpose. A design that instead stalls until queue space exists can still be correct but reduces the performance benefit. A memory management unit must not turn a speculative hint into an ordinary protection fault: discard an invalid target, or use an appropriately checked nonfaulting translation path. Suppress device transactions for memory-mapped input-output and avoid speculative writes. Use the existing address-generation datapath, add a prefetch request/control path, and disable demand-load waiting and writeback.
The register file in this datapath supplies both read ports combinationally for whatever read addresses the instruction fields present. Reading a processor register is harmless, and a result that the current MIPS instruction does not need can simply be ignored. Only writes require an explicit enable because writes change architectural state. There is consequently no functional need for RegRead in this design.
The data-memory read interface has an explicit MemRead enable. Non-load MIPS instructions can present irrelevant or invalid addresses at the memory address input; an external memory transaction should not be initiated merely because those bits happen to be present. An explicit enable also permits interfaces with memory-mapped input-output, where a read can have a side effect. It can reduce unnecessary activity even for ordinary memory.
This is a distinction in the supplied datapath, rather than a universal physical prohibition: other register files can have read enables, and an ideal side-effect-free memory can be modeled with continuous reads.
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 := none
After 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]
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]
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.