diff --git a/CHANGELOG.md b/CHANGELOG.md index ca06e96..5735cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] + +### Fixed +- **GC573 green/white speckles and audio crackle on HDMI 2.0 sources (4K60, 1440p120):** + - The IT6664 input receiver was left at its fixed power-on equaliser (RS `0x1f`, forced), so >340 MHz TMDS links decoded a steady stream of wrong characters. The errors were re-encoded by the IT6664, so they showed up on the HDMI OUT passthrough too and were invisible to the IT6805. Windows was clean on the same hardware and cable. + - New `gc573_user_rx_eq.[ch]` drives both receivers (IT6664 input and IT6805 capture share the same ITE receiver core). On HDMI 2.0 lock it measures per-lane TMDS character errors (CED), leaves error-free links alone, and otherwise runs hardware auto-EQ with DFE taps, falling back to a per-lane RS sweep and skew retraining scored by measured errors. + - Hardware result (RetroTINK 4K, 4K60 RGB, ~593 MHz): input errors went from ~25,500 per 100 ms to 0 on every lane. Tuning adds ~0.75 s at signal lock. + +### Added +- `[HDMI] TMDS errors B/G/R` line in `streamer.log` with per-lane error counts for the card input and the internal capture hop: every second while errors occur, otherwise every 10 s. +- `EVERLIVE_NO_AUTO_EQ=1` disables link equalisation; `EVERLIVE_EQ_INPUT_DFE=0` keeps the input on RS-only EQ (for comparison and troubleshooting). + ## [1.4.0] - 2026-11-06 ### Added diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 76a1ba5..0557ca6 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -17,10 +17,10 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) # Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, # which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_it6664.o third_party/jsmn.o +COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o third_party/jsmn.o STREAM_OBJS = gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag -HEADERS = gc573-user-compat.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h +HEADERS = gc573-user-compat.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_rx_eq.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h all: $(TARGETS) diff --git a/tools/opengc573-userd/gc573_user_it6664.c b/tools/opengc573-userd/gc573_user_it6664.c index 0e3161c..c08e468 100644 --- a/tools/opengc573-userd/gc573_user_it6664.c +++ b/tools/opengc573-userd/gc573_user_it6664.c @@ -2597,6 +2597,21 @@ static int it6664_handle_rx_lock(void *bar0) return 0; } +int gc573_user_it6664_relink_outputs(void *bar0) +{ + if (!bar0) + return -EINVAL; + if (!g_rx_locked) + return 0; + /* Same state reset as an RX sync loss, then the normal lock path. */ + g_tx2_powered = false; + g_tx2_output_enabled = false; + g_tx2_error_reported = false; + g_tx1_powered = false; + g_tx1_pclk_khz = 0; + return it6664_handle_rx_lock(bar0); +} + int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status) { struct gc573_user_it6664_status local = { 0 }; diff --git a/tools/opengc573-userd/gc573_user_it6664.h b/tools/opengc573-userd/gc573_user_it6664.h index 119bc17..9705c66 100644 --- a/tools/opengc573-userd/gc573_user_it6664.h +++ b/tools/opengc573-userd/gc573_user_it6664.h @@ -64,6 +64,14 @@ int gc573_user_it6664_enable_source(void *bar0); int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status); +/** + * gc573_user_it6664_relink_outputs - Re-arm TX1 (capture) and TX2 (HDMI + * OUT) as after a fresh RX lock. For when the RX input was disturbed (e.g. + * by equaliser changes) without the poll loop seeing sync drop, leaving the + * outputs on a stale clock. No-op unless the RX is locked. + */ +int gc573_user_it6664_relink_outputs(void *bar0); + /** * gc573_user_it6664_pulse_hpd - Pulse upstream HDMI RX HPD to trigger source re-sync * @bar0: Pointer to mapped BAR0 memory diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c new file mode 100644 index 0000000..5dc8a03 --- /dev/null +++ b/tools/opengc573-userd/gc573_user_rx_eq.c @@ -0,0 +1,639 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc573_user_rx_eq.c — HDMI 2.0 receiver equalisation for the GC573 + * + * At 4K60 RGB (~594 MHz pixel clock, 5.94 Gbit/s per TMDS lane) the fixed + * power-on equaliser setting (RS 0x1f, forced) is not always good enough: + * the receiver still locks, but a few TMDS characters per frame decode + * wrongly. That shows up as sparse coloured speckles in the picture and as + * crackle in the audio, which travels in the same data islands. + * + * Both GC573 receivers (IT6664 input, IT6805 capture) use the same ITE + * receiver core, so one implementation drives either. It provides: + * - per-channel TMDS character-error (CED) counters, blocking and + * non-blocking, so link quality can be measured and logged; + * - a tuning pass for HDMI 2.0 links: hardware successive-approximation + * auto-EQ, DFE tap derivation from the auto-EQ amplitude + * readback, a per-channel RS sweep scored by CED when errors remain, + * and a final RS-fixed skew training pass. + * + * Register usage is functional interoperability knowledge of the ITE + * receiver core; the control flow is EverLive's own. + * + * bank 0 0x07 EQ IRQ status (W1C): [4] auto-EQ done, [5] fail + * bank 0 0x0f bank select [1:0] + * bank 0 0x14 [7:6] HDMI 2.0 clock ratio / scrambling detected (IT6805) + * bank 0 0x23 [4] EQ reset (pulse), other bits preserved + * bank 0 0x3b CED options; [3] count from the EQ engine's counter + * bank 3 0x20/0x21 clock-select init (0x1b/0x03), 0x20[7] auto trigger + * bank 3 0x22 [6] DFE enable, [5:3] AMP/EQ/EQ-power, [2] EQ trigger + * bank 3 0x26-0x29 RS: 0x27 ch0(B) 0x28 ch1(G) 0x29 ch2(R), [7] force + * bank 3 0x2c/0x2d skew control (0x00/0x07 forces skew 0) + * bank 3 0x30-0x39 EQ engine options / timing + * bank 3 0x36[3:0] RS index for amplitude readback + * bank 3 0x37[7:6] channel select for readback, [4] RS one-stage + * bank 3 0x4b-0x53 manual DFE taps (3 per channel), 0x4b[7] enable + * bank 3 0x54[7] pre-EQ, 0x55 RSM threshold ([7] clear for CED read) + * bank 3 0x5d-0x60 amplitude readback A..D for the selected RS index + * bank 3 0x63/0x64 AMP timeout per channel (0x3fff = timed out) + * bank 3 0xd5-0xd7 auto-EQ resulting RS per channel + * bank 3 0xe9-0xeb CED counter control / low / high ([7] valid) + */ + +#include "gc573_user_rx_eq.h" +#include "gc573_user_it6805.h" +#include "gc573_user_i2c.h" +#include +#include +#include +#include + +#define BANK0 0x00 +#define BANK3 0x03 +#define REG_BANK_SELECT 0x0f + +#define EQ_DONE_TIMEOUT_MS 500 +#define EQ_POLL_MS 10 +#define CED_WINDOW_MS 100 +#define CED_SATURATED 0x7fff + +#define CH_COUNT GC573_RX_TMDS_CHANNELS + +struct rx_port { + const char *name; + u8 addr; + /* Apply DFE taps derived from the auto-EQ amplitude readback. */ + bool dfe; + int (*set_bank)(void *bar0, u8 bank); +}; + +/* The IT6664 RX has no shared bank cache; select explicitly every time. */ +static int it6664_rx_set_bank(void *bar0, u8 bank) +{ + u8 val = bank & 0x03; + + return gc573_user_i2c_write(bar0, GC573_I2C_BUS_PRIMARY, 0x38, + REG_BANK_SELECT, 1, &val, 1); +} + +static const struct rx_port rx_ports[GC573_RX_PORT_COUNT] = { + [GC573_RX_INPUT] = { + .name = "input (IT6664)", + .addr = 0x38, + /* + * Hardware-verified: without DFE the TMDS G lane still + * misdecoded ~2800 characters/s at 4K60 even at RS 0x00; + * auto-EQ + DFE brought every lane to zero errors. + */ + .dfe = true, + .set_bank = it6664_rx_set_bank, + }, + [GC573_RX_CAPTURE] = { + .name = "capture (IT6805)", + .addr = 0x48, + .dfe = true, + /* Keeps the IT6805 driver's bank cache coherent. */ + .set_bank = gc573_user_it6805_set_bank, + }, +}; + +/* RS values the amplitude readback indexes (0x36[3:0] = position). */ +static const u8 rs_ladder[] = { + 0x7f, 0x7e, 0x3f, 0x3e, 0x1f, 0x1e, 0x0f, + 0x0e, 0x07, 0x06, 0x03, 0x02, 0x01, 0x00, +}; +#define RS_LADDER_LEN (sizeof(rs_ladder) / sizeof(rs_ladder[0])) +#define RS_DEFAULT_INDEX 4 /* 0x1f */ + +/* + * The manual sweep tries every RS step the receiver defines. Short or + * strong links can want the weakest settings (down to 0x00), so the ends + * of the range must be covered. + */ +#define rs_sweep rs_ladder +#define RS_SWEEP_LEN RS_LADDER_LEN + +struct dfe_taps { + u8 tap[3]; +}; + +static struct { + bool armed; + u64 armed_at_ms; +} g_ced_poll[GC573_RX_PORT_COUNT]; + +/* ---------- register helpers ---------- */ + +static int rd(void *bar0, const struct rx_port *p, u8 bank, u8 reg, u8 *val) +{ + int ret = p->set_bank(bar0, bank); + + if (ret < 0) + return ret; + return gc573_user_i2c_read(bar0, GC573_I2C_BUS_PRIMARY, p->addr, + reg, 1, val, 1); +} + +static int wr(void *bar0, const struct rx_port *p, u8 bank, u8 reg, u8 val) +{ + int ret = p->set_bank(bar0, bank); + + if (ret < 0) + return ret; + return gc573_user_i2c_write(bar0, GC573_I2C_BUS_PRIMARY, p->addr, + reg, 1, &val, 1); +} + +static int upd(void *bar0, const struct rx_port *p, u8 bank, u8 reg, + u8 mask, u8 val) +{ + u8 cur = 0; + int ret = rd(bar0, p, bank, reg, &cur); + + if (ret < 0) + return ret; + return wr(bar0, p, bank, reg, (u8)((cur & ~mask) | (val & mask))); +} + +#define TRY(expr) \ + do { \ + int ret_ = (expr); \ + if (ret_ < 0) \ + return ret_; \ + } while (0) + +static u64 mono_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (u64)ts.tv_sec * 1000ULL + (u64)ts.tv_nsec / 1000000ULL; +} + +const char *gc573_rx_port_name(enum gc573_rx_port port) +{ + return port < GC573_RX_PORT_COUNT ? rx_ports[port].name : "?"; +} + +/* ---------- link state / CED ---------- */ + +bool gc573_rx_link_is_hdmi2(void *bar0) +{ + const struct rx_port *p = &rx_ports[GC573_RX_CAPTURE]; + u8 status14 = 0; + bool hdmi2; + + if (rd(bar0, p, BANK0, 0x14, &status14) < 0) + return false; + hdmi2 = (status14 & 0xc0) != 0; + p->set_bank(bar0, BANK0); + return hdmi2; +} + +static int ced_arm(void *bar0, const struct rx_port *p) +{ + TRY(upd(bar0, p, BANK0, 0x3b, 0x08, 0x08)); + TRY(upd(bar0, p, BANK3, 0x55, 0x80, 0x00)); + TRY(wr(bar0, p, BANK3, 0xe9, 0x80)); + return p->set_bank(bar0, BANK0); +} + +static int ced_latch(void *bar0, const struct rx_port *p, + struct gc573_rx_ced *out) +{ + /* 0xe9[6:5] selects the channel whose count shows in 0xea/0xeb. */ + static const u8 select[CH_COUNT] = { 0x00, 0x20, 0x40 }; + + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + u8 lo = 0, hi = 0; + + TRY(wr(bar0, p, BANK3, 0xe9, select[ch])); + TRY(rd(bar0, p, BANK3, 0xea, &lo)); + TRY(rd(bar0, p, BANK3, 0xeb, &hi)); + out->valid[ch] = (hi & 0x80) != 0; + out->count[ch] = (u16)(((hi & 0x7f) << 8) | lo); + } + return p->set_bank(bar0, BANK0); +} + +static int ced_measure(void *bar0, const struct rx_port *p, + unsigned int window_ms, struct gc573_rx_ced *out) +{ + memset(out, 0, sizeof(*out)); + TRY(ced_arm(bar0, p)); + msleep(window_ms); + out->window_ms = window_ms; + return ced_latch(bar0, p, out); +} + +int gc573_rx_ced_measure(void *bar0, enum gc573_rx_port port, + unsigned int window_ms, struct gc573_rx_ced *out) +{ + if (!bar0 || !out || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + return ced_measure(bar0, &rx_ports[port], window_ms, out); +} + +int gc573_rx_ced_poll(void *bar0, enum gc573_rx_port port, u64 now_ms, + struct gc573_rx_ced *out) +{ + const struct rx_port *p; + int ret; + + if (!bar0 || !out || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + p = &rx_ports[port]; + + if (!g_ced_poll[port].armed) { + ret = ced_arm(bar0, p); + if (ret < 0) + return ret; + g_ced_poll[port].armed = true; + g_ced_poll[port].armed_at_ms = now_ms; + return -EAGAIN; + } + + memset(out, 0, sizeof(*out)); + ret = ced_latch(bar0, p, out); + if (ret == 0) { + out->window_ms = (u32)(now_ms - g_ced_poll[port].armed_at_ms); + ret = ced_arm(bar0, p); + } + if (ret < 0) { + g_ced_poll[port].armed = false; + return ret; + } + g_ced_poll[port].armed_at_ms = now_ms; + return 0; +} + +void gc573_rx_ced_reset(void) +{ + memset(g_ced_poll, 0, sizeof(g_ced_poll)); +} + +/* Invalid or saturated channels score as the worst possible value. */ +static u32 ced_score(const struct gc573_rx_ced *c, unsigned int ch) +{ + return c->valid[ch] ? c->count[ch] : CED_SATURATED; +} + +static bool ced_any_valid(const struct gc573_rx_ced *c) +{ + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + if (c->valid[ch]) + return true; + return false; +} + +static u32 ced_total(const struct gc573_rx_ced *c) +{ + u32 sum = 0; + + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + sum += ced_score(c, ch); + return sum; +} + +/* ---------- EQ engine ---------- */ + +/* Pulse the EQ reset bit; leave every other bit of 0x23 as it was. */ +static int eq_reset_pulse(void *bar0, const struct rx_port *p) +{ + u8 reg23 = 0; + + TRY(rd(bar0, p, BANK0, 0x23, ®23)); + TRY(wr(bar0, p, BANK0, 0x07, 0xff)); + TRY(wr(bar0, p, BANK0, 0x23, (u8)(reg23 | 0xb0))); + msleep(10); + TRY(wr(bar0, p, BANK0, 0x23, (u8)((reg23 | 0xa0) & ~0x10))); + return 0; +} + +/* Waits for the auto-EQ done/fail IRQ; returns 1 done, 0 failed/timeout. */ +static int eq_wait(void *bar0, const struct rx_port *p) +{ + u64 deadline = mono_ms() + EQ_DONE_TIMEOUT_MS; + int result = 0; + + for (;;) { + u8 irq = 0; + + TRY(rd(bar0, p, BANK0, 0x07, &irq)); + if (irq & 0x30) { + TRY(wr(bar0, p, BANK0, 0x07, irq & 0x30)); + result = (irq & 0x10) ? 1 : 0; + break; + } + if (mono_ms() >= deadline) + break; + msleep(EQ_POLL_MS); + } + /* Drop the trigger again once the engine has finished. */ + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x00)); + TRY(p->set_bank(bar0, BANK0)); + return result; +} + +static int eq_run_auto(void *bar0, const struct rx_port *p) +{ + static const u8 setup[][2] = { + { 0x26, 0x00 }, + { 0x27, 0x1f }, { 0x28, 0x1f }, { 0x29, 0x1f }, + { 0x30, 0x8c }, /* skew option on */ + { 0x31, 0xb0 }, /* AMP time */ + { 0x32, 0x43 }, { 0x33, 0x47 }, { 0x34, 0x4b }, { 0x35, 0x53 }, + { 0x37, 0x0b }, + { 0x38, 0xf2 }, /* monitor time */ + { 0x39, 0x0d }, /* CED valid threshold */ + { 0x55, 0x40 }, /* RSM threshold */ + }; + + /* The trigger must be low before the EQ reset. */ + TRY(wr(bar0, p, BANK3, 0x20, 0x1b)); + TRY(wr(bar0, p, BANK3, 0x21, 0x03)); + TRY(upd(bar0, p, BANK3, 0x20, 0x80, 0x00)); + TRY(wr(bar0, p, BANK3, 0x22, 0x00)); + + TRY(eq_reset_pulse(bar0, p)); + TRY(wr(bar0, p, BANK0, 0x3b, 0x23)); + + TRY(upd(bar0, p, BANK3, 0x2c, 0xc0, 0xc0)); + TRY(upd(bar0, p, BANK3, 0x2d, 0xf7, 0x20)); + for (size_t i = 0; i < sizeof(setup) / sizeof(setup[0]); i++) + TRY(wr(bar0, p, BANK3, setup[i][0], setup[i][1])); + TRY(upd(bar0, p, BANK3, 0x36, 0xc0, 0x00)); + TRY(upd(bar0, p, BANK3, 0x4a, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x4b, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x54, 0x80, 0x80)); + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x04)); + return eq_wait(bar0, p); +} + +/* + * Derive DFE taps for every RS step on one channel from the amplitude + * samples the auto-EQ engine leaves behind. Tap n is half the difference + * between sample A and sample n+1, signed, clamped to 5/4/3 bits. + */ +static int eq_read_dfe(void *bar0, const struct rx_port *p, unsigned int ch, + struct dfe_taps taps[RS_LADDER_LEN]) +{ + static const u8 tap_max[3] = { 0x1f, 0x0f, 0x07 }; + static const u8 tap_flag[3] = { 0x40, 0x20, 0x10 }; + + TRY(upd(bar0, p, BANK3, 0x37, 0xc0, (u8)(ch << 6))); + for (unsigned int i = 0; i < RS_LADDER_LEN; i++) { + u8 amp[4]; + + TRY(upd(bar0, p, BANK3, 0x36, 0x0f, (u8)i)); + for (unsigned int k = 0; k < 4; k++) + TRY(rd(bar0, p, BANK3, (u8)(0x5d + k), &[k])); + for (unsigned int t = 0; t < 3; t++) { + bool negative = amp[0] > amp[t + 1]; + u8 mag = negative ? (u8)((amp[0] - amp[t + 1]) >> 1) : + (u8)((amp[t + 1] - amp[0]) >> 1); + + if (mag > tap_max[t]) + mag = tap_max[t]; + /* sign bit sits just above the magnitude field */ + taps[i].tap[t] = (u8)(tap_flag[t] | + (negative ? (tap_max[t] + 1) : 0) | + mag); + } + } + return 0; +} + +static unsigned int rs_index(u8 rs) +{ + for (unsigned int i = 0; i < RS_LADDER_LEN; i++) + if (rs_ladder[i] == rs) + return i; + return RS_DEFAULT_INDEX; +} + +/* + * Force one RS value per channel and, when @dfe is non-NULL, load the + * matching DFE taps and enable the manual DFE path. + */ +static int eq_apply(void *bar0, const struct rx_port *p, const u8 rs[CH_COUNT], + struct dfe_taps (*dfe)[RS_LADDER_LEN]) +{ + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + TRY(wr(bar0, p, BANK3, (u8)(0x27 + ch), (u8)(0x80 | rs[ch]))); + if (dfe) { + const struct dfe_taps *t = &dfe[ch][rs_index(rs[ch])]; + + for (unsigned int k = 0; k < 3; k++) + TRY(wr(bar0, p, BANK3, (u8)(0x4b + ch * 3 + k), + t->tap[k])); + } + } + if (p->dfe) { + /* 0x4b[7] shares a register with ch0's first tap: set last. */ + TRY(upd(bar0, p, BANK3, 0x4b, 0x80, dfe ? 0x80 : 0x00)); + TRY(upd(bar0, p, BANK3, 0x22, 0x40, dfe ? 0x40 : 0x00)); + } + return p->set_bank(bar0, BANK0); +} + +/* Skew training with the RS values already forced. */ +static int eq_run_skew(void *bar0, const struct rx_port *p) +{ + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x00)); + TRY(wr(bar0, p, BANK3, 0x2d, 0x00)); + TRY(wr(bar0, p, BANK3, 0x30, 0x94)); /* manual RS + skew option */ + TRY(wr(bar0, p, BANK3, 0x31, 0xb0)); + TRY(upd(bar0, p, BANK3, 0x37, 0x10, 0x10)); + TRY(upd(bar0, p, BANK3, 0x54, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x04)); + return eq_wait(bar0, p); +} + +static int eq_force_zero_skew(void *bar0, const struct rx_port *p) +{ + TRY(wr(bar0, p, BANK3, 0x2c, 0x00)); + TRY(wr(bar0, p, BANK3, 0x2d, 0x07)); + return p->set_bank(bar0, BANK0); +} + +static bool env_flag(const char *name) +{ + const char *v = getenv(name); + + return v && *v && strcmp(v, "0") != 0; +} + +/* + * EVERLIVE_EQ_INPUT_DFE=0 turns DFE off on the IT6664 input for comparison. + */ +static bool port_dfe(enum gc573_rx_port port) +{ + if (port == GC573_RX_INPUT) { + const char *v = getenv("EVERLIVE_EQ_INPUT_DFE"); + + if (v && strcmp(v, "0") == 0) + return false; + } + return rx_ports[port].dfe; +} + +/* + * Auto-EQ and apply its result. Fills @rs, @dfe and @dfe_ok, returns 1 + * when the hardware reported completion, 0 if it failed (RS left at the + * power-on value), negative errno on I2C failure. + */ +static int eq_auto_and_apply(void *bar0, const struct rx_port *p, + u8 rs[CH_COUNT], + struct dfe_taps (*dfe)[RS_LADDER_LEN], + bool *dfe_ok) +{ + int done = eq_run_auto(bar0, p); + + *dfe_ok = false; + if (done < 0) + return done; + if (!done) { + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + rs[ch] = 0x1f; + TRY(eq_apply(bar0, p, rs, NULL)); + return 0; + } + + *dfe_ok = p->dfe; + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + u8 lo = 0, hi = 0, r = 0; + + TRY(rd(bar0, p, BANK3, (u8)(0xd5 + ch), &r)); + rs[ch] = r & 0x7f; + if (!p->dfe) + continue; + TRY(upd(bar0, p, BANK3, 0x37, 0xc0, (u8)(ch << 6))); + TRY(rd(bar0, p, BANK3, 0x63, &lo)); + TRY(rd(bar0, p, BANK3, 0x64, &hi)); + if ((u16)((hi << 8) | lo) == 0x3fff) + *dfe_ok = false; /* amplitude search timed out */ + } + if (*dfe_ok) + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + TRY(eq_read_dfe(bar0, p, ch, dfe[ch])); + TRY(eq_apply(bar0, p, rs, *dfe_ok ? dfe : NULL)); + return 1; +} + +int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + struct gc573_rx_eq_result *res) +{ + struct dfe_taps dfe[CH_COUNT][RS_LADDER_LEN]; + struct rx_port port_copy; + const struct rx_port *p; + struct gc573_rx_ced ced; + u32 best_score[CH_COUNT]; + u8 best_rs[CH_COUNT]; + u8 rs[CH_COUNT]; + u64 start = mono_ms(); + bool dfe_ok = false; + u32 auto_total; + int ret; + + if (!bar0 || !res || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + port_copy = rx_ports[port]; + port_copy.dfe = port_dfe(port); + p = &port_copy; + memset(res, 0, sizeof(*res)); + memset(dfe, 0, sizeof(dfe)); + + if (env_flag("EVERLIVE_NO_AUTO_EQ") || !gc573_rx_link_is_hdmi2(bar0)) + return 0; + g_ced_poll[port].armed = false; + + /* 1. Baseline on the current EQ; leave an error-free link alone. */ + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + res->errors_before = ced_total(&ced); + res->ced_valid = ced_any_valid(&ced); + res->before_valid = res->ced_valid; + if (res->ced_valid && res->errors_before == 0) { + res->elapsed_ms = (u32)(mono_ms() - start); + return 0; + } + res->ran = true; + + /* 2. Hardware auto-EQ (+ DFE where supported). */ + ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok); + if (ret < 0) + return ret; + res->auto_eq_done = ret == 1; + res->dfe_applied = dfe_ok; + + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + auto_total = ced_total(&ced); + res->ced_valid |= ced_any_valid(&ced); + memcpy(best_rs, rs, sizeof(best_rs)); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + best_score[ch] = ced_score(&ced, ch); + + /* + * 3. Errors remain: sweep RS per channel with skew forced to 0. The + * sweep is scored by CED, so it is pointless without valid counts. + */ + if (auto_total && ced_any_valid(&ced)) { + u32 sweep_total = 0; + + res->swept = true; + TRY(eq_force_zero_skew(bar0, p)); + for (unsigned int i = 0; i < RS_SWEEP_LEN; i++) { + u8 trial[CH_COUNT] = { rs_sweep[i], rs_sweep[i], + rs_sweep[i] }; + + TRY(eq_apply(bar0, p, trial, dfe_ok ? dfe : NULL)); + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + if (ced_score(&ced, ch) < best_score[ch]) { + best_score[ch] = ced_score(&ced, ch); + best_rs[ch] = rs_sweep[i]; + } + } + } + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + sweep_total += best_score[ch]; + + /* 4. Retrain skew around the chosen RS values. */ + TRY(eq_apply(bar0, p, best_rs, dfe_ok ? dfe : NULL)); + ret = eq_run_skew(bar0, p); + if (ret < 0) + return ret; + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + + res->skew_trained = true; + /* Skew training made it worse: keep skew 0. */ + if (ced_total(&ced) > sweep_total) { + res->skew_trained = false; + TRY(eq_force_zero_skew(bar0, p)); + TRY(eq_apply(bar0, p, best_rs, dfe_ok ? dfe : NULL)); + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + } + + /* Nothing beat plain auto-EQ: run it again and keep that. */ + if (ced_total(&ced) > auto_total) { + ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok); + if (ret < 0) + return ret; + memcpy(best_rs, rs, sizeof(best_rs)); + res->dfe_applied = dfe_ok; + res->kept_auto = true; + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + } + } + + /* 5. Done: power the EQ engine down and clear its IRQs. */ + TRY(upd(bar0, p, BANK3, 0x22, 0x08, 0x00)); + TRY(wr(bar0, p, BANK0, 0x07, 0xf0)); + + memcpy(res->rs, best_rs, sizeof(res->rs)); + res->errors_after = ced_total(&ced); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + res->ch_errors_after[ch] = ced_score(&ced, ch); + res->elapsed_ms = (u32)(mono_ms() - start); + return p->set_bank(bar0, BANK0); +} diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/gc573_user_rx_eq.h new file mode 100644 index 0000000..8b461db --- /dev/null +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc573_user_rx_eq.h — HDMI 2.0 receiver equalisation and TMDS + * character-error (CED) monitoring for the GC573 userspace driver. + * + * The GC573 has two ITE HDMI receivers in series that share the same + * receiver core and EQ register layout: + * - the IT6664 splitter's input (the card's HDMI IN connector), and + * - the IT6805 capture receiver fed by IT6664 TX1. + * The IT6664 re-encodes the stream, so input errors are invisible to the + * IT6805's counters; each receiver is measured and tuned separately. + */ + +#ifndef GC573_USER_RX_EQ_H +#define GC573_USER_RX_EQ_H + +#include "gc573-user-compat.h" +#include + +#define GC573_RX_TMDS_CHANNELS 3 + +enum gc573_rx_port { + GC573_RX_INPUT = 0, /* IT6664 RX: the card's HDMI IN */ + GC573_RX_CAPTURE, /* IT6805 RX: internal hop from IT6664 TX1 */ + GC573_RX_PORT_COUNT, +}; + +/* Per-channel character error counts over one measurement window. */ +struct gc573_rx_ced { + bool valid[GC573_RX_TMDS_CHANNELS]; /* counter reported valid */ + u16 count[GC573_RX_TMDS_CHANNELS]; /* errors, saturates at 0x7fff */ + u32 window_ms; +}; + +struct gc573_rx_eq_result { + bool ran; /* false: not HDMI 2.0, disabled, or clean */ + bool ced_valid; /* CED counters reported valid counts */ + bool before_valid; /* baseline window had valid counts */ + bool auto_eq_done; /* hardware auto-EQ reported completion */ + bool dfe_applied; /* DFE taps derived from auto-EQ were applied */ + bool swept; /* manual per-channel RS sweep was needed */ + bool skew_trained; /* skew retraining improved on skew 0 */ + bool kept_auto; /* sweep lost to plain auto-EQ; kept that */ + u8 rs[GC573_RX_TMDS_CHANNELS]; /* final RS per channel (B/G/R) */ + u32 errors_before; /* summed CED over the baseline window */ + u32 errors_after; /* summed CED over the final window */ + u32 ch_errors_after[GC573_RX_TMDS_CHANNELS]; + u32 elapsed_ms; +}; + +const char *gc573_rx_port_name(enum gc573_rx_port port); + +/** + * gc573_rx_link_is_hdmi2 - True when the capture receiver sees a scrambled + * HDMI 2.0 (>340 MHz TMDS) link. The IT6664 retransmits at the input rate, + * so this also describes the input link. + */ +bool gc573_rx_link_is_hdmi2(void *bar0); + +/** Blocking CED measurement over @window_ms. */ +int gc573_rx_ced_measure(void *bar0, enum gc573_rx_port port, + unsigned int window_ms, struct gc573_rx_ced *out); + +/** + * gc573_rx_ced_poll - Non-blocking CED measurement for the streaming loop. + * The first call arms the counters and returns -EAGAIN; each later call + * latches the window since the previous call into @out and re-arms. + */ +int gc573_rx_ced_poll(void *bar0, enum gc573_rx_port port, u64 now_ms, + struct gc573_rx_ced *out); + +/** Forget any armed CED windows (call when the signal is lost). */ +void gc573_rx_ced_reset(void); + +/** + * gc573_rx_eq_tune - Equalise one receiver on an HDMI 2.0 link + * + * Measures CED first and leaves an error-free link untouched. Otherwise + * runs the receiver's hardware auto-EQ, applies the result plus DFE taps + * and, if errors remain, sweeps the RS (equaliser + * strength) per TMDS channel and keeps the setting with the fewest errors. + * No-op on HDMI 1.x links or when EVERLIVE_NO_AUTO_EQ=1 is set. Blocks for + * up to roughly 1.5 s; the link may briefly lose SCDT while EQ runs. + */ +int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + struct gc573_rx_eq_result *result); + +#endif /* GC573_USER_RX_EQ_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index db6d88e..0e54050 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -20,6 +20,7 @@ #include "gc573_user_edid.h" #include "gc573_user_it6664.h" #include "gc573_user_it6805.h" +#include "gc573_user_rx_eq.h" #include "gc573_user_fpga.h" #include "gc573_user_video.h" #include "gc573_user_audio.h" @@ -1105,6 +1106,93 @@ static int compute_output_and_buffers( return 0; } +/* Wait up to @timeout_ms for the IT6805 to report SCDT. */ +static bool wait_capture_scdt(void *bar0, unsigned int timeout_ms) +{ + for (unsigned int waited = 0; waited < timeout_ms; waited += 20) { + struct gc573_user_video_signal st = { 0 }; + + if (gc573_user_it6805_poll_status(bar0, &st) == 0 && + st.scdt_locked) + return true; + SDL_Delay(20); + } + return false; +} + +/* + * Equalise an HDMI 2.0 link before the timing is measured: first the card's + * input (IT6664), whose errors are re-encoded and invisible downstream, then + * the internal hop into the IT6805. Afterwards give the receiver a moment + * to re-assert SCDT if the EQ retraining dropped it. + */ +static void tune_link_eq(void *bar0) +{ + bool touched = false; + + if (!gc573_rx_link_is_hdmi2(bar0)) + return; + + for (int port = 0; port < GC573_RX_PORT_COUNT; port++) { + struct gc573_rx_eq_result eq; + const char *name = gc573_rx_port_name(port); + int ret = gc573_rx_eq_tune(bar0, port, &eq); + char before[16], after[16]; + + if (ret < 0) { + pr_warn("HDMI 2.0 link EQ (%s) failed: %d", name, ret); + touched = true; + continue; + } + if (!eq.ran) { + if (eq.ced_valid) + printf("[HDMI] Link EQ %s: error-free; keeping current EQ\n", + name); + else if (!getenv("EVERLIVE_NO_AUTO_EQ")) + printf("[HDMI] Link EQ %s: skipped\n", name); + continue; + } + touched = true; + if (eq.before_valid) + snprintf(before, sizeof(before), "%u", eq.errors_before); + else + snprintf(before, sizeof(before), "n/a"); + if (eq.ced_valid) + snprintf(after, sizeof(after), "%u", eq.errors_after); + else + snprintf(after, sizeof(after), "n/a"); + printf("[HDMI] Link EQ %s: TMDS errors %s -> %s per 100 ms (B/G/R %u/%u/%u) | RS B/G/R 0x%02x/0x%02x/0x%02x | auto-EQ %s | DFE %s | sweep %s | skew %s | %u ms\n", + name, before, after, + eq.ch_errors_after[0], eq.ch_errors_after[1], + eq.ch_errors_after[2], + eq.rs[0], eq.rs[1], eq.rs[2], + eq.auto_eq_done ? "done" : "failed", + eq.dfe_applied ? "on" : "off", + !eq.swept ? "no" : eq.kept_auto ? "lost to auto-EQ" : "yes", + !eq.swept ? "auto" : eq.skew_trained ? "trained" : "forced 0", + eq.elapsed_ms); + } + if (!touched) + return; + + if (wait_capture_scdt(bar0, 500)) + return; + /* + * Retuning the input can leave the IT6664's TX1 (the internal link to + * the IT6805) on a stale clock without the RX ever reporting a sync + * loss, so nothing re-arms it. Do what an RX re-lock would do. + */ + pr_warn("SCDT did not re-lock within 500 ms after link EQ; re-arming the IT6664 outputs"); + if (gc573_user_it6664_relink_outputs(bar0) < 0) { + pr_warn("IT6664 output re-arm failed"); + return; + } + if (wait_capture_scdt(bar0, 1000)) + pr_info("Capture link re-locked after re-arming the IT6664 outputs"); + else + pr_warn("SCDT still not locked after re-arming the IT6664 outputs"); +} + static bool signal_usable(const struct gc573_user_video_signal *sig) { return sig->scdt_locked && sig->avi_valid && sig->width && sig->height && @@ -6521,6 +6609,7 @@ int main(int argc, char *argv[]) rate_probe_after_scdt = true; if (signal_lost_hits >= SIGNAL_CHANGE_HITS) { printf("[VIDEO] HDMI signal lost; stopping capture.\n"); + gc573_rx_ced_reset(); audio_stop_capture(&audio); ret = gc573_user_it6805_audio_disable(ctx.bar0); if (ret < 0) @@ -6562,6 +6651,7 @@ int main(int argc, char *argv[]) * The preview thread keeps draining video DMA. */ audio_stop_capture(&audio); + tune_link_eq(ctx.bar0); ret = gc573_user_it6805_poll_signal(ctx.bar0, &fresh); if (ret < 0 || !signal_usable(&fresh)) { @@ -6697,6 +6787,7 @@ int main(int argc, char *argv[]) struct gc573_user_video_signal sig = { 0 }; printf("[VIDEO] SCDT locked; measuring input timing...\n"); + tune_link_eq(ctx.bar0); ret = gc573_user_it6805_poll_signal(ctx.bar0, &sig); if (ret == 0 && signal_usable(&sig)) { @@ -7195,6 +7286,55 @@ int main(int argc, char *argv[]) (unsigned long)__atomic_load_n(&g_preview.dropped, __ATOMIC_RELAXED), (double)g_preview.taken_age_us / 1000.0); + { + /* + * TMDS character errors since the last tick on the + * card's input and on the internal capture hop. + * Logged whenever non-zero, otherwise every 10 s, so + * link quality is visible without flooding the log. + */ + static unsigned int ced_quiet_ticks; + struct gc573_rx_ced ced[GC573_RX_PORT_COUNT]; + bool have[GC573_RX_PORT_COUNT] = { false }; + bool any = false, errors = false; + + for (int port = 0; port < GC573_RX_PORT_COUNT; + port++) { + struct gc573_rx_ced *c = &ced[port]; + + if (gc573_rx_ced_poll(ctx.bar0, port, now, + c) != 0 || + !(c->valid[0] || c->valid[1] || + c->valid[2])) + continue; + have[port] = any = true; + if (c->count[0] || c->count[1] || + c->count[2]) + errors = true; + } + if (any && (errors || ++ced_quiet_ticks >= 10)) { + ced_quiet_ticks = 0; + printf("[HDMI] TMDS errors B/G/R"); + for (int port = 0; + port < GC573_RX_PORT_COUNT; port++) { + const struct gc573_rx_ced *c = + &ced[port]; + + if (!have[port]) { + printf(" | %s n/a", + gc573_rx_port_name(port)); + continue; + } + printf(" | %s %u/%u/%u in %u ms", + gc573_rx_port_name(port), + c->valid[0] ? c->count[0] : 0, + c->valid[1] ? c->count[1] : 0, + c->valid[2] ? c->count[2] : 0, + c->window_ms); + } + printf("%s\n", errors ? "" : " (clean)"); + } + } pthread_mutex_lock(&g_v4l2_lock); if (v4l2_sink.path[0]) { if (v4l2_sink.configured)