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 配置文件。

浙公网安备 33010602011771号