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
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+46
-36
@@ -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,13 +4243,12 @@ 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)
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
+3
-3
@@ -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);
|
||||
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
+5
-1
@@ -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,6 +231,9 @@ function SetFormDefaults(){
|
||||
cf0.agc_att.value = Number(syspara[i++])*(-0.5);
|
||||
cf0.adrv9009_att.value = syspara[i++];
|
||||
cf0.orx_manu_gain.value = syspara[i++];
|
||||
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
|
||||
@@ -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
+26
-13
@@ -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
|
||||
/* 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(m.l,y); m.g.lineTo(m.w-m.r,y); m.g.stroke();
|
||||
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 = 'left'; m.g.textBaseline = 'bottom';
|
||||
m.g.fillText('shoulder ' + (d.shoulder - 11).toFixed(1) + ' dBc', m.l+6, y-3);
|
||||
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. */
|
||||
|
||||
Reference in New Issue
Block a user