Tinverse logo Tinverse LLC

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.

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 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.

Driver Bounded channel Actor or task HSM
// 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.

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.

HSM runtime dispatch

Measured path Mean p99
Runtime direct send 1.00 ns/op 1.84 ns/op
Runtime enqueue plus step 8.23 ns/op 8.66 ns/op

Hierarchical state-machine behavior

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

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

Capability What tin gives you Why it matters
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.
Hierarchical state machines States, events, guards, actions, hierarchy, history, and transitions as C++ types. Operating modes are easier to review and test without coupling them to runtime execution.
API reference Hosted API docs, package guides, examples, runtime policies, and indexes. Teams can evaluate the public surface before committing to integration work.

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.

Explore the typed BSP →
#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.

API reference