tin
Build Deterministic, Test-Driven Real-Time Systems in C++
tin is a state-machine and runtime framework that decouples hardware from business logic. Write product behavior once, then run the same C++ code on bare-metal, RTOS, Linux, and host unit tests.
It is built for embedded and real-time teams that need to replace tangled callbacks, giant switch statements, and target-specific glue with typed events, bounded handoff, and reviewable behavior.
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 generated trace checks.
- Generated documentation is built from the public header tree and product guides.
- Release label: 1.5.9.
Provably Correct Behavior
Replace nested switch statements with typed hierarchical state machines. Legal states, events, guards, and transitions become explicit C++ that reviewers can reason about.
Predictable Concurrency
Move work from interrupts to application logic through fixed-capacity channels, coroutine tasks, and deterministic actors without hidden allocation or framework-owned threads.
True Portability
Keep HAL calls, drivers, and platform wakeups at target boundaries so the behavior core stays portable across host tests, Linux simulation, RTOS targets, and bare-metal builds.
Workflow
Route hardware events into product behavior
Instead of scattering motor-drive logic across driver callbacks and RTOS tasks, tin gives the handoff one typed path. Hardware data enters bounded storage, an actor or coroutine translates raw samples into business events, and the state machine handles product behavior.
// Hardware boundary: deterministic driver handoff
struct SensorSample {
std::uint16_t millivolts{};
};
tin::channel<SensorSample, 8> 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 > 3000) {
co_await tsm::send<
typename Runtime::definition::OvervoltageFault>(
runtime,
sample.millivolts,
3000U);
}
}
}
};
// 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 = 3300U,
.limit_millivolts = 3000U,
});
Performance
Measured on the Linux host
These numbers are from the checked-in benchmark harness running
10,000,000 iterations on this Linux host with Ubuntu
/usr/bin/c++, C++20, -O3, and
-DNDEBUG. The harness measures typed HSM event dispatch,
history resume paths, runtime send/enqueue paths, binary size, and
latency percentiles. 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.
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.
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 generate 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 generated 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 by code generation.
Actor example
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 {
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_ ? 1U : 0U; }
MotorHardware* io_;
CurrentSample pending_{};
bool has_pending_{};
};
struct DriveActor {
bool send_event(CurrentSample sample) {
if (sample.milliamps > 3000U) {
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 0; }
};
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(420U);
} else {
hw.set_pwm_duty(0U);
}
}
Feature matrix
What tin gives embedded teams
Read the API reference
The hosted reference includes the package guides, public headers, examples, actor and coroutine pages, runtime policies, and generated class/function indexes.