ExternalSignalStimuliService

ExternalSignalStimuliService 四个 RPC 函数工作流详解

概述

ExternalSignalStimuliService 定义在 service_dut_rx_stimuli.proto 中,是 DUT(被测设备)RX 校准流程的核心服务。它负责管理外部射频激励信号的配置、调整和释放。

服务架构:

DUT (FAAP) ──gRPC──> ExternalSignalStimuliService ──SCPI/GPIB──> 信号发生器
                              │
                              ├── RequestExternalStimuli()       → 配置并开启信号
                              ├── UpdateConfiguration()          → 微调频率/功率
                              ├── TurnOffAndReleaseConfiguration() → 关闭并释放
                              └── GetRfInterface()               → 查询测试接口类型

Python 实现位置: test-interface-client/service_client/rf_stimuli.py


四个函数的调用时序

DUT 启动 RX 校准流程
│
├── 1. GetRfInterface()
│       → 查询测试接口类型(COMBINED / SPLIT_ON_POLARIZATION)
│       → DUT 据此决定是并发测试所有端口还是按极化方向分批
│
├── 2. RequestExternalStimuli()
│       → 测试站分配信号发生器硬件
│       → 配置信号(频率、功率、波形)
│       → 执行 Leveling(电平校准)
│       → 返回端口偏移 / 多音电平相位信息
│
├── 3. [可选,可多次调用] UpdateConfiguration()
│       → 校准迭代中微调功率或频率
│       → 无需重新分配资源
│
├── 4. DUT 采集数据、执行校准计算
│
└── 5. TurnOffAndReleaseConfiguration()
        → 关闭信号发生器输出
        → 释放硬件资源
        → 如果是 SPLIT 模式,回到步骤 2 处理另一个极化方向

函数一:RequestExternalStimuli

Proto 定义

rpc RequestExternalStimuli (ConfigurationRequest) returns (ConfigurationResponse) {}

业务目的

DUT 在进行 RX 校准时,请求测试站配置外部射频信号。测试站分配信号发生器硬件、设置频率/功率/波形、执行电平校准(Leveling),然后返回每个端口的信号电平偏移信息供 DUT 用于补偿。

请求消息结构

ConfigurationRequest
├── dut_id: DutIdentifier
│   ├── session_id: SessionId {device_id, start_time}
│   └── dut_pos: uint32 (DUT 物理位置)
└── configurations[]: SignalConfiguration 列表
    ├── configuration_id: uint32 (配置唯一标识)
    ├── polarization: Polarization (水平/垂直/双极化/不适用)
    ├── rfports[]: uint32 列表 (RF 端口号)
    └── signals[]: Signal 列表
        ├── signal_id: uint32
        └── oneof stimuli:
            ├── carrier: CarrierSignal
            │   ├── requirements: {stimuli_level_dbm, upper_bound_db, lower_bound_db}
            │   ├── test_signal: {carrier_type, model}
            │   └── carrier_freq_hz: double
            └── mt: MultiToneSignal
                ├── requirements: {stimuli_level_dbm, upper_bound_db, lower_bound_db}
                ├── no_tones: uint32
                ├── start_hz: double
                └── sep_hz: uint32

详细工作流

步骤 1: 参数校验
├── 检查 dut_id 是否存在
├── 检查 session_id 是否有效
└── 无效 → 返回 status.code = INVALID_ARGUMENT

步骤 2: 遍历每个 SignalConfiguration
│
├── 步骤 2.1: 遍历每个 Signal
│   │
│   ├── 判断信号类型 (WhichOneof "stimuli")
│   │   ├── "carrier" → 单载波信号
│   │   └── "mt" → 多音信号
│   │
│   ├── 步骤 2.2: 分配硬件资源
│   │   ├── CW 信号 (carrier_type == CW):
│   │   │   ├── 优先分配 CW 专用信号发生器 (timeout=1s)
│   │   │   ├── CW 发生器全忙 → 降级使用调制发生器 (timeout=10s)
│   │   │   └── 全部占用 → TimeoutException → 标记为 unhandled
│   │   │
│   │   └── 调制/多音信号:
│   │       ├── 分配带 IQ 调制 + ARB 的信号发生器
│   │       └── 全部占用 → TimeoutException → 标记为 unhandled
│   │
│   ├── 步骤 2.3: 加载波形 (仅调制信号)
│   │   ├── 生成波形唯一 key (hash 后 8 位)
│   │   ├── 检查 ARB 是否已存储该波形
│   │   │   ├── 已存储 → 跳过
│   │   │   └── 未存储 → 读取 IQ 文件 (Tab 分隔的 I/Q 值)
│   │   └── 上传波形到 ARB 发生器
│   │
│   ├── 步骤 2.4: 配置信号发生器
│   │   ├── 设置载波频率
│   │   ├── 设置输出功率电平
│   │   ├── 设置调制模式 (CW: OFF, 调制: ON)
│   │   └── 开启 RF 输出
│   │
│   ├── 步骤 2.5: 执行 Leveling (电平校准) ★核心步骤
│   │   ├── 计算目标电平: wantedLevel = stimuliLevel + pathCompensation
│   │   ├── 查询缓存 (14天有效期)
│   │   │   ├── 命中 → 直接使用缓存的 RF 电平值
│   │   │   └── 未命中 → 执行闭环校准:
│   │   │       ├── 关闭同连接点的其他信号发生器
│   │   │       ├── 连接功率计
│   │   │       ├── 迭代循环 (最多 10 次):
│   │   │       │   ├── 功率计测量实际电平
│   │   │       │   ├── 计算偏差 = measured - wanted
│   │   │       │   ├── |偏差| < 0.1 dB → 完成
│   │   │       │   ├── 调整发生器电平 (每3次迭代降低步长×0.75)
│   │   │       │   └── 累计调整 > 2.0 dB → 异常终止
│   │   │       ├── 保存结果到缓存
│   │   │       └── 重新打开其他信号发生器
│   │   └── 连接 RF 开关到 DUT
│   │
│   ├── 步骤 2.6: Arb 特性化 (仅多音信号)
│   │   ├── 补偿 ARB 模块对不同频率音的非线性响应
│   │   ├── 查询缓存
│   │   │   ├── 命中 → 使用缓存数据 + 电平修正
│   │   │   └── 未命中 → 执行特性化:
│   │   │       ├── 方式 A (频谱仪): CW测量 → 多音测量 → 计算补偿
│   │   │       └── 方式 B (功率计): 逐音测量 → 计算补偿
│   │   └── 验证音间一致性 (相邻音差 < 0.5dB, 最大最小差 < 1.5dB)
│   │
│   └── 步骤 2.7: 计算端口偏移响应
│       ├── 单载波: SignalLevelResponseAsync()
│       │   ├── 获取参考路径衰减
│       │   ├── 获取每个端口路径衰减
│       │   └── offset = referenceAtt - portAtt + signalDeviation
│       │
│       └── 多音: MultiToneLevelResponseAsync()
│           ├── 获取每个端口的 S 参数 (S21)
│           ├── 对每个音计算:
│           │   ├── toneLevel = rfGenLevel - toneLoss + refAtt - portAtt
│           │   └── phase = S21.Phase + rfGenPhase + metadataPhase
│           └── 相位归一化 (相对第一个音)
│
└── 步骤 3: 组装响应

响应消息结构

ConfigurationResponse
├── status: Status {code, message}
├── errors[]: ConfigurationError 列表
│   ├── configuration_id
│   ├── errors[]: StimuliError 枚举 (IBW_EXCEEDED / NO_CW_EXCEEDED / NO_MT_EXCEEDED)
│   └── signal_errors[]: SignalError {signal_id, errors[]}
├── active_configurations[]: SignalConfigurationResponse 列表
│   ├── configuration_id
│   └── signals[]: SignalResponse 列表
│       ├── signal_id
│       └── oneof signal_response:
│           ├── signal: SignalRequestResponse
│           │   └── rfports[]: RfPortTargetOffset {rfport, target_offset_db}
│           └── mt: MultiToneSignalResponse
│               └── tones[]: MultiTone 列表
│                   ├── rfport: RfPortTargetOffset
│                   ├── tones[]: Tone {frequency_hz, tone_level_dbm, phase_deg}
│                   └── par_db: double
└── unhandled_configurations[]: uint32 列表 (因资源不足未处理的配置 ID)

Python 实现要点

def RequestExternalStimuli(self, request, context):
    # 1. 校验 dut_id
    if not request.HasField("dut_id"):
        return ConfigurationResponse(status=Status(code=INVALID_ARGUMENT))

    # 2. 遍历 configurations
    for sig_config in request.configurations:
        for signal in sig_config.signals:
            stimuli_type = signal.WhichOneof("stimuli")
            if stimuli_type == "carrier":
                # 配置单载波: 设置频率、功率、开启输出
                sg.configure_cw(carrier.carrier_freq_hz, carrier.requirements.stimuli_level_dbm)
            elif stimuli_type == "mt":
                # 配置多音: 计算中心频率、设置调制信号
                center_freq = mt.start_hz + (mt.no_tones - 1) * mt.sep_hz / 2.0
                sg.configure_modulated(center_freq, mt.requirements.stimuli_level_dbm)

    # 3. 返回响应 (包含端口偏移信息)
    return ConfigurationResponse(status=Status(code=OK), active_configurations=[...])

函数二:UpdateConfiguration

Proto 定义

rpc UpdateConfiguration (ConfigurationUpdateRequest) returns (ConfigurationUpdateResponse) {}

业务目的

在校准过程中,DUT 需要微调已配置信号的功率或频率,而不需要完全释放再重新请求。这避免了重新分配资源和重新 Leveling 的开销。

设计动机(proto 注释):

  1. 避免校准期间因重新请求而被阻塞(可能影响温度读数)
  2. 调整功率时可能不需要用功率计重新对齐
  3. 调整频率时可能不需要用功率计重新对齐

请求消息结构

ConfigurationUpdateRequest
├── dut_id: DutIdentifier
├── configuration_id: uint32 (必须是已激活的配置)
└── oneof attribute:
    ├── total_power_offset_db: double (功率偏移,相对当前总功率)
    └── frequency_offset_hz: double (频率偏移,所有信号统一偏移)

详细工作流

步骤 1: 参数校验
├── 检查 dut_id 有效性
└── 无效 → 返回 INVALID_ARGUMENT

步骤 2: 查找活跃配置
├── 在内存中查找 configuration_id 对应的活跃配置
└── 找不到 → 返回错误 (UNAVAILABLE)

步骤 3: 判断更新类型 (WhichOneof "attribute")
│
├── "total_power_offset_db" (功率偏移):
│   ├── 对该配置下所有信号:
│   │   └── 新电平 = 当前电平 + offset
│   ├── 调整信号发生器输出功率: SCPI ":POW {new_level} dBm"
│   ├── 等待仪器稳定: "*OPC?"
│   └── 不执行 Leveling (设计意图: 快速响应)
│
└── "frequency_offset_hz" (频率偏移):
    ├── 对该配置下所有信号:
    │   └── 新频率 = 当前频率 + offset
    ├── 调整信号发生器输出频率: SCPI ":FREQ {new_freq}"
    ├── 等待仪器稳定: "*OPC?"
    └── 多音信号: 偏移起始频率 (所有音统一偏移)

步骤 4: 重新计算端口偏移
├── 基于新的频率/功率重新查询路径补偿数据
└── 计算更新后的 target_offset

步骤 5: 组装响应
├── 成功 → 返回更新后的 SignalConfigurationResponse
└── 失败 → 返回 ConfigurationError

响应消息结构

ConfigurationUpdateResponse
├── status: Status {code, message}
└── oneof response:
    ├── error: ConfigurationError (失败时)
    │   ├── configuration_id
    │   ├── errors[]: StimuliError
    │   └── signal_errors[]
    └── configurations: SignalConfigurationResponse (成功时)
        ├── configuration_id
        └── signals[]: 更新后的信号信息

Python 实现要点

def UpdateConfiguration(self, request, context):
    device_id = request.dut_id.session_id.device_id
    config_id = request.configuration_id

    # 查找活跃配置
    active_config = self._active[device_id][config_id]

    # 判断更新类型
    update_field = request.WhichOneof("attribute")

    for sig_id, active_signal in active_config.signals.items():
        if update_field == "total_power_offset_db":
            active_signal.stimuli_level_dbm += request.total_power_offset_db
            self._sg.set_level(active_signal.stimuli_level_dbm)

        elif update_field == "frequency_offset_hz":
            active_signal.frequency_hz += request.frequency_offset_hz
            self._sg.set_frequency(active_signal.frequency_hz)

    return ConfigurationUpdateResponse(status=Status(code=OK), ...)

使用场景示例

场景: RX 增益校准迭代

DUT 请求 -30 dBm 信号 → RequestExternalStimuli()
DUT 测量 RX 增益,发现偏低
DUT 请求增加 0.5 dB → UpdateConfiguration(total_power_offset_db=0.5)
DUT 再次测量,精度满足
DUT 释放 → TurnOffAndReleaseConfiguration()

函数三:TurnOffAndReleaseConfiguration

Proto 定义

rpc TurnOffAndReleaseConfiguration (ReleaseConfiguration) returns (ReleaseConfigurationAck) {}

业务目的

DUT 完成数据采集后,通知测试站关闭信号发生器并释放硬件资源。这是 RequestExternalStimuli 的逆操作。

⚠️ 关键约束(proto 注释强调):

  • 数据采集完成后必须立即释放
  • 测试中止时也必须释放
  • 不释放会导致信号发生器被占用,其他 DUT 无法使用

请求消息结构

ReleaseConfiguration
├── dut_id: DutIdentifier
└── configuration[]: uint32 列表 (要释放的配置 ID,可一次释放多个)

详细工作流

步骤 1: 参数校验
├── 检查 dut_id 有效性
└── 无效 → 返回 INVALID_ARGUMENT

步骤 2: 遍历每个 configuration_id
│
├── 步骤 2.1: 关闭信号发生器输出
│   ├── SCPI: ":OUTP OFF"
│   ├── SCPI: ":IQ:STAT OFF" (关闭 IQ 调制)
│   └── SCPI: "*OPC?" (等待完成)
│
├── 步骤 2.2: 断开 RF 开关连接
│   └── 释放开关矩阵路径 (ResourceManager.Release)
│
├── 步骤 2.3: 释放硬件资源
│   ├── 归还信号发生器到资源池
│   ├── 归还 ARB 发生器 (如果有)
│   └── 更新计数器: NumberOfUsedGenerators--
│
├── 步骤 2.4: 清除内存状态
│   └── 从活跃配置字典中删除该 configuration_id
│
└── 步骤 2.5: 错误处理
    └── configuration_id 不存在 → 记录错误,继续处理其他 ID

步骤 3: 组装响应
├── 全部成功 → status.code = OK
└── 部分失败 → status.code = UNAVAILABLE, message 说明哪个配置未找到

响应消息结构

ReleaseConfigurationAck
└── status: Status {code, message}

Python 实现要点

def TurnOffAndReleaseConfiguration(self, request, context):
    device_id = request.dut_id.session_id.device_id

    for config_id in request.configuration:
        # 关闭信号发生器
        self._sg.turn_off()  # :OUTP OFF + :IQ:STAT OFF

        # 清除内存状态
        del self._active[device_id][config_id]

    return ReleaseConfigurationAck(status=Status(code=OK))

C# 参考实现中的额外操作

在完整的 C# 实现中,释放流程还包括:

  • 断开 RF 开关矩阵连接
  • 归还信号发生器到 ResourceManager 资源池
  • 记录测试设备日志(用于追溯)

函数四:GetRfInterface

Proto 定义

rpc GetRfInterface (GetRfInterfaceRequest) returns (GetRfInterfaceResponse) {}

业务目的

DUT 查询测试站的 RF 测试接口类型,以决定测试策略:

  • COMBINED:所有端口、前端、tile 可以并发测试
  • SPLIT_ON_POLARIZATION:RF 接口一次只支持一个极化方向,需要分批测试

请求消息结构

GetRfInterfaceRequest
├── dut_id: DutIdentifier
├── no_of_ports: uint32 (DUT 支持的总端口数)
└── dut_is_supporting_testing_per_polarization: bool
    判断逻辑 (DUT 侧):
    1. 检查 /rfArrayType 是否存在,不存在 → false
    2. /rfArrayType > 0 → true

详细工作流

步骤 1: 接收请求参数
├── dut_id: 标识哪个 DUT
├── no_of_ports: DUT 的 RF 端口总数
└── dut_is_supporting_testing_per_polarization: DUT 是否支持按极化分测

步骤 2: 判断 RF 测试接口类型
│
├── dut_is_supporting_testing_per_polarization == true:
│   └── 返回 RF_TEST_INTERFACE_SPLIT_ON_POLARIZATION (2)
│       含义: RF 接口一次只支持一个极化方向
│       DUT 行为: 先测水平极化,释放后再测垂直极化
│
└── dut_is_supporting_testing_per_polarization == false:
    └── 返回 RF_TEST_INTERFACE_COMBINED (1)
        含义: 所有端口可以并发测试
        DUT 行为: 一次请求所有端口的信号

步骤 3: 组装响应
└── 返回 GetRfInterfaceResponse {status: OK, interface: 枚举值}

响应消息结构

GetRfInterfaceResponse
├── status: Status {code, message}
└── interface: RfTestInterface 枚举
    ├── RF_TEST_INTERFACE_UNSPECIFIED (0) — 默认值,不应使用
    ├── RF_TEST_INTERFACE_COMBINED (1) — 全部并发
    └── RF_TEST_INTERFACE_SPLIT_ON_POLARIZATION (2) — 按极化分批

Python 实现要点

def GetRfInterface(self, request, context):
    if request.dut_is_supporting_testing_per_polarization:
        interface = RF_TEST_INTERFACE_SPLIT_ON_POLARIZATION
    else:
        interface = RF_TEST_INTERFACE_COMBINED

    return GetRfInterfaceResponse(
        status=Status(code=OK),
        interface=interface,
    )

对后续流程的影响

COMBINED 模式:
    RequestExternalStimuli(configurations=[
        {config_id=1, polarization=BOTH, rfports=[1,2,3,4], signals=[...]}
    ])
    → 一次配置所有端口
    → 测试完成后一次释放

SPLIT_ON_POLARIZATION 模式:
    # 第一轮: 水平极化
    RequestExternalStimuli(configurations=[
        {config_id=1, polarization=HORIZONTAL, rfports=[1,2], signals=[...]}
    ])
    → 测试水平极化端口
    TurnOffAndReleaseConfiguration(configuration=[1])

    # 第二轮: 垂直极化
    RequestExternalStimuli(configurations=[
        {config_id=2, polarization=VERTICAL, rfports=[3,4], signals=[...]}
    ])
    → 测试垂直极化端口
    TurnOffAndReleaseConfiguration(configuration=[2])

附录 A:核心概念详解

A.1 信号类型分类

系统中的信号分为三类:

类型 Proto 字段 IQ 调制 ARB 需要 典型用途
CW(连续波) carrier + CARRIER_TYPE_CW OFF 简单电平测量、增益校准
调制载波 carrier + 其他 CarrierType ON EVM、灵敏度、阻塞测试
多音 mt (MultiToneSignal) ON IM(互调)测试、频率响应

CW vs 单载波:

  • CW = 连续波 = 单一频率的纯正弦波,无任何调制,频谱上只有一根线
  • 单载波 = 一个载波频率,但可以有调制(带宽内有 LTE/NR 信号内容)
  • CW 是单载波的特例——最简单的、没有调制的单载波

多音 vs 调制信号的关系:

  • 两者都需要 IQ 调制 + ARB,区别在于波形内容和后续处理
  • 多音是调制信号的一个子集
信号发生器输出
│
├── RF Output OFF (无信号)
│
└── RF Output ON
    │
    ├── IQ 调制 OFF → CW(纯正弦波)
    │
    └── IQ 调制 ON → 需要 ARB 播放波形
        │
        ├── 波形是调制载波 → "调制信号"
        │   (LTE TM3.1a, NR FR1 等)
        │   单载波,带宽内有复杂调制
        │
        └── 波形是多音 → "多音信号"
            (N 个等间距正弦波叠加)
            每个音可独立控制电平/相位

多音和调制信号的配置差异(C# 完整实现):

步骤 调制载波 多音
载波频率 carrier_freq_hz(直接用) start_hz + (N-1)*sep_hz/2(算中心频率)
波形来源 波形库中预定义的 IQ 文件 系统根据参数实时生成多音 IQ 波形
Arb 特性化 不需要 需要(补偿各音非线性)
响应计算 SignalLevelResponseAsync(端口偏移) MultiToneLevelResponseAsync(每个音的电平+相位)
返回给 DUT RfPortTargetOffset(一个偏移值/端口) Tone[](每个音的 freq、level、phase)

A.2 极化(Polarization)

极化是电磁波的物理属性,描述电场振动的方向。

在天线中的含义:

Massive MIMO 天线(如 5G 基站 AAU)中,每个天线单元有两个正交的极化方向:

天线面板正面视角:

    │  │  │  │  │  │  │  │   ← 垂直极化天线振子 (V)
    ── ── ── ── ── ── ── ──  ← 水平极化天线振子 (H)
    │  │  │  │  │  │  │  │
    ── ── ── ── ── ── ── ──
    │  │  │  │  │  │  │  │
    ── ── ── ── ── ── ── ──

实际上是 ±45° 交叉极化,但习惯上称为 H 和 V

一个 64T64R 的 Massive MIMO 天线有 64 个 RF 端口:

  • 32 个端口连接水平极化振子
  • 32 个端口连接垂直极化振子

为什么测试要分极化:

测试站的 RF 开关矩阵和信号发生器数量有限:

COMBINED 模式 (硬件充足):
  信号发生器 → 开关矩阵 → 同时连接 H 和 V 所有端口
  一次测完

SPLIT_ON_POLARIZATION 模式 (硬件受限):
  信号发生器 → 开关矩阵 → 只能连接一个极化方向的端口
  第一轮: 测 H 极化的 32 个端口
  第二轮: 测 V 极化的 32 个端口

Proto 定义:

enum Polarization {
    POLARIZATION_UNSPECIFIED = 0;
    POLARIZATION_NOT_APPLICABLE = 1;  // 非 OTA 测量(导线直连)
    POLARIZATION_HORIZONTAL = 2;      // 只测水平极化端口
    POLARIZATION_VERTICAL = 3;        // 只测垂直极化端口
    POLARIZATION_BOTH = 4;            // 两个极化同时测
}

在代码中的作用:

极化信息影响两件事:

  1. 开关路径选择 — 告诉 ResourceManager 连接哪组端口
  2. 响应中的端口标识 — 返回给 DUT 时标注每个偏移值对应哪个极化

A.3 Leveling(电平校准)详解

为什么需要: 信号发生器设置 -30 dBm,实际输出可能是 -30.3 或 -29.7 dBm。经过线缆衰减后偏差更大。Leveling 通过功率计实测来消除这个误差。

工作原理:

  • 测试站的 RF 开关矩阵把信号路径切换到功率计(而不是 DUT)
  • Leveling 期间信号不送到 DUT,而是送到功率计做闭环测量
  • 校准完成后再把开关切回 DUT

为什么要关闭其他信号发生器: 如果多个信号发生器共享同一个开关连接点,其他信号会干扰功率计的测量结果。

pathCompensation: 从信号发生器到 DUT 端口之间的线缆、开关、连接器的总衰减值(dB),来自 CompensationManager 数据库,是预先标定好的。

迭代示例:

目标: wantedLevel = -28.0 dBm

迭代 1: measured = -28.5 dBm, adjustment = +0.5 dB → rfLevel 调高
迭代 2: measured = -28.15 dBm, adjustment = +0.15 dB → rfLevel 再调
迭代 3: measured = -28.03 dBm → |差值| = 0.03 < 0.1 → 完成!

signalDeviation = -28.03 - (-28.0) = -0.03 dB

A.4 Arb 特性化详解

为什么需要: Leveling 只保证总功率准确,但多音信号中每个音的电平可能不一致(ARB 的 DAC、滤波器在不同频率上增益不同)。

问题示例:

理想情况 (4个音,总功率 -30 dBm,每音 -36 dBm):
  音1: -36.0 dBm
  音2: -36.0 dBm
  音3: -36.0 dBm
  音4: -36.0 dBm

实际情况 (ARB + 信号发生器有频率响应不平坦):
  音1: -35.5 dBm  ← 偏高 0.5 dB
  音2: -36.2 dBm  ← 偏低 0.2 dB
  音3: -36.8 dBm  ← 偏低 0.8 dB
  音4: -35.9 dBm  ← 偏高 0.1 dB

原因: ARB 的 DAC、重建滤波器、信号发生器的 IQ 调制器
      在不同频率上的增益不一样

Arb 特性化的目标: 测出每个音的实际电平,存入 MultiToneSettings.RFGeneratorLevel,后续在端口偏移计算中使用。


方式选择逻辑:

var possibleToPerformArbWithSpectrumAnalyzer = 
    await _rFGeneratorCharacterization.PossibleToPerformArbCharacterizationWithSpectrumAnalyzer(...);

if (possibleToPerformArbWithSpectrumAnalyzer)
    // 方式 A: 频谱仪 (更精确,能直接测每个音的频谱电平)
else
    // 方式 B: 功率计 (频谱仪不可用时的备选)

判断依据:尝试用频谱仪做一次测量,如果返回 OK 就用频谱仪,否则用功率计。


方式 A(频谱仪)— 完整流程:

步骤 1: 切换到 CW 模式 (关闭 IQ 调制)
│  carrierSignalConditions.RFSignalGeneration.IQ.Enabled = false
│
│  对每个音的频率,逐个设置信号发生器输出 CW:
│    设置频率 → 用功率计测量 → 记录
│    → rfSignalGeneratorLevel[] (功率计视角的各频率电平)
│
步骤 2: 同样 CW 模式,用频谱仪测量
│  对每个音的频率,逐个设置信号发生器输出 CW:
│    设置频率 → 用频谱仪测量 → 记录
│    → spectrumAnalyzerLevel[] (频谱仪视角的各频率电平)
│
步骤 3: 切换回多音模式 (开启 IQ 调制)
│  carrierSignalConditions.RFSignalGeneration.IQ.Enabled = true
│
│  用频谱仪测量多音信号中每个音的电平:
│    → frequencySweepMultiTone[] (多音模式下各音的实际电平)
│
步骤 4: 计算补偿值
│  对每个音:
│    rfLevel[tone] = frequencySweepMultiTone[tone]
│                  + rfSignalGeneratorLevel[tone]
│                  - spectrumAnalyzerLevel[tone]

方式 A 补偿值公式推导:

rfLevel[tone] = frequencySweepMultiTone[tone]   ← 多音模式下频谱仪测到的该音功率
              + rfSignalGeneratorLevel[tone]     ← CW 模式下功率计测到的该频率功率
              - spectrumAnalyzerLevel[tone]      ← CW 模式下频谱仪测到的该频率功率

推导逻辑:

CW 模式下:
  功率计读数 = 信号发生器实际输出 (真值,功率计最准)
  频谱仪读数 = 信号发生器实际输出 + 频谱仪测量误差

  → 频谱仪误差 = spectrumAnalyzerLevel - rfSignalGeneratorLevel

多音模式下:
  频谱仪读数 = 该音的实际功率 + 频谱仪测量误差

  → 该音的实际功率 = frequencySweepMultiTone - 频谱仪误差
                   = frequencySweepMultiTone - (spectrumAnalyzerLevel - rfSignalGeneratorLevel)
                   = frequencySweepMultiTone + rfSignalGeneratorLevel - spectrumAnalyzerLevel

方式 A 数值示例(4 个音):

音1 (3.4985 GHz):
  rfSignalGeneratorLevel = -29.8 dBm  (CW, 功率计测)
  spectrumAnalyzerLevel  = -29.6 dBm  (CW, 频谱仪测)
  frequencySweepMultiTone = -35.2 dBm (多音, 频谱仪测)
  rfLevel[0] = -35.2 + (-29.8) - (-29.6) = -35.4 dBm

音2 (3.4995 GHz):
  rfSignalGeneratorLevel = -30.0 dBm
  spectrumAnalyzerLevel  = -29.9 dBm
  frequencySweepMultiTone = -35.8 dBm
  rfLevel[1] = -35.8 + (-30.0) - (-29.9) = -35.9 dBm

音3 (3.5005 GHz):
  rfSignalGeneratorLevel = -30.1 dBm
  spectrumAnalyzerLevel  = -30.0 dBm
  frequencySweepMultiTone = -36.3 dBm
  rfLevel[2] = -36.3 + (-30.1) - (-30.0) = -36.4 dBm

音4 (3.5015 GHz):
  rfSignalGeneratorLevel = -30.2 dBm
  spectrumAnalyzerLevel  = -30.1 dBm
  frequencySweepMultiTone = -35.5 dBm
  rfLevel[3] = -35.5 + (-30.2) - (-30.1) = -35.6 dBm

结果: rfLevel = [-35.4, -35.9, -36.4, -35.6]
→ 音3 偏低,音1 偏高

方式 B(功率计)— 完整流程:

步骤 1: ALC 功率搜索 (建立基准)
│  在中心频率附近 (+50 kHz 偏移) 生成单音波形
│  用功率计测量,建立 ALC 参考电平
│
步骤 2: 逐音测量
│  对每个音:
│    生成只含该音的单音 IQ 波形 (用 RFStimuliCalculate.SinusoidalSignal)
│    上传到 ARB → 播放 → 用功率计测量
│    → measuredToneLevels[tone] = 功率计读数
│
│  注意: 如果音数为奇数,中心音 (频率=载波频率) 无法测量
│        因为 ARB 基带信号在 0 Hz 时无法产生有效输出
│        → 跳过中心音,后续用相邻音平均值填充
│
步骤 3: 恢复原始多音波形
│  切换回正确的多音波形,恢复 ALC 状态

方式 B 补偿值公式:

singleToneReduction = 10 * log10(N + 1)
rfLevel[tone] = measuredToneLevel[tone] - singleToneReduction

为什么减去 10*log10(N+1)

测量时用的是单音波形(只有一个音),功率计测到的是该音独占全部 DAC 动态范围时的功率。
实际多音波形中,N 个音分享 DAC 动态范围,每个音的功率更低。
10*log10(N+1) 是从单音功率换算到多音场景下等效功率的修正因子。
(N+1 而不是 N 是经验修正,考虑了 DAC 满量程利用率差异)

方式 B 数值示例(4 个音):

singleToneReduction = 10 * log10(4 + 1) = 6.99 dB

音1: 功率计测量 = -28.5 dBm (单音波形)
     rfLevel[0] = -28.5 - 6.99 = -35.49 dBm

音2: 功率计测量 = -29.0 dBm
     rfLevel[1] = -29.0 - 6.99 = -35.99 dBm

音3: 功率计测量 = -29.5 dBm
     rfLevel[2] = -29.5 - 6.99 = -36.49 dBm

音4: 功率计测量 = -28.7 dBm
     rfLevel[3] = -28.7 - 6.99 = -35.69 dBm

后续处理(两种方式共用):

步骤 5: 验证音间一致性
│  相邻音差: |rfLevel[tone] - rfLevel[tone-1]|
│    → 超过 0.5 dB → 抛异常 (硬件可能有问题)
│  最大最小差: max(rfLevel) - min(rfLevel)
│    → 超过 1.5 dB → 抛异常
│
步骤 6: Hilbert 变换求相位
│  phase[] = RFStimuliCalculate.HilbertTransform(rfLevel)
│  → 从幅度响应推算最小相位响应
│  → 假设: ARB + 信号发生器是最小相位系统
│
步骤 7: 处理中心音 (奇数个音时)
│  中心音频率 = 载波频率,无法测量
│  → 用相邻两个音的平均值填充:
│     middleTone.Level = (lowTone.Level + highTone.Level) / 2
│     middleTone.Phase = (lowTone.Phase + highTone.Phase) / 2
│
步骤 8: 保存结果
│  存入 carrierSignalConditions.MultiToneSettings.RFGeneratorLevel[]
│  每个音包含: {Index, Frequency, Level, Phase}
│
│  同时保存到缓存 (ArbCalibrationContainer):
│  → 14 天内同条件不需要重新测量

最终结果怎么用:

rfLevel[]phase[] 存入 MultiToneSettings.RFGeneratorLevel,在后续 MultiToneLevelResponseAsync 中用于计算返回给 DUT 的每个音的电平和相位(见 A.14)。

A.5 端口偏移计算详解

路径衰减数据来源: 测试站投入使用前做测试站校准,用网络分析仪/功率计测量每条路径在各频率上的衰减,存入 CompensationManager 数据库。不是实时测的,是查表。

单载波公式:

offset[port] = referenceAttenuation - portAttenuation + signalDeviation
  • referenceAttenuation:第一个端口路径的衰减(作为基准)
  • portAttenuation:该端口路径的衰减
  • signalDeviation:Leveling 残余偏差

DUT 用法:端口实际接收电平 = 标称电平 + offset

多音公式(每个音、每个端口):

toneLevel = rfGenLevel[tone] - toneLoss[tone] + referenceAtt - portAtt
phase = S21.Phase + rfGenPhase + metadataPhase
  • rfGenLevel:Arb 特性化得到的该音在发生器输出端的实际电平
  • toneLoss:该音频率上的路径损耗 = -20×log10(|S21|)
  • S21 来自 GetTwoPortAsync(),提供每个频率的精确衰减和相移

相位归一化: 所有音的相位减去第一个音的相位,DUT 关心的是音间相对相位。

精度链总结:

1. 测试站校准 (离线) → 路径衰减/S参数 → 存数据库
2. Leveling (在线) → 补偿发生器不确定性 → 精度 < 0.1 dB
3. Arb 特性化 (在线,多音) → 补偿各音非线性 → 相邻音差 < 0.5 dB
4. 端口偏移计算 (在线) → 查表计算 → 精度取决于标定质量

A.6 步骤 2.5/2.6/2.7 的触发条件

并非每次 Request 都会执行所有步骤,每个步骤都有前置条件判断。

步骤 2.5:Leveling(电平校准)触发条件:

// PerformLevelingAsync 开头的检查
if (!carrierSignalConditions.LevelingSupport)   // 开关网络不支持 Leveling
    return 0.0;                                  // → 跳过

if (!carrierSignalConditions.OutputEnabled)      // 输出未开启
    return 0.0;                                  // → 跳过

即使进入了 Leveling,还有缓存机制

var (levelingUpdate, levelingRFlevel, signalDeviation) =
    _levelingContainer.GetPerformedLeveling(序列号, 频率, 目标电平, 波形名);

if (levelingUpdate)
    // 缓存未命中 → 执行实际闭环校准(功率计迭代)
else
    // 缓存命中(14天内同条件做过)→ 直接用缓存值,不测量
情况 是否执行
开关网络不支持 Leveling 跳过
信号输出未开启 跳过
14 天内同仪器、同频率、同功率、同波形做过 用缓存,不实测
以上都不满足 执行闭环校准

步骤 2.6:Arb 特性化触发条件(更严格,三个条件必须同时满足):

// ArbCharacterizationAsync 开头的检查
if (carrierSignalConditions.MultiToneSettings.Frequencies.Count == 0)  // 不是多音
    return;                                                             // → 跳过

if (!carrierSignalConditions.OutputEnabled)                            // 输出未开启
    return;                                                             // → 跳过

if (!carrierSignalConditions.ModulationEnabled)                        // 调制未开启
    return;                                                             // → 跳过

进入后还有两层判断:

if (!carrierSignalConditions.ArbCharacterizationSupport)
    // 开关网络不支持 → 用波形元数据的默认值,不实测
    RFGeneratorLevelDefault(...);
    return;

var arbCalibrationData = _arbCalibrationContainer.GetPerformedArbCalibration(...);
if (arbCalibrationData.Update)
    // 缓存未命中 → 执行实际特性化(频谱仪或功率计)
else
    // 缓存命中 → 用缓存数据 + 电平修正
情况 是否执行
CW 信号 跳过
调制载波(非多音) 跳过
多音但输出未开启 跳过
多音但调制未开启 跳过
多音但开关网络不支持 Arb 特性化 用默认值,不实测
14 天内同条件做过 用缓存
以上都不满足 执行实际特性化

步骤 2.7:端口偏移计算(总是执行,但走不同分支):

基类中两个方法都会调用,但每个方法内部自行判断是否有事可做:

SignalLevelResponseAsync(单载波响应):

if (多音 && 调制已开启)  → return 空 (多音走另一个方法)
if (!输出已开启)         → return 空 (没输出就没偏移)
否则                     → 计算端口偏移

MultiToneLevelResponseAsync(多音响应):

if (无多音频率)    → return 空 (不是多音)
if (!输出已开启)   → return 空
if (!调制已开启)   → return 空
否则               → 计算每个音的电平和相位
信号类型 SignalLevelResponse MultiToneLevelResponse
CW ✅ 计算端口偏移 ❌ 返回空
调制载波 ✅ 计算端口偏移 ❌ 返回空
多音(调制开启) ❌ 返回空 ✅ 计算每个音的电平+相位
多音(调制关闭) ✅ 计算端口偏移 ❌ 返回空

总结:

每个 Signal 进入处理流程:

步骤 2.5 Leveling:
  是多音? 是CW? 是调制? → 都可能需要,关键看:
  ├── LevelingSupport == false → 跳过
  ├── OutputEnabled == false → 跳过
  ├── 缓存命中 → 用缓存
  └── 否则 → 实测

步骤 2.6 Arb 特性化:
  ├── 不是多音 → 跳过 (CW 和调制载波都跳过)
  ├── 是多音但输出/调制未开启 → 跳过
  ├── 不支持 Arb 特性化 → 用默认值
  ├── 缓存命中 → 用缓存
  └── 否则 → 实测

步骤 2.7 端口偏移:
  └── 总是执行,但根据信号类型走不同分支

对于最常见的 CW 信号,步骤 2.6 完全跳过,步骤 2.5 如果缓存命中也只是查表设值,整个流程很快。只有多音信号 + 缓存未命中时才会走完所有步骤。

A.7 波形加载工作流

DUT 不指定具体波形文件名,而是描述"我需要什么样的信号",测试站自动匹配。

DUT 请求示例:

调制载波:
  CarrierSignal {
      carrier_type: CARRIER_TYPE_N100000_122880   ← "NR TDD 100MHz"
      model: TEST_MODEL_TM3p1a                    ← "测试模型 TM3.1a"
      carrier_freq_hz: 3500000000                 ← "载波 3.5 GHz"
  }

多音:
  MultiToneSignal {
      no_tones: 7                                 ← "7 个音"
      sep_hz: 1000000                             ← "间隔 1 MHz"
  }

测试站侧匹配流程:

步骤 1: 构造波形查询请求
│
│  基类 GetWaveFormDataAsync() 中:
│  ├── 调制载波: WaveformRequestBuilder
│  │       .ConfigureTestModel(TM3p1a)
│  │       .ConfigureCarrierType(N100000_122880)
│  │       .ConfigureRadioFrequencyRange(3.5e9)
│  │       .Build()
│  └── 多音: WaveformRequestBuilder
│          .ConfigureMultitone(7, 1000000)
│          .Build()
│
步骤 2: 查询波形库 (独立 gRPC 微服务)
│
│  packageInfo = await _waveformLibrary.GetFilePackageInfoAsync(request)
│  → 波形库根据 CarrierType + TestModel 在数据库中匹配
│  → 返回 packageId (波形包唯一标识)
│  → 无匹配 → 抛异常 "Not a supported Waveform"
│
步骤 3: 下载元数据文件 (JSON)
│
│  waveformMetadata = await _waveformLibrary.DownloadFileAsync(
│      packageId, WaveformFileKind.Meta)
│
│  元数据包含:
│  ├── SampleRate (采样率)
│  ├── CarrierMetaData[].Par (峰均比)
│  ├── MultiTones[] (多音: 频率偏移、相位、电平)
│  ├── DisableLinearization (是否禁用线性化)
│  └── Samples (采样点数)
│
步骤 4: 下载 IQ 波形文件
│
│  waveformIqData = await _waveformLibrary.DownloadFileAsync(
│      packageId, WaveformFileKind.Iq)
│
│  IQ 文件格式 (Tab 分隔):
│      0.0312\t0.0156
│      0.0625\t0.0234
│      每行: I值<Tab>Q值
│
步骤 5: 上传到 ARB 发生器
│
│  子类 GetWaveformAsync() 中:
│  ├── 生成波形唯一 key = hash(IQ文件hash + 元数据hash) 后 8 位
│  ├── 检查 ARB 是否已存储该波形
│  │   ├── 已存储 → 跳过上传 (省时间)
│  │   └── 未存储 → 读取 IQ 文件,解析为 Complex 数组
│  │
│  基类 SetupSignalGenerator() 中:
│  ├── arbGeneration.WriteWaveform(clockFreq, waveformName, iqData)
│  ├── arbGeneration.SelectedWaveform = waveformName
│  └── arbGeneration.WaveformEnabled = true → 开始播放

波形库架构:

波形库 (IWaveformLibrary / WaveformLibraryService) 是 Raptor2 平台的独立微服务,通过 NuGet 包 Ericsson.Raptor2.ServiceWrappers 以编译好的 DLL 提供给 RFStimuli 使用。其 proto 定义和服务实现在 Ericsson 内部的其他代码仓库中。

波形库索引文件(实际数据):

波形库的核心是 Waveforms/Index.json,一个 JSON 数组,每条记录对应一个波形包:

{"Hash":"1657D8078A31F80A36DCA99CD158A8FB","CarrierType":"L50007680","TestModel":"TM1P1",
 "Bandwidth":5000000.0,"TotalNumberOfCarriers":1,"NumberOfCarriersPerBranch":1,
 "WaveformUsage":"Uplink,Performance","IsMultitone":false}
字段 含义 匹配作用
Hash 波形包目录名(MD5) 定位文件路径
CarrierType 载波类型 主匹配 key
TestModel 测试模型 主匹配 key
Bandwidth 信号带宽 多音匹配用
TotalNumberOfCarriers 载波数 辅助筛选
NumberOfCarriersPerBranch 每分支载波数 辅助筛选
WaveformUsage 用途 (Uplink/Downlink/Calibration等) 筛选条件
IsMultitone 是否多音 区分匹配策略

调制载波匹配示例:

DUT 请求: CarrierType=L50007680, TestModel=TM1P1, Usage=Uplink

在 Index.json 中查找:
  CarrierType == "L50007680"   ✅
  TestModel == "TM1P1"         ✅
  WaveformUsage 包含 "Uplink"  ✅
  TotalNumberOfCarriers == 1   ✅

→ 命中 Hash = "1657D8078A31F80A36DCA99CD158A8FB"
→ 波形文件在: Waveforms/1657D8078A31F80A36DCA99CD158A8FB/
    ├── E_UTRA_UL_5_15_36MHz.wf        ← IQ 数据
    └── WaveformMetadata.json           ← 元数据

多音匹配示例(CarrierType 和 TestModel 都是 UNSPECIFIED):

多音不按 CarrierType/TestModel 匹配,而是按 Bandwidth + IsMultitone 匹配:

DUT 请求: 44 tones, sep=960kHz → Bandwidth ≈ 44 × 960000 = 42240000

在 Index.json 中查找:
  IsMultitone == true          ✅
  WaveformUsage 包含 "Uplink"  ✅
  Bandwidth ≈ 41280000         ✅ (最接近的)

→ 命中 Hash = "213BBA3730CA7C27A29A10D84FA426D0"

波形元数据示例(调制载波):

{
  "Samples": 153600,
  "SampleRate": 15360000.0,
  "IsMultitone": false,
  "CarrierType": "L50007680",
  "TestModel": "Tm1P1",
  "CarrierMetaData": [{
    "Bandwidth": 5000000.0,
    "Par": -1.0,
    "CenterFrequency": 0.0
  }],
  "MultiTones": null
}

波形元数据示例(多音):

{
  "Samples": 256,
  "SampleRate": 122880000.0,
  "IsMultitone": true,
  "CarrierMetaData": [{"Par": 4.211, "Bandwidth": 41280000.0}],
  "MultiTones": [
    {"Index": 86, "Frequency": -20640000.0, "Level": -24.0824, "Phase": -1.151},
    {"Index": 88, "Frequency": -19680000.0, "Level": -24.0824, "Phase": -0.6695},
    ...
  ]
}

MultiTones 中每个音的字段:

  • Index:音在 FFT bin 中的索引
  • Frequency:相对载波中心的频率偏移 (Hz)
  • Level:该音的相对电平 (dB)
  • Phase:该音的初始相位 (rad)

IQ 波形文件格式(.wf 文件):

Tab 分隔的文本文件,每行一个 I/Q 采样点:

0.031250    0.015625
0.062500    0.023438
-0.015625   0.046875
...
每行: I值<Tab>Q值 (归一化浮点数,范围约 -1 到 +1)

上传到 R&S SGT100A 的 SCPI 流程:

步骤 1: 读取 IQ 文件,解析为 I/Q 数组
│
步骤 2: 配置 ARB 基带参数
│  SCPI: :SOURce:BB:ARB:CLOCk {sample_rate}       ← 设置采样率
│  SCPI: :SOURce:BB:ARB:TRIGger:SOURce IMMediate  ← 立即触发
│
步骤 3: 上传波形数据到信号发生器内存
│  将 I/Q 浮点数组打包为二进制 (IEEE 754 float32, 交替 I0,Q0,I1,Q1,...)
│  SCPI: :SOURce:BB:ARB:WAVeform:DATA "{name}",#<header><binary_iq_data>
│
步骤 4: 选择并播放波形
│  SCPI: :SOURce:BB:ARB:WAVeform:SELect "{name}"  ← 选择波形
│  SCPI: :SOURce:BB:ARB:STATe ON                   ← 开启 ARB 播放
│
步骤 5: 开启 IQ 调制
│  SCPI: :IQ:STATe ON                              ← 开启 IQ 调制器
│  SCPI: :OUTPut ON                                ← 开启 RF 输出

Python 实现示例:

import struct

def upload_waveform_to_sgt(gpib_client, wf_file_path, waveform_name, sample_rate):
    """上传 IQ 波形到 R&S SGT100A"""
    
    # 1. 读取 IQ 文件
    iq_pairs = []
    with open(wf_file_path, 'r') as f:
        for line in f:
            line = line.strip()
            if line:
                parts = line.split('\t')
                iq_pairs.append((float(parts[0]), float(parts[1])))
    
    # 2. 打包为二进制 (交替 I,Q float32)
    binary_data = b''
    for i_val, q_val in iq_pairs:
        binary_data += struct.pack('<ff', i_val, q_val)  # little-endian float32
    
    # 3. 构造 IEEE 488.2 块头
    data_len = len(binary_data)
    len_str = str(data_len)
    header = f'#{len(len_str)}{len_str}'
    
    # 4. 设置采样率
    gpib_client.send_and_receive(f':SOURce:BB:ARB:CLOCk {sample_rate}')
    
    # 5. 上传波形数据
    cmd = f':SOURce:BB:ARB:WAVeform:DATA "{waveform_name}",{header}'
    gpib_client.send_raw((cmd.encode() + binary_data + b'\n'))
    gpib_client.query('*OPC?')
    
    # 6. 选择波形并播放
    gpib_client.send_and_receive(f':SOURce:BB:ARB:WAVeform:SELect "{waveform_name}"')
    gpib_client.send_and_receive(':SOURce:BB:ARB:STATe ON')
    
    # 7. 开启 IQ 调制
    gpib_client.send_and_receive(':IQ:STATe ON')
    gpib_client.query('*OPC?')

ALC(Automatic Level Control)说明:

ALC 是信号发生器内部的自动电平控制闭环。在 Arb 特性化时需要关闭它:

  • ALC 开启:内部功率检测器实时监测输出,自动调整使输出稳定在设定值
  • ALC 关闭(DisabledTable):放大器/衰减器锁定在当前工作点,不再自动调整
  • Arb 特性化时关闭 ALC,才能测出各频率上的真实差异(否则 ALC 会把差异消除)

Python 实现现状: 当前 Python 版本没有波形库,调制信号只设置频率和功率,不加载 IQ 波形。CW 信号不需要波形,可以正常工作。有了上述波形文件和上传逻辑后,可以实现完整的调制信号支持。

A.8 功率计工作流

功率计用于 Leveling(电平校准),通过实测信号功率来闭环调整信号发生器输出。

硬件架构:

Raptor2 测试站中的功率计架构:

ExternalSignalStimuli (Leveling 调用方)
    │  gRPC 双向流
    ▼
RFPowerMeasureService (gRPC 服务层)
    │  RFPowerMeasureStreamAsync()
    ▼
RFPowerMeasurement (业务逻辑层)
    │  MeasureContinuousPowerAsync()
    ▼
ResourceManager → 分配功率计 + 连接开关路径
    │
    ▼
RohdeSchwarzNrpxxS (驱动层, SCPI 命令)
    │
    ▼
R&S NRP8S / NRP40S 功率计硬件

支持的功率计型号:

型号 连接方式 驱动
R&S NRP8S / NRP8SN USB 或 LAN Rcm.PM.RohdeSchwarzNrpxxS
R&S NRP40S / NRP40SN USB 或 LAN Rcm.PM.RohdeSchwarzNrpxxS
R&S NRP-Z11 / NRP-Z21 USB (RSNRP) Rcm.PM.RohdeSchwarzNrpZxx
R&S NRQ LAN Rcm.PM.RohdeSchwarzNrq
Keysight EPM / U20xxx GPIB/USB Rcm.PM.KeysightEpm / KeysightU20xxx

Leveling 中功率计的调用流程:

步骤 1: 打开流式连接
│  using (var pmClient = _rFPowerMeasurement.RFPowerMeasureStream())
│  → 建立 gRPC 双向流,功率计被锁定给当前调用者
│
步骤 2: 发送测量请求
│  var request = RfPowerMeasurementStreamRequestBuilder
│      .ConfigureCorrectionFrequency(频率, 带宽)
│      .ConfigureInstrumentConnection(连接点, connectId)
│      .Build()
│  await pmClient.RequestStream.WriteAsync(request)
│
步骤 3: 功率计配置 (服务层 SetupPM)
│  driver.Measurement.CorrectionFrequency = freq  → SCPI: SENS:FREQ {freq}
│  driver.Measurement.Mode = Average              → SCPI: SENS:FUNC "POW:AVG"
│  driver.Trigger.Source = Immediate               → SCPI: TRIG:SOUR IMM
│  driver.Trigger.Count = 1                        → SCPI: TRIG:COUN 1
│
步骤 4: 执行测量 (驱动层 ReadPowerCore)
│  SCPI: STAT:OPER:MEAS:EVEN?;:STAT:OPER:TRIG:EVEN?  ← 清除事件寄存器
│  SCPI: ABOR;INIT:IMM                                 ← 中止当前+启动新测量
│  轮询: STAT:OPER:MEAS:EVEN? 直到 bit1=1              ← 等待完成
│  SCPI: FETCH?                                         ← 读取结果 (瓦特值)
│
步骤 5: 结果转换
│  watt = float(response)           ← 功率计返回瓦特值 (如 8.641561E-12)
│  dBm = 10 * log10(watt) + 30     ← 转换为 dBm
│  dBm += path_compensation         ← 加上路径补偿 (如果启用)
│
步骤 6: 返回给 Leveling 调用方
│  measured = pmClient.ResponseStream.Current.Data.Scalar.DoubleValue
│
步骤 7: Leveling 迭代 (重复步骤 2-6)
│  while |measured - wanted| >= 0.1 dB:
│      调整信号发生器电平
│      发送新的测量请求 → 读取新的结果
│
步骤 8: 关闭流
│  await pmClient.RequestStream.CompleteAsync()
│  → 功率计被释放回资源池

关键 SCPI 命令汇总 (R&S NRP 系列):

步骤 SCPI 命令 含义
复位 *RST 恢复默认设置
字节序 FORM:BORD SWAP 设置小端字节序
关闭自动平均 SENS:AVER:COUN:AUTO OFF 手动控制平均次数
设置校正频率 SENS:FREQ {hz} 功率计内部频率补偿
设置测量模式 SENS:FUNC "POW:AVG" 平均功率模式
设置触发源 TRIG:SOUR IMM 立即触发
清除事件 STAT:OPER:MEAS:EVEN?;:STAT:OPER:TRIG:EVEN? 清除状态寄存器
启动测量 ABOR;INIT:IMM 中止当前+启动新测量
等待完成 轮询 STAT:OPER:MEAS:EVEN? bit1=1 表示完成
读取结果 FETCH? 返回瓦特值
零点校准 CAL:ZERO:AUTO ONCE 校准前需断开信号

瓦特转 dBm:

import math
def watt_to_dbm(watt):
    if watt <= 0:
        return -999.0
    return 10 * math.log10(watt) + 30
# 例: 1e-5 W = 10 μW → watt_to_dbm(1e-5) = -20.0 dBm

A.9 路径补偿 (CompensationManager) 工作流

路径补偿提供从信号发生器到 DUT 各端口之间的线缆/开关衰减数据,是端口偏移计算和 Leveling 的基础。

数据来源 — 测试站校准(离线):

校准时 (用网络分析仪测量每条路径):

网络分析仪 ──> 测量每条路径的 S 参数
    │
    ▼
生成 Touchstone 文件 (.s2p 格式)
    │
    ▼
存储到:
    ├── 本地: C:\TestStation\Calibration\Calibration.xml (加密 XML)
    └── RTD 数据库 (集中存储,通过 CompensationProvider 推送)

数据格式(概念):

路径 "SG1_to_DUT_Port1":
    频率(Hz)       衰减(dB)   S21幅度   S21相位(rad)
    1,000,000,000  2.8        0.724     -0.52
    2,000,000,000  3.1        0.699     -1.05
    3,000,000,000  3.5        0.668     -1.58
    4,000,000,000  3.9        0.638     -2.11
    5,000,000,000  4.3        0.610     -2.64

路径 "SG1_to_DUT_Port2":
    频率(Hz)       衰减(dB)   S21幅度   S21相位(rad)
    1,000,000,000  3.0        0.708     -0.55
    ...

运行时查询流程:

ExternalSignalStimuli 调用:
│
│  attenuation = await _compensationManager.GetAttenuationAsync(
│      sessionID,
│      CompensationsKeys {
│          DutPosition, FactoryCode, TestStationID,
│          SwitchNetworkProductNumber, SwitchNetworkRState
│      },
│      path = "SG1_to_DUT_Port1",    ← 路径名 (来自开关矩阵连接响应)
│      frequency = 3.5e9              ← 当前信号频率
│  )
│
▼
CompensationManager 内部:
│
├── 1. 用 CompensationsKeys 定位正确的校准数据集
│      (支持不同工厂、不同测试站的数据隔离)
│
├── 2. 用 path 名找到对应路径的衰减表
│
├── 3. 用 frequency 做线性插值
│      例: 3.5 GHz 在 3.0 GHz (3.5dB) 和 4.0 GHz (3.9dB) 之间
│      插值: 3.5 + (3.9-3.5) × 0.5 = 3.7 dB
│
└── 4. 返回 3.7 dB

两种查询接口:

方法 返回 用途
GetAttenuationAsync(path, freq) double (dB) 单载波 Leveling 和端口偏移
GetTwoPortAsync(path, freqList) TwoPortCompensationData[] 多音端口偏移(需要每个音的衰减+相位)

路径名从哪来:

RequestExternalStimuli 流程中:
1. ResourceManager 分配信号发生器 + 开关路径
2. 返回 SwitchConnectResponse.ConnectionDetails[]
3. 每个 ConnectionDetail 包含:
   ├── From: {PortName: "SG1"}                    ← 信号发生器端
   ├── To: {PortName: "1", Polarization: H}       ← DUT 端口
   └── CompensationPath: "SG1_to_DUT_Port1"       ← 路径名 ★
4. 这个 CompensationPath 就是查询补偿数据的 key

在 Leveling 中的使用:

pathCompensation = GetAttenuationAsync(path, frequency)  → 3.7 dB
wantedLevel = stimuliLevel + pathCompensation
            = -30 + 3.7 = -26.3 dBm  ← 信号发生器端的目标电平

在端口偏移中的使用:

refAtt   = GetAttenuationAsync("SG1_to_Port1", freq) = 3.7 dB
port2Att = GetAttenuationAsync("SG1_to_Port2", freq) = 4.1 dB

offset[port2] = refAtt - port2Att + signalDeviation
              = 3.7 - 4.1 + (-0.03) = -0.43 dB
→ 告诉 DUT: 端口2 比端口1 弱 0.43 dB

A.10 Leveling 缓存匹配机制

Leveling 结果缓存在本地 JSON 文件中,避免重复校准。

存储位置: C:\TestStation\Leveling\LevelingV2.json

存储结构:

{
    "仪器序列号A": [
        {
            "SerialNumber": "1419.4501k02/110489",
            "Frequency": 3500000000.0,
            "WantedLevel": -26.3,
            "RFLevel": -26.45,
            "SignalDeviation": -0.03,
            "Waveform": "cw",
            "ExpirationDate": "2026-05-13T..."
        },
        { ... }
    ],
    "仪器序列号B": [...]
}

匹配逻辑(GetPerformedLeveling 方法):

第一步: 按序列号查找
│  _levelingCapabilities.TryGetValue(SerialNumber)
│  → 找不到 → 返回 Update=true,需要实测
│
第二步: 在该仪器的记录中匹配三个条件 (必须同时满足)
│  Frequency.Equals(freq, 容差=1 Hz)
│  WantedLevel.Equals(level, 容差=0.1 dB)
│  Waveform == waveform (精确匹配)
│  → 无匹配 → 返回 Update=true,需要实测
│
第三步: 检查过期
│  ExpirationDate < DateTime.Now?
│  → 过期 → 返回 Update=true,需要实测
│  → 未过期 → 返回 (Update=false, RFLevel, SignalDeviation)
匹配字段 容差 含义
SerialNumber 精确 不同仪器的输出特性不同
Frequency ±1 Hz 频率必须几乎完全相同
WantedLevel ±0.1 dB 目标电平允许微小差异
Waveform 精确 "cw" 或波形文件名必须完全一样
ExpirationDate 14 天 超过 14 天需要重新校准

缓存未命中的典型场景:

场景 原因
换了信号发生器 序列号不同
换了频率 (3.5→3.6 GHz) 频率不匹配
换了功率 (-30→-25 dBm) WantedLevel 不匹配
换了波形 ("cw"→"a1b2c3d4.wf") 波形名不匹配
超过 14 天 过期

Arb 特性化的缓存(ArbCalibrationContainer)机制完全相同,只是额外存储了多音电平数据。

A.11 波形库匹配机制

DUT 不指定具体波形文件名,而是描述信号需求,波形库按条件精确匹配。

调制载波查询 key:

WaveformRequestBuilder
    .ConfigureUsage([WaveformUsage.Uplink])
    .ConfigureTestModel(TM3p1a)
    .ConfigureCarrierType(N100000_122880)
    .ConfigureNumberOfCarriers(1, 1)
    .ConfigureRadioFrequencyRange(3.5e9)
    .Build()

多音查询 key:

WaveformRequestBuilder
    .ConfigureUsage([WaveformUsage.Uplink])
    .ConfigureMultitone(7, 1000000)
    .Build()

匹配维度:

维度 调制载波示例 多音示例 含义
Usage Uplink Uplink 上行链路波形(RX 测试用)
CarrierType N100000_122880 NR TDD 100MHz @ 122.88 MSPS
TestModel TM3p1a 3GPP 测试模型
NumberOfCarriers 1 单载波
RadioFrequencyRange 3.5e9 验证波形适用于该频率
NumberOfTones 7 音数量
SeparationFrequency 1000000 音间隔 Hz

波形库内部匹配(推断自测试代码和文件结构):

波形库存储结构:
Waveforms/
├── Uplink/
│   ├── Carrier/
│   │   ├── NR_TDD_100MHz_TM3p1a/
│   │   │   ├── FilePackageInfo.json
│   │   │   ├── WaveformMetadata.json
│   │   │   └── NR_TDD_100M.wf
│   │   └── LTE_FDD_20MHz_TM1p1/
│   │       └── ...
│   └── Multitone/
│       ├── 7tone_1MHz/
│       │   └── ...
│       └── ...

CarrierType + TestModel 的组合唯一确定一个波形包,不是模糊匹配。无匹配时抛异常 "Not a supported Waveform"

A.12 路径补偿匹配机制

路径补偿的匹配分两层:先定位数据集,再定位路径+频率

第一层:CompensationsKeys 定位数据集

new CompensationsKeys()
{
    DutPosition = 1,                              // DUT 物理位置
    FactoryCode = "A23",                          // 工厂代码
    TestStationID = "7239",                       // 测试站 ID
    SwitchNetworkProductNumber = "KRD123456",     // 开关网络产品号
    SwitchNetworkRState = "R1A"                   // 开关网络版本
}
Key 为什么需要
TestStationID 不同测试站的线缆不同,衰减不同
FactoryCode 不同工厂的测试站配置可能不同
SwitchNetworkProductNumber + RState 换了开关网络版本,校准数据也变了
DutPosition 同一测试站不同 DUT 位置的线缆长度不同

这些 key 组合起来唯一确定用哪一套校准数据

第二层:path + frequency 定位具体值

GetAttenuationAsync(sessionID, compensationsKeys, path, frequency)
  • path:路径名,来自 ResourceManager 返回的 SwitchConnectResponse.ConnectionDetails[].CompensationPath
  • frequency:当前信号频率,在路径的衰减表中做线性插值

path 从哪来:

ResourceManager 分配信号发生器时返回:

ConnectionDetails[0]:
    From: {PortName: "SG1"}                       ← 信号发生器端口
    To: {PortName: "1", Polarization: Horizontal}  ← DUT 端口
    CompensationPath: "SG1_DUT1_H"                ← 路径名 ★

路径名是开关矩阵在连接时自动生成的,
描述了信号从哪到哪经过了哪些开关。
RFStimuli 不需要知道路径名的含义,只是把它当作 key 传给 CompensationManager。

frequency 匹配 — 线性插值:

校准数据 (路径 "SG1_DUT1_H"):
    3.0 GHz → 3.5 dB
    3.5 GHz → 3.7 dB
    4.0 GHz → 3.9 dB

查询 3.5 GHz → 精确命中,返回 3.7 dB
查询 3.2 GHz → 插值: 3.5 + (3.7-3.5) × (3.2-3.0)/(3.5-3.0) = 3.58 dB
查询 6.0 GHz → 超出范围 → 抛异常 (插值距离太大)

完整匹配流程:

GetAttenuationAsync(session, keys, "SG1_DUT1_H", 3.5e9)
│
├── 1. 用 CompensationsKeys 定位数据集
│      → 找到测试站 7239 + 开关网络 KRD123456/R1A 的校准数据
│
├── 2. 用 path="SG1_DUT1_H" 找到该路径的衰减表
│      → 一组 (频率, 衰减) 数据点
│
├── 3. 用 frequency=3.5e9 做线性插值
│      → 返回 3.7 dB
│
└── 4. 返回 3.7

三层精确定位:CompensationsKeys 选哪套数据,path 选哪条路径,frequency 在路径数据中插值。没有模糊匹配。

A.13 路径补偿的两种用途与端口偏移的作用

路径补偿在两个地方使用,作用完全不同:

用途一:Leveling 中 — 配置信号发生器(影响硬件)

pathCompensation = GetAttenuationAsync(参考路径, frequency) → 3.7 dB

wantedLevel = stimuliLevel + pathCompensation
            = -30 + 3.7 = -26.3 dBm

→ 信号发生器设置为 -26.3 dBm
→ 经过 3.7 dB 衰减后,DUT 端口收到 ≈ -30 dBm

用途二:端口偏移计算 — 只返回给 DUT(不影响硬件)

offset[port] = referenceAttenuation - portAttenuation + signalDeviation

→ 这个结果放在响应消息中返回给 DUT
→ 不用于配置信号发生器(信号发生器只有一个输出电平,已在 Leveling 中设好)

两者对比:

Leveling 中的路径补偿 端口偏移计算
目的 设置信号发生器输出电平 告诉 DUT 各端口的差异
影响硬件? ✅ 直接设置信号发生器 ❌ 不影响任何硬件
结果去哪? 内部使用,不返回给 DUT 放在响应消息中返回给 DUT
用哪条路径? 参考路径(第一个连接) 所有端口的路径

为什么信号发生器不能同时满足所有端口:

信号发生器 ──┬── 路径1 (3.2 dB) ── 端口1
             ├── 路径2 (3.5 dB) ── 端口2
             ├── 路径3 (3.8 dB) ── 端口3
             └── 路径4 (3.1 dB) ── 端口4

信号发生器只有一个输出电平,不可能同时满足所有端口。
所以:
  1. Leveling 用参考路径的衰减来设置发生器电平 → 保证参考路径精确
  2. 端口偏移告诉 DUT 其他端口相对参考路径的差异 → DUT 自己补偿

DUT 如何使用端口偏移:

DUT 收到响应:
  端口1: offset = -0.03 dB
  端口2: offset = -0.33 dB
  端口3: offset = -0.63 dB

DUT 计算每个端口的真实输入电平:
  端口1 真实输入 = -30 + (-0.03) = -30.03 dBm
  端口2 真实输入 = -30 + (-0.33) = -30.33 dBm
  端口3 真实输入 = -30 + (-0.63) = -30.63 dBm

DUT 用这些真值校准 RX 增益:
  端口2 增益 = ADC读数 - (-30.33)
  → 写入校准表,以后正常工作时用这个表补偿

A.14 多音信号的端口偏移计算

多音比单载波复杂,每个音在每个端口都有独立的电平和相位。

电平公式:

toneLevel[port][tone] = rfGenLevel[tone] - toneLoss[tone] + refAtt - portAtt
参数 来源 含义
rfGenLevel[tone] Arb 特性化结果 该音在信号发生器输出端的实际电平
toneLoss[tone] S 参数: -20×log10(|S21|) 该音频率上这条路径的损耗
refAtt CompensationManager (中心频率) 参考路径的标量衰减
portAtt CompensationManager (中心频率) 该端口的标量衰减

为什么多音用 S21 而不是标量衰减:

单载波: 只有一个频率,标量衰减够用
多音: 每个音频率不同,路径在不同频率上衰减不同

例如 3 个音 (3.499, 3.500, 3.501 GHz):
  S21[3.499 GHz]: |S21|=0.680 → toneLoss = 3.35 dB
  S21[3.500 GHz]: |S21|=0.668 → toneLoss = 3.51 dB
  S21[3.501 GHz]: |S21|=0.655 → toneLoss = 3.67 dB

每个音的损耗不一样,需要 S 参数提供逐频率的精确值

相位公式:

phase[port][tone] = S21.Phase[tone] + rfGenPhase[tone] + metadataPhase[tone]
参数 来源 含义
S21.Phase[tone] S 参数 该音频率上路径的相移
rfGenPhase[tone] Arb 特性化 Hilbert 变换 信号发生器 ARB 引入的相位
metadataPhase[tone] 波形元数据 波形设计时定义的相位

相位归一化(相对第一个音):

if toneIndex == 0:
    relativePhase = -phase    # 记住第一个音的相位
finalPhase = phase + relativePhase  # 所有音减去第一个音

DUT 关心的是音与音之间的相对相位,不关心绝对相位

数值示例(3 个音,端口1):

Arb 特性化结果:
  rfGenLevel  = [-35.4, -35.9, -35.6] dBm
  rfGenPhase  = [0.0, 0.12, -0.08] rad

波形元数据:
  metadataPhase = [0.0, 0.0, 0.0] rad

端口1 的 S 参数:
  S21[3.499 GHz]: |S21|=0.680, phase=-1.5 rad → toneLoss=3.35 dB
  S21[3.500 GHz]: |S21|=0.668, phase=-1.6 rad → toneLoss=3.51 dB
  S21[3.501 GHz]: |S21|=0.655, phase=-1.7 rad → toneLoss=3.67 dB

refAtt = 3.2 dB, portAtt = 3.2 dB (端口1 是参考)

电平计算:
  toneLevel[0] = -35.4 - 3.35 + 3.2 - 3.2 = -38.75 dBm
  toneLevel[1] = -35.9 - 3.51 + 3.2 - 3.2 = -39.41 dBm
  toneLevel[2] = -35.6 - 3.67 + 3.2 - 3.2 = -39.27 dBm

相位计算:
  phase[0] = -1.5 + 0.0 + 0.0 = -1.5 rad
  phase[1] = -1.6 + 0.12 + 0.0 = -1.48 rad
  phase[2] = -1.7 + (-0.08) + 0.0 = -1.78 rad

相位归一化 (relativePhase = +1.5):
  finalPhase[0] = 0.0 rad      ← 基准
  finalPhase[1] = +0.02 rad
  finalPhase[2] = -0.28 rad

DUT 如何使用多音响应:

电平信息 → 校准 RX 增益的频率平坦度
  DUT 知道不同频率上收到的信号电平不同
  → 写入频率补偿表

相位信息 → 校准 RX 通道间的相位一致性
  DUT 知道不同频率上的群延迟差异
  → 写入相位补偿表

多音端口偏移同样不影响信号发生器配置,只是返回给 DUT 用于校准。


附录 B:关键数据类型说明

消息类型 用途 关键字段
DutIdentifier 唯一标识被测设备 session_id (设备ID+时间戳), dut_pos
SignalRequirements 信号电平要求 stimuli_level_dbm, upper/lower_bound_db
CarrierSignal 单载波信号定义 requirements, test_signal, carrier_freq_hz
MultiToneSignal 多音信号定义 requirements, no_tones, start_hz, sep_hz
RfPortTargetOffset 端口电平偏移 rfport, target_offset_db
Tone 单个音的信息 frequency_hz, tone_level_dbm, phase_deg
Status gRPC 状态 code (0=OK), message

附录 C:错误码对照

错误码 含义
OK 0 成功
INVALID_ARGUMENT 3 参数无效 (dut_id 为空等)
UNAVAILABLE 14 资源不可用 (配置未找到等)
ABORTED 10 操作中止 (硬件异常等)

附录 D:信号类型枚举

CarrierType 含义 示例
CARRIER_TYPE_CW 连续波 纯正弦波,无调制
CARRIER_TYPE_L20000_30720 LTE FDD 20MHz 30.72 MSPS
CARRIER_TYPE_N100000_122880 NR TDD 100MHz 122.88 MSPS
CARRIER_TYPE_F100000_122880 NR FDD 100MHz 122.88 MSPS

附录 E:Python 实现与 C# 实现的差异

维度 C# 完整实现 Python 简化实现
资源管理 ResourceManagerV2 分配/释放 单一信号发生器实例
路径补偿 CompensationManager 查询数据库 返回 0 偏移
Leveling 功率计闭环迭代 (精度 < 0.1dB) 未实现 (直接设置电平)
Arb 特性化 频谱仪/功率计测量补偿 未实现
波形加载 从波形库读取 IQ 文件 未实现
缓存 14 天有效期的 Leveling/Arb 缓存 无缓存
开关矩阵 RF Switch Network 路径管理 无开关控制

Python 实现的简化是合理的——在没有功率计、频谱仪等校准硬件的环境下,直接设置信号发生器电平即可满足基本测试需求。

posted @ 2026-04-22 10:51  mo686  阅读(4)  评论(0)    收藏  举报