现有工具纳入版本管理: - 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。
282 lines
11 KiB
Python
282 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""ESC 网络侧寄存器诊断(绕过 GD32/HBI,直接从 EtherCAT 线上读 LAN9252)。
|
||
|
||
背景:GD32 侧 1.5.37 实测 EEPROM 300ms 不加载(DL_STATUS/EEP_CSR 经 PDI 读全 0),
|
||
但 PDI 侧读全 0 也可能是"PDI 未使能"的次生现象,不能区分:
|
||
A) EEPROM boot 加载真的失败(I2C 无应答 / CRC 错 / 寻址错)
|
||
B) 加载其实成功,PDI 通路另有问题
|
||
本工具从网络侧读真实寄存器一锤定音,并可用 --reload 命令 ESC 当场重新加载验证。
|
||
|
||
步骤:
|
||
1) pysoem 读 SII 前 16 字 + 重算 CRC -> EEPROM 内容是否完好(网络侧 I2C 是否可用)
|
||
2) Npcap 裸 EtherCAT 帧 APRD 读 0x0000/0x0010/0x0110/0x0130/0x0140/0x0500/0x0502
|
||
-> EEP Status(0x0502) 的 loaded/NACK/CRC 错误位
|
||
3) --reload:APWR 0x0502=0x0300 触发重新加载,再读一遍状态对比
|
||
|
||
用法:ecat_esc_regs.py [adapter-substr] [--reload] (默认 I226)
|
||
"""
|
||
import ctypes
|
||
import struct
|
||
import sys
|
||
import time
|
||
|
||
import pysoem
|
||
|
||
ECAT_ETHER_TYPE = 0x88A4
|
||
CMD_APRD = 0x07
|
||
CMD_APWR = 0x08
|
||
|
||
REGS = [
|
||
(0x0000, "Type/Revision"),
|
||
(0x0010, "Station Address"),
|
||
(0x0110, "DL Status"),
|
||
(0x0130, "AL Status"),
|
||
(0x0140, "ESC Config(PDI type)"),
|
||
(0x0500, "EEPROM Control"),
|
||
(0x0502, "EEPROM Status"),
|
||
]
|
||
|
||
EEP_FORCE_ECAT = 0x0001 # 0x0500 bit0: ECAT 侧强制取得 EEPROM 接口
|
||
EEP_CMD_RELOAD = 0x0300 # 0x0502: reload 命令(同 SOEM EC_ECMD_RELOAD)
|
||
|
||
|
||
def crc8_sii(data):
|
||
"""ETG.1000.6 word-7 checksum: CRC-8 poly 0x07, init 0xFF。"""
|
||
crc = 0xFF
|
||
for b in data:
|
||
crc ^= b
|
||
for _ in range(8):
|
||
crc = ((crc << 1) ^ 0x07) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF
|
||
return crc
|
||
|
||
|
||
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
|
||
raise SystemExit(f"没有找到描述包含 {substr!r} 的适配器")
|
||
|
||
|
||
# ---------- 第 1 步:pysoem 读 SII 内容 + CRC 复算 ----------
|
||
def sii_check(adapter_name):
|
||
master = pysoem.Master()
|
||
master.open(adapter_name)
|
||
try:
|
||
if not master.config_init():
|
||
print("!! 总线上没有从站应答")
|
||
return False
|
||
slave = master.slaves[0]
|
||
# 与 ecat_flash_eeprom.py 相同的读回偏移探针(写完即恢复)
|
||
probe = b"\xC3\xA5"
|
||
slave.eeprom_write(0x0200, probe)
|
||
rb = bytes(slave.eeprom_read(0x0200))
|
||
off = rb.find(probe)
|
||
slave.eeprom_write(0x0200, b"\xff\xff")
|
||
if off < 0:
|
||
print(f"!! 探针字读写失败,读回 {rb.hex().upper()}")
|
||
print("!! 网络侧 EEPROM 访问已坏 -> I2C 物理层(R12 SDA 上拉/EEPROM 焊接)")
|
||
return False
|
||
words = []
|
||
for w in range(16):
|
||
rb = bytes(slave.eeprom_read(w))
|
||
words.append(struct.unpack_from("<H", rb, off)[0])
|
||
img14 = b"".join(struct.pack("<H", v) for v in words[:7])
|
||
crc_calc = crc8_sii(img14)
|
||
crc_stored = words[7] & 0xFF
|
||
print("SII word0-15: " + " ".join(f"{v:04X}" for v in words))
|
||
print(f" PDI Control = 0x{words[0]:04X}(低字节期望 0x8D = HBI 16-bit)")
|
||
print(f" Checksum = 0x{crc_stored:02X},重算 = 0x{crc_calc:02X} -> "
|
||
f"{'MATCH' if crc_calc == crc_stored else '*** MISMATCH ***'}")
|
||
vid = words[8] | (words[9] << 16)
|
||
pc = words[10] | (words[11] << 16)
|
||
print(f" VendorID=0x{vid:08X} ProductCode=0x{pc:08X}")
|
||
return crc_calc == crc_stored
|
||
finally:
|
||
master.close()
|
||
|
||
|
||
# ---------- 第 2 步:Npcap 裸 EtherCAT 帧读 ESC 寄存器 ----------
|
||
class PcapPktHdr(ctypes.Structure):
|
||
_fields_ = [("tv_sec", ctypes.c_long), ("tv_usec", ctypes.c_long),
|
||
("caplen", ctypes.c_uint32), ("len", ctypes.c_uint32)]
|
||
|
||
|
||
def load_wpcap():
|
||
for cand in ("wpcap.dll", r"C:\Windows\System32\Npcap\wpcap.dll"):
|
||
try:
|
||
return ctypes.WinDLL(cand)
|
||
except OSError:
|
||
continue
|
||
raise SystemExit("找不到 wpcap.dll(Npcap)")
|
||
|
||
|
||
def ecat_frame(cmd, idx, adp, ado, payload):
|
||
"""帧布局按 ecat_sniff.py 对 SOEM 流量的实测:
|
||
EtherCAT 头 len = datagram 区长度(不含自身 2 字节);
|
||
datagram 头 = 10 字节(cmd/idx/adp/ado/len/irq=0),后接 data + WKC(2)。"""
|
||
dst = b"\xff" * 6
|
||
src = bytes([0x02, 0x44, 0x43, 0x55, 0x00, 0x01]) # 本地管理 MAC,仅作源标识
|
||
eth = dst + src + struct.pack(">H", ECAT_ETHER_TYPE)
|
||
dlen = 10 + len(payload) + 2
|
||
ecat_hdr = struct.pack("<H", (dlen & 0x7FF) | (0x1 << 12))
|
||
dgram = (struct.pack("<BBHHHH", cmd, idx, adp, ado, len(payload) & 0x7FF, 0)
|
||
+ payload + b"\x00\x00")
|
||
return (eth + ecat_hdr + dgram).ljust(60, b"\x00")
|
||
|
||
|
||
class RawBus:
|
||
def __init__(self, devname):
|
||
wpcap = load_wpcap()
|
||
# 64-bit 下必须显式声明签名:默认 restype=c_int 会把 pcap_t* 截成 32 位
|
||
pkt_pp = ctypes.POINTER(ctypes.POINTER(PcapPktHdr))
|
||
data_pp = ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte))
|
||
wpcap.pcap_open_live.restype = ctypes.c_void_p
|
||
wpcap.pcap_open_live.argtypes = [ctypes.c_char_p] + [ctypes.c_int] * 3 + [ctypes.c_char_p]
|
||
wpcap.pcap_sendpacket.restype = ctypes.c_int
|
||
wpcap.pcap_sendpacket.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
||
wpcap.pcap_next_ex.restype = ctypes.c_int
|
||
wpcap.pcap_next_ex.argtypes = [ctypes.c_void_p, pkt_pp, data_pp]
|
||
wpcap.pcap_close.restype = None
|
||
wpcap.pcap_close.argtypes = [ctypes.c_void_p]
|
||
wpcap.pcap_setdirection.restype = ctypes.c_int
|
||
wpcap.pcap_setdirection.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||
errbuf = ctypes.create_string_buffer(256)
|
||
name = devname.decode() if isinstance(devname, bytes) else devname
|
||
self.handle = wpcap.pcap_open_live(name.encode(), 65535, 1, 100, errbuf)
|
||
if not self.handle:
|
||
raise SystemExit(f"pcap_open_live 失败: {errbuf.value.decode(errors='replace')}")
|
||
self.wpcap = wpcap
|
||
self.idx = 0
|
||
|
||
def transact(self, cmd, ado, payload, timeout=1.5):
|
||
self.idx = (self.idx + 1) & 0xFF
|
||
idx = self.idx
|
||
frame = ecat_frame(cmd, idx, 0x0000, ado, payload)
|
||
if self.wpcap.pcap_sendpacket(self.handle, frame, len(frame)) != 0:
|
||
raise SystemExit("pcap_sendpacket 失败")
|
||
deadline = time.time() + timeout
|
||
ph = ctypes.POINTER(PcapPktHdr)()
|
||
pd = ctypes.POINTER(ctypes.c_ubyte)()
|
||
while time.time() < deadline:
|
||
r = self.wpcap.pcap_next_ex(self.handle, ctypes.byref(ph), ctypes.byref(pd))
|
||
if r != 1:
|
||
continue
|
||
data = bytes(bytearray(pd[:ph.contents.caplen]))
|
||
if len(data) < 28 or data[12:14] != b"\x88\xa4":
|
||
continue
|
||
d = data[16:]
|
||
if d[0] != cmd or d[1] != idx:
|
||
continue
|
||
if struct.unpack_from("<H", d, 4)[0] != ado:
|
||
continue # 并发主站流量的同 idx 帧
|
||
dlen = struct.unpack_from("<H", d, 6)[0] & 0x7FF
|
||
wkc = struct.unpack_from("<H", d, 10 + dlen)[0]
|
||
if wkc == 0:
|
||
continue # 自己发出去的帧(WKC=0),等回帧
|
||
return d[10:10 + dlen], wkc
|
||
return None, None
|
||
|
||
def read16(self, addr):
|
||
payload, wkc = self.transact(CMD_APRD, addr, b"\x00\x00")
|
||
if payload is None or wkc == 0:
|
||
return None
|
||
return struct.unpack_from("<H", payload)[0]
|
||
|
||
def close(self):
|
||
self.wpcap.pcap_close(self.handle)
|
||
|
||
|
||
def decode_eep_status(v):
|
||
bits = []
|
||
if v & 0x0800:
|
||
bits.append("LOADED")
|
||
if v & 0x1000:
|
||
bits.append("ERR:NACK(EEPROM 无应答 -> I2C 物理层: R12 SDA 上拉/焊接)")
|
||
if v & 0x2000:
|
||
bits.append("ERR:CRC(SII 校验失败 -> 内容或寻址)")
|
||
if v & 0x4000:
|
||
bits.append("ERR:CMD")
|
||
if v & 0x8000:
|
||
bits.append("BUSY")
|
||
return "|".join(bits) if bits else "(无错误位置位)"
|
||
|
||
|
||
def dump_regs(bus, tag):
|
||
vals = {}
|
||
print(f"--- ESC 寄存器({tag})---")
|
||
for addr, name in REGS:
|
||
v = bus.read16(addr)
|
||
vals[addr] = v
|
||
if v is None:
|
||
print(f" 0x{addr:04X} {name}: *** 无应答 ***")
|
||
continue
|
||
extra = ""
|
||
if addr == 0x0502:
|
||
extra = " | " + decode_eep_status(v)
|
||
elif addr == 0x0140:
|
||
extra = f" | pdi_type=0x{v & 0xFF:02X}(期望 0x8D)"
|
||
elif addr == 0x0110:
|
||
extra = f" | pdi_oper={v & 0x1}"
|
||
print(f" 0x{addr:04X} {name}: 0x{v:04X}{extra}")
|
||
return vals
|
||
|
||
|
||
def main():
|
||
substr = "I226"
|
||
do_reload = False
|
||
for arg in sys.argv[1:]:
|
||
if arg == "--reload":
|
||
do_reload = True
|
||
else:
|
||
substr = arg
|
||
adapter = find_adapter(substr)
|
||
print(f"适配器: {adapter.desc.decode() if isinstance(adapter.desc, bytes) else adapter.desc}")
|
||
|
||
crc_ok = sii_check(adapter.name)
|
||
|
||
bus = RawBus(adapter.name)
|
||
try:
|
||
vals = dump_regs(bus, "当前")
|
||
if do_reload:
|
||
print("\n触发 EEPROM 重新加载(0x0500=0x0001, 0x0502=0x0300)…")
|
||
bus.transact(CMD_APWR, 0x0500, struct.pack("<H", EEP_FORCE_ECAT))
|
||
payload, wkc = bus.transact(CMD_APWR, 0x0502, struct.pack("<H", EEP_CMD_RELOAD))
|
||
if payload is None:
|
||
print(" reload 写无应答")
|
||
else:
|
||
for _ in range(20): # 最长等 ~1s
|
||
time.sleep(0.05)
|
||
st = bus.read16(0x0502)
|
||
if st is not None and not (st & 0x8000):
|
||
break
|
||
vals = dump_regs(bus, "reload 后")
|
||
finally:
|
||
bus.close()
|
||
|
||
print("\n===== 判读 =====")
|
||
st = vals.get(0x0502)
|
||
cfg = vals.get(0x0140)
|
||
if st is None:
|
||
print("ESC 连网络侧寄存器都读不到——超出本工具范围。")
|
||
elif st & 0x0800 and cfg is not None and (cfg & 0xFF) == 0x8D:
|
||
print("EEPROM 实际已加载、PDI 类型 0x8D 已生效!")
|
||
print("=> GD32 侧读全 0 不是加载失败,而是 HBI 访问核心寄存器的通路问题,转查 PDI。")
|
||
elif st & 0x1000:
|
||
print("EEPROM 加载失败:NACK —— LAN9252 在 I2C 上得不到 EEPROM 应答。")
|
||
print("=> 硬件:量 AT24C32 pin6(SCL)/pin5(SDA) 空闲电平应≈3.3V;")
|
||
print(" SDA 若≈0V 或悬空 -> R12(10k->D3V3) 没焊(原理图标 DNP 的那颗)。")
|
||
elif st & 0x2000:
|
||
print("EEPROM 加载失败:CRC 校验错。")
|
||
print("=> 内容/CRC/寻址问题:上方 SII CRC 重算" +
|
||
("也不符 -> 重跑 ecat_flash_eeprom.py 重刷" if not crc_ok
|
||
else "相符 -> 内容对但 loader 读错位置,查 E2PSIZE strap 寻址"))
|
||
elif not (st & 0x0800):
|
||
print("loaded=0 且无错误位——loader 未跑/未跑完,异常状态,把完整输出发回分析。")
|
||
else:
|
||
print("loaded=1 但 pdi_type != 0x8D——EEPROM word0 不对,重刷 SII。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|