现有工具纳入版本管理: - ecat_tool.py: 一体化命令行主站(status/flash/eeprom/sdo/send) - ecat_server.py + ecat.html/css: 网页/桌面控制台(端口 8090) - ecat_scan.py / ecat_esc_regs.py / ecat_sniff.py: bring-up 扫描、 网络侧 ESC 寄存器诊断、裸帧抓包对照 - ecat_flash_eeprom.py: 经网线烧写 SII 镜像 + 逐字校验 - *.bat / *.vbs 启动入口 新增 .gitignore(排除 venv/ 与 __pycache__)、.gitattributes(不改写换行符)、 requirements.txt(锁定 pysoem 1.1.13 等依赖)、README.md。
563 lines
24 KiB
Python
563 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""ecat_tool.py — DCU LAN9252 EtherCAT 一体化主站工具(连接/收发/烧录)。
|
||
|
||
子命令:
|
||
adapters 列出 Npcap 适配器
|
||
status [substr] 扫描从站: 身份字/AL 状态/邮箱/PDO 尺寸
|
||
flash [--bin FILE] [substr] 烧录 EEPROM(gen_eeprom.py 镜像或 --bin)+逐字校验
|
||
eeprom read START COUNT [--out FILE] [substr]
|
||
eeprom write FILE [substr] 写 bin(奇数尾补 0xFF)+回读校验
|
||
eeprom verify FILE [substr] 只读校验
|
||
sdo read IDX SUB [--size N] [substr] CoE SDO 读(需 PREOP+,邮箱来自 SII)
|
||
sdo write IDX SUB HEX [substr] CoE SDO 写(HEX=字节流)
|
||
send [--hex H | --u64 a,b,c,d] [--rate-ms N] [--duration S | --forever]
|
||
[--dc-cycle-us N] [--dc-shift-us N] [substr]
|
||
进 OP 循环发送输出 PDO 并回显输入 PDO
|
||
monitor [--rate-ms N] [--duration S] [--dc-cycle-us N] [substr]
|
||
进 OP 只读输入(输出保持 0)
|
||
|
||
substr 默认 "I226"(按适配器描述模糊匹配)。send/monitor 的输出默认全 0。
|
||
--dc-cycle-us 非 0 时启用分布时钟: 从站 SYNC0 引脚按该周期输出脉冲,
|
||
GD32 侧 Sync0_Isr 才会被触发(默认 free-run 模式下 SYNC0 完全不打)。
|
||
验证: 主站设 1000us, 从站 [ECAT][SYNC] 日志应显示 sync0 ≈ 1000/s。
|
||
"""
|
||
import argparse
|
||
import os
|
||
import struct
|
||
import sys
|
||
import time
|
||
|
||
try:
|
||
import pysoem
|
||
except ImportError:
|
||
print("pysoem 无法加载:请先安装 Npcap(本目录 npcap-1.88.exe,"
|
||
"保持勾选 'Install WinPcap API-compatible Mode')")
|
||
sys.exit(1)
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
EEPROM_SIZE = 4096 # AT24C32
|
||
|
||
# state_check 的 timeout 单位是微秒(pysoem: "Timeout value in us");
|
||
# 2000000us = 2s,与 SOEM 的 EC_TIMEOUTSTATE 默认值一致。
|
||
# 此前各处传 2000 实际只等 2ms,能通过纯属从站 ack 快,属侥幸。
|
||
EC_TIMEOUT_STATE_US = 2_000_000
|
||
|
||
AL_STATE = {0x00: "NONE", 0x01: "INIT", 0x02: "PREOP",
|
||
0x03: "BOOT", 0x04: "SAFEOP", 0x08: "OP"}
|
||
|
||
|
||
def al_name(code):
|
||
return AL_STATE.get(code, f"0x{code:02X}")
|
||
|
||
|
||
def find_adapter(substr):
|
||
for a in pysoem.find_adapters():
|
||
desc = a.desc.decode() if isinstance(a.desc, bytes) else a.desc
|
||
if substr.lower() in desc.lower():
|
||
return a
|
||
return None
|
||
|
||
|
||
def open_master(substr):
|
||
adapter = find_adapter(substr)
|
||
if adapter is None:
|
||
print(f"没有找到描述包含 {substr!r} 的适配器,可用:")
|
||
list_adapters()
|
||
sys.exit(1)
|
||
desc = adapter.desc.decode() if isinstance(adapter.desc, bytes) else adapter.desc
|
||
master = pysoem.Master()
|
||
master.open(adapter.name)
|
||
try:
|
||
n = master.config_init()
|
||
except Exception as e:
|
||
raise SystemExit(f"总线上没有发现从站({type(e).__name__}: {e})——"
|
||
"检查 DCU 板供电、网线是否接到该网口")
|
||
if not n:
|
||
raise SystemExit("总线上没有发现从站——检查 DCU 板供电、网线是否接到该网口")
|
||
return master, desc
|
||
|
||
|
||
def list_adapters():
|
||
for i, a in enumerate(pysoem.find_adapters()):
|
||
desc = a.desc.decode() if isinstance(a.desc, bytes) else a.desc
|
||
print(f"[{i}] {desc}\n {a.name}")
|
||
|
||
|
||
def print_slave_info(master):
|
||
print(f"从站: {len(master.slaves)} 个")
|
||
for i, s in enumerate(master.slaves):
|
||
print(f" [{i}] name={s.name!r}")
|
||
print(f" VendorID=0x{s.man:08X} ProductCode=0x{s.id:08X} Rev={s.rev}")
|
||
print(f" AL state={al_name(s.state & 0x0F)} AL code=0x{s.al_status:04X}")
|
||
try:
|
||
print(f" 输出 {len(bytes(s.output))}B / 输入 {len(bytes(s.input))}B "
|
||
f"(config_map 后有效)")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
# ---------------------------------------------------------------- status
|
||
def cmd_status(args):
|
||
if args.substr in ("-l", "--list"):
|
||
list_adapters()
|
||
return
|
||
master, desc = open_master(args.substr)
|
||
print(f"适配器: {desc}")
|
||
print_slave_info(master)
|
||
if master.slaves and master.slaves[0].man == 0xFFFFFFFF:
|
||
print("\n提示: 身份字全 F = EEPROM 无效。先执行: ecat_tool.py flash")
|
||
|
||
|
||
# ---------------------------------------------------------------- eeprom
|
||
def eeprom_read_word(slave, word):
|
||
return bytes(slave.eeprom_read(word))[:2]
|
||
|
||
|
||
def flash_image(slave, img, label):
|
||
if len(img) % 2 != 0:
|
||
img += b"\xFF"
|
||
if len(img) > EEPROM_SIZE:
|
||
raise SystemExit(f"镜像 {len(img)}B 超过 EEPROM 容量 {EEPROM_SIZE}B")
|
||
# 字节序探针(写入 0x1234 找读回偏移,随后恢复擦除态)
|
||
test = b"\x34\x12"
|
||
slave.eeprom_write(0x0200, test)
|
||
rb = bytes(slave.eeprom_read(0x0200))
|
||
off = rb.find(test)
|
||
if off < 0:
|
||
raise SystemExit(f"EEPROM 写探针失败: 读回 {rb.hex().upper()}")
|
||
slave.eeprom_write(0x0200, b"\xFF\xFF")
|
||
print(f"字节序探针 OK (读回偏移 {off})")
|
||
|
||
n_written = n_fail = 0
|
||
fails = []
|
||
for word in range(len(img) // 2):
|
||
val = struct.unpack_from("<H", img, word * 2)[0]
|
||
if val == 0xFFFF:
|
||
continue
|
||
data = struct.pack("<H", val)
|
||
try:
|
||
slave.eeprom_write(word, data)
|
||
except Exception as e:
|
||
fails.append((word, data.hex(), f"WRITE EXC {type(e).__name__}: {e}"))
|
||
n_fail += 1
|
||
continue
|
||
if eeprom_read_word(slave, word) != data:
|
||
fails.append((word, data.hex(), eeprom_read_word(slave, word).hex()))
|
||
n_fail += 1
|
||
n_written += 1
|
||
if n_written % 64 == 0:
|
||
print(f" ... 已写 {n_written} 字")
|
||
print(f"[{label}] 刷写完成: 成功 {n_written} 字, 失败 {n_fail} 字")
|
||
if fails:
|
||
for w, exp, act in fails[:20]:
|
||
print(f" word 0x{w:04X}: 期望 {exp.upper()} 实际 {act.upper()}")
|
||
raise SystemExit(3)
|
||
print(">>> 板子断电重新上电后 ESC 才会重载配置(身份字/PDI/SYNC)")
|
||
|
||
|
||
def cmd_flash(args):
|
||
if args.bin:
|
||
with open(args.bin, "rb") as f:
|
||
img = f.read()
|
||
label = os.path.basename(args.bin)
|
||
if len(img) % 2:
|
||
img += b"\xFF"
|
||
else:
|
||
from ecat_flash_eeprom import load_image # 自带 gen_eeprom.py 自校验
|
||
img = load_image()
|
||
label = "gen_eeprom.py"
|
||
print(f"镜像: {label}, {len(img)} 字节")
|
||
master, desc = open_master(args.substr)
|
||
print(f"适配器: {desc}, 从站应答正常")
|
||
flash_image(master.slaves[0], img, label)
|
||
|
||
|
||
def cmd_eeprom(args):
|
||
master, _ = open_master(args.substr)
|
||
slave = master.slaves[0]
|
||
if args.op == "read":
|
||
start, count = args.start, args.count
|
||
rows = []
|
||
for w in range(start, start + count):
|
||
rows.append(struct.unpack("<H", eeprom_read_word(slave, w))[0])
|
||
for base in range(0, count, 8):
|
||
chunk = rows[base:base + 8]
|
||
print(f" 0x{start + base:04X}: " +
|
||
" ".join(f"{v:04X}" for v in chunk))
|
||
if args.out:
|
||
with open(args.out, "wb") as f:
|
||
f.write(struct.pack(f"<{count}H", *rows))
|
||
print(f"已保存 {args.out} ({count} word)")
|
||
elif args.op == "write":
|
||
with open(args.file, "rb") as f:
|
||
data = f.read()
|
||
if len(data) % 2:
|
||
data += b"\xFF"
|
||
if len(data) > EEPROM_SIZE:
|
||
raise SystemExit(f"{len(data)}B 超过 EEPROM 容量 {EEPROM_SIZE}B")
|
||
n = n_fail = 0
|
||
for word in range(len(data) // 2):
|
||
val = struct.unpack_from("<H", data, word * 2)[0]
|
||
if val == 0xFFFF:
|
||
continue
|
||
raw = struct.pack("<H", val)
|
||
slave.eeprom_write(word, raw)
|
||
if eeprom_read_word(slave, word) != raw:
|
||
print(f" word 0x{word:04X} 校验失败")
|
||
n_fail += 1
|
||
n += 1
|
||
print(f"写 {n} 字, 失败 {n_fail} 字" + ("" if not n_fail else " -> FAIL"))
|
||
sys.exit(0 if n_fail == 0 else 3)
|
||
elif args.op == "verify":
|
||
with open(args.file, "rb") as f:
|
||
data = f.read()
|
||
if len(data) % 2:
|
||
data += b"\xFF"
|
||
diffs = 0
|
||
for word in range(min(len(data), EEPROM_SIZE) // 2):
|
||
expect = struct.unpack_from("<H", data, word * 2)[0]
|
||
actual = struct.unpack("<H", eeprom_read_word(slave, word))[0]
|
||
if expect != actual:
|
||
print(f" word 0x{word:04X}: 文件 0x{expect:04X} != 芯片 0x{actual:04X}")
|
||
diffs += 1
|
||
print("verify: " + ("OK, 全部一致" if diffs == 0 else f"{diffs} 字不符 -> FAIL"))
|
||
sys.exit(0 if diffs == 0 else 3)
|
||
|
||
|
||
# ---------------------------------------------------------------- SDO
|
||
def cmd_sdo(args):
|
||
master, desc = open_master(args.substr)
|
||
s = master.slaves[0]
|
||
if args.op == "read":
|
||
try:
|
||
data = bytes(s.sdo_read(args.index, args.sub, args.size))
|
||
except Exception as e:
|
||
raise SystemExit(f"SDO 读失败: {type(e).__name__}: {e}")
|
||
print(f"0x{args.index:04X}:{args.sub} = {data.hex().upper()} ({len(data)}B)")
|
||
else:
|
||
try:
|
||
payload = bytes.fromhex(args.hex.replace(" ", ""))
|
||
except ValueError:
|
||
raise SystemExit("HEX 非法: 只能是十六进制字节流")
|
||
try:
|
||
s.sdo_write(args.index, args.sub, payload)
|
||
except Exception as e:
|
||
raise SystemExit(f"SDO 写失败: {type(e).__name__}: {e}")
|
||
print(f"0x{args.index:04X}:{args.sub} <- {payload.hex().upper()} ({len(payload)}B) OK")
|
||
|
||
|
||
# ---------------------------------------------------------------- PDO 收发
|
||
def enter_op(master, dc_cycle_us=0, dc_shift_us=0):
|
||
"""config_map → (可选 DC) → SAFEOP → OP。
|
||
|
||
dc_cycle_us=0 时保持 free-run(SYNC0 引脚无脉冲,从站 Sync0_Isr 不会触发)。
|
||
非 0 时先 config_dc() 测量传播延迟,再对从站下发 SYNC0 周期,
|
||
必须在 config_map() 之后、进 SAFEOP 之前完成(ETG.1020 DC 配置顺序)。
|
||
"""
|
||
master.state = pysoem.PREOP_STATE | pysoem.STATE_ACK
|
||
master.write_state()
|
||
if master.state_check(pysoem.PREOP_STATE, EC_TIMEOUT_STATE_US) != pysoem.PREOP_STATE:
|
||
# read_state 拿真实 AL Status Code(广播 state_check 不刷新 al_status,
|
||
# 陈旧缓存恒显示 0x0000)。首败常见 0x0016/0x001D 类:停止回 INIT 时 SSC
|
||
# 禁用了邮箱 SM0/SM1,CheckSmSettings 拒绝 INIT→PREOP,重新 config_init 即恢复。
|
||
try:
|
||
master.read_state()
|
||
except Exception:
|
||
pass
|
||
codes = [(al_name(sl.state & 0x0F), f"0x{sl.al_status:04X}") for sl in master.slaves]
|
||
raise SystemExit(f"进 PREOP 失败: AL={codes}(真实值) —— 从站未 ack AL Control=PREOP。"
|
||
"0x0016/0x001D 类=邮箱 SM0/SM1 被停止时禁用(重跑即恢复);"
|
||
"抓 GD32 串口看 [ECAT][AL] evt/req")
|
||
|
||
# free-run 时清残留 DC 激活(2026-09-03 00:27 卡死根因之一):之前跑过 DC 的
|
||
# 0x0981 激活位跨重连不清,从站会按 DC 同步模式等 PLL,free-run 下永不进 OP。
|
||
# 必须在 config_map(其内部请求 SAFEOP)之前清除。
|
||
if not dc_cycle_us:
|
||
try:
|
||
master.slaves[0].dc_sync(False, 0, 0)
|
||
print("已清除残留 DC 激活(free-run)")
|
||
except Exception as e:
|
||
print(f"WARN: 清 DC 激活失败({type(e).__name__}: {e})")
|
||
|
||
master.config_map()
|
||
|
||
# 不要写 0x1C32:01 / 0x1C33:01 去改 SyncType:该子索引只允许在 PREOP 写
|
||
# (ACCESS_WRITE_PREOP),而 config_map() 已把从站推到 SAFEOP,实测返回
|
||
# SdoError 0x08000022(当前状态不可写)。而且没必要——SSC 在 PREOP→SAFEOP 的
|
||
# StartInputHandler() 会自动按 DC 寄存器/PDO 长度推导出 SM_SYNCHRON
|
||
# (ecatslv.c:849-880),coeappl.c 里的 FREERUN 只是上电初值。
|
||
|
||
if dc_cycle_us:
|
||
has_dc = master.config_dc()
|
||
if not has_dc:
|
||
raise SystemExit("config_dc(): 总线上没有支持 DC 的从站 —— "
|
||
"检查 SII word1 高字节(0x0151 Sync/Latch 配置)是否为 0xCC")
|
||
master.slaves[0].dc_sync(True, dc_cycle_us * 1000, dc_shift_us * 1000)
|
||
print(f"DC 已启用: SYNC0 周期 {dc_cycle_us}us"
|
||
+ (f", shift {dc_shift_us}us" if dc_shift_us else "")
|
||
+ f", 主站 DC 时间={master.dc_time}ns")
|
||
|
||
# 显式带 ACK 写 SAFEOP(config_map 内部已推过一次,这里再立一次并顺带清掉
|
||
# 上一轮残留的错误位——SSC 只有带 ACK 位的写才能清错+迁移,普通写会被忽略)
|
||
master.state = pysoem.SAFEOP_STATE | pysoem.STATE_ACK
|
||
master.write_state()
|
||
if master.state_check(pysoem.SAFEOP_STATE, EC_TIMEOUT_STATE_US) != pysoem.SAFEOP_STATE:
|
||
try:
|
||
master.read_state() # 真实 AL Status Code(广播 state_check 不刷新)
|
||
except Exception:
|
||
pass
|
||
codes = [(al_name(sl.state & 0x0F), f"0x{sl.al_status:04X}") for sl in master.slaves]
|
||
raise SystemExit(f"进 SAFEOP 失败: AL={codes}(真实值) —— 检查 SII 的 SM/FMMU 类别"
|
||
"(EEPROM 是否已刷)与 GD32 SSC 是否运行")
|
||
|
||
# 主站侧直写过程数据看门狗(双保险,SOEM set_watchdog → FPWR 0x0420 = 1000ms)。
|
||
# 从站固件 ≥1.5.45 也会写 1s,但板上固件未更新时 100ms 复位默认值会在 OP 等待期
|
||
# 触发 0x001B 拖回 SAFEOP;主站每次连接直接写,不依赖固件版本。
|
||
try:
|
||
master.slaves[0].set_watchdog('processdata', 1000.0)
|
||
except Exception as e:
|
||
print(f"WARN: 主站侧写看门狗失败({type(e).__name__}: {e})")
|
||
|
||
# SAFEOP -> OP:从站有输出 PDO 时,SSC 要求先收到至少一帧过程数据才 ack OP
|
||
# (ecatslv.c:1993 的 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`),
|
||
# 否则迁移被挂起、随后回落,表现为 "进 OP 失败 ... code=0x0000"。
|
||
# 所以必须边发 processdata 边等,不能只 write_state 后干等。
|
||
# AL Control 一律带 ACK 位(0x10):SSC 对"错误位已置+无 ACK+请求≠INIT"的请求
|
||
# 直接忽略(ecatslv.c:1376-1384),残留的 0x001B 错误位不清掉,写 OP 无人应答。
|
||
master.state = pysoem.OP_STATE | pysoem.STATE_ACK
|
||
master.write_state()
|
||
|
||
# 状态查询用 read_state() 而非 state_check():后者 slave=0 广播模式不更新
|
||
# slavelist[0].ALstatuscode(报错里的 code 是陈旧缓存,恒 0x0000),且内部
|
||
# 1ms 轮询发帧风暴。read_state 无错误时 1 帧 BRD,有错误位时逐从站 FPRD
|
||
# 拿**真实 AL Status Code**。失败时输出真实错误码+状态轨迹。
|
||
deadline = time.time() + 12.0 # 12s:覆盖从站 ESM 超时(9s,固件≥1.5.47)+恢复重试
|
||
trail = []
|
||
last_key = None
|
||
state = None
|
||
code = 0
|
||
wkc_samples = []
|
||
next_rewrite = 0.0 # OP|ACK 重写节流:每 200ms 最多一次(避免 AL Control 写风暴)
|
||
while time.time() < deadline:
|
||
master.send_processdata()
|
||
wkc = master.receive_processdata(2000)
|
||
if not wkc_samples or wkc_samples[-1] != (wkc, master.expected_wkc):
|
||
wkc_samples.append((wkc, master.expected_wkc))
|
||
try:
|
||
master.read_state()
|
||
except Exception:
|
||
pass
|
||
state = master.slaves[0].state & 0x0F
|
||
code = master.slaves[0].al_status
|
||
key = (state, code)
|
||
if key != last_key:
|
||
trail.append((al_name(state), code))
|
||
last_key = key
|
||
if state == pysoem.OP_STATE and code == 0:
|
||
break
|
||
# 跌回 PREOP/INIT 时:带 ACK 重写 SAFEOP 立起状态,再带 ACK 重请 OP
|
||
if state < pysoem.SAFEOP_STATE:
|
||
master.state = pysoem.SAFEOP_STATE | pysoem.STATE_ACK
|
||
master.write_state()
|
||
master.state_check(pysoem.SAFEOP_STATE, 500_000)
|
||
next_rewrite = 0.0
|
||
if time.time() >= next_rewrite:
|
||
master.state = pysoem.OP_STATE | pysoem.STATE_ACK
|
||
master.write_state()
|
||
next_rewrite = time.time() + 0.2
|
||
time.sleep(0.005) # 循环节奏 ~7ms:LRW 间隔稳低于任何看门狗配置
|
||
|
||
if state != pysoem.OP_STATE or code != 0:
|
||
try:
|
||
master.read_state()
|
||
except Exception:
|
||
pass
|
||
st = master.slaves[0]
|
||
trail.append((al_name(st.state & 0x0F), st.al_status))
|
||
try:
|
||
wd_ms = st.get_watchdog('processdata')
|
||
wd_info = f", 从站 PD 看门狗={wd_ms:.0f}ms"
|
||
except Exception:
|
||
wd_info = ""
|
||
wkc_info = " / ".join(f"wkc={w}/{e}" for w, e in wkc_samples[:6])
|
||
raise SystemExit(
|
||
f"进 OP 失败: AL={al_name(st.state & 0x0F)} code=0x{st.al_status:04X}(真实值{wd_info}) —— "
|
||
f"等待期轨迹: {' -> '.join(f'{n}+0x{c:04X}' for n, c in trail)}; LRW WKC: {wkc_info}。"
|
||
"0x001B=SM watchdog;0x001A=DC 残留激活;0x001D/0x001E=SM 配置不匹配;"
|
||
"0x0000=从站在等首帧输出。请抓 GD32 串口 [ECAT][AL] evt/req 日志对照")
|
||
|
||
|
||
def leave_init(master):
|
||
try:
|
||
master.state = pysoem.INIT_STATE
|
||
master.write_state()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def run_pdo(master, out_data, rate_ms, duration, label, dc_cycle_us=0, dc_shift_us=0):
|
||
out_len = len(bytes(master.slaves[0].output))
|
||
if out_data is None:
|
||
out_data = bytes(out_len)
|
||
if len(out_data) < out_len:
|
||
out_data += bytes(out_len - len(out_data))
|
||
try:
|
||
master.slaves[0].output = out_data
|
||
except Exception as e:
|
||
raise SystemExit(f"写输出缓冲失败({type(e).__name__}: {e}) —— output 长度={out_len}")
|
||
|
||
enter_op(master, dc_cycle_us, dc_shift_us)
|
||
mode = f"DC {dc_cycle_us}us" if dc_cycle_us else "free-run"
|
||
print(f"[{label}] 已进 OP({mode}): 输出 {out_len}B / 输入 "
|
||
f"{len(bytes(master.slaves[0].input))}B, expected_wkc={master.expected_wkc}"
|
||
f"{', Ctrl+C 退出' if duration is None else f', 运行 {duration}s'}")
|
||
t_end = None if duration is None else time.time() + duration
|
||
cycles = wkc_errs = 0
|
||
last = 0.0
|
||
try:
|
||
while True:
|
||
master.send_processdata()
|
||
wkc = master.receive_processdata(2000)
|
||
cycles += 1
|
||
if wkc < master.expected_wkc:
|
||
wkc_errs += 1
|
||
now = time.time()
|
||
if now - last >= 0.5:
|
||
last = now
|
||
inp = bytes(master.slaves[0].input)
|
||
extra = ""
|
||
if len(inp) >= 32:
|
||
u64 = struct.unpack("<4Q", inp[:32])
|
||
extra = " u64=" + ",".join(f"0x{x:X}" for x in u64)
|
||
print(f"cycles={cycles} wkc_err={wkc_errs} in={inp.hex().upper()}{extra}")
|
||
if t_end is not None and now >= t_end:
|
||
break
|
||
time.sleep(rate_ms / 1000.0)
|
||
except KeyboardInterrupt:
|
||
print("\n用户中断")
|
||
finally:
|
||
leave_init(master)
|
||
print(f"[{label}] 结束: {cycles} 周期, wkc_err={wkc_errs}, 已回 INIT")
|
||
|
||
|
||
def parse_output_data(args):
|
||
if args.hex:
|
||
try:
|
||
return bytes.fromhex(args.hex.replace(" ", ""))
|
||
except ValueError:
|
||
raise SystemExit("--hex 非法: 只能是十六进制字节流")
|
||
if args.u64:
|
||
try:
|
||
vals = [int(v, 0) for v in args.u64.split(",")]
|
||
except ValueError:
|
||
raise SystemExit("--u64 非法: 逗号分隔的整数(支持 0x 前缀)")
|
||
if len(vals) > 4:
|
||
raise SystemExit("--u64 最多 4 个(4×64bit=32B)")
|
||
return struct.pack(f"<{len(vals)}Q", *vals)
|
||
return None
|
||
|
||
|
||
def cmd_send(args):
|
||
master, desc = open_master(args.substr)
|
||
print(f"适配器: {desc}")
|
||
run_pdo(master, parse_output_data(args), args.rate_ms,
|
||
None if args.forever else args.duration, "send",
|
||
args.dc_cycle_us, args.dc_shift_us)
|
||
|
||
|
||
def cmd_monitor(args):
|
||
master, desc = open_master(args.substr)
|
||
print(f"适配器: {desc}")
|
||
run_pdo(master, None, args.rate_ms,
|
||
None if args.forever else args.duration, "monitor",
|
||
args.dc_cycle_us, args.dc_shift_us)
|
||
|
||
|
||
# ---------------------------------------------------------------- main
|
||
def main():
|
||
p = argparse.ArgumentParser(description="DCU LAN9252 EtherCAT 主站工具")
|
||
sub = p.add_subparsers(dest="cmd", required=False)
|
||
|
||
sub.add_parser("adapters", help="列出 Npcap 适配器")
|
||
|
||
sp = sub.add_parser("status", help="扫描从站身份/AL 状态")
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
sp = sub.add_parser("flash", help="烧录 EEPROM + 逐字校验")
|
||
sp.add_argument("--bin", help="指定 bin(默认用 gen_eeprom.py 镜像)")
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
sp = sub.add_parser("eeprom", help="EEPROM read/write/verify")
|
||
sp.add_argument("op", choices=["read", "write", "verify"])
|
||
sp.add_argument("file_or_start", nargs="?", default=None)
|
||
sp.add_argument("count_or_file", nargs="?", default=None)
|
||
sp.add_argument("--out")
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
sp = sub.add_parser("sdo", help="CoE SDO read/write")
|
||
sp.add_argument("op", choices=["read", "write"])
|
||
sp.add_argument("index", type=lambda v: int(v, 0))
|
||
sp.add_argument("sub", type=lambda v: int(v, 0))
|
||
sp.add_argument("hex", nargs="?", default=None)
|
||
sp.add_argument("--size", type=lambda v: int(v, 0), default=8)
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
sp = sub.add_parser("send", help="进 OP 循环发送输出 PDO")
|
||
sp.add_argument("--hex", help="输出字节流(≤输出区长度)")
|
||
sp.add_argument("--u64", help="4 个 64bit, 逗号分隔, 如 0x1,0x2,0x3,0x4")
|
||
sp.add_argument("--rate-ms", type=int, default=100)
|
||
sp.add_argument("--duration", type=float, default=5.0)
|
||
sp.add_argument("--forever", action="store_true")
|
||
sp.add_argument("--dc-cycle-us", type=int, default=0,
|
||
help="启用 DC 并设 SYNC0 周期(us); 0=free-run(默认, SYNC0 不打)")
|
||
sp.add_argument("--dc-shift-us", type=int, default=0,
|
||
help="SYNC0 相移(us), 仅在 --dc-cycle-us 非 0 时有效")
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
sp = sub.add_parser("monitor", help="进 OP 只读输入 PDO")
|
||
sp.add_argument("--rate-ms", type=int, default=200)
|
||
sp.add_argument("--duration", type=float, default=10.0)
|
||
sp.add_argument("--forever", action="store_true")
|
||
sp.add_argument("--dc-cycle-us", type=int, default=0,
|
||
help="启用 DC 并设 SYNC0 周期(us); 0=free-run(默认, SYNC0 不打)")
|
||
sp.add_argument("--dc-shift-us", type=int, default=0,
|
||
help="SYNC0 相移(us), 仅在 --dc-cycle-us 非 0 时有效")
|
||
sp.add_argument("substr", nargs="?", default="I226")
|
||
|
||
args = p.parse_args()
|
||
if args.cmd is None:
|
||
# 双击/无参数时默认执行 status
|
||
print("(未指定子命令, 默认执行 status; 完整用法: ecat_tool.py -h)\n")
|
||
args.cmd = "status"
|
||
args.substr = "I226"
|
||
if args.cmd == "adapters":
|
||
list_adapters()
|
||
elif args.cmd == "status":
|
||
cmd_status(args)
|
||
elif args.cmd == "flash":
|
||
cmd_flash(args)
|
||
elif args.cmd == "eeprom":
|
||
if args.op == "read":
|
||
if args.file_or_start is None or args.count_or_file is None:
|
||
raise SystemExit("用法: eeprom read START COUNT [--out FILE]")
|
||
args.start = int(args.file_or_start, 0)
|
||
args.count = int(args.count_or_file, 0)
|
||
else:
|
||
if args.file_or_start is None:
|
||
raise SystemExit(f"用法: eeprom {args.op} FILE")
|
||
args.file = args.file_or_start
|
||
cmd_eeprom(args)
|
||
elif args.cmd == "sdo":
|
||
if args.op == "write" and args.hex is None:
|
||
raise SystemExit("用法: sdo write IDX SUB HEX")
|
||
cmd_sdo(args)
|
||
elif args.cmd == "send":
|
||
cmd_send(args)
|
||
elif args.cmd == "monitor":
|
||
cmd_monitor(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|