tin
Build deterministic embedded software from bounded components
tin is a C++20 runtime for software whose event flow, memory use, timing, and hardware boundaries need to stay explicit. It brings together fixed-capacity channels, caller-owned actors, cooperative tasks and timers, portable I/O contracts, target profiles, and hierarchical state machines.
Use the parts your system needs. A driver can hand samples to a channel, actors can compose a subsystem, a coroutine can express a timed workflow, and an HSM can govern behavior with distinct operating modes. The application keeps ownership of execution throughout.
Current release posture
- Header-only C++20 framework with CMake and pkg-config surfaces.
- Quality runs cover host C++20, GCC 16/C++26, examples, tooling, static analysis, and trace checks.
- API documentation is built from the public header tree and product guides.
- Release label: 1.5.9.
Bounded communication
Fixed-capacity queues and typed channels move values between components with explicit storage and overflow behavior.
Caller-owned actors
Typed ports, links, and actor groups compose local components while the application decides when and where they run.
Cooperative workflows
Static coroutine tasks express waits, sleeps, retries, and event handoff without framework-owned threads or callback chains.
Portable I/O
Small C++ contracts keep vendor HAL calls and board details at the boundary, so application components remain testable on a host.
Visible resource use
Resource manifests account for queue storage, timer slots, coroutine frames, and other bounded runtime capacity before deployment.
Explicit product behavior
Hierarchical state machines make operating modes, typed events, guards, actions, transitions, and history reviewable in one behavior model.
Runtime composition
Connect the hardware boundary to product behavior
This example uses the runtime as one continuous path. A driver places a sample in bounded storage, a coroutine translates it into a product event, and an HSM decides how the motor drive changes mode. Systems that do not need operating modes can stop at the channel, actor, or task.
// Hardware boundary: deterministic driver handoff
inline constexpr std::size_t voltage_sample_capacity = 8U;
inline constexpr std::uint16_t overvoltage_limit_millivolts = 3000U;
inline constexpr std::uint16_t test_voltage_millivolts = 3300U;
struct SensorSample {
std::uint16_t millivolts{};
};
tin::channel<SensorSample, voltage_sample_capacity> voltage_samples;
void poll_voltage_driver(VoltageDriver& driver) {
SensorSample sample{};
if (driver.try_read(sample)) {
(void)voltage_samples.try_send(sample);
}
}
// Coroutine bridge: raw input becomes a typed product event
struct VoltageMonitorTask {
template<typename Runtime>
tsm::task operator()(tsm::task_context&,
Runtime& runtime,
std::size_t) const {
while (true) {
const auto sample =
co_await runtime.context().voltage_samples.receive();
if (sample.millivolts > overvoltage_limit_millivolts) {
co_await tsm::send<
typename Runtime::definition::OvervoltageFault>(
runtime,
sample.millivolts,
overvoltage_limit_millivolts);
}
}
}
};
// Behavior model: test the same state machine on a host PC
struct MotorDrive {
struct Booting {};
struct Running {};
struct SafeHold {};
struct BootComplete {};
// Events can carry payloads. Guards and actions can inspect this data.
struct OvervoltageFault {
std::uint16_t measured_millivolts{};
std::uint16_t limit_millivolts{};
};
using transitions = tsm::Ts<
tsm::T<Booting, BootComplete, Running>,
tsm::T<Running, OvervoltageFault, SafeHold>>;
};
tsm::hsm<MotorDrive> drive{};
drive.handle(MotorDrive::BootComplete{});
drive.handle(MotorDrive::OvervoltageFault{
.measured_millivolts = test_voltage_millivolts,
.limit_millivolts = overvoltage_limit_millivolts,
});
Hierarchical state machines
Use an HSM when the product has meaningful operating modes
The state-machine API is one layer of tin, not a separate execution environment. It consumes the same typed events produced by channels, actors, coroutine tasks, timers, and I/O adapters. The surrounding runtime remains useful when a component does not need a state machine.
Typed transitions
States and events are C++ types; guards and actions stay next to the behavior they govern.
Hierarchy and history
Nested modes, orthogonal regions, and resume behavior model products without a single giant switch.
Runtime integration
Direct, queued, and timed dispatch policies connect an HSM to the rest of the application.
Performance
Measured on an x86-64 Linux host
These numbers are from the checked-in benchmark harness running
10,000,000 iterations on an x86-64 Linux host. The benchmark uses
the system C++ compiler in C++20 mode with -O3 and
-DNDEBUG. HSM runtime dispatch and transition behavior are
reported separately. The current harness does not use these numbers
as substitutes for channel, actor, coroutine, or I/O measurements.
Separate size checks keep the combined benchmark artifact distinct
from smaller example and target-style artifacts.
Hardware, compiler, cache state, and branch predictor history affect nanosecond-level results. Treat these as host measurements for this build, not target-board guarantees.
Benchmark artifact build time.
Non-sanitized sample app text segments measured with size.
Queues, channels, timers, and task frames stay explicit.
HSM runtime dispatch
Hierarchical state-machine behavior
The combined benchmark harness measured 103 KB of text because it links every benchmark scenario. Cortex-M HSM-only object checks measured 126-210 bytes of text before HAL, startup, and application code are added.
State-machine tooling
Export behavior before hardware is ready
The same workflow can start as reviewable YAML, export a Python simulator for tests and notebooks, and later export C++ or pybind11 scaffolds. Production execution remains C++.
# Export a Python simulator from machine YAML
python3 tools/tsm_tool.py export motor_drive.machine.yaml \
--format python \
--cpp-out build/motor_drive_model.cpp \
--shared-out build/libmotor_drive_model.so \
-o build/motor_drive_model.py
// build/motor_drive_model.cpp
// Compiled into build/libmotor_drive_model.so.
extern "C" MotorDriveHandle* motor_drive_create();
extern "C" bool motor_drive_send(
MotorDriveHandle*, MotorDriveEvent);
extern "C" MotorDriveSnapshot motor_drive_snapshot(
MotorDriveHandle const*);
from motor_drive_model import EventKind, MotorDriveModel
# motor_drive_model.py loads the exported
# build/libmotor_drive_model.so shared object.
# Python drives the same compiled C++ model.
model = MotorDriveModel()
assert model.send(EventKind.BootComplete)
assert model.send(EventKind.OvervoltageFault)
assert model.snapshot().active == "SafeHold"
schema: verified-tsm.machine.v1
machine:
initial: Booting
# Keep ADC and ISR details outside the behavior model.
transitions: [
{Booting, BootComplete, Running},
{Running, OvervoltageFault, SafeHold}
]
States and events are inferred from the transition table. YAML comments are ignored when the model is exported.
Actors + HSM
A caller-owned C++ loop with actors at the boundary
Keep vendor HAL details in one facade, then route samples through small C++ actors. The application owns the stepping policy, so the same behavior can run in a bare-metal loop, an RTOS task, Zephyr, or a host test without rewriting the state machine.
Hardware IRQs may wake the loop or update driver state, but tin code does not depend on HAL callback control flow.
// motor_app.cpp
#include "tin.h"
namespace {
inline constexpr std::uint16_t overcurrent_limit_milliamps = 3000U;
inline constexpr std::uint32_t running_pwm_duty = 420U;
inline constexpr std::uint32_t disabled_pwm_duty = 0U;
inline constexpr std::size_t one_pending_event = 1U;
inline constexpr std::size_t no_pending_events = 0U;
struct CurrentSample {
std::uint16_t milliamps{};
};
struct MotorDrive {
struct Booting {};
struct Running {};
struct SafeHold {};
struct BootComplete {};
struct CurrentOk {};
struct OverCurrent {};
struct Reset {};
using transitions = tin::tsm::Ts<
tin::tsm::T<Booting, BootComplete, Running>,
tin::tsm::T<Running, OverCurrent, SafeHold>,
tin::tsm::T<SafeHold, Reset, Booting>>;
};
tin::tsm::hsm<MotorDrive> drive;
struct MotorHardware {
void start_pwm();
void start_current_sampling();
bool read_current(CurrentSample& out);
void set_pwm_duty(std::uint32_t duty);
};
struct CurrentActor {
using event_type = CurrentSample;
explicit CurrentActor(MotorHardware& io) : io_(&io) {}
bool step() {
has_pending_ = io_->read_current(pending_);
return has_pending_;
}
bool try_receive(CurrentSample& out) {
if (!has_pending_) {
return false;
}
out = pending_;
has_pending_ = false;
return true;
}
bool empty() const { return !has_pending_; }
std::size_t pending_events() const {
return has_pending_ ? one_pending_event : no_pending_events;
}
MotorHardware* io_;
CurrentSample pending_{};
bool has_pending_{};
};
struct DriveActor {
bool send_event(CurrentSample sample) {
if (sample.milliamps > overcurrent_limit_milliamps) {
drive.handle(MotorDrive::OverCurrent{});
} else {
drive.handle(MotorDrive::CurrentOk{});
}
return true;
}
bool step() { return false; }
bool empty() const { return true; }
std::size_t pending_events() const { return no_pending_events; }
};
MotorHardware hw;
CurrentActor current{ hw };
DriveActor controller;
tin::actor_link current_to_controller{ current, controller };
tin::actor_group actors{ current, current_to_controller, controller };
} // namespace
void motor_init() {
hw.start_pwm();
hw.start_current_sampling();
drive.handle(MotorDrive::BootComplete{});
}
void motor_step() {
actors.drain();
if (drive.active<MotorDrive::Running>()) {
hw.set_pwm_duty(running_pwm_duty);
} else {
hw.set_pwm_duty(disabled_pwm_duty);
}
}
Feature matrix
What tin gives embedded teams
tin + thal
Keep the state machine independent of board wiring
thal supplies an object that satisfies the portable
tin::io::gpio_input contract. The adapter translates that
signal into a typed event, and the tin state machine decides what the
product should do. Host tests can substitute another input object
without changing the transition model.
#include "tin/io.h"
#include "tin/runtime.h"
struct MotorControl {
struct Running {};
struct SafeHold {};
struct GateFaultDetected {};
using transitions = tsm::Ts<
tsm::T<Running, GateFaultDetected, SafeHold>>;
};
template<tin::io::gpio_input GateFault>
void poll_gate_fault(GateFault& gate_fault,
tin::io::level asserted_level,
tsm::hsm<MotorControl>& motor)
{
if (gate_fault.read() == asserted_level) {
motor.handle(MotorControl::GateFaultDetected{});
}
}
Read the API reference
The hosted reference includes the package guides, public headers, examples, actor and coroutine pages, runtime policies, and class/function indexes.