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 配置文件。
posted @ 2026-04-10 15:29  mo686  阅读(3)  评论(0)    收藏  举报