文章来源说明本文由 ZeroOne AI 整理发布官网与 CSDN 双端同步首发原文见 www.zeroone-ai.com。Windows 下实现 4ms 虚拟 PLC 周期调度从 sleep_for 到高精度定时器一、项目背景在开发 ZeroOne Virtual Motion PLC 的过程中我们首先需要解决一个非常基础、但又非常关键的问题Windows 普通应用程序能不能稳定运行 4ms PLC 周期对于传统 PLC4ms、2ms 甚至更短周期通常由实时操作系统、专用控制器或者硬件定时机制保证。而 Virtual PLC 不同我们的开发环境首先是Windows ↓ C20 ↓ 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 TimerWindows 提供了 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_clockPLC 周期测量需要的是单调递增的时间例如 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 Hz2ms 意味着 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 PLCWindows 高精度 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、C20、周期抖动、Windows实时性、PREEMPT_RT