app
LAN7800 EEPROM Programmer
Public Made by Adomby adom
MPLAB-Connect-parity EEPROM tool for the Microchip LAN7800 USB-3.1 to Gigabit-Ethernet controller (VID 0x0424 / PID 0x7800). Read an adapter's EEPROM to a .bin, edit MAC / VID / PID / bcdDevice / strings / serial and LED/GPIO with a byte-exact preview, and program hardware with verify-after-write. Reads are free; physical writes are gated behind --force-physical-write. Ships an AI-oriented CLI (single source of truth) plus a Hydrogen webapp that shells out to it.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
"""LAN7800 EEPROM image model.
Pure, hardware-free representation of a LAN7800 EEPROM image (a raw .bin of
256 or 512 bytes) plus typed accessors for the fields the Core v1 tool edits:
signature, MAC, VID/PID/bcdDevice (across the SS/HS/FS descriptor blocks),
string descriptors, and serial.
Layout facts come from docs/01-eeprom-mechanism.md (datasheet DS00001992G
Table 10-2 + lan78xx.c). Anything the doc flagged "VERIFY against a real dump"
is centralized here as a named constant with a comment so it can be corrected
in one place once a physical adapter is available.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
# ---- constants (authoritative: lan78xx.c + datasheet Table 10-2) ----------
EEPROM_MAGIC = 0x78A5 # ethtool -E magic for LAN78xx
SIG_OFFSET = 0x00
SIG_VALUE = 0xA5 # byte0; device ignores EEPROM if != 0xA5
MAC_OFFSET = 0x01
MAC_LEN = 6
MICROCHIP_OUI = (0x00, 0x80, 0x0F)
MAX_EEPROM_SIZE = 512
SIZES = (256, 512)
# String descriptor pointer table: 5 entries at 0x25..0x2E, each (len, word_off).
STR_TABLE_OFFSET = 0x25
STR_NAMES = ("manufacturer", "product", "serial", "configuration", "interface")
# Config Flags 0..3: four 32-bit LE words at 0x13/0x17/0x1B/0x1F (docs/01).
CONFIG_FLAGS_OFFSET = 0x13
CONFIG_FLAGS_COUNT = 4
# Device-descriptor block pointers (word units). VID/PID/bcdDevice live inside.
# LAN7800 exposes SuperSpeed + HighSpeed + FullSpeed descriptor blocks.
DESC_PTRS = {"ss": 0x31, "hs": 0x35, "fs": 0x39}
# offsets within a standard USB device descriptor:
DD_VID = 8 # idVendor (LE u16)
DD_PID = 10 # idProduct (LE u16)
DD_BCDDEV = 12 # bcdDevice (LE u16)
DD_LEN = 18 # bLength of a device descriptor (0x12)
# LAN7800 identity guard — this tool is LAN7800 ONLY.
LAN7800_VID = 0x0424
LAN7800_PID = 0x7800
# MAC octet order in EEPROM. RESOLVED against real hardware (2026-07-30): the
# lan78xx driver reads EEPROM offset 0x01 as addr[0], i.e. NATURAL order, so
# MAC AA:BB:CC:DD:EE:FF is stored AA BB CC DD EE FF (0x01=AA ... 0x06=FF).
# The datasheet doc's "little-endian FF EE DD CC BB AA" note was WRONG: entering
# the device's known MAC 00:80:0F:78:00:00 under LE produced the byte-reverse
# 00:00:78:0F:80:00 on the wire. Natural order is confirmed correct.
MAC_LITTLE_ENDIAN = False
class EepromError(Exception):
pass
@dataclass
class DescriptorView:
kind: str # ss / hs / fs
ptr_word: int # raw pointer value (word units)
byte_offset: int # resolved byte offset of the descriptor block
present: bool
looks_valid: bool # bLength==0x12 and bDescriptorType==0x01
vid: Optional[int] = None
pid: Optional[int] = None
bcd_device: Optional[int] = None
@dataclass
class StringView:
name: str
length_field: int
word_offset: int
byte_offset: int
present: bool
value: Optional[str] = None
@dataclass
class Eeprom:
data: bytearray
# ---- construction ------------------------------------------------------
@classmethod
def from_bytes(cls, raw: bytes) -> "Eeprom":
if len(raw) not in SIZES:
# tolerate any length up to 512 by padding to the nearest known size
if len(raw) == 0 or len(raw) > MAX_EEPROM_SIZE:
raise EepromError(
f"unexpected EEPROM length {len(raw)}; expected one of {SIZES}")
size = 256 if len(raw) <= 256 else 512
buf = bytearray(raw) + bytearray(size - len(raw))
return cls(buf)
return cls(bytearray(raw))
@classmethod
def blank(cls, size: int = MAX_EEPROM_SIZE) -> "Eeprom":
if size not in SIZES:
raise EepromError(f"size must be one of {SIZES}")
return cls(bytearray(size))
@classmethod
def default_template(cls, size: int = MAX_EEPROM_SIZE) -> "Eeprom":
"""A sensible DEFAULT LAN7800 image: 0xA5 signature, a Microchip-OUI MAC
placeholder (00:80:0F:00:00:00), and SS/HS/FS USB device-descriptor
blocks carrying VID 0x0424 / PID 0x7800 / bcdDevice 0x0300. Strings are
left blank. This is the single source of truth for the webapp's default
pre-populated fields. Descriptor blocks sit at 0x60/0x80/0xA0, clear of
the fixed LED/GPIO register offsets."""
import struct
e = cls.blank(size)
e.set_signature()
e.set_mac("00:80:0F:00:00:00")
dd = struct.pack("<BBHBBBBHHHBBBB", 0x12, 0x01, 0x0300, 0xFF, 0x00, 0x00,
64, LAN7800_VID, LAN7800_PID, 0x0300, 0, 0, 0, 1)
block = 0x60
for kind in ("ss", "hs", "fs"):
e.data[block:block + len(dd)] = dd
e._put_u16le(DESC_PTRS[kind], block // 2) # pointer in word units
block += 24
return e
def to_bytes(self) -> bytes:
return bytes(self.data)
@property
def size(self) -> int:
return len(self.data)
# ---- low-level helpers -------------------------------------------------
def _u16le(self, off: int) -> int:
if off + 1 >= self.size:
raise EepromError(f"read u16 out of range at 0x{off:02X}")
return self.data[off] | (self.data[off + 1] << 8)
def _put_u16le(self, off: int, val: int) -> None:
if off + 1 >= self.size:
raise EepromError(f"write u16 out of range at 0x{off:02X}")
self.data[off] = val & 0xFF
self.data[off + 1] = (val >> 8) & 0xFF
def _u32le(self, off: int) -> int:
return (self.data[off] | (self.data[off + 1] << 8)
| (self.data[off + 2] << 16) | (self.data[off + 3] << 24))
def _put_u32le(self, off: int, val: int) -> None:
for i in range(4):
self.data[off + i] = (val >> (8 * i)) & 0xFF
# ---- config flags ------------------------------------------------------
def config_flags(self) -> list[int]:
return [self._u32le(CONFIG_FLAGS_OFFSET + 4 * i)
for i in range(CONFIG_FLAGS_COUNT)]
def set_config_flag(self, i: int, val: int) -> None:
if not 0 <= i < CONFIG_FLAGS_COUNT:
raise EepromError(f"config flag index must be 0..{CONFIG_FLAGS_COUNT-1}")
if not 0 <= val <= 0xFFFFFFFF:
raise EepromError("config flag must be a 32-bit value")
self._put_u32le(CONFIG_FLAGS_OFFSET + 4 * i, val)
# ---- signature ---------------------------------------------------------
@property
def signature(self) -> int:
return self.data[SIG_OFFSET]
@property
def has_signature(self) -> bool:
return self.signature == SIG_VALUE
def set_signature(self) -> None:
self.data[SIG_OFFSET] = SIG_VALUE
# ---- MAC ---------------------------------------------------------------
@property
def mac(self) -> str:
octets = self.data[MAC_OFFSET:MAC_OFFSET + MAC_LEN]
if MAC_LITTLE_ENDIAN:
octets = octets[::-1]
return ":".join(f"{b:02X}" for b in octets)
def set_mac(self, mac: str) -> None:
octets = parse_mac(mac)
stored = octets[::-1] if MAC_LITTLE_ENDIAN else octets
self.data[MAC_OFFSET:MAC_OFFSET + MAC_LEN] = bytes(stored)
# ---- descriptor blocks (VID/PID/bcdDevice) -----------------------------
def descriptor(self, kind: str) -> DescriptorView:
if kind not in DESC_PTRS:
raise EepromError(f"unknown descriptor kind {kind!r}")
ptr_off = DESC_PTRS[kind]
ptr_word = self._u16le(ptr_off)
byte_off = ptr_word * 2
present = ptr_word != 0 and byte_off + DD_LEN <= self.size
view = DescriptorView(kind=kind, ptr_word=ptr_word, byte_offset=byte_off,
present=present, looks_valid=False)
if present:
b_len = self.data[byte_off]
b_type = self.data[byte_off + 1]
view.looks_valid = (b_len == DD_LEN and b_type == 0x01)
view.vid = self._u16le(byte_off + DD_VID)
view.pid = self._u16le(byte_off + DD_PID)
view.bcd_device = self._u16le(byte_off + DD_BCDDEV)
return view
def descriptors(self) -> list[DescriptorView]:
return [self.descriptor(k) for k in DESC_PTRS]
def _set_desc_field(self, field_off: int, val: int) -> list[str]:
"""Write a u16 field into every present descriptor block. Returns the
list of blocks touched (kinds)."""
touched = []
for d in self.descriptors():
if d.present:
self._put_u16le(d.byte_offset + field_off, val)
touched.append(d.kind)
if not touched:
raise EepromError(
"no USB device-descriptor block present to write into; "
"load a base image that already has descriptor pointers")
return touched
def set_vid(self, vid: int) -> list[str]:
_check_u16(vid, "VID")
return self._set_desc_field(DD_VID, vid)
def set_pid(self, pid: int) -> list[str]:
_check_u16(pid, "PID")
return self._set_desc_field(DD_PID, pid)
def set_bcd_device(self, bcd: int) -> list[str]:
_check_u16(bcd, "bcdDevice")
return self._set_desc_field(DD_BCDDEV, bcd)
# ---- string descriptors ------------------------------------------------
def string(self, name: str) -> StringView:
if name not in STR_NAMES:
raise EepromError(f"unknown string {name!r}; one of {STR_NAMES}")
idx = STR_NAMES.index(name)
entry = STR_TABLE_OFFSET + idx * 2
length_field = self.data[entry]
word_off = self.data[entry + 1]
byte_off = word_off * 2
present = length_field != 0 and word_off != 0 and byte_off < self.size
sv = StringView(name=name, length_field=length_field, word_offset=word_off,
byte_offset=byte_off, present=present)
if present:
sv.value = self._read_usb_string(byte_off)
return sv
def strings(self) -> list[StringView]:
return [self.string(n) for n in STR_NAMES]
def _read_usb_string(self, off: int) -> Optional[str]:
# USB string descriptor: bLength, bDescriptorType(0x03), UTF-16LE payload
if off + 1 >= self.size:
return None
b_len = self.data[off]
b_type = self.data[off + 1]
if b_type != 0x03 or b_len < 2 or off + b_len > self.size:
return None
payload = self.data[off + 2:off + b_len]
try:
return payload.decode("utf-16-le", errors="replace")
except Exception:
return None
def set_string_inplace(self, name: str, value: str) -> None:
"""Overwrite an existing string descriptor IN PLACE.
v1 constraint: the new UTF-16LE payload must fit within the existing
descriptor's byte length (we do not relocate/repack the string heap in
v1 — that is an ADVANCED item). Shorter strings are zero-padded and
bLength is updated. Raises if it would not fit or the string is absent.
"""
sv = self.string(name)
if not sv.present:
raise EepromError(
f"string {name!r} is absent in this image; v1 can edit existing "
f"strings in place but not allocate new ones (ADVANCED)")
encoded = value.encode("utf-16-le")
capacity = self.data[sv.byte_offset] # existing bLength (incl. 2-byte header)
new_len = 2 + len(encoded)
if new_len > capacity:
raise EepromError(
f"string {name!r}: new value needs {new_len} bytes but the "
f"existing slot holds {capacity}; in-place edit only in v1")
off = sv.byte_offset
self.data[off] = new_len
self.data[off + 1] = 0x03
self.data[off + 2:off + 2 + len(encoded)] = encoded
# zero any leftover tail within the old slot
for i in range(off + new_len, off + capacity):
self.data[i] = 0x00
# ---- module helpers -------------------------------------------------------
def parse_mac(mac: str) -> list[int]:
parts = mac.replace("-", ":").split(":")
if len(parts) != 6:
raise EepromError(f"invalid MAC {mac!r}: need 6 octets")
try:
octets = [int(p, 16) for p in parts]
except ValueError:
raise EepromError(f"invalid MAC {mac!r}: non-hex octet")
if any(o < 0 or o > 0xFF for o in octets):
raise EepromError(f"invalid MAC {mac!r}: octet out of range")
return octets
def parse_u16(text) -> int:
if isinstance(text, int):
val = text
else:
t = str(text).strip().lower()
try:
val = int(t, 16) if t.startswith("0x") else int(t, 0)
except ValueError:
raise EepromError(
f"invalid 16-bit value {text!r}: expected decimal or 0x-hex")
_check_u16(val, "value")
return val
def _check_u16(val: int, label: str) -> None:
if not isinstance(val, int) or val < 0 or val > 0xFFFF:
raise EepromError(f"{label} must be a 16-bit value (0..0xFFFF), got {val!r}")