自定义 FB 开发 / SDK 扩展
概述
DarraRT 内置 200+ 标准功能块 (FB), 已覆盖绝大多数通用场景。当业务出现特殊工艺算法、第三方协议、 历史遗留库时, 需要把这些代码以功能块形式纳入 IEC 61131-3 工程中调用。DarraRT 提供三种扩展方式, 按安全性 / 性能 / 便利性三维取舍。
三种扩展方式
| 方式 | 安全性 | 性能 | 开发成本 | 适用 |
|---|---|---|---|---|
| SCL 源码库 (.darralib) | 最高 (IDE 沙箱) | 中 | 低 | 纯算法 / 流程 |
| C/C++ 动态库 (DLL/so) | 低 (原生, 崩溃拖垮 PLC) | 最高 | 高 | FFT / 图像 / 数学 |
| .NET 程序集 (DLL, 托管) | 中 (AppDomain 隔离) | 中 | 中 | 通讯 / 数据库 / 复杂协议 |
选型原则:
- 能用 SCL 写 → 用 SCL (可跨平台, 可热更)
- 必须高性能 → C/C++
- 要调库多 (HSL / Dapper / AMQP) → .NET
方式 1: SCL 源码库
1.1 创建库项目
IDE: 文件 → 新建 → 库项目 (.darralib)
MyLib.darralib
├── Manifest.yaml # 库元数据
├── FB/
│ ├── FB_MyControl.st
│ └── FB_MyFilter.st
├── UDT/
│ └── MyTypes.st
└── Functions/
└── Utils.st
Manifest.yaml:
library:
id: com.acme.mylib
name: Acme 自定义库
version: 1.2.0
vendor: Acme Corp
requires:
- darrart.stdlib >= 2.4
provides:
function_blocks:
- FB_MyControl
- FB_MyFilter
types:
- MyMotorCmd
functions:
- CrcXmodem
signature: SHA256:... # 构建时自动
1.2 示例 SCL 库函数
(* 工艺专用 PID, 带死区 + 积分分离 *)
FUNCTION_BLOCK FB_MyControl
VAR_INPUT
sp, pv : REAL;
kp,ki,kd : REAL;
deadband : REAL := 0.0;
END_VAR
VAR_OUTPUT
mv : REAL;
END_VAR
VAR
err_prev, integ : REAL;
first : BOOL := TRUE;
END_VAR
VAR err, derr : REAL; END_VAR
err := sp - pv;
IF ABS(err) < deadband THEN err := 0.0; END_IF;
// 积分分离: 误差大时停止积分
IF ABS(err) < 10.0 THEN
integ := integ + ki * err;
IF integ > 100.0 THEN integ := 100.0; END_IF;
IF integ < -100.0 THEN integ := -100.0; END_IF;
END_IF;
IF first THEN derr := 0.0; first := FALSE;
ELSE derr := err - err_prev;
END_IF;
err_prev := err;
mv := kp * err + integ + kd * derr;
1.3 发布与引用
IDE: 库 → 编译 → 签名 → 发布到企业库 (本地 %AppData%/DarraLib 或 HTTP 仓库 https://lib.acme.com)
主工程引用: 工程 → 依赖 → 添加 com.acme.mylib:1.2.0 → 直接用 FB_MyControl 即可。
方式 2: C/C++ 动态库
2.1 FB 模板 (C)
DarraRT FB C ABI 固定三个入口:
// mylib_fb.h
#include <stdint.h>
typedef struct DarraFbContext_ DarraFbContext;
typedef struct {
const char* fb_name; // "FB_MyProcess"
uint32_t api_version; // 1
uint32_t cycle_ns_hint; // 建议周期, 0=不关心
size_t state_size; // FB 内部状态字节数
int (*init) (DarraFbContext* ctx, const void* params, size_t n);
int (*cycle) (DarraFbContext* ctx);
int (*cleanup)(DarraFbContext* ctx);
} DarraFbDesc;
typedef struct {
// IO 绑定: 指向 PLC 变量区的指针
void* (*var_read) (DarraFbContext* c, const char* path);
void (*var_write)(DarraFbContext* c, const char* path, const void* val, size_t n);
uint64_t (*ns_clock) (void);
void (*log) (int level, const char* fmt, ...);
} DarraHostApi;
实现示例:
// mylib_fb.c
#include "mylib_fb.h"
typedef struct {
double accumulator;
uint64_t cycles;
} FbState;
static const DarraHostApi* H = NULL;
int my_init(DarraFbContext* ctx, const void* p, size_t n) {
FbState* s = (FbState*)fb_state(ctx);
s->accumulator = 0.0;
s->cycles = 0;
return 0;
}
int my_cycle(DarraFbContext* ctx) {
FbState* s = (FbState*)fb_state(ctx);
double* pv = (double*)H->var_read(ctx, "Input.pv");
s->accumulator += *pv;
s->cycles++;
double avg = s->accumulator / (double)s->cycles;
H->var_write(ctx, "Output.avg", &avg, sizeof(avg));
return 0;
}
int my_cleanup(DarraFbContext* ctx) { return 0; }
// 注册导出
FB_EXPORT DarraFbDesc FB_MyProcess = {
.fb_name = "FB_MyProcess",
.api_version = 1,
.cycle_ns_hint= 1000000, // 1 ms
.state_size = sizeof(FbState),
.init = my_init,
.cycle = my_cycle,
.cleanup = my_cleanup
};
FB_EXPORT void fb_set_host(const DarraHostApi* api) { H = api; }
2.2 编译打包
# Windows (MSVC)
cl /LD /O2 /D "FB_EXPORT=__declspec(dllexport)" mylib_fb.c
# 产出 mylib_fb.dll
# Linux
gcc -shared -fPIC -O2 -o mylib_fb.so mylib_fb.c
打包成 .darrafb:
# manifest.yaml
fb_package:
id: com.acme.fft
version: 1.0.0
abi: c-v1
binaries:
windows-x64: mylib_fb.dll
linux-x64: mylib_fb.so
function_blocks:
- FB_MyProcess
required_host_api: 1.x
signature_file: sig.bin
darra-pkg pack . -o mylib-1.0.0.darrafb
darra-pkg sign mylib-1.0.0.darrafb --cert ./acme.pfx
2.3 安全警告
C FB 运行在 Service 同进程空间, 崩溃 / 缓冲区溢出会直接导致 PLC 停止。建议:
- 代码签名强制, 未签名 FB Service 拒绝加载
- 使用 AddressSanitizer (
-fsanitize=address) 开发 - 生产前过至少 10^7 个周期的压力测试
- 对输入参数做严格校验, 不信任来自 PLC 的指针
- 不得在 cycle 内分配堆内存 (会破坏实时性)
方式 3: .NET 程序集
3.1 接口
// Darra.PLC.Extensions
public interface IFunctionBlock
{
string Name { get; }
void Init(IFbContext ctx, IDictionary<string, object>? args);
void Execute(IFbContext ctx);
void Dispose();
}
public interface IFbContext
{
T GetInput<T>(string name);
void SetOutput<T>(string name, T value);
long NsClock { get; }
ILogger Log { get; }
}
[FunctionBlock(Name = "FB_HttpFetch", CycleHint = CycleHint.Slow)]
public sealed class FB_HttpFetch : IFunctionBlock
{
public string Name => "FB_HttpFetch";
private HttpClient? _http;
public void Init(IFbContext ctx, IDictionary<string,object>? args)
{
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
}
public void Execute(IFbContext ctx)
{
var trigger = ctx.GetInput<bool>("trigger");
if (!trigger) return;
var url = ctx.GetInput<string>("url");
try
{
var body = _http!.GetStringAsync(url).Result; // 简化
ctx.SetOutput("ok", true);
ctx.SetOutput("response", body);
}
catch (Exception e)
{
ctx.Log.Error(e, "HTTP fetch 失败");
ctx.SetOutput("ok", false);
ctx.SetOutput("error", e.Message);
}
}
public void Dispose() => _http?.Dispose();
}
3.2 线程模型
- Fast FB (
CycleHint.Fast): 运行在 PLC 扫描线程, 必须<100μs, 不得阻塞 IO - Slow FB (
CycleHint.Slow): 运行在 Service 工作线程池, 可做 HTTP / SQL / 磁盘, 结果异步回写 PLC 变量
标注了 CycleHint.Slow 的 FB, Execute 在后台线程, PLC 调用方不等待返回, 只检查结果标志位。
3.3 打包与签名
<!-- MyFb.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>acme.snk</AssemblyOriginatorKeyFile>
<DarraFbPackageId>com.acme.netfb</DarraFbPackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Darra.PLC.Extensions" Version="2.4.0" />
</ItemGroup>
</Project>
dotnet build -c Release
darra-pkg pack-net ./bin/Release/net8.0 -o netfb-1.0.0.darrafb
darra-pkg sign netfb-1.0.0.darrafb --cert ./acme.pfx
错误传播
所有三种 FB 统一用FB 状态字 (ENO / Error / ErrorCode) 表达异常, 不抛不处理的异常会被 Host 拦截并转成 ENO=FALSE, ErrorCode=0xFB0000xx, 日志记录完整堆栈。
FB_MyProcess(pv := DB.pv);
IF NOT FB_MyProcess.ENO THEN
// 处理异常: 日志 / 停止 / 回退
StartAlarm(ALARM_FB_FAIL, FB_MyProcess.ErrorCode);
END_IF;
发布到企业库
企业级部署推荐独立 FB 仓库:
https://fblib.acme.com/
├── api/v1/search?q=...
├── api/v1/fetch/com.acme.mylib/1.2.0
└── api/v1/publish (需 API Key)
IDE 配置:
# ~/.darra/config.yaml
lib_sources:
- https://darra-lib.official.com/
- https://fblib.acme.com/
- file:///D:/darra-lib-local/
版本与兼容
| 主版本 | ABI 兼容 | 含义 |
|---|---|---|
1.x.y → 1.x.z | 兼容 | 接口 FB 签名不变 |
1.x → 2.x | 破坏 | 强制重编译, 需迁移 |
api_version | Host 检查 | 不匹配拒加载 |
排错
| 现象 | 原因 | 措施 |
|---|---|---|
| FB 加载失败 | 签名无效 / ABI 不匹配 | 检查 api_version 与签名证书 |
| FB 内存泄漏 | C 代码未释放 | Valgrind / DrMemory 跑压力测试 |
| FB 扫描拖慢 | Fast FB 里做了 IO | 改 Slow FB |
| .NET FB 崩溃 | 未捕获异常 | Host 自动 Dispose + 降级 |
高级技巧
- 双实现策略: SCL 作基准版, C/.NET 作加速版, 根据平台自动选
- 配置即参数: FB Init 接收 YAML 字典, 改行为不改代码
- 单元测试: Darra.PLC.TestHost 本地跑 FB, 不需要真 PLC
- 热更新: Slow FB 可无停机替换 Assembly, Fast FB 必须重启 PLC
- 沙箱限额: .NET FB 限制 CPU / 内存 / 文件访问