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 精简 DutIdentifierRfPortTestSignal
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_offsetupdate_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 客户端

7. 后续待办

posted @ 2026-04-17 14:32  mo686  阅读(0)  评论(0)    收藏  举报