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
+2 -2
View File
@@ -2312,7 +2312,7 @@ static void e2rom_read_dpd_para(S_DPD_PARA *ppara)
//gl default val 0x0370
ppara->gl = c256_byteread_default(EEPROM_DPD_GAIN_LIMIT_H, 0x03)<<8 | c256_byteread_default(EEPROM_DPD_GAIN_LIMIT_L, 0x70);
ppara->dpd_ctl = c256_byteread_default(EEPROM_DPD_CTL_TST, 0x03);
ppara->dpd_ctl = c256_byteread_default(EEPROM_DPD_CTL_TST, 0x07);
}
@@ -3288,7 +3288,7 @@ void e2rom_load_default_param(unsigned char menu)
atsc_para.dpd_para.phase6_minute=3;//TM
atsc_para.dpd_para.wpp_th =3900;//6000
atsc_para.dpd_para.gl =0x0370;//0x0A00
atsc_para.dpd_para.dpd_ctl =3;
atsc_para.dpd_para.dpd_ctl =0x07;
//cttb_para.dpd_para.amp_sat_sqr=0;//该值由amp_sat转换而成
e2rom_save_dpd_para(&atsc_para.dpd_para);
//----------------------------
Executable → Regular
+27
View File
@@ -52,6 +52,33 @@ uint8 orx_fssi_from_gain_index(uint8 gi, uint8 gi_min, uint8 gi_max)
return (uint8)(pct + 0.5f);
}
uint8 orx_fssi_from_power(uint16 fagc_cw, uint16 rms, uint16 rms_ref,
uint8 gi_min, uint8 gi_max)
{
float span_db; /* 量程(dB): (gi_max-gi_min) 个 0.5 dB 档 */
float p_db; /* 相对设定点的 ORx 输入功率(dB) */
float pct;
if (gi_max <= gi_min)
return 0; /* 防除零的退化入参 */
if (rms_ref == 0)
rms_ref = 1; /* 防 log10(0); 调用方不应传 0 */
/* 粗档 0.5*cw_hi 与细档 10*log10(rms/rms_ref) 同向(见 orx_power_combine_db
* 的方向说明), 直接相加。rms==rms_ref 时细档为 0, 退化成纯 gain index 版。 */
p_db = orx_power_combine_db(fagc_cw, rms)
- 10.0f * log10f((float)rms_ref);
span_db = 0.5f * (float)(gi_max - gi_min);
pct = p_db * 100.0f / span_db;
if (pct <= 0.0f)
return 0;
if (pct >= 100.0f)
return 100;
return (uint8)(pct + 0.5f);
}
int16 orx_pwr_halfdb(uint16 rms)
{
float v = (float)rms;
Executable → Regular
+4 -6
View File
@@ -11,17 +11,15 @@ void iq_spec_set_enable(int enable);
/*
* Newest UIMD/LIMD measured on the imx6's own 10 MSPS ORx window.
*
* Same taps as the FPGA's imd_imd_fft_meas -- centre bin at +24.414 kHz and the two
* shoulder bins at +-3.1494 MHz -- but averaged over 11 bins (+-12.2 kHz) each and
* Welch-averaged over the whole 26.2 ms window. LIMD is always the shoulder on the
* pilot's side of the carrier; which one that is follows the measured pilot, so a
* spectral inversion cannot silently swap the two.
* PR19 uses three fixed 500 kHz measurement windows. P0 is the mean linear power
* from -0.25 to +0.25 MHz, PL is the mean from -3.5 to -3.0 MHz, and PR is the mean
* from +3.0 to +3.5 MHz. The window spectra are Welch-averaged over 26.2 ms.
*
* The FPGA's own registers cannot be used any more: its 20-bit |DFT|^2 output
* quantises the shoulder bins to 0 once the shoulder is better than ~45 dB, and
* lcd_task.c's `if (imd_l == 0) imd_l = 1` then reports a constant.
*
* limd_db/uimd_db are 10*log10(P_centre / P_shoulder), i.e. positive dB, no +11 dB
* limd_db/uimd_db are P0-PL and P0-PR respectively, i.e. positive dB, with no +11 dB
* display offset applied.
*
* Returns 1 and fills both pointers when a window newer than IQ_IMD_MAX_AGE seconds
Executable → Regular
+6
View File
@@ -154,6 +154,12 @@ typedef enum
#define FSSI_GI_MIN 195
#define FSSI_GI_MAX 255
/* PR16: FAGC 功率设定点, 用作 FSSI 细档(死区内 rms 残差)的零点。
* PR11 §2: rms_target = fagcpwrlevel*4; 当前 fagcpwrlevel=256 -> 1024。
* rms 恰好等于它时, 新算法与旧的纯 gain index 版读数完全相同(见 orx_power.h)。
* 若日后改了 fagcpwrlevel, 这里要同步; 更准的做法是按 PR11 §3 用标定点实测中位数。 */
#define FSSI_RMS_REF 1024
#define EEPROM_BACKUP_PARA_TYPE_NONE 0
#define EEPROM_BACKUP_PARA_TYPE_PREF 1 //工厂默认参数备份
#define EEPROM_BACKUP_PARA_TYPE_CUST 2 //客户手动备份
Executable → Regular
+20
View File
@@ -20,6 +20,26 @@ uint8 orx_fssi_hysteresis(uint8 prev, uint8 now);
* 线性映射,四舍五入,越界钳位。gi_min/gi_max 由调用方传入(标定后可收窄)。 */
uint8 orx_fssi_from_gain_index(uint8 gi, uint8 gi_min, uint8 gi_max);
/* PR16: 用「FAGC 增益字 + 死区内 rms 残差」合成的 FSSI%,替代只看 gain index 的版本。
*
* 为什么必须这样: dpd_rx_rfagc_1x.vhd 的 dz=232 是一个 2.00 dB 死区(4.6 个 gain
* step 宽), 注释原文 "the AGC only re-acquires when the ORx level genuinely moves
* more than +-1 dB"。所以只看 gain index 时, ±1 dB 以内的输入变化 gain index 一步
* 不动 —— A/B 两路差异小于 1 dB 时 b_fssi 会恒等于 a_fssi, 看着在刷新, 其实是在
* 复读 A。死区内增益冻结, 功率变化恰好完整落在 ob_fb_rms_pwr 上, 所以把 rms 残差
* 加回来就能看进死区。PR11 §4.2 早有明确要求(rms 残差必须保留浮点, 否则
* "丢失 FAGC 死区内的真实功率变化"), PR13 改用纯 gain index 时丢掉了这一半。
*
* fagc_cw : seg1 0x36/0x37 agcctrlword
* rms : seg1 0x03/0x04 ob_fb_rms_pwr
* rms_ref : FAGC 功率设定点(fagcpwrlevel*4; fagcpwrlevel=256 -> 1024)
* gi_min/gi_max: 与 orx_fssi_from_gain_index 同义, 仍决定量程
*
* 后向兼容: rms == rms_ref 时本函数与 orx_fssi_from_gain_index(255-cw_hi,...)
* 完全相等 —— 即 AGC 恰好收敛在设定点时读数不变, 只有死区内才产生新的分辨力。 */
uint8 orx_fssi_from_power(uint16 fagc_cw, uint16 rms, uint16 rms_ref,
uint8 gi_min, uint8 gi_max);
/* PR13: ORx 数字功率 rms -> 0.5dB 步数: round(10*log10(rms)/0.5) = round(20*log10(rms))。
* rms==0 按 1 处理(防 -inf), 返回 0。 */
int16 orx_pwr_halfdb(uint16 rms);
Executable → Regular
+16 -8
View File
@@ -113,8 +113,16 @@ static void http_load_psip(S_PSTS_PARA *ppara)
void http_load_status(S_CGI_STATUS *ppara)
{
int tmp_imd;
uint16 measured_tsnr;
uint8 measured_l_imd;
uint8 measured_u_imd;
PS_LOG_PARA appara=&atsc_para.log_para;
/* PR19 values are produced asynchronously by iq_rx. Read that source here
* instead of relying on log_para's once-per-second refresh, which is skipped
* while update_flag is set during a DPD update. */
get_tsnr_limd_uimd(&measured_tsnr, &measured_l_imd, &measured_u_imd);
ppara->input = appara->asi_input;
ppara->status= appara->input_ok_flag?0:1;
@@ -145,32 +153,32 @@ void http_load_status(S_CGI_STATUS *ppara)
//ppara->tsnr = atsc_para.log_para.tsnr;
if(atsc_para.wd_para.sa >= 0)
ppara->tsnr = atsc_para.log_para.tsnr+atsc_para.wd_para.sa;
ppara->tsnr = measured_tsnr+atsc_para.wd_para.sa;
else
ppara->tsnr = atsc_para.log_para.tsnr-(u8)(atsc_para.wd_para.sa*-1);
ppara->tsnr = measured_tsnr-(u8)(atsc_para.wd_para.sa*-1);
//ppara->l_imd = atsc_para.log_para.l_imd;
if(atsc_para.wd_para.sb >= 0)
ppara->l_imd = atsc_para.log_para.l_imd+atsc_para.wd_para.sb;
ppara->l_imd = measured_l_imd+atsc_para.wd_para.sb;
else{
tmp_imd = atsc_para.log_para.l_imd - (int)(-1*atsc_para.wd_para.sb);
tmp_imd = measured_l_imd - (int)(-1*atsc_para.wd_para.sb);
if(tmp_imd<0)
ppara->l_imd = 0;
else{
int8 tmpdata = -1*atsc_para.wd_para.sb;
ppara->l_imd = atsc_para.log_para.l_imd-(u8)(tmpdata);
ppara->l_imd = measured_l_imd-(u8)(tmpdata);
}
}
//ppara->u_imd = atsc_para.log_para.u_imd;
if(atsc_para.wd_para.sc >= 0)
ppara->u_imd = atsc_para.log_para.u_imd+atsc_para.wd_para.sc;
ppara->u_imd = measured_u_imd+atsc_para.wd_para.sc;
else{
tmp_imd = atsc_para.log_para.u_imd - (int)(-1*atsc_para.wd_para.sc);
tmp_imd = measured_u_imd - (int)(-1*atsc_para.wd_para.sc);
if(tmp_imd<0)
ppara->u_imd = 0;
else{
ppara->u_imd = atsc_para.log_para.u_imd-(u8)(-1*atsc_para.wd_para.sc);
ppara->u_imd = measured_u_imd-(u8)(-1*atsc_para.wd_para.sc);
}
}
ppara->syserr = atsc_para.log_para.syserr;
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 */
Executable → Regular
+13 -8
View File
@@ -140,7 +140,6 @@ static int iq_vsb_log_allowed(void)
int iq_get_snr(float *snr_db)
{
int fresh;
if (!iq_spec_get_enable()) return 0;
pthread_mutex_lock(&snr_mtx);
fresh = snr_stamp && (time(NULL) - snr_stamp) <= IQ_SNR_MAX_AGE;
if (fresh) *snr_db = snr_db_pub;
@@ -1023,17 +1022,23 @@ static float line_cancel(float *r, float *q, int n, int k0)
}
/*
* T-spaced 11-tap complex LS equalizer, decision-directed, retrained per
* window (22 real unknowns; normal equations + Gaussian elimination). After
* T-spaced 41-tap complex LS equalizer, decision-directed, retrained per
* window (82 real unknowns; normal equations + Gaussian elimination). After
* the line canceller the residual floor (~-39 dB) is even-lag LINEAR ISI
* (observation-path ripple); this recovers it to ~43 dB (PC, half/half
* held-out). Real ATSC receivers/analyzers measure MER post-equalizer.
* Trains on every 2nd symbol; overfit bias with 22 dof over 70k samples is
* < 0.001 dB. Output written to out[] (edges copied through), returns SNR.
* PR16 offline held-out sweep on four eth0 IQ windows:
* EQ11=42.30..43.36 dB, EQ21=42.45..43.53 dB, EQ41=42.84..44.02 dB.
* PR16 stride sweep showed that using every 32nd symbol costs only
* 0.09..0.17 dB on four held-out windows while reducing the Cortex-A7 normal
* matrix work by 16x. The resulting ~4374 training rows for 82 unknowns are
* still safely overdetermined. Output written to out[] (edges copied through),
* returns SNR.
*/
#define EQ_M 5
#define EQ_M 20
#define EQ_NT (2 * EQ_M + 1)
#define EQ_NU (2 * EQ_NT)
#define EQ_TRAIN_STRIDE 32
static float eq_snr(const float *cr, const float *cq, int n, float *out)
{
static double xtx[EQ_NU][EQ_NU], xty[EQ_NU];
@@ -1043,7 +1048,7 @@ static float eq_snr(const float *cr, const float *cq, int n, float *out)
if (n < 100 * EQ_NT) return -99.0f;
memset(xtx, 0, sizeof(xtx));
memset(xty, 0, sizeof(xty));
for (k = EQ_M; k < n - EQ_M; k += 2) { /* stride-2 training */
for (k = EQ_M; k < n - EQ_M; k += EQ_TRAIN_STRIDE) {
double v[EQ_NU], d = slice1(cr[k]);
for (j = 0; j < EQ_NT; j++) {
v[2 * j] = cr[k - (j - EQ_M)];
@@ -1145,7 +1150,7 @@ static void pr12_run(struct pr12_result *R, float tau_old)
}
gain_track(ci, nsym);
R->line_db = line_cancel(ci, cq, nsym, k_lo); /* narrowband +Rs/4 canceller */
{ float s_eq = eq_snr(ci, cq, nsym, lc_er); /* 11-tap DD equalizer -> lc_er */
{ float s_eq = eq_snr(ci, cq, nsym, lc_er); /* 41-tap DD equalizer -> lc_er */
const float *eqstat;
if (s_eq > -90.0f) {
snr = s_eq;
Executable → Regular
+3 -2
View File
@@ -770,7 +770,7 @@ int get_sys_cfg(char *pbuf,short bufMaxLen)
//printf("adpc=%d\n", atsc_para.system_para.adpc);
sprintf(pbuf, "#%u#%u#%u#%u#%u#%s#%u#%u#%u#%u#%u#0x%04X#%u#%u#%u#%u",
sprintf(pbuf, "#%u#%u#%u#%u#%u#%s#%u#%u#%u#%u#%u#0x%04X#%u#%u#%u#%u#%u",
input, // 通道选择
atsc_para.system_para.gps, // 时钟源
atsc_para.system_para.adpc>=ADPC_UPDATE3?ADPC_UPDATE3:atsc_para.system_para.adpc, //预校正
@@ -786,7 +786,8 @@ int get_sys_cfg(char *pbuf,short bufMaxLen)
atsc_para.sn_para.eas_key_valid,
atsc_para.system_para.agc_att,
atsc_para.system_para.adrv9009_att,
atsc_para.system_para.orx_manu_gain
atsc_para.system_para.orx_manu_gain,
atsc_para.dpd_para.dpd_ctl
);
{
size_t used = strlen(pbuf);
Executable → Regular
+48 -38
View File
@@ -829,6 +829,27 @@ static uint8 orx_gain_index_now(void)
return (uint8)(255 - ((orx_median5(cw) >> 8) & 0xff));
}
/* PR16: 取一次 FSSI% —— 增益字(粗) + ob_fb_rms_pwr(细), 两者各 5 点中位数。
*
* 只看 gain index 时 FAGC 的 2 dB 死区会把 ±1 dB 以内的输入变化整个吃掉
* (dpd_rx_rfagc_1x.vhd dz=232), A/B 差异小于 1 dB 时 b_fssi 会恒等于 a_fssi。
* 死区内增益冻结, 功率变化完整落在 rms 上, 所以把 rms 残差加回来。详见 orx_power.h。
*
* 读法照抄 eeprom.c 里已验证的那段: 两对寄存器交替读、各取中位数 —— 0x03/0x04
* 共用 temp_reg 高字节锁存器, 交替读才能拿到配对的一组。 */
static uint8 orx_fssi_now(void)
{
uint16 cw[5], rms[5];
int i;
for (i = 0; i < 5; i++) {
rms[i] = fpga_word_read_lh(0x01, 0x03, 0x04);
cw[i] = fpga_word_read_hl(0x01, 0x36, 0x37);
}
return orx_fssi_from_power(orx_median5(cw), orx_median5(rms),
FSSI_RMS_REF, FSSI_GI_MIN, FSSI_GI_MAX);
}
static void calc_fwd(void)
{
#if 0
@@ -925,11 +946,14 @@ void get_tsnr_limd_uimd(uint16 *ptsnr, uint8 *plmid, uint8 *pumid)
uint16 u16_tsnr;
#if 1
if(atsc_para.system_para.adpc_err==DPD_SAVE_DATA || atsc_para.system_para.adpc_err==DPD_LOAD_DATA)
return;
imd_l = last_l; imd_r = last_r; u16_tsnr = last_snr;
/* Always initialise outputs. IQ results are protected independently and
* remain valid while DPD save/load suppresses the normal log refresh. */
*plmid = (uint8)imd_l;
*pumid = (uint8)imd_r;
*ptsnr = u16_tsnr;
// The FPGA's imd_imd_fft_meas registers (0x05:0x17/0x18/0x19) used to be read here.
// They are gone: its 20-bit |DFT|^2 output quantises both shoulder bins to 0 once
// the shoulder is better than ~45 dB, and the old `if(imd_l==0) imd_l=1` clamp then
@@ -942,8 +966,8 @@ void get_tsnr_limd_uimd(uint16 *ptsnr, uint8 *plmid, uint8 *pumid)
// alternative either: with DPD bypassed, snr_txpwr/snr_errpwr reads 1.84 dB.
//
// UIMD/LIMD and SNR now all come from the imx6's own 10 MSPS ORx window:
// - LIMD/UIMD: iq_rx.c, same taps as the FPGA but band-averaged over 11 bins
// and Welch-averaged. LIMD is the shoulder on the pilot's side. Half-dB units,
// - LIMD/UIMD: iq_rx.c, fixed 500 kHz centre/left/right windows,
// Welch-averaged. LIMD is P0-PL and UIMD is P0-PR. Half-dB units,
// displayed without the historical +11 dB offset.
// - SNR: iq_vsb.c, the real 8VSB MER (I-path) of a demodulated window.
// If a source has not produced a fresh value we keep the previous one rather than
@@ -4219,14 +4243,13 @@ static void refresh_ad4(void *parg,void *parv)
if(atsc_para.system_para.ab_mode==1)
{
if(check_delay_over(&tickbak,50)==TRUE)
{ // 0x23 IQB_UPD: 逻辑A/B由 DPD_RF_SEL_VALUE() 映射到反向硬件位
{
if(++ppara->ab_sec>16)
{
ppara->ab_flag = ppara->ab_flag?FALSE:TRUE;
ppara->ab_sec = 0;
}
set_ab_switch(ppara->ab_flag);
}
}
if(ppara->ab_flag)
{
ppara->fssi = st_extend_state.b_fssi;
@@ -10578,10 +10601,9 @@ static BOOL proc_key_wc1(void *parg,uint8 key)
add_4_hex(&ppara->gl, p->item_index);
break;
case 4:
ppara->dpd_ctl+=0x0010;
break;
case 5:
ppara->dpd_ctl+=0x0001;
// PR16: production supports only disabled (0x00) or enabled (0x07).
ppara->dpd_ctl = (ppara->dpd_ctl == 0x07) ? 0x00 : 0x07;
break;
default:
break;
@@ -10601,10 +10623,8 @@ static BOOL proc_key_wc1(void *parg,uint8 key)
dec_4_hex(&ppara->gl, p->item_index);
break;
case 4:
ppara->dpd_ctl-=0x0010;
break;
case 5:
ppara->dpd_ctl-=0x0001;
ppara->dpd_ctl = (ppara->dpd_ctl == 0x07) ? 0x00 : 0x07;
break;
default:
break;
@@ -14534,23 +14554,10 @@ static void sec_proc_func(void)
static void update_fssi_state(void)
{
// 0x23 IQB_UPD: 逻辑A/B由 DPD_RF_SEL_VALUE() 映射到反向硬件位
uint8 a_fssi;
uint8 b_fssi;
uint8 data;
static uint16 sec_cnt=0;
static uint8 ab_flg =0;
static int last_ab_mode = -1;
int ab_mode = (atsc_para.system_para.ab_mode == 1);
uint8 just_switched = 0; /* PR16: 本秒刚拨过开关 -> 只拨不读, 留 1s 给 AGC 建立 */
/* FSSI Refresh alternates the ORx A/B path. IQ UDP windows can therefore
* contain both paths and must not be collected until Refresh is off.
* This edge check is intentionally before every early return below, so all
* configuration entry points take effect even while DPD is busy. */
if (ab_mode != last_ab_mode) {
iq_spec_set_enable(!ab_mode);
last_ab_mode = ab_mode;
}
/*
1. UPDATE and UPDLIN 返回
@@ -14584,39 +14591,42 @@ static void update_fssi_state(void)
ab_flg = 0;
else
ab_flg = 1;
just_switched = 1;
}
if(atsc_para.system_para.ab_mode==1){
if(ab_flg){
//B
set_ab_switch(E_SELECT_B);
{ /* 显示滞回: FAGC 一步 0.5dB = FSSI 2.5%, 无滞回会来回闪 */
/* PR16: 拨完开关这一秒不读。AGC 一个 gain step = 256 个积分周期
* x 51.2us ~= 13ms, 1s 足够建立; 不跳过的话这一次读到的还是 A 的增益字
* (原代码 A 分支那句被注释掉的 delay_ms(9) 就是想解决这个)。
* 每路驻留 10s, 只丢首采, 剩 9 次照常刷新。 */
if(!just_switched){
/* 显示滞回: 相邻档位间来回闪时保持前值 */
static uint8 b_prev = 0;
b_prev = orx_fssi_hysteresis(b_prev,
orx_fssi_from_gain_index(orx_gain_index_now(), FSSI_GI_MIN, FSSI_GI_MAX));
b_prev = orx_fssi_hysteresis(b_prev, orx_fssi_now());
st_extend_state.b_fssi = b_prev;
}
}
else{
//delay_ms(9);
//A
set_ab_switch(E_SELECT_A);
{
if(!just_switched){
static uint8 a_prev = 0;
a_prev = orx_fssi_hysteresis(a_prev,
orx_fssi_from_gain_index(orx_gain_index_now(), FSSI_GI_MIN, FSSI_GI_MAX));
a_prev = orx_fssi_hysteresis(a_prev, orx_fssi_now());
st_extend_state.a_fssi = a_prev;
}
}
//printf("[%s,%d] A_FSSI=%d B_FSSI=%d\n",__FUNCTION__,__LINE__,st_extend_state.a_fssi,st_extend_state.b_fssi);
}
else{
/* ab_mode!=1: 开关常驻 A, 不轮换, 也就没有建立时间问题 */
set_ab_switch(0);
//printf("AB switch A\n");
{
static uint8 a_prev2 = 0;
a_prev2 = orx_fssi_hysteresis(a_prev2,
orx_fssi_from_gain_index(orx_gain_index_now(), FSSI_GI_MIN, FSSI_GI_MAX));
a_prev2 = orx_fssi_hysteresis(a_prev2, orx_fssi_now());
st_extend_state.a_fssi = a_prev2;
}
//printf("[%s,%d] only A_FSSI=%d\n",__FUNCTION__,__LINE__,st_extend_state.a_fssi);
@@ -14810,7 +14820,7 @@ int sys_rf_agc_switch_proc(uint8 trigger)
//1 射频开关切换到A
u_dpdctl_r23.u8val = 0;
u_dpdctl_r23.dpd_bt.iqb_upd = 0;
u_dpdctl_r23.dpd_bt.rf_sel = DPD_RF_SEL_VALUE(E_SELECT_A);
u_dpdctl_r23.dpd_bt.rf_sel = DPD_RF_SEL_VALUE(E_SELECT_A);
u_dpdctl_r23.dpd_bt.sync_mode = 0;
fpga_byte_write(0x05, FPGA_ADDR_DPD_IQB_UPD, u_dpdctl_r23.u8val);
+14
View File
@@ -0,0 +1,14 @@
# 宿主机(x86)单元测试。不依赖交叉工具链、不依赖板子。
CC = gcc
CFLAGS = -Wall -Wextra -O0 -g -I../inc
LDLIBS = -lm
test_orx_power: test_orx_power.c ../bsp/orx_power.c
$(CC) $(CFLAGS) -o $@ test_orx_power.c ../bsp/orx_power.c $(LDLIBS)
.PHONY: check clean
check: test_orx_power
./test_orx_power
clean:
rm -f test_orx_power
+172
View File
@@ -0,0 +1,172 @@
/* 宿主机单元测试: orx_power 纯算法模块。
* 注意: inc/type.h 自带 uint8_t/uint16_t typedef, 本文件禁止 include <stdint.h>。 */
#include <stdio.h>
#include <math.h>
#include "type.h"
#include "orx_power.h"
static int g_fail = 0;
static void chk_f(const char *name, float got, float want, float tol)
{
if (fabsf(got - want) > tol) {
printf("FAIL %s: got %.4f want %.4f (tol %.4f)\n", name, got, want, tol);
g_fail++;
} else {
printf("ok %s: %.4f\n", name, got);
}
}
static void chk_u(const char *name, unsigned got, unsigned want)
{
if (got != want) {
printf("FAIL %s: got %u want %u\n", name, got, want);
g_fail++;
} else {
printf("ok %s: %u\n", name, got);
}
}
int main(void)
{
/* --- 合成: 真机实测工作点 cw=0x0f00, rms=992 --- */
/* 0.5*15 + 10*log10(992) = 7.5 + 29.9651 = 37.4651 */
chk_f("combine_operating_point",
orx_power_combine_db(0x0f00, 992), 37.4651f, 0.001f);
/* --- 拼接连续性: AGC 跨一步时 p_db 必须连续 ---
cw_hi 15->16 使粗档 +0.5dB, 同时增益降 0.5dB 使 rms *10^-0.05 = 992*0.89125 = 884 */
chk_f("splice_continuity",
orx_power_combine_db(0x1000, 884),
orx_power_combine_db(0x0f00, 992), 0.02f);
/* --- 单调性: 输入升高 -> p_db 升高 --- */
if (!(orx_power_combine_db(0x0f00, 1100) > orx_power_combine_db(0x0f00, 992))) {
printf("FAIL monotonic_rms\n"); g_fail++;
} else printf("ok monotonic_rms\n");
if (!(orx_power_combine_db(0x1000, 992) > orx_power_combine_db(0x0f00, 992))) {
printf("FAIL monotonic_cw\n"); g_fail++;
} else printf("ok monotonic_cw\n");
/* --- rms=0 不得产生 -inf --- */
if (!isfinite(orx_power_combine_db(0x0f00, 0))) {
printf("FAIL rms_zero_guard\n"); g_fail++;
} else printf("ok rms_zero_guard\n");
/* --- 中位数: 真机观测到的污染模式(2/5 为野值) --- */
{
uint16 s[5] = {992, 8417, 990, 991, 8414};
chk_u("median5_rejects_outliers", orx_median5(s), 992);
}
{
uint16 s2[5] = {990, 991, 992, 993, 994};
chk_u("median5_clean", orx_median5(s2), 992);
}
/* --- 滞回: 1 个百分点的抖动被吃掉, 2 个以上放行 --- */
chk_u("hyst_holds_1", orx_fssi_hysteresis(75, 76), 75);
chk_u("hyst_passes_2", orx_fssi_hysteresis(75, 77), 77);
chk_u("hyst_down_2", orx_fssi_hysteresis(75, 73), 73);
chk_u("hyst_same", orx_fssi_hysteresis(75, 75), 75);
/* --- PR13: FSSI 由 ORx gain index 线性映射 --- */
chk_u("fssi_gi_full_weak", orx_fssi_from_gain_index(255, 195, 255), 0);
chk_u("fssi_gi_full_strong", orx_fssi_from_gain_index(195, 195, 255), 100);
chk_u("fssi_gi_mid", orx_fssi_from_gain_index(225, 195, 255), 50);
chk_u("fssi_gi_quarter", orx_fssi_from_gain_index(240, 195, 255), 25);
chk_u("fssi_gi_round", orx_fssi_from_gain_index(224, 195, 255), 52);
chk_u("fssi_gi_clamp_below", orx_fssi_from_gain_index(190, 195, 255), 100);
chk_u("fssi_gi_narrow_range",orx_fssi_from_gain_index(245, 240, 250), 50);
chk_u("fssi_gi_degenerate", orx_fssi_from_gain_index(200, 220, 220), 0);
/* --- PR13: ORx 数字功率 rms -> 0.5dB 步数 --- */
chk_u("halfdb_992", (unsigned)orx_pwr_halfdb(992), 60);
chk_u("halfdb_1000",(unsigned)orx_pwr_halfdb(1000), 60);
chk_u("halfdb_884", (unsigned)orx_pwr_halfdb(884), 59);
chk_u("halfdb_1", (unsigned)orx_pwr_halfdb(1), 0);
chk_u("halfdb_0_guard", (unsigned)orx_pwr_halfdb(0), 0);
/* --- PR13: Δ(0.5dB 步数) -> FWD% --- */
chk_u("fwd_hdb_zero", orx_fwd_percent_hdb(0), 100);
chk_u("fwd_hdb_clamp_p6", orx_fwd_percent_hdb(6), 200);
chk_u("fwd_hdb_clamp_p10", orx_fwd_percent_hdb(10), 200);
chk_u("fwd_hdb_minus6", orx_fwd_percent_hdb(-6), 50);
chk_u("fwd_hdb_minus20", orx_fwd_percent_hdb(-20), 10);
chk_u("fwd_hdb_minus40", orx_fwd_percent_hdb(-40), 0);
chk_u("fwd_hdb_minus39", orx_fwd_percent_hdb(-39), 1);
/* --- PR13: 拼接连续性 ---
FAGC 涨一步(0.5dB)与 rms 相应降 10^(-0.05) 时, cw_hi+pwr_hdb 之和不变 */
chk_u("splice_continuity_hdb",
(unsigned)(1 + orx_pwr_halfdb(884)),
(unsigned)(0 + orx_pwr_halfdb(992)));
/* ================= PR16: 死区内可分辨的 FSSI ================= */
#define REF 1024u
#define GIMIN 195u
#define GIMAX 255u
/* 1) 后向兼容: rms 恰在设定点时, 必须与纯 gain index 版逐档相等。
* cw_hi -> gi = 255-cw_hi。量程 60 档, 每档 100/60 = 1.667%。 */
{
unsigned cw_hi;
int mismatch = 0;
for (cw_hi = 0; cw_hi <= 60; cw_hi++) {
unsigned a = orx_fssi_from_power((uint16)(cw_hi << 8), REF, REF, GIMIN, GIMAX);
unsigned b = orx_fssi_from_gain_index((uint8)(255 - cw_hi), GIMIN, GIMAX);
if (a != b) {
printf("FAIL compat cw_hi=%u: power=%u gain_index=%u\n", cw_hi, a, b);
mismatch++;
}
}
chk_u("pr16_compat_at_setpoint_mismatches", (unsigned)mismatch, 0);
}
/* 2) 核心诉求: gain index 冻结(cw 不变)时, rms 变化必须改变读数。
* 这正是旧算法做不到的 —— 2dB 死区内 A/B 差异会被整个吃掉。 */
{
unsigned lo = orx_fssi_from_power(0x1E00, 812, REF, GIMIN, GIMAX); /* -1.0dB */
unsigned mid= orx_fssi_from_power(0x1E00, REF, REF, GIMIN, GIMAX); /* 0.0dB */
unsigned hi = orx_fssi_from_power(0x1E00, 1290, REF, GIMIN, GIMAX); /* +1.0dB */
unsigned same = orx_fssi_from_gain_index((uint8)(255-0x1E), GIMIN, GIMAX);
printf(" pr16 deadzone sweep: lo=%u mid=%u hi=%u (old algo would be %u for all)\n",
lo, mid, hi, same);
if (!(lo < mid && mid < hi)) {
printf("FAIL pr16_deadzone_resolves: not strictly increasing\n");
g_fail++;
} else {
printf("ok pr16_deadzone_resolves\n");
}
/* 死区两端(±1dB)之间至少要拉开 1 个百分点, 否则滞回(阈值2)会把它抹平 */
if ((int)hi - (int)lo < 3) {
printf("FAIL pr16_deadzone_span: hi-lo=%d, too small vs hysteresis\n",
(int)hi - (int)lo);
g_fail++;
} else {
printf("ok pr16_deadzone_span: %d pts across the 2dB dead zone\n",
(int)hi - (int)lo);
}
}
/* 3) 单调性与钳位 */
{
unsigned c0 = orx_fssi_from_power(0x0000, REF, REF, GIMIN, GIMAX);
unsigned c1 = orx_fssi_from_power(0x3C00, REF, REF, GIMIN, GIMAX); /* cw_hi=60 */
chk_u("pr16_clamp_low", c0, 0);
chk_u("pr16_clamp_high", c1, 100);
chk_u("pr16_clamp_over", orx_fssi_from_power(0xFF00, REF, REF, GIMIN, GIMAX), 100);
chk_u("pr16_clamp_under_rms", orx_fssi_from_power(0x0000, 1, REF, GIMIN, GIMAX), 0);
}
/* 4) 退化入参不得崩 / 不得除零 */
{
chk_u("pr16_guard_gi_equal", orx_fssi_from_power(0x1E00, REF, REF, 200, 200), 0);
chk_u("pr16_guard_rms_zero",
orx_fssi_from_power(0x1E00, 0, REF, GIMIN, GIMAX) <= 100 ? 1u : 0u, 1u);
chk_u("pr16_guard_ref_zero",
orx_fssi_from_power(0x1E00, REF, 0, GIMIN, GIMAX) <= 100 ? 1u : 0u, 1u);
}
printf(g_fail ? "\n%d FAILED\n" : "\nALL PASSED\n", g_fail);
return g_fail ? 1 : 0;
}
Executable → Regular
+9 -3
View File
@@ -226,12 +226,18 @@
var freqVal = calcFrequency(chan);
// SNR: half-dB value from the app, displayed as the true measured SNR/MER.
var snr = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[11], function(n){ return (n / 2).toFixed(1); });
var rawSnr = Number(form[11]);
var snr = (ab_modenum === 1) ? "N/A-FSSI" :
(rawSnr <= 0 ? "N/A" : fmtNum(form[11], function(n){ return (n / 2).toFixed(1); }));
// LIMD / UIMD are half-dB values from the app. Apply the fixed +11 dB
// display offset used by Status and the LCD.
var limd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[12], function(n){ return (n / 2 + 11).toFixed(1); });
var uimd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[13], function(n){ return (n / 2 + 11).toFixed(1); });
var rawLimd = Number(form[12]);
var rawUimd = Number(form[13]);
var limd = (ab_modenum === 1) ? "N/A-FSSI" :
(rawLimd <= 0 ? "N/A" : fmtNum(form[12], function(n){ return (n / 2 + 11).toFixed(1); }));
var uimd = (ab_modenum === 1) ? "N/A-FSSI" :
(rawUimd <= 0 ? "N/A" : fmtNum(form[13], function(n){ return (n / 2 + 11).toFixed(1); }));
// The status interface already carries the shared A/B FSSI values.
// Spectrum consumes the same values instead of issuing another request.
Executable → Regular
+4 -4
View File
@@ -147,16 +147,16 @@ function SetFormDefaults() {
// +10 dB display offset; this is the true measured MER).
var tmpdata = Number(form[11]);
var snrStr = (tmpdata/2).toFixed(1);
cf.tsnr.value = (ab_modenum==1) ? "N/A-FSSI" : snrStr;
cf.tsnr.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata<=0 ? "N/A" : snrStr);
// IMD. Half-dB from the app, plus the fixed +11 dB display offset.
// (Do not Math.round() the sum and then paste on the half: for an odd
// half-dB value that rounds x.5 up and reports 1 dB too much.)
tmpdata = Number(form[12]);
cf.l_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1);
cf.l_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata<=0 ? "N/A" : (tmpdata/2 + 11).toFixed(1));
tmpdata = Number(form[13]);
cf.u_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1);
tmpdata = Number(form[13]);
cf.u_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata<=0 ? "N/A" : (tmpdata/2 + 11).toFixed(1));
// Control & Amp
cf.f1ctrl.value = (Number(form[14])==0) ? "Remote" : "Local";
Executable → Regular
+6 -2
View File
@@ -27,6 +27,7 @@ function finishFpgaUpgrade(){
var remot_local_ctrl = 0; // 0 remote 1 local
var g_old_adpc_status = 0;
var g_old_adpc = 0;
var ldpd_ctl = 0;
var g_key_tsoip_valid = 0;
var g_key_eas_valid = 0;
var g_old_ntp_ip1 = 0;
@@ -230,7 +231,10 @@ function SetFormDefaults(){
cf0.agc_att.value = Number(syspara[i++])*(-0.5);
cf0.adrv9009_att.value = syspara[i++];
cf0.orx_manu_gain.value = syspara[i++];
cf0.hardware_version.value = (syspara[i] === undefined || Number(syspara[i]) !== 0) ? 1 : 0;
ldpd_ctl = Number(syspara[i++]);
var adpc_label = document.getElementById('adpc_label');
if (adpc_label) adpc_label.style.color = (ldpd_ctl === 0x07) ? '' : '#d6a700';
cf0.hardware_version.value = (syspara[i] === undefined || Number(syspara[i]) !== 0) ? 1 : 0;
// sysnet form1
i = 1;
@@ -1514,7 +1518,7 @@ function set_timezone() {
</td>
</tr>
<tr>
<td>ADPC</td>
<td id="adpc_label">ADPC</td>
<td>
<select name="adp" id="adp">
<option value="0">Off
Executable → Regular
+28 -15
View File
@@ -87,7 +87,9 @@
var specFails = 0;
var Y_TOP = 30, Y_BOT = -80; // dB rel in-band flat density
var SH_LO = 3.5, SH_HI = 4.8; // shoulder band the app takes the median over
/* PR19 measurement windows: P0=-0.25..+0.25 MHz,
* PL=-3.5..-3.0 MHz and PR=+3.0..+3.5 MHz. */
var SH_LO = 3.0, SH_HI = 3.5;
var activeView = 'spectrum';
var fssiRefreshActive = false;
@@ -202,13 +204,8 @@
}
function updateReadouts(d){
/* R7 (2026-08-11) + R10 (2026-08-12): apply the documented 11 dB pilot offset
so these readouts match the dashboard's LIMD/UIMD and the 1.0 convention.
SIGN CONVENTION (R10, two-level hardware-verified): d.limd/d.uimd are POSITIVE
attenuation magnitudes -> offset is +11. d.shoulder is a SIGNED level below
in-band flat density (negative) -> offset is -11 (more suppression = more
negative). Verified: raw -22.7 -> -33.7 vs IMD 32.3/32.5; raw ~-63.4 -> -74.4
vs IMD 73.6/73.9. */
/* The PR19 values are positive P0-P{L,R} attenuation magnitudes. Apply the
established +11 dB display correction so these cards match Status/LCD. */
var IMD_PILOT_OFFSET = 11; // dB; documented pilot offset (1.0 status_info_new.html)
/* Threshold colouring for the three shoulder-suppression readouts, to match
the dashboard's scheme (owner, 2026-08-17). The dashboard colours SNR/LIMD/
@@ -223,7 +220,7 @@
function imdColour(v){ // dBc magnitude vs the 47 dBc pass line
return (v === null) ? '' : (Math.abs(v) >= 47 ? 'var(--accent-green)' : 'var(--status-err)');
}
var shoulderVal = setReadout('rd_shoulder', d.shoulder - IMD_PILOT_OFFSET, 1);
var shoulderVal = setReadout('rd_shoulder', d.shoulder + IMD_PILOT_OFFSET, 1);
var limdVal = setReadout('rd_limd', d.limd + IMD_PILOT_OFFSET, 1);
var uimdVal = setReadout('rd_uimd', d.uimd + IMD_PILOT_OFFSET, 1);
var shoulderEl = document.getElementById('rd_shoulder');
@@ -462,6 +459,10 @@
var x0 = xOf(m,Math.max(b[0],fmin),fmin,fmax), x1 = xOf(m,Math.min(b[1],fmax),fmin,fmax);
g.fillRect(x0, m.t, x1-x0, m.h-m.t-m.b);
});
g.fillStyle = 'rgba(163,230,53,0.08)'; // P0 window
g.fillRect(xOf(m,Math.max(-0.25,fmin),fmin,fmax), m.t,
xOf(m,Math.min(0.25,fmax),fmin,fmax)-xOf(m,Math.max(-0.25,fmin),fmin,fmax),
m.h-m.t-m.b);
g.lineWidth = 1;
g.font = '11px sans-serif';
@@ -511,12 +512,24 @@
trace(m, d.psd, d.f0, d.df, fmin, fmax, P.trace, 1.2);
var y = yOf(m, d.shoulder); // measured shoulder level
m.g.strokeStyle = P.marker; m.g.setLineDash([5,4]); m.g.lineWidth = 1;
m.g.beginPath(); m.g.moveTo(m.l,y); m.g.lineTo(m.w-m.r,y); m.g.stroke();
m.g.setLineDash([]);
m.g.fillStyle = P.marker; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
m.g.fillText('shoulder ' + (d.shoulder - 11).toFixed(1) + ' dBc', m.l+6, y-3);
/* PR19: show the two uncorrected P0-PL/P0-PR shoulder results. The
* dashboard's LIMD/UIMD adds the established +11 dB correction, so its
* value is exactly 11 dB above the matching label here. Quantise to the
* same 0.5 dB resolution used by the status CGI. */
function drawShoulder(fa, fb, level, value, label, align){
var y = yOf(m, Math.max(Math.min(level,Y_TOP),Y_BOT));
var x0 = xOf(m,fa,fmin,fmax), x1 = xOf(m,fb,fmin,fmax);
m.g.strokeStyle = P.marker; m.g.setLineDash([5,4]); m.g.lineWidth = 1;
m.g.beginPath(); m.g.moveTo(x0,y); m.g.lineTo(x1,y); m.g.stroke();
m.g.setLineDash([]);
m.g.fillStyle = P.marker; m.g.textAlign = align; m.g.textBaseline = 'bottom';
m.g.fillText(label + ' ' + (Math.round(value*2)/2).toFixed(1) + ' dB',
align === 'left' ? x0+3 : x1-3, y-3);
}
if (d.pl_db !== undefined && d.limd !== undefined)
drawShoulder(-3.5,-3.0,d.pl_db,d.limd,'L shoulder','left');
if (d.pr_db !== undefined && d.uimd !== undefined)
drawShoulder(3.0,3.5,d.pr_db,d.uimd,'R shoulder','right');
/* PR12: pilot vertical marker removed on request (it covered the pilot
* peak). The pilot frequency is still available in d.pilot. */