Code generation
Generators of your own are in generators.md; every option of
archec gen is in commands.md.
archec gen main.arche --target <t> [-o generated] writes source code from the model. Files you own
(app.rs, main.rs, Cargo.toml) are written once and never overwritten; --check fails when a
generated file is out of date (for CI). In VS Code: Arche: Generate code….
| target | what | from |
|---|---|---|
embassy |
Rust / Embassy: one crate per binary. arche.rs (regenerated): the model's types and messages, a Channel per link (depth, overflow) or a Signal (buffer sample), an Out handle and a behaviour trait per part, one task per thread (a Ticker for period + step, select over the inputs), encode/decode and a BusFrame per routed message (CAN id = the route's id), spawn_all. hal std|stm32|rp|nrf|esp32 and chip "…" on the processor pick the HAL; hal rp with threads on two cores starts core 1. |
deployment |
dbc |
CAN DBC, one file per bus: a frame per routed message, a signal per field, in the same layout as the Embassy codec; a message with a wire format (at, bits, scale, byte-order…, see the spec) has each signal on its bits, with its start bit, byte order, sign, factor and offset |
deployment routes |
wire |
the codecs of the messages that say their wire format, with their types: wire/c/arche_wire.h/.c (C99, no heap, no libm), wire/rust/ (a no_std crate), wire/go/ (package archewire), wire/python/arche_wire.py (dataclasses). Encode puts each field on its bits, decode fails on a short frame, a wrong checksum or an unknown enum value; the four write the same bytes. --target c, embassy and go use the same code for these messages |
messages |
aadl |
AADL v2, aadl/<deployment>.aadl, one package for OSATE: data with Data_Model properties per message and type; a processor per processor (a virtual processor per core, a memory for its ram), a bus (Transmission_Time from bitrate); a virtual processor per container, bound to the cores of its cpuset, its processes bound to it; a process per process (or per binary with none) and a thread per thread with Dispatch_Protocol, Period, Deadline, Compute_Execution_Time (0 ms .. wcet, else the step's cost), Priority, Stack_Size; event data ports (a data port for buffer sample, Queue_Size from depth), a device per linked part on no thread, a port connection per link (Latency from latency), Actual_Processor_Binding to the core, Actual_Connection_Binding to the route's bus. A port carrying several messages is typed by a union of them |
deployment |
arxml |
AUTOSAR Classic ARXML (R4, AUTOSAR_00049), arxml/<deployment>.arxml: an IMPLEMENTATION-DATA-TYPE per type and message (an enum is uint8 with a TEXTTABLE COMPU-METHOD), a SENDER-RECEIVER-INTERFACE per message, an APPLICATION-SW-COMPONENT-TYPE per part with behaviour (SENSOR-ACTUATOR for sensors and hardware) with a P-/R-PORT-PROTOTYPE per port and message, RUNNABLE-ENTITYs on a TIMING-EVENT (thread period + step), DATA-RECEIVED-EVENTs (handlers, receives) and an INIT-EVENT (fn main); the system as a COMPOSITION-SW-COMPONENT-TYPE with an ASSEMBLY-SW-CONNECTOR per link and message; per message routed over a CAN bus, I-SIGNALs in the DBC's layout, an I-SIGNAL-I-PDU, a CAN-FRAME and a CAN-FRAME-TRIGGERING with the route's CAN id, with its PDU and signal triggerings (a structure also has a signal group); an ECU-INSTANCE per processor a thread runs on, with a CAN connector per bus and a frame, PDU and signal port per frame it sends or receives; the SYSTEM, with its root composition, the SWC-TO-ECU-MAPPING of each part, and each routed message's data element mapped on its signals, field by field for a structure. Validates against AUTOSAR's AUTOSAR_00049.xsd (the examples' deployments are checked with xmllint when AUTOSAR_XSD names it). Left out: send and receive points, units and data constraints, and arrays, optional fields and unions in the field-by-field mapping |
deployment |
arxml-adaptive |
AUTOSAR Adaptive ARXML (R20-11, AUTOSAR_00049), four files in adaptive/<deployment>/. Design.arxml: the data types as arxml writes them; a SERVICE-INTERFACE per interface a port provides or requires — what its provider sends as EVENTs, its operations as METHODs (their ARGUMENTs, a result as an OUT argument, FIRE-AND-FORGET without one), a message the provider only receives as a fire-and-forget METHOD, a message it both sends and receives as a FIELD with getter, setter and notifier — and one of EVENTs per Ethernet out port that sends without an interface; per process (a binary without processes: the binary) an ADAPTIVE-APPLICATION-SW-COMPONENT-TYPE with a P-/R-PORT-PROTOTYPE per service its parts provide or require, and an EXECUTABLE whose ROOT-SW-COMPONENT-PROTOTYPE it is. MachineManifest.arxml: a MACHINE-DESIGN (an ETHERNET-COMMUNICATION-CONNECTOR) and a MACHINE (its PROCESSOR and cores) per processor with an operating system that is not an RTOS or bare metal, the machine function group MachineFG (Startup, Running, Shutdown), an ETHERNET-CLUSTER with a NETWORK-ENDPOINT per machine (attr ip = "10.0.0.2" on the processor or a binary on it, else 192.168.0.10 and up). ExecutionManifest.arxml: a PROCESS per process of its EXECUTABLE, started in MachineFG.Running with a STARTUP-CONFIG — the policy (fifo, rr, else other) and priority of its highest-priority thread. ServiceInstanceManifest.arxml: a SOMEIP-SERVICE-INTERFACE-DEPLOYMENT per service with the ids of --target someip (service, version, methods, events, event group; a field's getter and setter numbered after the methods, its notifier the message's event id), a PROVIDED- or REQUIRED-SOMEIP-SERVICE-INSTANCE per port, its SERVICE-INSTANCE-TO-PORT-PROTOTYPE-MAPPING to the process and a SOMEIP-SERVICE-INSTANCE-TO-MACHINE-MAPPING per machine. A binary on a microcontroller is a Classic ECU (arxml) and is left out, and a service it provides has no provided instance; the notes say which. Left out: UDP/TCP ports and service discovery timings, E2E, security, persistency, platform health management, StdCppImplementationDataTypes. The examples' deployments are checked with xmllint against AUTOSAR_00049.xsd when AUTOSAR_XSD names it |
deployment |
bom |
bill of materials: bom/bom.csv, a row per definition (and per part with none) with its parts, kind, quantity ([n] copies counted) and each number a budget adds up — mass, power, cost, ram and every name a budget is written for — then totals, overall and per system; bom/bom.md, the same table and the roll-up of each part with everything beneath it. A number in two units gets a column per unit: nothing is converted |
root systems (no deployment needed) |
icd |
interface control documents in Markdown: icd/<port-type or interface>.md and icd/<bus>.md, with each message's fields (type, unit, range, default, description), byte layout, the links that provide and consume it (latency, buffer, route, transport time), on a bus the frames with CAN id, period and frame time, the types used, and the requirements traced to the parts, messages and interface (or naming them); icd/index.md lists them |
root systems, deployment routes |
containerd |
for containerd through nerdctl: containers/<container>/Containerfile, a multi-stage build per binary (C from c/<binary>, Rust from embassy/<binary>; generate those into the same directory, the build context) into a slim Debian image, with an entrypoint.sh when it runs several; containers/<processor>/compose.yaml for nerdctl compose up — image, platform (from the processor's isa or core-type), cpuset, mem_limit, network_mode, devices, cap_add, privileged, an rtprio ulimit at its highest real-time priority, environment; containers/deploy.sh builds every image (PUSH=1 pushes it) and says what to start on each processor |
deployment containers |
k8s |
for Kubernetes / k3s, the images containerd builds: k8s/<container>.yaml, a Deployment of one replica pinned to its processor's node (nodeSelector kubernetes.io/hostname = the processor's name, - for _; kubernetes.io/arch from its isa), resources with requests = limits from its cpuset size and memory-limit (the Guaranteed class: exclusive cores with the kubelet's static CPU manager), hostNetwork for network host, privileged for capabilities all (else capabilities.add), each device mounted from the host (hostPath), its props as env; a deny-all NetworkPolicy for network none; k8s/kustomization.yaml for kubectl apply -k k8s. What Kubernetes cannot say (the exact cores, a bridge latency) is a note |
deployment containers |
ros2 |
the messages as the ament interface package ros2/ (.msg, .srv); with a deployment that says packaging ros2, ros2_nodes/<binary>/ (rclcpp nodes around the parts' C modules, see ROS 2) and ros2_nodes/<deployment>_bringup/launch/<deployment>.launch.py; colcon build builds them |
data; deployment |
gazebo |
the plant of a packaging ros2 deployment in Gazebo (gz sim Harmonic): gazebo/worlds/<system>.sdf (sensors, actuators, masses), gazebo/config/bridge.yaml (ros_gz_bridge), gazebo/scripts/sim_adapter.py, gazebo/launch/<system>_sim.launch.py (see Gazebo) |
system, deployment |
mujoco |
the plant in MuJoCo: mujoco/<system>.xml (MJCF: bodies from the parts' mass, shape and joint, sensors, actuators) and mujoco/plant_bridge.py (the model's messages to and from it; archec sim --plant runs the model against it) (see MuJoCo) |
system |
devicetree |
a Zephyr devicetree overlay per binary on a board's microcontroller: devicetree/<binary>/boards/<zephyr board>.overlay enables each CAN, UART, I2C and SPI connector of the board the platform links, on its pins (pinctrl: hal_stm32's usart2_tx_pa2 nodes, NRF_PSEL, the RP2040 and ESP32 pinmux macros; a part's own pins over the board's), at the bitrate of the link or the bus (bitrate, current-speed, clock-frequency), an MCP2515 under the SPI for a CAN on SPI, the I2C devices at their address; an alias per bus (arche-chassis-can) and zephyr,canbus; prj.conf with the drivers (written once). The C of --target c opens each CAN or UART bus by that alias under Zephyr (DEVICE_DT_GET(DT_ALIAS(arche_chassis_can)) in drivers.c) |
deployment on boards |
renode |
the deployment's microcontrollers in Renode, without the boards: renode/<deployment>/<machine>.repl per processor with a binary, Renode's own platform for its chip (using "platforms/cpus/stm32h743.repl"; a chip it has none for is a note, see the boards); machines.resc loads each binary's ELF (where cargo build --release puts the Embassy crate's, a variable to change), a CANHub per CAN bus and a UARTHub per UART link of the platform, and <deployment>.resc starts it; <deployment>.robot for renode-test: the firmware starts its CAN controllers, and the test cases' frames — put on the bus for a part Renode does not run, counted by CAN id for one it runs. Linux processors are left out (a SocketCAN bridge, commented) |
deployment |
ui-web |
the product's screens as a web app: ui-web/index.html, ui.js (the UI runtime, the web renderer, the browser simulator), ui.css, ui.json (the UI models, sample values, the sim program). It runs on its own from the folder: the model simulated in the page by default, or live values over a WebSocket with ?ws=ws://host:port (the protocol in ui) |
ui blocks |
freertos, zephyr, posix |
the Arche app's RTOS skeleton (objects, tasks, priorities, stacks) | threads / tasks |
uppaal |
UPPAAL timed automata (model.xml) and queries (model.q). There is one template per part with a state machine, its states as locations. Composites and pseudo-states that move on at once are committed. A message a link brings is a broadcast channel: ? on the transition it triggers, ! where an action sends it, or an emitter template for a sender whose sends no transition places. Each within guarantee gets an observer with a clock, and each periodic thread of the deployment ticks. A guard on the component's own variables becomes a UPPAAL guard (see below); the rest of a guard is a comment and is dropped. Of the transitions of one state on one trigger, the first that can fire wins. The queries are A[] not deadlock, E<> P.S per state, and A[] not Obs.late per deadline. |
state machines, links, contracts |
nusmv |
NuSMV / nuXmv, untimed: one MODULE per state machine (state, a free input event, a TRANS relation by the transitions, the first that can fire winning; one whose guard is not fully translated may fire or not) and CTLSPEC EF per state |
state machines |
components |
the Arche app's component sources in Rust / C / C++, your code kept between generations (arche-cli gen) |
components |
Monitors in the firmware (--monitors). archec gen --target c --monitors compiles the requirements
into each binary of the deployment, the same checks archec replay makes on a log: an EARS requirement
watches for its trigger (a message the part receives, a state it enters) and its response within its
within (with none, right after the trigger is handled); a formal never/always/implies holds at every
check; eventually C within 30ms holds by 30 ms after the binary starts, and A leads to B within 20ms each
time A holds (or, for X receives M, M arrives) — the earliest answer awaited is the one that counts, so each
takes one flag and one deadline: constant memory, constant time per check. With no within, eventually and
leads to cannot fail at run time and are left out. A binary checks the requirements whose parts it runs. c/<binary>/monitors.h lists them; the wiring arms
a monitor when the trigger arrives, before the part handles it, clears it when the part sends the message or
reaches the state, and checks after each message and from a monitor thread. A violation calls
arche_monitor_violation(req, at_ns), yours in monitor_hook.c (written once; it logs). No heap: one small
struct per requirement. Define ARCHE_MONITOR_LOCK/UNLOCK as a critical section when the binary's threads
preempt each other. With --trace as well, each verdict is also a trace record,
arche-trace <ns> monitor <REQ> <1|0>: a failure always, a pass when a response arrives in time.
--target embassy --monitors does the same in Rust: a Requirement enum, a Monitors struct behind a blocking
mutex (a copy of each watched part's state, kept by its task), a monitor_task, and the MonitorHook trait,
implemented by Hook in src/monitor_hook.rs (yours, written once).
One instant, deadlines and watchdogs. What happens at one instant happens in the order the
specification gives — as in Lingua Franca, the model says it, not the machine:
what is due first runs first, a part does one thing at a time, a message fires the innermost transition
first, then the port's on-receive, then its handlers. The simulator (archec sim) and the C follow it.
A port's or a transition's deadline with an on-deadline body runs that body instead, for a trigger the
part starts on too late; a watchdog w { timeout 50ms; on-timeout … } runs out unless a body says
kick w in time. Both are simulated and generated in C (arche_item_t.sent, <part>_kick,
<part>_timeouts; the wiring's threads wait for the next timer) and in Rust: in the Embassy crate a
channel into a part with a deadline carries (Instant, message), the task compares elapsed() with the
deadline and calls on_deadline_<port> or on_deadline(t) instead; a part's watchdogs are a
<Part>Watchdogs field (self.wd.kick(…)), and its task's select also waits on an
embassy_time::Timer at the next one to run out, then calls on_timeout(w). With --trace, each is a
line: arche-trace <ns> deadline <part>.<port or From->To> <µs late>, arche-trace <ns> watchdog <part>.<name> 0.
archec difftest main.arche --seed 3 checks the C against the simulator: the same random inputs into
both, the C built with a harness on a virtual clock, the two traces compared, the first difference shown.
Calls to another part. port.op(…) in the action language (an operation of an interface the
port requires) is <part>_call_<port>_<op>(self, …) in C: the wiring gives it, calling the part at
the other end of the link by which part asks; difftest checks it against the simulator. In Rust it is
an Out method that sends a request to the provider's task (CALL_…, a channel) and awaits its reply
(REPLY_…, a signal); in Go Call, in C++ a std::function member, both set by the binary's main
(the specification).
Guards in UPPAAL and NuSMV. A variable of the component, such as var mode: Mode = MANUAL, is followed when two things hold. It must start at a value written in the model. Only the machine's transitions may change it, each time to a value written there too (/ self.mode = AUTO). It then becomes a small integer: the index of its value in the list of values it can take, which the export prints as a comment. A guard on it, such as [self.mode == MANUAL and not self.armed] or [self.tries < LIMIT], is translated exactly. Some variables are not followed, and the export notes each one:
- one no transition sets is an input;
- one other code also changes, such as a handler or a function;
- one a transition sets to a computed value.
The same goes for the rest of a guard: a message field, a call or an or. Those parts are dropped, so the checked model allows more than the real one, never less. A value that a test step or the physics writes into a followed variable is not seen.
Thread priorities are enforced with one executor per priority level, so a more urgent level preempts the ones below it (a slow handler on a low-priority thread cannot hold up the 1 ms loop):
- on a chip (
hal stm32|nrf|rp|esp32), each level is anInterruptExecutoron a spare interrupt (stm32:UART4,UART5…; nrf:SWIn_EGUn; rp:SWI_IRQ_n; esp32: software interrupts 1–3, esp-hal's) at an interrupt priority in the same order, the most urgent level lowest-numbered,P0left to the HAL. Change the interrupt inmain.rsif your firmware uses it. More levels than the chip has priority bits (rp: 3): the least urgent ones share the last executor, andgensays so; - on a host (
hal std), each level is an executor on its own OS thread at that priority (thread-priority; on Linux, SCHED_FIFO needsCAP_SYS_NICE); - tasks with no priority (bus tasks, threads without
priority) run in thread mode, the lowest level; with none, the least urgent level takes thread mode. Anisrwith no priority is above every thread.
Within one level, and on a bare cortex-m skeleton, tasks are cooperative. archec check warns (handler-blocking)
when a message handler on a periodic thread can hold its step past the deadline: it runs to its end first.
On a bus, an optional field is a presence byte then its value, a union a case byte then its largest case
(the DBC multiplexes on it, or says in a comment when a signal counts). src/can.rs (yours once written) moves
the frames between the bus channels and the hardware: Linux SocketCAN on a host (CAN FD when the bus is), bxCAN
on CAN1 for hal stm32 (classic frames, up to 1 Mbit/s), the TWAI controller (TWAI0) for hal esp32
(classic frames, at the nearest of 125k/250k/500k/1M bit/s). Check the pins in main.rs. nRF chips, the RP2040
and the ESP32-C2 have no CAN controller, so the channels are left to your driver (an external one such as an
MCP2515 on SPI). main.rs is yours
once written: a crate generated before this does not start the levels; gen notes it.