#!/usr/bin/env python3 """Program the re-bba-rb FT2232H EEPROM so the unit enumerates as "re-BBA-rb". Uses pyftdi (libusb) — the in-container tool, since debian's libftdi1-2 ships only the runtime lib, not the `ftdi_eeprom` binary. The intended descriptor values mirror hardware/re-bba-rb/ftdi_eeprom.conf. DRY-RUN BY DEFAULT: prints the diff and writes a raw backup, but does NOT touch the EEPROM unless you pass --commit. Keeps the stock VID/PID 0403:6010. Usage (inside the devcontainer, FT2232H attached): sudo /opt/venv/bin/python hardware/re-bba-rb/flash_ftdi_eeprom.py # dry-run + backup sudo /opt/venv/bin/python hardware/re-bba-rb/flash_ftdi_eeprom.py --erase --commit # blank U7 ⚠️⚠️ re-bba-rb V1: DO NOT PROGRAM THE STRINGS — IT SOFT-BRICKS THE FT2232H. ⚠️⚠️ U7 is a 93LC46B (128-byte / 1 Kbit) EEPROM, TOO SMALL for the FT2232H — the H-series needs a 93LC56B (256 B) or 93LC66B (the 93LC46 is for the FT232R / FT2232D). The FT2232H config mirrors in the 128-byte chip, so a string write lands a PARTIAL, checksum-valid-but-garbage config. The FT2232H then reads it at power-up and **FAILS USB ENUMERATION ENTIRELY** — dead silent, no dmesg attach. It does NOT fall back to ROM defaults (only a BLANK / checksum-INVALID EEPROM does that). This was confirmed on hardware 2026-08-22. The script therefore REFUSES the write when it detects mirroring; --force exists only to reproduce the soft-brick deliberately. RECOVERY if U7 ever gets a bad config written (done on the V1 unit, works): 1. Power the board on with U7's CLK pin shorted to GND — the FT2232H then can't read a valid EEPROM and enumerates on ROM defaults ("Dual RS232-HS", 0403:6010). 2. Release the short and immediately blank U7: sudo /opt/venv/bin/python hardware/re-bba-rb/flash_ftdi_eeprom.py --erase --commit (Classic FTDI EEPROM recovery.) With U7 blank the board is fully functional; iceprog + the channel-B UART work normally. Fix for a future rev: 93LC56B at U7. reset_device() is intentionally NOT called — it re-enumerates the FT2232H and can drop a usbip attachment; re-plug / re-attach manually instead. """ import argparse import sys from pathlib import Path VID = 0x0403 PID = 0x6010 URL = f"ftdi://0x{VID:04x}:0x{PID:04x}/1" MANUFACTURER = "hashru" PRODUCT = "re-BBA-rb" SERIAL = "RBBARB001" BACKUP = Path(__file__).with_name("eeprom-backup.bin") def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--commit", action="store_true", help="actually write the EEPROM (default: dry-run only)") ap.add_argument("--erase", action="store_true", help="blank U7 to 0xFF (FT2232H falls back to ROM defaults)") ap.add_argument("--force", action="store_true", help="attempt the string write even if U7 reports mirroring " "(undersized 93LC46B) — WILL SOFT-BRICK the FT2232H on " "V1 (fails USB enumeration; recover via CLK-to-GND + erase)") ap.add_argument("--url", default=URL, help=f"pyftdi device URL (default {URL})") args = ap.parse_args() try: from pyftdi.eeprom import FtdiEeprom except ImportError: print("pyftdi not installed. In the devcontainer it is preinstalled; " "otherwise: pip install pyftdi", file=sys.stderr) return 2 eeprom = FtdiEeprom() try: eeprom.open(args.url) except Exception as exc: # noqa: BLE001 - surface the real libusb error print(f"Could not open {args.url}: {exc}", file=sys.stderr) print("Is the FT2232H usbipd-attached, and are you running under sudo?", file=sys.stderr) return 1 # 1) Backup the current EEPROM image before any change. try: BACKUP.write_bytes(bytes(eeprom.data)) print(f"Backed up current EEPROM ({len(eeprom.data)} bytes) -> {BACKUP}") except Exception as exc: # noqa: BLE001 print(f"WARNING: could not write backup ({exc}); aborting.", file=sys.stderr) eeprom.close() return 1 # --- Erase mode: blank U7 back to 0xFF (FT2232H then uses ROM defaults). --- if args.erase: return _erase(eeprom, args.commit) # ------------------------------------------------------------------ # HARDWARE NOTE — U7 on re-bba-rb V1 is a 93LC46B (1 Kbit = 128 bytes). # The FT2232H addresses a 256-byte EEPROM and its config does NOT fit in # 128 bytes: pyftdi sees the 128-byte chip mirror into the upper half # (has_mirroring=True), the config write mirror-clobbers word 0x80->0x00, # and verify fails. FTDI specs a 93LC56B (256 B) / 93LC66B for the H-series # (the 93LC46 is for the FT232R / FT2232D). Writing the strings leaves a # PARTIAL checksum-valid-but-garbage config that SOFT-BRICKS the FT2232H: it # then fails USB enumeration entirely (dead silent — does NOT fall back to # ROM defaults). Recovery: power on with U7 CLK shorted to GND, then --erase. # So REFUSE the write — do not half-brick the chip. # ------------------------------------------------------------------ if eeprom.has_mirroring and not args.force: print("\nREFUSING: U7 reports EEPROM mirroring — it is a 128-byte chip " "(93LC46B), too small for the FT2232H's 256-byte config.\n" "Writing the 're-BBA-rb' strings SOFT-BRICKS the FT2232H on this " "board (partial config -> fails USB enumeration; recover by shorting " "U7 CLK to GND at boot then --erase; see header / REVIEW.md).\n" "Use --erase to blank U7; --force ONLY to reproduce the soft-brick.", file=sys.stderr) eeprom.close() return 3 # Stage a COMPLETE FT2232H default config, then overlay our strings. print(f"\nEEPROM currently blank: {eeprom.is_empty} — staging FT2232H defaults.") eeprom.initialize() cfg = eeprom._config # decoded config dict (no public accessor in pyftdi 0.57) print("\nCurrent (staged-base) descriptors:") print(f" vendor_id = 0x{cfg.get('vendor_id', 0):04x}") print(f" product_id = 0x{cfg.get('product_id', 0):04x}") # Stage the new strings (VID/PID/channel config untouched). eeprom.set_manufacturer_name(MANUFACTURER) eeprom.set_product_name(PRODUCT) eeprom.set_serial_number(SERIAL) print("\nStaged descriptors:") print(f" manufacturer = {MANUFACTURER!r}") print(f" product = {PRODUCT!r}") print(f" serial = {SERIAL!r}") if not args.commit: print("\nDRY-RUN — nothing written. Re-run with --commit to program.") eeprom.close() return 0 try: eeprom.commit(dry_run=False) except Exception as exc: # noqa: BLE001 print(f"\nWRITE FAILED ({exc}). U7 may be partially written — run with " "--erase to blank it back to ROM-default fallback.", file=sys.stderr) eeprom.close() return 1 eeprom.close() # NOTE: do NOT call reset_device() — it re-enumerates the FT2232H, which # drops the usbipd attachment on WSL2. Re-plug / re-attach manually instead. print("\nEEPROM written. Re-attach USB (usbipd) so the host shows 're-BBA-rb'.") return 0 def _erase(eeprom, commit: bool) -> int: """Blank U7 to 0xFF so the FT2232H falls back to ROM defaults. On the mirrored 128-byte chip pyftdi's 256-byte verify trips even though the 0xFF writes land, so we loop and check the actual content instead of trusting commit()'s verify. """ if not commit: print("\n--erase DRY-RUN: would write 0xFF over U7. Add --commit to do it.") eeprom.close() return 0 for attempt in range(5): data = bytes(eeprom.data) nonff = sum(1 for b in data if b != 0xFF) print(f"erase attempt {attempt}: non-0xFF bytes = {nonff}") if nonff == 0: print("U7 is fully blank — FT2232H will use ROM defaults.") eeprom.close() return 0 eeprom.erase(0xFF) try: eeprom.commit(dry_run=False) except Exception as exc: # noqa: BLE001 - mirrored-chip verify trips; writes still land print(f" (commit verify raised, expected on mirrored chip: {exc})") # Re-read fresh next loop without reset_device() (which drops usbip). eeprom.sync() print("Could not fully blank U7 after retries — re-run --erase after re-attach.", file=sys.stderr) eeprom.close() return 1 if __name__ == "__main__": raise SystemExit(main())