C++ Embedded — 30-minute journey
Use plcopen as a header-only library in your controller project. No runtime
dependencies, no dynamic linking — just #include and go.
In this journey you will install the published v0.20.0 source package,
build a two-axis consumer, execute MC_GroupEnable and
MC_MoveLinearAbsolute, and inspect the success/error boundary. The exact
consumer below is compiled and run on Windows and Linux CI.
Requirements
- C++17 compiler (GCC 9+, Clang 10+, MSVC 2022+)
- CMake 3.21+
0–5 minutes: choose an install path
v0.20.0 was published on 2026-07-20. FetchContent is the shortest clean
start; install + find_package is the production-shaped path.
The repository ships a tested overlay port. Until the external curated registry accepts it, point vcpkg at the checked-out port explicitly:
The repository recipe and its consumer test_package are tested together.
ConanCenter acceptance is tracked separately from this source recipe:
The current-source recipe and the overlay port are usable now; the separate
ConanCenter submission asset pins the published v0.20.0 archive. Neither is
yet a listing in ConanCenter or the vcpkg curated registry, so those external
PRs and reviews must not be inferred from a green local consumer test.
5–15 minutes: build the canonical consumer
Create a directory containing this CMakeLists.txt:
cmake_minimum_required(VERSION 3.21)
project(plcopen_find_package_smoke LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(PLCOPEN_SMOKE_REQUIRE_SERIAL_CHAIN
"Require the unreleased H2 SerialChain install surface" ON)
option(PLCOPEN_SMOKE_REQUIRE_H1
"Require the unreleased H1 synchronized joint stream install surface" ON)
option(PLCOPEN_SMOKE_REQUIRE_H3
"Require the unreleased H3 fixed-base dynamics install surface" ON)
find_package(plcopen REQUIRED CONFIG)
add_executable(plcopen_find_package_smoke main.cpp)
target_link_libraries(plcopen_find_package_smoke PRIVATE plcopen::plcopen)
if(PLCOPEN_SMOKE_REQUIRE_SERIAL_CHAIN)
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_SERIAL_CHAIN=1)
else()
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_SERIAL_CHAIN=0)
endif()
if(PLCOPEN_SMOKE_REQUIRE_H1)
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_H1=1)
else()
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_H1=0)
endif()
if(PLCOPEN_SMOKE_REQUIRE_H3)
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_H3=1)
else()
target_compile_definitions(plcopen_find_package_smoke
PRIVATE PLCOPEN_SMOKE_REQUIRE_H3=0)
endif()
Copy this CI-owned program to main.cpp:
#include "axis/group.h"
#include "fb/motion.h"
#if PLCOPEN_SMOKE_REQUIRE_H3
#include "dyn/fixed_base_chain.h"
#define PLCOPEN_SMOKE_HAS_H3 1
#elif __has_include("dyn/fixed_base_chain.h")
#include "dyn/fixed_base_chain.h"
#define PLCOPEN_SMOKE_HAS_H3 1
#else
#define PLCOPEN_SMOKE_HAS_H3 0
#endif
#if PLCOPEN_SMOKE_REQUIRE_H1
#include "stream/joint_group.h"
#define PLCOPEN_SMOKE_HAS_H1 1
#else
#define PLCOPEN_SMOKE_HAS_H1 0
#endif
#if PLCOPEN_SMOKE_REQUIRE_SERIAL_CHAIN
#include "kin/serial_chain.h"
#define PLCOPEN_SMOKE_HAS_SERIAL_CHAIN 1
#elif __has_include("kin/serial_chain.h")
#include "kin/serial_chain.h"
#define PLCOPEN_SMOKE_HAS_SERIAL_CHAIN 1
#else
#define PLCOPEN_SMOKE_HAS_SERIAL_CHAIN 0
#endif
#include <cmath>
namespace
{
bool verify_h1_joint_stream()
{
#if PLCOPEN_SMOKE_HAS_H1
using namespace plcopen::core;
stream::JointStreamGroupConfig config{};
config.joint_count = 1;
config.mode = stream::JointFrameMode::direct;
config.gain_ramp_cycles = 2;
config.joints[0].filter.limits = {1.0, 0.1, 0.1, 0.01};
config.joints[0].filter.timeout_cycles = 10;
config.joints[0].filter.extrapolation_cycles = 4;
config.joints[0].max_abs_tau_ff = 2.0;
config.joints[0].max_kp = 20.0;
config.joints[0].max_kd = 5.0;
config.joints[0].safe_kp = 1.0;
config.joints[0].safe_kd = 0.5;
stream::JointStreamGroup stream_group;
if(stream_group.configure_frame(config) != rt::ErrorCode::ok ||
stream_group.reset(0, {}) != rt::ErrorCode::ok) {
return false;
}
stream::JointCommandFrame frame{};
frame.joint_count = 1;
frame.timestamp_cycles = 1;
frame.joints[0] = {0.25, 0.1, 0.5, 5.0, 2.0};
if(stream_group.push_frame(frame) != rt::ErrorCode::ok) {
return false;
}
stream_group.cycle();
const stream::JointSetpointFrame &setpoint = stream_group.read_setpoint_frame();
return setpoint.joint_count == 1 && setpoint.frame_sequence == 1 &&
setpoint.producer_timestamp_cycles == 1 &&
std::fabs(setpoint.joints[0].position - 0.25) < 1e-12 &&
std::fabs(setpoint.joints[0].velocity - 0.1) < 1e-12 &&
std::fabs(setpoint.joints[0].tau_ff - 0.5) < 1e-12;
#else
return true;
#endif
}
bool verify_h3_dynamics()
{
#if PLCOPEN_SMOKE_HAS_H3
using namespace plcopen::core;
dyn::FixedBaseChainSpec spec{};
spec.joint_count = 1;
spec.bodies[0].joint_axis = {0.0, 0.0, 1.0};
spec.bodies[0].mass = 2.0;
spec.bodies[0].center_of_mass = {0.4, 0.0, 0.0};
spec.bodies[0].inertia_com[0][0] = 0.20;
spec.bodies[0].inertia_com[1][1] = 0.25;
spec.bodies[0].inertia_com[2][2] = 0.30;
const dyn::FixedBaseChain chain(spec);
const double zero[1] = {};
const double acceleration[1] = {1.0};
const double gravity[3] = {};
double tau[1] = {};
return chain.valid() &&
chain.inverse_dynamics(zero, zero, acceleration, gravity, tau) ==
rt::ErrorCode::ok &&
std::fabs(tau[0] - 0.62) < 1e-12;
#else
return true;
#endif
}
} // namespace
int main()
{
using namespace plcopen::core;
axis::AxisModel x;
axis::AxisModel y;
if(x.set_power(true) != rt::ErrorCode::ok || y.set_power(true) != rt::ErrorCode::ok) {
return 1;
}
axis::AxisGroup group;
if(group.add_axis(x) != rt::ErrorCode::ok || group.add_axis(y) != rt::ErrorCode::ok) {
return 2;
}
fb::FbGroupEnable enable;
enable.group_ref = &group;
enable.execute = true;
enable.call();
if(!enable.outputs.done) {
return 3;
}
#if PLCOPEN_SMOKE_HAS_SERIAL_CHAIN
kin::SerialChainSpec chain_spec{};
chain_spec.joint_count = 1;
chain_spec.links[0] = {1.0, 0.0, 0.0, 0.0, -1.0, 1.0};
const kin::SerialChain chain(chain_spec);
const double joint[1] = {0.25};
kin::Pose6 pose{};
chain.forward(joint, pose);
if(chain.joint_count() != 1 ||
std::fabs(pose.position[0] - std::cos(joint[0])) > 1e-12 ||
std::fabs(pose.position[1] - std::sin(joint[0])) > 1e-12) {
return 4;
}
#endif
fb::FbMoveLinearAbsolute move;
move.group_ref = &group;
move.position.size = 2;
move.position.value[0] = 3.0;
move.position.value[1] = 4.0;
move.velocity = 2.0;
move.execute = true;
move.call();
for(int cycle = 0; cycle < 100 && !move.outputs.done; ++cycle) {
group.cycle();
move.call();
}
return move.outputs.done && !move.outputs.error && verify_h1_joint_stream() &&
verify_h3_dynamics() &&
std::fabs(x.snapshot().command_position - 3.0) < 1e-8 &&
std::fabs(y.snapshot().command_position - 4.0) < 1e-8
? 0
: 5;
}
Configure, build, and run it. Replace /opt/plcopen with your install prefix:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/plcopen
cmake --build build --config Release
./build/plcopen_find_package_smoke
On Windows the executable is normally under build/Release/. Exit code 0
means both axes reached (3, 4) without an FB error.
15–25 minutes: understand the lifecycle
AxisModelobjects outlive the non-owningAxisGroupmembership.FbGroupEnableandFbMoveLinearAbsoluteuse anexecutelevel/edge and exposebusy,done,command_aborted,error, anderror_id.group.cycle()advances exactly one deterministic cycle; the example has no wall-clock sleep because the host owns scheduling.- Return values and
error_idarert::ErrorCode; usert::to_string()and the diagnostic hint API before deciding whether to retry.
Dynamics at the low-level API are per cycle. Use rt::CycleConfig or the SI
axis/group configuration objects to convert once at the load boundary; do not
perform floating-point time accumulation in the cycle loop.
25–30 minutes: choose the production boundary
The teaching loop owns both planning and cycle advancement. Production uses
the committed-frame executor below, or supplies its own scheduler and
Servo adapter. Keep configuration and diagnostics outside the RT tick.
Real-Time Constraints
The core library is designed for hard real-time:
- Zero heap allocation, zero locks, no exceptions on the entire cycle path — this covers the full RT scan surface (rt / otg / geom / exec / kin / stream / dyn / adapters, plus the L5 axis and L6 fb cycle paths)
- L0-L4 plus kin/stream/dyn carry zero PLCopen semantics — the generic trajectory kernel is reusable on its own
- No exceptions, no RTTI (
-fno-exceptions -fno-rtti) - No OS calls in the cycle loop
- No floating-point time accumulation (integer cycle counter)
The Servo narrow interface (ADR-0004) bridges to your hardware driver.
Production Runtime Shape (ADR-0007)
The single-thread while loops in the examples above are a teaching
simplification — they are correct, but not the production shape.
In production (ADR-0007), a planning-domain thread is the sole owner
of AxisGroup / AxisModel: it drains commands, bridges feedback, runs
cycle(), and fills a committed trajectory ring up to H frames ahead
of the RT clock. The RT thread only pops one frame per tick —
O(1), zero-alloc. If planning runs slow, the ring level drops and the
lookahead depth shrinks; RT cycle timing and motion smoothness are never
disturbed. The only cross-domain sharing is four SPSC queues (commands,
trajectory ring, feedback, state snapshots); the reference executor runs
with zero TSAN findings.
See core/demo/rt_executor_demo.cpp for the reference two-thread executor
with ServoSim, and the
runtime diagram on the home page.
Embedding the ST runtime
Continue with the independent ST direct-motion journey. Its complete
host program is compiled and run by CTest; it covers compile() → load() →
bind_axis() → cyclic scan() and AxisModel::cycle() without placeholders.
Build Options
| Option | Default | Description |
|---|---|---|
PLCOPEN_BUILD_TESTS |
ON (top-level) | All test targets |
PLCOPEN_BUILD_DEMOS |
ON (top-level) | Example executables in core/demo/ |
PLCOPEN_BUILD_PYTHON_BINDINGS |
OFF | pyplcopen pybind11 module |
PLCOPEN_BUILD_LEGACY |
OFF | Frozen v0.x line |
Next Steps
- Python simulation guide — prototype without a C++ toolchain
- ST direct-motion guide — execute
MC_PowerandMC_MoveAbsolutefrom ST - Algorithm internals — how the motion planner works
- BUILD_README.md — full build reference