Windows 下实现 4ms 虚拟 PLC 周期调度:从 sleep_for 到高精度定时器
2026/9/14 23:38:00 网站建设 项目流程
文章来源说明:本文由 ZeroOne AI 整理发布,官网与 CSDN 双端同步首发,原文见 www.zeroone-ai.com。

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 包括:

  • CreateWaitableTimerW():创建定时器
  • SetWaitableTimer():设置等待时间
  • WaitForSingleObject():等待定时器触发

基本流程:

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 更适合:

  • PLC 程序开发
  • ST 逻辑调试
  • Motion 算法验证
  • IO 逻辑验证
  • Modbus TCP 测试
  • 上位机联调
  • ROS2 联调
  • 设备仿真
  • 软件测试

而对于硬实时 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 周期是不是越短越好?并不是。

  • 4ms 意味着 250 Hz
  • 2ms 意味着 500 Hz

周期越短,对 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询