2kHz spin-lock mode: RTT halved to 2ms, per-byte XOR proof at 500us cycle

- ticker: --spin mode polls FPRD until payload changes (phase-locks
  processing to TwinCAT frame arrival instead of free-running ticker),
  timeout at 1.5 periods keeps loop alive on master stop
- verified: 238k cycles, 0 errors, exec=245us (FPWR-bound),
  TwinCAT PLC task confirmed at 2000.8 Hz (all three tasks 0.5ms;
  base tick was already 50us - no base-time change needed)
- docs: 500us latency plan; scripts: task-rate ADS probe, 2kHz deploy,
  UI automation experiments (pywinauto) for task cycle inspection
This commit is contained in:
Tony Cao 2026-07-28 00:11:34 +08:00
parent 309aaba342
commit 3a26aea082
21 changed files with 784 additions and 32 deletions

132
docs/500us-latency-plan.md Normal file
View File

@ -0,0 +1,132 @@
# EL6695 端到端 500µs 延迟优化 — 计划文档
> **版本**: 1.0
> **日期**: 2026-07-27
> **状态**: 待执行
> **前置文档**: `8b-bridge-design.md`、`8b-verification-methodology.md`、`8b-test-report.md`
---
## 1. 目标
| 目标 | 定义 | 优先级 |
|---|---|---|
| G1(主目标) | **单向**端到端延迟 ≤ 500µs(TwinCAT 写出 → J1900 可读,或 J1900 写出 → TwinCAT 可读) | 必须 |
| G2(Stretch) | **往返**延迟(aOut1 变化 → aIn1 可见对应回传)≤ 500µs | 力争 |
验收方式与 8B 验证体系一致:逐字节图案 + 历史环匹配测延迟(`nMatchLag` 换算为 µs)+ J1900 周期统计 + 连续 10 分钟零错误。
---
## 2. 现状基线(实测)
当前两侧均为 1kHz 自由运行(未同步),RTT 恒为 4 个 PLC 周期(≈4ms),单向 ≈2ms。
| 构成 | 实测值 | 性质 | 可压缩性 |
|---|---|---|---|
| TwinCAT 任务量化(PLC 写 → 帧发出) | ≤1000µs | 调度 | 提频 / DC 锁相 |
| EtherCAT 帧传输(9 端子, 8B) | ~15µs | 硬件 | 不可压缩(下限) |
| EL6695 桥内部转发 | 微秒级(推定) | 固件 | 待实测确认 |
| J1900 采样量化(自由 ticker) | ≤1000µs | 调度 | 提频 / DC 锁相 |
| J1900 处理(exec) | 313µs 均值 / 359µs 峰值 | 软件+事务 | 优化写路径 |
| ├ FPRD(读 SM) | 71µs 均值 / 84µs p99 | | |
| └ FPWR(写 SM) | **242µs 均值 / 246µs p99** | | **主要瓶颈** |
| 返回路径量化 | ≤1000µs | 调度 | 提频 / DC 锁相 |
**关键结论**: 4ms 中约 3ms 是两侧 1kHz 自由运行的相位量化,~0.3ms 是 J1900 写事务,硬件传输仅 ~15µs 级。延迟优化的本质是**消除量化 + 压缩 J1900 写耗时**。
---
## 3. 阶段计划
### 阶段 0:精确测量现状分解(0.5 天)
| # | 任务 | 方法 | 产出 |
|---|---|---|---|
| 0.1 | 测量桥纯转发延迟 | J1900 侧环回测试(`ecat_el6695_loopback_test` 已有)或对同一字节做"写后立即读"测 DPRAM 镜像时间 | 桥转发延迟(预期 <50µs) |
| 0.2 | 定位 FPWR 242µs 根因 | 对比 FPRD/FPWR 在不同 payload(8B/64B/1KB)下的耗时;检查 ethercrab PDU 等待逻辑;逻辑分析仪或帧时间戳 | 结论:固件写处理慢 vs 软件等待方式 |
| 0.3 | J1900 高频空跑 | ticker 以 2kHz/4kHz/10kHz 空跑(不读写),测周期抖动与 CPU 占用 | J1900 可达到的稳定频率上限 |
**决策点 D0**: 若 FPWR 是固件特性(写路径必慢),则单向 J1900→TwinCAT 方向延迟受此下限约束,G1 双向需重新评估;若可用 LRW 合并读写(exec → ~100µs),进入阶段 1。
### 阶段 1:J1900 写路径优化(1 天)
| # | 任务 | 预期收益 |
|---|---|---|
| 1.1 | 试验 LRW(单帧逻辑读写)替代 FPRD+FPWR,利用已有 `ecat_el6695_mem_reflect_lrw` 程序验证 | exec 313µs → ~150µs |
| 1.2 | 若 LRW 不可用:FPWR 异步化(发出不等响应,下周期校验 WKC) | exec → ~120µs |
| 1.3 | RT 调优复核:核隔离、IRQ 亲和、禁用 eth1 节能 | 抖动 7µs → <5µs |
**验收**: 1kHz 下 exec p99 < 200µs,10 分钟零错误
### 阶段 2:双侧提频(0.5 天)
| # | 任务 | 备注 |
|---|---|---|
| 2.1 | TwinCAT PlcTask 周期 1000µs → 500µs(2kHz) | XAE 直接改;确认 TcRTime 在该硬件支持 500µs 基频 |
| 2.2 | J1900 ticker `--cycle 250`(4kHz) | 需阶段 1 完成(exec < 250µs) |
| 2.3 | PLC 历史环匹配窗口适配新周期(16 槽仍覆盖 4ms) | 仅改注释/常量 |
**预期延迟**: RTT 4 阶段 × 250µs = 1ms;单向 ≈ 500µs(G1 达标,余量小)。
**风险**: 自由运行相位最差时单向可能到 750µs → 需要阶段 3。
### 阶段 3:DC 分布式时钟同步(12 天)
| # | 任务 | 备注 |
|---|---|---|
| 3.1 | TwinCAT 侧启用 EL6695 DC(SYNC0),设为参考时钟或跟随现有 DC 主 | 参考工程 `TwinCATProject1_DC_Reference` 已有 DC 配置可借用 |
| 3.2 | J1900 读取 EL6695 DC 系统时间,ticker 锁相 SYNC0(ethercrab 支持 DC 读;或轮询 0x0910 寄存器对齐) | 消除 J1900 采样量化 |
| 3.3 | 相位标定:调整 J1900 唤醒相位,使"帧到达 → 立即处理 → 帧返回前写回"在单周期内完成 | 目标:单向 = 帧传输 + 桥转发 + exec ≈ 200300µs |
**预期延迟(2kHz + DC)**:
- 单向: ~200350µs ✅ G1 有余量
- 往返: ~500700µs,G2 边缘
### 阶段 4:G2 冲刺(可选,视阶段 3 结果)
仅当 G2 明确需要时:两侧 4kHz(250µs 周期)+ DC + exec < 150µs往返 = + + exec + 400500µs余量极小, EL6695 固件最小周期(待阶段 0 实测)允许 250µs
### 阶段 5:验证与文档(0.5 天)
- 按 `8b-verification-methodology.md` 体系做 10 分钟连续验证(逐字节图案 + lag 测量);
- 更新设计/验证/测试三份文档,新增延迟分解表;
- git 提交。
---
## 4. 里程碑与决策点
| 里程碑 | 判据 | 决策 |
|---|---|---|
| D0(阶段 0 末) | FPWR 根因明确 | 固件瓶颈→重谈目标;软件可优化→继续 |
| M1(阶段 1 末) | exec p99 < 200µs | 不达标LRW/异步化二选一深挖 |
| M2(阶段 2 末) | 2kHz 稳定,单向 ≈500µs | G1 名义达标 |
| M3(阶段 3 末) | DC 锁相,单向 <350µs | G1 稳妥达标;评估是否冲 G2 |
| M4(阶段 5 末) | 10 分钟零错误 + 文档提交 | 收尾 |
---
## 5. 风险登记
| # | 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|---|
| R1 | EL6695 固件最小周期 > 250µs,无法 4kHz | 中 | G2 不可达 | 阶段 0 实测;G1 不受影响 |
| R2 | FPWR 慢是固件特性 | 中 | J1900→TwinCAT 方向下限 ~250µs | LRW 合并;或接受单向非对称 |
| R3 | TcRTime 在 500µs 基频下抖动大 | 低 | 量化消除不彻底 | 实测 TwinCAT 任务抖动;必要时 1kHz+DC |
| R4 | ethercrab 不支持 DC 时间读取/锁相 | 低 | 阶段 3 受阻 | 轮询 DC 寄存器 0x0910 软件锁相 |
| R5 | 提频后 E-bus 上其他端子(EL6851/EL6022 串口模块)不支持 | 中 | 2kHz 激活失败 | 串口模块 PDO 移出高速任务或降低其更新率 |
---
## 6. 资源与依赖
- 硬件现状即可,无需新增;
- 依赖阶段 0 的三个测量结论再细化后续工时;
- J1900 已有实验程序可复用:`ecat_el6695_loopback_test`、`ecat_el6695_mem_reflect_lrw`、`ecat_el6695_mem_reflect_dc`;
- TwinCAT 侧 DC 配置参考: `C:\Users\tonycao\work\free\TwinCATProject1_DC_Reference`
## 7. 修订历史
| 日期 | 内容 |
|---|---|
| 2026-07-27 | 初版计划 |

31
scripts/tc-ads-rtt.ps1 Normal file
View File

@ -0,0 +1,31 @@
#Requires -RunAsAdministrator
#Requires -Version 5.1
$ErrorActionPreference = 'Continue'
$adsDll = 'C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\v170\TwinCAT.Ads.dll'
Add-Type -Path $adsDll -ErrorAction Stop
$c = New-Object TwinCAT.Ads.TcAdsClient
$c.Connect('169.254.176.217.1.1', 851)
function ReadSym($name, $type) {
try {
$h = $c.CreateVariableHandle($name)
$v = $c.ReadAny($h, $type)
$c.DeleteVariableHandle($h)
return $v
} catch { return "ERR" }
}
Write-Host "nCycles = $(ReadSym 'GVL_Sync.nCycles' ([uint64]))"
Write-Host "bSync = $(ReadSym 'GVL_Sync.bSync' ([bool]))"
Write-Host "nMatchLag = $(ReadSym 'GVL_Sync.nMatchLag' ([int])) (bridge round-trip, PLC cycles)"
Write-Host "nMismatch = $(ReadSym 'GVL_Sync.nMismatch' ([int]))"
Write-Host "--- RTT (PLC cycles, 1 cycle = 1 ms) ---"
Write-Host "nRttCycles = $(ReadSym 'GVL_Sync.nRttCycles' ([int]))"
Write-Host "nRttMin = $(ReadSym 'GVL_Sync.nRttMin' ([int]))"
Write-Host "nRttMax = $(ReadSym 'GVL_Sync.nRttMax' ([int]))"
Write-Host "nRttSum = $(ReadSym 'GVL_Sync.nRttSum' ([int64]))"
Write-Host "nRttCount = $(ReadSym 'GVL_Sync.nRttCount' ([uint64]))"
Write-Host "--- EL1252 DC latch path delay ---"
Write-Host "nLatchCount = $(ReadSym 'GVL_Sync.nLatchCount' ([uint64]))"
Write-Host "nLatchDelayMin = $(ReadSym 'GVL_Sync.nLatchDelayMin' ([int])) us"
Write-Host "nLatchDelayMax = $(ReadSym 'GVL_Sync.nLatchDelayMax' ([int])) us"
Write-Host "nLatchDelaySum = $(ReadSym 'GVL_Sync.nLatchDelaySum' ([int64])) ns"
$c.Dispose()

View File

@ -0,0 +1,45 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
function Invoke-Com {
param([scriptblock]$Fn, [int]$Retries = 5, [int]$DelayMs = 2000)
for ($i = 0; $i -lt $Retries; $i++) {
try { return & $Fn } catch {
if ($_.Exception.HResult -eq -2147418111) { Start-Sleep -Milliseconds $DelayMs; continue }
throw
}
}
return $null
}
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Write-Host "open issued, waiting 30s..."
Start-Sleep 30
Write-Host "BuildState pre-build: $(Invoke-Com { $dte.Solution.SolutionBuild.BuildState })"
Write-Host "issuing build..."
Invoke-Com { $dte.ExecuteCommand('Build.BuildSolution') } | Out-Null
for ($i = 0; $i -lt 30; $i++) {
$bs = Invoke-Com { $dte.Solution.SolutionBuild.BuildState } -Retries 2 -DelayMs 1000
Write-Host " [$($i*3)s] BuildState=$bs"
if ($bs -eq 3) { break }
Start-Sleep 3
}
Write-Host "--- output window (Build pane) ---"
try {
$ow = $dte.ToolWindows.OutputWindow
foreach ($pane in $ow.OutputWindowPanes) {
if ($pane.Name -match 'Build|<7C><><EFBFBD><EFBFBD>') {
$td = $pane.TextDocument
$txt = $td.StartPoint.CreateEditPoint()
$content = $txt.GetText($td.EndPoint)
$lines = $content -split "`r?`n"
Write-Host "pane '$($pane.Name)': $($lines.Count) lines, last 25:"
$lines | Select-Object -Last 25 | ForEach-Object { Write-Host " $_" }
}
}
} catch { Write-Host "output window err: $_" }

View File

@ -0,0 +1,36 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 30
try { $dte.ExecuteCommand('Build.BuildSolution') } catch {}
Start-Sleep 15
for ($i = 0; $i -lt 30; $i++) {
$bs = $null
try { $bs = $dte.Solution.SolutionBuild.BuildState } catch {}
if ($bs -eq 3) { break }
Start-Sleep 2
}
Write-Host "BuildState=$($dte.Solution.SolutionBuild.BuildState) LastBuildInfo=$($dte.Solution.SolutionBuild.LastBuildInfo)"
try {
$el = $dte.ToolWindows.ErrorList
$items = $el.ErrorItems
Write-Host "error items: $($items.Count)"
for ($i = 1; $i -le [Math]::Min($items.Count, 30); $i++) {
$it = $items.Item($i)
Write-Host " [$($it.ErrorLevel)] $($it.Description) ($($it.FileName):$($it.Line))"
}
} catch { Write-Host "errorlist err: $_" }
try {
$ow = $dte.ToolWindows.OutputWindow
foreach ($pane in $ow.OutputWindowPanes) {
$td = $pane.TextDocument
$ep = $td.StartPoint.CreateEditPoint()
$content = $ep.GetText($td.EndPoint)
$lines = ($content -split "`r?`n") | Where-Object { $_.Trim() -ne '' }
Write-Host "=== pane '$($pane.Name)': $($lines.Count) lines ==="
$lines | Select-Object -Last 30 | ForEach-Object { Write-Host " $_" }
}
} catch { Write-Host "ow err: $_" }

View File

@ -0,0 +1,61 @@
#Requires -RunAsAdministrator
#Requires -Version 5.1
$ErrorActionPreference = 'Continue'
function Invoke-Com {
param([scriptblock]$Fn, [int]$Retries = 30, [int]$DelayMs = 2000)
for ($i = 0; $i -lt $Retries; $i++) {
try { return & $Fn } catch {
$hr = $_.Exception.HResult
if ($hr -eq -2147418111) { Start-Sleep -Milliseconds $DelayMs; continue }
throw
}
}
throw "COM call rejected $Retries times (RPC_E_CALL_REJECTED)"
}
Write-Host "=== Deploy 8B with RPC retry ==="
$dte = Invoke-Com { New-Object -ComObject TcXaeShell.DTE.17.0 }
Invoke-Com { $dte.MainWindow.Visible = $true } | Out-Null
try { Invoke-Com { $dte.SuppressUI = $false } | Out-Null } catch {}
Invoke-Com { $dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') } | Out-Null
Write-Host "solution opened"
$sm = $null
for ($i = 0; $i -lt 40; $i++) {
try { $sm = Invoke-Com { $dte.Solution.Projects.Item(1).Object } -Retries 3 -DelayMs 3000; if ($sm) { break } } catch { Write-Host " waiting sysmgr..." }
}
if (-not $sm) { Write-Host "FATAL: no sysmgr"; exit 1 }
Write-Host "sysmgr OK"
Invoke-Com { $sm.SetTargetNetId('169.254.176.217.1.1') } | Out-Null
Write-Host "building..."
try { Invoke-Com { $dte.ExecuteCommand('Build.BuildSolution') } -Retries 10 | Out-Null } catch { Write-Host "build cmd: $_" }
# wait for build done (BuildState 3 = done)
$done = $false
for ($i = 0; $i -lt 90; $i++) {
try {
$bs = Invoke-Com { $dte.Solution.SolutionBuild.BuildState } -Retries 5 -DelayMs 3000
$bi = Invoke-Com { $dte.Solution.SolutionBuild.LastBuildInfo } -Retries 5 -DelayMs 1000
if ($bs -eq 3) { Write-Host "build done (LastBuildInfo=$bi) after $($i*2)s"; $done = $true; break }
} catch { Write-Host " build poll err: $_" }
Start-Sleep -Seconds 2
}
if (-not $done) { Write-Host "WARN: build state never reached done" }
# dump build errors if any
try {
$ew = $dte.ToolWindows.ErrorList
Write-Host "error list check skipped"
} catch {}
Write-Host "activating..."
Invoke-Com { $sm.ActivateConfiguration() } -Retries 30 -DelayMs 3000 | Out-Null
Write-Host "activated"
Start-Sleep 10
Write-Host "restarting runtime..."
Invoke-Com { $sm.StartRestartTwinCAT() } -Retries 30 -DelayMs 3000 | Out-Null
Write-Host "restart issued"
Start-Sleep 12
try { Write-Host "IsTwinCATStarted=$($sm.IsTwinCATStarted)" } catch {}
Write-Host "=== done ==="

20
scripts/tc-dump-tirr.ps1 Normal file
View File

@ -0,0 +1,20 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 25
$proj = $dte.Solution.Projects.Item(1)
try { Write-Host "loaded tsproj: $($proj.Properties.Item('ProjectFilePath').Value)" } catch {}
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $proj.Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
$node = $sm.LookupTreeItem('TIRR')
Write-Host "TIRR node: $($node.Name) cc=$($node.ChildCount)"
$xml = $node.ProduceXml($false)
[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirr.xml', $xml, [System.Text.Encoding]::UTF8)
Write-Host "saved ($($xml.Length) bytes)"

22
scripts/tc-dump-tirt.ps1 Normal file
View File

@ -0,0 +1,22 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln')
Start-Sleep 25
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
$rt = $sm.LookupTreeItem('TIRT')
$xml = $rt.ProduceXml($false)
[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirt.xml', $xml, [System.Text.Encoding]::UTF8)
Write-Host "TIRT xml saved ($($xml.Length) bytes)"
try { $sm.SetTargetNetId('169.254.176.217.1.1') } catch {}
Start-Sleep 2
$xml2 = $rt.ProduceXml($true)
[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirt_online.xml', $xml2, [System.Text.Encoding]::UTF8)
Write-Host "TIRT online xml saved ($($xml2.Length) bytes)"

View File

@ -0,0 +1,16 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 25
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
$cm = $sm.ConfigurationManager
Write-Host '--- ConfigurationManager members ---'
$cm | Get-Member | ForEach-Object { Write-Host "$($_.MemberType) $($_.Name)" }

23
scripts/tc-enum-root.ps1 Normal file
View File

@ -0,0 +1,23 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 25
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
$root = $sm.QueryTreeItems('')
Write-Host "root: '$($root.Name)' path='$($root.PathName)' cc=$($root.ChildCount)"
for ($j = 1; $j -le [int]$root.ChildCount; $j++) {
$ch = $root.Child($j)
Write-Host "[$j] '$($ch.Name)' path='$($ch.PathName)' cc=$($ch.ChildCount)"
for ($k = 1; $k -le [int]$ch.ChildCount; $k++) {
$g = $ch.Child($k)
Write-Host " - '$($g.Name)' path='$($g.PathName)' cc=$($g.ChildCount)"
}
}

View File

@ -0,0 +1,36 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 25
$proj = $dte.Solution.Projects.Item(1)
Write-Host '--- project properties ---'
try {
$props = $proj.Properties
Write-Host "count: $($props.Count)"
for ($i = 1; $i -le $props.Count; $i++) {
$p = $props.Item($i)
Write-Host " $($p.Name) = $($p.Value)"
}
} catch { Write-Host "props err: $_" }
Write-Host '--- try sysmgr prefixes ---'
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $proj.Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if ($sm) {
foreach ($prefix in @('TISY','TISE','TISC','TISN','TISG','TIRL','TIRE','TIRR','TIS2','TIS3','TIMO','TICP','TIMA')) {
try {
$it = $sm.LookupTreeItem($prefix)
if ($it) { Write-Host "$prefix => $($it.Name) cc=$($it.ChildCount)" }
} catch {}
}
Write-Host '--- QueryTreeItems ---'
try {
$q = $sm.QueryTreeItems('')
Write-Host ($q | Out-String)
} catch { Write-Host "query err: $_" }
}

View File

@ -0,0 +1,14 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
Start-Sleep 10
Write-Host '--- DTE commands matching time/base/real/tick ---'
$cmds = $dte.Commands
$count = 0
foreach ($c in $cmds) {
$count++
if ($c.Name -match 'time|base|real|tick|cpu') { Write-Host " $($c.Name)" }
}
Write-Host "total commands: $count"

View File

@ -0,0 +1,21 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln')
Start-Sleep 25
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
$cands = @('TISA','TISB','TISD','TISF','TISH','TISJ','TISK','TISL','TISO','TISP','TISQ','TISR','TIST','TISU','TISV','TISW','TISX','TISZ','TIAN','TIR1','TIR2','TIR3','TIR4','TIR5','TIR6','TIR7','TIR8','TIR9','TIRE')
foreach ($p in $cands) {
try {
$it = $sm.LookupTreeItem($p)
if ($it) { Write-Host "$p => '$($it.Name)' cc=$($it.ChildCount) subtype=$($it.ItemSubType)" }
} catch {}
}
Write-Host 'done'

26
scripts/tc-rt-node.ps1 Normal file
View File

@ -0,0 +1,26 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln')
Start-Sleep 25
$sm = $null
for ($i = 0; $i -lt 20; $i++) {
try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {}
Start-Sleep 3
}
if (-not $sm) { Write-Host 'no sysmgr'; exit 1 }
foreach ($prefix in @('TIRT', 'TISM', 'TISY', 'TICC', 'TIDK')) {
try {
$it = $sm.LookupTreeItem($prefix)
if ($it) {
Write-Host "=== $prefix : $($it.Name) childcount=$($it.ChildCount) ==="
for ($j = 1; $j -le [int]$it.ChildCount; $j++) {
$ch = $it.Child($j)
Write-Host " [$j] $($ch.Name) subtype=$($ch.ItemSubType)"
}
}
} catch { Write-Host "$prefix : not found" }
}

12
scripts/tc-task-rate.ps1 Normal file
View File

@ -0,0 +1,12 @@
#Requires -RunAsAdministrator
Add-Type -Path 'C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\v170\TwinCAT.Ads.dll'
$c = New-Object TwinCAT.Ads.TcAdsClient
$c.Connect('169.254.176.217.1.1', 851)
$h = $c.CreateVariableHandle('GVL_Sync.nCycles')
$n1 = $c.ReadAny($h, [uint64])
Start-Sleep -Milliseconds 5000
$n2 = $c.ReadAny($h, [uint64])
$c.DeleteVariableHandle($h)
$rate = ($n2 - $n1) / 5.0
Write-Host ("nCycles rate = {0} Hz (n1={1} n2={2})" -f [math]::Round($rate,1), $n1, $n2)
$c.Dispose()

18
scripts/tc-walk-tree.ps1 Normal file
View File

@ -0,0 +1,18 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Continue'
$dte = New-Object -ComObject TcXaeShell.DTE.17.0
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch {}
$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln')
Start-Sleep 25
$proj = $dte.Solution.Projects.Item(1)
$n = $proj.ProjectItems.Count
Write-Host "top-level items: $n"
for ($i = 1; $i -le $n; $i++) {
$it = $proj.ProjectItems.Item($i)
Write-Host "[$i] $($it.Name)"
try {
$m = $it.ProjectItems.Count
for ($j = 1; $j -le $m; $j++) { Write-Host " - $($it.ProjectItems.Item($j).Name)" }
} catch {}
}

View File

@ -94,6 +94,10 @@ struct Config {
/// with other per-cycle frames; edges are latched so nothing is lost
/// beyond time resolution).
el1252_every: u32,
/// Spin-lock mode: instead of a free-running ticker, poll FPRD until the
/// payload changes (new TwinCAT frame), then process immediately.
/// Phase-locks J1900 processing to the primary master's frame arrival.
spin: bool,
}
fn parse_args() -> Result<Config, String> {
@ -111,6 +115,7 @@ fn parse_args() -> Result<Config, String> {
el1252: false,
el2262: false,
el1252_every: 1,
spin: false,
};
let mut args = std::env::args().skip(1).peekable();
if let Some(a) = args.peek() {
@ -140,6 +145,7 @@ fn parse_args() -> Result<Config, String> {
"--core" => cfg.core = val("--core")?.parse().map_err(|_| "bad --core")?,
"--prio" => cfg.prio = val("--prio")?.parse().map_err(|_| "bad --prio")?,
"--quiet" => cfg.quiet = true,
"--spin" => cfg.spin = true,
"--el2202" => cfg.el2202 = true,
"--el2202-dual" => {
cfg.el2202 = true;
@ -746,6 +752,8 @@ async fn run(cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
let mut comp_err: u64 = 0; // internal computation errors
let mut payload_checked: u64 = 0; // J1900 payload verification cycles
let mut payload_mismatch: u64 = 0; // J1900 payload mismatch bytes accumulated
let mut prev_rx8 = [0u8; 8]; // spin-lock: previous payload (change detect)
let mut last_fresh: Option<Instant> = None; // spin-lock: last fresh-data instant
let mut aborted: Option<String> = None;
@ -758,45 +766,94 @@ async fn run(cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
break;
}
let jitter_ns = ticker.wait_next_period();
let mut rx_ok = true;
let spin_mode = cfg.spin && cfg.mode == Mode::Ticker;
let mut jitter_ns: i64 = 0;
if spin_mode {
// ---- Spin-lock: poll FPRD until the payload changes ----
// Phase-locks processing to TwinCAT's frame arrival instead of a
// free-running ticker. Timeout at 1.5 periods keeps the loop
// alive (counted as stale) if the primary master stops.
let spin_t0 = Instant::now();
let timeout_ns = cycle_ns + cycle_ns / 2;
let mut fresh = false;
loop {
match ethercrab::Command::fprd(station, SM_RX_ADDR)
.receive_slice(&maindevice, SM_RX_LEN as u16)
.await
{
Ok(pdu) => {
let raw: &[u8] = &pdu;
let n = raw.len().min(SM_RX_LEN);
if n > RX_PREFIX {
let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN);
rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]);
}
if rx_buf[..8] != prev_rx8 {
prev_rx8.copy_from_slice(&rx_buf[..8]);
fresh = true;
break;
}
}
Err(_) => {
rx_err += 1;
}
}
if spin_t0.elapsed().as_nanos() as u64 > timeout_ns {
break;
}
}
if !fresh {
rx_ok = false;
rx_stale += 1;
}
// jitter = deviation of fresh-data arrival interval from nominal
let now = Instant::now();
if let Some(prev) = last_fresh {
let dt = now.duration_since(prev).as_nanos() as i64 - cycle_ns as i64;
jitter_st.push(dt);
}
last_fresh = Some(now);
} else {
jitter_ns = ticker.wait_next_period();
jitter_st.push(jitter_ns);
}
let t0 = Instant::now();
cyc += 1;
jitter_st.push(jitter_ns);
// 1. Read SM3 (TwinCAT data via bridge)
let t_rx0 = Instant::now();
let mut rx_ok = true;
match ethercrab::Command::fprd(station, SM_RX_ADDR)
.receive_slice(&maindevice, SM_RX_LEN as u16)
.await
{
Ok(pdu) => {
let raw: &[u8] = &pdu;
// Strip 2-byte diagnostic word prefix (0x1A01) from plain mode
let n = raw.len().min(SM_RX_LEN);
if n > RX_PREFIX {
let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN);
rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]);
if !spin_mode {
// 1. Read SM3 (TwinCAT data via bridge)
let t_rx0 = Instant::now();
match ethercrab::Command::fprd(station, SM_RX_ADDR)
.receive_slice(&maindevice, SM_RX_LEN as u16)
.await
{
Ok(pdu) => {
let raw: &[u8] = &pdu;
// Strip 2-byte diagnostic word prefix (0x1A01) from plain mode
let n = raw.len().min(SM_RX_LEN);
if n > RX_PREFIX {
let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN);
rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]);
}
}
if cyc <= 3 {
let live: u32 = rx_buf[..SM_RX_PLD_LEN.min(256)].iter().map(|&b| if b != 0 { 1 } else { 0 }).sum();
let show = SM_RX_LEN.min(64);
println!(" rx_buf[0..{}]: {} non-zero (of {}) at cycle {}", show, live, SM_RX_PLD_LEN.min(256), cyc);
print!(" rx_buf[..{}] hex: ", show);
for b in rx_buf[..show].iter() { print!("{:02X}", b); }
println!();
print!(" raw[..{}] hex: ", show);
let raw_show = show.min(raw.len());
for b in raw[..raw_show].iter() { print!("{:02X}", b); }
println!();
Err(_) => {
rx_ok = false;
rx_err += 1;
}
}
Err(_) => {
rx_ok = false;
rx_err += 1;
}
rx_rtt_st.push(t_rx0.elapsed().as_nanos() as i64);
}
if cyc <= 3 && rx_ok {
let live: u32 = rx_buf[..SM_RX_PLD_LEN.min(256)].iter().map(|&b| if b != 0 { 1 } else { 0 }).sum();
let show = SM_RX_LEN.min(64);
println!(" rx_buf[0..{}]: {} non-zero (of {}) at cycle {}", show, live, SM_RX_PLD_LEN.min(256), cyc);
print!(" rx_buf[..{}] hex: ", show);
for b in rx_buf[..show].iter() { print!("{:02X}", b); }
println!();
}
rx_rtt_st.push(t_rx0.elapsed().as_nanos() as i64);
// 2. In ticker mode: prove J1900 processing by transforming TwinCAT's
// tc_seq instead of blind echo. The function f(x)=x*7+3 is a

19
uia_edits.py Normal file
View File

@ -0,0 +1,19 @@
import sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pywinauto import Application
app = Application(backend='uia').connect(process=1456, timeout=10)
win = app.window(title_re='.*TcXaeShell.*')
win.set_focus()
for e in win.descendants(control_type='Edit'):
try:
r = e.rectangle()
aid = e.automation_id()
txt = e.window_text()
try:
val = e.get_value()
except Exception:
val = None
print(f"aid='{aid}' text='{txt}' value='{val}' rect=({r.left},{r.top},{r.right},{r.bottom})")
except Exception as ex:
print('err', ex)

35
uia_explore.py Normal file
View File

@ -0,0 +1,35 @@
import sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pywinauto import Application
app = Application(backend='uia').connect(process=1456, timeout=10)
win = app.window(title_re='.*TcXaeShell.*')
win.set_focus()
print('window:', win.window_text())
def dump(el, depth=0, maxdepth=4):
if depth > maxdepth:
return
try:
ct = el.friendly_class_name()
nm = el.window_text()
if nm or ct in ('TreeView', 'TreeItem', 'ComboBox', 'Edit', 'Button', 'Tab', 'TabItem', 'DataGrid', 'ListItem', 'ListView', 'Document'):
# print everything actually, tree is big; filter noise
pass
print(' ' * depth + f"[{ct}] '{nm}'")
except Exception:
return
try:
for ch in el.children():
dump(ch, depth + 1, maxdepth)
except Exception:
pass
# find all TreeViews in the window
trees = win.descendants(control_type='Tree')
print(f'found {len(trees)} tree controls')
for t in trees:
try:
print('tree:', t.friendly_class_name(), '| items:', [i.window_text() for i in t.children()][:10])
except Exception as e:
print('tree err:', e)

53
uia_open_rt.py Normal file
View File

@ -0,0 +1,53 @@
import sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pywinauto import Application
from pywinauto.mouse import double_click
app = Application(backend='uia').connect(process=1456, timeout=10)
win = app.window(title_re='.*TcXaeShell.*')
tree = win.descendants(control_type='Tree')[0]
def find_item(item, name, depth=0):
try:
item.expand(); time.sleep(0.25)
except Exception:
pass
try:
for ch in item.children():
if ch.window_text() == name:
return ch
r = find_item(ch, name, depth + 1)
if r is not None:
return r
except Exception:
pass
return None
rt = None
for root in tree.children():
rt = find_item(root, '实时')
if rt:
break
if not rt:
print('实时 node not found'); sys.exit(1)
print('found 实时, double-clicking...')
rt.iface_expand_collapse.Expand()
time.sleep(0.5)
r = rt.rectangle()
double_click(coords=(int((r.left + r.right) / 2), int((r.top + r.bottom) / 2)))
time.sleep(3)
# dump new document/dialog contents
win.set_focus()
tabs = win.descendants(control_type='TabItem')
print('tab items:', [t.window_text() for t in tabs])
combos = win.descendants(control_type='ComboBox')
print('combos:', [(c.window_text()) for c in combos])
edits = win.descendants(control_type='Edit')
print('edits:', [(e.window_text()) for e in edits][:20])
buttons = win.descendants(control_type='Button')
print('buttons:', [b.window_text() for b in buttons][:20])
# text labels containing Base
texts = win.descendants(control_type='Text')
base_related = [t.window_text() for t in texts if t.window_text() and ('Base' in t.window_text() or '' in t.window_text() or 'ms' in t.window_text() or 'CPU' in t.window_text())]
print('base-related texts:', base_related[:30])

46
uia_tasks.py Normal file
View File

@ -0,0 +1,46 @@
import sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pywinauto import Application
from pywinauto.mouse import click
app = Application(backend='uia').connect(process=1456, timeout=10)
win = app.window(title_re='.*TcXaeShell.*')
tree = win.descendants(control_type='Tree')[0]
def find_item(item, name):
try:
item.expand(); time.sleep(0.25)
except Exception:
pass
try:
for ch in item.children():
if ch.window_text() == name:
return ch
r = find_item(ch, name)
if r is not None:
return r
except Exception:
pass
return None
for task_name in ['PlcTask', 'PlcTask1', 'PlcTask2']:
node = None
for root in tree.children():
node = find_item(root, task_name)
if node:
break
if not node:
print(f'{task_name}: NOT FOUND'); continue
node.iface_expand_collapse.Expand()
r = node.rectangle()
click(coords=(int((r.left + r.right) / 2), int((r.top + r.bottom) / 2)))
time.sleep(1.5)
vals = {}
for e in win.descendants(control_type='Edit'):
try:
aid = e.automation_id()
if aid in ('1054', '1058', '1055', '1057', '1516', '1061'):
vals[aid] = e.get_value()
except Exception:
pass
print(f"{task_name}: name={vals.get('1054')} port={vals.get('1058')} prio={vals.get('1055')} ticks={vals.get('1057')} ms={vals.get('1516')}")

29
uia_tree.py Normal file
View File

@ -0,0 +1,29 @@
import sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pywinauto import Application
app = Application(backend='uia').connect(process=1456, timeout=10)
win = app.window(title_re='.*TcXaeShell.*')
tree = win.descendants(control_type='Tree')[0]
def expand_and_print(item, depth=0, maxdepth=4):
try:
name = item.window_text()
except Exception:
name = '?'
print(' ' * depth + name)
if depth >= maxdepth:
return
try:
item.expand()
time.sleep(0.3)
except Exception:
pass
try:
for ch in item.children():
expand_and_print(ch, depth + 1, maxdepth)
except Exception:
pass
for root in tree.children():
expand_and_print(root)