Update FPGA and Vivado skills

This commit is contained in:
Myfro9
2026-07-14 18:18:17 +08:00
parent d20ec12fee
commit bceafcd58c
14 changed files with 1707 additions and 14 deletions
+151
View File
@@ -0,0 +1,151 @@
---
name: smod_atsc_dev
description: >
在 SMOD 硬件平台(Artix-7 xc7a200t + i.MX6 + ADRV9009)上开发、调试、固化 ATSC 激励器:
改 RTL 并用 vivado-mcp 综合实现、在 Ubuntu VM 上交叉编译 imx6 的 atsc_app 并部署、
用 ILA/串口/频谱做闭环验证。(生成 quickboot .mcs 和远程升级 FPGA 转交 fpga_update skill。)
只要用户提到 SMOD、my_exciter、RU_golden、smod_top、atsc_app、ATSC 激励器、8VSB 调制、
ADRV9009/ORx/DPD/TxAGC、tuner(Sony CXD2878)、TSoIP、quickboot、远程升级 mcs、192.168.1.143、
或在 E:\Xilinx_design\smod\SMOD_ATSC 下做任何 FPGA / imx6 改动,就必须使用这个 skill —— 即便
他们没点名 "skill",也没说 "SMOD",只是说 "把新 bit 升上去"、"改一下激励器的 RTL"、
"imx6 app 编译部署一下"、"抓个 ILA 看看"、"重新出个 mcs"。这个 skill 给出平台事实、
两条烧写路径的选择、上电时序铁律,并把闭环调试转交给 fpga-imx6-flow。
---
# SMOD ATSC 激励器开发
SMOD 是一块 **Artix-7 (xc7a200tfbg676-2) + i.MX6 + ADRV9009** 的 ATSC 激励器板。
FPGA 做 8VSB 调制 / DPD / AGC / TS 前处理,imx6 跑 `atsc_app`(配置 ADRV9009、tuner、
网页、SNMP),两者通过一个 I2C/寄存器桥通信。
这个 skill 是**总入口**:它告诉你平台的硬事实、该走哪条烧写路径、以及什么时候把活
交给别的 skill。具体细节在 `references/`,可直接运行的脚本在 `scripts/`。
## 平台事实速查
| 项 | 值 |
|---|---|
| 激励器 Vivado 工程 | `E:\Xilinx_design\smod\SMOD_ATSC\my_exciter\my_exciter.xpr`(top = `smod_top`,也是 **git 仓库根**) |
| 打底 golden 工程 | `E:\Xilinx_design\smod\SMOD_ATSC\RU_golden\RU_golden.xpr`(top = `RU_golden_top`) |
| FPGA / Flash | `xc7a200tfbg676-2` / Winbond **w25q128bv**(128 Mbit = 16 MB,SPIx1) |
| Vivado | `D:\Xilinx\Vivado\2018.1`(`bin\vivado.bat`) |
| imx6 源码 | `my_exciter\imx6\src\atsc_app`(Makefile 在该目录根) |
| imx6 设备 | `192.168.1.143`,`root` / `anywavecom`(网页 + SSH;Windows 主机 `192.168.1.125` 同网段) |
| 交叉编译 VM | `bowei@192.168.223.128`,密码 `lr7402`,构建目录 `~/atsc_head_build` |
| 工具链 | `/opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi` |
| app 部署路径 | `/aw/anywave/app/imx_atsc_app`(先写 `imx_atsc_app_head`,再 `cp -a` 覆盖 active) |
| app 日志 | `/var/module_log/imx_atsc_app.log` |
| 串口 | COM 号**逐机而异,两处配置都在 `~` 下、都不入库**:两个继电器(电源 / USB-Blast)看 `~/vivado_mcp_usb.conf`(缺了就跑 vivado-mcp 的 `detect_serial_relays` 自动探测生成);imx6 控制台看 `~/fpga_imx6_flow.json` 的 `ports.imx6`。**任何地方都不要写死 COM 号。** |
| 原理图 | `my_exciter\Notes\SMOD_V1.0_SCH251010.PDF` |
| 构建产物约定 | `my_exciter\bitstream\<tag>.{bit,ltx,mcs,prm}`,一次迭代一个 tag(`smod_p1`、`smod_dpd`…) |
## ⚠️ 硬件铁律:上电时序
**给 SMOD 上电/断电之前,USB-Blast 必须先断开。** 绝不在 USB-Blast 连接状态下切换板卡电源
——这会打坏 JTAG 适配器和板卡。
```
断电(power_off) → 断开 USB-Blast(USB_Blast_off) → 上电(power_on) → 连接 USB-Blast(USB_Blast_on) → JTAG/ILA
```
用 vivado-mcp 的 `power_on` / `power_off` / `USB_Blast_on` / `USB_Blast_off` 工具执行。
上电后要**等 imx6 串口控制台打印 `sys init over` 再连 JTAG**,否则 TX 链路会挂掉。
## 两条烧写路径 —— 先选对,再动手
这是本平台最容易搞错的地方。板上 flash 是 **quickboot 双镜像**布局:golden 镜像在
`0x000000..0x3FFFFF`,update 镜像在 `0x400000..0xBFFFFF`,切换字 `0xAA995566` 在 `0x000FFC`。
| | **A. JTAG 直烧** | **B. 网络远程升级**(PR07 主线) |
|---|---|---|
| 何时用 | 板子是砖 / 首次装机 / 要换 golden | 日常迭代,板子已有 golden 且网络能通 |
| 烧什么 | `RU_golden\bitstream\golden_design_initial.mcs` | `my_exciter\bitstream\<tag>_update.mcs` |
| 怎么做 | `/fpga_flash` skill(flash 型号 `w25q128bv`) | **`/fpga_update` skill** |
| 需要 | USB-Blast + 上电时序 | 只要 `192.168.1.143` 能访问 |
| 风险 | 高(要动电源和 JTAG) | 低(写坏了 golden 会兜底) |
**调试期**还有第三种:`program_device` 直接把 `.bit` 下到 FPGA(掉电即失),配合 `.ltx` 抓 ILA。
迭代阶段用它最快,验证通过后再走 A 或 B 固化。
### B 走 `/fpga_update`
**远程升级不走网页。** 网页的 Remote Update 只认 imx6 的 `*.tar.gz` 固件包,`.mcs` 传上去会被
`check_package.sh` 删掉。FPGA 走的是 TCP **1234** 上的私有协议。生成 mcs、烧写、排错的完整流程
都在 `fpga_update` skill 里,包括两个必须知道的事实:
- **擦除卡住 ≠ 还在擦。** imx6 的 `POLL_ERASER` 不看 ERROR 位,programmer 报错和正常忙碌
返回同一个 `ERASER_ING`。读设备日志的 `rstate` 才知道真相(`0x7`=已中止,`0x0`=真在擦)。
- `-imagesize 64` 和 `SpiFlashProgrammer.vhd` 里的 `cAddrUpdateStart/End` 是一对,别改。
升级完成后**断电冷启动**,让 FPGA 从 flash 重新加载,再验证。
## 三条典型工作流
**只改 RTL** → 编辑 `my_exciter.srcs/sources_1/**` → vivado-mcp `run_synthesis` +
`run_implementation` + `generate_bitstream` → `program_device` 下 bit → 抓 ILA 验证 →
满意后走 `/fpga_update` 固化。
**只改 imx6** → 编辑 `imx6/src/atsc_app/**` → 提交到 git(脚本默认打包 `HEAD`)→
```bash
python .../scripts/imx6_app.py build-deploy --tag mychange
```
→ 冷启动让 `rc.local` 拉起新 app → `imx6_app.py log` 看日志。
**FPGA + imx6 联调** → 交给 **`/fpga-imx6-flow`**。那个 skill 驱动一张 LangGraph 图,
把上电/JTAG/烧写/重启做成确定性节点,把「分析 → 判断是否达标 → 提方案 → 改 FPGA / 改 imx6」
这四步作为中断交回当前会话执行,一轮一轮迭代。PR07 明确要求走这条路做联合调试。
## 什么时候调别的 skill
| 场景 | 用 |
|---|---|
| 上电 / JTAG / 综合实现 / 抓 ILA / 下 bit | **vivado-mcp** 工具(`power_on`、`program_device`、`run_tcl`…) |
| 闭环设计-调试迭代(本 skill 的主推荐) | **`/fpga-imx6-flow`** |
| JTAG 烧 flash(golden 或砖机恢复) | **`/fpga_flash`**(flash 型号选 `w25q128bv`) |
| 单模块 RTL 仿真 / 出 testbench | **`/vivado-sim`** |
| ILA 抓出来的 IQ CSV 做频谱 / OBW / 带肩 | **`/fft-analyse`** |
| 网页操作(登录、升级、看频谱页) | **claude-in-chrome** |
| 实测频谱(N9320B 频谱仪) | `mcp__mcp_n9320b__*` 工具 |
## scripts/
| 脚本 | 用途 |
|---|---|
| `imx6_app.py` | `build` / `deploy` / `build-deploy` / `log` —— VM 交叉编译 + 推到 imx6 + 读日志 |
出 `.mcs` 和远程升级的脚本在 `/fpga_update` skill 里(`gen_update_mcs.py`、`fpga_ru.py`)。
两个脚本都 `--help` 可查。`imx6_app.py deploy` 会自动装 paramiko 2.12 到缓存目录并 re-exec
(imx6 的 dropbear 太老,paramiko 5 连不上,见下)。
## references/
按需读,别一次全读进来:
- `references/fpga.md` —— RTL 目录结构、XDC、vivado-mcp 用法、ILA/ltx 约定、已知时序告警的性质
- `references/imx6.md` —— app 源码结构、交叉编译细节、部署/日志/寄存器访问、网页
- `references/upgrade.md` —— quickboot 双镜像布局、mcs 生成的确切参数、golden 恢复
- `references/history.md` —— 历史 PR/Task 笔记索引(tuner、TSoIP、ORx、DPD、AGC 各自踩过什么坑)
## 常见坑
| 症状 | 原因 / 处理 |
|---|---|
| 远程升级后重新上电 FPGA 不跑 | 传错了文件 —— 必须是 `_update.mcs`(起始 `0x800000`),不是 `write_cfgmem` 出的基础 mcs |
| 远程升级卡在 "erasing" 不动 | programmer 已报错退出(`rstate=0x7`),imx6 的轮询不看 ERROR 位。见 `/fpga_update` |
| imx6 SSH 报 `unknown cipher` / `no acceptable host key` | dropbear 太老。用 paramiko **2.12**(`imx6_app.py` 已处理);命令行 ssh 加 `-o HostKeyAlgorithms=+ssh-rsa` |
| imx6 `open_sftp()` → `EOF during negotiation` | 设备没有 SFTP 子系统。用 `cat > file` 走 exec channel 传文件 |
| VM SSH `kex_exchange_identification: Connection closed` | VM 的 sshd 挂了,请用户 `sudo systemctl restart sshd` |
| 上电后 TX 链路死掉 | 上电后没等 `sys init over` 就连了 JTAG |
| `power_off` 卡在循环里 | 先 `USB_Blast_off` 再 `power_off`,顺序反了 |
| WNS ≈ -9 ~ -13 ns | JESD204 `init_seq_data` 的既有 false path,与功能无关,不要为它改设计 |
| `i2cget` 读 FPGA 寄存器时好时坏 | 带调试 ILA 的 bit 时序更差;且会和 app 抢总线。以生产 bit 为准,或从 app 日志读 |
## 写笔记
这个工程的约定是 `my_exciter\Notes\PRxx.md` 记**需求**,`Taskxx.md` 记**方案与实现总结**
(含验证数据和踩过的坑)。做完一轮有结论的工作,补一份 `Taskxx.md` —— 后来的人(和后来的你)
靠它避免重复踩坑。`references/history.md` 是已有笔记的索引。
+81
View File
@@ -0,0 +1,81 @@
# FPGA 侧(my_exciter / RU_golden)
## 工程
| | my_exciter | RU_golden |
|---|---|---|
| xpr | `E:\Xilinx_design\smod\SMOD_ATSC\my_exciter\my_exciter.xpr` | `...\RU_golden\RU_golden.xpr` |
| top | `smod_top`(`my_exciter.srcs/sources_1/top/smod_top.v`,~2300 行) | `RU_golden_top` |
| 作用 | ATSC 激励器全功能 | 打底镜像:网口 + quickboot 自烧,用来兜底和支撑远程升级 |
| 器件 | xc7a200tfbg676-2 | 同左 |
`my_exciter` 目录本身是 **git 仓库根**(`imx6/` 也在里面)。RU_golden 的 xpr 引用了
my_exciter 里的少数源文件(`ts_lock_top.vhd`、`sdram_tsoip_top.vhd`),改动时留意。
## RTL 目录(`my_exciter.srcs/sources_1/`)
| 目录 | 内容 |
|---|---|
| `top/` | `smod_top.v` 顶层 + 时钟/AGC/DA 前处理/ORx 抽取等杂项 |
| `atsc_mod_xip/` | 8VSB 调制核(加密 IP) |
| `dpd/` | 数字预失真 |
| `txagc/` | 发射 AGC |
| `asi_pre/`, `dvb_asi/`, `ts/` | ASI / TS 输入与包同步(`src_preproc`、`spi_lock_det`) |
| `tsoip/`, `tsoip_atsc/`, `udp/`, `sdram/` | TS over IP 收发与缓存 |
| `tuner/` | Sony CXD2878 tuner 的 TS 接口(`TUN_D[7:0]/TUN_VLD/TUN_CLK`) |
| `quickboot/` | `SpiFlashProgrammer.vhd` + `SpiSerDes.vhd` + `spi_slave32.v` —— 远程升级的 FPGA 侧 |
| `i2c/` | imx6↔FPGA 的寄存器桥 |
| `gps/`, `uart/`, `misc/`, `bd_cfg/`, `coef/`, `ip/` | 其余 |
约束在 `constrs_1/new/`:`smod_ioplanning.xdc`(SMOD 板的引脚,**这块板用这个**)、
`project_ioplanning.xdc`(旧板,别混用)、`timing.xdc`、`timing_cleanup_120m.xdc`、
`bitstream.xdc`、`debug.xdc`(当前为空,ILA 用 IP 例化而非 XDC 标记)。
## 用 vivado-mcp 干活
典型顺序(工具名即 `mcp__vivado-mcp__*`):
```
start_session(project=<xpr>) # 打开工程
→ verilog_compile_check # 改完 RTL 先做语法检查,比跑综合快得多
→ run_synthesis
→ get_critical_warnings # 综合后立刻看,别等到实现完
→ run_implementation
→ generate_bitstream
→ get_timing_report / get_utilization_report
→ power_off → USB_Blast_off → power_on → (等 sys init over) → USB_Blast_on
→ program_device # 下 bit(掉电即失)
```
其它常用:`run_tcl` / `safe_tcl`(任意 Tcl)、`get_run_progress`(长任务轮询)、
`get_ip_status` / `inspect_ip_params`、`xdc_lint` / `xdc_auto_fix`、`get_io_report`。
综合+实现在这个工程上要 **20~40 分钟**。改一行就重跑之前,先用 `verilog_compile_check`。
## ILA
`smod_top.v` 里 ILA 是**手工例化的 IP**(不是 `mark_debug`)。历史上用过 `ila_eth2`、
`ila_2`、`ila_3`、`ila_adrv9009_fb`;当前活跃的是 `u_ila_adrv9009_rx`(复用
`ila_adrv9009_fb` IP,探针宽度 16,16,1,8,1,1,1,8,8,8,8,5)。
要换探针就直接改那个例化的 probe 连接。**探针宽度必须和 IP 里配的一致**,否则综合报错。
需要新宽度就改 IP 或换一个 ILA IP。
抓波形要 `.ltx`(`impl_1/smod_top.ltx`)。归档时 `.bit` / `.ltx` / `.mcs` 三件套用**同一个 tag**
放进 `bitstream/`,否则下次想复现就对不上号了。ILA 导出的 CSV 交给 `/fft-analyse` 做频谱。
**固化前把调试 ILA 去掉**,重新实现出干净的生产 bit:ILA 吃资源、恶化时序,还会和 imx6
抢寄存器总线。
## 时序
`get_timing_report` 上 WNS 常年在 **-9 ~ -13 ns**,路径落在 JESD204 TX 的
`init_seq_data`(`jesd204_phy_1lane_38j82_refclk` group)。这是基础设计里**既有的
初始化序列 false path**,与 ATSC 数据通路无关 —— JESD 链路实测能起来,RF/调制/tuner 都正常。
不要为了这个数字去改设计。真正要盯的是你改动那部分的时钟域和 CDC。
## 出 mcs
走 `/fpga_update` skill。一句话:远程升级要的是 `_update.mcs`,不是 `write_cfgmem` 的直接产物;
而且它走 TCP 1234,不走网页。
+35
View File
@@ -0,0 +1,35 @@
# 历史笔记索引(`my_exciter\Notes\`)
约定:**`PRxx.md` = 需求**(用户提出的要做什么),**`Taskxx.md` = 方案 / 实现总结**
(怎么做的、踩了什么坑、验证结论)。`discussNN-*.md` 是过程讨论。
遇到相关主题**先读对应的 Task 文件**——里面的结论往往能省掉一整轮硬件迭代。
| 主题 | 需求 | 方案 / 总结 | 结论要点 |
|---|---|---|---|
| 从 EMV7.1 移植到 SMOD | `PR00.md`、`PR01.md` | `Task00.md`、`Task01.md` | 引脚重排到 `smod_ioplanning.xdc`;IO bank / 自由引脚勘查脚本也在 Notes 里(`task01_*.tcl`) |
| ADRV9009 JESD + SPI 寄存器核对 | `PR02.md` | `Task02.md` | JESD204B TX/RX lock status 寄存器位置 |
| TSoIP(网口 2 收 IP 流取 TS) | `PR03.md` | — | 根因见 memory `tsoip-root-cause-0x50` |
| Tuner(473 MHz RF → TS → 调制核) | `PR04.md` | `Task04.md` | demod 是 **Sony CXD2878 + FREIA**,不是 MN88436;移植 EMV9.3 的 `sonytuner/`;ILA 实测 19.41 Mbps、`ts_lock_a=1`、0x47 间隔 188 |
| ATSC 信号质量分析(RX1 路) | `PR06.md` | `Task06.md` | 用 RX1 接收自发信号,算带肩 + 8VSB 星座 + SNR,网页 `sys_spectrum.html` 显示 |
| 改用 ORx→DPD 40 MHz 反馈 | `PR06-1.md` | `Task06-1.md` | 反馈通道换成 ORx |
| ORx/RX1 双通道可切换 + DPD 内部 AGC | `PR06-2.md` | `Task06-2.md` | 共用滤波下采样;大量频谱截图见 `PR06-2_*.png`,索引在 `PR06-2_频谱截图索引.md` |
| 本 skill 的由来 | `PR07.md` | (本 skill) | 要求:SMOD ATSC 开发总入口,联调走 `/fpga-imx6-flow` |
## 其它散落资源
- `Notes/*.png` —— 历次频谱 / 星座 / 波形截图,文件名带 iter 号,配合 Task 文件看
- `Notes/create_ila_adrv9009_fb.tcl` —— 建 ILA IP 的 Tcl
- `Notes/task01_*.tcl` —— IO / DSP / 未约束端口的勘查脚本
- `Notes/workflow_state.db`、`workflow_threads.json` —— fpga-imx6-flow 的 LangGraph 状态
- `RU_golden/skills/*.py` —— 早期 RMII/GMII 抓包分析脚本(网口调试用)
- `bitstream/ila_*.csv`、`orx_*.csv` —— 历次 ILA 导出,交给 `/fft-analyse` 复现频谱
## 已固化的经验(别重新踩)
- 上电时序铁律;上电后等 `sys init over` 再连 JTAG,否则 TX 死。
- WNS -9~-13 ns 是 JESD204 `init_seq_data` 的既有 false path,与功能无关。
- imx6 dropbear 老:paramiko 2.12 + `cat >` 传文件(无 SFTP)。
- 设备上 `imx_atsc_app.rx1` 和 `M=2/lane 0x04` 变体会打断 TX;git `HEAD` 的版本是好的。
- 远程升级走 TCP 1234(不是网页!),只能传 `_update.mcs`,传错文件会导致冷启动后 FPGA 不跑。
- 远程升级"卡在擦除"通常是 programmer 已报错(rstate=0x7),imx6 轮询不看 ERROR 位。见 `/fpga_update`。
- 带调试 ILA 的 bit 下 `i2cget` 读 FPGA 寄存器不可靠。
+84
View File
@@ -0,0 +1,84 @@
# imx6 侧(atsc_app)
## 源码
`E:\Xilinx_design\smod\SMOD_ATSC\my_exciter\imx6\src\atsc_app`(Makefile 在这一层):
| 目录 | 内容 |
|---|---|
| `main.c` | 起线程:`fpga_upgrade_task`、`tcp_task`、`web`、`lcd`、`gps`… |
| `bsp/` | 板级:`i2c_drv.c`、`fpga.c`(FPGA 寄存器读写)、`eeprom.c`(配置)、`dac38j82.c`、`dpd_proc.c`、`isys_config.c` |
| `talise/`, `adrv9009/`, `adi_hal/` | ADI ADRV9009 驱动 |
| `sonytuner/` | Sony CXD2878 + FREIA(CXD6821) 解调驱动(从 EMV9.3 移植,见 PR04) |
| `task/` | `upgrade.c`(软件包升级)、`tcp_task.c`、`iweb_task.c`、`tsoip.c`、`iq_rx.c` |
| `inc/` | 头文件;`fpga.h` 里 `FPGA_UPGRADE_I2C_REG_ADDRES = 0xE0` |
同级还有 `imx6/src/web/`(cgi + www 页面,含 `upload.cgi` 远程升级入口)。
## 交叉编译
工具链在 VM `bowei@192.168.223.128`(密码 `lr7402`):
```
/opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
```
流程(`scripts/imx6_app.py build` 已封装):
1. Windows 侧从 git 打包源码 —— **用 `HEAD` 而不是工作区**,这样构建的东西可追溯:
`git -C my_exciter archive --format=tar -o app.tar HEAD:imx6/src/atsc_app`(约 13 MB)
2. SFTP 上传到 VM,解到 **`~/atsc_head_build`**(一个干净目录;不要碰 `~/atsc_tsoip_build/` 之类的旧目录)
3. `bash -lc 'source <envsetup>; cd ~/atsc_head_build && make'` → 产出 `imx_atsc_app`(~2.9 MB ELF)
4. SFTP 取回 Windows
VM 的 sshd 是现代版本,paramiko 5 / OpenSSH 都能连。若报
`kex_exchange_identification: Connection closed`,是 VM 的 sshd 挂了,请用户 `sudo systemctl restart sshd`。
## 部署到设备
设备 `192.168.1.143`,`root` / `anywavecom`。**它的 SSH 是老 dropbear**,有两个坑:
1. **算法太老**:paramiko 5 会报 `unknown cipher` / `no acceptable host key`。
要用 **paramiko 2.12**(有 TripleDES / aes-cbc / ssh-rsa):
`python -m pip install --target <dir> paramiko==2.12.0`,然后 `PYTHONPATH=<dir>` 跑。
命令行 ssh 则加 `-o HostKeyAlgorithms=+ssh-rsa`。
2. **没有 SFTP 子系统**:`open_sftp()` 直接 `EOF during negotiation`。传文件走 exec channel:
```python
ch = client.get_transport().open_session()
ch.exec_command("cat > /aw/anywave/app/imx_atsc_app_head")
ch.sendall(open(local_bin, "rb").read())
ch.shutdown_write(); assert ch.recv_exit_status() == 0
```
(另一条老路是 SSH 反向端口转发 + 设备侧 `wget`,见 Task04;现在没必要了。)
部署顺序 —— **先落一个新名字,验证 md5,再覆盖 active**,这样传输失败不会毁掉能跑的 app:
```
cat > /aw/anywave/app/imx_atsc_app_head
chmod 755 imx_atsc_app_head
md5sum 对比
cp -a imx_atsc_app_head imx_atsc_app # 覆盖 active
```
`scripts/imx6_app.py deploy` 就是这个顺序。已知好用的备份是 `imx_atsc_app.bak_pre_rx1`。
改完 app 要**冷启动**(断电重启,遵守上电时序)让 `rc.local` 拉起新进程。
## 观察设备
- 日志:`/var/module_log/imx_atsc_app.log`。健康标志:`TALISE_radioOn`、`fb init state ok`、
`fpga[0xBB]=0x03`(sync=1 / tvalid=1)。`imx6_app.py log` 直接拉。
- 配置:`/aw/anywave/cfg/exc_cfg.conf`,`[BLK_SYS_PARA] reg50` 选输入源
—— `0`=Tuner,`1`=TS,`2`=EAS,`3`=TSoIP。
- 网页:`http://192.168.1.143`(`sys_rf.html`、`sys_ts.html`、`sys_set.html`、
`status_info.html`、`log_output.html`、以及远程升级的 `upload.cgi`)。
- 串口控制台(COM 号见 `~/fpga_imx6_flow.json` 的 `ports.imx6`,逐机而异、不入库)能用,但会自动登出、比较飘。**优先走网络 SSH**读日志和跑命令。
- 直接读 FPGA 寄存器可以 `i2cget`,但它会和 app 抢总线;带调试 ILA 的 bit 下更不可靠。
以 app 日志为准,或者临时停掉 app。
## 已知不能用的构建
设备上的 `imx_atsc_app.rx1`、以及任何 `M=2 / lane 0x04` 的变体**会打断 TX**。
git `HEAD` 的版本是 RX1-on-framerA(`M=4` / lane `0x03` / RX1RX2)+ TX 正常的已知好版本。
+24
View File
@@ -0,0 +1,24 @@
# 远程升级 / Flash 布局
**这块内容的唯一真源是 `fpga_update` skill。** 要生成 `_update.mcs` 或者把它烧进板子,
直接用那个 skill,别在这里另起炉灶。
- `~/.claude/skills/fpga_update/SKILL.md` —— 流程
- `~/.claude/skills/fpga_update/references/quickboot.md` —— flash 布局、切换字兜底、`-imagesize 64` 的由来
- `~/.claude/skills/fpga_update/references/protocol.md` —— TCP 1234 协议、以及 DPD 污染升级数据流的缺陷
三件容易记反的事:
1. **FPGA 的远程升级不走网页。** 网页的 Remote Update 只认 imx6 的 `*.tar.gz` 固件包;
`.mcs` 传上去会被 `check_package.sh` 直接删掉。FPGA 走 TCP **1234** 的私有协议
(原厂工具 `tools/Xfpga_ru.exe`,替代品 `fpga_update/scripts/fpga_ru.py`)。
2. **升级前必须关掉 DPD**,否则 DPD 的 SPI1 流量会混进镜像,CRC 校验失败。
3. **`_update.mcs` 不能 JTAG 烧写**,`_initial.mcs` 也不能远程升级。首次装机 / 救砖走
`/fpga_flash`(flash 型号 `w25q128bv`,烧 `RU_golden/bitstream/golden_design_initial.mcs`)。
## 调试期的第三条路
`program_device` 直接把 `.bit` 下到 FPGA,掉电即失。迭代验证用它最快 —— 不碰 flash,
配 `.ltx` 就能抓 ILA。功能确认后再走远程升级固化。
固化用的 bit 要**去掉调试 ILA 重新实现**:ILA 吃资源、恶化时序,还会和 imx6 抢寄存器总线。
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""Cross-compile the SMOD imx6 atsc_app on the Ubuntu VM and deploy it to the board.
build package app source -> VM (~/atsc_head_build) -> make -> pull imx_atsc_app back
deploy push a local imx_atsc_app to 192.168.1.143, md5-verify, then swap it in
build-deploy both
log tail /var/module_log/imx_atsc_app.log from the device
Two device quirks drive the design here:
* The imx6 runs an old dropbear. Modern paramiko can't negotiate with it
("unknown cipher" / "no acceptable host key"), so this script vendors
paramiko 2.12 into ~/.cache/smod_atsc_dev and re-execs itself under it.
* That dropbear has no SFTP subsystem, so files go over an exec channel with
``cat > file`` rather than sftp.put().
Deploy writes imx_atsc_app_head first and only overwrites the active imx_atsc_app once
the md5 matches -- a half-transferred binary should never become the thing rc.local runs.
After deploying you still need a cold boot (power_off -> USB_Blast_off -> power_on ->
USB_Blast_on) for rc.local to pick up the new app. This script deliberately does not
touch the relays; that sequencing lives in the vivado-mcp tools.
"""
import argparse
import hashlib
import os
import subprocess
import sys
import tarfile
import tempfile
REPO = r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter"
APP_SUBDIR = "imx6/src/atsc_app"
VM_HOST, VM_USER, VM_PASS = "192.168.223.128", "bowei", "lr7402"
VM_BUILD_DIR = "atsc_head_build" # relative to the VM home dir
TOOLCHAIN = "/opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi"
IMX_HOST, IMX_USER, IMX_PASS = "192.168.1.143", "root", "anywavecom"
IMX_APP_DIR = "/aw/anywave/app"
IMX_STAGING = f"{IMX_APP_DIR}/imx_atsc_app_head"
IMX_ACTIVE = f"{IMX_APP_DIR}/imx_atsc_app"
IMX_LOG = "/var/module_log/imx_atsc_app.log"
BINARY = "imx_atsc_app"
VENDOR_MARKER = "SMOD_PARAMIKO_VENDORED"
# paramiko 2.12 reaches for algorithms.TripleDES, which cryptography >= 43 removed.
VENDOR_PKGS = ["paramiko==2.12.0", "cryptography<43"]
# --------------------------------------------------------------------------- vendoring
def ensure_legacy_paramiko():
"""Re-exec under a vendored paramiko 2.12 unless we're already running under it."""
if os.environ.get(VENDOR_MARKER):
return
cache = os.path.join(os.path.expanduser("~"), ".cache", "smod_atsc_dev", "paramiko212")
if not os.path.isdir(os.path.join(cache, "paramiko")):
print(f"[setup] installing legacy paramiko into {cache} (one time)", flush=True)
os.makedirs(cache, exist_ok=True)
# --no-warn-conflicts: pip diffs --target installs against the ambient env and
# reports conflicts that don't apply here, which reads like a failure.
subprocess.run([sys.executable, "-m", "pip", "install", "--quiet", "--no-warn-conflicts",
"--target", cache] + VENDOR_PKGS, check=True)
env = dict(os.environ, **{VENDOR_MARKER: "1"})
env["PYTHONPATH"] = cache + os.pathsep + env.get("PYTHONPATH", "")
env["PYTHONWARNINGS"] = "ignore"
sys.exit(subprocess.run([sys.executable, os.path.abspath(__file__)] + sys.argv[1:], env=env).returncode)
# ------------------------------------------------------------------------------- ssh
def connect(host, user, password):
import paramiko
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(host, username=user, password=password, timeout=20, look_for_keys=False, allow_agent=False)
return cli
def run(cli, cmd, check=True, quiet=False):
if not quiet:
print(f" [ssh] {cmd}")
_, out, err = cli.exec_command(cmd, timeout=1800)
rc = out.channel.recv_exit_status()
stdout, stderr = out.read().decode(errors="replace"), err.read().decode(errors="replace")
if check and rc != 0:
sys.exit(f"[FAIL] rc={rc}\n{stdout}\n{stderr}")
return rc, stdout, stderr
def md5_of(path):
h = hashlib.md5()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
# ----------------------------------------------------------------------------- build
def package_source(repo, source_mode):
tar_path = os.path.join(tempfile.mkdtemp(prefix="smod_app_"), "app.tar")
if source_mode == "git":
subprocess.run(["git", "-C", repo, "archive", "--format=tar", "-o", tar_path, f"HEAD:{APP_SUBDIR}"],
check=True)
rev = subprocess.run(["git", "-C", repo, "rev-parse", "--short", "HEAD"],
capture_output=True, text=True, check=True).stdout.strip()
print(f"[build] packaged {APP_SUBDIR} from git HEAD ({rev})")
else:
src = os.path.join(repo, *APP_SUBDIR.split("/"))
skip = {".o", ".bak", ".zip", ".tar"}
with tarfile.open(tar_path, "w") as tf:
for root, _, files in os.walk(src):
for name in files:
if os.path.splitext(name)[1] in skip or name == BINARY:
continue
full = os.path.join(root, name)
tf.add(full, arcname=os.path.relpath(full, src))
print(f"[build] packaged {APP_SUBDIR} from the working tree (stale .o excluded)")
return tar_path
def cmd_build(args):
tar_path = package_source(args.repo, args.source)
out_path = os.path.abspath(args.out)
vm = connect(VM_HOST, VM_USER, VM_PASS)
try:
sftp = vm.open_sftp()
print(f"[build] uploading {os.path.getsize(tar_path)/1e6:.1f} MB to VM")
sftp.put(tar_path, f"/home/{VM_USER}/app_src.tar")
run(vm, f"rm -rf ~/{VM_BUILD_DIR} && mkdir -p ~/{VM_BUILD_DIR} && "
f"tar xf ~/app_src.tar -C ~/{VM_BUILD_DIR}")
print("[build] cross-compiling (this takes a few minutes)")
rc, out, err = run(vm, f"bash -lc 'source {TOOLCHAIN} && cd ~/{VM_BUILD_DIR} && make {args.make_args}'",
check=False)
tail = "\n".join((out + err).splitlines()[-40:])
if rc != 0:
sys.exit(f"[FAIL] make exited {rc}. Last output:\n{tail}")
print(tail)
remote_bin = f"/home/{VM_USER}/{VM_BUILD_DIR}/{BINARY}"
rc, _, _ = run(vm, f"test -f {remote_bin}", check=False)
if rc != 0:
sys.exit(f"[FAIL] make succeeded but {remote_bin} is missing")
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
sftp.get(remote_bin, out_path)
finally:
vm.close()
print(f"\n[OK] {out_path} ({os.path.getsize(out_path)/1e6:.2f} MB, md5 {md5_of(out_path)})")
return out_path
# ---------------------------------------------------------------------------- deploy
def push_file(cli, local, remote):
"""Send a file over an exec channel: the device's dropbear has no SFTP subsystem."""
stdin, stdout, _ = cli.exec_command(f"cat > {remote}", timeout=600)
with open(local, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 16), b""):
stdin.write(chunk)
stdin.channel.shutdown_write()
rc = stdout.channel.recv_exit_status()
if rc != 0:
sys.exit(f"[FAIL] `cat > {remote}` exited {rc}")
def cmd_deploy(args):
local = os.path.abspath(args.binary)
if not os.path.exists(local):
sys.exit(f"[FAIL] not found: {local}")
want = md5_of(local)
print(f"[deploy] {local} (md5 {want})")
imx = connect(IMX_HOST, IMX_USER, IMX_PASS)
try:
print(f"[deploy] staging -> {IMX_STAGING}")
push_file(imx, local, IMX_STAGING)
run(imx, f"chmod 755 {IMX_STAGING}")
_, out, _ = run(imx, f"md5sum {IMX_STAGING}")
got = out.split()[0]
if got != want:
sys.exit(f"[FAIL] md5 mismatch after transfer: device {got} != local {want}. "
f"Active app untouched.")
print("[deploy] md5 verified")
run(imx, f"cp -a {IMX_STAGING} {IMX_ACTIVE}")
_, out, _ = run(imx, f"md5sum {IMX_ACTIVE}")
if out.split()[0] != want:
sys.exit("[FAIL] active binary md5 mismatch after swap")
finally:
imx.close()
print(f"\n[OK] deployed to {IMX_ACTIVE}")
print("Now cold-boot the board so rc.local starts the new app:")
print(" vivado-mcp: power_off -> USB_Blast_off -> power_on -> (wait 'sys init over') -> USB_Blast_on")
print(f"Then check the log: python {os.path.basename(__file__)} log")
# ------------------------------------------------------------------------------- log
def cmd_log(args):
imx = connect(IMX_HOST, IMX_USER, IMX_PASS)
try:
cmd = f"tail -n {args.lines} {IMX_LOG}"
if args.grep:
cmd += f" | grep -E '{args.grep}'"
_, out, _ = run(imx, cmd, check=False, quiet=True)
print(out)
finally:
imx.close()
print("\nHealthy markers: TALISE_radioOn / 'fb init state ok' / fpga[0xBB]=0x03")
# ------------------------------------------------------------------------------ main
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
def add_build_args(p):
p.add_argument("--repo", default=REPO)
p.add_argument("--source", choices=["git", "worktree"], default="git",
help="git (default, reproducible) packages HEAD; worktree packages uncommitted edits")
p.add_argument("--make-args", default="", help="extra args passed to make on the VM")
p.add_argument("--out", default=os.path.join(tempfile.gettempdir(), BINARY))
b = sub.add_parser("build", help="cross-compile on the VM and pull the binary back")
add_build_args(b)
d = sub.add_parser("deploy", help="push a local binary to the imx6 and swap it in")
d.add_argument("binary", nargs="?", default=os.path.join(tempfile.gettempdir(), BINARY))
bd = sub.add_parser("build-deploy", help="build then deploy")
add_build_args(bd)
lg = sub.add_parser("log", help="tail the app log on the device")
lg.add_argument("-n", "--lines", type=int, default=80)
lg.add_argument("-g", "--grep", help="extended regex to filter with")
args = ap.parse_args()
ensure_legacy_paramiko() # after parsing, so --help never triggers a pip install
if args.cmd == "build":
cmd_build(args)
elif args.cmd == "deploy":
cmd_deploy(args)
elif args.cmd == "build-deploy":
args.binary = cmd_build(args)
cmd_deploy(args)
elif args.cmd == "log":
cmd_log(args)
if __name__ == "__main__":
main()