主题
timer — 定时器
模块名:timer
创建周期性或单次的定时任务。回调函数在 Lua 主线程的消息循环中执行,不是独立线程,因此回调里不要做长时间阻塞的事。
函数总览
| 函数 | 作用 |
|---|---|
timer.start(ms, callback, [once]) | 创建并立即启动一个定时器,返回定时器对象 |
timer.stop() | 停止并销毁定时器 |
对象方法:定时器对象支持 t:stop(),与 timer.stop(t) 完全等价。
timer.start
lua
timer.start(ms, callback, [once]) --> timer创建并立即启动一个定时器。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 定时周期,单位毫秒。传入小于 10 的值会被强制按 10 处理 |
| 2 | callback | function | 是 | — | 到时执行的回调,无入参、无返回值要求。类型不是 function 会直接报错 |
| 3 | once | boolean | number | 否 | false | true 或数值 >= 1 → 只触发一次;false、数值 < 1、省略 → 周期循环 |
返回值
| 类型 | 说明 |
|---|---|
| userdata | 定时器对象。只在需要 stop() 时才用得上,不接住也不影响定时器运行(见下方说明) |
示例
lua
-- 周期定时器:每 1000ms 执行一次
local t1 = timer.start(1000, function()
print("tick")
end)
-- 单次定时器:2000ms 后只执行一次
local t2 = timer.start(2000, function()
print("只响一次")
end, true)
-- 计数到 10 次后自行停止
local n = 0
local t3
t3 = timer.start(500, function()
n = n + 1
if n >= 10 then t3:stop() end
end)说明
- 启动后的定时器由固件自己持有,返回值丢掉也照样跑:lua只有需要提前停掉它时才必须用变量接住。
timer.start(1000, function() print("tick") end) -- 可以这么写 - 周期定时器不会自动停止,需要显式调用
:stop()。丢掉了返回值又想停,只能重启脚本。 - 单次定时器触发一次后自动释放,不需要
stop()。
老固件的行为不一样
2026-08-22 之前的固件里,定时器对象一旦没有变量引用就会被 GC 回收, 连带把定时器杀掉(而且每次定时回调里都会走一次增量 GC,通常第一次都触发不了), 现象是"定时器完全不工作、也没有任何报错"。 如果设备固件较早,timer.start 的返回值必须用全局或长生命周期变量接住。
实用例程
lua
--洞子塘程序2026-8-8
plc.df(6,0.072)
ds=timer.start(2000, function()
print("D6:",plc.df(6))
end)把上面的地址、主题、引脚替换成你项目里的即可。
timer.stop
lua
timer.stop(t)
t:stop()停止定时器、释放回调引用并销毁底层对象。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | t | userdata | 是 | — | timer.start 返回的定时器对象。用 t:stop() 写法时此参数由冒号语法自动传入 |
返回值
无返回值。
示例
lua
local t = timer.start(1000, function() print("running") end)
-- 两种写法等价
t:stop()
-- timer.stop(t)说明
- 可以重复调用,第二次起是空操作,不会报错。
- 停止后该对象不能再启动,需要重新
timer.start。 stop()之外没有别的释放途径:跑着的定时器被固件自己持有,不会因为没人引用而被 GC 回收。
例程
每个例程都是可直接下载运行的完整脚本:注册完定时器后脚本就结束,固件自动进入消息泵。 API 只用本页已文档化的函数;统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:周期采集 + 回调内异常保护
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 | 依赖模块:timer plc | 难度:★☆☆
场景:最常见的一种用法 —— 每隔 1 秒读一次现场数据并做判断。回调里必须自己用 pcall 兜住异常:定时器回调外面不是 try/catch,一处报错会让 Lua 状态机重建、脚本从头重跑,现象是"定时器突然重新开始计数"。
完整代码(可直接下载运行):
lua
local period = timer.start(1000, function()
local ok, err = pcall(function()
local temp = plc.df(100) -- 读 D100-D101 浮点温度
plc.d(200, math.floor(temp * 10)) -- 乘 10 取整给 modbus 用
if temp > 80 then plc.y(0, 1) end -- 超温联锁
end)
if not ok then
print("采集回调出错:", err)
end
end)要点:
pcall包的是整个回调体:出错时只丢这一拍,定时器不会被销毁、下一次照常触发。- 周期性的活不要再加任何形式的等待语句,回调干完就
return(隐含)。 - 返回值不是必须接住的:只有要
stop()时才用得到。这里接住是为了能在别的地方停掉它。 - 周期建议 ≥ 100ms,最小技术值是 10ms,太小会挤占主线程。
验证方法:
- 下载运行后,每秒 D200 随着 DA100 附近温度更新,超 80 时 Y0 吸合。
- 故意写错一行(例如调用一个不存在的函数)→ 日志每秒打印
采集回调出错: …,但定时器没有停,下一秒还在刷(证明pcall生效)。 - 去掉
pcall直接调用 → 报错后脚本会从头重执行(timer.start被重复注册)。
注意事项:
- 回调报错不是只跳过一次 —— 未捕获的错误会让整个 Lua 状态机重建,脚本 5 秒后从头再来,注册过的定时器会重复注册。
- 别在回调里
system.sleep:事件泵停了,其它定时器一起卡住。
例程 2:单次定时器做"延时一步"(替换 sleep 的正解)
适用:同上 | 依赖模块:timer plc | 难度:★☆☆
场景:按下启动后先开阀,2 秒后自动关阀。很多人会本能写成"开阀 → system.sleep(2000) → 关阀",那会把整台设备冻住 2 秒。单次定时器才是这里的正解:第三参传 true,到点跑一次就自动释放。
完整代码(可直接下载运行):
lua
-- X0 上升沿 → Y0 打开 → 2 秒后自动关闭
local last_x0 = 0
timer.start(200, function()
local x0 = plc.x(0)
if x0 == 1 and last_x0 == 0 then
plc.y(0, 1)
print("阀已开")
timer.start(2000, function()
plc.y(0, 0)
print("2 秒到,阀已关")
end, true) -- true = 只触发一次,跑完自动释放
end
last_x0 = x0
end)要点:
- 第三参
true(或数值 ≥ 1)表示单次触发;false/ 省略是周期循环。 - 单次定时器触发后自动释放,不需要
stop()。 - 用 200ms 轮询 +
last_x0记录上一拍状态来识别上升沿;Lua 侧没有"边沿寄存器"可用。 - 想做多步工序就继续在每一步里挂下一个单次定时器,形成"定时器接力"。
验证方法:
- 让 X0 由断变通(真机上短接 X0 与 COM):日志打印
阀已开,Y0 立刻吸合。 - 2 秒后自动打印
2 秒到,阀已关,Y0 断开,不需要再操作 X0。 - 这 2 秒里给设备发一条 MQTT 下发或看另一个定时器:仍能正常响应(证明没有阻塞)。
- X0 保持 ON 不会重复开阀(上升沿只在
0 → 1那一次触发)。
注意事项:
- 写成
plc.y(0,1) system.sleep(2000) plc.y(0,0):主线程被卡住 2 秒,期间 MQTT / 串口 / 其它定时器全部失效。 - X0 抖动会导致多次触发:单次定时器只擎得住一次,必要时加"动作中"标志位。
例程 3:一个定时器跑多个周期 + 到次数自停
适用:同上 | 依赖模块:timer plc | 难度:★★☆
场景:设备资源有限,与其给每件事开一个定时器,不如用一个基准定时器按 tick 取模分时;另外有些活(比如上电重试 10 次)跑到次数就该把自己停掉 —— 这两件事合在一起就是下面这段。
完整代码(可直接下载运行):
lua
-- 一个 1s 基准定时器承担 1s / 5s / 60s 三件事;重试类任务到次数自停
local tick = 0
local retry = 0
local retry_timer
local t = timer.start(1000, function()
tick = tick + 1
local temp = plc.df(100) -- 每 1s 采集
if tick % 5 == 0 then print("5s 上报", temp) end -- 每 5s
if tick % 60 == 0 then tick = 0 end -- 每 60s 归零,防溢出
end)
-- 独立的重试任务:连不上就重来,最多 10 次
retry_timer = timer.start(3000, function()
retry = retry + 1
print("第", retry, "次重试")
if retry >= 10 then
retry_timer:stop() -- 到次数停掉,与被停对象在同一层可见
print("重试上限已到,停止")
end
end)要点:
- 多个周期任务合用一个定时器:
tick % N判这一段该不该跑,比开 N 个定时器省资源。 tick长时间运行会持续增长,建议定期归零(整数溢出到不了,但归零后日志好读、判断也更直观)。- 想在回调里停掉自己,必须先用外层变量接住
timer.start的返回值(闭包引用自身变量)。 stop()可以重复调用,第二次起是空操作;停掉之后要再用只能重新timer.start。
验证方法:
- 下载后日志每 5 秒出现一条
5s 上报 <温度>;60 秒内tick归零一次。 - 重试任务每 3 秒打印一次,第 10 次打印
重试上限已到,停止后不再出现 —— 定时器真的停了。 - 把
:stop()那行删掉重跑作为对照:重试会无限刷下去。
注意事项:
- 早于 2026-08-22 的固件里,定时器没人引用会被 GC 回收并连带杀掉(现象是"定时器完全不工作、一句报错都没有");那种固件上必须用全局变量接住。
- 周期任务不要嵌套加入新定时器却又忘了停掉,会越滚越多。
注意事项
- 最小周期 10ms;周期设得过小会挤占主线程,采集类任务建议 ≥ 100ms。
- 回调在主线程执行,避免在回调里做
system.sleep之类的长阻塞,否则会拖慢其它定时器和消息处理。 - 需要真正并行执行的耗时任务,不要靠定时器堆叠。