RFSoC 10GS/s 交织 ADC 的 Offset 失配校准:为什么平均算法会把 2.5GHz 信号一起消掉?

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

但工程中不能只避开一个精确频点。

原因包括:

  1. 扫频信号不是理想单频。

  2. 1024 点有限长度平均会产生频率泄漏。

  3. 模拟前端和 ADC 具有非理想频响。

  4. 2.5GHz 附近的频率也可能在单个 lane 中表现出较慢变化。

  5. 扫频在窗口内会变化,平均值不一定为零。

因此,实际设计应设置一个保护带。

例如:

复制代码
禁止估计区域: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。

更稳定的方式是:

  1. 当前周期使用旧 offset。

  2. 后台累加新数据。

  3. 累加完成后计算新 offset。

  4. 在帧边界或明确的更新时刻一次性替换。

这样可以避免一个周期内 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 附近的危险频段。

最终需要通过扫频频谱验证三个结果:

  1. 直流 offset 是否下降。

  2. 2.5GHz 交织杂散是否改善。

  3. 2.5GHz 附近的有效扫频信号是否不再出现凹陷。

只有这三个结果同时满足,才能说明 offset 校准方案真正有效。

相关推荐
FPGA工程笔记6 天前
从1024点滑动平均到EMA:双路5GS/s ADC Offset校准的FPGA工程实现
fpga·adc·ema·rfsoc·tiadc·offset校准·滑动平均
神电测控15 天前
神电新型PG-RFSOC-27DR/47DR/67DR sbRIO板卡(内置8路上GS/s AD/DA模块,兼容LabVIEW FPGA开发)
labview·fpga·usrp·sdr·神电测控·王电令·rfsoc
expipi刘家伟1 个月前
RFSOC 信号采集处理模块
信号处理·rfsoc·阵列信号采样
FPGA-ADDA2 个月前
雷通微系统推出RTSD2605:RFSoC + 英伟达GPU一体化信号处理平台
信号处理·fpga·rfsoc·zu47dr·47dr
FPGA-ADDA6 个月前
第六篇:多速率信号处理——抽取、插值与半带滤波器
fpga开发·信号处理·软件无线电·rfsoc·47dr
FPGA-ADDA6 个月前
第四篇:射频数据转换器(RF-DAC)——重构模拟信号的关键
ai·fpga·rfsoc·vu13p·xczu47dr
FPGA-ADDA6 个月前
第一篇:软件无线电(SDR)概念与架构演进
fpga开发·信号处理·软件无线电·rfsoc·47dr
FPGA-ADDA7 个月前
第一篇:从“软件无线电”到“单芯片无线电”——RFSoC如何重塑无线系统设计
arm开发·信号处理·fpga·通信系统·rfsoc
FPGA-ADDA7 个月前
Xilinx Zynq UltraScale+ RFSoC XCZU47DR 开发板
fpga开发·fpga·rfsoc·xczu47dr