From 94a6772782c076e37a7bf24afe1fec5ab32357c6 Mon Sep 17 00:00:00 2001 From: rfeng Date: Fri, 28 Aug 2026 13:31:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=B0=84=E9=A2=91=E6=81=A2=E5=A4=8D=E4=B8=8E=E6=A0=A1=E5=87=86?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PR17_DPD结束后恢复ORx_FAGC实时跟踪方案.md | 108 ++++++ .../tar_nopower_off_for_rf_recover.md | 341 ++++++++++++++++++ .../tools/measure_fullband_chpower_once.py | 142 ++++++++ .../CHANGELOG_20260729_dashboard_spectrum.md | 37 ++ 4 files changed, 628 insertions(+) create mode 100644 doc/modify_guide_260825/PR17_DPD结束后恢复ORx_FAGC实时跟踪方案.md create mode 100644 src/atsc_app/tar_nopower_off_for_rf_recover.md create mode 100755 src/atsc_app/tools/measure_fullband_chpower_once.py create mode 100644 src/web/CHANGELOG_20260729_dashboard_spectrum.md diff --git a/doc/modify_guide_260825/PR17_DPD结束后恢复ORx_FAGC实时跟踪方案.md b/doc/modify_guide_260825/PR17_DPD结束后恢复ORx_FAGC实时跟踪方案.md new file mode 100644 index 0000000..b00fb68 --- /dev/null +++ b/doc/modify_guide_260825/PR17_DPD结束后恢复ORx_FAGC实时跟踪方案.md @@ -0,0 +1,108 @@ +# PR17:DPD结束后恢复ORx FAGC实时跟踪 + +## 1. 背景 + +当前冷启动流程会调用 `boot_cfg_fagc_update()`,向 FPGA 的 +`FPGA_ADDR_DPD_CTRL3` 写入 `DPD_CTRL3_FAGC_UPDATE`,因此在未执行 +DPD Update 时,ORx FAGC保持实时跟踪。 + +执行DPD Update后,FAGC在训练阶段工作一段时间,然后被锁定。DPD完整结束后, +当前流程没有恢复冷启动时的FAGC工作状态,导致后续ORx反馈射频电平发生较大变化时, +FAGC不能再次收敛。 + +## 2. 需求 + +DPD Update完整结束后,恢复与设备冷启动后相同的ORx FAGC实时跟踪状态。 + +要求: + +- 只修改i.MX6应用代码。 +- 不修改FPGA RTL。 +- 不修改ORx DAGC逻辑。 +- 不改变DPD训练期间现有的FAGC启停和锁定时序。 +- 不触发新的DPD训练,不清除已经生成的DPD系数。 + +## 3. 修改方案 + +目标文件: + +```text +imx6/src/atsc_app/bsp/dpd_proc.c +``` + +复用现有函数: + +```c +boot_cfg_fagc_update(); +``` + +该函数定义于: + +```text +imx6/src/atsc_app/bsp/fpga.c +``` + +如果 `dpd_proc.c` 中没有函数声明,则增加: + +```c +extern void boot_cfg_fagc_update(void); +``` + +在DPD Update完整成功结束、即将退出更新状态机的位置,调用: + +```c +/* 恢复冷启动后的ORx FAGC实时跟踪状态。 */ +boot_cfg_fagc_update(); + +log_printf(LOG_INFO, I_USER_CFG, + "[DPD] update complete, ORx FAGC tracking restored\n"); +``` + +如果该完整结束出口当前执行以下最终清零: + +```c +fpga_byte_write(0x05, FPGA_ADDR_DPD_CTRL3, 0); +``` + +则将这一次最终清零替换为 `boot_cfg_fagc_update()`。 + +必须保留phase 1、phase 7以及其他DPD训练阶段内部用于锁定FAGC的 +`FPGA_ADDR_DPD_CTRL3 = 0` 操作。这些操作属于现有DPD收敛流程,不能替换。 + +如果工程中存在多套DPD Update状态机,应分别检查它们的完整成功结束出口,确保最终都恢复FAGC。 + +## 4. 修改后的工作状态 + +1. 冷启动、不执行DPD Update:行为不变,FAGC持续实时跟踪。 +2. DPD训练期间:行为不变,FAGC先收敛,再按现有流程锁定。 +3. DPD Update完整结束:重新置位 `DPD_CTRL3_FAGC_UPDATE`。 +4. 后续ORx反馈电平超出RTL现有死区时,FAGC再次调整ADRV9009 ORx增益。 +5. DAGC控制方式和已完成的DPD系数保持不变。 + +## 5. 注意事项 + +- `boot_cfg_fagc_update()` 只有在ADRV9009 SPI控制权属于FPGA时才会写入;DPD结束时应确认SPI控制权已经交还FPGA。 +- 不增加 `DPD_CTRL3_FAGC_SLOWDN`,确保恢复状态与当前冷启动后的状态完全一致。 +- 不触发 `DPD_START`。 +- 只修改整个DPD Update的最终出口,不修改phase之间的临时控制操作。 + +## 6. 验证步骤 + +1. 冷启动设备,不执行DPD Update,记录FAGC控制字或ORx `gain_index`的工作状态。 +2. 执行一次完整DPD Update。 +3. 确认DPD结束后日志出现: + + ```text + [DPD] update complete, ORx FAGC tracking restored + ``` + +4. DPD结束后改变反馈链路衰减,使ORx输入电平变化超过现有约±1 dB死区。 +5. 确认FAGC控制字及ADRV9009 ORx `gain_index`重新变化并最终稳定。 +6. 确认DPD系数没有被清除或重新训练,发射输出保持正常。 + +## 7. 验收标准 + +- DPD Update结束后,FAGC工作状态与冷启动且未执行DPD时一致。 +- 反馈射频电平明显变化后,FAGC能够重新收敛。 +- DPD训练阶段原有的FAGC锁定逻辑不变。 +- 不涉及RTL和DAGC修改。 diff --git a/src/atsc_app/tar_nopower_off_for_rf_recover.md b/src/atsc_app/tar_nopower_off_for_rf_recover.md new file mode 100644 index 0000000..f36986c --- /dev/null +++ b/src/atsc_app/tar_nopower_off_for_rf_recover.md @@ -0,0 +1,341 @@ +# 不掉电恢复 ADRV9009 / JESD / 射频链路流程 + +## 1. 目的与适用范围 + +本文整理设备在不切断整机电源的情况下,完成以下操作后恢复射频输出的处理链路: + +- IMX 应用程序升级后,结束旧进程并启动新进程; +- IMX 应用程序异常退出后,由脚本或守护进程重新启动; +- FPGA 在线升级并通过 IPROG 重新加载 bitstream; +- FPGA 已重新加载,但 ADRV9009 和 AD9528 仍保持上电运行。 + +这里的核心问题是:ADRV9009 已经处于运行态时,再次执行 `TALISE_resetDevice()`、`TALISE_initialize()` 和初始化校准,可能返回 `err10 / TALACT_ERR_RESET_FULL`,从而造成 JESD 或 TX 通道恢复失败。因此,不掉电恢复不能简单复用完整冷启动流程,必须区分“9009 仍存活”和“9009 确实处于冷启动状态”。 + +## 2. 硬件状态与基本原则 + +当前板卡上,FPGA、ADRV9009 和 AD9528 在 IMX 应用进程退出后仍保持供电。FPGA 在线重载时,9009 的器件时钟仍由 OCXO/AD9528 提供,因此 9009 CLKPLL 通常不会因为 FPGA 重载而失锁。 + +不掉电恢复遵循以下原则: + +1. 9009 已经正常运行时,只重新建立软件句柄和 FPGA 侧 JESD 链路,不重新初始化 TALISE。 +2. FPGA 重载后,重新写入 FPGA 静态寄存器、配置 JESD、复位 FPGA GT,并重新完成 SYNC/ILAS 握手。 +3. 射频输出在链路恢复完成前保持关闭或隔离,完成 PLL、JESD、功率参数恢复后再打开。 +4. FPGA 状态、9009 状态应分别判断,不能长期只依赖一个 FPGA `cfg_valid` 标志推断所有硬件状态。 + +## 3. 当前使用的状态标志 + +### 3.1 FPGA `cfg_valid` + +FPGA segment 5、地址 `0x60` 的 bit0 被用作配置有效标志: + +- `1`:应用已经完成过 FPGA/RF 配置,FPGA 自上次配置后没有重新加载; +- `0`:冷启动或 FPGA bitstream 已重新加载,需要恢复 FPGA 配置和 JESD 链路。 + +完成 `config_fpga_para()` 后,应用执行: + +```c +fpga_byte_write(0x05, 0x60, 0x01); +g_rf_boot_done = 1; +``` + +实现位置:`bsp/fpga.c` 中的 `fpga_read_cfg_valid()` 及 `fpga_save_all_para()` 尾部。 + +### 3.2 `g_skip_talise_cfg` + +`g_skip_talise_cfg` 控制是否跳过 ADRV9009 的完整 TALISE 初始化: + +- `0`:冷启动,执行 reset、initialize、ARM 加载和初始化校准; +- `1`:不掉电恢复,仅执行 `TALISE_openHw()` 和状态读取,跳过完整初始化。 + +实现位置:`adrv9009/adrv9009_config.c`。 + +### 3.3 `g_rf_boot_done` + +该标志表示本进程已经完成 RF 启动配置。运行时 FPGA `cfg_valid` 轮询只有在该标志置位后才启用,避免启动阶段误触发热恢复。 + +### 3.4 `g_hot_relink_busy` + +热恢复重入保护。FPGA 升级完成路径和后台 `cfg_valid` 检测路径都可能触发恢复,使用该标志保证同一时间只有一个 `hot_reinit_rf()` 执行。 + +## 4. 统一恢复链路 + +不掉电恢复的逻辑链路如下: + +```text +触发恢复 + | + +-- IMX 应用升级/重启 ---------- FPGA、9009、AD9528保持上电 + | + +-- FPGA 在线升级/IPROG -------- FPGA重载,9009、AD9528保持上电 + | + v +停止 DPD 和可能访问 SPI/RF 的后台状态机 + | + v +判断 FPGA 是否重载、9009 是否仍存活 + | + +-- 9009存活:TALISE_openHw,跳过reset/initialize/cals + | + +-- 9009未运行:执行完整TALISE冷启动初始化 + | + v +恢复 FPGA 静态配置和业务参数 + | + v +配置 JESD TX/RX/FB + FPGA GT reset + SYNC/ILAS重握手 + | + v +恢复频率、衰减、ORx增益、FB route、SPI控制权 + | + v +恢复 FAGC、ORx测量、DPD曲线/自适应状态 + | + v +检查 PLL、JESD、TX状态 + | + +-- 成功:按保存的RF状态打开TX通道和物理RF开关 + | + +-- 失败:保持RF关闭,记录错误并进入有限次数重试/告警 +``` + +## 5. IMX 应用升级或 kill/restart 流程 + +### 5.1 推荐的进程重启顺序 + +应用升级脚本应按以下顺序执行: + +1. 停止 `daemon_system` 和 `logrotateStart`,避免守护进程抢先拉起旧程序。 +2. 执行升级包的 `remove.sh` 和 `cp.sh`,复制新应用及配套文件。 +3. `sync`,确保文件写入完成。 +4. 根据 `/proc//exe` 找到所有旧 `imx_atsc_app` 进程,而不是仅依赖进程名。 +5. 先发送普通终止信号,等待旧进程退出;超时后再发送强制终止信号。 +6. 确认旧进程全部退出后,清理遗留的 `loglog` 管道进程。 +7. 无参数启动新应用: + + ```sh + ./imx_atsc_app + ``` + +8. 重新启动 `daemon_system` 和 `logrotateStart`。 + +ATSC 守护进程已经改为无参数启动新应用。App 端同时保留对历史参数 `1` 的兼容,但会将其按普通启动处理,即保持 `g_restart_app_flg=0`,确保新进程重新建立 TALISE 句柄并重建 FPGA/JESD 链路。 + +### 5.2 新进程中的 RF 启动流程 + +新进程进入 `fpga_para_init()` 后: + +1. 在恢复参数前启用 RF hold,确保初始化期间物理输出保持关闭。 +2. 读取 FPGA `cfg_valid`。 +3. 若 `cfg_valid=1`,认为 FPGA 和 9009 未掉电: + - 设置 `g_skip_talise_cfg=1`; + - 调用 `config_fpga_para()`; + - `adrv9009_init()` 仍负责恢复 FPGA 侧 JESD,但 `adrv9009_cfg()` 只轻挂接 9009; + - 完成后清除 `g_skip_talise_cfg`。 +4. 恢复 AGC、功率、DPD 和 FAGC 参数。 +5. 若保存的 `atsc_para.rf_para.rf=1`,调用 `adrv9009_set_tx_path_enable(1)`。 +6. 等待射频稳定延时后,释放 RF hold,并恢复物理 RF 开关和衰减值。 + +相关代码: + +- `bsp/isys_config.c`:`fpga_para_init()`; +- `bsp/fpga.c`:`config_fpga_para()`、`fpga_save_all_para()`; +- `adrv9009/adrv9009_config.c`:`adrv9009_init()`、`adrv9009_cfg()`; +- `bsp/fpga.c`:`fpga_boot_rf_hold_begin()`、`fpga_boot_rf_hold_release()`。 + +## 6. FPGA 在线升级后的恢复流程 + +### 6.1 FPGA 升级完成 + +Web 或 TCP 升级路径将 `.mcs` 写入 FPGA flash。升级完成后: + +1. `check_fpga_upgrade_done()` 检测 DONE 状态; +2. `reset_fpga_system()` 触发 FPGA IPROG,加载新 bitstream; +3. FPGA 重载后 `cfg_valid` 自动清零; +4. 延时约 3 秒等待 FPGA 配置稳定; +5. 调用 `hot_reinit_rf(0)`,不重启 IMX,不重新初始化 9009。 + +除升级完成回调外,`imx_up_thr()` 每秒检查一次 `cfg_valid`。在本进程已经完成启动、且曾经读到过 `cfg_valid=1` 的前提下,连续两次读到 `0`,也会触发 `hot_reinit_rf(0)`。这是 FPGA 异常重载时的补偿路径。 + +### 6.2 `hot_reinit_rf()` 处理步骤 + +`task/upgrade.c` 中的 `hot_reinit_rf()` 执行: + +1. 使用 `g_hot_relink_busy` 防止重复进入; +2. 记录升级前 DPD/ADPC 模式; +3. 设置 `atsc_adpc_force_stop_flag=TRUE`,停止 DPD 状态机访问 SPI/RF; +4. 等待约 1.5 秒,使后台处理退出临界区; +5. 如果参数 `reload_fpga=1`,先执行 IPROG 并等待约 3 秒; +6. 设置 `g_skip_talise_cfg=1`; +7. 调用 `config_fpga_para()`,恢复 FPGA、JESD 和射频参数; +8. 清除 `g_skip_talise_cfg`; +9. 重置 DPD 软件状态,恢复 FAGC/ORx 测量; +10. 触发 TSNR/ORx phase 7~10 后台流程; +11. 如果升级前 DPD 处于 HOLD,重新加载保存曲线;如果处于 UPDATE,重新启动自适应; +12. 清除热恢复 busy 标志。 + +## 7. ADRV9009 轻挂接流程 + +当 `g_skip_talise_cfg=1` 时,`adrv9009_cfg()` 执行: + +1. 根据通道选择对应的 `taliseDeviceInfo`; +2. 重建 `p_myTaliseDevice_shm` 指针; +3. 复制 TALISE 初始化结构到本进程内存; +4. 调用 `TALISE_openHw()`,重新打开 HAL/SPI 访问; +5. 调用 `TALISE_getPllsLockStatus()` 读取 PLL 状态; +6. 直接返回,不执行以下冷启动操作: + - `TALISE_resetDevice()`; + - `TALISE_initialize()`; + - ARM firmware 重新加载; + - `TALISE_runInitCals()`; + - 其他只应在冷启动执行的校准。 + +该流程解决了“9009 仍在运行时重复 TALISE 初始化导致 err10”的问题。 + +## 8. JESD204B 重连流程 + +轻挂接只跳过 TALISE 侧完整初始化,`adrv9009_init()` 中 FPGA 侧链路配置仍继续执行: + +1. 初始化 IMX 到 ADRV9009 的 SPI 设备; +2. 暂停或隔离射频输出; +3. 配置 FPGA JESD TX0; +4. 轻挂接 ADRV9009; +5. 配置 FPGA JESD FB0 和 RX0; +6. 写 FPGA AXI `0x78 = 0x111` 后清零,复位 TX/RX/FB GT; +7. 使能 JESD TX、RX 链路; +8. 恢复 RF route、FB route 和相关 FPGA AXI 寄存器; +9. 在 9009 framer 已开始发送后,再执行一次 RX/FB GT 晚复位: + + ```text + AXI 0x78 = 0x110 + delay 2 ms + AXI 0x78 = 0x000 + ``` + +10. 读取 FPGA `0xBB` 检查同步状态;当前代码把 `0x07` 或 `0x03` 作为 FB 初始化正常的日志判据; +11. 恢复 ORx/RF monitor、DPD 和 FB route。 + +本链路使用 JESD204B Subclass 0 的 SYNC/ILAS 重握手,不依赖重新初始化 9009 CLKPLL。 + +## 9. 射频参数和输出恢复 + +FPGA/JESD 恢复过程中需要恢复以下运行参数: + +- FPGA 调制和系统静态寄存器; +- 发射频率、功率和频偏; +- ADRV9009 TX attenuation; +- ORx manual gain; +- HMC472/FPGA AGC attenuation; +- ADRV9009 SPI 控制权,从 MCU 切回 FPGA; +- TX/RX/ORx RF route; +- DPD bypass/HOLD/UPDATE 状态; +- 保存的 DPD 曲线; +- FAGC、ORx RMS 和 TSNR 后台测量状态。 + +外部射频打开应放在恢复流程最后: + +1. `adrv9009_set_tx_path_enable(1)` 调用 `TALISE_setRxTxEnable()` 打开 TX1; +2. 回读 `TALISE_getRxTxEnable()` 确认 TX 状态; +3. 打开 FPGA/物理 RF 开关; +4. 等待衰减稳定; +5. 恢复保存的输出功率或衰减设置。 + +如果用户保存的 RF 状态为 OFF,则恢复完成后仍必须保持 TX 和物理 RF 输出关闭。 + +## 10. 恢复成功判据 + +不能只以函数执行结束作为成功,建议至少检查以下项目: + +| 检查项 | 建议判据 | +|---|---| +| 9009 SPI | scratch-pad 写入/回读一致 | +| 9009 PLL | CLKPLL、RFPLL 必须锁定;使用 AUXPLL 时同时检查 AUXPLL | +| 9009 ARM | ARM 状态正常,命令接口可响应 | +| 9009 TX | `TALISE_getRxTxEnable()` 回读包含期望的 TX 通道 | +| FPGA JESD | TX/RX/FB SYNC 状态符合当前设计 | +| FPGA `0xBB` | 当前代码期望恢复后为 `0x03` 或 `0x07` | +| FPGA config | `cfg_valid` 最终重新写为 `1` | +| ORx | `ob_fb`/FWD-FSSI 不应长期为 0、1 或饱和值 `0x7fff` | +| DPD | HOLD 曲线已重新加载,或 UPDATE 状态机已重新启动 | +| 射频输出 | 频谱仪测得中心频率、功率和肩部符合保存配置 | + +建议日志中保留以下关键字,方便现场定位: + +```text +[PR10] boot: FPGA cfg_valid=... +[JESD-RELINK] light-attach 9009... +[HOT-RESTART] begin... +[HOT-RESTART] FAGC/ORx measurement restarted... +[HOT-RESTART] done +``` + +## 11. 当前实现的限制和待补强项 + +### 11.1 IMX `reboot` 与 AD9528 保持 + +当前板卡已确认 Linux `reboot` 可以重新进入 U-Boot 和内核。实测表明,异常不是 `reboot` 本身造成,而是启动脚本无条件重新配置 AD9528,打断了正在运行的参考时钟/SYSREF/JESD 关系。 + +启动脚本现在先调用 `imx_cfgad9528 --check-ready` 进行纯读检查:芯片型号、版本、PLL 配置、SYSREF 控制和 PLL2 锁定状态均正确时,完全不触碰 RESET 和 AD9528 寄存器;状态无效时才进入断电启动的复位+完整配置流程。因此无需依赖可能残留的文件标志。 + +### 11.2 升级包脚本同步保护 + +源码 `src/atsc_app/runme.sh` 和待打包目录 `out/upgrade_imx_fs/SMOD/runme.sh` 已统一为自动分流版本:包含 `zImage` 或指定 DTB 时执行 warm reboot;纯用户态升级则执行“kill 旧 App -> 启动新 App”。`make install` 会自动同步 `runme.sh` 和 `anywave_exc_start.sh`,避免后续构建重新引入旧版。生成 tar 包后仍应使用以下方式复查: + +```sh +tar -tzf SMOD*.tar.gz | grep runme.sh +tar -xOzf SMOD*.tar.gz SMOD/runme.sh +``` + +### 11.3 守护进程启动入口已统一 + +ATSC 守护进程使用 `/proc//exe` 精确检测 `/aw/anywave/app/imx_atsc_app`,不再使用可能受线程改名影响的 `ps | grep`。守护拉起命令不再传入参数 `1`;App 仍兼容旧版守护程序传入的 `1`,但会进入与无参数启动相同的 RF 恢复流程。 + +### 11.4 `cfg_valid` 不能完全代表 9009 状态 + +应把 FPGA 和 9009 状态拆开判断: + +| FPGA状态 | 9009状态 | 应执行流程 | +|---|---|---| +| 未重载 | 存活且PLL锁定 | 轻挂接,按需刷新FPGA/JESD和射频参数 | +| 已重载 | 存活且PLL锁定 | 跳过TALISE初始化,完整重建FPGA/JESD | +| 已重载 | 未初始化/无SPI响应 | 完整冷启动TALISE,再配置FPGA/JESD | +| 未重载 | 无SPI响应或PLL异常 | 保持RF关闭,尝试9009硬复位和完整初始化 | + +当 warm reboot 保留 AD9528 时,App 仍需结合 FPGA/9009 状态选择 light-attach 或完整恢复;不能单独把 `cfg_valid` 当作整条 RF 链路有效的证明。 + +### 11.5 当前错误处理偏弱 + +现有 light-attach 会打印 PLL 状态,但未严格检查 API 返回值和锁定 bit;部分 JESD 状态异常也只是打印日志,恢复流程仍会继续。发布版本建议: + +- 严格检查 `TALISE_openHw()`、PLL、TX enable 和 JESD 状态; +- 任一关键检查失败时保持物理 RF 关闭; +- 增加有限次数的 FPGA/JESD 重连重试; +- 重试失败后产生明确告警,禁止无效链路下打开射频。 + +### 11.6 热恢复后的 RF 最终状态需要统一收口 + +热恢复中存在 `set_pa_output(0)`,而对应的 `set_pa_output(1)` 已被注释。需要结合 FPGA `AXI 0x100` 的实际定义确认 `0` 是否代表正常调制路径。如果它确实是 PA 输出使能,则必须在所有检查成功后统一恢复,避免 JESD 已同步但内部 RF 数据仍被关闭。 + +## 12. 推荐的最终启动决策 + +推荐在应用启动时使用以下决策顺序: + +```text +1. RF物理开关置OFF,启动RF hold +2. 检查FPGA cfg_valid +3. 检查9009 SPI、scratch-pad、PLL和ARM状态 +4. 9009存活 -> light-attach +5. 9009未运行 -> full TALISE init +6. FPGA重载或新进程启动 -> 恢复FPGA参数并重建JESD +7. 恢复频率/衰减/ORx/DPD/FAGC +8. 校验PLL + JESD + TX状态 +9. 成功 -> 按保存状态释放RF hold +10. 失败 -> RF保持OFF,重试或告警 +``` + +这样可以让以下入口最终汇聚到同一套 RF 恢复状态机: + +- IMX 应用升级后的 kill/restart; +- 守护进程异常重启应用; +- 命令行/Web/IMX 内核或 DTB 升级触发的 warm reboot; +- FPGA 在线升级/IPROG; +- FPGA 运行时异常重载。 diff --git a/src/atsc_app/tools/measure_fullband_chpower_once.py b/src/atsc_app/tools/measure_fullband_chpower_once.py new file mode 100755 index 0000000..e197085 --- /dev/null +++ b/src/atsc_app/tools/measure_fullband_chpower_once.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import csv +import datetime as dt +import subprocess +import time +from pathlib import Path + +import vxi11 + + +DOC_DIR = Path(r"\\192.168.10.100\goodman\fox_work\emv7x_pltform\atsc_smod1.0\src\atsc_app\说明_ssh访问方式") +DOC_PATH = DOC_DIR / "全频段功率归一化差值操作说明.txt" +BOARD_HOST = "192.168.1.143" +BOARD_PASS = "anywavecom" +BOARD_HOSTKEY = "ssh-rsa 2048 SHA256:qjLNx9e5kBgids1ULw3jYDq8U3ka3MfQPMAUJDCMHYY" +SA_IP = "192.168.1.147" + +WAIT_STABLE_S = 5 +LOW_POINTS = [58, 63, 69, 79, 85, 177, 183, 189, 195, 201, 207, 213] +U_POINTS = list(range(473, 888, 6)) +POINTS = LOW_POINTS + U_POINTS + + +def set_tx_freq(mhz): + remote = "echo 'freq {}' > /tmp/atsc_ctrl".format(mhz) + cmd = [ + "plink.exe", + "-ssh", + "root@{}".format(BOARD_HOST), + "-pw", + BOARD_PASS, + "-batch", + "-hostkey", + BOARD_HOSTKEY, + remote, + ] + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + return proc.returncode, (proc.stdout or "").strip(), (proc.stderr or "").strip() + + +def set_sa_center(inst, mhz): + commands = [ + ":SENS:FREQ:CENT {}E6".format(mhz), + ":SENS:FREQ:CENT {} MHz".format(mhz), + ":FREQ:CENT {}MHz".format(mhz), + ] + last = None + for _attempt in range(3): + for cmd in commands: + inst.write(cmd) + time.sleep(0.6) + center = float(inst.ask(":SENS:FREQ:CENT?").strip()) / 1e6 + err = inst.ask(":SYST:ERR?").strip() + last = center, err + if abs(center - mhz) <= 0.001: + return center, err, True + time.sleep(0.8) + center, err = last + return center, err, False + + +def read_chpower(inst): + raw = inst.ask(":READ:CHPower?").strip() + return float(raw.split(",")[0].strip()) + + +def write_outputs(stamp, rows): + csv_path = DOC_DIR / "fullband_chpower_once_{}.csv".format(stamp) + txt_path = DOC_DIR / "fullband_chpower_once_{}.txt".format(stamp) + + with csv_path.open("w", encoding="utf-8-sig", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=["freq_mhz", "chpower_dbm"]) + writer.writeheader() + writer.writerows( + {"freq_mhz": row["freq_mhz"], "chpower_dbm": row["chpower_dbm"]} + for row in rows + ) + + lines = [ + "# 全频段 Channel Power 单次读取结果 ({})".format(stamp), + "", + "- 测量条件:发端设频后设置频谱仪中心频率,等待 {}s,读取一次 CHPower。".format(WAIT_STABLE_S), + "- 数据列:freq_mhz, chpower_dbm。", + "- CSV:{}".format(csv_path.name), + "", + "| freq_mhz | chpower_dbm |", + "|---:|---:|", + ] + for row in rows: + lines.append("| {} | {} |".format(row["freq_mhz"], row["chpower_dbm"])) + text = "\n".join(lines) + "\n" + txt_path.write_text(text, encoding="utf-8") + + with DOC_PATH.open("a", encoding="utf-8") as handle: + handle.write("\n\n") + handle.write(text) + + return csv_path, txt_path + + +def main(): + stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S") + rows = [] + csv_path = DOC_DIR / "fullband_chpower_once_{}.csv".format(stamp) + print("CSV: {}".format(csv_path), flush=True) + print("Total points: {}".format(len(POINTS)), flush=True) + + inst = vxi11.Instrument(SA_IP) + inst.timeout = 15 + print("SA IDN: {}".format(inst.ask("*IDN?").strip()), flush=True) + inst.write("*CLS") + + for idx, mhz in enumerate(POINTS, 1): + rc, out, err = set_tx_freq(mhz) + center, sa_err, ok = set_sa_center(inst, mhz) + if rc != 0 or not ok: + print( + "[{:02d}/{}] {} MHz setup warning tx_rc={} center={:.6f} err={} out={} stderr={}".format( + idx, len(POINTS), mhz, rc, center, sa_err, out, err + ), + flush=True, + ) + time.sleep(WAIT_STABLE_S) + chpower = read_chpower(inst) + rows.append({"freq_mhz": mhz, "chpower_dbm": "{:.6f}".format(chpower)}) + + with csv_path.open("w", encoding="utf-8-sig", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=["freq_mhz", "chpower_dbm"]) + writer.writeheader() + writer.writerows(rows) + print("[{:02d}/{}] {} MHz chpower={:.6f} dBm".format(idx, len(POINTS), mhz, chpower), flush=True) + + csv_path, txt_path = write_outputs(stamp, rows) + print("DONE", flush=True) + print("CSV={}".format(csv_path), flush=True) + print("TXT={}".format(txt_path), flush=True) + print("DOC={}".format(DOC_PATH), flush=True) + + +if __name__ == "__main__": + main() diff --git a/src/web/CHANGELOG_20260729_dashboard_spectrum.md b/src/web/CHANGELOG_20260729_dashboard_spectrum.md new file mode 100644 index 0000000..834dde8 --- /dev/null +++ b/src/web/CHANGELOG_20260729_dashboard_spectrum.md @@ -0,0 +1,37 @@ +# 2026-07-29 Web Dashboard / Spectrum 修正记录 + +涉及目录:`www/`(主 web 站点)。`www_korea/`、`www_nlogo/` 未包含 Spectrum 页面,本次未修改。 + +## 修改内容 + +1. Dashboard 的 IMD 卡片已直接在各页面静态 HTML 中调整为 **LIMD 在前、UIMD 在后**;不再依赖运行时 DOM 重排,兼容旧浏览器。 +2. Dashboard 的 SNR 去除了错误的固定 `+10 dB` 显示偏移;状态阈值同步改为真实 SNR/MER 的 `27 / 20 dB`。 +3. Spectrum 页面复用 Dashboard 已获取的状态接口字段 `a_fssi` / `b_fssi`,在频谱标签中显示 FSSI;不新增轮询请求。 +4. 恢复 Spectrum 的 Shoulder、LIMD、UIMD、Pilot、Peak、RBW、Packets、Contiguous、`> 1 field` 信息,以及 Eye Diagram 的 SNR/MER、眼高、眼宽、τ0、段同步、载波偏移、SNR raw、ORx AGC jitter;LIMD/UIMD 以不可拆分的组合卡片同排显示。缺失或非数值字段显示 `--`,不影响其他指标刷新。 +5. Spectrum 与 Eye Diagram 改为标签页:默认选择 Spectrum;切换标签时仅显示并轮询当前标签对应的图形。 +6. 修正标题读取:Spectrum 从 `sys_cfg` 返回的标题字段(索引 6)读取,避免显示 `0`;Login 页面改为设置 `document.title`,并保留 `Digital Exciter` 作为空标题回退。 +7. Spectrum / Eye Diagram 标签页改用完整名称 `ORx Receive Spectrum`、`8VSB Eye Diagram`,并删除内容区重复标题。 +8. 恢复 Spectrum 工具栏:刷新周期(0.5 / 1 / 2 / 5 秒)、Pause、Peak hold;暂停和刷新周期对当前选中的 Spectrum 或 Eye Diagram 标签页生效。 +9. 压缩 Spectrum 指标卡片尺寸并禁用换行,使 FSSI、频谱指标及 LIMD/UIMD 组合保持同一行;卡片最小内容宽度调整为 120 px,以在常用页面宽度下接近充满整行;窄窗口通过横向滚动完整查看。 +10. 移除 Dashboard 与 LCD 中 LIMD/UIMD 的历史固定 `+11 dB` 显示偏移,使其与 Spectrum 原始测量值一致;保留 `sb/sc` 校准量。Dashboard 的 LIMD/UIMD 正常状态阈值由 47 dB 同步调整为 36 dB。 +11. FPGA `.mcs` 上传和写 Flash 进度轮询期间暂停 Settings 页的前端 20 分钟自动登出;在上传异常、升级完成、升级失败或确认重载后恢复原超时与常规状态刷新。 + +## 后端核对 + +`atsc_app/task/iweb_task.c` 已在 `State_SysState` 返回中输出 FSSI A/B 字段,`atsc_app/task/http_get.c` 已从 `st_extend_state` 赋值。本次无需修改 `atsc_app` 接口或字段顺序。 + +## 验证建议 + +1. 登录后确认所有 Dashboard 页面均为 `SNR, LIMD, UIMD` 顺序,且 SNR 与 Status 页一致。 +2. 打开 Spectrum,确认默认显示频谱和 FSSI;点击 Eye Diagram 后只显示眼图和 SNR/MER。 +3. 将网页标题配置为空与非空各验证一次,确认 Spectrum/Login 不再显示 `0` 或空标题。 +12. FPGA consecutive-upgrade status race fixed: publish PENDING 0 0 when the web command is accepted, and start polling only after the upload CGI returns. Completion text now states FPGA reload/RF reconnect instead of IMX reboot. +13. FPGA upgrade publishes RELINK after Flash/IPROG and READY only after hot_reinit_rf() completes. The page redirects to login.html with a five-second countdown only on READY; transient CGI errors keep polling. +14. Login title fix: atsc_status_cfg.cgi is session-protected and returned unauthorized before login. Added login.cgi?action=get_title as a public read-only title lookup and updated login.html to use it. + +15. Restored the historical fixed +11 dB display offset for LIMD/UIMD in the shared Dashboard header and LCD, and restored the matching 47 dB Dashboard normal threshold. Status already used this same display conversion. Spectrum now labels its unshifted values LIMD RAW / UIMD RAW. +16. When FSSI Refresh (A/B switching) is ON, Spectrum and Eye Diagram polling and drawing are suspended and both panels show an explicit unavailable notice. Polling resumes automatically when FSSI Refresh is OFF. +17. FSSI Refresh now controls backend IQ collection as well: update_fssi_state() detects only OFF/ON edges, calls iq_spec_set_enable(0) while A/B Refresh is ON, and resumes clean collection with iq_spec_set_enable(1) when it is OFF. The edge handling precedes FSSI's DPD-busy early returns, so web and LCD changes are covered. +18. Corrected login.cgi?action=get_title HTTP headers: C source now emits real CRLF separators instead of literal backslash text. Rebuild and deploy login.cgi for the configured login title to reach the browser. + +19. 新增 overview.html 作为登录后的默认落地页;登录 CGI 成功重定向、认证代理默认页及已有会话访问登录页时,统一进入 Overview,并将 Overview 加入受保护页面列表。