内外Flash

两种主流Flash

DUT(Device Under Test,被测设备)这两种启动模式,本质上对应了 NOR FlashMMC/NAND Flash 两种主流存储芯片的区别。

一、核心概念:什么是 Flash?

Flash 是一种 非易失性存储器,断电后数据不会丢失。你可以把它理解为 U盘的底层技术

在嵌入式设备(比如你的 DUT)里,它主要用来存放:

  1. Bootloader(引导程序):就像电脑的 BIOS,负责初始化硬件、引导操作系统。
  2. 固件/操作系统:设备真正的功能代码。
  3. 文件系统:存放配置文件、日志等。

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 固件。一个非常精简的测试固件。
  • 启动流程
    1. DUT 内部芯片上电后,根据硬件引脚(Boot Pin)的配置,决定先从外部 SPI 接口读取代码。
    2. 因为连接的是 NOR Flash,支持 XIP,CPU 可以直接在 NOR Flash 上运行 FAAP 固件。
    3. 这个 FAAP 固件的主要任务:初始化内存、网卡等,然后通过网络或其它接口,把完整的、正式的操作系统/固件下载并写入到内部的 MMC Flash 中。
  • 使用场景首次加载紧急恢复。比如出厂时,内部 Flash 还是空的;或者内部 Flash 里的系统坏了,变砖了,可以通过这个外部 NOR 启动一个“救援系统”来修复。

模式 2:Internal Flash (内部 MMC Flash)

  • 硬件位置:集成在 DUT 主芯片内部,或者是贴在主 PCB 上的 eMMC 芯片。
  • 存储内容完整的操作系统(Linux/RTOS)、应用程序、配置文件等
  • 启动流程
    1. DUT 内部的 BootROM(芯片内部固化的只读存储器,无法修改)先运行。
    2. BootROM 根据启动引脚,决定从内部 MMC 接口读取。
    3. 关键点:MMC Flash 是 NAND 类型的,不支持 XIP。所以 BootROM 会先从 MMC 的前几个扇区读取 二级 Bootloader(比如 U-Boot SPL)到芯片内部的小 RAM 里运行。
    4. 这个二级 Bootloader 再初始化外部大容量的 DDR 内存,然后把完整的 U-Boot 从 MMC 里读到 DDR 中运行。
    5. U-Boot 最后加载 Linux 内核和文件系统。
  • 使用场景正常运行模式。速度快、容量大(几 GB 到几十 GB),可以存储复杂的系统和海量数据。

  1. BootROM(芯片内部固化的只读存储器)
  • 是什么:芯片(比如 CPU/MCU)内部的一小块 只读存储器,在芯片出厂时就已经写死了程序,你无法修改它
  • 作用:芯片上电后,硬件自动执行的第一段代码就是 BootROM 里的程序。它的任务很有限:
    • 初始化最基本的硬件(比如时钟、一些引脚)。
    • 根据外部引脚(BOOT 引脚)的电平高低,决定从哪个接口(SPI、MMC、UART 等)去读取下一阶段的启动代码。
    • 把这个下一阶段的代码(很小,通常是几 KB 到几十 KB)复制到芯片内部的 SRAM(很小的一块内存)里,然后跳转过去执行。
  • 类比:就像电脑主板上的 BIOS 芯片,但 BootROM 是集成在 CPU 内部的,而且更小、功能更简单。

你不需要修改 BootROM,它只是固定的“启动引导器”。


  1. 二级 Bootloader(例如 U-Boot SPL)
  • 是什么SPL = Secondary Program Loader(二级程序加载器)。它是 U-Boot 的一个极简版本,体积很小(几十 KB),能够被 BootROM 加载到内部 SRAM 里运行。
  • 为什么需要它:BootROM 能访问的硬件有限,而且内部 SRAM 很小(可能只有几十 KB)。但完整的 U-Boot 通常有几百 KB,放不进内部 SRAM。所以需要一个“二级加载器”来做两件事:
    1. 初始化外部内存控制器,让 CPU 能够访问 DDR 内存(容量大,几百 MB 到几 GB)。
    2. 完整的 U-Boot 从存储介质(比如 eMMC、SD 卡、网络)读到 DDR 内存中。
  • 执行完后:跳转到 DDR 中完整的 U-Boot 代码,继续运行。

  1. DDR 内存(Double Data Rate SDRAM)
  • 是什么:就是我们常说的 内存条 上的那种内存芯片,但在嵌入式设备里是直接焊在板子上的。
  • 特点
    • 容量大(256 MB、512 MB、1 GB 甚至更大)。
    • 速度很快(CPU 可以直接从这里取指令、读写数据)。
    • 易失性:断电后数据全部丢失。
  • 为什么启动时要初始化它:上电时,DDR 控制器还没有配置,CPU 无法访问 DDR。只有二级 Bootloader 配置好控制器后,DDR 才能用。之后完整的 U-Boot 和 Linux 内核都要在 DDR 里运行。

简单记:DDR 就是设备的“运行内存”,程序运行时都在里面。


  1. U-Boot(Universal Bootloader)
  • 是什么:一个非常流行的 开源引导程序,用于嵌入式 Linux 设备。它功能很强,类似电脑上的 GRUB。
  • 作用
    • 初始化更多的硬件(网卡、USB、显示等)。
    • 提供一个命令行界面(通过串口),你可以敲命令来:
      • 查看/修改环境变量。
      • 从网络(TFTP)、eMMC、USB 等加载 Linux 内核。
      • 烧写固件。
    • 最终负责把 Linux 内核 从存储介质(eMMC)读到 DDR 内存里,并跳转执行。
  • 在启动流程中的位置:二级 Bootloader(SPL)加载 U-Boot → U-Boot 加载 Linux 内核。

  1. Linux 内核
  • 是什么:操作系统的核心。它管理所有硬件(CPU、内存、硬盘、网卡等),并为应用程序提供运行环境。
  • 在启动流程中:U-Boot 会把内核镜像(比如 zImageImage)从 eMMC 读到 DDR 的某个位置,然后跳转过去执行。内核开始启动,初始化各种驱动,最后挂载根文件系统。

  1. 根文件系统(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?既大又快。

答案:安全性、可恢复性和成本权衡。

  1. 变砖恢复:如果内部 MMC Flash 里的 Bootloader 或者系统刷坏了,设备上电后无法启动。这时候如果没有外部 NOR Flash 作为“备份启动源”,设备就彻底“变砖”了,只能返厂用烧录器重新烧写。有了外部 NOR,你可以强行从它启动,然后重新刷写内部 MMC。
  2. 首次烧录:工厂生产线上,内部 MMC 是空白的。总不能先拆开用编程器写吧?成本太高。一般是让设备从外部 NOR 启动一个最小的烧录固件(就是你提到的 FAAP),然后这个固件通过网口或 USB,把完整的系统镜像烧录进内部 MMC。
  3. 成本与性能: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)
  1. 如何切换模式? 去看 DUT 的原理图,找 BOOT_MODE 或类似名字的引脚、拨码开关或寄存器。硬件上通常通过拉高/拉低几个引脚的电平来决定从哪个 Flash 启动。
  2. 如何烧写? 在 External Flash 模式下,FAAP 固件提供了什么命令或界面(比如串口命令行、TFTP 服务)来烧写 Internal Flash?这是你写测试用例时要调用的接口。
  3. 验证方法: 如何从软件上确认当前是从哪种模式启动的?比如读取某个寄存器,或者检查 /proc/mtd/proc/partitions 的差异。

明白了,你希望我把 NOR FlashNAND 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

MMCMultiMediaCard(多媒体卡)的缩写。

它是一种 存储卡/接口标准,由西门子和 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-protest-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)协议:

  1. 客户端先告诉服务端"我有这些固件文件"(发送元数据)
  2. 服务端按自己的顺序逐个请求"把 PBOOT 传给我"
  3. 客户端分块上传文件内容(每块 1MB)
  4. 服务端收到后自动写入 Flash 对应地址
  5. 全部完成后服务端返回 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_22FLASH_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?

因为外部启动的流程是:

  1. Radio 对象只负责建立 SSH 连接(用于后续文件传输)
  2. 文件传输和 Flash 切换由外层脚本的 switch_2_internal_flash() 函数处理
  3. 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 extbm 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 一行配置
posted @ 2026-04-10 15:29  mo686  阅读(70)  评论(0)    收藏  举报