# RT 时钟计划

本文记录 `rpc-b4*` RT cyclic 发送路径的现状、风险和后续时钟改造计划。

## 当前判断

- 当前实现能完成 AR 建链、PrmEnd、ApplicationReady 与 RT 数据交换，但不是严格实时控制器。
- IOCR 当前默认协商 `send_clock_factor=32`、`reduction_ratio=2`，名义 UpdateInterval 为 `32 * 2 * 31.25us = 2ms`。
- RT 帧 `cycle_counter` 当前按 IOCR 周期步进填充：`cycle * send_clock_factor * reduction_ratio`；默认 `32 * 2 = 64`，`--period-us 4000` 优先保持 CA400 默认 send clock，映射为 `32 * 4 = 128`。
- 实际发送节拍当前由用户态循环和 `receive(1)` 轮询间接限制，没有按照 IOCR 周期做 deadline 调度。
- `rpc-b4*` live RT 命令默认持续发送，直到用户用 Ctrl+C 停止；可选 `finite-cycle-count` 只用于有界回归，不表示精确运行时长。

## 目标

1. ~~保留当前 `receive-paced` 行为作为兼容模式~~ 2026-06-12 起 `receive-paced` 已弃用：它唯一的节拍来源是阻塞接收，Windows 上 `GetTickCount` 约 15.6ms 的粒度会让单周期停顿超过协商 DataHoldTime（CA0400 实测 12ms），基线复测稳定触发 `0xCF81FD05` consumer DHT abort。CLI 默认已切换为 `highres`；`--clock receive-paced` 仍可解析（打印 `rt_clock_mode_deprecated=` 警告），仅用于对照实验。
2. 新增显式 RT clock 模式，使发送周期由 IOCR 参数计算并由单调时钟驱动。
3. 在日志中输出可审计的时序指标：目标周期、实际间隔、最大/平均 jitter、missed-deadline 次数。
4. 用 pcap 时间戳和本地发送统计双重验证，不把 `cycle_counter` 递增误判为真实时序准确。

## 实施阶段

### Phase 1：观测与日志

- 状态：代码已完成，待 live pcap 验证。
- [x] 计算并输出 `rt_update_interval_us = send_clock_factor * reduction_ratio * 31.25`。
- [x] 计算并输出 `rt_cycle_counter_step = send_clock_factor * reduction_ratio`，避免自定义 IOCR 周期时仍按 2ms 固定步进。
- [x] 在 `run_rt_cycles_with_output_update` 中记录发送循环的 `monotonic_ms` 时间戳。
- [x] 输出 `rt_clock_mode`、`rt_timing_resolution`、`rt_elapsed_ms`、`rt_expected_elapsed_ms`、`rt_avg_send_interval_us`、`rt_max_send_jitter_us`、`rt_missed_deadline_count`。
- [x] 新增白盒测试覆盖周期计算：`32/2 => 2000us`、`32/1 => 1000us`、`8/1 => 250us`。

### Phase 2：单调时钟调度

- 状态：代码已完成，待 live pcap 验证；当前 `monotonic-ms` 使用毫秒单调钟忙等，仅保留为 comparison-only 对照模式，精度边界仍由 Phase 3 处理。
- [x] 增加内部 `RtClockPlan`：
  - `mode`: `receive-paced | monotonic-ms | highres`
  - `period_us`
- [x] CLI 默认曾为 `receive-paced`；2026-06-12 起默认 `highres`，`receive-paced` 弃用（见「目标」第 1 条）。可选参数：
  - `--clock receive-paced`（弃用，仅对照）
  - `--clock monotonic-ms`
  - `--period-us <value>`，同步设置 Connect.req 的 IOCR `send_clock_factor/reduction_ratio` 与本地 RT 发送周期
  - `--rt-period-us <value>`，只覆盖本地 RT 发送周期，不修改 IOCR 协商值
- [x] `monotonic-ms` 模式按 deadline 发送；当前实现为毫秒级 deadline 忙等，不引入 sleep FFI。2026-06-16 起日志输出 `rt_clock_counter_bits=32`、`rt_clock_wrap_window_days=24.8`、`rt_clock_wrap_risk=32-bit-ms-wrap-around-24.8d`、`rt_clock_long_run_role=comparison-only` 和 `rt_clock_long_run_recommendation=use-highres`。
- [x] `pcap-baseline` 的 RT 场景已透传 clock/period 参数；`io-write-highres` 未显式指定 `--clock` 时仍默认 highres，可用 `--period-us` 做 IOCR/本地同步周期，或用 `--rt-period-us` 只覆盖本地发送周期。

### Phase 3：高精度 native 后端

- 状态：代码已完成，待 live pcap 验证；当前 `highres` 仍是用户态 deadline 调度，不等同于硬实时。
- [x] Windows：新增 `rawnet_highres_now_us()`，使用 `QueryPerformanceCounter`。
- [x] Linux：新增 `rawnet_highres_now_us()`，优先使用 `clock_gettime(CLOCK_MONOTONIC_RAW)`，失败后使用 `CLOCK_MONOTONIC`。
- [x] 新增 `sleep_until_us()` FFI；Windows 使用粗粒度 `Sleep` 加末段忙等，Linux 使用 `nanosleep` 加末段忙等。
- [x] CLI 新增 `--clock highres`；RT 日志在该模式输出 `rt_timing_resolution=highres_us`、`rt_elapsed_us`、`rt_first_send_us`、`rt_last_send_us`。
- [x] `rpc-b4*` live RT 命令省略 `finite-cycle-count` 时进入 continuous 模式；进入无限循环前打印建链上下文，循环中输出 AppReady、首次 data exchange 和心跳计数。
- [ ] 若 live pcap 证明 highres 后端不可用或 jitter 不可接受，再补运行时降级与 `rt_clock_degraded=true`。

### Phase 3.5：Windows IoT 实时运行开关

- 状态：代码与实机 pcap 已完成；时序仍未达到 2ms 名义周期。
- [x] 新增 `--win-rt` / `--windows-iot-rt` 显式开关；默认关闭，不改变普通 Windows/Npcap 路径。
- [x] Windows native FFI 调用 `SetPriorityClass(..., REALTIME_PRIORITY_CLASS)` 设置进程实时优先级。
- [x] 新增 `--win-rt-mask <mask>`，同时调用 `SetProcessAffinityMask` 与 `SetThreadAffinityMask` 绑定进程和当前 RT 发送线程；也支持 `--win-rt-process-mask` / `--win-rt-thread-mask` 分别设置。
- [x] 新增 `--win-rt-thread-priority <16..31>`，通过 `NtSetInformationThread(ThreadBasePriority)` 设置当前线程基础优先级，默认值为 `31`；实测 Windows 返回 `STATUS_INVALID_PARAMETER` 时，priority `31` fallback 到 `SetThreadPriority(..., THREAD_PRIORITY_TIME_CRITICAL)`。
- [x] Windows IoT 环境实机回归：先用 DCP `observe-set-ip` 将设备从 `0.0.0.0` 恢复到 `192.168.3.4/24`，再执行 release 构建 `pcap-baseline --win-rt --win-rt-mask 0x1 --win-rt-thread-priority 31 ... io-write-highres ... 512`，日志显示 `win_rt_process_priority=realtime:ok`、`win_rt_process_affinity_mask=0x00000001:ok`、`win_rt_thread_base_priority=31:ok`、`win_rt_thread_affinity_mask=0x00000001:ok`，并进入 `b4=data_exchange`。

### Phase 3.6：长跑稳定性（2026-06-12）

- 状态：代码已完成；1ms 有界回归已实机验证，35.8 分钟以上长跑待实机复核。
- 背景：长跑中观察到发送周期不断缩减（`rt_heartbeat_avg_send_interval_us` 持续下降）。审查出三个叠加根因：
  1. **31 位 µs 时钟回绕**：`rawnet_highres_now_us()` 把 QPC µs 截断到 31 位，每约 35.8 分钟（机器开机时间相位）从 `0x7FFFFFFF` 跳回 0；而 deadline 累加器按 32 位 Int mod 2^32 增长，两者模数不一致。回绕后 `deadline - now ≈ -2^31`，`sleep_until` 永不睡眠，循环全速空转补帧（1ms 周期下要 2.1M 个周期才能追平），表现为周期崩塌、设备被淹没。
  2. **`send_interval_sum_us` 32 位溢出**：1ms 周期下累计和约 35 分钟达到 2^31，平均值输出变为垃圾/负数。
  3. **无追赶钳制**：OS 停顿后固定栅格落后，循环背靠背补发而不是重新同步；停顿越久洪泛越长。
- 修复：
  - [x] FFI 新增 64 位时钟 `moonbit_rawnet_highres_now_us64` / `moonbit_rawnet_sleep_until_us64`（Windows QPC / Linux CLOCK_MONOTONIC_RAW），`@rawnet.highres_micros()`/`sleep_until_micros()` 切换为 `Int64`；31 位导出保留但 RT 路径不再使用。
  - [x] RT 循环全部计时变量（deadline、send 时间戳、间隔累计、jitter）改为 `Int64`。
  - [x] 追赶钳制 `rt_resync_deadline`：栅格落后超过 4 个周期即重新同步到当前时刻并计数 `rt_deadline_resync_count`（新增统计输出），不再 burst 补帧。
  - [x] 白盒测试覆盖：阈值内追帧、阈值边界、超阈值重同步、跨 2^31 µs 值回归保护。

### Phase 4：验收与抓包

- 状态：部分完成；`rpc-b4-io-write --clock highres` 已完成 CA0400 有界 pcap 回归，其他命令仍待补齐。
- [ ] 对 `rpc-b4`、`rpc-b4-io-write`、`rpc-b4-update-io`、`rpc-b4-read-io` 都输出相同 clock 指标。
- [x] 使用 `pcap-baseline` 捕获 `rpc-b4-io-write --clock highres` RT 数据交换，比较 pcap 帧间隔和本地发送统计。
- [ ] 设备回归目标：
  - CA0400：`rpc-b4 --clock monotonic-ms` 进入 `data_exchange`。
  - CA0600：`rpc-b4-io-write --clock monotonic-ms` 连续输出字写入，`rt_output_write_sent_count == rt_sent_count`。
  - CA0400：`rpc-b4-io-write --clock highres` 输出 `rt_timing_resolution=highres_us`，pcap 帧间隔与 `rt_avg_send_interval_us` 量级一致。2026-06-11 已完成：默认构建本地 `rt_avg_send_interval_us=2864`、pcap `output_write_avg_interval_us=2864.8`；release 构建本地 `rt_avg_send_interval_us=2769`、pcap `output_write_avg_interval_us=2769.1`。release 略好，但名义 2ms 周期仍未达成。
  - CA0400：`rpc-b4-io-write --clock highres --period-us 4000` 同步 IOCR 为 `send_clock_factor=32`、`reduction_ratio=4`，进入 `data_exchange`；2026-06-12 pcap `output_write_avg_interval_us=4000.0`、`median=4000`、`p95=4009`、`max=7748`，input `avg=4003.6`。4ms 平均节拍达标，但仍保留用户态抖动尾部。
  - CA0400 Windows IoT：`rpc-b4-io-write --clock highres --win-rt --win-rt-mask 0x1 --win-rt-thread-priority 31` 进入 `data_exchange`，pcap `output_write_avg_interval_us=2857.9`、`median=2989`、`p95=4007`、`max=5971`。WinRT 优先级/affinity 生效，但输出节拍仍未达到 2ms 名义周期。
  - CA0600：`rpc-b4-read-io --clock monotonic-ms` 保持输入采样稳定。

## 验收口径

- 功能验收：仍能进入 `b4=data_exchange`，无新增 AR/PrmEnd/AppReady 回归；`DataStatus=0x35` 为正常目标，若实测为 `0x0015` 这类 ProblemDetected 状态，必须按设备侧状态单独记录。2026-06-16 起 RT-003 L1 已补位级日志字段，L2 仍需新实机日志回填。
- 时序验收：日志必须明确区分名义周期和实际发送间隔。
- 抓包验收：pcap 帧时间戳与本地 `rt_avg_send_interval_us` 同方向一致。
- 合规口径：highres pcap 验证已证明 Windows/Npcap 用户态输出节拍在默认构建和 release 构建下均偏离 2ms 名义周期；4ms 同步 IOCR 周期可以达到平均节拍，但仍有毫秒级尾部抖动。不宣称严格 RT/IRT，只宣称 user-space RT cyclic exchange。
- Windows IoT 口径：`--win-rt` 只在系统已保留实时核心时作为专项优化路径；它设置 Win32/NT 优先级和 CPU affinity，但仍需 pcap 证明输出周期改善，不能单凭 API 调用宣称时序达标。
- 周期口径：`--period-us` 调整本地 RT 发送 loop 的 deadline 周期，并同步修改 Connect.req 里的 IOCR `send_clock_factor/reduction_ratio` 协商值；当前按 `period_us = send_clock_factor * reduction_ratio * 31.25us` 精确换算。对整数毫秒周期优先保持 CA400 默认 `send_clock_factor=32` 并调整 `reduction_ratio`，例如 `2000us => 32/2`、`4000us => 32/4`；其他可精确表示的周期才退化为 `reduction_ratio=1`。`--rt-period-us` 只调整本地发送 loop，不修改 IOCR。`rt_rx_count` 是收到并解析为 responder input 的 RT 帧数，不是 output 写入 ACK；若 `rt_heartbeat_appready_done=false` 且 `rt_heartbeat_rx_count` 不再增长，应先按 AppReady/设备启动失败排查，而不是按 1:1 ACK 缺包解释；日志中的 `rt_exchange_result=appready_timeout` 表示未完成 ApplicationReady，不能按 data exchange 解读。
- DHF 口径：自动 Data-RTC `DataHoldFactor` 按目标 12ms 反推，但默认只选择 mandatory `0x0003..0x00FF`，避免在极小周期下默默要求设备支持 optional `0x0100..0x1E00`。需要 optional 范围时必须显式传 `--data-hold-factor <3..0x1E00>` 或 `--data-hold-ms <ms>`；`0x0000` 不能用来关闭看门狗。
- 本端 DHT 口径：`rt_local_dht_timeout_us` 来自协商 IOCR 周期和 DataHoldFactor；本端 consumer DHT 只在 ApplicationReady 完成且至少收到一个有效 input 后启动，避免把启动阶段无 input 误报为 data-exchange 超时。`rt_exchange_result=local_dht_expired` 表示本端监视器触发并进入 Release.req best-effort 清理；L2 仍需真实 input 中断或断链日志/pcap 证明。
- Alarm/RTA 口径：`0xFE01` 只是 AlarmLow FrameID，不保证载荷一定是 AlarmNotification。2026-06-12 CA0400 旧代码实测该帧为 ERR-RTA-PDU（`PDUType=0x14`），`PNIOStatus=0xCF81FD02` 表示 RTA abort / Instance closed；`rt_post_rta_error_input_count=0` 证明低 `rt_rx_count` 是设备在 RTA ERR 后停止 input，而不是 4ms 时钟本身少计数，也不是 AlarmAck 未发导致的普通 DATA ACK 场景。根因已由 pcap 复核为 ApplicationReady.rsp 未按设备 big-endian CLRPC/NDR 请求端序响应：旧回包后设备发 CLRPC Reject，随后关闭 RTA instance。修复为按请求 `drep/header` 镜像回包后，alarm 与 io-write 4ms 有限回归均进入持续 `data_exchange`，`rt_rta_error_rx_count=0`，pcap 无 CLRPC Reject；有限回归 capture 尾部在本机停止 output 后仍可能出现 `0xFE01/0xCF81FD05`，这是运行结束后的停发关闭，不代表周期内 input 停计。
- RT-004 口径：2026-06-16 起 `rpc-b4-alarm` 已输出 ALPMR/responder 状态机字段（`rt_alarm_state_*`），可区分首个 AlarmNotification DATA、重复 DATA、乱序和 ACK 发送结果；但没有真实 AlarmNotification DATA 抓包前，只能算 L1 离线完成，不能宣称 alarm ACK 实机闭环。
- RT-005 口径：2026-06-16 起 `rpc-b4*` 与 `pcap-baseline` 已显式输出本端 RT output VLAN 策略字段（默认 `rt_output_vlan_tci=0xC000`，PCP 6 / DEI 0 / VID 0），并支持 `--rt-output-vlan-pcp` / `--rt-output-vlan-tci` 互斥覆盖；覆盖值同步到 Connect.req output IOCRTagHeader 与实际 output Ethernet tag。该项仍需同一次 pcap 证明本端 output 帧实际 PCP，不能用设备 input `0xA000` 反推。
- RT-006 口径：2026-06-16 起 IOCR Phase/Sequence 有 L4 契约。Phase 必须满足 `1 <= Phase <= ReductionRatio`，否则属于 Connect 阶段 IOCR 参数错误；Sequence=0 是当前 CA400 路径保持的随机序策略，非 0 在规范上是 optional defined sequence，不应被描述成通用协议非法。自定义周期实机仍需 Connect.rsp 与 data_exchange 证据。
- RT-008 口径：`monotonic-ms` 保留为对照模式，不作为长跑推荐模式。RT 摘要和 `pcap-baseline` CLI 意图摘要均输出 counter bits、wrap window、wrap risk、long-run role/recommendation；看到 `32-bit-ms-wrap-around-24.8d` 时应切换 `highres` 做长跑。当前没有 24.8 天实跑验证声明。

## 非目标

- 不在本阶段实现 IRT / RT_CLASS_3 的相位同步、PTCP、硬件时间戳或网卡队列级调度。
- 不把 `cycle_counter` 递增本身当作时钟准确性的证据；它必须与协商 IOCR 周期一致，但真实发送间隔仍以 pcap 与本地 timestamp 为准。
- 不要求 Windows/Npcap 路径达到工业控制器级 jitter，只要求可观测、可降级、可验证。
