文章来源说明:本文由 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 SchedulerVirtual 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 shutdown4.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 │ └── LinuxHighResTimer5.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