Skip to content

a2mega 1.0a3 DP bring-up: closed-loop training, boot-draw characterization, flash reliability, recovery tooling - #2

Open
edanuff wants to merge 134 commits into
mainfrom
claude/a2mega-1-0a3-prep-79601a
Open

a2mega 1.0a3 DP bring-up: closed-loop training, boot-draw characterization, flash reliability, recovery tooling#2
edanuff wants to merge 134 commits into
mainfrom
claude/a2mega-1-0a3-prep-79601a

Conversation

@edanuff

@edanuff edanuff commented Aug 17, 2026

Copy link
Copy Markdown
Owner

What this is

The complete a2mega 1.0a3 DisplayPort bring-up campaign (134 commits): from AUX closed-loop training first light through boot-reliability characterization, flash tooling hardening, and the recovery/diagnostic infrastructure. The branch preserves granular history of the debugging arc; the bringup log (boards/a2mega/docs/bringup_1_0a3_log.md) is the narrative index.

Highlights

DP link (2-lane HBR, production config)

  • Closed-loop AUX training works end-to-end on hub DP→HDMI converters (LVDS25+pulldown RX front-end, native-DPCD presence check, EDID skip for converters that DEFER all DDC)
  • Blind-ladder path proven 5/5 reliable monitor-direct (Friday-snapshot verification)
  • Un-chained Customized PHY (TX Channel Bonding off): the IDE's bonding checkbox emits only a half-bond (chained tx_if mst_sel without bond machinery) — un-chaining took hub boots 2/5 → 4/5 and killed the retrain doom-loop
  • Full telemetry over FPGA UART → ESP32 telnet tee: ladder state, sink DPCD locks/status, SERDES status, FIFO fill, watchdog counters

Boot-draw characterization (the residual 1-in-5)

  • Proven config-domain state: survives held CMU/CPLL/POR resets ×7 and a full 357-write CSR-sequence replay over DRP (both silicon-verified placebos); only full reconfiguration re-rolls it
  • Sub-word (FIFO fill levels identical good vs bad); monitor-class sinks tolerate it, converter-class may not
  • Decision of record: reconfiguration is never a user-level fix; shipping bar is 5/5 by construction (FFE/swing sweep and full Gowin-stack tiers queued)

Flash reliability (GW5A + esp_usb_jtag)

  • Root causes: SecurityBit default + auto-boot re-arm racing the JTAG-SPI passthrough; content-present race (first attempt fails ~1:22, erased-chip attempt succeeds ~1:45 — deterministic)
  • flash_rescue SRAM core: MSPI-as-user-IO probe/erase with telnet telemetry — deterministic ~3 min recovery path, used throughout
  • Patched openFPGALoader (tools/openfpgaloader-a2mega.patch): honest empty-chain reporting, freq-honored flash phase, detect retries

Diagnostics preserved for reference

  • EDP PHY hardened-8b10b dead end (no sink symbol lock, polarity-independent per DRP live-write experiment) — Gowin outreach draft in docs/gowin_outreach.md
  • 4-lane RBR datapath (dormant branch; systematically dark on 4-lane sinks — needs sim audit before revisit); MSA 4ch wiring gap found and fixed
  • Watchdog with CSR-replay recovery (now known-placebo but the DRP write engine is proven and reusable); blind-sink gating fix

Known open items (queued as tasks, documented in the log)

  • ESP32 firmware: two-persona regression — Friday-PM source-role rework fixed in-slot hotplug but cost one monitor orientation; Saturday cost the other. Bisect plan documented (98461af3..HEAD)
  • Cable/EQ cold-morning marginality under investigation (mux EQ default 1.0 vs 6.5 dB)
  • Pin-assignment-D firmware support for D-only hubs; FFE/swing sweep; bench-reload tooling

Reviewer notes

  • Build artifacts (impl/pnr/*.fs) are committed intentionally — bitstream provenance (sha256 in commit messages/log) was load-bearing for the flash forensics and the Friday-binary bisection
  • hdl/displayport/ is the vendored DisplayPort_Verilog core with local modifications; boards/a2mega/hdl/gowin/60B/dp_serdes/ is IDE-generated IP (regenerate via the IDE, sidecars must stay together)
  • The ESP32 firmware on the bench board currently runs the end-of-Friday snapshot (98461af3), not HEAD — see the two-persona note in the log

edanuff added 30 commits August 9, 2026 14:33
- hdl/displayport/: dp_transmitter + core/video/audio/auxch/gowin from
  DisplayPort_Verilog @ 87cd538 (MIT), provenance README with a2mega
  lane/refclk facts
- boards/a2mega/hdl/gowin/60B/dp_serdes/: generated GW5AT-60B Customized
  PHY (Q0 L2+L3 bonded, REFCLK1 135 MHz, QPLL0, HBR, tx_pol_invert) incl.
  .csr sidecar and .ipc regen config
- boards/a2mega/hdl/dp_gowin_defines.v: DP_VENDOR_GOWIN/GOWIN_SERDES_IP/
  GOWIN_PLL_IP/DP_SERDES_LANES_23
- ESP32 firmware: vendor portable USB-C PD stack (fusb302, usbc_port,
  esp32s3_integration HAL skeleton) into the sketch
- docs: BRINGUP_1_0A3_PLAN.md (phased bring-up plan for the 1.0a3 board)
1.0a3 reshuffled the ESP32-S3 pinout: OPI D0-7 moved to IO11-18, JTAG TDO
IO45->IO39, I2C (FUSB302B+TUSB1046A) on IO1/2 with INT on IO3, PPO
narrowed to 4 bits, VBUS_SRC_EN on strapping pin IO46, six spare GPIO
lines to the FPGA (GPIO0=DP HPD out, GPIO5=JTAG-bridge TRST dummy —
IO3 now belongs to the FUSB302B and IO45 is the VDD_SPI strap).

- board_pins.h: all pins for both revs, -DA2MEGA_BOARD_REV selects
  (default 103 on this branch; Makefile BOARD_REV=102 for older boards)
- SD-card code compiled out on 1.0a3 (no slot); mount_sd() stub keeps the
  no-card path; storage returns later as LittleFS at the same /sdcard
  VFS prefix
- setup(): pin VBUS_SRC_EN + HPD low, FUSB INT_N input, before subsystems
- fpga_jtag.c/a2fpga_jtag.h take pins from board_pins.h (+_Static_assert
  the out1/in1 bank assumption)

Both revs compile green (arduino-cli, esp32s3).
On 1.0a3 the USB-C port faces the DP monitor, so WiFi telnet becomes the
primary console. Port of the BL616 telnetd (port 23, single client):
console view streams osd_log lines live (ring tee, never blocks logging
threads); menu view mirrors the 40x24 OSD page as ANSI and maps keys to
pad buttons via a new thread-safe menu_inject/menu_request_menu_view, so
the whole menu is drivable with no gamepad. Enhanced-only bus-capture
diagnostics not ported (no such fabric in the a2mega gateware).

Support APIs added: fpga_screen shadow row accessor + generation counter,
osd_console_snapshot backlog replay, osd_log tee hook.

New CLI: 'wifi [<ssid> [psk]]' persists credentials to NVS and starts the
bridge immediately when booted unconfigured (replaces wifi.txt on SD-less
1.0a3), plus 'restart'. Both board revs compile green.
usbc_glue.cpp binds the vendored usbc_port/fusb302 policy core to the
1.0a3 hardware. The vendored esp32s3_integration skeleton assumes a
GPIO-mode TUSB1046A; the board straps I2C mode, so this glue provides its
own HAL:
- shared Wire bus (IO1/IO2): FUSB302B @0x22, TUSB1046A @0x12
- DP enable = general reg 0x0A CTLSEL=10 | FLIPSEL | HPDIN_OVRRIDE,
  disable = CTLSEL=01 (as-built decisions from usb-c/SPEC.md §6)
- HPD level driven on PIN_DP_HPD_OUT (ESP32_GPIO0 -> FPGA
  hotplug_decode); IRQ_HPD = 0.75 ms low pulse generated in firmware
- VBUS sourcing via TPS2553 EN (strapping-safe), 5 V / 1 A single PDO
- 1 kHz policy task on core 1, woken early by FUSB302B INT_N (IO3)

CLI gains 'pd' (state/orientation/HPD/VBUS/mux status). PD init runs at
setup(), before the FPGA link — the PD contract is time-bound and must
complete regardless of FPGA state. Both board revs compile green.
Mount a LittleFS (1.5 MB 'spiffs' partition, format-on-first-use) at the
/sdcard VFS prefix on 1.0a3, so disk.c, ftpd.c and fpgaupdate.c — all
plain POSIX on that prefix — serve disk images and FPGA update files from
flash unchanged. Images arrive over FTP once WiFi is up. Both revs
compile green.
The 1.0a3 board carries the same module as the a2p25 — N4R2 (4 MB flash,
2 MB quad PSRAM) — not the N8 (8 MB, no PSRAM) of earlier revs. Makefile
now selects the FQBN per BOARD_REV: 103 = FlashSize=4M, PSRAM=enabled,
PartitionScheme=no_ota (2 MB app + 1.875 MB LittleFS; no OTA pair — the
ESP32 reflashes over USB-C); 102 keeps 8M/no-PSRAM/default_8MB.

Consequence noted in Makefile + plan: an uncompressed GW5AT-60 bitstream
(~2.6 MB) no longer fits the flash FS, so menu FPGA-update-from-file will
need compression or streaming; PC-driven JTAG is unaffected. PSRAM is now
available for large buffers. Both revs compile green (103 app = 48% of
the 2 MB slot).
The MINI footprint accepts either ESP32-S3 variant (N4R2/N8 are
pin-compatible). N4R2 was selected when DOC-on-ESP32 was anticipated —
now known unnecessary — but its 2 MB PSRAM may still be needed for the
much larger buffers WiFi throughput wants. Either module supports the
current bring-up path.

FPGA update-from-file: decided to stream the bitstream to JTAG (USB
stick / network source) rather than stage ~2.6 MB in MCU flash — wasteful
on any module, impossible on the N4R2 FS.
… pin map

Phase 2 step 1 of the 1.0a3 plan, built before hardware arrival.

DP test project (a2mega_dp_test.gprj -> impl/pnr/a2mega_dp_test.fs):
- a2mega_dp_test_top.sv: vendored dp_transmitter in the timing-verified
  config (1080p59.94, 2-lane HBR, Q0 L2+L3 bonded on REFCLK1), SMPTE-ish
  colorbars + frame border, quiet 48 kHz test tone, AUX pseudo-diff pair
  on G15/G16, HPD in from ESP32_GPIO0 (A19, pull-down), LED bring-up
  ladder: heartbeat / HPD / link_established / video_live (active low)
- Timing CLEAN: 0 setup+hold violations, TNS 0.000 all five clocks, Fmax
  >= constraint (clk_sym 135.352 vs 135.007 MHz)
- Gowin project gotchas captured in the plan doc: per-project
  impl/<name>_process_config.json must carry Verilog_Standard=Sysv2017,
  TopModule, and a DISTINCT OUTPUT_BASE_NAME (default silently overwrote
  the main a2mega outputs; restored from git). The .gprj.user is
  force-added past .gitignore: it registers RES.serdes.csr, without
  which PnR fails CM2031.
- bitstream sha256:
  6f1346dbb5c68c0971465aa174b9b351c8835178bc81cc0808136fbf1c0aee91
  (base 44ba210 + this tree, gw_sh @ GowinIDE.app)

Master pin map (user-requested):
- docs/a2mega_pinmap_1_0a3.csv: signal / ESP32-S3 GPIO / BTB position /
  FPGA ball for ALL of J1 (Apple bus), J2 (ESP32+DP), J3 (GS), plus
  SERDES rows with die-true 60K vs 138K lane naming (balls identical on
  PG484; schematics use 138K names). Notes that 1.0a4 moves pins again.
- tools/btb_pinmap.py: regenerable extractor (KiCad pcb pad nets +
  BTB-sheet pin-name texts, coordinate-matched). Validated: 1.0a2a
  netlist reproduces the working a2mega.cst 12/12 (incl. the deliberate
  USB D+/D- swap), and J1 spot checks (phi1 H19, d0 N18, a0 T20,
  a15 G17) match exactly.
- docs/btb_pinout_1_0a3.md: narrative + the FPGA-side pin MOVES on
  1.0a3 (OSPI/UART/PPO all relocated; UART now on old USB balls G13/H13;
  GS RDY split with RDY_OUT=V15)
… schematics

The io_60k/io_138k columns were previously empty except for SERDES lane
rows (the a2-mega sheets only carry 138K names). Now sourced from the
Sipeed SOM schematic PDFs (tang_mega_60k_30353 / tang_mega_138k_30354):
- docs/tang_mega_die_names.json: ball -> per-die BANKn_IOxxx dictionary
  (60K: 107 full + 24 bank-only of 135 rows; 138K: 134/135 — the 138K
  PDF labels fewer balls, fallback = a2-mega annotation). The 138K PDF's
  subsetted font emits text shifted by -0x1D; decoder noted in the tool.
- Cross-checks: 0 mismatches between Sipeed-138K names and the a2-mega
  schematic annotations on every overlapping ball; per-die names really
  do differ (e.g. FPGA_A0 T20 = BANK7_IOB111B on 60K vs BANK4_IOB102B
  on 138K).
- Confirmed at the source: the Sipeed 60K sheet labels the BTB DP pins
  with 138K-style lane names (Q0_LN1 on the die-true lane-3 pins) —
  supports the a2-mega die-true lane note; CSV lane columns stay
  die-true (what the timing-verified SERDES IP implements).
The Apple II core now outputs 720x480p59.94 over 2-lane HBR DisplayPort
(USB-C) instead of HDMI. Bitstream sha256
307fc24dfa9f1746595b4872fd62043a4032458bb225c5868a4bc09b9a26d112,
0 setup + 0 hold violations, TNS 0.000, Fmax >= constraint on all eight
clocks (clk_sym 135.055 vs 135.007 - met, thin; see margin note below).

top.sv:
- hdmi encoder + OSER10 serializer + ELVDS replaced by dp_transmitter at
  the framebuffer_480p seam: the DP core owns clk_pixel_w (27 MHz =
  135 MHz symbol clock / 5 from its pixel PLL) and pulls rgb via the
  same hdl-util-style cx/cy contract, so framebuffer/OSD/DebugOverlay
  connect unchanged. Board PLLs untouched - the board 27 MHz tap remains
  solely pll_ddr3's source (do NOT re-source; DDR3 calib fragility).
- USB HID host + report CDC removed (no USB-A on 1.0a3); ESP32 readback
  regs tied to no-device. AUX pseudo-diff pair, HPD in, 4 LEDs with the
  bring-up ladder (heartbeat / calib / link_established / video_live).

cst/sdc: 1.0a3 ball moves (OSPI A14..B17/A18, UART on old USB balls
G13/H13, AUX G15/G16, HPD A19, 4 LEDs), TMDS/USB pins gone; DP clock set
(clk100/clk_sym/clk_pix + async groups), TMDS x5 and clk_usb dropped.

DP core timing closure - IN RTL, not floorplanning (vendored core,
upstreamable; sim-verified cycle-exact against in-module shadow models
at both 480p and 1080p geometries, 3 frames each):
- video_stream_packer: the whole two-slot TU walk (Bresenham valid
  count, phase, FS/FE, word loads, FIFO pop) is computed one cycle early
  from a self-contained registered state copy and REGISTERED; the live
  datapath is flag muxes only. Region flags additionally pre-registered
  from exactly-advanced two-step-ahead line counters. The original walk
  survives as a translate_off shadow with  equivalence checks.
- sdp_engine: the four per-cycle wire-byte lookups (deep mux over the
  256-bit payload) precomputed one cycle early into registers; b-index
  derived as {ins_cyc,1} (algebraic identity) to avoid a subtractor -
  the design's final failing path.
- gowin_plls/dp_transmitter: pixel PLL ODIV0 parameterized off the fixed
  1188 MHz VCO (8 -> 148.5 MHz, 44 -> 27 MHz), derived from
  PIXEL_CLK_MULT/DIV with an elab check.
- process config: Place_Option=2, Route_Option=1 (part of the recipe).

Margin note: clk_sym worst slack +0.003 ns. GW5AT PnR variance can
wobble this on re-runs (docs/gotchas.md); the RTL now has no deep cones,
so a wobble is a re-run, not a redesign. Do NOT reach for GROUP/GRP_LOC
regions: two attempts thrashed the placer for 50 min and 9+ hours (new
gotchas entry). tools/build.sh now enforces BUILD_TIMEOUT (default 30
min) so a pathological PnR fails loudly.

Pin map: SERDES rows corrected from the Sipeed 60K U1L die symbol
(matches Gowin UG1222) + BTB nets - ALL FOUR DP pairs are P/N swapped
(TXM on the lower pin, uniformly); lane map DP0-L3/DP1-L2/DP2-L1/DP3-L0
confirmed; correct per-pair balls C7/D7 A6/B6 C5/D5 A4/B4. The 2-lane
IP's tx_pol_invert on L2+L3 is correct as shipped; 4-lane needs invert
on L0+L1 too. The a2-mega BTB1 schematic note (DP3 not swapped + ball
orderings) needs correcting in the hardware repo.
Artifacts now match the restructured hdl/displayport sources (registered
early-walk packer, sdp wire-byte precompute, parameterized pixel PLL).
Timing clean: 0 violations, TNS 0.000, clk_sym Fmax 136.077 vs 135.007
constraint — the RTL closure work IMPROVED standalone margin over the
original build's 135.352. Bitstream sha256:
30f04a1d77736eda3226fd138335aa46e7241acd9ba1689ff81738ad165477b2
The full core now scans out at 1920x1080p59.94 over 2-lane HBR DP.
Bitstream sha256
2270857e7e55d05dc20af2138a668718e4924b65868d10c17b6c8893bff19451:
0 setup + 0 hold violations, all eight clocks meet constraint
(clk_sym 136.03 vs 135.01, clk_pix 148.67 vs 148.50).

hdl/video/framebuffer_1080p.sv (new, derived from framebuffer_480p
which stays the 480p/HDMI variant): the framebuffer still stores
SOURCE resolution (560x192 Apple II / 640x200 SHR); the read side
scales x3 horizontal / x5 vertical into 1080p with centered borders
(1680x960 / 1920x1000). No dividers: both axes use incremental phase
trackers off the DP core's monotonic cx/cy; the clk-domain fetch pacing
consumes a CDC'd {approach,active,fb_line} bundle computed in the pixel
domain. Same 8-bank line buffer (source-res lines) — zero BSRAM growth.
Registered input coords (+1 lookahead compensation) and a pre-registered
left-border trigger keep the address cone shallow at 148.5 MHz.

top.sv: dp_transmitter at the timing-verified 1080p config (2200/1125,
MULT 11 / DIV 10); audio strobe rebuilt as a sign-bit phase accumulator
(148.5M/48k is not integer); OSD window centered (680,348).

148.5 MHz closure of the 27 MHz-era pixel pipeline:
- osd_text_overlay: 4Kx8 font ROM forced to block ROM (pure sync read;
  glyph inversion moved to the consumer — ~rom[addr] blocked inference)
- DebugOverlay: 5-stage pipeline (input regs -> region compares ->
  region-relative subtracts + hex mux -> char ROM row -> output), local
  registration of the quasi-static hex/bit values, syn_keep against
  cross-module register merging. Overlay shifts a few px — cosmetic.
- video_stream_packer: VB-ID vsel() compares pre-registered from the
  two-step-ahead counters (pf_vb*); redundant zero-gate dropped from the
  sym decisions (stage B already gates on running) removing RESET arcs
- sdp_engine: GF parity tree split at the 2-byte midpoint (2-stage
  pipeline, shadow-checked against pb_group in sim); packet arming =
  pending for >=4 CONSECUTIVE cycles — the shadow wire-byte assertion
  caught that a plain shift register retained stale armed state across
  packet boundaries and back-to-back packets would have inserted the
  PREVIOUS packet's parity bytes (real functional bug, killed in sim)

All packer/sdp changes verified cycle-exact against in-module shadow
models (3 frames x 480p + 1080p geometries, iverilog).

tools/build.sh: gw_sh runs under caffeinate on macOS (two silent
30-minute startup hangs; App Nap suspected, one recurrence caffeinated
— cause still open, BUILD_TIMEOUT contains the damage either way).

Margin note: clk_sym/clk_pix are met with thin margin; GW5AT PnR
variance can wobble marginal paths run-to-run (observed band this
session: 0-72 violations on identical-or-near sources). The RTL now has
no deep cones — a wobble is a re-run, not a redesign.
Section 5 added to BRINGUP_1_0A3_PLAN.md: staged first-article runbook
built around the one-port constraint (J4 = power + programming + monitor
on the bench; flash-boot for bench swaps, in-slot SRAM iteration once
trusted). Stages: electrical (no SOM) -> ESP32/WiFi/telnet -> SOM+JTAG
-> PD/Alt-Mode entry -> link training + colorbars soak -> full core on
bench -> IIgs slot regression -> second-unit replication. Includes both
LED ladders, console checkpoints, and failure plays per stage.
Found on first hardware boot of board #1 (Stage 2, no SOM fitted):
start_subsystems() bailed out when fpga_link_init() failed, BEFORE
settings/WiFi/ftpd/telnetd ever initialized — so with no SOM (or with
the colorbars bring-up bitstream, which has no OSPI service in fabric)
the firmware sat in the OSPI retry loop forever and telnet never came
up. That inverts the 1.0a3 design intent: WiFi/telnet is the PRIMARY
console once a monitor occupies the USB-C port, and Stages 4-5 of the
bring-up runbook depend on watching 'pd' over telnet while colorbars
(OSPI-less) is loaded.

- setup() now calls start_network() before start_subsystems():
  settings_init + FS mount + WiFi-from-NVS + ftpd + telnetd,
  unconditionally. FPGA-side subsystems (OSD show, disk, menu, w5100,
  DDR3 telemetry, service tasks) stay behind the link probe and still
  late-bind when the link appears.
- osd_console repaint() bails until fpga_link_ok(): boot-time osd_log
  lines buffer (and tee to telnet) instead of writing through an
  uninitialized SPI device; osd_console_show() replays the buffer when
  the link comes up. All other a2spi entry points already guard on
  a2spi_is_ready().
- OSPI retry message rate-limited (1 per ~30 s) — permanent-miss is the
  normal, expected state under bring-up bitstreams.

usbc_pd_init() was already FPGA-independent (runs first in setup) —
PD/Alt-Mode + HPD work with any bitstream. Compiles clean at 48% flash.

Bring-up log started: boards/a2mega/docs/bringup_1_0a3_log.md
(board #1 Stage 1 PASS: VBUS 5.06 V, +5V 4.92 V, +3V3 3.31 V, 38 mA).
With no OSPI device (no SOM, or a bring-up bitstream without the OSPI
service) register reads complete at the SPI-driver level but return
floating-bus junk — live-observed on board #1 as 'DDR3: CALIBRATED
(retries=254 seq=0xFE)' with NO SOM fitted (0xFE = floating pattern).
status now prints 'FPGA link: UP/DOWN' and only shows DDR3 telemetry
over a verified link, so bring-up readings can't masquerade as real.
Board #1 bring-up, flash-write saga (full narrative in
docs/bringup_1_0a3_log.md). Root causes fixed:

1. Bit-bang JTAG vs USB-JTAG bridge pad ownership (GENERAL bug): while
   route_usb_jtag_to_gpio() has the pads matrix-routed to the USB-JTAG
   peripheral, gpio_config() does NOT reclaim them — every bit-banged
   edge is lost. fpgaerase read garbage IDCODE; menu self-update would
   have failed the same way with a PC attached. fpga_jtag_init_pins()
   now releases the bridge first (extern-C shims in a2fpga_jtag.cpp)
   and release_pins() restores it.

2. fpgaerase CLI never called fpga_jtag_init_pins() — bit-banged pins
   left as INPUTs by the bridge disconnect handler ('block 0x00000
   TIMEOUT'). Now delegates to fpgaupdate_erase_bitstream_region()
   (new): keepsram SPI entry, header-block erase (128 KB, validated
   semantic), blank verify, leave WITHOUT reload so the SRAM fabric
   survives. fpga_jtag_flash_leave() added alongside reload().

3. route_usb_jtag_to_gpio() now runs FIRST in setup(), before the slow
   network bring-up: with a corrupt flash image the GW5A MSPI retry
   loop kills the chain, and the only way in is JTAG activity during
   the boot window — an openFPGALoader retry loop at replug captures
   the TAP (SRAM-load payload proven: attempts 115/15/1).

4. telnet console: 'p' key = condensed PD status through the osd_log
   tee (usbc_pd_status_log) — the only live PD view once the monitor
   occupies the USB-C port. Removed a duplicate fpgaerase branch.

Board #1 state: colorbars verified in SPI flash, heartbeat after power
cycle PASS. Stages 1-3 closed.
…bility

The vendored PD driver only implemented the source role (SPEC scoped
out monitors that present Rp to charge their partner — which is what
real USB-C monitors do). Board #1 stalled in 'device' with the VDM
ladder never running. Implemented per the SPEC's own sketch:

- Sink engine: PD RX enabled on sink attach (the FUSB302 driver never
  enabled the BMC TX driver in sink role either — a sink was mute),
  Source_Capabilities -> RDO Request (5 V, bounded by offer) ->
  Accept/PS_RDY -> SINK_READY.
- DR_Swap both directions (we request it; we Accept the partner's) ->
  data role DFP -> the existing discover/enter/status/configure ladder
  runs unchanged. fusb302_set_data_role() flips the GoodCRC DATAROLE
  bit post-swap. Role-correct TX headers throughout (were hardcoded
  source/DFP). Sink-side Soft_Reset/hard-reset recovery, Get_Sink_Cap.
- HW-verified on board #1: DP-ACTIVE SNK/DFP, both cable orientations,
  mux configured, HPD delivered (led ladder led[1]).

Observability for the monitor-occupied port (telnet is the console):
- telnet 'p' = usbc_pd_status_log (state + roles + CC/HPD/MUX).
- telnet 'x' = TUSB1046A register dump. The DCI AUX snooper parses our
  DPCD writes off the wire: reg 0x12 read back LANE_COUNT_SET=2 —
  TI-silicon proof the FPGA's AUX transmit path works end-to-end.
  (Known issue: dump races the 1 kHz PD poller on the I2C bus — reads
  intermittently fail 0xEE; sample repeatedly. Mutex TODO.)
a2mega 1.0a3 hardware truth (board #1, first HW contact of the DP
core): the AUX pair is AC-coupled per the TUSB1046A reference circuit
(TI Fig 28) with mux-side bias per the datasheet's mandated AUX CM
levels (AUXP 0-0.4 V, AUXN 2.7-3.6 V). The reference assumes a
GPU-class AUX PHY with internal receive bias behind its own caps; a
bare FPGA LVCMOS pin has none, so AUX RECEIVE is electrically
unreachable (a <=1.38 Vpp reply can't cross LVCMOS thresholds, and the
pair never crosses zero differentially at the mux-side CM). Transmit
works — proven end-to-end by the mux's DCI AUX snooper decoding our
LANE_COUNT_SET write (reg 0x12 = 0x02).

BLIND_SINK=1 (parameter, default 0 = spec flow, chained
dp_transmitter -> channel_management -> aux_channel):
- FSM advances on a 100 us dwell after each request drains, instead of
  on reply bytes; reply timeouts don't reset it; training status
  assumed good; sink lane count assumed = source's.
- aux_interface REPLY_TIMEOUT_TICKS param: blind callers use 400 us
  (spec-ish) instead of the historical 39999 ticks = 20 ms (the
  '400us' comment was stale) — sim caught the 20 ms/transaction walk
  colliding with the 0.5 s retry watchdog and never completing.
- HPD gate: blind ladder holds in reset until hpd_present and restarts
  on every HPD rise (live-hit: ladder walked to 'established' against
  an empty connector, so a later-attached monitor never saw training).
- New DPCD 0x600=D0 wake write before link config (state 0x31): the
  stock flow NEVER woke the sink (snooper: SET_POWER_STATE stayed 00);
  a monitor idling in low power post-Alt-Mode-entry ignores training.
  Snooper readback (0x12[6:5]==01) doubles as a per-build AUX
  wire-verification loop.

Sim: tb_blind_aux (scratchpad) — held while HPD low, full walk with
both training phases in ~12 ms, stable link, teardown on HPD loss.
Status on hardware: PD/HPD/AUX-TX all verified; main-link lock still
not achieved (monitor black) — suspects are SERDES serialization
bit-order/polarity semantics (CSR blob unverifiable by inspection) and
per-monitor tolerance. Fabric lane crossover (lane0->LN3, lane1->LN2)
and .ipc invert config (both lanes) verified correct by inspection.
…folding

DP core / colorbars top:
- clk_symbol_out port on dp_transmitter; colorbars led[2] repurposed as
  an in-fabric line-rate check (clk_sym counted against the 50 MHz
  crystal over 1 s windows). HW-VERIFIED LIT: the GTR12 truly runs
  2.7 Gb/s — clocking/QPLL/refclk eliminated as no-lock causes.
- Blind-mode per-state dwell extended to ~1.3 ms (blind_dwell) so sinks
  get tens of ms of each training pattern. Tested: still no lock.
- TX_WORD_MODE A/B scaffold in transceiver_bank_gowin: 4 serializer
  word conventions; modes 0 (straight) and 1 (full reversal) tested on
  hardware, no lock; modes 2 (symbol swap) and 3 (per-symbol reverse)
  remain. Default back to 0.

ESP32 firmware:
- REVERTED early route_usb_jtag_to_gpio() in setup() (broke the bridge
  for the whole boot: Serial.begin's CDC init reconfigures the USB
  peripheral over an early-routed bridge).
- fpgaupdate_erase_bitstream_region: full 4 MB region with PER-BLOCK
  SPI-mode re-entry — empirically only the first block erase per entry
  completes; per-block entry converges (HW-verified: 64/64 blank, then
  flash.sh works first-try again). Slow (~4-8 min) but it is the
  corrupt-flash recovery of record together with the SRAM-load race.

Stage 5 status (see bringup log): eliminated = PD/HPD/mux, AUX TX
(TI-snooper-proven), D0 wake, line rate, training dwell, full-word bit
order. Remaining = word modes 2/3, tx polarity-invert semantics (IP
regen), swing/EQ/SI. Monitor+cable+sink proven good via MacBook.
ROOT-CAUSE CANDIDATE for the Stage 5 no-lock (overnight I2C
investigation, user's hunch): in I2C mode the DPEQ strap pins double as
address pins and are floated per SPEC — the chip latches the F/F
4-level default at reset, which is EQ setting 10 = 12.3 dB of DP
receiver equalization (datasheet Table 7) on a few-cm trace needing
~1 dB. The linear redriver then re-drives a wrecked eye to the
connector; the monitor never locks. Register dump evidence: 0x10/0x11
read 0xAA (setting 10, all four lanes) with EQ_OVERRIDE=0. Explains
the MacBook control test (no mux in that path). The firmware never
programmed EQ; SPEC.md always said I2C-mode EQ is register-settable.

- hal_set_tusb1046: program DP0-3EQ_SEL = 0 (1.0 dB) BEFORE enabling
  lanes; set EQ_OVERRIDE in the General write.
- telnet 'e': cycle EQ presets 0/3/6/10 (1.0/6.5/9.5/12.3 dB) live on
  the running link — A/B without reflashing.
- I2C bus mutex in the glue's i2c_read/write (PD task vs CLI vs telnet
  dumps raced; the 'x' dump's 0xEE noise).
- PD: auto-retry DP discovery 2 s after a VDM-flake fallback to
  usb-only (sink+DFP keeps its contract; no physical replug needed).

Not yet uploaded to board #1 (asleep-shift: board is on the monitor,
serial upload needs the PC). Morning: upload + flash mode-0 colorbars
+ monitor test; then 'x' should show 0x10/0x11 = 0x00 and General
bit4 set.
Per the approved debugging plan (plans/quirky-toasting-meteor.md):

- tx_data packing: {60'b0, code} -> {4{code}} (Sipeed SFP+ idiom). The
  width-20 GTR12 slice selection of the 80-bit bus was never verified
  (sims used the behavioral stub, not the IP netlist); if the macro
  reads any slice other than [19:0] we have been transmitting constant
  zeros through every experiment — which would explain the total
  invariance of all eight content-side A/Bs and the monitor never once
  reacting. Replication covers every possible static slice. CSR
  restored to generator-original (polarity invert bits back to 0x408).

- serdes_status export: {pll_lock, lane_ready[1:0], ~pcs_tx_rst,
  tx_running[1:0]} from transceiver_bank_gowin through dp_transmitter
  (ties 0x3F on non-Gowin builds). These reset-sequencer gates have
  never been observed — clk_sym at 135 MHz does NOT imply the lane
  transmits (PCS clock runs with TX still in reset).

- Debug UART (colorbars top, H13 -> ESP32 Serial1): one ASCII status
  line ~2x/s (serdes_status, dp debug byte, frame counter, HPD/link/
  video flags); firmware tees Serial1 lines into the telnet console.
  FPGA-side status finally readable over WiFi with the monitor on the
  port. Word-mode scaffold left at 0 (canonical).

Both bitstreams timing-clean. TX_WORD_MODE eliminations and the
polarity A/B are CONDITIONAL results — if the slice or reset gating
was the fault, they tested a dead line (noted in the plan).
…ecoder

THE finding of the bring-up (debug-plan Phase A2, first-ever live
SERDES status readback over the new FPGA-UART->telnet channel):
'DP S:20' = pll_lock=1, lane_ready=00, pcs_tx_rst STILL ASSERTED.
The transmitter was held in reset for the entire bring-up — the lanes
never carried one bit while every upstream indicator (PD, HPD, AUX TX,
135 MHz symbol clock, blind-ladder LEDs) looked healthy. The reset
sequencer gated pcs_tx_rst release on lane_ready, which the GTR12
never asserts in this TX-only config (the working Sipeed SFP+ example
releases both resets statically and ignores ready_o entirely — it is
wired to nothing there).

Fixes/changes:
- transceiver_bank_gowin: ungated static reset release (~330 us after
  powerup); tx_running is status-only. Post-fix readback: S:24 (PCS out
  of reset; lane_ready stays 0 = status-only, matching the example).
- TX FIFO wren gated on ~afull (example idiom; was constant-1).
- TX_PROBE parameter (bank + dp_transmitter + test top): forces the
  quad powered and transmits a raw ~4.2 MHz square on both lanes for
  breakout-board + basic-scope lane presence/mapping/polarity checks.
  Probe bitstream stashed in scratchpad; test top back at TX_PROBE=0.
- tools/aux_decode.py: Manchester-II decoder for AD2 CSV captures of
  the AUX channel — reads the monitor's replies (ACK/NAK, DPCD link
  status) that the FPGA electrically cannot; labels known DPCD addrs.

All content-side eliminations from before this fix are VOID (dead
line). Still no picture after the reset fix alone — remaining
suspects: slice packing (4x replication now in), polarity semantics,
content; the probe + AUX captures discriminate next.
…A/B prep

- usbc_glue: telnet 'f' toggles the CC-orientation->FLIPSEL convention
  live and rewrites mux General in place. FLIPSEL steers BOTH the
  SBU/AUX crossbar and the SS-lane crosspoint: an inverted convention
  crosses AUX polarity at the sink AND parks the lanes on unwatched
  connector pins — while every register readback, the AUX snooper
  (which sits UPSTREAM of the crossbar), PD, and HPD all stay green.
  Candidate explanation for the remaining no-lock; TI's USB-C DP
  debugging guide (user-supplied) motivated the crossbar focus.
- serdes_status widened to 8 bits: TX FIFO afull/full visible on the
  debug UART line (is the PCS consuming symbols?).
- Word modes 1-3 rebuilt WITH the reset-deadlock fix (all prior
  word-mode results were against dead lanes) — stashed in scratchpad
  as mode{1,2,3}_live.fs for slot-powered SRAM A/B.
- Polarity CSR restored to generator-original (0x408 both lanes);
  live-lane polarity A/B completed: both settings no-lock (caveat:
  bit identity still inferential — IDE regen pending).
- tools/aux_decode.py minor: (added earlier) AUX Manchester decoder.
- In-slot SRAM iteration workflow proven: IIgs slot power holds the
  fabric through USB swaps; blank flash makes SRAM loads first-try.
…cause

Two days of JTAG 'wedges' (works first-op-after-replug, dies on op
two, retry-races eventually win, ~50%+ failure) were one line of
over-clever code: bridge routing was edge-triggered on
usb_serial_jtag_is_connected(), an SOF-activity heuristic that macOS
USB autosuspend defeats whenever no host process holds a port open —
suspend -> unroute -> the next openFPGALoader open races the polled
re-route and loses. Correlations explained: successes tracked whatever
held a serial port open (esptool, cat sessions); the 1.0a2a-era
jtag_flash_reliability.md 'cold boot ritual' was very likely the same
disease.

Fix: route once in setup() after USB init, never unroute (bit-bang
path does its own pad handoff). Acid test: 10/10 detects with 4 s
idle gaps — previously ~1-2/10.

Also audited per user challenge: firmware JTAG pins verified against
the 1.0a3 netlist (TDO=39/TCK=40/TMS=41/TDI=42, module pads 35-38,
all match); eFuses read (STRAP_JTAG_SEL=0 — the IO3 strap is inert
for JTAG; pad-JTAG contention theory retired).

Rides along: aux_channel debug_pmod now = ladder FSM state; colorbars
status line gains clk100-liveness bit (HLVC) — every future test
self-certifies its clocking and ladder progress.
Session results (every test now self-certifies via the status line:
D=ladder FSM state, S=SERDES+FIFO, P=hpd gate, E=HPD edges, C=clk100):

- Serializer msg_idx widened 5->6 bits (37-char line truncated at 5 —
  'DP S:' fragments); hpd_present + raw-HPD-edge-count instrumented
  through cm/dp_transmitter to the status line.
- CERTIFIED eliminations on live, established links (D:2E observed
  during every test): word modes 0,1,2,3; FLIPSEL both directions
  (live toggle, twice); with EQ=1.0dB, D0 wake, long dwells, 4x bus
  replication all active. Screen asleep throughout.
- Intermittent ladder-freeze root progress: PROVEN a per-boot race
  (same bitstream boots to D:00-pinned or D:2E across power cycles;
  gate open P:1, clk100 alive, no HPD edges when stuck). Open issue —
  detectable now; reboot clears.
- PD ladder auto-retry observed working in the field (ALT MODE
  RESPONSE TIMEOUT -> DP-ACTIVE with no replug).

Remaining hypothesis space (unreachable from software): TX polarity
bit semantics (IDE regen A/B), analog/swing, deeper GTR12 config
semantics. Next: AD3 + USB-C/DP breakouts — first capture reads the
monitor's LANE0_1_STATUS off SBU: CR_DONE=1/SYM_LOCK=0 => polarity;
CR_DONE=0 => electrical. tools/aux_decode.py ready.
Verified design-arounds for real AD3 limits:
- AUX must be captured on the ANALOG channels: the digital inputs'
  ~1.4 V threshold can read both AUX lines as constant (P idles 0.3 V
  swinging up <=1.38 V; N idles 3.0 V dipping to >=1.6 V). 14-bit
  analog + software thresholding instead.
- Whole conversations exceed the capture buffer: Record mode streaming
  at 4 MS/s (4 samples per 1 us Manchester cell).

tools/ad3_aux_capture.py — WaveForms SDK (ctypes, framework path inside
WaveForms.app verified, dwf 3.25.1 loads + enumerates): Record-mode 1-2
channel capture to CSV. tools/ad3_lane_probe.py — buffer capture with
automated verdict: lane ALIVE/DEAD (Vpp), probe-pattern period check,
and POLARITY from duty cycle. aux_decode.py — differential 3-column
CSV support + adaptive threshold.

Probe pattern reworked for flywire probes + polarity readout: 128-word
frame, 96 high / 32 low = ~1.05 MHz at 75% duty (P-true leg reads 75%,
inverted reads 25%). dp_lane_probe_v2.fs stashed; test top back at
TX_PROBE=0.
…LOSED — GTR12 TX pads silent

- usbc_glue: disable AUX snooper lane power-gating on DP entry (datasheet
  8.3.2: snoop-enabled mux mutes all lanes until LANE_COUNT_SET is snooped
  — any build without a completed AUX ladder transmitted into a muted mux)
- AD3 evidence: mux DP0 output pair drives 1.83 V TX CM with zero data on
  a fresh verified TX_PROBE=1 build -> FPGA SERDES pads have never driven;
  every prior content A/B was void. Critical path: IP regen (task C).
- ad3_lane_probe.py: 3 s range-relay settle, DC-mean reporting (the
  measurement that split mux-alive from FPGA-dead)
- top: TX_PROBE=1 (matches flashed bitstream)
…; provenance settled

- 0x809x68 bit6 patch (tang parity) flashed+probed: pads still silent; reverted
- IPUG1024 archived in boards/a2mega/docs/: TX_ONLY = legit operation mode;
  ready_o = TX channel status; DRP timing documented (readback bridge viable)
- Provenance: entire DP stack incl. both SERDES configs is first-silicon;
  a2_mega CSR is authentic GUI-generator output (2026-07-19 session)
…f-design validates fabric idiom; UG718 archived
edanuff added 24 commits August 16, 2026 16:03
…d) — draw is sub-word, no fabric cancellation target; B (ESP32-JTAG reload fw) now main line
…doc (thesis: reference-design framing dissolves the marginality). Key finds: USB-C/PD/mux already Pin-C/DP4 = zero changes; DP core is param-only 1/2/4-lane + rate (LINK_RATE_MBPS/270 auto RBR byte); real work = transceiver_bank 4-lane extension + pixel-PLL re-source off the now-81MHz symbol clock
… branch (die ln3/2/1/0 <- ML0/1/2/3, clock master stays ln2 so the SDC anchor pin is unchanged), encoder generate loop, MSA 4ch branch wired (was a silent gap — LANE_COUNT=4 would have used the 2ch inserter), pixel PLL ratio parameterized for the 81MHz symbol clock (IDIV1/MDIV11/ODIV6 -> VCO 891 -> 148.5MHz, DP synchronous-clock mode PRESERVED — earlier scope doc claim to re-source it was wrong), dp_test params + SDC 12.346ns. All 103 4-lane port bindings verified against a generated stub: zero errors. Awaiting IDE regen of the PHY for 4 lanes @1.62G
…x_pol_invert x4, txlev 13, un-bonded (self-mastered x4), REFCLK1@135/QPLL0, DRP on. Emission verified lane-by-lane
…s build compiled the STALE 2-lane ROM — watchdog replay would have rewritten 2-lane/2.7G values onto the 4-lane config; caught before flash)
…s 525 writes, overflowing the 9-bit index (entries 512-524 would alias to 0-12); generator emits [9:0] indices now
…ymbol clock, pixel PLL 891MHz VCO, 525-write replay ROM w/ 10-bit index. Timing 0/0
…mask correctly refused 4-lane vs the hub's honest 2-lane DPCD — Anker A8365 teardown confirms 2-lane architecture: IT6563 fed 2 lanes, USB3 runs alongside; chip is 4-lane capable but hub wiring isn't). Test vehicle = monitor-direct 4-lane RBR blind. Timing 0/0
…nks never show readable SINK_STATUS — v1 tore down the healthy blind link every 8s, W cycling on hw, invalidating the monitor run). Timing 0/0
… hub class is 2-lane+USB3; 4-lane systematically dark on monitor AND on a native-DP USB2 adapter = our 4-lane datapath implicated, maiden flight never sim-audited). IP/defines/SDC/top restored byte-for-byte from tag a2mega-2lane-planc-4of5; replay ROM back to 357; kept: blind-gated watchdog, parameterized PLL (identical defaults), dormant 4-lane RTL branch, 10-bit replay idx
…(no user-facing reconfig ever; 2-lane HBR production; tier ladder), revert complete
…ven 2-lane HBR config (monitor-direct gauntlet: was the blind streak luck?)
…6ns on the debug freq-checker path sym_delta->freq_ok — GW5AT PnR variance, non-functional path; rebuilding)
…two registered 28-bit compares then AND — the single-cycle pair missed 135MHz by ~70ps on some seeds); timing 0/0
…eudo-diff 2Vpp pad, cst reverted to separate LVCMOS33 pins; TLVDS-pad monitor confound identified: monitor never lit on ANY TLVDS build, all monitor successes were 2Vpp-era; 0/2 dark on the TLVDS-blind build was ambiguous)
…actual streak-era config) + pseudo-diff pad + blind. Hypothesis under test: chained-vs-unchained interlane profile is SINK-dependent (monitor lit only on chained; hub measured 4/5 on un-chained)
…(FPGA .fs byte-identical across Friday's good period; only ESP32 firmware changed, incl PD source-role rewrite). SOM#2/Saturday-firmware fully confounded. Faithful test = rebuild+upload end-of-Friday firmware 98461af (its .fs=5d8e15f0 already flashed)
…lights the monitor on SOM#2 — HARDWARE EXONERATED, monitor-direct regression is Saturday ESP32 firmware. Orientation-dependent (one plug flip works) => CC/FLIPSEL/mux-flip suspect. Bisect 98461af..HEAD next session
…eplug = full reconfig each time) — streak was REAL not luck; reframes boot-lottery as possibly hub/firmware-specific not fixed silicon tax. Re-test hub w/ Friday fw next session
…rientation AUX_SBU_OVR f6f3a6d) and in-slot hotplug (SOURCE-attach, Fri-PM 29f65c1+) are two personas; likely a Saturday trade-off (fixed slot, broke monitor orientation). Goal = fw satisfying BOTH, bisect watching both signals
… not Saturday. 11:52 5513d72 = monitor BOTH orientations (no in-slot); Fri-PM source-role rework 29f65c1..98461af FIXED in-slot but traded monitor 2-orient->1 (1508618 slot works); Saturday broke 1->0. Goal = both monitor orientations + in-slot together
edanuff added a commit that referenced this pull request Sep 4, 2026
…r) for the schedule-ROM address and the pf_* precompute compares.

nc2/s0n2/s1n2 were two adders + two wrap muxes deep off the line_cycle
flops and fed BOTH the BSRAM schedule-ROM address and the pf_* compare
chains: 'line_cycle -> sched_rom AD[13]' (-0.060) and 'line_cycle ->
pf_vb1/RESET' (+0.016) were the two worst clk_sym paths left in the 60K
after the FIFO skid. nc2_r is now a counter in its own right sharing
line_cycle's zero/increment control (lc_zero/lc_inc — the same terms that
already drive line_cycle's own reset), with its own shallow wrap compare
and a reload to 2 whenever line_cycle reloads to 0; s0n2/s1n2 are pure
wiring off it ({nc2_r,0} / {nc2_r,1}). A first formulation that
recomputed nc2 from line_cycle's next value put a five-way mux + two
wrap-adders on the flop's reset pin and drew -0.055 in a full-core roll —
hence the counter form. Sim-only $fatal proves nc2_r/s0n2_r/s1n2_r equal
the combinational values at every clock. Full-chain harness both packer
configs: identical to baseline (3,037,500 cycles no underrun, 720 rows
pixel-exact, ALL AUDIO CHECKS PASSED); the assertion never fired.
edanuff added a commit that referenced this pull request Sep 4, 2026
…k_sym +0.36/+0.41/+0.10) and (b); remaining-cone list updated (floor = wr_fifo DI clk_logic; overlays/framebuffer clk_pix; DOC; AUX; sdp)
edanuff added a commit that referenced this pull request Sep 4, 2026
… (nc2_r counter); dp_test 0/0, 60K rolls 0/0/0

Timing round 2 #2: packer registered two-step lookahead (nc2_r counter) — dp_test 0/0, 60K rolls 0/0/0
edanuff added a commit that referenced this pull request Sep 4, 2026
…ares split into registered 14-bit half results, combined a cycle later.

sym_delta -> freq_ok was dp_test's last violating family after cones #1/#2
(-0.52 .. -1.62 across deterministic draws): two 28-bit compares in one
135 MHz cycle on a value that is static for a full second. Now each
compare is A>B <=> Ah>Bh || (Ah==Bh && Al>Bl) with the half results
registered, combined next cycle — three cycles of latency on a 1 Hz
instrument (led[2], telemetry F:), zero behavior change. dp_test-only;
verified by the deterministic gate (0 freq_ok endpoints, 0/0 overall).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant