IEC 61131-3 ST — 30 分钟上手
如果你希望在同一套运动内核之上使用 PLC 风格时序逻辑,就使用内置 ST 运行时。 内存、绑定和 scan 调度仍由宿主负责;VM 只负责执行程序。
这条上手路径会带你完成:编译一段 ST 程序、绑定一个 AXIS_REF、驱动
MC_Power 和 MC_MoveAbsolute,然后通过宿主侧的 symbol 读取观察完成状态。
下面这份完整宿主程序已在 CI 中编译并运行。
环境要求
- C++17 编译器(GCC 9+、Clang 10+、MSVC 2022+)
- CMake 3.21+
- 一个已安装的 plcopen 包,或本地构建前缀
0–10 分钟:理解所有权模型
ST 运行时不会替你隐藏宿主职责:
compile()属于 load-domain 工作,允许内存分配scan()属于 cycle-domain 工作,并遵守 RT 规则- 调用方拥有传给
Instance::load()的对齐字节缓冲区 AXIS_REF/GROUP_REF对象保留在宿主中,并通过显式方式绑定
这种分离是有意设计的。它让 RT 语义保持直观且可测试。
10–20 分钟:构建标准 ST consumer
创建一个目录,并放入下面这个 CMakeLists.txt:
cmake_minimum_required(VERSION 3.21)
project(plcopen_st_journey LANGUAGES CXX)
find_package(plcopen CONFIG REQUIRED)
add_executable(st_motion st_motion.cpp)
target_compile_features(st_motion PRIVATE cxx_std_17)
target_link_libraries(st_motion PRIVATE plcopen::plcopen)
把下面这份宿主程序复制到 st_motion.cpp:
#include "axis/state.h"
#include "rt/error_text.h"
#include "st/st.h"
#include <cmath>
#include <cstdint>
#include <iostream>
#include <vector>
namespace
{
using namespace plcopen::core;
constexpr const char *kSource = R"(PROGRAM motion_demo
VAR
AxisX : AXIS_REF;
Power : MC_Power;
Move : MC_MoveAbsolute;
Powered : BOOL;
Busy : BOOL;
Done : BOOL;
Error : BOOL;
ErrorID : DINT;
END_VAR
Power(Axis := AxisX, Enable := TRUE);
Move(
Axis := AxisX,
Execute := Power.Status,
ContinuousUpdate := FALSE,
Position := 1.25,
Velocity := 1.0,
Acceleration := 2.0,
Deceleration := 2.0,
Jerk := 10.0,
Direction := MC_DIRECTION#current,
BufferMode := MC_BUFFER_MODE#aborting);
Powered := Power.Status;
Busy := Move.Busy;
Done := Move.Done;
Error := Move.Error;
ErrorID := Move.ErrorID;
END_PROGRAM
)";
std::vector<std::uint64_t> make_storage(const st::Program &program)
{
return std::vector<std::uint64_t>((program.required_bytes() + 7U) / 8U + 1U);
}
int fail_compile(const st::CompileResult &compiled)
{
std::cerr << "st motion demo: compile failed\n";
for (const st::Diagnostic &diagnostic : compiled.diagnostics)
{
std::cerr << " line " << diagnostic.line << ", column " << diagnostic.column << ": "
<< st::to_string(diagnostic.code);
if (!diagnostic.message.empty())
{
std::cerr << " - " << diagnostic.message;
}
std::cerr << '\n';
}
return 1;
}
int fail_runtime(const char *stage, rt::ErrorCode error)
{
std::cerr << "st motion demo: " << stage << " failed: " << rt::to_string(error) << '\n';
return 1;
}
int fail_scan(st::ScanError error, int cycle)
{
std::cerr << "st motion demo: scan failed at cycle " << cycle << ": " << st::to_string(error)
<< '\n';
return 1;
}
} // namespace
int main()
{
const st::CompileResult compiled = st::compile(kSource);
if (!compiled.ok)
{
return fail_compile(compiled);
}
std::vector<std::uint64_t> storage = make_storage(compiled.program);
st::Instance instance;
axis::AxisModel axis;
const rt::ErrorCode loaded =
instance.load(compiled.program, reinterpret_cast<unsigned char *>(storage.data()),
storage.size() * sizeof(storage[0]), 1000000);
if (loaded != rt::ErrorCode::ok)
{
return fail_runtime("load", loaded);
}
if (instance.bind_axis("AxisX", &axis) != st::BindingError::ok)
{
std::cerr << "st motion demo: bind_axis failed\n";
return 1;
}
const int powered_index = instance.find("Powered");
const int busy_index = instance.find("Busy");
const int done_index = instance.find("Done");
const int error_index = instance.find("Error");
const int error_id_index = instance.find("ErrorID");
if (powered_index < 0 || busy_index < 0 || done_index < 0 || error_index < 0 ||
error_id_index < 0)
{
std::cerr << "st motion demo: demo symbols not found\n";
return 1;
}
bool done = false;
bool saw_powered = false;
int cycle = 0;
for (; cycle < 4000; ++cycle)
{
const st::ScanError scan = instance.scan(512);
if (scan != st::ScanError::ok)
{
return fail_scan(scan, cycle);
}
saw_powered = saw_powered || instance.value_bool(static_cast<std::size_t>(powered_index));
if (instance.value_bool(static_cast<std::size_t>(error_index)))
{
const auto code = static_cast<rt::ErrorCode>(
instance.value_i64(static_cast<std::size_t>(error_id_index)));
std::cerr << "st motion demo: motion FB error: " << rt::to_string(code) << '\n';
return 1;
}
done = instance.value_bool(static_cast<std::size_t>(done_index));
if (done)
{
break;
}
axis.cycle();
}
const double final_position = axis.snapshot().command_position;
std::cout << "st motion demo: powered=" << saw_powered << ", done=" << done
<< ", busy=" << instance.value_bool(static_cast<std::size_t>(busy_index))
<< ", cycles=" << cycle << ", position=" << final_position << '\n';
return saw_powered && done && std::fabs(final_position - 1.25) < 1e-8 ? 0 : 1;
}
配置、构建并运行它。把 /opt/plcopen 替换成你的安装前缀:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/plcopen
cmake --build build --config Release
./build/st_motion
退出码为 0 表示:
- ST 源码已成功编译
- 程序已装载到调用方持有的 buffer 中
bind_axis("AxisX", &axis)成功- 反复执行
scan()+axis.cycle()后,轴到达位置1.25
20–25 分钟:读懂关键边界
宿主内嵌的这段 ST 源码使用的是每周期 motion 值:
Velocity := 1.0Acceleration := 2.0Deceleration := 2.0Jerk := 10.0
这些数值会按传给 Instance::load() 的 1 kHz task 中的每周期量来解释。如果你的
面向人的配置使用的是 SI 单位,请在宿主里用 rt::CycleConfig 先做一次转换,
再据此生成或注入 ST 输入。
25–30 分钟:判断 ST 是否是合适界面
在这些情况下选择 ST:
- 机器逻辑需要保持 PLC 形态
- 时序逻辑应该与 IEC 61131-3 功能块放在一起
- 硬件桥接仍然由宿主应用负责
在这些情况下选择直接 C++:
- 你希望所有运动调用点都由原生代码直接控制
你也可以混合使用两者:用 ST 做时序编排,用 C++ 做 adapter。