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.
The interrupt handler is the only writer of
interval_ms, valid and next_slot. The foreground function takes an interrupt-safe ring-buffer snapshot inside a short critical section, copying the count and exactly that many valid entries before restoring the previous mask. It performs the comparatively expensive summation and division after interrupts are restored. A new pulse then affects the next query, while the current query consistently describes the earlier snapshot.Without exclusion, the foreground could read an old entry, be interrupted while the buffer is updated, then read new entries or an inconsistent count. Even indivisible 32-bit loads would not make that multi-element snapshot atomic. The volatile-qualified objects in C ensure that the compiler performs required shared or device accesses;
volatile does not provide mutual exclusion or a coherent multiword snapshot.[ref]C11 committee draft N1570. Sections 5.1.2.3 and 6.7.3, observable volatile accesses and type qualifiers
This design assumes a single-core foreground/interrupt execution model: no other interrupt, DMA engine or core modifies the protected buffer. The mask hooks must prevent compiler reordering across their boundaries and really mask the GPIO interrupt. Restoring the saved mask also makes the function safe to call when interrupts were already disabled. Keep the critical section short enough that the event register does not coalesce multiple heartbeats while interrupts are masked; otherwise the hardware interface cannot reconstruct events it never recorded.
A 32-bit free-running timer operates modulo . If its previous reading is , and the actual elapsed count is with , the new reading is . The modulo-counter elapsed-time identity therefore givesUnsigned integer arithmetic in C reduces the subtraction modulo , so the identity remains valid when a wrap occurs between pulses. For example, readings and give 16,000 ticks, hence one millisecond, rather than a negative interval.[ref]
C11 committee draft N1570. Section 6.2.5 paragraph 9, unsigned arithmetic modulo one greater than the maximum representable value
At the specified clock rate, the true wrap period is seconds. The printed 256 seconds is inconsistent with the other two specifications. The implementation uses the stated 32-bit width and 16 MHz rate. Assuming consecutive handled pulses are less than 256 seconds apart is a sufficient conservative bound; the exact allowable bound is less than 268.435456 seconds.
This calculation requires less than one entire timer modulus of elapsed ticks between successive stored timestamps. If a whole wrap or several wraps elapse, identical register readings cannot reveal how many occurred; an overflow counter or an independent longer-range clock would be needed. Also assume pulse events are not lost and interrupt latency is constant or sufficiently small for the required accuracy: timestamps read in software measure handler-to-handler intervals, not perfect hardware capture times. Conversion to milliseconds occurs after modular subtraction, so separately rounded absolute timestamps cannot corrupt the wrap calculation.
Articles by others on the same topic
There are currently no matching articles.