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:
Executable → Regular
+50
-93
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user