Skip to content

4.3 Lua 接口与编写详解 ​

这一节把自定义块里 Lua 的写法讲透。只要记住「一个固定骨架 + 一张 I 表 + return 对应输出」,就能写出绝大多数控制块。建议先做完 4.2 手把手 再看本页。

4.3.1 固定骨架 ​

每个自定义块的 Lua 都是同一个形状,头尾不要改,只在中间写您的计算:

lua
return function(I)
  -- 在这里:用 I.inputs / I.params 取输入,用 I.state 记东西
  local result = 0
  return result   -- 最后 return 结果给输出引脚
end

它本质是「返回一个函数」,设备每个扫描周期调用这个函数一次。

4.3.2 I 表完整参考 ​

运行时设备把这个块的输入、参数、私有状态打包成一张表 I 传进来:

运行时传进来的 I 表:输入按序号取、参数按 key 名取

写法含义注意
I.inputs[1]、I.inputs[2]第 1、第 2 个输入引脚当前的值序号按输入列从上到下从 1 开始,与 key 无关
I.params.键名key 为该名字的参数当前值如参数 key 是 kp 就写 I.params.kp
I.state本块私有、跨周期保留的表用来记累计值、上一次的值,详见 4.3.4
I.scan_ms当前程序扫描周期(毫秒)做时间换算时用,默认 200

输入用「序号」、参数用「键名」,别搞混

输入引脚是按位置序号取的(I.inputs[1]),所以调整输入引脚的上下顺序会影响 Lua;参数是按 key 名字取的(I.params.kp),调换参数顺序不影响。建议引脚顺序定好后就别再拖动。

4.3.3 返回值如何对应输出引脚 ​

return 出去的值,按输出引脚从上到下的顺序一一对应:

return 的值按顺序发给输出引脚,少给了补 0

  • 1 个输出引脚就 return 一个值;
  • 2 个输出引脚就 return 值1, 值2,第 1 个给第 1 个输出,第 2 个给第 2 个;
  • return 的个数少于输出引脚数时,没给到的输出写 0;
  • 输出具体送到哪个寄存器,由拖用时在块上选,Lua 里不用关心地址。

两个输出的例子(同时输出「平均值」和「是否超限标志」):

lua
return function(I)
  local avg = (I.inputs[1] + I.inputs[2]) / 2
  local alarm = (avg > (I.params.hi or 100)) and 1 or 0
  return avg, alarm   -- 第1个输出=平均值,第2个输出=超限(1/0)
end

4.3.4 I.state:让块「记住」东西(重点) ​

普通变量每次调用都会重置,而 I.state 里的内容会跨扫描周期一直保留,非常适合做累计、计时、滤波、沿判断。它还有一个关键特性:同一个块拖多份,每份的 I.state 互相独立,不会串数据。

I.state 跨扫描周期保留;同一块拖两份,各自的 state 互不影响

第一次使用时先给初值(I.state.x = I.state.x or 初值 是固定写法):

lua
return function(I)
  I.state.acc = I.state.acc or 0          -- 首次进来初始化为 0
  if I.inputs[1] ~= 0 then                -- 输入使能时累加一个周期
    I.state.acc = I.state.acc + I.scan_ms / 1000
  end
  return I.state.acc                       -- 输出累计秒数
end

state 什么时候清零

I.state 是设备运行内存里的状态:设备断电重启、或重新下发程序后会重新初始化;正常扫描过程中一直保留。需要「上电只执行一次」的逻辑,也用 I.state.ready 之类的标志判断。

4.3.5 三条必须遵守的约定 ​

  1. 骨架固定:始终是 return function(I) ... end,别改成别的形式,别在函数外写执行语句。
  2. 返回与输出对应:几个输出引脚就 return 几个值,顺序一致;不想输出的值也用 0 占位。
  3. 绝不能阻塞:这段逻辑每个扫描周期都跑一遍,禁止 sleep、死循环、以及可能算很久的操作,否则会拖慢甚至卡住整个程序的扫描。循环必须能在有限次数内结束。

4.3.6 拿来即用的常用片段 ​

限幅(把输入夹在上下限之间)——参数 lo、hi:

lua
return function(I)
  local v, lo, hi = I.inputs[1], (I.params.lo or 0), (I.params.hi or 100)
  if v < lo then v = lo elseif v > hi then v = hi end
  return v
end

滑动平均(近 N 次平均,参数 n)——用 state 存历史:

lua
return function(I)
  I.state.buf = I.state.buf or {}
  local buf = I.state.buf
  buf[#buf + 1] = I.inputs[1]
  local n = I.params.n or 4
  while #buf > n do table.remove(buf, 1) end
  local s = 0
  for _, x in ipairs(buf) do s = s + x end
  return s / #buf
end

沿判断(输入从 0 变 1 的那一拍输出一个脉冲):

lua
return function(I)
  local now = I.inputs[1] ~= 0 and 1 or 0
  local prev = I.state.prev or 0
  I.state.prev = now
  return (now == 1 and prev == 0) and 1 or 0   -- 上升沿=1,其余=0
end

更多完整块见 4.5 实战案例集。

4.3.7 运行出错时会怎样 ​

设备端对每个自定义块做了错误隔离:某一块的 Lua 一旦运行报错,只会记录一条日志、这一轮不再更新它的输出(输出保持上一次的值),不会影响主循环里的其它块和自带块,也不会让整个程序停机。

某个块出错只影响它自己,其余块照常运行,程序不停机

因此当某个自定义块输出不变、疑似没有执行时,优先去日志里看有没有「功能块 xxx 执行出错」,再回来检查:变量是不是 nil(输入没接线)、有没有除以 0、return 数量对不对。

4.3.8 语言能力边界 ​

  • 可以放心使用:局部变量、if / elseif / else、for / while(必须能结束)、算术与比较运算、与或非、字符串到数字的常规处理、I.state 上的表操作(ipairs、table.insert/remove、#)。
  • 不要使用:require、文件 / 网络 / 操作系统相关库、sleep 类延时、任何全局写操作。块应当是「给输入、出结果」的纯计算,副作用只通过输出引脚体现。
  • 当前版本一个自定义块内部不能再调用另一个自定义块,需要的功能请在同一个块里写完。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研