RTE is an open-source model-based development toolchain for motor drives:
design control as a node graph, and the toolchain turns the graph into
flashable firmware. It ships with the base image for our OpenVVVF
STM32H723 inverter, but it is not tied to our hardware — any platform
can be targeted by writing your own base image: the HAL/driver layer,
plus the small platform_api contract the generated code calls.
This repo holds the STM32H723 base firmware image, the node-graph libraries, the Qt NodeGUI editor, and the tools that turn a graph into a flashable firmware binary. A plant/inverter simulator based on ngspice is planned, so graphs can be exercised in closed loop before touching hardware.
Hardware designs and safety documentation live in OpenVVVF/Hardware.
A note on the name: VVVF stands for Variable Voltage Variable Frequency — it describes the output, not the control strategy. This platform is not limited to scalar V/Hz control; it supports vector control (FOC), arbitrary modulation schemes, and any control scheme you can express through the node graph.
RTE/
├── Assets/
│ ├── Examples/ # Example NodeAPI graphs
│ └── NodeTemplates/ # Reusable node types for GUI + codegen
├── Images/
│ └── Gen6FW/ # STM32H7 base firmware image (HAL, startup, linker)
├── Lib/
│ ├── NodeAPI/ # Graph/node serialization and timing validation
│ ├── InverterCodegen/ # Graph -> C++ code generation engine
│ ├── RTELogger/ # Shared logging used by the host tools
│ └── InverterProtocol/ # Shared host/device telemetry + command protocol
└── Source/
├── NodeGUI/ # Qt6 + QtNodes node editor
├── RTECodeEmitter/ # Inserts generated code into a base firmware tree
└── RTEFirmwareBuilder/ # Builds the STM32 firmware from a firmware tree
Assets/holds graphs and node-type templates shared by NodeGUI and codegen.Images/contains the base firmware image that the emitter copies and modifies.Lib/contains reusable CMake libraries used by the host tools, GUI, and device firmware.Source/contains end-user executables.
A base image is a normal firmware tree (HAL, startup, linker, drivers) with
three additions: // RTE_EMIT: markers at the timing-domain dispatch points,
an AppState global for the generated domain state, and an implementation
of Images/Gen6FW/Inc/Inverter/platform_api.h — the only contract between
generated code and hardware (PWM out, sensor reads, faults, config,
telemetry, time). RTECodeEmitter --base-src <your-tree> then produces a
flashable image from any graph. Images/Gen6FW is the reference
implementation.
Requires CMake 3.24+, a C++20 compiler, Ninja, and Qt 6 (for NodeGUI). Clone with
--recurse-submodules (or run git submodule update --init --recursive) so the
QtNodes dependency under Source/NodeGUI/third_party is present.
cmake -B build -G Ninja
cmake --build build -j8cmake --build build --target NodeGUI -j8
./build/Source/NodeGUI/NodeGUI Assets/Examples/foc_demo.jsonOn Windows, pass your Qt prefix to CMake (e.g. -DCMAKE_PREFIX_PATH=C:/Qt/6.7.3/mingw_64).
ctest --test-dir build --output-on-failureLib/InverterProtocol is a portable C/C++ library that encodes and decodes the
shared telemetry/command packet format used by the host tools and the inverter
firmware. It is split into two parts:
InverterProtocolCore— HAL-free C code (constants, header, CRC16-CCITT, COBS, packet builders/parsers). It can be compiled into both host tools and bare-metal STM32 firmware.InverterProtocol— host-only C++ layer with a cross-platform serial port and a callback-based client (ivp::InverterClient).
The packet format is transport-agnostic; the UART adapter adds COBS framing and
0x00 delimiters. Future CAN/CAN-FD adapters can reuse the same packet builders
and parsers by adding their own segmentation/reassembly.
- Magic:
0x544C4D31(TLM1), version1 - Header (16 bytes, little-endian):
magic, version, msg_type, payload_len, seq, time_us - CRC16-CCITT (
0x1021, init0xFFFF) over header + payload - UART framing: COBS +
0x00delimiter
Message types include the existing telemetry frames (TELEMETRY_DATA,
TELEMETRY_DEFINE) and reserved values for binary commands
(COMMAND_REQ, COMMAND_RSP, ACK, NACK).
cmake --build build --target InverterProtocol_tests
./build/Lib/InverterProtocol/InverterProtocol_testsRTEFirmwareBuilder handles CMake configuration, ARM toolchain detection, and
the optional RTECodeEmitter step. If you do not have arm-none-eabi-gcc/g++
installed, run the bundled installer first:
./scripts/install_stm32_toolchain.shThen build the baseline firmware:
./build/Source/RTEFirmwareBuilder/RTEFirmwareBuilder \
--fw-src Images/Gen6FW \
--build-dir build/rtetest-fw \
--graph Images/Gen6FW/baseline_graph.json \
--base-src Images/Gen6FW \
--output build/rtetest-fw-src \
--verbosity infoAfter a successful build:
build/rtetest-fw/
├── STM32CubeMX.elf
└── STM32CubeMX.bin
NodeGUI— Qt node editor for NodeAPI graphs (open/save, domains, bridges, parameter edit, auto-arrange).InverterCodegen— generates C++ domain files from a NodeAPI graph JSON.RTECodeEmitter— takes a base firmware source tree and a graph, copies the firmware, generates domain code, and inserts it at// RTE_EMIT:markers.RTEFirmwareBuilder— wraps CMake, auto-detects the ARM toolchain, optionally runsRTECodeEmitter, and builds the STM32 firmware.
The cal command runs hierarchical calibration routines; results persist to
the FRAM KV store under Motor.* and are consumed by graph config nodes at
boot:
cal list— routine tree + stored valuescal all— full profile in dependency order (poles+encoder, resistance, PMSM inductance/flux)cal Motor.Poles,cal Motor.Encoder.SinCos,cal Motor.Resistance,cal Motor.PMSM[.Inductance|.FluxLinkage],cal stop,cal status
Flux linkage is measured two ways: a FOC back-EMF sweep with a joint
least-squares fit for (psi_m, V_off) — V_off captures inverter deadtime/IGBT
drop — and stores to Motor.PMSM.FluxLinkage.Wb, which enables the
flying-start feature (resume-into-spin with a back-EMF-matched pre-seed;
without a flux value, live starts are refused).
hw.phase_voltages exposes all MAX22530 channels as graph outputs
(V_U, V_V, V_W, V_Dc, filtered reads) in the vsense timing domain.
Telemetered as cg_vu_v, cg_vv_v, cg_vw_v in the demo graph.
The base-image ApplicationSensors driver samples all slow analog inputs
with zero CPU involvement: TIM3 TRGO at 100 Hz triggers an ADC1 regular
scan (board temps 1..3 on INP19/17/16, throttle A/B on INP15/18) into
circular DMA, and ADC3 free-runs on the motor temp channel (INP9). The CPU
never blocks on an ADC — update() only harvests finished conversions.
hw.temperatures(motor + 3 board channels, °C; NAN when a channel is disabled or open/short). Per-channel KV config:Hw.Temp.Bx.*/Motor.Temp.*(En,Type,R25,Beta,RSer,Orient,CritC; types viatemp types). Board channels default disabled (not populated). Open/short sustained 500 ms raises a Warning; over-CritCraises Critical.hw.throttle(dual channels normalized 0..1 viaHw.ThrA/B.MinV/MaxV+Valid). |A−B| > 0.10 sustained 100 ms raises Critical and zeroes both.hw.digital_in(USER_DIN_1..8) /hw.digital_out(USER_DOUT_1..4, pins 5/6 = green/orange debug LEDs), pin selected by node parameter.- Shell:
temp(live V/ohm/°C + throttle),temp types,temp reload,temp debug.config set/getalso reach raw KV keys (driver config). - Rate telemetry (
hz_app_loop,hz_vsense,hz_tim_isr,hz_adc_isr) is always on as a throughput canary.
Two FDCANs (FDCAN1 "A" on PA11/PA12, FDCAN2 "B" on PB12/PB6, classic
frames; transceiver rail on PC8). Per-bus KV enables (Can.A.En/Can.B.En),
KV bit rate (Can.BitRate, default 500 kbit/s).
- Graph nodes:
hw.can_tx(Bus/Id/Ext/Dlc/Rate + D0..D7 bytes),hw.can_rx(Bus/Id mailbox,Freshon new frame).canshell command: status/send/rxdump, bus-off auto-recovery. - Session protocol (KV
Can.Proto.*, masterCan.Proto.En): a host attaches (HELLO), the device answers with its info and the KV allow mask, the host requests capabilities (telemetry / commands / flash-reserved), and keeps the session alive with heartbeats (timeout drops it — fail-safe on host loss). Telemetry streams to the session only while granted (decimated,Can.Proto.TelemDiv); shell commands ride the same transport into the sharedCommandManager. Packets use the sharedLib/InverterProtocolformat segmented over classic CAN frames; AUTH message IDs are reserved for a future password gate. IDs default 0x700/0x701 (Can.Proto.IdBase). Tools/can_session_client.py— dependency-free socketcan test client that runs the full attach/capability/telemetry/command dance.- Flashing over CAN: not pursued on this board. The H723 ROM FDCAN bootloader listens on PD0/PD1, which is not where Gen6 routes CAN; the planned dual-MCU hardware will let the MCUs reflash each other instead.
Done recently:
- Calibration suite restored (hierarchical
cal, results in theMotor.*KV namespace; flux via LS fit with V_off; flying start) - Phase voltage sensing (
hw.phase_voltages,vsensedomain, snapshot reads) - Implicit unit extraction (dimensionless inputs accept voltage/current; ToDim converters deleted)
- Config node persistence (FRAM KV store,
config set/save/list/delete) - PI voltage limit is now the true SVPWM linear limit (
Vdc/sqrt(3) * 0.95) - Temperature sensing, dual throttle with plausibility check, and user
digital IO (
hw.temperatures,hw.throttle,hw.digital_in/out) - Graph-owned variables (
var.bool/var.float/var.current+varshell), parameter-as-input, default node instance names - Encoder/control timebase sync + angle extrapolation: killed the
speed-dependent commutation "crackle" (encoder and FOC ran on independent
clocks; the consumed angle stalled then caught up in speed-proportional
steps). On-target instruments that found it are permanent: spike event
recorder (
spikes), encoder linearity trace (enc_trace), per-domain rate telemetry (hz_*) - Bench builds default to Release — at
-O0the CPU cannot service the control ISR load (hz_app_loopcollapses to <100 Hz)
Next up, roughly in priority order:
- Current-loop tuning from measured motor parameters: run the R/L
calibrators, compute PI gains for a target bandwidth, slew-limit the
current references (the
control.slewnode exists, unwired) - ngspice-based plant/inverter simulator for closed-loop graph testing before hardware
- Sensorless (observer-based) angle path for high-speed operation
- Zip-based project format: a library that packages project assets (node
templates as folders with
index.json+ separate.cpp/.hfiles, no inline code) into a renamed zip - Node library expansion: CAN bus (CAN1/CAN2)
- NodeGUI: node create/delete palette, emit/flash actions, live telemetry
- Safety: compare against HARA/TARA/SWAD, verify base-image safety subsystems, then bring the docs in line
- Full dyno validation