From 61c85f1fdc9d17f134a60a60c11d138afa0e7708 Mon Sep 17 00:00:00 2001 From: efox Date: Fri, 28 Aug 2026 20:04:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(atsc=5Fapp):=20=E5=A2=9E=E5=8A=A0ORx=20FSS?= =?UTF-8?q?I=E4=B8=8E=E5=AE=9E=E6=97=B6IQ=E6=B5=8B=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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训练后的实机验证。 --- src/atsc_app/bsp/eeprom.c | 4 +- src/atsc_app/bsp/orx_power.c | 27 +++++ src/atsc_app/inc/iq_rx.h | 10 +- src/atsc_app/inc/lcd_task.h | 6 + src/atsc_app/inc/orx_power.h | 20 ++++ src/atsc_app/task/http_get.c | 24 ++-- src/atsc_app/task/iq_rx.c | 143 ++++++++--------------- src/atsc_app/task/iq_vsb.c | 21 ++-- src/atsc_app/task/iweb_task.c | 5 +- src/atsc_app/task/lcd_task.c | 86 ++++++++------ src/atsc_app/tests/Makefile | 14 +++ src/atsc_app/tests/test_orx_power.c | 172 ++++++++++++++++++++++++++++ src/web/www/js/dashboard.js | 12 +- src/web/www/status_info.html | 8 +- src/web/www/sys_set.html | 8 +- src/web/www/sys_spectrum.html | 43 ++++--- 16 files changed, 422 insertions(+), 181 deletions(-) mode change 100755 => 100644 src/atsc_app/bsp/eeprom.c mode change 100755 => 100644 src/atsc_app/bsp/orx_power.c mode change 100755 => 100644 src/atsc_app/inc/iq_rx.h mode change 100755 => 100644 src/atsc_app/inc/lcd_task.h mode change 100755 => 100644 src/atsc_app/inc/orx_power.h mode change 100755 => 100644 src/atsc_app/task/http_get.c mode change 100755 => 100644 src/atsc_app/task/iq_rx.c mode change 100755 => 100644 src/atsc_app/task/iq_vsb.c mode change 100755 => 100644 src/atsc_app/task/iweb_task.c mode change 100755 => 100644 src/atsc_app/task/lcd_task.c create mode 100644 src/atsc_app/tests/Makefile create mode 100644 src/atsc_app/tests/test_orx_power.c mode change 100755 => 100644 src/web/www/js/dashboard.js mode change 100755 => 100644 src/web/www/status_info.html mode change 100755 => 100644 src/web/www/sys_set.html mode change 100755 => 100644 src/web/www/sys_spectrum.html diff --git a/src/atsc_app/bsp/eeprom.c b/src/atsc_app/bsp/eeprom.c old mode 100755 new mode 100644 index 6e1f9ca..c7a5b37 --- a/src/atsc_app/bsp/eeprom.c +++ b/src/atsc_app/bsp/eeprom.c @@ -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); //---------------------------- diff --git a/src/atsc_app/bsp/orx_power.c b/src/atsc_app/bsp/orx_power.c old mode 100755 new mode 100644 index 93547d4..f27b4f5 --- a/src/atsc_app/bsp/orx_power.c +++ b/src/atsc_app/bsp/orx_power.c @@ -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; diff --git a/src/atsc_app/inc/iq_rx.h b/src/atsc_app/inc/iq_rx.h old mode 100755 new mode 100644 index 1410980..f94260c --- a/src/atsc_app/inc/iq_rx.h +++ b/src/atsc_app/inc/iq_rx.h @@ -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 diff --git a/src/atsc_app/inc/lcd_task.h b/src/atsc_app/inc/lcd_task.h old mode 100755 new mode 100644 index 6261e82..3921eae --- a/src/atsc_app/inc/lcd_task.h +++ b/src/atsc_app/inc/lcd_task.h @@ -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 //客户手动备份 diff --git a/src/atsc_app/inc/orx_power.h b/src/atsc_app/inc/orx_power.h old mode 100755 new mode 100644 index 770211d..40d71ff --- a/src/atsc_app/inc/orx_power.h +++ b/src/atsc_app/inc/orx_power.h @@ -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); diff --git a/src/atsc_app/task/http_get.c b/src/atsc_app/task/http_get.c old mode 100755 new mode 100644 index dfbf25c..fc4eb93 --- a/src/atsc_app/task/http_get.c +++ b/src/atsc_app/task/http_get.c @@ -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; diff --git a/src/atsc_app/task/iq_rx.c b/src/atsc_app/task/iq_rx.c old mode 100755 new mode 100644 index f7ae504..c55c9ab --- a/src/atsc_app/task/iq_rx.c +++ b/src/atsc_app/task/iq_rx.c @@ -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 */ diff --git a/src/atsc_app/task/iq_vsb.c b/src/atsc_app/task/iq_vsb.c old mode 100755 new mode 100644 index 07fd399..e646ff9 --- a/src/atsc_app/task/iq_vsb.c +++ b/src/atsc_app/task/iq_vsb.c @@ -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; diff --git a/src/atsc_app/task/iweb_task.c b/src/atsc_app/task/iweb_task.c old mode 100755 new mode 100644 index 05fd1f1..8aaf9bc --- a/src/atsc_app/task/iweb_task.c +++ b/src/atsc_app/task/iweb_task.c @@ -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); diff --git a/src/atsc_app/task/lcd_task.c b/src/atsc_app/task/lcd_task.c old mode 100755 new mode 100644 index 0488987..f4a25a3 --- a/src/atsc_app/task/lcd_task.c +++ b/src/atsc_app/task/lcd_task.c @@ -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); diff --git a/src/atsc_app/tests/Makefile b/src/atsc_app/tests/Makefile new file mode 100644 index 0000000..4bc0249 --- /dev/null +++ b/src/atsc_app/tests/Makefile @@ -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 diff --git a/src/atsc_app/tests/test_orx_power.c b/src/atsc_app/tests/test_orx_power.c new file mode 100644 index 0000000..c00e576 --- /dev/null +++ b/src/atsc_app/tests/test_orx_power.c @@ -0,0 +1,172 @@ +/* 宿主机单元测试: orx_power 纯算法模块。 + * 注意: inc/type.h 自带 uint8_t/uint16_t typedef, 本文件禁止 include 。 */ +#include +#include +#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; +} diff --git a/src/web/www/js/dashboard.js b/src/web/www/js/dashboard.js old mode 100755 new mode 100644 index 15efdef..887720a --- a/src/web/www/js/dashboard.js +++ b/src/web/www/js/dashboard.js @@ -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. diff --git a/src/web/www/status_info.html b/src/web/www/status_info.html old mode 100755 new mode 100644 index 57b498a..387c23c --- a/src/web/www/status_info.html +++ b/src/web/www/status_info.html @@ -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"; diff --git a/src/web/www/sys_set.html b/src/web/www/sys_set.html old mode 100755 new mode 100644 index 5abfe96..82c12e2 --- a/src/web/www/sys_set.html +++ b/src/web/www/sys_set.html @@ -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() { - ADPC + ADPC