Generic templates¶
hf::FlagsSaver, hf::SeqlockSnapshot and hf::ErrorHistory: shared state for status flags, the latest value
of a struct and a history of errors, each with a read side and a write side.
The shape they share¶
Each template is three types in one header: a pure-virtual reader interface, a pure-virtual writer interface and a
final class that implements both. Give producers a reference to the writer and consumers a reference to the
reader; the type system then stops a consumer from writing.
| Template | Reader | Writer | Concurrency | Storage |
|---|---|---|---|---|
hf::FlagsSaver<Id, N> |
hf::FlagsReader<Id, N> |
hf::FlagsWriter<Id, N> |
Lock-free, any number of tasks | (N + 63) / 64 atomic 64-bit words |
hf::SeqlockSnapshot<T> |
hf::SnapshotReader<T> |
hf::SnapshotWriter<T> |
One writer, any number of readers, no locks | One T and a sequence counter |
hf::ErrorHistory<R, N> |
hf::ErrorHistoryReader<R> |
hf::ErrorHistoryWriter<R> |
Internal RtosMutex |
R[N] ring |
None allocates on the heap. T and R must be trivially copyable (a static_assert checks it). None is for
interrupt context.
FlagsSaver¶
A set of on / off flags named by an enum. Set() and Clear() are one atomic operation each, return false when
the flag was already in that state, and bump a sequence counter. A waiter can block until any flag changes.
#include "FlagsSaver.h"
#include "OsUtility.h"
enum class SystemFlag : uint8_t { kReady = 0, kFault, kCount };
constexpr std::size_t kSystemFlagCount = static_cast<std::size_t>(SystemFlag::kCount);
static hf::FlagsSaver<SystemFlag, kSystemFlagCount> system_flags;
// Producers get the write side, consumers the read side.
hf::FlagsWriter<SystemFlag, kSystemFlagCount>& flags_out = system_flags;
hf::FlagsReader<SystemFlag, kSystemFlagCount>& flags_in = system_flags;
void MarkReady() { flags_out.Set(SystemFlag::kReady, os_get_elapsed_time_msec()); }
bool WaitUntilReady() {
flags_in.ClearWaitEvent(); // forget changes already seen
while (!flags_in.IsSet(SystemFlag::kReady)) {
if (!flags_in.WaitForChange(/*timeout_ms=*/100)) { // consumes the change signal
return false; // nothing changed in time
}
}
return true;
}
| Member | Side | Does |
|---|---|---|
Set(id, now_ms = 0) / Clear(id, now_ms = 0) |
Writer | Change one flag; false when it already had that state |
ClearAll(now_ms = 0) |
Writer | Clear every flag |
IsSet(id) |
Reader | One atomic load |
Snapshot_(out) |
Reader | Copy of all words, the sequence number and the last change time |
Seq(), LastChangeMs() |
Reader | Change counter; the now_ms of the last change |
WaitForChange(timeout) / ClearWaitEvent() |
Reader | Block until a change is signalled; discard a pending signal |
WaitForChange() consumes the change signal when it returns true, so the next call waits for a new change.
Call ClearWaitEvent() to forget changes made before you start waiting, as the example does at the top. The
timeout is milliseconds, converted to kernel ticks like every other timeout in the library.
The event group behind the waiter is created on the first WaitForChange() or ClearWaitEvent().
SeqlockSnapshot¶
The latest value of a struct, published by one task and read by any number, without locks. The writer makes the sequence counter odd, copies the value and makes it even again; a reader copies the value and retries if the counter was odd or changed meanwhile.
#include "SeqlockSnapshot.h"
struct MotorState { // trivially copyable
float speed_rpm;
float current_a;
uint32_t timestamp_ms;
};
static hf::SeqlockSnapshot<MotorState> motor_state;
void PublishState(const MotorState& s) { motor_state.Publish(s); } // one writer
MotorState LatestState() { // any number of readers
MotorState out{};
(void)motor_state.Read(out); // returns the sequence number read
return out;
}
| Member | Side | Does |
|---|---|---|
Publish(value) |
Writer | Copy value in; one writer only |
Read(out) |
Reader | Copy a consistent value out; returns the (even) sequence number it read |
Seq() |
Reader | Current sequence: even when stable, odd during a publish |
WaitForChange(timeout) / ClearWaitEvent() |
Reader | As for FlagsSaver |
Read() costs two atomic loads and one memcpy of T when no publish is in progress, and retries while one
is. Two tasks calling Publish() at once corrupt the value: protect them with a mutex if you ever have two
writers.
ErrorHistory¶
The last N records of something (errors, events), oldest first. A full ring overwrites its oldest record and
counts the overwrite, so a reader can tell that records were lost.
#include "ErrorHistory.h"
struct ErrorRecord {
uint32_t timestamp_ms;
uint16_t code;
};
static hf::ErrorHistory<ErrorRecord, /*kCapacity=*/32> errors;
void Report(uint16_t code) {
(void)errors.Push(ErrorRecord{os_get_elapsed_time_msec(), code}); // overwrites the oldest when full
}
std::size_t CopyErrors(ErrorRecord (&out)[32]) {
return errors.Snapshot(out, 32); // oldest first
}
| Member | Side | Does |
|---|---|---|
Push(record) |
Writer | Append; overwrites the oldest when full; false if the mutex could not be taken |
Pop(out) |
Writer | Remove the oldest |
Clear() |
Writer | Remove all |
Size(), Capacity() |
Reader | Records held; N |
Snapshot(out, max) |
Reader | Copy up to max records, oldest first; returns the count |
Seq(), OverwriteCount() |
Reader | Change counter; records lost to overwrites |
Every call takes the internal RtosMutex, so all are O(1) except Snapshot(), which is O(max).
Next: Utility.