Waveform Loading
Waveform Loading 工作流
概述
Waveform Loading 功能实现了从波形库微服务下载 IQ 波形数据,并上传到 R&S SGT100A 信号发生器进行播放的完整链路。该功能集成在 RFStimuliServicer._process_signal() 中,当 DUT 请求调制信号(非 CW)时自动触发。
架构
DUT (FAAP)
│ gRPC: RequestExternalStimuli
▼
RFStimuliServicer._process_signal()
│
├── CW 信号 → configure_cw(freq, level) → 完成
│
└── 调制/多音信号 → WaveformLoader
│
├── 1. 查询 WaveformLibraryService (gRPC)
├── 2. 下载元数据 JSON + IQ 文本文件
├── 3. 解析 IQ 数据 (IQParser)
├── 4. 生成波形密钥 (WaveformKeyGenerator)
├── 5. 检查 ARB 缓存 (is_waveform_stored)
├── 6. 上传 .wv 文件 (ARBUploader → MMEM:DATA)
├── 7. 选择波形 (BB:ARB:WAVeform:SELect)
└── 8. configure_modulated(freq, level) → RF ON
模块结构
test-interface-client/
├── service_client/
│ ├── rf_stimuli.py # SignalGeneratorController + RFStimuliServicer
│ └── waveform_loader.py # IQParser, WaveformKeyGenerator, ARBUploader,
│ # WaveformLibraryClient, WaveformLoader
├── generated/
│ ├── waveform_library_pb2.py # protobuf 消息定义
│ └── waveform_library_pb2_grpc.py # gRPC stub
└── test_case/
└── test_waveform_loading_verification.py # 端到端验证脚本
核心类职责
| 类 | 文件 | 职责 |
|---|---|---|
IQParser |
waveform_loader.py | 解析制表符分隔的 IQ 文本 ↔ float 对列表 |
WaveformKeyGenerator |
waveform_loader.py | 根据 IQ 内容 + 元数据生成 8 字符唯一密钥 |
ARBUploader |
waveform_loader.py | 构建 .wv 文件、通过 MMEM:DATA 上传到仪器 |
WaveformLibraryClient |
waveform_loader.py | gRPC 客户端,查询/下载波形文件 |
WaveformLoader |
waveform_loader.py | 编排完整流程的协调器 |
SignalGeneratorController |
rf_stimuli.py | SCPI 命令封装(含 ARB 扩展方法) |
详细工作流
步骤 1: 查询波形库
# WaveformLibraryClient.query_standard_waveform()
WaveformRequest {
session_id: SessionId,
usage: [WAVEFORM_USAGE_UPLINK],
standard: {test_model, carrier_type, frequency_range}
}
→ GetFilePackageInfo RPC → 返回 package_id
步骤 2: 下载文件
# WaveformLibraryClient.download_metadata() → dict
# WaveformLibraryClient.download_iq_file() → str
FileDownloadRequest {session_id, package_id, kind_of_file}
→ DownloadFile RPC (streaming) → 拼接 data 块 → 完整文件内容
步骤 3: 解析 IQ 数据
# IQParser.parse(iq_text_content)
输入: "0.123456\t-0.654321\n0.789012\t0.345678\n..."
输出: [(0.123456, -0.654321), (0.789012, 0.345678), ...]
- 每行制表符分隔的 I 和 Q 值
- 跳过空行和纯空白行
- 格式错误抛出 ValueError(含行号)
步骤 4: 生成波形密钥
# WaveformKeyGenerator.generate_key(iq_content, metadata_json)
算法: MD5(MD5(iq_content) + MD5(metadata_json)) 取最后 8 字符
输出: "f3fae887"
波形名: "f3fae887.wf"
步骤 5: 检查 ARB 缓存
# SignalGeneratorController.is_waveform_stored("f3fae887.wf")
SCPI: :BB:ARB:WAVeform:CATalog?
→ 解析返回列表,检查是否已存在
→ 命中则跳过上传,直接到步骤 7
步骤 6: 上传 .wv 文件
# ARBUploader.upload_waveform(iq_data, "f3fae887.wf", sample_rate)
6a. 构建 R&S .wv 文件格式:
{TYPE:SMU-WV,0}
{COMMENT:Kiro waveform loader}
{CLOCK:15360000.0}
{LEVEL OFFS:3.123456,0.000000}
{SAMPLES:153600}
{WAVEFORM-614400:#<binary_data>}
- IQ 数据编码为 16-bit 有符号整数(little-endian,交错 I,Q)
- 浮点值 × 32767 → int16
- 每个样本 4 字节(2 bytes I + 2 bytes Q)
6b. 通过 MMEM:DATA 上传到仪器:
SCPI: :MMEM:DATA "/var/user/f3fae887.wv",#<ieee488_block>
- 使用 IEEE 488.2 确定长度数据块格式
- 通过 GPIB send_raw() 发送二进制数据
步骤 7: 选择并启用波形
SCPI: :BB:ARB:WAVeform:SELect "/var/user/f3fae887.wv"
SCPI: :BB:ARB:CLOCk 15360000.0
SCPI: :BB:ARB:STATe ON
步骤 8: 配置 RF 输出
# SignalGeneratorController.configure_modulated(freq_hz, level_dbm)
SCPI: :FREQ 3500000000
SCPI: :POW -20.00 dBm
SCPI: :IQ:STAT ON
SCPI: :OUTP ON
数据格式
IQ 文本文件 (.wf)
0.123456 -0.654321
0.789012 0.345678
-0.234567 0.890123
R&S .wv 二进制文件
| 区域 | 格式 | 说明 |
|---|---|---|
| 头部 | ASCII | {TYPE:SMU-WV,0}{CLOCK:...}{SAMPLES:...}... |
| 数据标记 | ASCII | {WAVEFORM-<byte_count>:# |
| IQ 数据 | Binary | int16 LE 交错: I0,Q0,I1,Q1,... |
| 结束 | ASCII | } |
波形元数据 JSON
{
"Samples": 153600,
"SampleRate": 15360000.0,
"IsMultitone": false,
"CarrierType": "L50007680",
"TestModel": "Tm1P1"
}
SCPI 命令汇总
| 操作 | SCPI 命令 |
|---|---|
| 查询波形目录 | :BB:ARB:WAVeform:CATalog? |
| 上传文件 | :MMEM:DATA "/var/user/{name}.wv",#<block> |
| 选择波形 | :BB:ARB:WAVeform:SELect "/var/user/{name}.wv" |
| 设置采样率 | :BB:ARB:CLOCk {rate} |
| 启用 ARB | :BB:ARB:STATe ON |
| 启用 IQ 调制 | :IQ:STATe ON |
| 设置频率 | :FREQ {hz} |
| 设置功率 | :POW {dbm} dBm |
| 启用 RF 输出 | :OUTP ON |
缓存机制
波形密钥基于 IQ 文件内容和元数据的 MD5 哈希生成,确保:
- 相同波形数据 → 相同密钥 → 跳过重复上传
- 不同波形数据 → 不同密钥 → 触发新上传
首次加载: 查询 → 下载 → 解析 → 生成密钥 → 未缓存 → 上传 → 选择
再次加载: 查询 → 下载 → 解析 → 生成密钥 → 已缓存 → 直接选择
错误处理
| 错误场景 | 行为 |
|---|---|
| gRPC 连接失败 | 抛出异常,_process_signal 捕获,返回 ABORTED |
| 波形查询无匹配 | RuntimeError,阻止信号配置 |
| IQ 文件格式错误 | ValueError(含行号),阻止信号配置 |
| GPIB 连接断开 | ConnectionError,阻止信号配置 |
| 无 WaveformLibraryService | waveform_loader 为 None,跳过波形加载 |
配置
| 配置项 | 方式 | 说明 |
|---|---|---|
| 波形库地址 | waveform_library_address 参数或 WAVEFORM_LIBRARY_ADDRESS 环境变量 |
host:port 格式 |
| 信号源连接 | VISA resource string | 如 TCPIP0::192.168.1.61::hislip0::INSTR |
验证方法
无硬件验证
python test-interface-client/tests/verify_waveform_loading.py
验证核心逻辑:IQ 解析 round-trip、二进制转换、IEEE 488.2 块、密钥生成。
有硬件验证
python test-interface-client/test_case/test_waveform_loading_verification.py
连接真实 SGT100A,验证完整流程:上传 → 选择 → 配置 → RF 输出 → 仪器回读。
单元测试
cd test-interface-client
python -m pytest tests/test_signal_generator_arb.py -v
Mock 测试 SCPI 命令序列和参数正确性。
开发过程中遇到的问题及解决方法
问题 1: 模块导入路径错误
现象: 运行测试脚本时报 ModuleNotFoundError: No module named 'service_client'
原因: 测试脚本从 repo 根目录运行,但 service_client 包位于 test-interface-client/ 子目录中,Python 找不到模块。
解决: 在测试脚本顶部添加 sys.path.insert,将 test-interface-client/ 目录加入搜索路径:
_project_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
问题 2: protobuf 枚举名称不正确
现象: AttributeError: module 'enums_pb2' has no attribute 'TEST_MODEL_TM1P1'
原因: 枚举名称大小写敏感。实际生成的枚举是 TEST_MODEL_TM1p1(小写 p),不是 TEST_MODEL_TM1P1。同样 CARRIER_TYPE_L5 不存在,正确名称是 CARRIER_TYPE_L5000_7680。
解决: 通过 dir(enums_pb2) 查询实际可用的枚举值,使用正确名称:
# 错误
enums_pb2.TEST_MODEL_TM1P1
enums_pb2.CARRIER_TYPE_L5
# 正确
enums_pb2.TEST_MODEL_TM1p1
enums_pb2.CARRIER_TYPE_L5000_7680
问题 3: BB:ARB:WAVeform:DATA 命令无法正确存储波形
现象: 仪器报错 Cannot read file;/var/user/kiro_test.wf.wv 和 Settings conflict;no or empty waveform selected
原因: R&S SGT100A 的 BB:ARB:WAVeform:DATA 命令不是用来直接上传裸 IQ 二进制数据的。SGT100A 要求波形以 .wv 文件格式存储在仪器文件系统中,该格式包含特定的 ASCII 头部 + int16 二进制数据。
错误的做法:
:BB:ARB:WAVeform:DATA "name",#<ieee488_block_of_raw_float32>
正确的做法: 构建完整的 R&S .wv 文件,通过 MMEM:DATA 命令传输到仪器文件系统:
:MMEM:DATA "/var/user/name.wv",#<ieee488_block_of_wv_file>
问题 4: 文件扩展名冲突导致双重后缀
现象: 仪器报错 Cannot read file;/var/user/kiro_test.wf.wv
原因: 波形逻辑名称是 kiro_test.wf,但 R&S 仪器内部使用 .wv 扩展名。如果直接用 kiro_test.wf 作为文件名上传,仪器会存为 kiro_test.wf.wv(双重扩展名),导致后续 SELect 找不到文件。
解决: 上传时去掉 .wf 后缀,只用密钥作为文件名:
file_stem = waveform_name.replace('.wf', '') # "f3fae887"
remote_path = f"/var/user/{file_stem}.wv" # "/var/user/f3fae887.wv"
问题 5: TRIGger:SOURce IMMediate 命令无效
现象: 仪器报错 Invalid character data;IMMEDIATE 或 Invalid character data;AUTO
原因: SGT100A 的 ARB 触发源命令参数值与预期不同。尝试了 IMMediate、AUTO、INT 等值均报错。
解决: 最终方案中移除了单独的触发源设置命令。R&S .wv 文件上传后,仪器默认以连续模式运行(从截图可见 "Running" 状态),无需额外设置触发源。
问题 6: :SOURce 前缀问题
现象: 部分 SCPI 命令加了 :SOURce 前缀后行为异常。
原因: SGT100A 的 BB(基带)子系统命令不需要 :SOURce 前缀。:SOURce:BB:ARB:... 和 :BB:ARB:... 在某些仪器上等价,但在 SGT100A 上直接用 :BB:ARB:... 更可靠。
解决: BB 子系统命令统一去掉 :SOURce 前缀:
# 之前
":SOURce:BB:ARB:WAVeform:CATalog?"
":SOURce:BB:ARB:STATe ON"
# 之后
":BB:ARB:WAVeform:CATalog?"
":BB:ARB:STATe ON"
问题 7: RF 输出未开启
现象: 波形上传成功、ARB Running、I/Q Mod On,但 RF Output 显示 Off。
原因: ARBUploader.upload_waveform() 只负责上传和启用 ARB,不负责开启 RF 输出。在正常集成流程中,RF 输出由 configure_modulated() 方法控制(调用 set_output(True))。但在单独测试 upload_waveform 时,没有后续调用 configure_modulated。
解决: 这不是 bug。在完整集成流程中,_process_signal 会在波形加载后调用 configure_modulated(freq, level),该方法内部会执行 set_output(True) 开启 RF。测试脚本中需要手动调用 sg.set_output(True) 或 sg.configure_modulated(freq, level)。
问题 8: R&S .wv 文件格式
最终确认的 .wv 文件格式:
{TYPE:SMU-WV,0}{COMMENT:...}{CLOCK:采样率}{LEVEL OFFS:par,0.000000}{SAMPLES:样本数}{WAVEFORM-字节数:#<int16_LE_binary>}
关键点:
- IQ 数据用 int16 有符号整数(不是 float32)
- 浮点值范围 [-1.0, 1.0] 映射到 [-32767, 32767]
- 交错格式:I0(2B), Q0(2B), I1(2B), Q1(2B), ...
- 每样本 4 字节(不是 float32 的 8 字节)
- 文件以
}结尾
经验总结
| 教训 | 说明 |
|---|---|
| 不要猜测 SCPI 命令 | 不同厂商/型号的命令差异很大,必须参考具体仪器文档 |
| R&S 仪器用 .wv 文件格式 | 不能直接发送裸二进制 IQ 数据,需要构建带头部的 .wv 文件 |
| 用 MMEM:DATA 传输文件 | 这是 R&S 仪器接收外部波形文件的标准方式 |
| 分层验证 | 先验证纯逻辑(无硬件),再验证 SCPI 通信,最后验证完整流程 |
| 检查仪器错误队列 | 用 :SYST:ERR? 和 *CLS 来诊断问题 |

浙公网安备 33010602011771号