Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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).
- `streamer.log` lines carry the seconds since the streamer started (`[ 5.706] ...`), and the log is now written line by line instead of in 4 KB blocks, so the Control Hub sees status changes immediately. `EVERLIVE_LOG_TIMESTAMPS=0` turns the prefix off.

### Changed
- **Faster GC573 start-up:**
- The FPGA DDR sampler lock is polled instead of waited out with a fixed 2 s sleep (it locks after ~1.6 s on the test card).
- Link EQ settings that left a receiver error-free are saved in `~/.local/state/everlive/link-eq` and tried first on the next lock. One 100 ms error count confirms them, which skips the hardware auto-EQ (227 ms instead of ~730 ms on the input). Stale settings fall back to the full tune automatically.

## [1.4.0] - 2026-11-06

Expand Down
4 changes: 2 additions & 2 deletions tools/opengc573-userd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3)
# 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_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
STREAM_OBJS = gc573_user_log.o 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_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
HEADERS = gc573-user-compat.h gc573_user_log.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)

Expand Down
29 changes: 23 additions & 6 deletions tools/opengc573-userd/gc573_user_hw.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
#include "gc573_user_hw.h"
#include "gc573_user_i2c.h"

/* FPGA DDR sampler gate: budget per attempt and poll interval. */
#define DDR_LOCK_TIMEOUT_MS 2000
#define DDR_LOCK_POLL_MS 10
#define DDR_LOCK_STABLE_POLLS 5 /* 50 ms of continuous lock */

/*
* Software copy of the GPIO output latch (reg 0x00040).
*
Expand Down Expand Up @@ -158,12 +163,24 @@ int gc573_user_bridge_wait_for_ddr(void *bar0)
writel(0x8, base + GC573_REG_DDR_STATUS);
writel(0x1, base + GC573_REG_DDR_STATUS);

msleep(2000);
status = readl(base + GC573_REG_DDR_STATUS);
if (status & BIT(0)) {
pr_info("FPGA DDR sampler locked on attempt %d (status 0x%08x)",
attempt + 1, status);
return 0;
/*
* Poll instead of sleeping the whole window: the sampler
* normally locks long before the 2 s budget runs out. Only
* accept a lock that holds for several consecutive polls, so a
* transient bit right after the kick is not mistaken for it.
*/
int stable = 0;

for (int waited = DDR_LOCK_POLL_MS; waited <= DDR_LOCK_TIMEOUT_MS;
waited += DDR_LOCK_POLL_MS) {
msleep(DDR_LOCK_POLL_MS);
status = readl(base + GC573_REG_DDR_STATUS);
stable = (status & BIT(0)) ? stable + 1 : 0;
if (stable >= DDR_LOCK_STABLE_POLLS) {
pr_info("FPGA DDR sampler locked on attempt %d after %d ms (status 0x%08x)",
attempt + 1, waited, status);
return 0;
}
}
}

Expand Down
138 changes: 138 additions & 0 deletions tools/opengc573-userd/gc573_user_log.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* gc573_user_log.c — Line-buffered, timestamped stdout/stderr.
*
* The streamer's output usually goes to a file (streamer.log), where stdio
* would otherwise buffer it in 4 KB blocks: the Control Hub saw status lines
* late and the log carried no timing at all. stdout and stderr are replaced
* with fopencookie() streams that write straight to fd 1 / fd 2, adding the
* elapsed time at the start of every line. Output stays synchronous, so a
* crash loses no more than the line being written.
*/

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>

#include "gc573_user_log.h"

struct ts_stream {
int fd;
bool line_start;
};

static struct timespec g_t0;
static char g_start_time[32] = "";

static int write_all(int fd, struct iovec *iov, int cnt)
{
while (cnt > 0) {
ssize_t n = writev(fd, iov, cnt);

if (n < 0) {
if (errno == EINTR)
continue;
return -1;
}
while (cnt > 0 && (size_t)n >= iov->iov_len) {
n -= (ssize_t)iov->iov_len;
iov++;
cnt--;
}
if (cnt > 0) {
iov->iov_base = (char *)iov->iov_base + n;
iov->iov_len -= (size_t)n;
}
}
return 0;
}

static ssize_t ts_write(void *cookie, const char *buf, size_t len)
{
struct ts_stream *s = cookie;
size_t done = 0;

while (done < len) {
const char *nl = memchr(buf + done, '\n', len - done);
size_t chunk = nl ? (size_t)(nl - (buf + done)) + 1 : len - done;
struct iovec iov[2];
char prefix[24];
int cnt = 0;

if (s->line_start) {
struct timespec now;
int64_t ms;

clock_gettime(CLOCK_MONOTONIC, &now);
ms = (int64_t)(now.tv_sec - g_t0.tv_sec) * 1000 +
(now.tv_nsec - g_t0.tv_nsec) / 1000000;
iov[cnt].iov_base = prefix;
iov[cnt].iov_len = (size_t)snprintf(prefix, sizeof(prefix),
"[%4lld.%03lld] ", (long long)(ms / 1000),
(long long)(ms % 1000));
cnt++;
}
iov[cnt].iov_base = (void *)(buf + done);
iov[cnt].iov_len = chunk;
cnt++;

if (write_all(s->fd, iov, cnt) < 0)
return done ? (ssize_t)done : -1;
done += chunk;
s->line_start = nl != NULL;
}
return (ssize_t)len;
}

static FILE *ts_open(struct ts_stream *s)
{
cookie_io_functions_t io = { .write = ts_write };
FILE *f = fopencookie(s, "w", io);

if (f)
setvbuf(f, NULL, _IOLBF, 0);
return f;
}

void gc573_user_log_init(void)
{
static struct ts_stream out = { .fd = STDOUT_FILENO, .line_start = true };
static struct ts_stream err = { .fd = STDERR_FILENO, .line_start = true };
const char *env = getenv("EVERLIVE_LOG_TIMESTAMPS");
time_t wall = time(NULL);
struct tm tm;
FILE *f;

clock_gettime(CLOCK_MONOTONIC, &g_t0);
if (localtime_r(&wall, &tm))
strftime(g_start_time, sizeof(g_start_time),
"%Y-%m-%d %H:%M:%S", &tm);

fflush(stdout);
fflush(stderr);
if (env && strcmp(env, "0") == 0) {
setvbuf(stdout, NULL, _IOLBF, 0);
return;
}

f = ts_open(&out);
if (f)
stdout = f;
else
setvbuf(stdout, NULL, _IOLBF, 0);
f = ts_open(&err);
if (f)
stderr = f;
}

const char *gc573_user_log_start_time(void)
{
return g_start_time;
}
21 changes: 21 additions & 0 deletions tools/opengc573-userd/gc573_user_log.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* gc573_user_log.h — Line-buffered, timestamped stdout/stderr for the
* GC573 streamer, so streamer.log shows when each step happened.
*/

#ifndef GC573_USER_LOG_H
#define GC573_USER_LOG_H

/**
* gc573_user_log_init - Make stdout/stderr line-buffered and, unless
* EVERLIVE_LOG_TIMESTAMPS=0, prefix every line with the time since start
* as "[ 12.345] ". Call once from main(), after argument parsing
* (so --help stays plain) and before the startup banner.
*/
void gc573_user_log_init(void);

/** Wall-clock time gc573_user_log_init() ran, for the log banner. */
const char *gc573_user_log_start_time(void);

#endif /* GC573_USER_LOG_H */
60 changes: 56 additions & 4 deletions tools/opengc573-userd/gc573_user_rx_eq.c
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,25 @@ static int eq_force_zero_skew(void *bar0, const struct rx_port *p)
return p->set_bank(bar0, BANK0);
}

/* Force saved RS values and DFE taps (skew is left as it is). */
static int eq_apply_state(void *bar0, const struct rx_port *p,
const struct gc573_rx_eq_state *st)
{
for (unsigned int ch = 0; ch < CH_COUNT; ch++) {
TRY(wr(bar0, p, BANK3, (u8)(0x27 + ch), (u8)(0x80 | st->rs[ch])));
if (st->dfe)
for (unsigned int k = 0; k < 3; k++)
TRY(wr(bar0, p, BANK3, (u8)(0x4b + ch * 3 + k),
st->taps[ch][k]));
}
if (p->dfe) {
/* 0x4b[7] shares a register with ch0's first tap: set last. */
TRY(upd(bar0, p, BANK3, 0x4b, 0x80, st->dfe ? 0x80 : 0x00));
TRY(upd(bar0, p, BANK3, 0x22, 0x40, st->dfe ? 0x40 : 0x00));
}
return p->set_bank(bar0, BANK0);
}

static bool env_flag(const char *name)
{
const char *v = getenv(name);
Expand Down Expand Up @@ -522,6 +541,7 @@ static int eq_auto_and_apply(void *bar0, const struct rx_port *p,
}

int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port,
const struct gc573_rx_eq_state *saved,
struct gc573_rx_eq_result *res)
{
struct dfe_taps dfe[CH_COUNT][RS_LADDER_LEN];
Expand Down Expand Up @@ -559,7 +579,26 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port,
}
res->ran = true;

/* 2. Hardware auto-EQ (+ DFE where supported). */
/*
* 2. Settings that were clean on this port last time. Source and
* cable rarely change, so this usually skips the auto-EQ entirely.
*/
if (saved && saved->valid && (!saved->dfe || p->dfe)) {
TRY(eq_apply_state(bar0, p, saved));
TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced));
res->saved_tried = true;
res->saved_errors = ced_total(&ced);
if (ced_any_valid(&ced) && ced_total(&ced) == 0) {
res->used_saved = true;
res->ced_valid = true;
res->dfe_applied = saved->dfe;
res->state = *saved;
memcpy(best_rs, saved->rs, sizeof(best_rs));
goto finish;
}
}

/* 3. Hardware auto-EQ (+ DFE where supported). */
ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok);
if (ret < 0)
return ret;
Expand All @@ -574,7 +613,7 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port,
best_score[ch] = ced_score(&ced, ch);

/*
* 3. Errors remain: sweep RS per channel with skew forced to 0. The
* 4. 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)) {
Expand All @@ -598,7 +637,7 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port,
for (unsigned int ch = 0; ch < CH_COUNT; ch++)
sweep_total += best_score[ch];

/* 4. Retrain skew around the chosen RS values. */
/* 5. 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)
Expand Down Expand Up @@ -626,7 +665,20 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port,
}
}

/* 5. Done: power the EQ engine down and clear its IRQs. */
/* Remember what a clean link ended on, for the next lock. */
if (ced_any_valid(&ced) && ced_total(&ced) == 0) {
struct gc573_rx_eq_state *st = &res->state;

st->valid = true;
st->dfe = dfe_ok;
memcpy(st->rs, best_rs, sizeof(st->rs));
for (unsigned int ch = 0; ch < CH_COUNT; ch++)
memcpy(st->taps[ch], dfe[ch][rs_index(best_rs[ch])].tap,
sizeof(st->taps[ch]));
}

finish:
/* 6. 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));

Expand Down
25 changes: 22 additions & 3 deletions tools/opengc573-userd/gc573_user_rx_eq.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,23 @@ struct gc573_rx_ced {
u32 window_ms;
};

/*
* EQ settings that left a link error-free, so the next lock can try them
* before running the hardware auto-EQ. Only RS and the DFE taps are kept;
* a saved state is always verified by measurement before it is trusted.
*/
struct gc573_rx_eq_state {
bool valid;
bool dfe; /* manual DFE taps enabled */
u8 rs[GC573_RX_TMDS_CHANNELS];
u8 taps[GC573_RX_TMDS_CHANNELS][3]; /* DFE tap registers */
};

struct gc573_rx_eq_result {
bool ran; /* false: not HDMI 2.0, disabled, or clean */
bool used_saved; /* the saved settings were clean; no auto-EQ */
bool saved_tried; /* saved settings were applied and measured */
u32 saved_errors; /* CED with the saved settings */
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 */
Expand All @@ -46,6 +61,7 @@ struct gc573_rx_eq_result {
u32 errors_after; /* summed CED over the final window */
u32 ch_errors_after[GC573_RX_TMDS_CHANNELS];
u32 elapsed_ms;
struct gc573_rx_eq_state state; /* valid when the link ended clean */
};

const char *gc573_rx_port_name(enum gc573_rx_port port);
Expand Down Expand Up @@ -76,13 +92,16 @@ 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.
* tries @saved (may be NULL) and keeps it if it measures error-free; failing
* that, 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. @result->state
* receives the final settings when the link ends error-free.
* 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,
const struct gc573_rx_eq_state *saved,
struct gc573_rx_eq_result *result);

#endif /* GC573_USER_RX_EQ_H */
Loading
Loading