1. 项目背景
本项目使用 Xilinx RFSoC 48DR Gen3 开发板。
ADC 配置为:
ADC0:5GS/s
ADC1:5GS/s
两路 ADC 时钟相差 180°
交织后等效采样率:10GS/s
两路 ADC 的采样点交织关系可以简单表示为:
ADC0 采样:x[0]、x[2]、x[4]、x[6]、...
ADC1 采样:x[1]、x[3]、x[5]、x[7]、...
交织后的数据流为:
x[0] = ADC0 的第 0 个采样点
x[1] = ADC1 的第 0 个采样点
x[2] = ADC0 的第 1 个采样点
x[3] = ADC1 的第 1 个采样点
...
项目中已经完成了 gain 和 phase 校准,本文只讨论 offset 失配校准。
需要特别说明的是,RFSoC ADC IP 输出的 adc0_data 和 adc1_data 虽然看起来是两路数据,但经过交织之后,实际需要按照四路 ADC lane 来处理。
原因是:
每一路 5GS/s ADC 内部还包含多个交织采样通道
因此,offset 校准不能只计算一个 ADC0 offset 和一个 ADC1 offset,而应该分别计算四个 lane 的 offset。
2. 当前接口结构
ADC IP 输出接口如下:
adc0_data : 256 bit
adc1_data : 256 bit
adc_data_vld
其中:
-
adc0_data和adc1_data是两路 256 bit 数据。 -
两路数据使用同一个
adc_data_vld。 -
原 IP 中有两个 valid 信号。
-
两个 valid 信号已经通过 FIFO 做过对齐。
-
进入 offset 校准模块后,使用公共的
adc_data_vld。
数据流位置如下:
flowchart LR
A["RFSoC ADC IP"] --> B["adc0_data / adc1_data"]
B --> C["Offset 失配校准"]
C --> D["Gain / Phase 校准"]
D --> E["交织后的 ADC 数据"]
offset 校准模块放在 gain 和 phase 校准之前,主要原因是:
先消除各 lane 的直流偏置,
再进行增益和相位匹配,
有利于后续交织校准。
3. Offset 失配是什么?
理想情况下,四路 ADC lane 对同一个输入信号的响应应该一致。
实际中,每个 lane 都可能存在不同的直流偏置:
Lane0 offset = O0
Lane1 offset = O1
Lane2 offset = O2
Lane3 offset = O3
如果四路 offset 不一致,交织后的输出可以表示为:
y[4k] = x[4k] + O0
y[4k + 1] = x[4k + 1] + O1
y[4k + 2] = x[4k + 2] + O2
y[4k + 3] = x[4k + 3] + O3
如果四个 offset 完全相同,那么它们只是一个整体直流偏置。
但如果四个 offset 不同,误差会按照四个采样点重复一次:
O0、O1、O2、O3、O0、O1、O2、O3、...
这种周期性误差会在频谱上产生明显杂散。
对于四路交织系统,offset 失配最典型的杂散位置就是:
Fs / 4
本项目的等效采样率是:
Fs = 10GS/s
因此:
Fs / 4 = 2.5GHz
所以在 2.5GHz 附近看到凸起或凹陷,是四路交织 offset 失配的重要表现。
4. 为什么 offset 失配会出现在 2.5GHz?
先不考虑输入信号,只看四路 offset 误差:
O0、O1、O2、O3、O0、O1、O2、O3、...
这是一个长度为 4 的周期序列。
周期为 4 个采样点,对应的数字频率为:
1 / 4 个采样周期
换算到采样率就是:
Fs / 4
所以四路 offset 失配会在:
10GHz / 4 = 2.5GHz
附近形成杂散。
可以用下面的结构理解:
flowchart TB
A["Lane0 offset O0"] --> E["交织"]
B["Lane1 offset O1"] --> E
C["Lane2 offset O2"] --> E
D["Lane3 offset O3"] --> E
E --> F["周期为 4 的误差序列"]
F --> G["Fs / 4 = 2.5GHz 杂散"]
这也是为什么只对某一路输出进行 offset 处理,通常不能彻底解决 2.5GHz 附近的问题。
必须按照实际交织关系,对四个 lane 分别估计和补偿。
5. 最初考虑的矩形滑动平均
最直接的做法,是对每一路数据做滑动平均。
假设当前 lane 的输入数据为:
x[n]
使用长度为 N 的平均窗口:
offset_est[n] =
(x[n] + x[n-1] + ... + x[n-N+1]) / N
然后做 offset 补偿:
y[n] = x[n] - offset_est[n]
硬件结构大致如下:
flowchart LR
A["输入数据"] --> B["滑动窗口"]
B --> C["累加器"]
C --> D["除以窗口长度"]
D --> E["offset 估计值"]
A --> F["减法器"]
E --> F
F --> G["校准后数据"]
这种方法实现简单,资源消耗也不高。
但它有一个根本问题:
平均窗口中包含有效信号时,
平均值并不只是 offset。
平均值实际上是:
平均值 = 直流偏置 + 信号平均值
如果信号本身在窗口内存在明显的非零平均值,算法就会把信号的一部分误认为 offset。
6. EMA 为什么也会误消有效信号?
EMA,也就是指数移动平均,常见形式为:
offset_est[n] =
offset_est[n-1]
+ α × (x[n] - offset_est[n-1])
其中:
α 越小,更新越慢
α 越大,更新越快
EMA 看起来比矩形滑动平均更加平滑,但它仍然有同样的问题:
只要有效信号进入估计器,
估计值就会跟随有效信号变化。
最终输出:
校准输出 = 原始信号 - EMA 估计值
如果 EMA 估计值中包含信号成分,真实信号就会被一起减掉。
因此,矩形滑动平均和 EMA 都不是天然安全的 offset 校准方法。
关键不在于"平均算法选哪一种",而在于:
用于估计 offset 的数据,是否真的不包含有效信号?
7. 项目中的实际信号情况
项目中的扫频信号大致如下:
40 us:有效扫频信号
10 us:底噪或无有效信号
整体周期:
50 us
扫频范围:
1GHz 到 4GHz
FFT 只显示:
0GHz 到 5GHz
另一半频谱由于实数采样对称,没有重复显示。
问题表现为:
蓝色:offset 校准前
橙色:offset 校准后
在 2.5GHz 附近,校准后的频谱出现明显凹陷。
这说明 offset 估计模块不只消除了直流失配,还消掉了 2.5GHz 附近的有效扫频信号。
8. 第一个改进思路:只使用尾部 10us 底噪数据
由于每个周期包含:
40 us 有效扫频
10 us 底噪
可以只使用尾部 10us 的数据估计 offset。
处理流程:
flowchart LR
A["50us 周期"] --> B["40us 有效扫频"]
A --> C["10us 底噪区间"]
C --> D["累加 1024 个样本"]
D --> E["右移 10 位"]
E --> F["更新四路 offset"]
如果每一路累加 1024 个样本:
offset_est = sum / 1024
由于:
1024 = 2^10
因此平均值可以通过右移 10 位实现:
offset_est = sum >>> 10
这种方法相对于逐点滑动平均有一个明显优点:
offset 不是每个采样点都更新,
而是每个 50us 周期更新一次。
这相当于背景校准。
校准值在本周期内保持不变,下一周期再更新。
9. 为什么最后发现不一定非要使用底噪段?
实际验证中发现:
只要选择的 1024 个采样点避开 2.5GHz 扫频频段,
就可以得到较好的 offset 校准效果。
这说明算法真正需要满足的条件不是:
必须位于底噪段
而是:
用于估计 offset 的窗口不能包含会被 lane 当成直流的有效信号。
原因与四路交织结构有关。
10. 2.5GHz 信号为什么会被误认为 DC?
等效采样率为:
Fs = 10GHz
四路交织后,每个 lane 的采样间隔相当于:
4 / Fs
当输入信号频率为:
Fs / 4 = 2.5GHz
时,离散采样序列可以写成:
x[n] = cos(2π × n / 4)
其采样值是:
n = 0: 1
n = 1: 0
n = 2:-1
n = 3: 0
n = 4: 1
n = 5: 0
n = 6:-1
n = 7: 0
完整序列:
1、0、-1、0、1、0、-1、0、...
现在按四个 lane 拆开:
Lane0:1、1、1、1、...
Lane1:0、0、0、0、...
Lane2:-1、-1、-1、-1、...
Lane3:0、0、0、0、...
在每个 lane 内,这个 2.5GHz 信号不再表现为快速变化,而表现为一个固定电平。
flowchart TB
A["10GS/s 下的 2.5GHz 信号"] --> B["1、0、-1、0 重复"]
B --> C["按 4 路拆分"]
C --> D["Lane0:固定为 1"]
C --> E["Lane1:固定为 0"]
C --> F["Lane2:固定为 -1"]
C --> G["Lane3:固定为 0"]
D --> H["每个 lane 看起来像 DC"]
E --> H
F --> H
G --> H
因此,如果 offset 估计窗口正好覆盖 2.5GHz 扫频段:
Lane0 平均值中包含 +1
Lane2 平均值中包含 -1
算法会把这部分有效信号误认为 offset,然后进行扣除。
结果就是:
2.5GHz 附近的有效信号被压低
这正是频谱中出现凹陷的原因。
11. 为什么要避开 2.5GHz,而不是只避开一个频点?
理论上,最危险的频率是:
Fs / 4 = 2.5GHz
但工程中不能只避开一个精确频点。
原因包括:
-
扫频信号不是理想单频。
-
1024 点有限长度平均会产生频率泄漏。
-
模拟前端和 ADC 具有非理想频响。
-
2.5GHz 附近的频率也可能在单个 lane 中表现出较慢变化。
-
扫频在窗口内会变化,平均值不一定为零。
因此,实际设计应设置一个保护带。
例如:
禁止估计区域:2.4GHz 到 2.6GHz
具体保护带需要根据:
-
扫频速度。
-
1024 点窗口长度。
-
信号幅度。
-
允许的校准误差。
-
实际频谱结果。
进行测试确定。
这里不能简单写成:
只要不是 2.5GHz 就一定安全。
更准确的说法是:
要避开 Fs / 4 及其附近可能被 lane 视为低频或直流的频段。
12. 推荐的校准策略
本项目推荐使用周期性块平均,而不是连续滑动平均。
处理流程如下:
每 50us 触发一次校准周期
→ 选择一个安全窗口
→ 每个 lane 累加 1024 个样本
→ 对每个 lane 求平均
→ 更新四个 offset 寄存器
→ 下一个 50us 周期继续
四路 offset 估计值:
offset0 = sum0 / 1024
offset1 = sum1 / 1024
offset2 = sum2 / 1024
offset3 = sum3 / 1024
校准输出:
lane0_out = lane0_in - offset0
lane1_out = lane1_in - offset1
lane2_out = lane2_in - offset2
lane3_out = lane3_in - offset3
整体结构:
flowchart LR
A["adc0_data / adc1_data"] --> B["256bit 数据拆分"]
B --> C["四路 lane"]
C --> D["安全窗口选择"]
D --> E["四路独立累加"]
E --> F["右移 10 位"]
F --> G["offset0~offset3"]
C --> H["四路减法补偿"]
G --> H
H --> I["输出到 gain / phase 校准"]
13. 关键时序设计
建议将 offset 估计和 offset 补偿分开。
offset 估计:周期性更新
offset 补偿:每个有效采样点实时执行
时序关系可以设计为:
sequenceDiagram
participant V as adc_data_vld
participant A as 累加器
participant U as Offset 更新
participant C as 补偿输出
V->>A: 有效数据到来
A->>A: 累加 1024 个样本
A->>U: sample_count = 1023
U->>U: sum >>> 10
U->>U: 更新 offset0~offset3
U->>C: 下一周期使用新 offset
V->>C: 当前数据减去稳定 offset
这里有一个重要原则:
不要在累加窗口尚未结束时,
逐点改变正在使用的 offset。
更稳定的方式是:
-
当前周期使用旧 offset。
-
后台累加新数据。
-
累加完成后计算新 offset。
-
在帧边界或明确的更新时刻一次性替换。
这样可以避免一个周期内 offset 不断变化,减少输出跳变。
14. 1024 点累加的位宽怎么计算?
假设每个 lane 的输入位宽为:
W bit
累加 1024 个有符号数据,理论上需要增加:
log2(1024) = 10 bit
因此累加器至少可以按下面估算:
累加器位宽 = W + 10 bit
实际设计中还需要考虑:
-
输入数据是否已经左移。
-
是否使用补码。
-
最大正值和最小负值是否对称。
-
是否存在额外保护位。
-
累加器是否可能跨周期清零。
-
数据有效信号是否存在空拍。
例如:
输入:16 bit signed
累加器:至少 26 bit signed
平均值:累加结果右移 10 位
伪代码如下:
if (adc_data_vld && sample_enable) begin
sum0 <= sum0 + lane0_data;
sum1 <= sum1 + lane1_data;
sum2 <= sum2 + lane2_data;
sum3 <= sum3 + lane3_data;
if (sample_cnt == 10'd1023) begin
offset0_next <= sum0 >>> 10;
offset1_next <= sum1 >>> 10;
offset2_next <= sum2 >>> 10;
offset3_next <= sum3 >>> 10;
sum0 <= '0;
sum1 <= '0;
sum2 <= '0;
sum3 <= '0;
sample_cnt <= '0;
end
else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
实际 RTL 中要注意非阻塞赋值带来的"最后一个样本没有进入平均值"问题。
上面的写法只是表达思路。更严谨的写法应在计算新 offset 时把当前输入一起加入:
sum0_full = sum0 + lane0_data;
offset0_next <= sum0_full >>> 10;
或者让累加器先完成最后一次更新,再在下一个时钟计算平均值。
15. 1024 个样本到底是哪 1024 个?
这是方案能否稳定工作的关键。
不能只写:
每 50us 取 1024 个样本
还要明确:
从哪个时间位置开始?
是否位于有效扫频段?
是否避开 2.5GHz 附近?
是否要求连续 1024 个样本?
推荐使用一个明确的窗口控制器:
stateDiagram-v2
[*] --> WAIT_PERIOD
WAIT_PERIOD --> WAIT_WINDOW: 到达校准周期
WAIT_WINDOW --> ACCUMULATE: 进入安全窗口
ACCUMULATE --> ACCUMULATE: 累加有效样本
ACCUMULATE --> UPDATE: 累加满1024点
UPDATE --> HOLD: 更新四路offset
HOLD --> WAIT_PERIOD: 等待下一个周期
如果使用尾部底噪数据:
50us 周期
├── 0us 到 40us:有效扫频
└── 40us 到 50us:底噪区间
可以在底噪区间内选取一段连续窗口。
如果不想依赖尾部底噪,也可以在有效扫频期间设置屏蔽区:
检测或预先知道扫频经过 2.5GHz 的时间
→ 屏蔽该时间段
→ 在其他时间选取 1024 个样本
但如果有效扫频幅度变化很大,仍然要通过实验确认该窗口的平均值不会明显偏离 offset。
16. 50us 更新一次是否足够?
每 50us 更新一次 offset,属于背景校准。
它的优点是:
-
硬件实现简单。
-
不会每个采样点都改变校准值。
-
对正常慢变化的 offset 漂移有跟踪能力。
-
不容易把瞬时有效信号直接当成 offset。
但它有一个前提:
ADC offset 在 50us 内不能发生过快变化。
如果 offset 漂移速度很慢,50us 更新一次通常足够。
如果温度变化、时钟变化或输入状态导致 offset 快速变化,就需要重新评估:
更新周期是否太长?
1024 点窗口是否太长?
是否需要增加温度或状态触发的校准?
更新周期不是越短越好。
更新太快会导致:
估计值更容易受到有效信号影响
更新太慢又会导致:
offset 漂移不能及时跟踪
所以应根据实际 offset 漂移速度和扫频信号规律选择。
17. 常见错误方案
17.1 只对 ADC0 和 ADC1 各算一个 offset
错误原因:
ADC0_data 和 ADC1_data 内部仍然包含多个交织 lane
结果是:
不同 lane 的 offset 被混在一起平均
不能真正消除四路交织误差。
正确做法是:
先按照实际数据排列拆出四个 lane,
再对四个 lane 分别估计 offset。
17.2 每个采样点都更新 offset
错误原因:
有效扫频数据会直接进入估计器
最终校准模块变成了一个会跟踪信号的高通结构,真实信号会被削弱。
17.3 只避开 2.5GHz 一个精确频点
错误原因:
有限长度平均和扫频过程会带来频率扩展
实际应避开 2.5GHz 附近设置保护带,并用频谱结果验证。
17.4 用普通加法器累加 1024 个样本
错误原因:
累加器位宽不够会溢出
溢出后,平均值可能完全错误,甚至变成符号相反的数值。
17.5 忽略 adc_data_vld
错误原因:
无效数据也被加入累加器
如果 adc_data_vld 不是每拍都有效,样本计数和累加计数必须只在有效时钟上推进。
18. 推荐的验证方法
18.1 全零或无输入信号
检查:
四个 lane 的平均值是否稳定
offset 更新是否正常
输出是否接近零
18.2 输入固定频率信号
选择远离 2.5GHz 的频率,观察:
校准前后的信号幅度
四个 lane 的 offset 估计值
是否出现信号被误消除
18.3 扫频信号
重点观察:
1GHz 到 4GHz 扫频过程
2.5GHz 附近是否出现凹陷
校准窗口改变后凹陷是否变化
18.4 改变 1024 点窗口位置
分别测试:
底噪区间
有效扫频区间
2.5GHz 附近
远离 2.5GHz 的有效信号区间
如果只有 2.5GHz 附近窗口会产生明显错误,就能验证:
2.5GHz 信号被每个 lane 误认为 DC
18.5 软件模型逐 lane 对比
建议在 Python 或 MATLAB 中先完成:
四路拆分
1024 点平均
四路 offset 扣除
重新交织
FFT
伪代码如下:
lane0 = data[0::4]
lane1 = data[1::4]
lane2 = data[2::4]
lane3 = data[3::4]
offset0 = np.mean(lane0[start:start + 1024])
offset1 = np.mean(lane1[start:start + 1024])
offset2 = np.mean(lane2[start:start + 1024])
offset3 = np.mean(lane3[start:start + 1024])
lane0_corr = lane0 - offset0
lane1_corr = lane1 - offset1
lane2_corr = lane2 - offset2
lane3_corr = lane3 - offset3
corrected = np.empty_like(data)
corrected[0::4] = lane0_corr
corrected[1::4] = lane1_corr
corrected[2::4] = lane2_corr
corrected[3::4] = lane3_corr
这里的 data[0::4] 只是说明四路拆分的思想。
实际工程中,必须根据 RFSoC ADC IP 的 256 bit 数据打包方式确认:
哪个 bit 段对应哪个采样点?
哪个采样点属于哪个 lane?
一个 valid 对应多少个有效采样点?
不能直接假设数据排列一定是简单的 0、1、2、3 交替。
19. 最终推荐结构
本项目可以采用下面的整体结构:
flowchart TB
A["RFSoC ADC IP"] --> B["256bit数据解包"]
B --> C["按真实打包关系拆分四路lane"]
C --> D["实时offset补偿"]
C --> E["校准窗口选择"]
E --> F["Lane0累加1024点"]
E --> G["Lane1累加1024点"]
E --> H["Lane2累加1024点"]
E --> I["Lane3累加1024点"]
F --> J["offset0"]
G --> K["offset1"]
H --> L["offset2"]
I --> M["offset3"]
J --> D
K --> D
L --> D
M --> D
D --> N["Gain / Phase校准"]
实现时建议遵循以下原则:
四个 lane 分别估计
每 50us 更新一次
每路累加 1024 个有效样本
平均值通过右移 10 位获得
校准窗口避开 Fs / 4 附近
offset 寄存器一次性更新
实时数据只做减法补偿
20. 总结
这个问题表面上是一个简单的 DC offset 校准问题,实际上同时涉及:
多路 ADC 交织
数据 lane 映射
周期性误差频谱
背景校准
扫频信号规律
固定点累加
更新时序
最初的矩形滑动平均和 EMA 都能消除一部分 DC 偏置,但它们没有区分:
真正的 offset
有效信号的局部平均值
因此,在扫频信号经过 2.5GHz 时,算法会把有效信号当成 offset 一起消除。
四路交织系统的关键现象是:
2.5GHz = 10GHz / 4
对于四路 lane 来说,2.5GHz 信号在每个 lane 内可能表现为固定电平,因此会被平均算法误判为 DC。
更稳妥的工程方案是:
将两路 256 bit 数据按照真实打包关系拆成四个 lane,
每个 lane 独立估计 offset,
每 50us 更新一次,
每次累加 1024 个有效样本,
并避开 Fs / 4 附近的危险频段。
最终需要通过扫频频谱验证三个结果:
-
直流 offset 是否下降。
-
2.5GHz 交织杂散是否改善。
-
2.5GHz 附近的有效扫频信号是否不再出现凹陷。
只有这三个结果同时满足,才能说明 offset 校准方案真正有效。