Tinverse logo Tinverse LLC

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.

Commercial licensing To evaluate or license tin for company use, contact sales@tinverse.com with a short company profile, target platforms, and intended deployment context.

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.

Driver actor tin::channel actor/task tsm::hsm
// 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.

3.29s Compile

Benchmark artifact build time.

7-24 KB Host examples

Non-sanitized sample app text segments measured with size.

Fixed Storage

Queues, channels, timers, and task frames stay explicit.

Measured path Mean p99
Flat two-state toggle dispatch 0.80 ns/op 0.91 ns/op
Hierarchical sibling transition 22.28 ns/op 19.50 ns/op
Shallow history pause/resume 22.78 ns/op 19.10 ns/op
Deep history pause/resume 23.86 ns/op 21.15 ns/op
Multi-level workflow, runtime sequences 26.98 ns/op 27.22 ns/op
Multi-level workflow, precomputed sequences 12.03 ns/op 12.36 ns/op
Parent-sourced runtime transition 26.26 ns/op 28.43 ns/op
Parent-sourced precomputed transition 6.21 ns/op 5.78 ns/op
Runtime direct send 1.00 ns/op 1.84 ns/op
Runtime enqueue plus step 8.23 ns/op 8.66 ns/op

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

Capability What tin gives you Why it matters
Typed HSMs States, events, guards, actions, hierarchy, and transitions as C++ types. Invalid behavior is easier to spot in review and easier to test on a host.
Fixed-capacity channels Bounded value movement with explicit overflow behavior and ISR-shaped send paths. Interrupts can hand off work without blocking, allocation, or unbounded queues.
Coroutine tasks Static cooperative workflows for waits, sleeps, retries, and event emission. Sequential logic stays readable without framework-owned threads or callback chains.
Actor composition Typed ports, actor links, and caller-owned stepping for local components. Subsystems compose deterministically while preserving backpressure visibility.
Target profiles Linux, bare-metal ARM, FreeRTOS ARM, Zephyr ARM, and QNX-oriented boundaries. Platform assumptions stay explicit at integration points instead of leaking into behavior.
Target API smoke Profile compile checks cover bare-metal, FreeRTOS, Zephyr, and QNX APIs; FreeRTOS also has an STM32 QEMU firmware path. Adapter drift is caught before board bring-up, while full kernel simulation remains opt-in per target workspace.
I/O facades Portable contracts for driver-shaped adapters and HAL boundaries. Business logic can run against test doubles, simulators, or real board code.
Resource manifests Reviewable accounting for bounded queues, timers, task frames, and heap use. Deployment constraints become visible artifacts instead of tribal knowledge.
API reference Hosted API docs, package guides, examples, runtime policies, and indexes. Teams can evaluate the public surface before committing to integration work.

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.

API reference