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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
"""LED + GPIO EEPROM field accessors (advanced tier, moved into v1).
⚠ BIT-LAYOUT VERIFY: docs/01 give the byte OFFSETS for these regions but not the
datasheet's bit-level packing. The offset placement below is authoritative (from
docs/01 + the task brief); the *bit assignments within each byte* are a
best-effort mapping of the standard LAN7800 LED_CFG / GPIO_CFG registers and are
flagged VERIFY — confirm against datasheet DS00001992G §15 (LED_CFG, GPIO_*) or a
real dump before trusting for production. A raw get/set_region() escape hatch is
provided so an operator can always write exact bytes regardless of this mapping.
Regions (docs/01 + task brief):
LED: 0x0B-0x0D (config 0/1/2) + 0x58-0x59 (behavior/blink)
GPIO: 0x07 wake-enable, 0x0E wake-polarity, 0x48-0x4F per-pin config
"""
from __future__ import annotations
from .eeprom import Eeprom, EepromError, DD_LEN
# ---- LED --------------------------------------------------------------------
LED_MODES = [
"Link/Act", "Link1000/Act", "Link100/Act", "Link10/Act",
"Link100+1000/Act", "Link10+1000/Act", "Link10+100/Act", "reserved",
"Duplex/Collision", "Collision", "Activity", "reserved",
"AutoNeg Fault", "Serial Mode", "Force Off", "Force On",
]
LED_COUNT = 4
LED_MODE_BASE = 0x0B # LED i mode nibble: byte 0x0B+(i//2), nibble (i%2)*4 [VERIFY]
LED_ENABLE_OFFSET = 0x0D # bit i = LED i enabled [VERIFY]
LED_BEHAVIOR_OFFSET = 0x58 # u16 LE; blink rate in bits[1:0] [VERIFY]
LED_BLINK_RATES = {0: "2.5Hz", 1: "5Hz", 2: "10Hz", 3: "20Hz"}
# ---- GPIO -------------------------------------------------------------------
GPIO_COUNT = 8
GPIO_WAKE_EN_OFFSET = 0x07 # bit i = GPIO i wake enable [VERIFY]
GPIO_WAKE_POL_OFFSET = 0x0E # bit i = GPIO i wake polarity (1=active high) [VERIFY]
GPIO_CFG_BASE = 0x48 # one byte per pin, 0x48..0x4F [VERIFY]
# per-pin config byte bits [VERIFY]:
GPIO_DIR_BIT = 0x01 # 1 = output, 0 = input
GPIO_OPEN_DRAIN_BIT = 0x02 # 1 = open-drain
GPIO_PUSH_PULL_BIT = 0x04 # 1 = push-pull
GPIO_OUT_VALUE_BIT = 0x08 # output level when dir=output
def _check_index(i, count, what):
if not isinstance(i, int) or i < 0 or i >= count:
raise EepromError(f"{what} index must be 0..{count - 1}, got {i}")
# Fixed LED/GPIO register byte ranges (inclusive). These are absolute EEPROM
# offsets; USB descriptor blocks are pointer-placed and *should* live elsewhere.
# If a descriptor block lands on one of these, a named LED/GPIO write would
# corrupt the descriptor (and vice-versa) — so we detect and block it.
FIXED_RANGES = [
("GPIO wake-enable", GPIO_WAKE_EN_OFFSET, GPIO_WAKE_EN_OFFSET),
("LED config", LED_MODE_BASE, LED_ENABLE_OFFSET),
("GPIO wake-polarity", GPIO_WAKE_POL_OFFSET, GPIO_WAKE_POL_OFFSET),
("GPIO config", GPIO_CFG_BASE, GPIO_CFG_BASE + GPIO_COUNT - 1),
("LED behavior", LED_BEHAVIOR_OFFSET, LED_BEHAVIOR_OFFSET + 1),
]
def _descriptor_at(eep: Eeprom, offset: int):
"""Return the descriptor kind whose block covers `offset`, else None."""
for d in eep.descriptors():
if d.present and d.byte_offset <= offset < d.byte_offset + DD_LEN:
return d.kind
return None
def _guard(eep: Eeprom, offset: int, field: str):
kind = _descriptor_at(eep, offset)
if kind:
raise EepromError(
f"{field} at 0x{offset:02X} lies inside the {kind.upper()} USB "
f"descriptor block; a named write here would corrupt the "
f"descriptor. This image has an overlapping layout. Use --set-raw "
f"only if you truly intend to write that byte.")
def region_overlaps(eep: Eeprom) -> list[dict]:
"""List every collision between a fixed LED/GPIO range and a descriptor."""
out = []
for name, lo, hi in FIXED_RANGES:
for off in range(lo, hi + 1):
kind = _descriptor_at(eep, off)
if kind:
out.append({"register": name, "offset": off, "descriptor": kind})
return out
# ===== raw region escape hatch (authoritative) ==============================
def get_region(eep: Eeprom, offset: int, length: int) -> bytes:
if offset < 0 or offset + length > eep.size:
raise EepromError(f"region 0x{offset:02X}+{length} out of range")
return bytes(eep.data[offset:offset + length])
def set_region(eep: Eeprom, offset: int, values: bytes) -> None:
if offset < 0 or offset + len(values) > eep.size:
raise EepromError(f"region 0x{offset:02X}+{len(values)} out of range")
eep.data[offset:offset + len(values)] = values
# ===== LED ===================================================================
def get_led(eep: Eeprom, i: int) -> dict:
_check_index(i, LED_COUNT, "LED")
byte = eep.data[LED_MODE_BASE + (i // 2)]
mode = (byte >> ((i % 2) * 4)) & 0x0F
enabled = bool(eep.data[LED_ENABLE_OFFSET] & (1 << i))
return {"index": i, "mode": mode, "mode_name": LED_MODES[mode],
"enabled": enabled}
def set_led_mode(eep: Eeprom, i: int, mode: int) -> None:
_check_index(i, LED_COUNT, "LED")
if not 0 <= mode <= 15:
raise EepromError("LED mode must be 0..15")
off = LED_MODE_BASE + (i // 2)
_guard(eep, off, f"LED{i} mode")
shift = (i % 2) * 4
eep.data[off] = (eep.data[off] & ~(0x0F << shift)) | (mode << shift)
def set_led_enable(eep: Eeprom, i: int, on: bool) -> None:
_check_index(i, LED_COUNT, "LED")
_guard(eep, LED_ENABLE_OFFSET, f"LED{i} enable")
bit = 1 << i
if on:
eep.data[LED_ENABLE_OFFSET] |= bit
else:
eep.data[LED_ENABLE_OFFSET] &= ~bit
def get_led_blink(eep: Eeprom) -> dict:
val = eep.data[LED_BEHAVIOR_OFFSET] | (eep.data[LED_BEHAVIOR_OFFSET + 1] << 8)
rate = val & 0x03
return {"raw": val, "blink_rate": LED_BLINK_RATES.get(rate, "?"), "code": rate}
def set_led_blink(eep: Eeprom, rate_code: int) -> None:
if rate_code not in LED_BLINK_RATES:
raise EepromError(f"blink rate code must be one of {list(LED_BLINK_RATES)}")
_guard(eep, LED_BEHAVIOR_OFFSET, "LED blink")
val = eep.data[LED_BEHAVIOR_OFFSET] | (eep.data[LED_BEHAVIOR_OFFSET + 1] << 8)
val = (val & ~0x03) | rate_code
eep.data[LED_BEHAVIOR_OFFSET] = val & 0xFF
eep.data[LED_BEHAVIOR_OFFSET + 1] = (val >> 8) & 0xFF
# ===== GPIO ==================================================================
def get_gpio(eep: Eeprom, i: int) -> dict:
_check_index(i, GPIO_COUNT, "GPIO")
cfg = eep.data[GPIO_CFG_BASE + i]
return {
"index": i,
"direction": "out" if cfg & GPIO_DIR_BIT else "in",
"drive": ("open-drain" if cfg & GPIO_OPEN_DRAIN_BIT
else "push-pull" if cfg & GPIO_PUSH_PULL_BIT else "default"),
"output_value": 1 if cfg & GPIO_OUT_VALUE_BIT else 0,
"wake_enabled": bool(eep.data[GPIO_WAKE_EN_OFFSET] & (1 << i)),
"wake_active_high": bool(eep.data[GPIO_WAKE_POL_OFFSET] & (1 << i)),
}
def set_gpio(eep: Eeprom, i: int, direction=None, drive=None, output_value=None,
wake_enabled=None, wake_active_high=None) -> None:
_check_index(i, GPIO_COUNT, "GPIO")
off = GPIO_CFG_BASE + i
if any(v is not None for v in (direction, drive, output_value)):
_guard(eep, off, f"GPIO{i} config")
if wake_enabled is not None:
_guard(eep, GPIO_WAKE_EN_OFFSET, f"GPIO{i} wake-enable")
if wake_active_high is not None:
_guard(eep, GPIO_WAKE_POL_OFFSET, f"GPIO{i} wake-polarity")
cfg = eep.data[off]
if direction is not None:
if direction not in ("in", "out"):
raise EepromError("direction must be 'in' or 'out'")
cfg = (cfg | GPIO_DIR_BIT) if direction == "out" else (cfg & ~GPIO_DIR_BIT)
if drive is not None:
cfg &= ~(GPIO_OPEN_DRAIN_BIT | GPIO_PUSH_PULL_BIT)
if drive == "open-drain":
cfg |= GPIO_OPEN_DRAIN_BIT
elif drive == "push-pull":
cfg |= GPIO_PUSH_PULL_BIT
elif drive != "default":
raise EepromError("drive must be open-drain/push-pull/default")
if output_value is not None:
cfg = (cfg | GPIO_OUT_VALUE_BIT) if int(output_value) else (cfg & ~GPIO_OUT_VALUE_BIT)
eep.data[off] = cfg & 0xFF
if wake_enabled is not None:
bit = 1 << i
eep.data[GPIO_WAKE_EN_OFFSET] = (eep.data[GPIO_WAKE_EN_OFFSET] | bit) \
if wake_enabled else (eep.data[GPIO_WAKE_EN_OFFSET] & ~bit)
if wake_active_high is not None:
bit = 1 << i
eep.data[GPIO_WAKE_POL_OFFSET] = (eep.data[GPIO_WAKE_POL_OFFSET] | bit) \
if wake_active_high else (eep.data[GPIO_WAKE_POL_OFFSET] & ~bit)
def describe_regs(eep: Eeprom) -> dict:
overlaps = region_overlaps(eep)
return {
"leds": [get_led(eep, i) for i in range(LED_COUNT)],
"led_blink": get_led_blink(eep),
"gpios": [get_gpio(eep, i) for i in range(GPIO_COUNT)],
"overlaps": overlaps,
"overlap_warning": (
"LED/GPIO register offsets overlap a USB descriptor block in this "
"image; named LED/GPIO edits are blocked on the colliding bytes to "
"avoid corrupting the descriptor." if overlaps else None),
"verify_note": "LED/GPIO bit layout is best-effort; confirm vs datasheet "
"sec 15 or a real dump. Offsets are authoritative.",
}