RF Stimuli
1. 任务背景
1.1 任务来源
分配任务:将 C# Raptor2.MS.RFStimuli 服务中 外部信号源请求处理 的逻辑理解清楚,并用 Python 在 test-interface-client 项目中实现对应的 gRPC 客户端。
1.2 业务场景
在 RX(接收端)校准过程中,DUT(被测设备)内部的 FAAP 需要外部信号源发射特定的 RF 信号进来,以测试 DUT 的接收功能。
DUT (FAAP) Raptor2.MS.RFStimuli 服务 (C#) 信号源仪表 / 功率计
│ │ │
│── gRPC: RequestExternalStimuli ──>│ │
│ (频率、功率、信号类型、端口) │ │
│ │── 1. 资源分配 (ResourceManager) │
│ │── 2. 获取信号发生器 (CW/调制) │
│ │── 3. 路径补偿计算 (CompensationMgr) ──>│
│ │── 4. 加载波形 (WaveformLibrary) │
│ │── 5. 配置信号发生器 │
│ │── 6. Leveling 校准 ─────────────────>│ (功率计测量)
│ │── 7. Arb 特性化 (多音) ─────────────>│ (频谱仪/功率计)
│ │── 8. 计算端口补偿偏移 │
│ │ │
│<── gRPC: ConfigurationResponse ──│ │
│ (状态、端口偏移、多音相位/电平) │ │
│ │ │
│ ... DUT 进行 RX 校准数据采集 ... │ │
│ │ │
│── gRPC: UpdateConfiguration ────>│ │
│ (功率偏移 / 频率偏移) │── 调整信号发生器参数 │
│<── 更新结果 ────────────────────│ │
│ │ │
│── gRPC: TurnOffAndRelease ──────>│ │
│ │── 关闭信号发生器,释放资源 │
│<── 释放确认 ────────────────────│ │
2. C# 源码分析
2.1 项目架构总览
Eric 指出的核心源码位于 PCSW/Raptor2.MS.RFStimuli/,这是一个完整的 gRPC 服务端 微服务,不是简单的客户端封装。
Raptor2.MS.RFStimuli/Source/
├── Services/ # gRPC 服务入口
│ ├── ExternalSignalStimuliService.cs # gRPC service 实现 (proto V0)
│ ├── ExternalSignalStimuliServiceV1.cs # gRPC service 实现 (proto V1)
│ ├── RfStimuliService.cs # 内部 RPC 服务
│ └── MaintenanceServiceV2.cs # 维护服务
│
├── SupportClasses/ # 核心业务逻辑
│ ├── ExternalSignalStimuliBase.cs # ★ 基类:请求流程编排
│ ├── ExternalSignalStimuli.cs # ★ 子类:具体硬件交互实现
│ ├── ExternalSignalStimuliIUDC.cs # IUDC 变体实现
│ ├── CarrierSignalConditions.cs # 信号状态数据结构
│ ├── ConfigurationRequestHandler.cs # 请求解析
│ ├── RFGeneratorCharacterization.cs # RF 发生器特性化
│ ├── RFPowerMeasurement.cs # 功率测量封装
│ ├── FrequencySweep.cs # 频谱扫描封装
│ ├── MultiToneConfiguration.cs # 多音配置
│ ├── TestEquipmentLogHandler.cs # 测试设备日志
│ └── ...
│
├── SupportClassesProto/ # 内部数据模型 (与 proto 解耦)
│ ├── RfStimuliConfigurationRequest.cs
│ ├── RfStimuliConfigurationResponse.cs
│ ├── RfSignal.cs
│ ├── RfSignalConfiguration.cs
│ ├── ConnectionPort.cs
│ └── ...
│
├── Extensions/ # Proto ↔ 内部模型转换
│ ├── ConfigurationRequestExtensions.cs
│ ├── StimuliConfigurationRequestExtensionsV1.cs
│ └── ...
│
├── Container/ # 缓存容器
│ ├── LevelingContainer.cs # Leveling 结果缓存
│ ├── ArbCalibrationContainer.cs # Arb 校准结果缓存
│ └── SignalCapabilities.cs
│
├── Calculations/
│ └── RFStimuliCalculate.cs # Hilbert 变换等数学计算
│
├── Interfaces/ # 依赖接口定义
│ ├── IExternalSignalStimuli.cs
│ ├── IRFPowerMeasurement.cs
│ ├── IFrequencySweep.cs
│ ├── ILevelingContainer.cs
│ └── IArbCalibrationContainer.cs
│
└── Startup.cs # DI 注册
2.2 核心类关系
ExternalSignalStimuliBase (基类 — 流程编排)
│
├── RequestExternalStimuliFlowAsync() # 请求处理主流程
├── TurnOffAndReleaseConfigurationFlowAsync() # 释放流程
├── UpdateConfigurationFlowAsync() # 更新流程
│
├── abstract CwResourceGeneratorAsync() # 获取 CW 信号发生器资源
├── abstract ModulationResourceGeneratorAsync() # 获取调制信号发生器资源
├── abstract GetWaveformAsync() # 加载 IQ 波形
├── abstract SignalLevelResponseAsync() # 计算信号电平响应
├── abstract MultiToneLevelResponseAsync() # 计算多音电平响应
├── abstract PerformLevelingAsync() # 执行 Leveling
└── abstract ArbCharacterizationAsync() # 执行 Arb 特性化
│
▼
ExternalSignalStimuli (子类 — 具体实现)
│
├── 依赖注入:
│ ├── IResourceManagerV2 # 硬件资源管理器
│ ├── ICompensationManager # 路径补偿管理器
│ ├── IRFPowerMeasurement # 功率计接口
│ ├── IFrequencySweep # 频谱仪接口
│ ├── IWaveformLibrary # 波形库
│ ├── ILevelingContainer # Leveling 缓存
│ └── IArbCalibrationContainer # Arb 校准缓存
│
└── 实现所有 abstract 方法
2.3 RequestExternalStimuli 主流程详解
ExternalSignalStimuliBase.RequestExternalStimuliFlowAsync() 是核心入口,处理流程:
1. 初始化响应对象 (Status = OK)
2. 获取信号发生器硬件能力列表
3. 检查 SessionId 有效性
4. 遍历每个 SignalConfiguration:
│
├── 遍历每个 Signal:
│ │
│ ├── 4a. 获取波形数据 (GetWaveFormDataAsync)
│ │ → 从 WaveformLibrary 下载 IQ 波形和元数据
│ │
│ ├── 4b. 分配信号发生器资源 (SignalResourceAsync)
│ │ → CW 信号: CwResourceGeneratorAsync()
│ │ - 优先分配 CW 专用发生器
│ │ - CW 忙则尝试调制发生器
│ │ → 调制信号: ModulationResourceGeneratorAsync()
│ │ - 分配带 IQ 调制 + ARB 的发生器
│ │ - 解析波形元数据 (PAR, 多音信息)
│ │
│ ├── 4c. 加载波形到 ARB (GetWaveformAsync)
│ │ → 读取 IQ 文件 (Tab 分隔的 I/Q 值)
│ │ → 生成唯一波形 key (hash)
│ │ → 如果发生器未存储该波形则上传
│ │
│ ├── 4d. 配置信号发生器 (SetupSignalGenerator)
│ │ → 设置频率、功率、调制参数
│ │ → 加载波形、设置采样率
│ │ → 打开 RF 输出
│ │
│ ├── 4e. ★ Leveling 校准 (PerformLevelingAsync)
│ │ → 通过功率计实测信号电平
│ │ → 迭代调整发生器输出 (最多 10 次)
│ │ → 精度目标: ±0.1 dB
│ │ → 最大调整量: ±2.0 dB
│ │ → 缓存结果 (14 天有效期)
│ │
│ ├── 4f. ★ Arb 特性化 (ArbCharacterizationAsync) — 仅多音
│ │ → 方式 1: 频谱仪 (SpectrumAnalyzer)
│ │ - 测 CW 电平 → 测多音电平 → 计算补偿
│ │ → 方式 2: 功率计 (PowerMeter)
│ │ - ALC 功率搜索 → 逐音测量 → 计算补偿
│ │ → Hilbert 变换计算相位
│ │ → 验证音间差异 (< 0.5 dB) 和最大最小差 (< 1.5 dB)
│ │
│ └── 4g. 计算响应
│ → 单载波: SignalLevelResponseAsync()
│ - 查询每个端口的路径补偿
│ - 计算 RfPortTargetOffset
│ → 多音: MultiToneLevelResponseAsync()
│ - 查询 S 参数 (S21 幅度和相位)
│ - 计算每个音的电平和相位补偿
│ - 相位相对于起始音
│
└── 5. 组装响应
→ ActiveConfigurations (成功的配置)
→ UnhandledConfigurations (资源不足的配置)
→ Errors (失败的配置)
2.4 关键技术细节
Leveling(电平校准)
目的: 补偿信号发生器输出精度不足的问题
方法: 闭环迭代调整
wanted_level = stimuli_level + path_compensation
while |measured - wanted| >= 0.1 dB:
adjustment = (wanted - measured) * adjustment_factor
rf_level += adjustment
设置发生器电平 = rf_level
measured = 功率计读数
# 每 3 次迭代降低调整因子 (×0.75) 提高精度
# 超过 10 次迭代或累计调整 > 2 dB 则报错
路径补偿(Path Compensation)
目的: 补偿从信号发生器到 DUT 端口之间的线缆/开关损耗
reference_attenuation = 参考路径的衰减值
port_attenuation = 当前端口路径的衰减值
target_offset = reference_attenuation - port_attenuation + signal_deviation
→ 每个端口返回不同的 target_offset
→ DUT 用这个偏移来校正接收电平
多音相位/电平补偿
目的: 补偿信号发生器和路径对不同频率音的非线性响应
对每个音 (tone):
attenuation_tone = -20 * log10(|S21|) # 路径损耗
tone_level = rf_generator_level - attenuation_tone + ref_attenuation - port_offset
phase = S21.phase + rf_generator_phase + multitone_phase
# 相位相对于第一个音
if tone_index == 0:
relative_phase = -phase
final_phase = phase + relative_phase
2.5 与之前分析的 FaapProxyClient/RFStimuli.cs 的区别
| 维度 | FaapProxyClient/RFStimuli.cs | Raptor2.MS.RFStimuli/ExternalSignalStimuli.cs |
|---|---|---|
| 角色 | gRPC 客户端 — 发送请求 | gRPC 服务端 — 处理请求 |
| 复杂度 | ~170 行,简单的请求构建 | ~900+ 行,完整的硬件交互逻辑 |
| 硬件交互 | 无 — 只构建 protobuf 消息 | 直接控制信号发生器、功率计、频谱仪 |
| 补偿计算 | 无 | 路径补偿、Leveling、Arb 特性化 |
| 资源管理 | 无 | ResourceManager 分配/释放硬件资源 |
| 波形处理 | 无 | 从 WaveformLibrary 下载并加载 IQ 波形 |
| 缓存 | 无 | LevelingContainer + ArbCalibrationContainer |
结论: FaapProxyClient/RFStimuli.cs 是一个测试/调试用的简易客户端,而 ExternalSignalStimuli.cs 才是真正的业务实现。我们的 Python 实现是 客户端角色(类似 FaapProxyClient),但需要理解服务端逻辑才能正确构建请求和解读响应。
3. gRPC 接口定义
3.1 ExternalSignalStimuliService
service ExternalSignalStimuliService {
rpc RequestExternalStimuli (ConfigurationRequest) returns (ConfigurationResponse);
rpc UpdateConfiguration (ConfigurationUpdateRequest) returns (ConfigurationUpdateResponse);
rpc TurnOffAndReleaseConfiguration (ReleaseConfiguration) returns (ReleaseConfigurationAck);
rpc GetRfInterface (GetRfInterfaceRequest) returns (GetRfInterfaceResponse);
}
3.2 请求消息结构
ConfigurationRequest
├── dut_id: DutIdentifier
│ ├── session_id: SessionId (device_id + start_time)
│ └── dut_pos: uint32
└── configurations[]: SignalConfiguration
├── configuration_id: uint32
├── polarization: Polarization enum
├── rfports[]: uint32
└── signals[]: Signal
├── signal_id: uint32
└── oneof stimuli:
├── carrier: CarrierSignal
│ ├── carrier_freq_hz: double
│ ├── requirements: SignalRequirements {stimuli_level_dbm, upper_bound_db, lower_bound_db}
│ └── test_signal: TestSignal {carrier_type, model}
└── mt: MultiToneSignal
├── start_hz: double
├── no_tones: uint32
├── sep_hz: uint32
└── requirements: SignalRequirements
3.3 响应消息结构
ConfigurationResponse
├── status: Status {code, message}
├── errors[]: ConfigurationError
│ ├── configuration_id
│ ├── errors[]: StimuliError enum
│ └── signal_errors[]: SignalError
├── active_configurations[]: SignalConfigurationResponse
│ ├── configuration_id
│ └── signals[]: SignalResponse
│ ├── signal_id
│ └── oneof:
│ ├── 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
4. Python 实现
4.1 实现范围
我们实现的是 gRPC 客户端,对应 FaapProxyClient/RFStimuli.cs 的角色,但覆盖了服务端全部 4 个 RPC 方法。
4.2 新增文件
Proto 文件(test-interface-client/protos/)
| 文件 | 来源 | 内容 |
|---|---|---|
radio.proto |
从 PCSW/production/protos/radio.proto 精简 |
DutIdentifier、RfPort、TestSignal |
stimuli.proto |
完整复制 PCSW/production/protos/stimuli.proto |
信号请求/响应全部消息 |
service_dut_rx_stimuli.proto |
完整复制 | ExternalSignalStimuliService 服务定义 |
生成的 Python 代码(test-interface-client/generated/)
| 文件 | 内容 |
|---|---|
radio_pb2.py / _grpc.py / .pyi |
Radio 消息 |
stimuli_pb2.py / _grpc.py / .pyi |
Stimuli 消息 |
service_dut_rx_stimuli_pb2.py / _grpc.py / .pyi |
服务 stub |
Python 客户端(test-interface-client/service_client/rf_stimuli.py)
4.3 代码结构
# 数据类 — 请求参数封装
@dataclass
class DutConnectionPoint:
rfports: List[int] # RF 端口号列表
polarization: int # 极化方向枚举值
@dataclass
class CarrierRequest:
carrier_freq_hz: float # 载波频率 (Hz)
stimuli_level_dbm: float # 信号功率 (dBm)
carrier_type: int # CarrierType 枚举
test_model: int # TestModel 枚举
connection_point: DutConnectionPoint
configuration_id: int
@dataclass
class MultiToneRequest:
first_freq_hz: float # 起始频率 (Hz)
stimuli_level_dbm: float # 信号功率 (dBm)
number_of_tones: int # 音数量
tone_separation_hz: int # 音间隔 (Hz)
connection_point: DutConnectionPoint
configuration_id: int
# 客户端类
class RFStimuliClient:
def connect() -> bool
def configure_carrier(request: CarrierRequest) -> ConfigurationResponse
def configure_multitone(request: MultiToneRequest) -> ConfigurationResponse
def update_power_offset(configuration_id, offset_db) -> ConfigurationUpdateResponse
def update_frequency_offset(configuration_id, offset_hz) -> ConfigurationUpdateResponse
def release_configuration(configuration_id) -> ReleaseConfigurationAck
def get_rf_interface(no_of_ports, supports_per_polarization) -> GetRfInterfaceResponse
4.4 设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 参数封装 | @dataclass |
Python 惯用方式,比 C# class 更简洁 |
| 端口模型 | rfports + polarization |
直接映射 proto 的 SignalConfiguration 字段 |
| Update 方法 | 拆分为 update_power_offset 和 update_frequency_offset |
proto 中是 oneof,拆分更 Pythonic |
| 代码风格 | 遵循 GPIOClient / ConfigurationClient 模式 |
项目一致性 |
4.5 C# 客户端 → Python 对照
// C# — FaapProxyClient/RFStimuli.ConfigureCarrier()
var carrierSignal = new CarrierSignal() {
CarrierFreqHz = request.CarrierCenterFrequency,
Requirements = new SignalRequirements() { StimuliLevelDbm = request.StimuliLevel },
TestSignal = new TestSignal() { Model = request.TestModel, CarrierType = request.CarrierType }
};
var signalConfiguration = new SignalConfiguration() { ConfigurationId = 1 };
signalConfiguration.Signals.Add(new Signal() { Carrier = carrierSignal, SignalId = 1 });
signalConfiguration.Signals.Last().Ports.AddRange(request.DutConnectionPoint.Ports);
# Python — RFStimuliClient.configure_carrier()
carrier_signal = stimuli_pb2.CarrierSignal(
carrier_freq_hz=request.carrier_freq_hz,
requirements=stimuli_pb2.SignalRequirements(stimuli_level_dbm=request.stimuli_level_dbm),
test_signal=radio_pb2.TestSignal(carrier_type=request.carrier_type, model=request.test_model),
)
signal_config = stimuli_pb2.SignalConfiguration(configuration_id=request.configuration_id)
signal = stimuli_pb2.Signal(signal_id=1, carrier=carrier_signal)
signal_config.signals.append(signal)
signal_config.rfports.extend(request.connection_point.rfports)
5. 使用示例
import grpc
import logging
from generated import enums_pb2
from service_client.rf_stimuli import (
RFStimuliClient, CarrierRequest, MultiToneRequest, DutConnectionPoint,
)
logger = logging.getLogger("rf_stimuli")
channel = grpc.insecure_channel("192.168.1.100:50051")
client = RFStimuliClient(channel, logger, dut_position=1)
client.connect()
# ---- 1. 配置单载波 CW 信号 (3500 MHz, -30 dBm) ----
carrier_req = CarrierRequest(
carrier_freq_hz=3500e6,
stimuli_level_dbm=-30.0,
carrier_type=enums_pb2.CARRIER_TYPE_CW,
test_model=enums_pb2.TEST_MODEL_CW,
connection_point=DutConnectionPoint(
rfports=[1, 2, 3, 4],
polarization=enums_pb2.POLARIZATION_HORIZONTAL,
),
configuration_id=1,
)
resp = client.configure_carrier(carrier_req)
# 解读响应: 每个端口的功率偏移
for config in resp.active_configurations:
for sig_resp in config.signals:
if sig_resp.HasField("signal"):
for port_offset in sig_resp.signal.rfports:
print(f"Port {port_offset.rfport.rfport}: offset={port_offset.target_offset_db:.2f} dB")
# ---- 2. 校准中微调功率 ----
client.update_power_offset(configuration_id=1, total_power_offset_db=-0.5)
# ---- 3. 校准完成,释放 ----
client.release_configuration(configuration_id=1)
# ---- 4. 配置 NR TDD 调制信号 ----
nr_req = CarrierRequest(
carrier_freq_hz=3600e6,
stimuli_level_dbm=-25.0,
carrier_type=enums_pb2.CARRIER_TYPE_N100000_115200,
test_model=enums_pb2.TEST_MODEL_TM3p1a,
connection_point=DutConnectionPoint(rfports=[1, 2]),
configuration_id=3,
)
resp = client.configure_carrier(nr_req)
# ... 使用后释放
client.release_configuration(configuration_id=3)
# ---- 5. 配置多音信号 ----
mt_req = MultiToneRequest(
first_freq_hz=3500e6,
stimuli_level_dbm=-25.0,
number_of_tones=100,
tone_separation_hz=1000,
connection_point=DutConnectionPoint(rfports=[1, 2]),
configuration_id=2,
)
resp = client.configure_multitone(mt_req)
# 解读多音响应: 每个音的电平和相位
for config in resp.active_configurations:
for sig_resp in config.signals:
if sig_resp.HasField("mt"):
for multi_tone in sig_resp.mt.tones:
print(f"Port offset: {multi_tone.rfport.target_offset_db:.2f} dB, PAR: {multi_tone.par_db:.2f} dB")
for tone in multi_tone.tones:
print(f" {tone.frequency_hz/1e6:.3f} MHz: {tone.tone_level_dbm:.2f} dBm, phase={tone.phase_deg:.1f}°")
client.release_configuration(configuration_id=2)
channel.close()
6. 产出文件清单
| 文件路径 | 类型 | 说明 |
|---|---|---|
protos/radio.proto |
新增 | Radio 消息定义 |
protos/stimuli.proto |
新增 | Stimuli 消息定义 |
protos/service_dut_rx_stimuli.proto |
新增 | gRPC 服务定义 |
generated/radio_pb2.py |
生成 | Radio protobuf |
generated/radio_pb2_grpc.py |
生成 | Radio gRPC |
generated/stimuli_pb2.py |
生成 | Stimuli protobuf |
generated/stimuli_pb2_grpc.py |
生成 | Stimuli gRPC |
generated/service_dut_rx_stimuli_pb2.py |
生成 | 服务消息 |
generated/service_dut_rx_stimuli_pb2_grpc.py |
生成 | 服务 stub |
generated/*.pyi |
生成 | 类型提示 |
service_client/rf_stimuli.py |
新增(核心) | Python 版 RF Stimuli 客户端 |

浙公网安备 33010602011771号