ZeroOne AI
← 返回文章列表

Windows 下实现 4ms 虚拟 PLC 周期调度:从 sleep_for 到高精度定时器

👁 2
分类:机器人自动化

Windows 用户态线程无法保证实时唤醒,直接使用 std::this_thread::sleep_for 承担 4ms PLC 周期调度会出现周期丢失与抖动。本文记录 ZeroOne Virtual Motion PLC 从 sleep_for 到 Windows Waitable Timer + 500us 高精度等待的调度方案演进,包含 steady_clock 单调时钟、Scheduler 与平台 Timer 解耦的架构设计,以及 4ms 周期实测数据(平均 3999.75us、最大抖动 204us)。

Windows 下实现 4ms 虚拟 PLC 周期调度:从 sleep_for 到高精度定时器

一、项目背景

在开发 ZeroOne Virtual Motion PLC 的过程中,我们首先需要解决一个非常基础、但又非常关键的问题:

Windows 普通应用程序能不能稳定运行 4ms PLC 周期?

对于传统 PLC,4ms、2ms 甚至更短周期通常由实时操作系统、专用控制器或者硬件定时机制保证。而 Virtual PLC 不同,我们的开发环境首先是:

Windows
  ↓
C++20
  ↓
Qt / MinGW
  ↓
Virtual PLC
  ↓
4ms PLC Scheduler

Virtual PLC 本质上是运行在 Windows 用户态环境中的普通应用程序。因此,第一个问题并不是 PLC 算法,而是:

Windows 普通线程到底能不能可靠地"每 4ms 执行一次"?

二、技术方案:最初的 Scheduler 实现

第一版 Scheduler 使用的是:

std::this_thread::sleep_for();

基本思路非常简单:

任务周期
  ↓
计算下一次执行时间
  ↓
sleep_for()
  ↓
执行 PLC Task
  ↓
计算下一周期

例如:

std::this_thread::sleep_for(
    std::chrono::microseconds(3950)
);

看起来没有问题。但是实际测试以后发现,情况并没有这么简单。

2.1 4ms 为什么没有想象中那么简单?

理论上:

1000ms / 4ms = 250

因此运行 1 秒,理论周期次数约为 250 次。但实际测试过程中出现过:

4ms task cycles = 10

后来修改 Scheduler 后又出现:

4ms task cycles = 13

这并不意味着 4ms PLC 算法本身存在问题。真正的问题在于:Windows 用户态线程的睡眠和唤醒时间并不是实时保证的。

std::this_thread::sleep_for() 的含义更接近"至少等待这么长时间之后,线程才具备重新运行的条件",而不是"精确经过这么长时间后立即执行"。

2.2 Windows 下的两个时间概念

这里需要区分"时间经过了"和"线程什么时候真正获得 CPU"两件事。

例如:

T0 = 1000.000 ms

等待 4ms,理论目标是:

T1 = 1004.000 ms

但实际情况可能变成:

1000.000 ms
  ↓
sleep
  ↓
1004.000 ms
  ↓
线程仍然没有立即获得 CPU
  ↓
1004.2 ms
  ↓
线程真正开始执行

于是实际周期可能变成 4.2ms,甚至更长。所以:

sleep_for(4ms)  ≠  4.000ms 后执行

2.3 为什么普通 sleep 不适合直接承担 PLC 调度

PLC Scheduler 最关注的是周期、抖动、超时、执行时间和周期稳定性。例如目标 4ms,实际可能是:

3.95ms
4.02ms
4.01ms
4.20ms
3.88ms
4.05ms

虽然平均值可能接近 4ms,但是对于 Motion Control 来说,周期抖动本身就是一个需要关注的工程指标。尤其当 PLC 后面继续加入 PLC ST、Motion、Axis、IO、EtherCAT、Modbus TCP、ROS2、LinuxCNC 以后,Scheduler 就不能简单地依赖普通 sleep_for()

三、系统架构:Scheduler 与平台 Timer 分离

ZeroOne Virtual Motion PLC 最终采用了一个比较明确的架构:

        Scheduler
            │
            ▼
   WindowsHighResTimer
            │
   ┌────────┴────────┐
   │                 │
Waitable Timer   精确等待
   │                 │
   └────────┬────────┘
            ▼
         PLC Task

也就是说,Scheduler 不直接负责 Windows 定时器细节。Scheduler 只负责任务、周期、执行、统计、超时;平台层负责 Windows Timer。这样未来迁移 Linux 时,可以变成:

Windows:
Scheduler
  ↓
WindowsHighResTimer

Linux:
Scheduler
  ↓
LinuxHighResTimer

核心调度逻辑不需要重写。最终形成的目录结构是:

Core
 ├── Scheduler
 ├── Runtime
 ├── VariableManager
 ├── IOManager
 └── Motion

Platform
 ├── Windows
 │    └── WindowsHighResTimer
 └── Linux
      └── LinuxHighResTimer

这也是 Virtual Motion PLC 后续跨平台的重要基础。

3.1 Windows Waitable Timer

Windows 提供了 Waitable Timer 机制,核心 API 包括:

基本流程:

CreateWaitableTimer
  ↓
SetWaitableTimer
  ↓
WaitForSingleObject
  ↓
Timer Signal
  ↓
继续执行

相比简单使用 sleep_for(),这种方案可以更明确地控制 Windows 等待机制。

四、实施过程:为什么没有完全依赖 Waitable Timer

这是整个设计中比较重要的一点。我们没有认为"Windows Waitable Timer = 实时系统",这是错误的。Windows 仍然不是硬实时操作系统。因此我们的设计采用"粗粒度等待 + 精确等待"的两级策略:

粗粒度等待
  ↓
Windows Waitable Timer
  ↓
剩余约 500us
  ↓
精确等待
  ↓
执行任务

也就是:长时间使用 Timer,短时间进行精确等待。这样可以避免整个 4ms 周期一直忙等。

4.1 500us 精确等待窗口

例如目标时间 T = 4.000ms,假设当前距离目标还有 2ms,这时候没有必要忙等,可以使用 Waitable Timer 等待大部分时间:

2ms
  ↓
Waitable Timer
  ↓
剩余 500us
  ↓
进入最后 500us 后高精度时间检查

代码核心思想:

while (Clock::now() < target)
{
    std::this_thread::yield();
}

这里使用 std::chrono::steady_clock,而不是系统墙上时钟。

4.2 为什么使用 steady_clock?

PLC 周期测量需要的是单调递增的时间,例如 1000、1001、1002、1003。不能因为用户修改 Windows 系统时间(10:00 → 09:00)而导致 Scheduler 的时间轴倒退。所以:

using Clock = std::chrono::steady_clock;

非常适合周期调度、执行时间测量、超时判断和抖动统计。

4.3 第一次高精度 Timer 实测

完成 WindowsHighResTimer 后,我们没有直接把它接入 Scheduler,而是先做独立平台测试。测试条件:

系统:Windows
周期:4ms
测试时间:1000ms
理论周期:250

实际结果:

Target period : 4000 us
Test duration : 1000 ms
Cycles        : 249

Min period    : 3848 us
Max period    : 4204 us
Average       : 3999.75 us
Max jitter    : 204 us

结果判定:

[PASS] 4ms cycle execution
[PASS] Period measurement
[PASS] Timer shutdown

4.4 如何理解这个测试结果?

最值得关注的不是 249 这个次数,而是:

Average = 3999.75 us

目标 4000us,实际 3999.75us,两者非常接近。同时 Min = 3848us、Max = 4204us,最大偏差约 204us。这说明:在当前测试环境下,Windows 用户态高精度定时机制已经能够为 Virtual PLC 的 4ms 调度提供一个相对可靠的基础。

但这不能解释为"Windows 已经具备硬实时能力"——这是两个完全不同的概念。

五、应用价值:Virtual PLC 的正确定位

5.1 Virtual PLC 和真正实时 PLC 的区别

这一点在工程产品设计中非常重要。我们的目标是 Virtual PLC,而不是 Hard Real-Time PLC。

Windows Virtual PLC 更适合:

而对于硬实时 Motion Control、EtherCAT DC、极低周期控制、严格确定性控制,最终仍然应该使用 Linux PREEMPT_RT、专用实时控制器、MCU 或实时工业计算平台。

5.2 因此 Virtual PLC 的正确定位

ZeroOne Virtual Motion PLC 的定位不是用 Windows 取代真正的实时运动控制器,而是:让同一套 PLC / Motion Runtime 可以先在 Windows 环境完成开发、测试和验证,再迁移到 Linux 或实际控制硬件。

整体架构:

      PLC / Motion Runtime
              │
    ┌─────────┴─────────┐
    │                   │
Windows 平台        Linux 平台
    │                   │
WindowsHighResTimer LinuxHighResTimer
    │                   │
Virtual PLC        Real Controller

这才是我们希望实现的平台抽象。

5.3 Scheduler 不应该和 Windows API 耦合

不推荐的做法:

// Scheduler.cpp
#include <windows.h>

CreateWaitableTimer();
SetWaitableTimer();
WaitForSingleObject();

因为这样以后迁移 Linux 会非常麻烦。更合理的是通过抽象时间接口分层:

Scheduler
  │
  │ 抽象时间接口
  ▼
HighResTimer
  │
  ├── WindowsHighResTimer
  │
  └── LinuxHighResTimer

5.4 4ms 并不是越快越好

还有一个容易产生误区的问题:PLC 周期是不是越短越好?并不是。

周期越短,对 CPU、Scheduler、Timer、任务执行时间、系统抖动和实时性的要求越高。因此 PLC Scheduler 应该支持 10ms / 5ms / 4ms / 2ms / 1ms,但具体周期需要结合任务数量、任务执行时间、Motion 算法、IO 数量、通信任务、CPU 性能和操作系统实时性综合确定。

六、最终形成的工程原则

经过这次 Windows 4ms 调度问题,我们确定了几个原则。

原则一:不要直接把 sleep_for 当作实时定时器。 sleep_for 适合普通后台任务,不应该直接承担 Motion PLC 的核心周期调度。

原则二:Windows 可以做 Virtual PLC,但不能假装成硬实时系统。 应该明确:Windows Virtual PLC ≠ Hard Real-Time PLC。

原则三:Scheduler 与平台 Timer 分离。 应该是 Scheduler → Platform Timer,而不是 Scheduler → Windows API

原则四:先测 Timer,再测 Scheduler。 开发过程中应该按 WindowsHighResTimerTest → SchedulerTest → RuntimeTest 的顺序逐级确认,而不是所有东西一起调试。

七、下一阶段

目前 ZeroOne Virtual Motion PLC 已经完成:

VariableManager
  ↓
IOManager
  ↓
Scheduler
  ↓
WindowsHighResTimer

其中 Windows 高精度定时器已经完成独立测试。下一步就是:

Scheduler
  ↓
WindowsHighResTimer
  ↓
4ms PLC Task

进行真正的集成测试。最终测试目标不是简单看 cycles = 250,而是进一步统计周期平均值、最小周期、最大周期、平均抖动、最大抖动、任务执行时间、最大执行时间、Overrun 与 CPU 占用率。这样才能真正评价一个 Virtual PLC Scheduler 的工程质量。

八、总结

Windows 并不是一个硬实时操作系统,但这并不意味着 Windows 不能用于 Virtual PLC,关键在于明确产品定位和技术边界。

对于 Virtual Motion PLC:

Windows
 + 高精度 Timer
 + 单调时钟
 + 绝对时间调度
 + Scheduler 统计
 + 平台抽象

可以构建一个比较可靠的虚拟 PLC 运行环境。而对于最终的实时运动控制产品,则可以进一步迁移到:

Linux
 + PREEMPT_RT
 + 实时调度
 + 专用硬件

这样就形成:

Windows 开发
  ↓
Virtual PLC 验证
  ↓
Linux Runtime
  ↓
实际 Motion Controller

同一套 PLC / Motion Runtime,多平台运行,是 ZeroOne Virtual Motion PLC 当前架构设计的核心目标之一。

附:WindowsHighResTimer 实测输出

========================================
 ZeroOne Windows HighResTimer Test
========================================
[PASS] Timer initialize
[PASS] Timer initialized state

Target period : 4000 us
Test duration : 1000 ms
Cycles        : 249
Min period    : 3848 us
Max period    : 4204 us
Average       : 3999.75 us
Max jitter    : 204 us
[PASS] 4ms cycle execution
[PASS] Period measurement
[PASS] Timer shutdown

----------------------------------------
WindowsHighResTimer: TEST PASSED
========================================

九、SEO关键词

虚拟PLC、Windows高精度定时器、4ms周期调度、Waitable Timer、PLC Scheduler、Virtual PLC、运动控制、Motion Control、实时系统、steady_clock、C++20、周期抖动、Windows实时性、PREEMPT_RT

推荐阅读

更多工业 AI、智能制造与技术实践文章

工业互联网

IMX6ULL 裸机开发学习(一):汇编点亮 LED

基于野火 IMX6ULL mini 开发板,用 GNU ARM 汇编完成 LED 闪烁实验:CCM 时钟使能、IOMUX 复用配置、GPIO 方向与数据寄存器操作,配套 Makefile 与 imxdownload 烧写流程。

工业互联网

Linux系统篇(七)——工具篇(二):一篇搞懂 C 语言程序从代码到可执行文件的完整旅程

拆解 C 程序预处理、编译、汇编、链接四步变身流程,讲透条件编译与静态、动态链接差异,附分步实操与报错排查。

工业AI

小龙虾 Skill 技能编写入门:从 Markdown 到工业智能体工具调用

本文从最简单的 SKILL.md 讲起,系统介绍工业智能体“小龙虾”的 Skill 技能编写方法:功能说明、触发条件、执行流程、工具调用、权限分级,并结合 MES、MQTT、RAG、AGV 等工业场景,说明如何让 AI 从“聊天机器人”进化为能查询实时数据、执行任务的工业智能体。

机器人自动化

TRIO-Basic 从入门到精通(三):轴参数的含义

本文系统讲解 TRIO 运动控制器伺服轴参数表中各参数的含义与用途,涵盖单位当量 UNITS/AXIS_UNITS、增益参数、速度加减速、软件/硬件限位、原点信号、位置偏差报警、点动与色标捕捉等,并说明 UNITS 在 FRAME 坐标系开启前后的区别,是配置伺服轴与调试运动表现的必备基础。

机器人自动化

TRIO-Basic 从入门到精通(十二):Trio 实现 ModbusTCP 通讯

本教程演示两台Trio控制器间基于ModbusTCP的以太网通讯,主站通过MODBUS指令对从站保持寄存器进行读写,并实现快速超时、断线重连等可靠通讯机制,适用于与PLC、传感器等从站设备的数据交换。

M3 Max 96GB 本地部署 Qwen3.6-35B-A3B:打造 SolidWorks 智能机械设计与企业知识库 AI 工作站

M3 Max 96GB 统一内存适合同时运行本地大模型与设计工具链。本文推荐 MoE 架构的 Qwen3.6-35B-A3B(总参数约 35B、单 Token 激活约 3B)配合 MLX 4bit 量化本地部署,打造 SolidWorks 智能机械设计与企业知识库 AI 工作站:mlx-lm 提供 OpenAI 兼容 API,OpenWebUI 做对话入口,通过 SolidWorks MCP 工具调用实现参数化建模,结合 BGE-M3 + Reranker 构建企业知识库 RAG,并规划多模型架构与“小龙虾”本地 AI Agent 平台的演进路线。

评论(0