#!/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. SAFE BY DESIGN: * Keeps the stock VID/PID 0403:6010 and the dual-channel config, so iceprog (MPSSE on channel A) and the channel-B UART keep working unchanged — only the descriptor STRINGS change. * DRY-RUN BY DEFAULT: prints the diff and writes a raw backup, but does NOT touch the EEPROM unless you pass --commit. * Always dumps the current EEPROM to a backup file first. Usage (inside the rebuilt devcontainer, FT2232H usbipd-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 --commit # write strings sudo /opt/venv/bin/python hardware/re-bba-rb/flash_ftdi_eeprom.py --erase --commit # blank U7 ⚠️ re-bba-rb V1 HARDWARE LIMITATION: U7 is a 93LC46B (128-byte / 1 Kbit) EEPROM, which is 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 and the string write fails verify, so the "re-BBA-rb" product string CANNOT be programmed on V1. This script therefore REFUSES the write when it detects mirroring (use --force to override, --erase to blank U7). The board is fully functional without it: it enumerates on ROM defaults ("Dual RS232-HS", 0403:6010) and iceprog + the channel-B UART work normally. Fix in a future rev by fitting a 93LC56B at U7. Flash the FPGA bitstream FIRST and confirm iceprog works — the string is purely cosmetic. Do NOT rely on reset_device(): it re-enumerates the FT2232H and drops the usbipd attachment on WSL2 — re-attach with usbipd 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 likely fail/corrupt") 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). So the product string CANNOT be # stored on V1 — the board still works fine on ROM defaults ("Dual RS232-HS", # 0403:6010; iceprog + the UART are unaffected). Refuse rather than # half-write (which corrupts U7 and needs an --erase recovery). # ------------------------------------------------------------------ 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" "The 're-BBA-rb' product string cannot be stored on this board " "(see the header comment / REVIEW.md). The board works fine on ROM " "defaults. Use --erase to (re)blank U7; --force to attempt anyway.", 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())