feat(atsc_app): 增加ORx FSSI与实时IQ测量

V1.1平台的FAGC粗调死区内无法反映功率变化,且频谱页面关闭或DPD
更新期间,状态页面的SNR和左右带肩可能停止更新。

组合FAGC控制字和RMS残差计算FSSI,并保留A/B切换防抖;采用固定
500 kHz窗口实时计算SNR、LIMD和UIMD,后台测量不再依赖频谱页面。
同步网页无效值、A/B模式和左右带肩显示,并将DPD控制默认值设为
0x07,同时保持HardwareVersion参数解析顺序兼容。

验证:make -C src/atsc_app/tests check,全部通过。
验证:加载Yocto环境后执行make -C src/atsc_app -j2,编译通过。
未执行V1.1板端射频、AVQ及DPD训练后的实机验证。
This commit is contained in:
efox
2026-08-28 20:04:33 +08:00
parent 94a6772782
commit 61c85f1fdc
16 changed files with 422 additions and 181 deletions
Executable → Regular
+50 -93
View File
@@ -46,24 +46,23 @@
#define IQ_WBYTES (IQ_NPKT * IQ_PAY) /* 1 MiB per window */
/* Welch over the whole window. The FFT is 4096 points because at 10 MSPS that is
* a 2441.40625 Hz bin -- bit for bit the same bin the FPGA's imd_imd_fft_meas uses
* at 40 MHz/16384. So bins 10 / +1290 / -1290 land exactly on its cent/L/R taps and
* UIMD/LIMD can be recomputed here without redefining anything. The published trace
* is folded down to 1024 points (4 bins each) to keep the JSON small. */
* a 2441.40625 Hz bin. The published trace is folded down to 1024 points (4 bins
* each) to keep the JSON small. */
#define IQ_FFT 4096
#define IQ_SEGPKT (IQ_FFT / IQ_NF) /* 16 packets per segment */
#define IQ_NSEG (IQ_NPKT / IQ_SEGPKT)
#define IQ_OUT 1024 /* published trace points */
#define IQ_FOLD (IQ_FFT / IQ_OUT)
/* imd_imd_fft_meas taps: cent = +24.414 kHz, L = +3.1494 MHz, R = -3.1494 MHz.
* HALF widens each tap to 11 bins (+-12.2 kHz): a single 2.4 kHz bin of a data
* signal is chi-square with 2 dof, i.e. +-8 dB of scatter, and the -3.1494 MHz bin
* additionally sits on a narrow feature ~5 dB above its neighbours. */
#define IMD_CENT 10
#define IMD_L 1290
#define IMD_R (IQ_FFT - IMD_L)
#define IMD_HALF 5
/* PR19 shoulder measurement windows, in MHz relative to the channel centre.
* Average linear power in each 500 kHz window before converting the ratios to dB. */
#define IMD_CENT_LO_MHZ (-0.25f)
#define IMD_CENT_HI_MHZ ( 0.25f)
#define IMD_LEFT_LO_MHZ (-3.50f)
#define IMD_LEFT_HI_MHZ (-3.00f)
#define IMD_RIGHT_LO_MHZ ( 3.00f)
#define IMD_RIGHT_HI_MHZ ( 3.50f)
#define IMD_BW_KHZ 500.0f
#define IQ_JSON "/tmp/iq_spectrum.json"
#define IQ_JSON_TMP "/tmp/iq_spectrum.json.tmp"
@@ -128,12 +127,6 @@ static int longest_run(const unsigned char *seen)
return best;
}
static int cmp_float(const void *a, const void *b)
{
float x = *(const float *)a, y = *(const float *)b;
return (x < y) ? -1 : (x > y);
}
/* ---- newest UIMD/LIMD, published to lcd_task.c's get_tsnr_limd_uimd() ---- */
static pthread_mutex_t imd_mtx = PTHREAD_MUTEX_INITIALIZER;
static float imd_l_db, imd_u_db;
@@ -142,7 +135,6 @@ static time_t imd_stamp; /* 0 = nothing measured yet */
int iq_get_imd(float *limd_db, float *uimd_db)
{
int fresh;
if (!iq_spec_get_enable()) return 0;
pthread_mutex_lock(&imd_mtx);
fresh = imd_stamp && (time(NULL) - imd_stamp) <= IQ_IMD_MAX_AGE;
if (fresh) { *limd_db = imd_l_db; *uimd_db = imd_u_db; }
@@ -169,19 +161,8 @@ void iq_spec_set_enable(int enable)
iq_spec_enable = enable;
if (!enable) {
pthread_mutex_lock(&iq_mtx);
iq_ready = -1;
pthread_mutex_unlock(&iq_mtx);
pthread_mutex_lock(&vsb_mtx);
vsb_pending = 0;
pthread_mutex_unlock(&vsb_mtx);
pthread_mutex_lock(&imd_mtx);
imd_stamp = 0;
pthread_mutex_unlock(&imd_mtx);
iq_vsb_clear_results();
/* This switch controls spectrum-page files only. Status-page SNR and
* shoulders must continue to be measured in the background. */
unlink(IQ_JSON);
unlink(IQ_JSON_TMP);
unlink("/tmp/iq_eye.json");
@@ -203,11 +184,9 @@ static void *iq_vsb_thr(void *arg)
while (!vsb_pending) pthread_cond_wait(&vsb_cond, &vsb_mtx);
pthread_mutex_unlock(&vsb_mtx);
if (!iq_spec_get_enable()) continue;
iq_vsb_analyse(vsb_win, vsb_wid);
if (!iq_spec_get_enable()) {
iq_vsb_clear_results();
unlink("/tmp/iq_eye.json");
}
@@ -218,13 +197,21 @@ static void *iq_vsb_thr(void *arg)
return 0;
}
/* mean power of the 2*half+1 bins centred on k, wrapping the FFT index */
static float band_power(const float *psd, int k, int half)
/* Mean linear power of all FFT bins inside the inclusive signed-frequency range. */
static float band_power_range(const float *psd, float f_lo_mhz, float f_hi_mhz)
{
float acc = 0.0f;
int d;
for (d = -half; d <= half; d++) acc += psd[(k + d) & (IQ_FFT - 1)];
return acc / (2 * half + 1);
const float df = IQ_FS_MHZ / IQ_FFT;
int i, count = 0;
for (i = 0; i < IQ_FFT; i++) {
float f = (i < IQ_FFT / 2 ? i : i - IQ_FFT) * df;
if (f >= f_lo_mhz && f <= f_hi_mhz) {
acc += psd[i];
count++;
}
}
return count ? acc / count : 0.0f;
}
/*
@@ -258,13 +245,6 @@ static void *iq_spec_thr(void *arg)
buf = iq_ready; npkt = iq_ready_pkts; run = iq_ready_run; wid = iq_ready_wid;
pthread_mutex_unlock(&iq_mtx);
if (!iq_spec_get_enable()) {
pthread_mutex_lock(&iq_mtx);
iq_ready = -1;
pthread_mutex_unlock(&iq_mtx);
continue;
}
for (i = 0; i < IQ_FFT; i++) psd[i] = 0.0f;
nseg = 0;
for (s = 0; s < IQ_NSEG; s++) {
@@ -290,19 +270,12 @@ static void *iq_spec_thr(void *arg)
}
if (nseg == 0) { pthread_mutex_lock(&iq_mtx); iq_ready = -1;
pthread_mutex_unlock(&iq_mtx); continue; }
if (!iq_spec_get_enable()) {
pthread_mutex_lock(&iq_mtx);
iq_ready = -1;
pthread_mutex_unlock(&iq_mtx);
continue;
}
{
const float df = IQ_FS_MHZ / IQ_FFT; /* 2.44140625 kHz, == FPGA's 40MHz/16384 */
const float dfo = df * IQ_FOLD; /* published bin */
float flat = 0.0f, pk = 0.0f, pilot = 0.0f, shoulder = 0.0f;
float p_cent, p_hi, p_lo, limd, uimd;
int nflat = 0, nsh = 0, pkbin = 0, pos = 0, j;
float flat = 0.0f, pk = 0.0f, pilot = 0.0f;
float p_cent, p_hi, p_lo, p0_db, pl_db, pr_db, limd, uimd;
int nflat = 0, pkbin = 0, pos = 0, j;
for (i = 0; i < IQ_FFT; i++) {
float f = (i < IQ_FFT / 2 ? i : i - IQ_FFT) * df;
@@ -313,16 +286,17 @@ static void *iq_spec_thr(void *arg)
flat = (nflat ? flat / nflat : 1e-12f) + 1e-12f;
pilot = (pkbin < IQ_FFT / 2 ? pkbin : pkbin - IQ_FFT) * df;
/* UIMD/LIMD on imd_imd_fft_meas's own taps, band-averaged.
* LIMD is by definition the shoulder on the pilot's side of the carrier,
* so key it off the measured pilot rather than off a hard-coded sign: an
* inverted ORx spectrum then swaps the taps instead of the labels. */
p_cent = band_power(psd, IMD_CENT, IMD_HALF);
p_hi = band_power(psd, IMD_L, IMD_HALF); /* +3.1494 MHz */
p_lo = band_power(psd, IMD_R, IMD_HALF); /* -3.1494 MHz */
if (pilot > 0) { float t = p_hi; p_hi = p_lo; p_lo = t; }
/* PR19: P0 is the mean power in the centre 500 kHz; PL and PR are
* the mean powers in -3.5..-3.0 MHz and +3.0..+3.5 MHz. The reported
* left/right shoulders are P0-PL and P0-PR respectively. */
p_cent = band_power_range(psd, IMD_CENT_LO_MHZ, IMD_CENT_HI_MHZ);
p_lo = band_power_range(psd, IMD_LEFT_LO_MHZ, IMD_LEFT_HI_MHZ);
p_hi = band_power_range(psd, IMD_RIGHT_LO_MHZ, IMD_RIGHT_HI_MHZ);
limd = 10.0f * log10f(p_cent / (p_lo + 1e-12f) + 1e-12f);
uimd = 10.0f * log10f(p_cent / (p_hi + 1e-12f) + 1e-12f);
p0_db = 10.0f * log10f(p_cent / flat + 1e-12f);
pl_db = 10.0f * log10f(p_lo / flat + 1e-12f);
pr_db = 10.0f * log10f(p_hi / flat + 1e-12f);
pthread_mutex_lock(&imd_mtx);
imd_l_db = limd; imd_u_db = uimd; imd_stamp = time(NULL);
@@ -337,25 +311,18 @@ static void *iq_spec_thr(void *arg)
}
for (j = 0; j < IQ_OUT; j++) psd[j] = sh[j];
for (j = 0; j < IQ_OUT; j++) {
float f = (j < IQ_OUT / 2 ? j : j - IQ_OUT) * dfo;
float a = f < 0 ? -f : f;
if (a >= 3.5f && a <= 4.8f) sh[nsh++] = psd[j]; /* shoulder, clear of Nyquist */
}
if (nsh) {
qsort(sh, nsh, sizeof(float), cmp_float);
shoulder = sh[nsh / 2];
}
pos = snprintf(json, sizeof(json),
"{\"wid\":%d,\"pkts\":%d,\"npkt\":%d,\"run\":%d,\"ms\":%.2f,"
"\"nseg\":%d,\"fs\":%.3f,\"n\":%d,\"f0\":%.6f,\"df\":%.6f,"
"\"rbw_khz\":%.2f,\"pilot\":%.4f,\"peak\":%.2f,\"shoulder\":%.2f,"
"\"limd\":%.2f,\"uimd\":%.2f,\"imd_rbw_khz\":%.3f,\"psd\":[",
"\"rbw_khz\":%.2f,\"pilot\":%.4f,\"peak\":%.2f,"
"\"shoulder\":%.2f,\"lshoulder\":%.2f,\"rshoulder\":%.2f,"
"\"limd\":%.2f,\"uimd\":%.2f,\"p0_db\":%.2f,\"pl_db\":%.2f,\"pr_db\":%.2f,"
"\"imd_rbw_khz\":%.3f,\"psd\":[",
wid, npkt, IQ_NPKT, run, run * IQ_NF / (IQ_FS_MHZ * 1e3f),
nseg, IQ_FS_MHZ, IQ_OUT, -IQ_FS_MHZ / 2.0f + 0.5f * (IQ_FOLD - 1) * df, dfo,
dfo * 1000.0f, pilot, 10.0f * log10f(pk / flat + 1e-12f), shoulder,
limd, uimd, df * 1000.0f * (2 * IMD_HALF + 1));
dfo * 1000.0f, pilot, 10.0f * log10f(pk / flat + 1e-12f),
limd < uimd ? limd : uimd, limd, uimd, limd, uimd,
p0_db, pl_db, pr_db, IMD_BW_KHZ);
/* emit fftshift-ed, i.e. ascending frequency from f0 */
for (j = 0; j < IQ_OUT && pos < (int)sizeof(json) - 16; j++) {
int k = (j + IQ_OUT / 2) & (IQ_OUT - 1);
@@ -364,7 +331,8 @@ static void *iq_spec_thr(void *arg)
}
pos += snprintf(json + pos, sizeof(json) - pos, "]}\n");
{ FILE *f = fopen(IQ_JSON_TMP, "w"); /* write+rename: the CGI never sees a partial file */
if (iq_spec_get_enable()) {
FILE *f = fopen(IQ_JSON_TMP, "w"); /* write+rename: the CGI never sees a partial file */
if (f) { fwrite(json, 1, pos, f); fclose(f); rename(IQ_JSON_TMP, IQ_JSON); }
}
}
@@ -388,7 +356,7 @@ static void *iq_spec_thr(void *arg)
}
}
#endif
if (npkt == IQ_NPKT && ++nraw >= IQ_RAW_EVERY) {
if (iq_spec_get_enable() && npkt == IQ_NPKT && ++nraw >= IQ_RAW_EVERY) {
FILE *r = fopen("/tmp/iq_raw.bin", "wb"); /* whole contiguous window, for PC-side analysis */
if (r) { fwrite(iq_win[buf], 1, IQ_WBYTES, r); fclose(r); }
nraw = 0;
@@ -416,7 +384,7 @@ static void *iq_spec_thr(void *arg)
void *iq_rx_thr(void *arg)
{
int fd, n, cur = -1, buf = 0, npkt = 0, active = 0;
int fd, n, cur = -1, buf = 0, npkt = 0;
struct sockaddr_in a;
unsigned char pkt[2048];
pthread_t spec_id;
@@ -427,7 +395,7 @@ void *iq_rx_thr(void *arg)
printf("[sched] iq_rx_thr nice 5 failed: %s\n", strerror(errno));
#endif
/* PR06 step6: the IQ frames are now injected by the FPGA into eth0 (management port, RMII2)
* and addressed to 192.168.1.143 -- eth0's OWN IP -- so the kernel hands them straight to
* and addressed to limited broadcast on eth0, so the kernel hands them straight to
* this UDP socket. This removes the old dependency on the external eth1 cable: eth0 is always
* connected while you observe the spectrum via the web, so the internal FPGA->imx6 IQ transfer
* no longer needs eth1's PHY link to be up. (busybox has no bare `ip`; absolute path required.) */
@@ -468,17 +436,6 @@ void *iq_rx_thr(void *arg)
memset(iq_seen[0], 0, IQ_NPKT);
for (;;) {
int wid, idx;
if (!iq_spec_get_enable()) {
active = 0;
cur = -1; npkt = 0;
memset(iq_seen[buf], 0, IQ_NPKT);
sleep(1);
continue;
}
if (!active) {
while (recvfrom(fd, pkt, sizeof(pkt), MSG_DONTWAIT, NULL, NULL) > 0) {}
active = 1;
}
n = recvfrom(fd, pkt, sizeof(pkt), 0, NULL, NULL);
if (n < 8 + IQ_PAY) continue;
if (pkt[0] != 0x49 || pkt[1] != 0x51 || pkt[6] != 0x02) continue; /* 'IQ', fmt=2 */