tin  1.5.9
embedded_framework

tsm separates state-machine semantics from the runtime mechanisms that move events. Embedded applications should be structured around four roles:

  • drivers collect hardware-facing data and expose typed payloads;
  • runtime queues carry bounded typed events;
  • tasks adapt driver payloads, ticks, and wakeups into state-machine events;
  • HSM definitions own states, transitions, guards, actions, and context.

The immediate compatibility target is the embedded developer experience usually expected from async-first frameworks:

  1. Peripheral and driver contracts for GPIO, UART, SPI, I2C, ADC, CAN, and ticks.
  2. ISR-shaped wake APIs for flags, signals, and channels.
  3. Fixed-capacity timer services for task sleeps and delayed HSM events.
  4. Executor variants for caller-driven, IRQ-woken, host-thread, and RTOS task execution.
  5. Cooperative cancellation tokens and static task groups for supervised workflows.
  6. Embedded examples that mirror STM32, FreeRTOS, Zephyr, and bare-metal usage.
  7. Static resource accounting for queues, event payloads, task frames, groups, and timers.
  8. A documented driver/runtime/task/HSM separation pattern.

The core HSM layer remains independent of peripherals, operating systems, and wall-clock time. Platform code converts interrupts, RTOS notifications, or host events into wakeups and explicit tick counts before they reach the runtime.

Wake-aware executors expose the same scheduling surface across targets: wake(), wake_from_isr(), and wait_for_work(). On Linux this maps to eventfd and epoll; on FreeRTOS it maps to task notifications; on Zephyr it maps to a semaphore; on QNX Neutrino 7.x it maps to pulses; on bare-metal it delegates to board hooks that can use WFE, WFI, SEV, or another interrupt flag. These calls are scheduling signals only. They do not store events and do not change HSM transition semantics. Caller-driven task execution defaults to FIFO fairness. Applications that need fixed criticality ordering can use tsm::task_priority<N> on task definitions with tsm::priority_cooperative_executor; larger priority numbers run first, and equal-priority tasks retain FIFO order.

The runtime resource snapshot API returns a compile-time resource summary that examples, review tools, or board manifests can inspect without constructing a runtime. The snapshot reports queue slots, maximum event payload size, delayed-event timer slots, task count, task group count, task group entry count, task arena bytes, task timer slots, and whether heap allocation is used. tsm::write_resource_manifest(...) emits the same values as a stable JSON record for host-side build artifacts.

Tick sources are platform adapters. They expose ticks() for a monotonic integer counter, and host chrono adapters can additionally expose poll() to return elapsed ticks since the last poll. Applications pass those elapsed ticks to executor.tick(n) or delayed-event timers. HSM definitions should continue to treat time as explicit tick events or delayed typed events. tsm::runtime::drive_elapsed_ticks(source, executor, timers) is the common caller-driven shape when one elapsed tick source should advance coroutine sleeps and delayed HSM events together.

End users can still think in wall-clock terms at the platform boundary. A tick domain names the mapping:

using namespace std::chrono_literals;
constexpr auto tick_domain = tsm::chrono_ticks::one_tick_per_millisecond();
std::chrono::milliseconds>{
tick_domain
};
co_await tsm::chrono_ticks::sleep_for(tick_domain, 250ms);
Definition: chrono_ticks.h:137
constexpr millisecond_domain one_tick_per_millisecond() noexcept
Definition: chrono_ticks.h:92
tsm::sleep_ticks_awaitable sleep_for(tick_domain< TickPeriod > domain, Duration duration) noexcept
Definition: chrono_ticks.h:105

In that example, 1 runtime tick means 1 ms. A slow supervisory task could use one_tick_per_second() instead. The conversion happens before the HSM runtime sees time, so target builds still run on integer ticks.

Driver adapters in tsm::io are deliberately small. They provide contracts for peripheral facades and helpers such as sample_adc_from_isr, push_from_isr, and notify_from_isr. Those helpers move payloads into fixed runtime primitives; they do not own peripheral drivers, allocate buffers, or dispatch HSM transitions directly.

Async-shaped driver concepts extend the same facade idea. A GPIO input can provide wait_for(level), a UART can provide read_async(span) and write_async(span), and buses such as SPI, I2C, and CAN can expose async transaction methods. These contracts describe how tasks await driver work; they do not require a specific coroutine scheduler or vendor HAL.

Static task groups let an application shell supervise related task entries:

using arm_tasks = tsm::task_group<PrepareArmTask{}, ArmMonitorTask{}>;
executor.start_group(arm_tasks{});
executor.cancel_group(arm_tasks{});
Definition: coroutine.h:1073

Cancellation is cooperative. A tsm::cancellation_source creates tokens that tasks can poll or await. Requesting cancellation wakes token waiters, but task code still decides where cancellation is observed.

Target profiles make deployment assumptions explicit:

static_assert(
true));
consteval void enforce_compiler_profile()
Require the active compiler configuration to match Profile.
Definition: profile.h:120
Conservative production profile for safety-oriented embedded builds.
Definition: profile.h:95

For a safety or bare-metal profile, this check requires compiler options that disable exceptions and RTTI. Heap use is handled as a resource and review contract: core embedded headers are checked for dynamic-allocation vocabulary, and runtime resource contracts report whether a configuration uses heap-backed storage.

Application Shell Pattern

An application shell owns the assembled runtime, executor, platform facades, and high-level methods used by the rest of the program. The state-machine definition remains the behavior artifact; the shell is the operational owner.

class SensorShell {
public:
void adc_ready_from_isr();
void tick(tin::tick_count elapsed_ticks);
bool alerting() const;
private:
app_type app_{};
};
Definition: app.h:34

The shell can expose names that fit the product domain while hiding dispatch model, executor choice, and platform wake details. On Linux the shell might run behind an eventfd/epoll executor. On FreeRTOS it might wake a task with a notification. On bare metal it might drain from a superloop after an interrupt flag. The HSM definition does not change.

HAL Binding Strategy

Bind vendor HALs outside the HSM definition. The application or board-support layer should wrap STM32 HAL, LL, Zephyr device APIs, FreeRTOS drivers, Linux file descriptors, or test doubles behind small facade types that satisfy the tsm::io concepts. The HSM depends on typed driver capabilities.

A typical binding has three layers:

  • a vendor driver object that owns registers, DMA descriptors, or OS handles;
  • a narrow facade with methods such as read(), write(level), try_send(frame), set_duty(duty), or enable();
  • a task or ISR adapter that samples the facade, pushes a fixed payload into a tsm::channel, and later sends a typed HSM event.

The facade methods should return tsm::io::bus_status where the operation can fail or be busy. That keeps backpressure visible without exceptions or dynamic allocation. Payload types should be compact structs such as tsm::io::adc_sample, tsm::io::digital_input_state, tsm::io::can_frame, or application-defined equivalents.

The same state machine can then build for different targets by selecting a different facade implementation:

struct BoardButton {
tsm::io::level read() const;
};
struct TestButton {
tsm::io::level value{};
tsm::io::level read() const { return value; }
};
level
Definition: io.h:26

Both satisfy tsm::io::gpio_input. The HSM sees only the context member and the typed event produced by the service task. Target headers stay in the board or platform translation unit, while transition definitions remain portable.