Files
DCU_ECAT_Master/ecat_esc_regs.py
T
cbb d594b8fd94 init: DCU LAN9252 EtherCAT 主站工具集
现有工具纳入版本管理:
- 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。
2026-09-03 01:56:29 +08:00

282 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()