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 注释):
- 避免校准期间因重新请求而被阻塞(可能影响温度读数)
- 调整功率时可能不需要用功率计重新对齐
- 调整频率时可能不需要用功率计重新对齐
请求消息结构
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; // 两个极化同时测
}
在代码中的作用:
极化信息影响两件事:
- 开关路径选择 — 告诉 ResourceManager 连接哪组端口
- 响应中的端口标识 — 返回给 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[].CompensationPathfrequency:当前信号频率,在路径的衰减表中做线性插值
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 实现的简化是合理的——在没有功率计、频谱仪等校准硬件的环境下,直接设置信号发生器电平即可满足基本测试需求。

浙公网安备 33010602011771号