内外Flash
两种主流Flash
DUT(Device Under Test,被测设备)这两种启动模式,本质上对应了 NOR Flash 和 MMC/NAND Flash 两种主流存储芯片的区别。
一、核心概念:什么是 Flash?
Flash 是一种 非易失性存储器,断电后数据不会丢失。你可以把它理解为 U盘的底层技术。
在嵌入式设备(比如你的 DUT)里,它主要用来存放:
- Bootloader(引导程序):就像电脑的 BIOS,负责初始化硬件、引导操作系统。
- 固件/操作系统:设备真正的功能代码。
- 文件系统:存放配置文件、日志等。
Flash 主要分两种,而你的 DUT 正好各用了一种。
二、两种关键的 Flash 技术
| 特性 | NOR Flash | NAND Flash |
|---|---|---|
| 读取方式 | 支持 XIP (eXecute In Place),CPU 可以直接从 Flash 里运行代码,无需拷贝到内存。 | 不支持 XIP,必须把里面的代码先拷贝到 RAM(内存)里才能运行。 |
| 接口 | SPI (串行) 或 并行接口。引脚少,简单。 | 并行接口 或 MMC/eMMC 协议。引脚多,复杂。 |
| 容量 | 小(通常 1MB ~ 256MB) | 大(通常 128MB ~ 几十 GB) |
| 成本 | 贵(每 MB 单价高) | 便宜 |
| 可靠性 | 几乎没有坏块,出错率低。 | 天生可能有坏块,需要软件管理(坏块管理、ECC 纠错)。 |
| 主要用途 | 存储 Bootloader 和关键的启动代码。 | 存储 操作系统、文件系统、用户数据。 |
简单记忆:NOR = 代码少、贵、稳定、可直接运行。NAND = 容量大、便宜、不能直接运行。
三、对照你的两种启动模式
现在你再看 DUT 的两种模式,就非常清晰了:
模式 1:External Flash (外部 SPI-NOR Flash)
- 硬件位置:在 接口板 上(不在主 DUT 芯片内部)。
- 存储内容:FAAP 固件。一个非常精简的测试固件。
- 启动流程:
- DUT 内部芯片上电后,根据硬件引脚(Boot Pin)的配置,决定先从外部 SPI 接口读取代码。
- 因为连接的是 NOR Flash,支持 XIP,CPU 可以直接在 NOR Flash 上运行 FAAP 固件。
- 这个 FAAP 固件的主要任务:初始化内存、网卡等,然后通过网络或其它接口,把完整的、正式的操作系统/固件下载并写入到内部的 MMC Flash 中。
- 使用场景:首次加载或 紧急恢复。比如出厂时,内部 Flash 还是空的;或者内部 Flash 里的系统坏了,变砖了,可以通过这个外部 NOR 启动一个“救援系统”来修复。
模式 2:Internal Flash (内部 MMC Flash)
- 硬件位置:集成在 DUT 主芯片内部,或者是贴在主 PCB 上的 eMMC 芯片。
- 存储内容:完整的操作系统(Linux/RTOS)、应用程序、配置文件等。
- 启动流程:
- DUT 内部的 BootROM(芯片内部固化的只读存储器,无法修改)先运行。
- BootROM 根据启动引脚,决定从内部 MMC 接口读取。
- 关键点:MMC Flash 是 NAND 类型的,不支持 XIP。所以 BootROM 会先从 MMC 的前几个扇区读取 二级 Bootloader(比如 U-Boot SPL)到芯片内部的小 RAM 里运行。
- 这个二级 Bootloader 再初始化外部大容量的 DDR 内存,然后把完整的 U-Boot 从 MMC 里读到 DDR 中运行。
- U-Boot 最后加载 Linux 内核和文件系统。
- 使用场景:正常运行模式。速度快、容量大(几 GB 到几十 GB),可以存储复杂的系统和海量数据。
- BootROM(芯片内部固化的只读存储器)
- 是什么:芯片(比如 CPU/MCU)内部的一小块 只读存储器,在芯片出厂时就已经写死了程序,你无法修改它。
- 作用:芯片上电后,硬件自动执行的第一段代码就是 BootROM 里的程序。它的任务很有限:
- 初始化最基本的硬件(比如时钟、一些引脚)。
- 根据外部引脚(BOOT 引脚)的电平高低,决定从哪个接口(SPI、MMC、UART 等)去读取下一阶段的启动代码。
- 把这个下一阶段的代码(很小,通常是几 KB 到几十 KB)复制到芯片内部的 SRAM(很小的一块内存)里,然后跳转过去执行。
- 类比:就像电脑主板上的 BIOS 芯片,但 BootROM 是集成在 CPU 内部的,而且更小、功能更简单。
你不需要修改 BootROM,它只是固定的“启动引导器”。
- 二级 Bootloader(例如 U-Boot SPL)
- 是什么:SPL = Secondary Program Loader(二级程序加载器)。它是 U-Boot 的一个极简版本,体积很小(几十 KB),能够被 BootROM 加载到内部 SRAM 里运行。
- 为什么需要它:BootROM 能访问的硬件有限,而且内部 SRAM 很小(可能只有几十 KB)。但完整的 U-Boot 通常有几百 KB,放不进内部 SRAM。所以需要一个“二级加载器”来做两件事:
- 初始化外部内存控制器,让 CPU 能够访问 DDR 内存(容量大,几百 MB 到几 GB)。
- 把 完整的 U-Boot 从存储介质(比如 eMMC、SD 卡、网络)读到 DDR 内存中。
- 执行完后:跳转到 DDR 中完整的 U-Boot 代码,继续运行。
- DDR 内存(Double Data Rate SDRAM)
- 是什么:就是我们常说的 内存条 上的那种内存芯片,但在嵌入式设备里是直接焊在板子上的。
- 特点:
- 容量大(256 MB、512 MB、1 GB 甚至更大)。
- 速度很快(CPU 可以直接从这里取指令、读写数据)。
- 易失性:断电后数据全部丢失。
- 为什么启动时要初始化它:上电时,DDR 控制器还没有配置,CPU 无法访问 DDR。只有二级 Bootloader 配置好控制器后,DDR 才能用。之后完整的 U-Boot 和 Linux 内核都要在 DDR 里运行。
简单记:DDR 就是设备的“运行内存”,程序运行时都在里面。
- U-Boot(Universal Bootloader)
- 是什么:一个非常流行的 开源引导程序,用于嵌入式 Linux 设备。它功能很强,类似电脑上的 GRUB。
- 作用:
- 初始化更多的硬件(网卡、USB、显示等)。
- 提供一个命令行界面(通过串口),你可以敲命令来:
- 查看/修改环境变量。
- 从网络(TFTP)、eMMC、USB 等加载 Linux 内核。
- 烧写固件。
- 最终负责把 Linux 内核 从存储介质(eMMC)读到 DDR 内存里,并跳转执行。
- 在启动流程中的位置:二级 Bootloader(SPL)加载 U-Boot → U-Boot 加载 Linux 内核。
- Linux 内核
- 是什么:操作系统的核心。它管理所有硬件(CPU、内存、硬盘、网卡等),并为应用程序提供运行环境。
- 在启动流程中:U-Boot 会把内核镜像(比如
zImage或Image)从 eMMC 读到 DDR 的某个位置,然后跳转过去执行。内核开始启动,初始化各种驱动,最后挂载根文件系统。
- 根文件系统(Root Filesystem)
- 是什么:一个目录结构(比如
/bin,/etc,/home),包含了所有用户程序、配置文件、库文件等。通常被做成一个镜像(如rootfs.ext4)存储在 eMMC 的某个分区里。 - 内核启动的最后一步:内核会找到并挂载这个根文件系统,然后执行第一个用户进程(通常是
/sbin/init)。之后你就能看到 shell 命令行,或者系统启动图形界面。
简单记:内核是操作系统的大脑,根文件系统是操作系统存放“知识”和“工具”的硬盘。
整个流程(把名词串起来)
[上电]
→ BootROM(芯片内部固定程序)运行
→ 根据启动引脚,决定从 eMMC 读取
→ 把 eMMC 前几个扇区的“二级 Bootloader(U-Boot SPL)”读到 内部 SRAM
→ SPL 初始化 DDR 内存控制器
→ SPL 把完整的 U-Boot 从 eMMC 读到 DDR
→ U-Boot 运行(显示命令行,或自动启动)
→ U-Boot 把 Linux 内核镜像从 eMMC 读到 DDR
→ U-Boot 跳转到内核入口
→ Linux 内核启动,挂载 eMMC 上的根文件系统
→ 执行 /sbin/init → 系统正常启动
一张小表总结这些名词
| 名词 | 本质 | 存储位置 | 大小 | 作用 |
|---|---|---|---|---|
| BootROM | 固化的只读程序 | CPU 内部 | 几 KB | 上电最先执行,决定启动源,加载 SPL |
| 二级 Bootloader (SPL) | U-Boot 的简化版 | eMMC 起始扇区 | 几十 KB | 初始化 DDR,加载完整 U-Boot |
| DDR | 运行内存(RAM) | 板上的内存芯片 | 几百 MB 起 | 存放运行中的代码和数据 |
| U-Boot | 功能强大的引导程序 | eMMC 的某个分区 | 几百 KB | 加载内核,提供命令行烧录/调试 |
| Linux 内核 | 操作系统核心 | eMMC 的独立分区 | 几 MB ~ 几十 MB | 管理硬件,挂载文件系统 |
| 根文件系统 | 系统文件和程序集合 | eMMC 的独立分区 | 几十 MB ~ 几 GB | 提供用户环境和应用程序 |
四、为什么要有两种模式?
你可能想问,为什么不只用内部 MMC Flash?既大又快。
答案:安全性、可恢复性和成本权衡。
- 变砖恢复:如果内部 MMC Flash 里的 Bootloader 或者系统刷坏了,设备上电后无法启动。这时候如果没有外部 NOR Flash 作为“备份启动源”,设备就彻底“变砖”了,只能返厂用烧录器重新烧写。有了外部 NOR,你可以强行从它启动,然后重新刷写内部 MMC。
- 首次烧录:工厂生产线上,内部 MMC 是空白的。总不能先拆开用编程器写吧?成本太高。一般是让设备从外部 NOR 启动一个最小的烧录固件(就是你提到的 FAAP),然后这个固件通过网口或 USB,把完整的系统镜像烧录进内部 MMC。
- 成本与性能:NOR 贵但稳,用来存关键的“救命代码”(几 MB 就够)。NAND/MMC 便宜容量大,用来存复杂的“业务代码”。
五、总结与建议
| External Flash (SPI-NOR) | Internal Flash (eMMC) | |
|---|---|---|
| 技术类型 | NOR Flash | NAND Flash (via MMC接口) |
| 是否支持XIP | 是 | 否 (需拷贝到RAM) |
| 容量 | 小 (MB级) | 大 (GB级) |
| 内容 | 最小启动固件 (FAAP) | 完整操作系统/文件系统 |
| 作用 | 引导加载、救援、首次烧录 | 正常运行、存储数据 |
| 类比PC | 主板上的 BIOS ROM 芯片 | 硬盘 (SSD/HDD) |
- 如何切换模式? 去看 DUT 的原理图,找
BOOT_MODE或类似名字的引脚、拨码开关或寄存器。硬件上通常通过拉高/拉低几个引脚的电平来决定从哪个 Flash 启动。 - 如何烧写? 在 External Flash 模式下,FAAP 固件提供了什么命令或界面(比如串口命令行、TFTP 服务)来烧写 Internal Flash?这是你写测试用例时要调用的接口。
- 验证方法: 如何从软件上确认当前是从哪种模式启动的?比如读取某个寄存器,或者检查
/proc/mtd、/proc/partitions的差异。
明白了,你希望我把 NOR Flash 和 NAND Flash 这两者单独拎出来,做一次更聚焦、更详细的对比讲解。没问题,下面我专门为你展开。
NOR Flash vs NAND Flash
下面从 5 个关键维度 帮你彻底分清。
1. 内部结构
为什么 NOR 可以“直接执行”,NAND 不行?
-
NOR Flash
每个存储单元并联在位线上,可以像 NOR 逻辑门 一样独立寻址。
→ 支持 XIP (Execute In Place):CPU 通过 SPI 或并行接口发送一个地址,NOR 直接返回该地址的数据。不需要把代码复制到 RAM,CPU 可以直接在 NOR 上取指执行。 -
NAND Flash
存储单元串联成串,只能以 页 (Page) 为单位访问,无法按字节随机读取单个地址。
→ 不支持 XIP:必须先把整个页(通常是 2KB/4KB)读到 RAM 里,再从 RAM 执行。
一句话:NOR 能“随地大小读”,NAND 只能“整页整页搬”。
2. 接口与引脚
| 类型 | 常见接口 | 引脚数 | 复杂度 |
|---|---|---|---|
| NOR | SPI(串行)或并行 | SPI 仅 4~6 根线 | 简单 |
| NAND | 并行(8/16位)或 MMC/eMMC | 并行 >20 根线 | 复杂 |
- 你项目里的 External Flash 是 SPI-NOR:引脚少,板子布线容易,适合放 Bootloader。
- Internal MMC Flash 实际上是 eMMC,内部是 NAND + MMC 控制器,对外是 MMC 协议。
3. 读写性能与寿命
| 特性 | NOR | NAND |
|---|---|---|
| 随机读取 | 极快(微秒级,直接地址访问) | 慢(需先加载整页) |
| 连续读取 | 较慢(SPI 时钟限制) | 快(页模式,带宽高) |
| 写入/擦除 | 慢(按字节写,擦除块大) | 快(按页写,擦除块适中) |
| 擦写寿命 | 10k ~ 100k 次 | 1k ~ 10k 次(但 eMMC 有磨损均衡,实际寿命更长) |
- 写入慢 是 NOR 的一大缺点。你往外部 NOR 烧写几百 KB 的 FAAP 固件可能还不明显,但如果写几 MB 就会很慢。
- NAND 写入快,适合频繁更新系统、存储日志。
4. 坏块管理
- NOR Flash:几乎不存在出厂坏块,使用中也很少出现坏块。软件可以完全信任它。
- NAND Flash:天生可能有坏块(生产时就有)。而且使用中会不断产生新坏块。
→ 必须由软件(或 eMMC 控制器)做 坏块管理 和 ECC 纠错。
这就是为什么 裸 NAND 很难用,而 eMMC 把控制器集成进去后,对主 CPU 就透明了——控制器自动处理坏块和 ECC。
5. 典型用途
| 场景 | 用什么 | 原因 |
|---|---|---|
| 存储 Bootloader(几 KB ~ 几 MB) | NOR Flash | 支持 XIP,上电就能跑,可靠,无需复杂驱动 |
| 首次烧录 / 救援系统 | NOR Flash | 安全,不容易变砖 |
| 存储 Linux 内核 + 根文件系统(几十 MB ~ GB) | NAND Flash (eMMC) | 容量大,成本低,写入快 |
| 存储用户数据、日志、配置 | NAND Flash (eMMC) | 同上 |
你的 DUT:
- External SPI-NOR → 放 FAAP 固件(Bootloader / 救援程序)
- Internal eMMC → 放完整的操作系统和应用
总结对比
| 对比项 | NOR Flash | NAND Flash |
|---|---|---|
| 内部结构 | 并联,字节可寻址 | 串联,页访问 |
| 是否支持 XIP | ✅ 支持(可直接运行代码) | ❌ 不支持(需拷贝到 RAM) |
| 典型容量 | 1 MB ~ 256 MB | 128 MB ~ 64 GB+ |
| 读取速度(随机) | 快 | 慢(相对) |
| 读取速度(连续) | 慢 | 快 |
| 写入/擦除速度 | 慢 | 快 |
| 擦写寿命 | 10k~100k 次 | 1k~10k 次(裸片),eMMC 控制器可改善 |
| 坏块 | 几乎没有 | 天生存在 + 后天产生 |
| 需要软件管理 | 不需要(简单可靠) | 必须(坏块管理、ECC) |
| 成本(每 MB) | 高 | 低 |
| 典型接口 | SPI / Parallel | Parallel / eMMC / UFS |
形象的比喻
-
NOR Flash 像一个 小本子(几十页)。
你可以直接翻开任意一页读,也能直接在那页写字(但擦掉重写很慢)。
适合放最重要的“开机步骤”。 -
NAND Flash 像一个 大仓库(几万箱货物)。
你不能直接去某一箱里取一个螺丝,必须先把一整箱搬到工作台上(读到 RAM),再从箱里拿。
适合大量存储。 -
eMMC 则是在这个仓库门口配了一个 管理员(控制器)。
你只需要告诉管理员“我要仓库里的第 100 箱”,管理员会帮你把整箱搬出来、处理坏箱子、记下哪些箱子坏了。你根本不知道仓库内部有多麻烦。
再回看两种启动模式
-
External Flash 模式:
上电 → CPU 通过 SPI 直接读取 NOR 上的 FAAP 固件(XIP 执行) → 初始化硬件 → 准备烧写内部 eMMC。 -
Internal Flash 模式:
上电 → CPU 内部的 BootROM 从 eMMC 的前几个扇区读取二级 Bootloader 到内部 RAM → 二级 Bootloader 初始化 DDR → 把完整 U-Boot 从 eMMC 读到 DDR → U-Boot 从 eMMC 加载内核 → 正常启动。
MMC
MMC 是 MultiMediaCard(多媒体卡)的缩写。
它是一种 存储卡/接口标准,由西门子和 SanDisk 在 1997 年推出。你问题里的 MMC Flash 通常不是指可插拔的 MMC 卡,而是它的嵌入式版本——eMMC(embedded MultiMediaCard,嵌入式多媒体卡)。
简单理解:
- MMC 本身是一种 物理外形 + 通信协议(类似于 SD 卡,SD 卡就是基于 MMC 发展而来的)。
- 在嵌入式设备(你的 DUT)中,工程师不会真的插一张 MMC 卡,而是直接把 MMC 接口的控制器和 NAND Flash 芯片 封装在一起,焊接在主板上,这就是 eMMC。
为什么叫 eMMC 而不直接叫 NAND?
因为裸的 NAND Flash(你前面学过的)用起来很麻烦:需要坏块管理、ECC 纠错、磨损均衡……这些如果让主 CPU 来做,会非常消耗资源和开发时间。
eMMC 内部 = MMC 控制器 + NAND Flash
主 CPU 只需要通过 MMC 协议(与 SD/MMC 卡完全相同的命令)去读写 eMMC 芯片,控制器会自动处理 NAND 底层的脏活累活。对软件来说,eMMC 就像一个简单的块设备(类似硬盘)。
你代码/文档里看到的 “MMC Flash”:
大概率就是指 eMMC,也就是 DUT 自带的、用于正常启动和存储文件系统的那片内部 Flash。
一句话总结:
MMC 是一种存储卡/接口协议;MMC Flash 在嵌入式语境下通常指 eMMC——它把 MMC 接口控制器和 NAND Flash 封装在一起,让 CPU 可以像操作硬盘一样简单地使用大容量 NAND,而不用关心底层的坏块和管理细节。
Flash 启动模式详解
1. 项目中的两种 Flash 硬件
DUT(被测无线电设备)涉及两种物理 Flash:
1.1 SPI-NOR Flash(外部 Flash)
- 物理位置:焊在接口板(NIB 或 TCPE3)上,不在 DUT 本体上
- 类型:NOR Flash,通过 SPI 总线与 DUT 连接
- 特点:容量小(通常几 MB ~ 几十 MB),但支持字节级随机读取,CPU 可以直接从上面执行代码
- 用途:当 DUT 内部 Flash 为空(全新板子)或损坏时,DUT 从这块外部 Flash 启动,加载 FAAP 工厂固件
1.2 eMMC Flash(内部 Flash)
- 物理位置:在 DUT 自身的 PCB 上
- 类型:NAND Flash + 控制器(MMC 接口)
- 特点:容量大,存储正式的运行固件
- 用途:正常运行模式,DUT 从自身 eMMC 启动
2. 两套项目的架构差异
ci-tools-pro 和 test-interface-client 做的是同一件事(控制接口板、切换 Flash 启动模式、加载固件),但架构完全不同:
| 维度 | ci-tools-pro | test-interface-client |
|---|---|---|
| 通信方式 | 直接 Telnet/SSH/Serial 连接硬件 | 通过 gRPC 调用接口板上的微服务 |
| 控制粒度 | 手动操作每个 GPIO 引脚 | 发送高层语义命令,服务端处理 GPIO |
| 固件传输 | FTP 上传 + FT4222 SPI 编程 | gRPC 双向流式传输 |
| 架构模式 | 脚本直连硬件(胖客户端) | 客户端-服务端分离(瘦客户端) |
架构对比图:
ci-tools-pro 的方式(直连):
┌──────────┐ Telnet/SSH ┌──────────┐ GPIO/SPI ┌─────┐
│ 测试 PC │ ────────────→ │ NIB/TCPE │ ─────────→ │ DUT │
│ (Python) │ 直接控制引脚 │ (接口板) │ │ │
└──────────┘ └──────────┘ └─────┘
test-interface-client 的方式(gRPC 微服务):
┌──────────┐ gRPC:50051 ┌──────────────────┐ GPIO/SPI ┌─────┐
│ 测试 PC │ ────────────→ │ 接口板 gRPC 服务 │ ─────────→ │ DUT │
│ (Python) │ 语义化命令 │ (Raptor2 平台) │ │ │
└──────────┘ └──────────────────┘ └─────┘
3. 启动模式切换的对比
3.1 ci-tools-pro:手动 GPIO 操作
TCPE3 接口板(Hawkowl/Mongoose 产品)
在 common/tcpe3.py 中,TCPE3 的切换相对简单:
# tcpe3.py — 直接发 Telnet 命令
def tcpe_swtich_to_external_flash(logger, tcpe_telent_connection):
tcpe_telent_connection.write("bm ext", "COMMAND_OK")
def tcpe_swtich_to_internal_flash(logger, tcpe_telent_connection):
tcpe_telent_connection.write("bm int", "COMMAND_OK") # bm = boot mode
NIB 接口板(Krypton 产品)
在 test_case/nib_case.py 中,需要逐个操作 GPIO 引脚:
# nib_case.py — 逐个操作 GPIO 引脚
def nib_swtich_to_external_flash(nib_telnet_connection, nib_ssh_connection, logger):
# 通过 TCPE 命令设置 IO 引脚
nib_telnet_connection.write("tcpe o01 1", "COMMAND_OK") # 设置输出引脚 01 为高电平
nib_telnet_connection.write("tcpe i01", "1") # 验证读回来也是 1
# 通过 SSH 控制 NIB 上的 GPIO
nib_ssh_connection.ssh_exec_cmd("gpio mode 7 out") # GPIO 7 设为输出模式
nib_ssh_connection.ssh_exec_cmd("gpio mode 13 out") # GPIO 13 设为输出模式
nib_ssh_connection.ssh_exec_cmd("gpio write 13 0") # GPIO 13 拉低
# 通过 FT4222 芯片控制 SPI 总线上的 GPIO
nib_telnet_connection.write("ft4222_gpio_write 3 0", "COMMAND_OK") # 复位脉冲
nib_telnet_connection.write("ft4222_gpio_write 3 1", "COMMAND_OK")
nib_telnet_connection.write("tcpe o04 1", "COMMAND_OK") # 使能外部 Flash 通路
nib_telnet_connection.write("ft4222_gpio_write 2 0", "COMMAND_OK") # SPI 片选切到外部
nib_ssh_connection.ssh_exec_cmd("gpio write 7 1") # GPIO 7 拉高 → 选择外部 Flash
切换到内部 Flash 则是反向操作:
def nib_swtich_to_internal_flash(nib_telnet_connection, nib_ssh_connection, logger):
nib_telnet_connection.write("tcpe o01 0", "COMMAND_OK") # 引脚拉低
nib_ssh_connection.ssh_exec_cmd("gpio write 7 0") # GPIO 7 拉低 → 选择内部 Flash
nib_telnet_connection.write("tcpe o04 0", "COMMAND_OK") # 关闭外部 Flash 通路
nib_telnet_connection.write("ft4222_gpio_write 2 1", "COMMAND_OK") # SPI 片选切回内部
关键点:这些 GPIO 引脚控制的是一个硬件多路选择器(MUX),决定 DUT 的 SPI 总线连接到哪块 Flash。
3.2 test-interface-client:gRPC 语义化调用
# service_client/mode_select.py
_BOOT_MODE_MAP = {
"BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL":
common_enums_pb2.BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL,
"BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL":
common_enums_pb2.BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL,
}
def select(self, mode: str, timeout: float = 10.0) -> bool:
request = pb2.ModeSelectRequest(
session=_create_session(),
dut_position=self.dut_position,
boot_mode=boot_mode_enum, # 只需传枚举值
)
response = self.mode_select_stub.Select(request, timeout=timeout)
客户端只需要说"我要外部启动"或"我要内部启动",具体哪些 GPIO 引脚怎么拉高拉低,由接口板上的 gRPC 服务根据 Profile 配置自动处理。
3.3 Profile 配置中的启动模式定义
Profile 配置文件(Raptor2.TestInterface.AIR3286.Configuration.json)定义了每种启动模式对应的 GPIO 状态:
"BootModeConfigurations": [
{
"Mode": "EnterDutBootModeInternal",
"IOSettings": [
{"IoName": "GPIO_1", "State": true},
{"IoName": "GPIO_2", "State": true},
{"IoName": "GPIO_3", "State": true},
{"IoName": "GPIO_22", "State": false}
]
},
{
"Mode": "EnterDutBootModeExternal",
"IOSettings": [
{"IoName": "GPIO_1", "State": false},
{"IoName": "GPIO_2", "State": true},
{"IoName": "GPIO_3", "State": true},
{"IoName": "GPIO_22", "State": true}
]
}
]
注意:
GPIO_22的别名是FLASH_SWITCH,这就是控制 Flash 切换的关键引脚。外部启动时拉高(true),内部启动时拉低(false)。
4. 固件加载(DZ Loading)的对比
4.1 ci-tools-pro:FTP + FT4222 SPI 编程
# nib_case.py
def ft4222LoadingFile(nib_telent_connection, ssh_connection, filename, address, ...):
ftp_upload(filename, 'etsw/', NIB_IP_ADDRESS, NIB_FTP_DEFAULT_PATH) # FTP 上传到 NIB
nib_telent_connection.write(f"ft4222_flash_load {base_name} {address}", "COMMAND_OK") # SPI 写入
nib_telent_connection.repeat_write("ft4222_status?", "FT4222_OK", timeout) # 轮询等待完成
ssh_connection.write(f"ft4222_flash_dump {address} 32", want_str) # dump 前 32 字节验证文件头
FAAP 完整加载流程(ft4222_load_faap()):
def ft4222_load_faap(nib_telnet_connection, nib_ssh_connection, config):
# 1. 预配置 FT4222 芯片(USB-SPI 桥接芯片)
ft4222_preconfig(nib_telnet_connection)
# 2. 擦除整个 Flash(地址 0 到 0,表示全片擦除)
nib_telnet_connection.write("ft4222_flash_erase 0 0", "COMMAND_OK", 1200)
# 3. 解析 FAAP DZ 固件包
loading_info = parse_dz_package(zip_dir, product_number, r_state)
# 4. 按顺序加载 8 个组件到 Flash 的指定地址
load_order = [
("SCT", "31 54 43 53"),
("FLASHIMG", "45 48 44 52"),
("PBOOT", "45 48 44 52"),
("SBOOT", "45 48 44 52"),
("PRODUCTION_PARAMETERS", "d0 0d fe ed"),
("TRUSTED_ANCHOR", "54 52 43 45"),
("XCS_CONFIG", "45 48 44 52"),
("AUAPPLIC", "45 48 44 52"),
]
for swtype, want_str, timeout in load_order:
ft4222LoadingFile(...) # 逐个写入 Flash
4.2 test-interface-client:gRPC 双向流式传输
# service_client/software_repository_client.py
def _send_to_server(self, sw_files, dut_position):
# 1. 发送所有 SoftwareItem 元数据(文件名、MD5、大小、类型)
for sw_type, file_path, item in items_info:
req = pb2.SoftwareRequest(session=session, dut_position=dut_position, item=item)
request_queue.put(req)
# 2. 发送 SoftwareItemsFinalized 信号
request_queue.put(req_finalized)
# 3. 服务端按需请求上传(服务端决定加载顺序)
for response in call:
if msg_type == "sw_item_upload_request":
# 服务端说"给我 PBOOT",客户端就分块上传 PBOOT
with open(file_path, "rb") as f:
while chunk := f.read(1024 * 1024): # 1MB 分块
content_req = pb2.SoftwareRequest(
content=pb2.SoftwareItemContent(swType=item.sw_type, data=chunk)
)
request_queue.put(content_req)
elif msg_type == "sw_preparations_done":
# 服务端说"全部写入完成"
break
这是一个典型的 gRPC 双向流(bidirectional streaming)协议:
- 客户端先告诉服务端"我有这些固件文件"(发送元数据)
- 服务端按自己的顺序逐个请求"把 PBOOT 传给我"
- 客户端分块上传文件内容(每块 1MB)
- 服务端收到后自动写入 Flash 对应地址
- 全部完成后服务端返回
sw_preparations_done
4.3 固件地址映射
两个项目都使用 flashmap.json 来定义固件组件在 Flash 中的地址布局。
ci-tools-pro 中的解析(common/parse_dz_package.py):
def get_loading_address(flashmap, swtype):
for area in flashmap["SoftwareAreaConfigurations"]:
if area["SwType"] == swtype:
return area["StartAddress"]
test-interface-client 中的解析(service_client/dz_container_parser.py):
_FLASHMAP_SW_TYPE_MAP = {
"Pboot": SOFTWARE_TYPE_PBOOT,
"Sboot": SOFTWARE_TYPE_SBOOT,
"Faap": SOFTWARE_TYPE_FAAP,
"SlotContentTable": SOFTWARE_TYPE_SLOT_CONTENT_TABLE,
"XcsConfig": SOFTWARE_TYPE_XCS_CONFIG,
"InitialFlashImage": SOFTWARE_TYPE_INITIAL_FLASH_IMAGE,
"ProductionParameters": SOFTWARE_TYPE_PRODUCTION_PARAMETERS,
"ProductionDb": SOFTWARE_TYPE_PRODUCTION_DB,
"TrustedAnchor": SOFTWARE_TYPE_TRUSTED_ANCHOR,
}
Flash 地址空间布局(以 AIR3286 为例):
Flash 地址空间 (SPI-NOR Flash)
┌─────────────────────────┐ 0x00000000
│ SlotContentTable (SCT) │ 256KB ← 安全配置表
├─────────────────────────┤ 0x00040000
│ (reserved) │
├─────────────────────────┤ 0x00080000
│ InitialFlashImage │ 256KB ← Flash 镜像(分区表等元数据)
├─────────────────────────┤ 0x000C0000
│ Pboot │ 512KB ← 主引导程序(CPU 上电后第一个执行的代码)
├─────────────────────────┤ 0x00140000
│ Sboot │ 256KB ← 次引导程序(加载 Linux 内核)
├─────────────────────────┤ 0x00180000
│ XcsConfig │ 512KB ← 硬件配置(设备树 DTB)
├─────────────────────────┤ 0x00200000
│ ProductionParameters │ 256KB ← 产品参数
├─────────────────────────┤ 0x00240000
│ TrustedAnchor │ 256KB ← 可信锚点(安全证书)
├─────────────────────────┤
│ ... │
├─────────────────────────┤ 0x00A00000
│ ProductionDb │ 1.25MB ← 生产数据库
├─────────────────────────┤ 0x00B40000
│ Faap (AUAPPLIC) │ ~245MB ← 应用程序(最大的组件)
└─────────────────────────┘
5. 完整工作流对比
5.1 ci-tools-pro 的外部启动流程
以 external_boot_faap_via_tcpe3.py 为例:
┌──────────────────────────────────────────────────────────────────┐
│ 第1步:准备阶段 │
│ │
│ config = ProductConfigManager.get_product_config('AIR1672') │
│ → 加载产品配置(固件路径、接口板IP、电源参数等) │
│ → 解析命令行参数(restart_mode、slot 等) │
│ → 加载固件文件(ELF、SWDB、SQLite、DC) │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第2步:切换到外部 Flash 并上电 │
│ │
│ power_supply.power_off() # 先断电 │
│ tcpe_swtich_to_external_flash() # bm ext │
│ power_supply.power_on() # 再上电 │
│ │
│ ⚠️ 顺序很重要!必须先断电再切换,否则外部 Flash 会被写入脏数据 │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第3步:DUT 从外部 Flash 启动 │
│ │
│ DUT CPU 上电后,从 SPI-NOR Flash 读取 bootloader (PBOOT/SBOOT) │
│ → 加载 FAAP 固件 → 启动 Linux 系统 │
│ → 出现 "mongoose-dc login" 提示符 │
│ │
│ DUT_connection = TelnetConnection(..., "mongoose-dc login") │
│ → 通过 TCPE3 的 Telnet 端口 3001 连接 DUT 串口 │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第4步:建立 SSH 连接,传输固件文件到 DUT │
│ │
│ Radio.__init__() 内部: │
│ → __bootstrap_ssh() # 通过串口获取 DUT IP,建立 SSH │
│ → __transfer_elf() # SCP 传输 Radio 应用 ELF 文件 │
│ → __transfer_swdb() # SCP 传输 SWDB 数据库 │
│ → __transfer_dc() # SCP 传输 DC 配置文件 │
│ → MD5 校验确保文件完整 │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第5步:从外部 Flash 切换到内部 Flash(关键步骤) │
│ │
│ tcpe_switch_2_internal_flash() 内部: │
│ │
│ 1. DUT_connection.write("cat /proc/mtd") │
│ → 查看当前 MTD 分区表(此时看到的是外部 Flash 的分区) │
│ │
│ 2. tcpe_swtich_to_internal_flash() │
│ → bm int,SPI 总线从外部 Flash 切到内部 eMMC │
│ │
│ 3. DUT_connection.write("flash-hotswap internal") │
│ → 热切换命令,让 Linux 内核重新识别 Flash 设备 │
│ → 类似"热插拔",不需要重启 DUT │
│ │
│ 4. DUT_connection.write("prepare_onboard_flash ...") │
│ → 初始化内部 Flash 的分区表和文件系统 │
│ → 把固件从 RAM 写入内部 eMMC │
│ │
│ 5. DUT_connection.write("source /run/env/mtdenv") │
│ → 加载新的 MTD 环境变量 │
│ │
│ 6. DUT_connection.write("cat /proc/mtd") │
│ → 再次查看分区表,确认已切换到内部 Flash │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第6步:启动 Radio 应用并执行测试 │
│ │
│ radio.startup_mama() │
│ → 执行 mamastart 命令 │
│ → DUT 进入正常工作状态 │
│ → 执行 RF 校准测试 (PA → RX cal → RX perf → TX) │
└──────────────────────────────────────────────────────────────────┘
5.2 test-interface-client 的外部启动流程
以 main.py 配合 AIR3286_config.yaml 为例:
┌──────────────────────────────────────────────────────────────────┐
│ 第1步:加载配置 │
│ │
│ config = load_board_config('AIR3286') │
│ → 读取 configuration/AIR3286_config.yaml │
│ → 获取 gRPC 地址、DZ 固件路径、GPIO 配置、启动模式等 │
│ │
│ channel = grpc.insecure_channel("192.168.2.71:50051") │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第2步:上传硬件配置 Profile │
│ │
│ gRPC → ConfigurationService.SetProfile() │
│ → 上传 Raptor2.TestInterface.AIR3286.Configuration.json │
│ → 告诉服务端:GPIO 方向、UART 波特率、启动模式 GPIO 映射、 │
│ Flash 地址布局等 │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第3步:DZ 固件加载 │
│ │
│ gRPC → SoftwareRepositoryService.InitiateBootSoftwareRequest() │
│ → 解析 manifest.yaml + flashmap.json │
│ → 双向流式传输固件文件到接口板 │
│ → 服务端自动写入 Flash 对应地址 │
│ → 等待 sw_preparations_done 确认 │
│ │
│ gRPC → SoftwareRepositoryService.CleanupAfterBoot() │
│ → 清理临时资源 │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第4步:GPIO 控制 │
│ │
│ gRPC → GPIOService.SetOutput() │
│ → 设置 TEST_JTAG_SELECT = LOW │
│ → 设置 DISABLE_WD_N = LOW │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第5步:选择启动模式 │
│ │
│ gRPC → ModeSelectService.Select() │
│ → mode = "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL" │
│ → 服务端根据 Profile 中的 BootModeConfigurations 自动设置 GPIO │
│ (GPIO_1=LOW, GPIO_2=HIGH, GPIO_3=HIGH, GPIO_22/FLASH_SWITCH=HIGH) │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 第6步:触发启动 & 检查连接 │
│ │
│ gRPC → TriggerService.SetTrigger(ENABLED) │
│ → 触发 DUT 上电启动 │
│ │
│ gRPC → DutConnectionService.DutConnected() │
│ → 检查 DUT 是否成功启动并在线 │
│ │
│ gRPC → InterfaceStatusService.GetStatus() │
│ → 获取接口板整体状态 │
└──────────────────────────────────────────────────────────────────┘
5.3 YAML 配置驱动
test-interface-client 的流程完全由 YAML 配置文件驱动,切换内外部启动只需改一行:
# AIR3286_config.yaml
# 外部启动
mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL"
# 内部启动(注释掉上面,用这个)
# mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL"
dz_loading:
dz_container_path: 'external\hawkowl\AIR3286_B25B66\...'
product_number: "KRD 901 316/2"
product_rstate: "R1A"
extra_sw_files:
ProductionParameters: 'external\hawkowl\AIR3286_B25B66\ppar_ext1.rif'
TrustedAnchor: 'external\hawkowl\AIR3286_B25B66\TA_dev.bin'
ProductionDb: 'external\hawkowl\AIR3286_B25B66\prod_db_...'
gpio:
outputs:
- alias: "TEST_JTAG_SELECT"
state: false
- alias: "DISABLE_WD_N"
state: false
6. Flash 操作中的关键细节
6.1 擦除后才能写入
# ci-tools-pro/test_case/nib_case.py
def ft4222_load(nib_logger, nib_telnet_connection, nib_ssh_connection, load_addr, load_file, check_head_str):
# 先计算文件大小,对齐到 256 字节(Flash 的页大小)
load_file_size = os.path.getsize(load_file)
if load_file_size % 256:
load_file_size = load_file_size + (256 - load_file_size % 256)
ft4222_erase(nib_telnet_connection, load_addr, load_file_size) # 先擦除
ft4222LoadingFile(...) # 再写入
Flash 的物理特性决定了必须先擦除(所有位变为 1),然后才能写入(把 1 变成 0)。而且擦除必须按块进行,写入按页(256 字节)对齐。
6.2 写入后验证
# ci-tools-pro — 手动 dump 验证文件头魔数
ssh_connection.write(f"ft4222_flash_dump {address} 32", want_str)
# test-interface-client — MD5 校验
file_hash = _calculate_md5(file_path)
item = pb2.SoftwareItem(hash=file_hash, total_size=file_size, ...)
ci-tools-pro 通过 dump Flash 前 32 字节与预期文件头魔数比对(如 45 48 44 52)来验证。test-interface-client 通过 MD5 哈希校验整个文件完整性。
6.3 断电切换的重要性
# ci-tools-pro/common/tcpe3.py
def tcpe_switch_2_external_flash(power_supply_com, interface_board_connection, logger):
power_supply_com.power_off()
sleep(2)
# must power off first, then switch, power on at last,
# or external flash will be written some other content
tcpe_swtich_to_external_flash(logger, interface_board_connection)
power_supply_com.power_on()
⚠️ 必须先断电再切换 Flash 模式,否则在切换瞬间 DUT 的 CPU 可能会向外部 Flash 写入垃圾数据。
7. 总结对比
| 维度 | ci-tools-pro | test-interface-client |
|---|---|---|
| 外部 Flash 切换 | bm ext (TCPE3) / GPIO 组合操作 (NIB) |
ModeSelect("EXTERNAL") gRPC 调用 |
| 内部 Flash 切换 | bm int + flash-hotswap |
ModeSelect("INTERNAL") gRPC 调用 |
| 固件传输方式 | FTP 上传 → FT4222 SPI 写入 | gRPC 双向流式传输 |
| 固件写入验证 | dump 文件头魔数比对 | MD5 哈希校验 |
| 配置管理 | Python 字典硬编码 (ProductConfigManager) |
YAML 配置 + JSON Profile |
| 加载顺序控制 | 客户端硬编码 load_order 列表 |
服务端按需请求(sw_item_upload_request) |
| 新增产品支持 | 修改 Python 代码 | 新增 YAML + JSON 配置文件 |
| 入口脚本 | bootup_dut_from_external_*.py / bootup_dut_from_internal_*.py |
main.py(统一入口,YAML 驱动) |
| 适用场景 | 快速原型、调试、NIB 专用操作 | 标准化生产测试、多产品支持 |
为什么要有两套系统
- ci-tools-pro 是早期的直连脚本方案,优点是简单直接,缺点是每种接口板需要写不同的 GPIO 操作代码,换产品可能要改一堆硬编码的引脚号,客户端需要知道底层硬件细节。
- test-interface-client 是新一代的 Raptor2 平台方案,接口板上运行 gRPC 微服务(端口 50051),把硬件操作抽象成标准化的 API。客户端不需要知道
GPIO_22是FLASH_SWITCH,只需要说"切到外部启动",服务端根据 Profile 配置自动处理。换产品只需要换一个 JSON 配置文件。
详细流程与配置参数对比
1. 核心区别概述
| 维度 | 外部启动 (External Boot) | 内部启动 (Internal Boot) |
|---|---|---|
| Flash 来源 | 接口板上的 SPI-NOR Flash | DUT 自身的 eMMC Flash |
| 适用场景 | DUT 内部 Flash 为空(全新/损坏),需首次烧录 | DUT 已有固件,正常运行或更新文件 |
| 是否需要 Flash 切换 | 需要:先切外部 → 启动 → 再切回内部 | 直接切到内部即可 |
| Radio 对象参数 | external_boot=True |
external_boot=False |
| 是否执行 mamastart | Radio 初始化时不执行,切回内部后单独调用 | Radio 初始化时直接执行 |
| 是否传输固件文件 | Radio 初始化时不传输,切回内部后由脚本处理 | Radio 初始化时根据 restart_mode 传输 |
2. 参数在内外部启动中的实际使用差异(ci-tools-pro)
这是最容易混淆的部分。很多参数虽然在命令行中都可以传入,但在外部启动和内部启动中的实际作用完全不同,有些参数在某种模式下根本不会被使用。
2.1 参数使用对照表
| 参数 | 外部启动是否使用 | 内部启动是否使用 | 说明 |
|---|---|---|---|
-r restart_mode |
⚠️ 传入但被忽略 | ✅ 核心参数 | 外部启动时 Radio 在 external_boot=True 处直接 return,restart_mode 对应的文件传输和 mamastart 全部跳过 |
-s slot |
⚠️ 传入但无实际意义 | ✅ 核心参数 | 外部启动时虽然调用了 __boot_slot(slot_nr),但 DUT 从外部 Flash 启动时只有一个 slot,切换无意义;内部启动时用于选择 AUAPPLIC slot |
-g test_group |
✅ 使用 | ✅ 使用 | 两种模式都在 Radio 启动后执行测试 |
-b branch |
✅ 使用 | ✅ 使用 | 传给测试用例函数 |
-d download_files |
✅ 使用 | ✅ 使用 | 解析参数时就执行下载,与启动模式无关 |
-m power_control |
⚠️ 外部启动脚本自己控制电源 | ✅ 使用 | 外部启动有严格的 off→切换→on 顺序,不使用此参数;内部启动可用此参数控制 |
-e boot_from_external |
❌ 不需要 | ❌ 不需要 | 外部启动由入口脚本决定(直接传 external_boot=True),不依赖此命令行参数 |
-f ft4222_load |
✅ NIB 专用 | ❌ 不使用 | 仅在 NIB 外部启动前加载 FAAP 到 SPI Flash 时使用 |
-u update_fw |
✅ 使用 | ✅ 使用 | NIB 固件升级,与启动模式无关 |
| loading_files (ELF/SWDB/DC/lib) | ⚠️ 加载但 Radio 不使用 | ✅ Radio 根据 restart_mode 使用 | 外部启动时文件由 switch_2_internal_flash() 函数单独处理 DC 文件,其余不用 |
2.2 slot 参数详解
内部启动时 slot 的作用:
DUT 内部 eMMC 上有多个 slot(固件槽位),每个 slot 可以存放不同版本的 AUAPPLIC。mr -s {slot_nr} -m auapplic 命令让 DUT 从指定 slot 启动:
# 内部启动时,slot 决定从哪个槽位加载固件
def __boot_slot(self, slot_nr):
self.serial_connection.write(f'mr -s {slot_nr} -m auapplic', wait_for="login", timeout=180)
lmclist 命令可以查看所有 slot 的状态:
Slot Active Type
0 No AUAPPLIC
1 No AUAPPLIC
2 Yes AUAPPLIC ← 当前活跃的 slot
外部启动时 slot 为什么无意义:
外部启动时 DUT 从接口板上的 SPI-NOR Flash 启动。这块外部 Flash 上只有一份完整的 FAAP 固件(由 ft4222_load_faap() 写入),没有多 slot 的概念。虽然代码中仍然调用了 __boot_slot(slot_nr),但此时 mr -s 2 -m auapplic 命令实际上只是触发了一次重启,slot 编号不影响从外部 Flash 加载的内容。
2.3 restart_mode 参数详解
内部启动时 restart_mode 的作用:
restart_mode 决定了 Radio 初始化时替换 DUT 上的哪些文件:
| 值 | 模式 | 内部启动时的操作 | 外部启动时的操作 |
|---|---|---|---|
| 0 | NOT_RESTART | 检查 slot → 检查 radioapp 是否运行 → 按需 mamastart | 检查 slot → SSH 连接 → 直接 return |
| 1 | REPLACE_RADIOAPP_SWDB | SCP 传输 ELF + SWDB → mamastart | boot_slot → SSH 连接 → 直接 return |
| 2 | REPLACE_RADIOAPP | SCP 传输 ELF → mamastart | boot_slot → SSH 连接 → 直接 return |
| 3 | REPLACE_SWDB | SCP 传输 SWDB → mamastart | boot_slot → SSH 连接 → 直接 return |
| 4 | REPLACE_NONE | 仅重启 → mamastart | boot_slot → SSH 连接 → 直接 return |
| 5 | REPLACE_DC | 切 AUBOOT → 传输 DC → 重启 → mamastart | boot_slot → SSH 连接 → 直接 return |
| 6 | REPLACE_ET_LIB | SCP 传输 libetsw → mamastart | boot_slot → SSH 连接 → 直接 return |
| 7 | REPLACE_LIB_SWDB | SCP 传输 libetsw + SWDB → mamastart | boot_slot → SSH 连接 → 直接 return |
关键代码路径:
def __handle_restart(self):
self.__boot_slot(self.slot_nr) # 两种模式都执行
self.__bootstrap_ssh() # 两种模式都执行
if self.external_boot:
return # ← 外部启动在这里就结束了!
# 下面所有操作全部跳过
# ===== 以下只有内部启动才会执行 =====
handler = restart_handlers.get(self.restart) # 根据 restart_mode 传输文件
if handler:
handler()
self.mamastart() # 启动 Radio 应用
self.__post_mamastart() # 后续操作
2.4 外部启动时文件传输的实际发生位置
外部启动时 Radio 对象不传输文件,但文件传输并非不发生,而是由外层脚本在 Flash 切换阶段处理:
# bootup_dut_from_external_nib.py / external_boot_faap_via_tcpe3.py
# Radio 初始化:只建立 SSH,不传文件
radio = Radio(config, DUT_connection, args.slot, loading_files, args,
serial_logger, external_boot=True)
# Flash 切换函数中传输 DC 文件
def nib_switch_2_internal_flash(...):
dc_file_name = get_dc_files(f"{loading_file_path}/pis")
radio.transfer_file(dc_file_name, f"/tmp/{os.path.basename(dc_file_name)}") # ← DC 文件在这里传输
DUT_connection.write("flash-hotswap internal")
DUT_connection.write(f"prepare_onboard_flash \"{radio.product.production_number}\" \"{radio.product.r_state}\"")
# mamastart 在 Flash 切换完成后单独调用
radio.startup_mama() # ← 注意:调用的是 startup_mama(),不是 mamastart()
注意外部启动调用的是 startup_mama()(不含文件传输逻辑),而内部启动中 Radio 调用的是 mamastart()(hawkowl 产品会先传输 DC 文件再 mamastart)。
2.5 mamastart() vs startup_mama() 的区别
# mamastart() — 内部启动时由 Radio.__handle_restart() 调用
def mamastart(self):
if self.product.family == 'hawkowl':
self.transfer_file(self.loading_files.dc, ...) # 传输 DC 文件
self.serial_connection.write("prepare_onboard_flash ...") # 准备 Flash
self.serial_connection.write("source /run/env/mtdenv")
self.serial_connection.write("refresh_pid")
self.serial_connection.write("source /run/env/sysenv")
self.serial_connection.write("mamastart")
return self.serial_connection.write("waitradiohal", wait_for='HAL is ready')
# startup_mama() — 外部启动时由外层脚本在 Flash 切换后调用
def startup_mama(self):
self.serial_connection.write("refresh_pid")
self.serial_connection.write("db list /freqClassUsage")
self.serial_connection.write("cid")
self.serial_connection.write("parget --flat /sys/hw/")
self.serial_connection.write("mamastart")
self.serial_connection.write("waitradiohal", wait_for='HAL is ready')
| 维度 | mamastart() (内部启动) |
startup_mama() (外部启动) |
|---|---|---|
| DC 文件传输 | ✅ hawkowl 产品会传输 | ❌ 不传输(已在 switch_2_internal_flash 中完成) |
| prepare_onboard_flash | ✅ hawkowl 产品会执行 | ❌ 不执行(已在 switch_2_internal_flash 中完成) |
| refresh_pid | ✅ hawkowl 产品会执行 | ✅ 执行 |
| 查询硬件信息 | ❌ 不查询 | ✅ 查询 (db list, cid, parget) |
| mamastart 命令 | ✅ 执行 | ✅ 执行 |
| waitradiohal | ✅ 等待 | ✅ 等待 |
2.6 __bootstrap_ssh() 中 external_boot 的影响
SSH 连接建立时,external_boot 还影响了网络配置行为:
def __bootstrap_ssh(self) -> None:
outp = self.serial_connection.write("ifconfig eth0")
if self.ip not in outp:
# 外部启动 + Krypton 产品时,不配置 IP(因为外部 Flash 的系统可能不支持)
if not self.external_boot or self.product.family != 'krypton':
self.serial_connection.write(f"ifconfig eth0 {self.ip}")
self.serial_connection.write("ping -c1 192.168.1.1")
| 条件 | 是否配置 DUT IP |
|---|---|
| 内部启动,任何产品 | ✅ 配置 |
| 外部启动,Krypton | ❌ 不配置 |
| 外部启动,非 Krypton (Mongoose/Hawkowl) | ✅ 配置 |
2.7 电源操作的差异
| 维度 | 外部启动 | 内部启动 |
|---|---|---|
| 电源操作方式 | 脚本硬编码:power_off() → 切换 → power_on() |
可选:power_cycle() 或由 -m 参数控制 |
| 操作顺序 | 严格顺序:必须先断电再切换 Flash | 无严格顺序要求 |
| 是否必须重启 | ✅ 必须(DUT 需要从外部 Flash 冷启动) | ❌ 可选(restart_mode=0 时不重启) |
| 代码位置 | 外层脚本 tcpe_switch_2_external_flash() |
parse_power_mode(args) 或脚本中 power_cycle() |
外部启动的电源操作(硬编码在切换函数中):
def tcpe_switch_2_external_flash(power_supply_com, interface_board_connection, logger):
power_supply_com.power_off() # ① 必须先断电
sleep(2) # ② 等待电容放电
tcpe_swtich_to_external_flash() # ③ 切换 Flash
power_supply_com.power_on() # ④ 上电启动
内部启动的电源操作(可选,由参数控制):
# internal_boot_faap_via_tcpe3.py
tcpe_swtich_to_internal_flash(serial_logger, interface_board_connection) # 先切换
if args.restart_mode != RestartMode.NOT_RESTART.value:
gpib_tcpe_com.power_cycle() # 仅在需要重启时才 power_cycle
3. 参数在内外部启动中的实际使用差异(test-interface-client)
test-interface-client 中内外部启动的参数差异主要体现在 YAML 配置文件中:
3.1 YAML 配置参数对照表
| YAML 配置项 | 外部启动是否需要 | 内部启动是否需要 | 说明 |
|---|---|---|---|
address |
✅ 必须 | ✅ 必须 | gRPC 服务地址 |
dut_position |
✅ 必须 | ✅ 必须 | DUT 位置编号 |
profile_path |
✅ 必须 | ✅ 必须 | 硬件配置 Profile |
dz_loading |
✅ 必须 | ⚠️ 可选 | 外部启动必须加载固件到 Flash;内部启动时 DUT 已有固件,通常不需要 |
dz_loading.product_number |
✅ 必须 | — | 用于匹配 manifest 中的固件文件 |
dz_loading.product_rstate |
✅ 必须 | — | 用于匹配硬件版本 |
dz_loading.security_level |
✅ 必须 | — | 决定选择哪个 FLASHIMG(secure_locked/secure_unlocked/none) |
dz_loading.extra_sw_files |
✅ 通常需要 | — | manifest 中未包含的文件(PPAR、TA、ProdDB) |
gpio |
✅ 使用 | ✅ 使用 | GPIO 控制 |
mode_select |
✅ 设为 EXTERNAL | ✅ 设为 INTERNAL | 唯一需要修改的参数 |
trigger |
✅ 使用 | ✅ 使用 | 触发上电 |
3.2 内外部启动切换方式
在 test-interface-client 中,切换内外部启动只需要改 YAML 中的一行:
# 外部启动
mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL"
# 内部启动
mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL"
以及决定是否需要 dz_loading 配置块:
- 外部启动:必须有
dz_loading,因为需要把固件写入外部 Flash - 内部启动:通常不需要
dz_loading,因为 DUT 内部 Flash 已有固件
3.3 security_level 参数的影响
security_level 仅在外部启动的 DZ 加载中使用,它决定了选择哪个版本的 InitialFlashImage:
# dz_container_parser.py
if target_mtd == "initialflashimage":
if security_level == "secure_locked":
function = "FLASHIMG_SECLOCK" # 安全锁定版本
elif security_level == "secure_unlocked":
function = "FLASHIMG_SECUNLOCK" # 安全解锁版本(开发用)
# else: function = "FLASHIMG" # 无安全版本
| security_level | 选择的 FLASHIMG | 适用场景 |
|---|---|---|
none |
FLASHIMG | 无安全启动 |
secure_locked |
FLASHIMG_SECLOCK | 生产环境,安全启动锁定 |
secure_unlocked |
FLASHIMG_SECUNLOCK | 开发/测试环境,安全启动但未锁定 |
3.4 与 ci-tools-pro 的参数对应关系
| ci-tools-pro 参数 | test-interface-client 对应 | 说明 |
|---|---|---|
-r restart_mode |
无对应 | test-interface-client 不管理 DUT 上的文件替换 |
-s slot |
无对应 | test-interface-client 不管理 slot 切换 |
-e boot_from_external |
mode_select: "...EXTERNAL" |
启动模式选择 |
-m power_control |
trigger.state |
电源/触发控制 |
-f ft4222_load |
dz_loading 配置块 |
固件加载 |
| product_number | dz_loading.product_number |
产品编号 |
| r_state | dz_loading.product_rstate |
硬件版本 |
| 无对应 | dz_loading.security_level |
ci-tools-pro 中没有安全级别概念 |
| 无对应 | profile_path |
ci-tools-pro 中硬件配置硬编码在 Python 中 |
4. ci-tools-pro 项目详细配置
4.1 入口脚本对应关系
| 启动模式 | 接口板 | 入口脚本 | 产品 |
|---|---|---|---|
| 外部启动 | NIB | bootup_dut_from_external_nib.py |
4451_B1 (Krypton) |
| 外部启动 | TCPE3 | external_boot_faap_via_tcpe3.py |
AIR1672 / AIR3286 |
| 内部启动 | NIB | bootup_dut_from_internal_nib.py |
4451_B1 (Krypton) |
| 内部启动 | TCPE3 | internal_boot_faap_via_tcpe3.py |
AIR1672 |
| 内部启动 | FIC (串口) | bootup_dut_from_internal_fic.py |
4451_B1 (Krypton) |
4.2 命令行参数
# 通用参数
-r, --restart-mode # 重启模式 (0-8),决定启动后替换哪些文件
-s, --slot # Slot 编号 (默认 2)
-b, --branch # 测试分支
-g, --test-group # 测试组 (tx_non_spectrum / pa_cal / rx_cal / rx_perf)
-d, --download-files # 从 E2C 服务器下载固件
-m, --power-control # 电源控制 (0=不操作 / 1=循环 / 2=关 / 3=开)
# NIB 专用参数
-e, --boot-from-external # 从外部 Flash 启动
-i, --boot-from-internal # 从内部 Flash 启动
-f, --ft4222-load # 使用 FT4222 加载固件
-p, --prod-mode # 生产模式 (0=内部 / 1=外部)
-u, --update-fw # 更新 NIB 固件
4.3 产品配置参数
每个产品在 product_manager.py 中的配置结构:
'4451_B1': {
'product': {
'family': 'krypton', # 产品族,影响 login 提示符和特殊处理逻辑
'production_number': 'KRC 161 4451/1', # 产品编号,用于 FAAP 包匹配和 prepare_onboard_flash
'r_state': 'R1A', # 硬件版本,用于 FAAP 包匹配
'db_suffix': '2_45' # 数据库后缀
},
'loading': {
'e2c_path': '/repo/eaaitjn/', # E2C 服务器上的固件路径
'e2c_ip': '100.79.120.235', # E2C 服务器 IP
'e2c_username': 'eaaitjn' # E2C 服务器用户名
},
'interface_board': {
'InterfaceBoardType': InterfaceBoardType.NIB, # 接口板类型
'position': 1 # 接口板位置(影响 IP 地址计算)
},
'power_supply': {
'type': PowerSupplyType.DC, # 电源类型 (DC/AC)
'com_mode': ComMode.GPIB, # 通信模式 (GPIB/TCPIP)
'addr': '20', # GPIB 地址或 IP 地址
'vol': 56, # 电压 (V)
'current': 10 # 电流限制 (A)
}
}
4.4 网络地址与端口配置
# constant.py 中定义的网络常量
# NIB 接口板
NIB_IP_ADDRESS = "192.168.2.51" # NIB 基础 IP
NIB_SSH_PORT = 2222 # NIB SSH 端口
NIB_SSH_USERNAME = "pi" # NIB SSH 用户名
NIB_SSH_PASSWD = "nib" # NIB SSH 密码
NIB_TELNET_PORT = 3002 # NIB 控制 Telnet 端口
NIB_DUT_SERIAL_TELNET_PORT = 3003 # NIB 转发的 DUT 串口 Telnet 端口
NIB_FTP_DEFAULT_PATH = 'etsw' # NIB FTP 默认路径
# TCPE3 接口板
TCPE_TELNET_ADDRESS = "192.168.2.71" # TCPE3 基础 IP
TCPE_TELNET_PORT = 3000 # TCPE3 控制 Telnet 端口
DUT_SERIAL_TELNET_PORT = 3001 # TCPE3 转发的 DUT 串口 Telnet 端口
# DUT
DUT_BASE_IP = "192.168.1.20" # DUT 基础 IP(通过 position 偏移计算实际 IP)
# E2C 构建服务器
E2C_IP_ADDRESS = "100.79.120.235"
E2C_IP_USERNAME = "eaaitjn"
# 串口 (FIC 模式)
SERIAL_COM = "COM3"
IP 地址计算规则(基于 position 参数):
- NIB position=1 → IP =
192.168.2.51 - TCPE3 position=1 → IP =
192.168.2.71,position=2 → IP =192.168.2.72 - DUT position=1 → IP =
192.168.1.20,position=2 → IP =192.168.1.21
4.5 外部启动完整流程(以 TCPE3 为例)
入口脚本:external_boot_faap_via_tcpe3.py
步骤 1: 加载配置
│
│ config = ProductConfigManager.get_product_config('AIR1672')
│ 产品配置:
│ family = "mongoose-dc"
│ production_number = "KRD 901 258/1"
│ r_state = "R1A"
│ 接口板类型 = TCPE3
│ 接口板 IP = 192.168.2.72 (base 71 + position 2 - 1)
│ 控制端口 = 3000
│ 串口端口 = 3001
│ DUT IP = 192.168.1.21
│ 电源类型 = AC, TCPIP, 地址 192.168.3.34, 110V/10A
│
├─→ 步骤 2: 解析命令行参数
│ args = parse_command_line_arguments()
│ loading_files = parse_restart_mode(args)
│ → 扫描 mongoose-dc/AIR1672/ 目录
│ → 按扩展名分类: .elf → elf, .bin → swdb, .sqlite → sqlite, .dc → dc
│
├─→ 步骤 3: 建立接口板连接
│ interface_board_connection = TelnetConnection(192.168.2.72, 3000)
│ gpib_tcpe_com = ComWithGpib(AC, TCPIP, 192.168.3.34, 110V, 10A)
│
├─→ 步骤 4: 切换到外部 Flash 并上电
│ tcpe_switch_2_external_flash():
│ ① gpib_tcpe_com.power_off() # 先断电
│ ② sleep(2) # 等待电容放电
│ ③ tcpe_telnet.write("bm ext") # 切换到外部 Flash
│ ④ gpib_tcpe_com.power_on() # 上电,DUT 从外部 Flash 启动
│
│ ⚠️ 必须先断电再切换!否则切换瞬间 CPU 可能向外部 Flash 写入垃圾数据
│
├─→ 步骤 5: 等待 DUT 启动,建立串口连接
│ dut_serial_connection = TelnetConnection(192.168.2.72, 3001, "mongoose-dc login")
│ → 通过 TCPE3 的 Telnet 端口 3001 连接 DUT 串口
│ → 等待出现 "mongoose-dc login" 提示符(DUT Linux 启动完成)
│
├─→ 步骤 6: 创建 Radio 对象 (external_boot=True)
│ radio = Radio(config, dut_serial_connection, slot=2, loading_files, args,
│ external_boot=True)
│
│ Radio.__init__() 内部流程(external_boot=True 时):
│ ① __boot_slot(slot_nr=2)
│ → serial.write("mr -s 2 -m auapplic") # 切换到 slot 2
│ → 等待 "login" 提示符(最长 180 秒)
│ → serial.write("root") # 登录
│ ② __bootstrap_ssh()
│ → serial.write("ifconfig eth0") # 检查网络
│ → SSH 连接到 DUT (192.168.1.21:22, root, 无密码)
│ ③ return ← 关键!external_boot=True 时到这里就停了
│ → 不传输文件
│ → 不执行 mamastart
│ → 不执行任何 restart_mode 对应的操作
│
├─→ 步骤 7: 从外部 Flash 切换到内部 Flash
│ tcpe_switch_2_internal_flash():
│ ① serial.write("parget --flat /sys/hw/") # 读取硬件信息
│ ② serial.write("cat /proc/mtd") # 查看当前 MTD 分区(外部 Flash)
│ ③ serial.write("getenv | grep mtd") # 查看 MTD 环境变量
│ ④ tcpe_telnet.write("bm int") # 切换到内部 Flash
│ ⑤ radio.transfer_file(dc_file, "/tmp/xxx.dc") # SCP 传输 DC 文件到 DUT
│ ⑥ serial.write("flash-hotswap internal") # 热切换:Linux 内核重新识别 Flash
│ ⑦ serial.write('prepare_onboard_flash "KRD 901 258/1" "R1A"')
│ → 初始化内部 Flash 分区表
│ → 将固件从 RAM 写入 eMMC
│ ⑧ serial.write("cat /proc/mtd") # 确认分区已切换
│ ⑨ serial.write("source /run/env/mtdenv") # 加载新 MTD 环境变量
│ ⑩ serial.reconnect() # 重新连接串口
│ ⑪ serial.write("getenv | grep mtd") # 验证环境变量
│
│ 如果是 hawkowl 产品,还需要额外步骤:
│ ⑫ serial.write("dbtool -w -t radiodb") # 写入 Radio 数据库
│ ⑬ serial.write("dbtool -w -t ppar") # 写入产品参数
│
├─→ 步骤 8: 启动 Radio 应用
│ radio.startup_mama():
│ ① serial.write("refresh_pid")
│ ② serial.write("db list /freqClassUsage")
│ ③ serial.write("cid")
│ ④ serial.write("parget --flat /sys/hw/")
│ ⑤ serial.write("mamastart") # 启动 Radio 应用
│ ⑥ serial.write("waitradiohal") # 等待 "HAL is ready"
│
└─→ 步骤 9: 执行 RF 校准测试
pa_cal(radio, branch) # PA 校准
rx_cal(radio, branch) # RX 校准
rx_perf(radio, branch) # RX 性能测试
tx_cal(radio, branch) # TX 校准
4.6 内部启动完整流程(以 TCPE3 为例)
入口脚本:internal_boot_faap_via_tcpe3.py
步骤 1: 加载配置(同外部启动)
│
│ config = ProductConfigManager.get_product_config('AIR1672')
│
├─→ 步骤 2: 解析命令行参数(同外部启动)
│
├─→ 步骤 3: 建立接口板连接
│ interface_board_connection = TelnetConnection(192.168.2.72, 3000)
│ gpib_tcpe_com = ComWithGpib(...)
│
├─→ 步骤 4: 切换到内部 Flash
│ tcpe_swtich_to_internal_flash():
│ tcpe_telnet.write("bm int") # 一条命令,不需要断电
│
│ ⚠️ 注意:内部启动不需要先断电!因为 DUT 本来就应该从内部 Flash 启动
│
├─→ 步骤 5: 电源循环(仅在 restart_mode != NOT_RESTART 时)
│ if args.restart_mode != RestartMode.NOT_RESTART.value:
│ gpib_tcpe_com.power_cycle() # 断电 → 等待 → 上电
│
│ ⚠️ 与外部启动的区别:
│ - 外部启动:必须 power_off → 切换 → power_on(严格顺序)
│ - 内部启动:先切换,然后可选 power_cycle(或不重启)
│
├─→ 步骤 6: 等待 DUT 启动,建立串口连接
│ dut_serial_connection = TelnetConnection(192.168.2.72, 3001, "mongoose-dc login")
│
├─→ 步骤 7: 创建 Radio 对象 (external_boot=False)
│ radio = Radio(config, dut_serial_connection, slot=2, loading_files, args,
│ external_boot=False)
│
│ Radio.__init__() 内部流程(external_boot=False 时):
│
│ ┌─ 如果 restart_mode == NOT_RESTART (0):
│ │ ① __correct_slot() # 检查当前 slot 是否正确
│ │ ② __bootstrap_ssh() # 建立 SSH 连接
│ │ ③ __check_started() # 检查 radioapp 是否已运行
│ │ → 如果已运行: __connect_1524() # 连接 1524 数据库
│ │ → 如果未运行: mamastart() # 启动 Radio 应用
│ │
│ └─ 如果 restart_mode != NOT_RESTART (1-8):
│ ① __boot_slot(slot_nr=2) # 切换 slot 并重启
│ ② __bootstrap_ssh() # 建立 SSH 连接
│ ③ 根据 restart_mode 传输文件:
│ │
│ ├─ mode=1 (REPLACE_RADIOAPP_SWDB):
│ │ → __transfer_swdb() # SCP 传输 SWDB 到 /opt/radiosw/srv/database/
│ │ → __transfer_elf() # SCP 传输 ELF 到 /opt/radiosw/bin/ (如果 MD5 不同)
│ │
│ ├─ mode=2 (REPLACE_RADIOAPP):
│ │ → __transfer_elf() # SCP 传输 ELF (如果 MD5 不同)
│ │
│ ├─ mode=3 (REPLACE_SWDB):
│ │ → __transfer_swdb() # SCP 传输 SWDB
│ │
│ ├─ mode=4 (REPLACE_NONE):
│ │ → (不传输任何文件,仅重启)
│ │
│ ├─ mode=5 (REPLACE_DC):
│ │ → __boot_slot_auboot(1) # 先切到 AUBOOT 模式
│ │ → __transfer_dc() # SCP 传输 DC 到 /tmp/slotd_debug_{slot}.fifo
│ │
│ ├─ mode=6 (REPLACE_ET_LIB):
│ │ → __transfer_lib() # SCP 传输 libetsw 到 /usr/lib/
│ │
│ └─ mode=7 (REPLACE_LIB_SWDB):
│ → __transfer_lib() # SCP 传输 libetsw
│ → __transfer_swdb() # SCP 传输 SWDB
│
│ ④ mamastart() # 启动 Radio 应用
│ → 如果是 hawkowl 产品:
│ transfer_file(dc, "/tmp/xxx.dc")
│ serial.write("prepare_onboard_flash ...")
│ serial.write("source /run/env/mtdenv")
│ serial.write("refresh_pid")
│ serial.write("source /run/env/sysenv")
│ → serial.write("mamastart")
│ → serial.write("waitradiohal") # 等待 "HAL is ready"
│ ⑤ __post_mamastart()
│ → serial.write("linksup -d") # 链路监控
│ → __transfer_1524() # 传输 SQLite 数据库
│ → __connect_1524() # 连接 1524 数据库
│
└─→ 步骤 8: 执行 RF 校准测试(同外部启动)
4.7 Radio 类中 external_boot 参数的影响
external_boot 参数是区分内外部启动的核心开关,它改变了 Radio.__init__() 的行为:
# common/radio.py
class Radio:
def __init__(self, config, serial_connection, slot_nr, loading_files, args,
logger=None, external_boot=False):
self.external_boot = external_boot or args.boot_from_external
# ...
if self.restart == RestartMode.NOT_RESTART.value:
self.__handle_not_restart()
else:
self.__handle_restart()
| 行为 | external_boot=True (外部启动) | external_boot=False (内部启动) |
|---|---|---|
__boot_slot() |
✅ 执行 | ✅ 执行 |
__bootstrap_ssh() |
✅ 执行 | ✅ 执行 |
| 文件传输 (ELF/SWDB/DC/lib) | ❌ 跳过 (直接 return) | ✅ 根据 restart_mode 执行 |
mamastart() |
❌ 跳过 (直接 return) | ✅ 执行 |
__post_mamastart() |
❌ 跳过 | ✅ 执行 |
__check_started() |
❌ 跳过 | ✅ 执行 (NOT_RESTART 模式) |
关键代码:
def __handle_restart(self):
self.__boot_slot(self.slot_nr)
self.__bootstrap_ssh()
if self.external_boot:
return # ← 外部启动到这里就停了!
# 以下只有内部启动才会执行
handler = restart_handlers.get(self.restart)
if handler:
handler() # 传输文件
self.mamastart() # 启动 Radio 应用
self.__post_mamastart() # 后续操作
为什么外部启动时 Radio 不传输文件也不执行 mamastart?
因为外部启动的流程是:
- Radio 对象只负责建立 SSH 连接(用于后续文件传输)
- 文件传输和 Flash 切换由外层脚本的
switch_2_internal_flash()函数处理 - mamastart 由外层脚本在 Flash 切换完成后单独调用
radio.startup_mama()
4.8 NIB vs TCPE3 的外部启动差异
| 维度 | NIB (Krypton) | TCPE3 (Mongoose/Hawkowl) |
|---|---|---|
| 切换到外部 Flash | GPIO 组合操作(约 10 条命令) | bm ext(1 条命令) |
| 切换到内部 Flash | GPIO 反向操作(约 10 条命令) | bm int(1 条命令) |
| SSH 连接目标 | NIB IP (192.168.2.51:22) | DUT IP (192.168.1.2x:22) |
| DUT 串口连接 | Telnet 到 NIB:3003 | Telnet 到 TCPE3:3001 |
| 控制连接 | Telnet 到 NIB:3002 + SSH 到 NIB:2222 | Telnet 到 TCPE3:3000 |
| Flash 编程方式 | FT4222 USB-SPI 桥接 | 服务端内部处理 |
| 电源控制 | DC 电源, GPIB 通信 | AC 电源, TCPIP 通信 |
NIB 的 SSH 连接特殊性:
# radio.py __bootstrap_ssh()
target_ip = self.interface_board.ip_addr if self.interface_board.type == InterfaceBoardType.NIB else self.ip
# NIB 时: SSH 连接到 NIB (192.168.2.51),NIB 再转发到 DUT
# TCPE3 时: SSH 直接连接到 DUT (192.168.1.2x)
4.9 FIC 内部启动的特殊性
bootup_dut_from_internal_fic.py 使用串口直连 DUT,不经过任何接口板:
# 不需要接口板连接
# 不需要 Flash 切换命令
# 直接通过 COM3 串口连接 DUT
DUT_connection = SerialConnection(serial_logger, want_str)
radio = Radio(config, DUT_connection, args.slot, loading_files, args,
serial_logger, external_boot=False)
| 维度 | FIC 模式 | NIB/TCPE3 模式 |
|---|---|---|
| 连接方式 | 串口 COM3 直连 | Telnet 经接口板转发 |
| Flash 切换 | 不支持(只能内部启动) | 支持内外部切换 |
| 接口板 | 不需要 | 必须 |
| 电源检测 | 读取电压判断是否需要上电 | 由脚本控制 |
5. test-interface-client 项目详细配置
5.1 配置文件结构
test-interface-client 使用两层配置:
第一层:YAML 配置文件(configuration/AIR3286_config.yaml)— 定义测试流程参数
# 基础连接
address: "192.168.2.71:50051" # gRPC 服务地址(接口板 IP + gRPC 端口)
dut_position: 1 # DUT 位置编号
# 硬件配置 Profile 路径
profile_path: "configuration/Raptor2.TestInterface.AIR3286.Configuration.json"
# 硬件信息(用于 SetHWInfo 测试)
test_hw_infos:
- serial_number: "E23G123456"
product_number: "ROA1281122/1"
product_rstate: "R1A"
hw_name: "NIB"
comment: "Use default values!"
production_date: "2025-02-14"
# DZ 固件加载配置
dz_loading:
dz_container_path: 'external\hawkowl\AIR3286_B25B66\R1A393\RND\dev-hawkowl-applic-prebuilt-rnd'
product_number: "KRD 901 316/2" # 用于 manifest.yaml 中匹配固件文件
product_rstate: "R1A" # 用于 manifest.yaml 中匹配硬件版本
security_level: "secure_unlocked" # 安全级别:none / secure_locked / secure_unlocked
profile_path: "configuration/Raptor2.TestInterface.AIR3286.Configuration.json"
extra_sw_files: # manifest 中未包含的额外固件文件
ProductionParameters: 'external\hawkowl\AIR3286_B25B66\ppar_ext1.rif'
TrustedAnchor: 'external\hawkowl\AIR3286_B25B66\TA_dev.bin'
ProductionDb: 'external\hawkowl\AIR3286_B25B66\prod_db_AIR3286-B25B66-CXC1745529_1-R1A14.bin'
# GPIO 控制
gpio:
outputs:
- alias: "TEST_JTAG_SELECT" # GPIO 别名(映射到实际引脚由 Profile 定义)
state: false
- alias: "DISABLE_WD_N"
state: false
inputs: ["PS_MODE1"] # 要读取的 GPIO 输入
# 启动模式选择 ← 这里决定内部还是外部启动
mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL"
# mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL"
# 触发器配置
trigger:
state: "ENABLED"
source: "TEST_SEL1_TRIG"
第二层:JSON Profile 配置文件(Raptor2.TestInterface.AIR3286.Configuration.json)— 定义硬件映射
{
"TestInterfaceConfiguration": {
"AIR3286": {
"TCPE": {
"BootModeConfigurations": [...], // 启动模式 → GPIO 映射
"SoftwareAreaConfigurations": [...], // Flash 地址布局
"IOConfigurations": [...], // GPIO 别名 → 实际引脚映射
"IOConfigurationInitialSetting": [...], // GPIO 初始方向和电压
"UartConfigurations": [...], // UART 别名映射
"UartConfigurationInitialSetting": [...], // UART 初始参数
"VoltageRailConfigurations": [...], // 电压轨别名映射
"TriggerConfigurations": [...] // 触发器别名映射
},
"TIB3": {
// TIB3 板的配置...
}
}
}
}
5.2 启动模式的 GPIO 映射详解
Profile JSON 中的 BootModeConfigurations 定义了每种启动模式需要设置的 GPIO 状态:
"BootModeConfigurations": [
{
"Mode": "EnterDutBootModeInternal",
"IOSettings": [
{"IoName": "GPIO_1", "State": true}, // PROD_BOOT_EN_N = HIGH
{"IoName": "GPIO_2", "State": true}, // TEST_JTAG_SELECT = HIGH
{"IoName": "GPIO_3", "State": true}, // DISABLE_WD_N = HIGH
{"IoName": "GPIO_22", "State": false} // FLASH_SWITCH = LOW → 内部 Flash
]
},
{
"Mode": "EnterDutBootModeExternal",
"IOSettings": [
{"IoName": "GPIO_1", "State": false}, // PROD_BOOT_EN_N = LOW
{"IoName": "GPIO_2", "State": true}, // TEST_JTAG_SELECT = HIGH
{"IoName": "GPIO_3", "State": true}, // DISABLE_WD_N = HIGH
{"IoName": "GPIO_22", "State": true} // FLASH_SWITCH = HIGH → 外部 Flash
]
}
]
GPIO 别名与实际引脚的映射(IOConfigurations):
| 别名 (Alias) | 实际引脚 (ActualName) | 用途 |
|---|---|---|
PS_MODE1 |
GPIO_0 | 电源模式检测(输入) |
PROD_BOOT_EN_N |
GPIO_1 | 生产启动使能(低有效) |
TEST_JTAG_SELECT |
GPIO_2 | JTAG 选择 |
DISABLE_WD_N |
GPIO_3 | 看门狗禁用(低有效) |
BUS_SWITCH |
GPIO_14 | 总线开关 |
I2C_ENABLE |
GPIO_15 | I2C 使能 |
FLASH_SWITCH |
GPIO_22 | Flash 切换(核心引脚) |
TEST_DUT_CHECK |
DUT_CHECK | DUT 在位检测 |
内部 vs 外部启动的 GPIO 差异:
| GPIO 引脚 | 内部启动 | 外部启动 | 说明 |
|---|---|---|---|
| GPIO_1 (PROD_BOOT_EN_N) | HIGH (true) | LOW (false) | 外部启动时使能生产模式 |
| GPIO_2 (TEST_JTAG_SELECT) | HIGH (true) | HIGH (true) | 两种模式相同 |
| GPIO_3 (DISABLE_WD_N) | HIGH (true) | HIGH (true) | 两种模式相同 |
| GPIO_22 (FLASH_SWITCH) | LOW (false) | HIGH (true) | 关键差异:控制 Flash MUX |
5.3 Flash 地址布局配置
SoftwareAreaConfigurations 定义了每个固件组件在 Flash 中的地址:
| 组件 (SwType) | 起始地址 | 大小 | 擦除起始地址 | 擦除大小 | 说明 |
|---|---|---|---|---|---|
| SlotContentTable | 0x000000 | 256KB | 0x000000 | 256KB | 安全配置表 |
| InitialFlashImage | 0x080000 | 256KB | 0x080000 | 256KB | Flash 镜像 |
| Pboot | 0x0C0000 | 512KB | 0x0C0000 | 512KB | 主引导程序 |
| Sboot | 0x140000 | 256KB | 0x140000 | 256KB | 次引导程序 |
| XcsConfig | 0x180000 | 512KB | 0x180000 | 512KB | 硬件配置 DTB |
| ProductionParameters | 0x200000 | 256KB | 0x200000 | 256KB | 产品参数 |
| TrustedAnchor | 0x240000 | 256KB | 0x240000 | 256KB | 安全证书 |
| ProductionDb | 0xA00000 | 1.25MB | 0xA00000 | 1.25MB | 生产数据库 |
| Faap | 0xB40000 | ~245MB | 0xB40000 | ~245MB | 应用程序(最大) |
5.4 外部启动完整流程
入口脚本:main.py,配置 mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL"
步骤 1: 加载 YAML 配置
│
│ config = load_board_config('AIR3286')
│ address = "192.168.2.71:50051"
│ channel = grpc.insecure_channel(address)
│
├─→ 步骤 2: 固件升级(可选)
│ 如果 config 中有 "upgrade_file":
│ test_fw_update(address, upgrade_file, logger)
│ → 升级接口板固件
│
├─→ 步骤 3: 上传 Profile 配置
│ test_configuration_service(channel, profile_path, logger):
│ ① 读取 Raptor2.TestInterface.AIR3286.Configuration.json
│ ② 解析 BootModeConfigurations、IOConfigurations、UartConfigurations 等
│ ③ 构建 SetProfileRequest protobuf 消息
│ ④ gRPC → ConfigurationService.SetProfile()
│ → 服务端保存配置,知道每种启动模式对应哪些 GPIO 状态
│ ⑤ gRPC → ConfigurationService.SetHWInfo()
│ → 设置硬件信息(序列号、产品编号等)
│
├─→ 步骤 4: DZ 固件加载
│ test_dz_loading(channel, logger, config=config["dz_loading"]):
│ ① DZContainerParser.parse(manifest.yaml)
│ → 解析 manifest.yaml 获取固件组件列表
│ → 解析 flashmap.json 获取 Flash 地址映射
│ → _translate_flashmap(): SCT→SlotContentTable, PBOOT→Pboot, ...
│ ② parser.update_profile_sw_area_configs(profile_path)
│ → 用 flashmap 中的地址更新 Profile JSON 中的 SoftwareAreaConfigurations
│ → gRPC → ConfigurationService.SetProfile() (更新后的 Profile)
│ ③ 解析固件文件路径:
│ → 从 manifest 中按 product_number + product_rstate 匹配:
│ initialflashimage → FLASHIMG / FLASHIMG_SECUNLOCK
│ pboot → PBOOT
│ sboot → SBOOT
│ faap → AUAPPLIC
│ slotcontenttable → SCT
│ xcsconfig → XCS_CONFIG
│ → 从 extra_sw_files 中补充:
│ ProductionParameters → ppar_ext1.rif
│ TrustedAnchor → TA_dev.bin
│ ProductionDb → prod_db_xxx.bin
│ ④ gRPC → SoftwareRepositoryService.InitiateBootSoftwareRequest()
│ → 双向流式传输:
│ 客户端发送 SoftwareItem 元数据 (文件名、MD5、大小、类型)
│ 客户端发送 SoftwareItemsFinalized
│ 服务端逐个请求 sw_item_upload_request
│ 客户端分块上传文件内容 (1MB/块)
│ 服务端返回 sw_preparations_done
│ ⑤ gRPC → SoftwareRepositoryService.CleanupAfterBoot()
│ → 清理临时资源
│
├─→ 步骤 5: GPIO 控制
│ test_gpio(channel, logger, gpio_config):
│ ① gRPC → GPIOService.SetOutput("TEST_JTAG_SELECT", false)
│ ② gRPC → GPIOService.SetOutput("DISABLE_WD_N", false)
│ ③ gRPC → GPIOService.GetInput("PS_MODE1")
│ ④ gRPC → GPIOService.SetSafeDefaults()
│
├─→ 步骤 6: 选择外部启动模式
│ test_mode_select(channel, logger, mode="BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_EXTERNAL"):
│ ① gRPC → ModeSelectService.Select(boot_mode=EXTERNAL)
│ → 服务端根据 Profile 中的 BootModeConfigurations 自动设置:
│ GPIO_1 = LOW, GPIO_2 = HIGH, GPIO_3 = HIGH, GPIO_22 = HIGH
│
├─→ 步骤 7: 触发启动
│ test_trigger_service(channel, logger, trigger_config):
│ ① gRPC → TriggerService.SetTrigger(ENABLED)
│ ② gRPC → TriggerService.SetTriggerSource("TEST_SEL1_TRIG")
│
├─→ 步骤 8: 检查 DUT 连接
│ test_dut_connect_service(channel, logger):
│ ① gRPC → DutConnectionService.DutConnected()
│ → 返回 true/false 表示 DUT 是否在线
│
└─→ 步骤 9: 获取接口板状态
test_get_interface_status_service(channel, logger):
① gRPC → InterfaceStatusService.GetStatus()
5.5 内部启动完整流程
将 YAML 配置中的 mode_select 改为内部启动:
mode_select: "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL"
流程与外部启动几乎相同,区别在于:
| 步骤 | 外部启动 | 内部启动 | 差异说明 |
|---|---|---|---|
| 步骤 3: Profile | ✅ 上传 | ✅ 上传 | 相同 |
| 步骤 4: DZ 加载 | ✅ 执行 | ⚠️ 可选 | 内部启动时 DUT 已有固件,通常不需要重新加载 |
| 步骤 5: GPIO | ✅ 执行 | ✅ 执行 | 相同 |
| 步骤 6: 模式选择 | EXTERNAL |
INTERNAL |
GPIO_1=HIGH, GPIO_22=LOW |
| 步骤 7: 触发 | ✅ 执行 | ✅ 执行 | 相同 |
| 步骤 8: DUT 检查 | ✅ 执行 | ✅ 执行 | 相同 |
内部启动时 DZ 加载是否需要?
取决于场景:
- 如果 DUT 内部 Flash 已有正确固件 → 不需要 DZ 加载,可以从 YAML 中移除
dz_loading配置 - 如果需要更新固件 → 仍然需要 DZ 加载
5.6 AIR1672 vs AIR3286 的配置差异
| 配置项 | AIR1672 (Mongoose-DC) | AIR3286 (Hawkowl) |
|---|---|---|
| gRPC 地址 | 192.168.2.72:50051 | 192.168.2.71:50051 |
| DZ 容器路径 | external\mongoose-dc\AIR1672\... |
external\hawkowl\AIR3286_B25B66\... |
| product_number | KRD 901 258/1 | KRD 901 316/2 |
| product_rstate | R1B | R1A |
| security_level | secure_unlocked | secure_unlocked |
| GPIO 输出 | TEST_GPIO_2=HIGH, LOAD_DC_FW=LOW | TEST_JTAG_SELECT=LOW, DISABLE_WD_N=LOW |
| GPIO 输入 | TEST_GPIO_8 | PS_MODE1 |
| Profile | AIR1672.Configuration.json | AIR3286.Configuration.json |
6. 两个项目的流程对比总结
6.1 外部启动流程对比
ci-tools-pro test-interface-client
───────────────────────────────────────── ─────────────────────────────────────────
1. 加载 Python 字典配置 1. 加载 YAML + JSON Profile 配置
2. power_off() 2. gRPC → SetProfile (上传硬件配置)
3. Telnet: "bm ext" / GPIO 操作 3. gRPC → InitiateBootSoftwareRequest
4. power_on() (流式上传固件到 Flash)
5. 等待 DUT login 4. gRPC → SetOutput (GPIO 控制)
6. Radio(external_boot=True) 5. gRPC → ModeSelect(EXTERNAL)
→ SSH 连接,不传文件,不 mamastart (服务端自动设置 GPIO)
7. "bm int" / GPIO 反向操作 6. gRPC → SetTrigger(ENABLED)
8. "flash-hotswap internal" (触发上电)
9. "prepare_onboard_flash ..." 7. gRPC → DutConnected()
10. radio.startup_mama() (检查 DUT 是否在线)
11. 跑 RF 测试 8. gRPC → CleanupAfterBoot()
6.2 内部启动流程对比
ci-tools-pro test-interface-client
───────────────────────────────────────── ─────────────────────────────────────────
1. 加载 Python 字典配置 1. 加载 YAML + JSON Profile 配置
2. Telnet: "bm int" / GPIO 操作 2. gRPC → SetProfile
3. power_cycle() (可选) 3. gRPC → SetOutput (GPIO)
4. 等待 DUT login 4. gRPC → ModeSelect(INTERNAL)
5. Radio(external_boot=False) 5. gRPC → SetTrigger(ENABLED)
→ SSH 连接 6. gRPC → DutConnected()
→ 根据 restart_mode 传输文件
→ mamastart
→ post_mamastart
6. 跑 RF 测试
6.3 关键差异总结
| 维度 | ci-tools-pro 外部启动 | ci-tools-pro 内部启动 | test-interface-client |
|---|---|---|---|
| Flash 切换命令 | bm ext → bm int (两次切换) |
bm int (一次切换) |
ModeSelect(mode) (一次 gRPC 调用) |
| 电源操作顺序 | 必须 off → 切换 → on | 切换 → cycle (可选) | 由 Trigger 服务控制 |
| flash-hotswap | ✅ 需要 | ❌ 不需要 | 由服务端内部处理 |
| prepare_onboard_flash | ✅ 需要 | ❌ 不需要 (hawkowl 在 mamastart 中处理) | 由服务端内部处理 |
| 固件传输时机 | Flash 切换后由脚本处理 | Radio 初始化时根据 restart_mode | DZ 加载阶段统一处理 |
| Radio 对象角色 | 仅建立 SSH (不传文件) | 完整生命周期管理 | 不使用 Radio 对象 |
| 配置修改方式 | 改 Python 代码 | 改 Python 代码 | 改 YAML 一行配置 |

浙公网安备 33010602011771号