Threading and RTOS¶
How hf-core locks, waits and runs tasks without naming an RTOS: the selection, the primitives and what they do on the host.
The RTOS is chosen once¶
HF_CORE_RTOS picks the backend at configure time and defines HF_RTOS_<NAME>=1. Code above the backends uses
OsAbstraction.h / OsUtility.h (os_delay_msec(), os_time_get(), the OS_* types) and the C++ classes of
hf-utils-rtos-wrap, never FreeRTOS headers.
HF_CORE_RTOS |
Backend header | HF_THREAD_SAFE |
Used by |
|---|---|---|---|
FREERTOS |
os/os_freertos.h |
1 | ESP-IDF builds, both board HALs |
THREADX |
os/os_threadx.h |
1 | Selectable; no consumer yet |
NONE |
os/os_none.h |
not defined | The host build; bare-metal bring-up |
The rtos wrapper also has a CMSIS-RTOS header (os/os_cmsis.h, HF_RTOS_CMSIS); the build settings do not offer
it yet.
The primitives¶
| Class | Header | Use it for |
|---|---|---|
RtosMutex |
RtosMutex.h |
The handler lock: recursive, lock(), try_lock_for(ms), unlock() |
MutexLockGuard |
RtosMutex.h |
RAII lock on an RtosMutex (RtosUniqueLock<RtosMutex>), optional timeout, IsLocked() |
BaseThread |
BaseThread.h |
A task with Setup(), Step() (returns the delay to the next step) and Cleanup() |
PeriodicTimer |
PeriodicTimer.h |
A software timer calling a function |
OsQueue, OsEventFlags, SignalSemaphore |
same names | Passing items, flags and signals between tasks |
CriticalGuard |
CriticalGuard.h |
A short critical section |
handler_utils::DelayMs(), DelayUs() |
handlers/common/HandlerCommon.h |
Delays inside handlers, on any platform |
HardFOC runs FreeRTOS at 1 kHz, so a millisecond delay or timeout is one tick. On a slower tick a non-zero
millisecond value still waits at least one tick, never zero: os_convert_msec_to_delay_ticks() in hf-utils-rtos-wrap
and handler_utils::DelayMs() round the same way, and DelayUs() rounds a wait of 1 ms or more up to whole
milliseconds.
A class guarding its own state:
class Counter {
public:
void Add(uint32_t n) noexcept {
MutexLockGuard lock(mutex_); // released on every return path
total_ += n;
}
uint32_t Total() noexcept {
MutexLockGuard lock(mutex_, 10); // wait at most 10 ms
return lock.IsLocked() ? total_ : 0;
}
private:
RtosMutex mutex_; // recursive: a locked method may call another one
uint32_t total_{0};
};
A task that owns a sensor read:
class FlowTask : public BaseThread {
public:
explicit FlowTask(SfmHandler& meter) noexcept : BaseThread("flow"), meter_(meter) {}
protected:
bool Initialize() noexcept override { // creates the task on a static stack
return CreateBaseThread(stack_, sizeof(stack_), 5, 5, 0, OS_AUTO_START);
}
bool ResetVariables() noexcept override { samples_ = 0; return true; }
bool Setup() noexcept override { return meter_.EnsureInitialized(); }
uint32_t Step() noexcept override {
if (meter_.ReadMeasurement()) {
++samples_;
}
return 100; // ms until the next Step()
}
bool Cleanup() noexcept override { return true; }
private:
SfmHandler& meter_;
uint32_t samples_{0};
uint8_t stack_[4096]{};
};
// Start it from the code that owns the handler:
// FlowTask task(meter);
// task.EnsureInitialized() && task.StartThreadAndWaitToVerify(1000);
Mutexes inside the interface wrap¶
hf-internal-interface-wrap cannot depend on the RTOS wrapper, so its PlatformMutex defaults to a no-op
backend. hf-core supplies the real one: handlers/common/PlatformMutexBackend.h is on the include path, and
PlatformMutex.h picks it up with __has_include. With HF_RTOS_FREERTOS it maps to a FreeRTOS mutex; with
HF_RTOS_NONE it stays a no-op. A standalone build of the interface wrap therefore has no locking.
On the host¶
With HF_RTOS_NONE there is no scheduler and no tick. The host build defines HF_RTOS_NONE_EXTERNAL_CLOCK and
routes every clock read and delay to hf::host::FakeClock: a delay advances the clock and returns, and each
clock read advances it by 10 µs, so polling loops reach their timeouts. BaseThread runs Setup() inline in
StartThreadAndWaitToVerify() and the test calls Step() itself. RtosMutex locks and unlocks but never
blocks. Details: Host build and mocks.
Next: MCU boundary.