1. 先说项目背景
项目里有两路独立的5GS/s ADC:
adc0_data[255:0]
adc1_data[255:0]
FPGA工作时钟为:
312.5 MHz
每路数据宽度256bit,一个采样点16bit,因此每拍包含:
256 / 16 = 16 samples
对应数据率:
312.5 MHz × 16 = 5 GS/s
两路ADC经过前级FIFO对齐后,同时给出:
adc_data_vld
adc0_data[255:0]
adc1_data[255:0]
每路数据从低位开始依次为:
adc0_data[15:0] -> sample0
adc0_data[31:16] -> sample1
adc0_data[47:32] -> sample2
...
adc0_data[255:240] -> sample15
adc1_data同理。
后级最终按照下面顺序手动交织:
adc0_s0, adc1_s0,
adc0_s1, adc1_s1,
adc0_s2, adc1_s2,
...
得到10GS/s数据流。
本文最终采用的数据通路是:
RFDC
│
▼
FIFO 对齐
│
▼
Offset Correction
│
▼
Gain/Phase Correction
│
▼
手动交织成10GS/s
这里要特别说明一下。
前期讨论里曾出现过一句"Offset在Gain/Phase之后",但按照实际接口描述:
Offset模块输出
↓
Gain/Phase模块输入
所以真正的数据流应该是:
Offset → Gain/Phase
本文全部按照这个顺序讨论。
2. 为什么一开始想用1024点滑动平均?
MATLAB离线处理最容易想到的方法是:
当前数据 - 前1024点平均值
数学表达式:
yn=xn−11024∑k=11024xn−kyn = xn - \frac{1}{1024} \sum_{k=1}^{1024}xn-k
这种方法的问题不在于"不能实现",而是它实际上做的是:
把每一路自己的DC分量全部去掉。
但对于交织ADC,我们真正关心的是:
不同采样相位之间的Offset差值。
假设四个逻辑采样相位的平均值分别为:
μ0,μ1,μ2,μ3\mu_0,\mu_1,\mu_2,\mu_3
真正需要消除的是:
μ1−μ0\mu_1-\mu_0 μ2−μ0\mu_2-\mu_0 μ3−μ0\mu_3-\mu_0
而不是把:
μ0,μ1,μ2,μ3\mu_0,\mu_1,\mu_2,\mu_3
全部减成0。
这两个目标差别很大。
比如输入信号本身真的带有一个+100 LSB的公共DC:
真实信号DC = +100
如果每一路都减自己的平均值:
最终DC ≈ 0
真实信号DC也被一起干掉了。
而如果只消除通道间的相对Offset:
四路最终仍然保持在约 +100 LSB
这才是本文需要的结果。
3. 为什么两路ADC要拆成4个"逻辑相位组"?
每个ADC一拍有16个采样点。
我们把每路的偶数点和奇数点分开。
对于adc0_data:
P0:
s0, s2, s4, s6, s8, s10, s12, s14
P2:
s1, s3, s5, s7, s9, s11, s13, s15
对于adc1_data:
P1:
t0, t2, t4, t6, t8, t10, t12, t14
P3:
t1, t3, t5, t7, t9, t11, t13, t15
于是每个312.5MHz周期,可以得到4组数据:
| 逻辑相位 | 数据来源 | 每拍采样点数 |
|---|---|---|
| P0 | adc0偶数点 | 8 |
| P1 | adc1偶数点 | 8 |
| P2 | adc0奇数点 | 8 |
| P3 | adc1奇数点 | 8 |
最终10GS/s交织后的逻辑顺序实际上就是:
P0 → P1 → P2 → P3 → P0 → P1 → P2 → P3 ...
所以从FPGA算法角度,可以把它看成一个4相位周期。
但是这里不要犯一个概念错误:
P0/P1/P2/P3是我们根据PL输出数据人为定义的"逻辑相位组",不能直接说它们就是RFSoC内部4个物理sub-ADC。
RFSoC内部实际还有自己的sub-ADC交织结构。
本文只讨论:
从PL看到的数据里,
按照even/odd拆分以后,
这种4相位统计方法能够改善实际Offset失配。
如果板上测试发现还有更高周期的Offset杂散,那么说明只做even/odd拆分还不够,需要继续细分。
4. 一个关键修正:不能只做两个Offset
前面讨论中曾有一个版本:
adc0:
odd 对齐 even
adc1:
odd 对齐 even
这样只有两个Offset系数。
看起来很省资源,但实际上漏了一个问题:
adc0整体DC
和
adc1整体DC
仍然可能不一样。
假设校准后:
adc0_even ≈ adc0_odd ≈ +20
adc1_even ≈ adc1_odd ≈ -15
虽然两个ADC内部已经分别对齐,但最终10GS/s交织后仍然是:
+20, -15, +20, -15 ...
显然还有35 LSB的ADC间Offset mismatch。
所以最终应该使用:
1个参考相位 + 3个相对Offset系数。
本文选择:
P0 = adc0_even
作为参考。
定义:
Offset1=μ1−μ0Offset_1=\mu_1-\mu_0 Offset2=μ2−μ0Offset_2=\mu_2-\mu_0 Offset3=μ3−μ0Offset_3=\mu_3-\mu_0
校正:
P0:保持不变
P1:sample - Offset1
P2:sample - Offset2
P3:sample - Offset3
最后:
μ0≈μ1,corr≈μ2,corr≈μ3,corr\mu_0 \approx \mu_{1,corr} \approx \mu_{2,corr} \approx \mu_{3,corr}
这样既消除了ADC内部even/odd偏差,也消除了adc0和adc1之间的整体Offset差。
5. 每拍先求8点和,不要逐点估计Offset
这里是整个设计降低资源的第一步。
以P0为例:
P0 = adc0_s0
adc0_s2
adc0_s4
...
adc0_s14
先计算:
B0=s0+s2+s4+...+s14B_0= s_0+s_2+s_4+...+s_{14}
同理:
B1,B2,B3B_1,B_2,B_3
然后计算:
D1=B1−B0D_1=B_1-B_0 D2=B2−B0D_2=B_2-B_0 D3=B3−B0D_3=B_3-B_0
因为每个B都包含8个采样点,所以:
Di8\frac{D_i}{8}
就是这一拍对应两个逻辑相位之间的平均Offset差估计。
注意:
FPGA里面没必要马上真的除8。
后面统一利用移位处理即可。
6. 8输入加法树怎么写才适合312.5MHz?
千万不要写成一长串:
sum = a+b+c+d+e+f+g+h;
虽然综合工具可能会帮你优化,但312.5MHz下没必要赌工具。
直接做三级加法树。
Level 1:
x0 + x1 ─┐
├─
x2 + x3 ─┘
x4 + x5 ─┐
├─
x6 + x7 ─┘
Level 2:
sum01 + sum23
sum45 + sum67
Level 3:
sum0123 + sum4567
对应位宽:
16bit sample
16 + 16
↓
17bit
17 + 17
↓
18bit
18 + 18
↓
19bit
所以8个16bit有符号数求和,19bit signed就够。
四个相位一共需要:
4 × 7 = 28个加法器
完全没有必要使用DSP。
7. 为什么不用真正的1024点滑动窗口?
如果严格按照1024点矩形滑动平均:
Mn=xn−1024+...+xn−11024Mn = \frac{xn-1024+...+xn-1}{1024}
硬件需要保存历史数据。
但注意,现在一个逻辑相位每拍已经有8个采样点。
因此:
1024 samples / 8 samples/clk
=
128 clk
所以即使实现块级滑动平均,也可以只保存最近128个D_i:
Sik+1=Sik+Dik−Dik−128S_ik+1 = S_ik+D_ik-D_ik-128
然后:
Offseti=Si>>10Offset_i=S_i>>10
为什么右移10?
因为:
128×8=1024=210128\times8=1024=2^{10}
三个通道需要保存:
3 × 128 × 20bit
≈ 7680bit
其实也不算大。
所以这里要说清楚:
EMA不是因为1024点滑窗"做不了"才使用,而是因为Offset本身属于慢变化参数,没有必要为了矩形窗口特性保存历史数据。
8. EMA到底是什么?
标准EMA公式:
yk=yk−1+α(xk−yk−1)yk = yk-1 + \alpha (xk-yk-1)
也可以写成:
yk=(1−α)yk−1+αxkyk = (1-\alpha)yk-1 + \alpha xk
展开以后:
yk=αxk+α(1−α)xk−1+α(1−α)2xk−2+⋯yk = \alpha xk + \alpha(1-\alpha)xk-1 + \alpha(1-\alpha)^2xk-2 +\cdots
也就是说,EMA同样是在做平均。
区别是:
滑动平均
最近N个数据权重完全一样:
********
********
********
********
超过窗口以后,权重直接变成0。
EMA
越新的数据权重越大,越老的数据权重指数下降:
********
******
****
**
*
.
.
所以:
EMA和1024点滑动平均并不是数学意义上的完全等价。
这是一定要说清楚的。
两者:
冲激响应不同
阶跃响应不同
历史数据权重不同
我们这里只要求:
对一个缓慢变化的Offset参数,达到接近的统计平滑效果。
9. 为什么这里选择α=1/64?
EMA对白噪声的等效平均长度可以近似写成:
Neq=2−αα=2α−1N_{eq} = \frac{2-\alpha}{\alpha} = \frac{2}{\alpha}-1
如果:
α=164\alpha=\frac1{64}
那么:
Neq=127N_{eq}=127
这里一个EMA update代表8个逻辑相位采样点,因此:
127×8=1016127\times8=1016
与原来的:
1024 samples
非常接近。
因此第一版RTL与MATLAB做效果对比时,可以直接选择:
K = 6
α = 2^-6 = 1/64
注意,这里的"1016点等效"指的是:
对白噪声平均效果或者噪声带宽意义上的近似等效。
不是说:
EMA输出 == 1024点滑动平均输出
两条波形不会逐点完全一样。
10. EMA在FPGA里不要直接这么写
最直观的写法是:
error = input - offset;
offset = offset + (error >>> 6);
但如果offset只是整数LSB,这种写法有一个问题。
比如:
error = 20
那么:
20 >>> 6 = 0
EMA直接不更新。
也就是说,会出现明显的:
定点死区。
所以更推荐保留EMA内部的小数精度。
11. 推荐的EMA硬件形式:缩放累加器
我们已经有:
Di=Bi−B0D_i=B_i-B_0
不要先做:
D_i >>> 3
而是直接维护:
Aik+1=Aik+Dik−Aik2KA_ik+1 = A_ik + D_ik - \frac{A_ik}{2^K}
FPGA就是:
ema_acc <= ema_acc
+ diff
- (ema_acc >>> K);
最终真正用于样点校正的Offset:
Offseti=Ai2K×8Offset_i = \frac{A_i}{2^K\times8}
因为8等于:
232^3
所以:
offset = ema_acc >>> (K + 3);
以K=6为例:
offset = ema_acc >>> 9;
这个形式有几个好处:
1. 不需要乘法器
2. 不需要除法器
3. 不需要BRAM
4. 保留了EMA内部的小数信息
5. 不会因为过早右移3bit和6bit损失大量精度
这是我更推荐的RTL实现形式。
12. 为什么这个公式是对的?
标准EMA:
Mk+1=Mk+Dk−Mk2KMk+1 = Mk + \frac{Dk-Mk}{2^K}
定义:
Ak=2KMkAk=2^K Mk
两边乘:
2K2^K
得到:
Ak+1=Ak+Dk−Ak2KAk+1 = Ak + Dk - \frac{Ak}{2^K}
也就是:
A_new = A + D - (A >>> K);
而这里的D是8个采样点的Offset差之和。
所以:
Offset=A2K×8Offset=\frac{A}{2^K\times8}
最终就是:
offset = A >>> (K+3);
整个算法只剩:
加法
减法
算术右移
寄存器
非常适合FPGA。
13. Foreground + Background,比纯EMA更实用
实际工程里我不建议上电以后直接让EMA从0慢慢爬。
更实用的方式是:
Foreground快速建立初始值
↓
Background EMA慢速跟踪
13.1 Foreground校准
每个逻辑相位一拍有8个采样点。
累计128个有效拍:
128 × 8 = 1024 samples
所以:
ACCi=∑k=0127DikACC_i = \sum_{k=0}^{127}D_ik
最终:
Offseti=ACCi/1024Offset_i = ACC_i/1024
即:
offset_i = acc_i >>> 10;
这里不要先:
diff >>> 3
再累计。
推荐:
直接累计原始20bit diff
最后一次性 >>> 10
这样定点误差更小。
13.2 注意一个非常常见的RTL Bug
假设计数器:
0 ~ 127
当:
fg_cnt == 127
时,如果直接:
offset <= fg_acc >>> 10;
实际上会漏掉当前这一次diff。
正确思路应该是:
fg_acc_next = fg_acc + diff;
if (fg_cnt == 127)
offset <= fg_acc_next >>> 10;
这是累加类RTL里非常常见的off-by-one错误。
14. Background校准
Foreground结束以后进入Background。
三个EMA分别运行:
EMA1:P1 - P0
EMA2:P2 - P0
EMA3:P3 - P0
即:
ema1 <= ema1 + diff1_ext - (ema1 >>> K);
ema2 <= ema2 + diff2_ext - (ema2 >>> K);
ema3 <= ema3 + diff3_ext - (ema3 >>> K);
最终:
offset1 = ema1 >>> (K+3);
offset2 = ema2 >>> (K+3);
offset3 = ema3 >>> (K+3);
然后实时用于数据校正。
15. Freeze信号一定建议保留
简单EMA有一个天然问题:
它不知道当前看到的"相位均值差",到底来自ADC Offset,还是输入信号本身。
如果输入信号在某些和交织周期强相关的频率上有明显能量,那么Background EMA可能会:
把真实信号的一部分
误认为Offset
所以建议增加:
cal_freeze
逻辑非常简单:
cal_freeze = 0:
正常更新EMA
cal_freeze = 1:
Offset系数保持不变
数据仍然继续使用当前系数校正
注意:
Freeze只冻结"系数更新",不要冻结数据通路。
以后可以根据:
TDD状态
训练序列
输入信号状态
系统工作模式
控制它。
16. 状态机没必要做得很复杂
第一版建议只有:
IDLE/RESET
│
▼
FOREGROUND
│
│ 128个有效数据拍
▼
BACKGROUND
而:
cal_freeze
不要单独做一个状态。
直接作为Background内部的更新使能即可。
例如:
if (state == BACKGROUND) begin
if (stat_vld && !cal_freeze) begin
// update EMA
end
end
这样状态机会简单很多。
17. 一个更重要的FPGA优化:估计通路和数据通路分开
早期最容易画成下面这样:
输入数据
↓
拆分
↓
三级SUM
↓
diff
↓
EMA
↓
Offset
↓
校正当前输入数据
如果这样做,为了等Offset计算出来,就要把两路256bit原始数据跟着延迟很多拍:
512bit × 多级pipeline
会消耗大量寄存器。
实际上完全没有必要。
因为Offset是一个非常慢的参数。
当前这一拍的数据:
完全可以使用"上一拍已经得到的Offset系数"进行校正。
因此推荐把模块拆成两条并行路径。
17.1 数据校正主通路
┌───────────────┐
adc0/adc1 ─────>│ Offset减法器 │─────> 输出
└───────▲───────┘
│
当前Offset系数
只需要:
输入寄存
↓
减法
↓
饱和
↓
输出寄存
两三拍就够。
17.2 Offset估计旁路
输入数据
│
├─> P0/P1/P2/P3
│
├─> 4组SUM8
│
├─> 3组diff
│
└─> FG / EMA
│
▼
更新Offset寄存器
这条通路即使需要:
5~6拍
也完全没有问题。
因为它只是在更新下一时刻的校准参数。
18. 最终推荐架构
整个模块可以理解成:
┌─────────────────────────────┐
│ Offset Estimator │
│ │
adc0_data ───────┬──────>│ even/odd → SUM8 ──┐ │
│ │ ├─diff──EMA│
adc1_data ───────┼──────>│ even/odd → SUM8 ──┘ │
│ │ │
│ └──────────────┬──────────────┘
│ │
│ offset1/2/3
│ │
▼ ▼
┌────────────────────────────────┐
│ Offset Corrector │
│ │
│ P0:保持 │
│ P1:- offset1 │
│ P2:- offset2 │
│ P3:- offset3 │
└──────────────┬─────────────────┘
│
▼
adc0_corr[255:0]
adc1_corr[255:0]
│
▼
Gain/Phase Correction
这比"数据跟着估计器跑完整条pipeline"更合理。
19. Offset校正具体落在哪些sample上?
定义:
P0 = adc0_even
P1 = adc1_even
P2 = adc0_odd
P3 = adc1_odd
P0作为参考。
那么:
adc0
s0 -> P0 -> 不处理
s1 -> P2 -> -offset2
s2 -> P0 -> 不处理
s3 -> P2 -> -offset2
...
adc1
t0 -> P1 -> -offset1
t1 -> P3 -> -offset3
t2 -> P1 -> -offset1
t3 -> P3 -> -offset3
...
最终:
adc0偶数:不减
adc0奇数:减offset2
adc1偶数:减offset1
adc1奇数:减offset3
所以一共需要:
3 × 8 = 24个并行sample减法器
20. Offset正负方向不要写反
这里一定要把定义固定死。
本文统一定义:
Offseti=Mean(Pi)−Mean(P0)Offset_i=Mean(P_i)-Mean(P_0)
所以:
Pi,corr=Pi−OffsetiP_{i,corr}=P_i-Offset_i
举个例子。
假设:
P0平均值 = +20
P1平均值 = -10
则:
offset1 = -10 - 20
= -30
校正:
P1_corr = P1 - (-30)
= P1 + 30
因此:
-10 + 30 = +20
正好和P0对齐。
以后MATLAB、RTL、ILA、寄存器文档都使用同一个定义:
offset = phase - reference
corrected = phase - offset
这样最不容易出错。
21. 位宽怎么定?
以下假设输入为:
16bit signed two's complement
如果实际RFDC接口格式不同,第一步应该先转换成统一的signed格式。
21.1 SUM8
16bit signed范围:
-32768 ~ +32767
8点相加:
19bit signed
足够。
推荐:
Level1:17bit
Level2:18bit
Level3:19bit
21.2 diff
两个19bit SUM相减:
20bit signed
所以:
logic signed [19:0] diff1;
logic signed [19:0] diff2;
logic signed [19:0] diff3;
21.3 Foreground accumulator
需要累计128个20bit数据。
理论增加:
log2(128)=7bit
所以27bit刚好够。
工程里建议留1bit保护:
28bit signed
21.4 EMA accumulator
EMA采用:
A <= A + D - (A >>> K);
大致需要:
DIFF_WIDTH + K
位。
建议再留一个保护位:
EMA_WIDTH = 20 + K_MAX + 1
如果:
K_MAX = 16
则:
EMA_WIDTH = 37bit
三个37bit寄存器几乎不算什么资源。
22. K到底要不要运行时可配置?
算法上我建议K可调。
比如:
K = 6
K = 8
K = 10
K = 12
K = 14
K = 16
但是RTL实现要注意:
如果直接写:
ema_acc >>> k_reg
而k_reg是运行时变量,综合器通常会做一个:
barrel shifter
相比固定移位,它会多消耗一些LUT,也可能增加路径延迟。
所以有两种选择。
第一版,资源优先
直接写成parameter:
parameter integer EMA_K = 6;
编译时固定。
这是最省资源的方式。
上板调试版
只允许几个离散值,例如:
case(k_sel)
3'd0: ema_shift = ema_acc >>> 6;
3'd1: ema_shift = ema_acc >>> 8;
3'd2: ema_shift = ema_acc >>> 10;
3'd3: ema_shift = ema_acc >>> 12;
endcase
这样可控一些。
23. 为什么最终上板可能不应该继续用K=6?
现在每个逻辑相位的等效采样率大约是:
10GS/s/4=2.5GS/s10GS/s / 4 = 2.5GS/s
1024个采样点只对应:
1024/2.5G=409.6ns1024/2.5G = 409.6ns
也就是说:
原来的1024点窗口实际上只有约0.41微秒。
对于温度漂移之类的慢Offset变化,这个时间已经非常短。
所以我的建议是:
仿真阶段:
K=6
方便和原1024点MATLAB结果比较
上板阶段:
逐渐测试K=8/10/12/14/16
观察spur和跟踪速度
不要因为MATLAB最初用了1024,就认为硬件最终必须永远等效1024。
24. 312.5MHz下推荐的流水结构
估计器可以这样流水。
Cycle N
输入32个sample
│
▼
Stage 1
8点分组 + 第一层pair sum
│
▼
Stage 2
第二层sum
│
▼
Stage 3
最终SUM8
│
▼
Stage 4
diff1 / diff2 / diff3
│
▼
Stage 5
Foreground累加 或 EMA更新
每一级基本只有:
一次加减法
312.5MHz时钟周期:
1/312.5MHz=3.2ns1/312.5MHz=3.2ns
这种流水压力不大。
真正需要注意的反而是:
512bit宽总线的布线
+
24个并行校正减法器
所以数据校正通路最好单独做2~3级pipeline。
25. Valid怎么处理?
这里必须建立一个原则:
所有统计更新都以adc_data_vld为准,而不是单纯数FPGA时钟。
比如Foreground需要:
128个有效数据拍
不是:
复位以后过128个系统时钟
正确逻辑:
if (stat_vld) begin
fg_cnt <= fg_cnt + 1'b1;
end
当:
vld = 0
时:
FG counter保持
FG accumulator保持
EMA accumulator保持
如果输入valid中间有空洞,那么校准的"样本数量"仍然是正确的。
26. 数据通路和Estimator的valid不要混在一起
由于我们已经把两条路径拆开:
Correction Datapath
和
Offset Estimator
所以实际上有两套延迟。
例如:
adc_data_vld
│
├────────> correction valid delay ────> adc_corr_vld
│
└────────> estimator valid delay ─────> stat_vld
adc_corr_vld只和输出数据对齐。
stat_vld只和:
SUM8
diff
对齐。
两者没有必要延迟相同的拍数。
这种写法比强行把所有valid做成一个总流水线清楚得多。
27. RTL:数据拆分示例
下面使用SystemVerilog写法,主要为了代码更清楚。
logic signed [15:0] adc0_s [0:15];
logic signed [15:0] adc1_s [0:15];
generate
for (genvar i = 0; i < 16; i++) begin : GEN_UNPACK
assign adc0_s[i] = $signed(adc0_data[i*16 +: 16]);
assign adc1_s[i] = $signed(adc1_data[i*16 +: 16]);
end
endgenerate
逻辑分组:
P0 = adc0_s[0], adc0_s[2], ..., adc0_s[14]
P1 = adc1_s[0], adc1_s[2], ..., adc1_s[14]
P2 = adc0_s[1], adc0_s[3], ..., adc0_s[15]
P3 = adc1_s[1], adc1_s[3], ..., adc1_s[15]
28. RTL:一个SUM8三级流水示意
以P0为例:
logic signed [16:0] p0_l1 [0:3];
logic signed [17:0] p0_l2 [0:1];
logic signed [18:0] p0_sum;
always_ff @(posedge clk) begin
if (rst) begin
p0_l1[0] <= '0;
p0_l1[1] <= '0;
p0_l1[2] <= '0;
p0_l1[3] <= '0;
p0_l2[0] <= '0;
p0_l2[1] <= '0;
p0_sum <= '0;
end
else begin
// Level 1
p0_l1[0] <= adc0_s[0] + adc0_s[2];
p0_l1[1] <= adc0_s[4] + adc0_s[6];
p0_l1[2] <= adc0_s[8] + adc0_s[10];
p0_l1[3] <= adc0_s[12] + adc0_s[14];
// Level 2
p0_l2[0] <= p0_l1[0] + p0_l1[1];
p0_l2[1] <= p0_l1[2] + p0_l1[3];
// Level 3
p0_sum <= p0_l2[0] + p0_l2[1];
end
end
P1/P2/P3完全同样的结构。
这里只需要注意valid同步延迟3拍即可。
29. RTL:diff计算
得到:
p0_sum
p1_sum
p2_sum
p3_sum
以后:
logic signed [19:0] diff1;
logic signed [19:0] diff2;
logic signed [19:0] diff3;
always_ff @(posedge clk) begin
if (rst) begin
diff1 <= '0;
diff2 <= '0;
diff3 <= '0;
end
else begin
diff1 <= $signed(p1_sum) - $signed(p0_sum);
diff2 <= $signed(p2_sum) - $signed(p0_sum);
diff3 <= $signed(p3_sum) - $signed(p0_sum);
end
end
方向固定为:
phase - reference
不要一会儿写:
P1-P0
一会儿又写:
P0-P1
30. RTL:EMA核心
以一个通道为例。
localparam integer EMA_K = 6;
localparam integer DIFF_W = 20;
localparam integer EMA_W = DIFF_W + EMA_K + 1;
logic signed [EMA_W-1:0] ema_acc;
logic signed [EMA_W-1:0] diff_ext;
always_comb begin
diff_ext =
{{(EMA_W-DIFF_W){diff1[DIFF_W-1]}}, diff1};
end
always_ff @(posedge clk) begin
if (rst) begin
ema_acc <= '0;
end
else if (bg_enable &&
stat_vld &&
!cal_freeze) begin
ema_acc <= ema_acc
+ diff_ext
- (ema_acc >>> EMA_K);
end
end
对应Offset:
wire signed [EMA_W-1:0] offset_ext;
assign offset_ext =
ema_acc >>> (EMA_K + 3);
另外两个相位完全一样。
实际工程建议把三个EMA封装成一个参数化模块。
31. 为什么一定要用>>>?
有符号数右移一定要明确使用:
>>>
即:
arithmetic right shift
算术右移
不要不加思考地写:
>>
因为Offset和EMA内部都会出现负数。
例如:
-64 >>> 6 = -1
这才符合有符号定点计算。
所有参与运算的数据也建议明确声明:
logic signed
不要完全依赖Verilog自动推断符号类型。
32. 校正以后不要直接截低16bit
这一点也很重要。
假设输入:
sample = 32760
Offset:
offset = -20
校正:
32760 - (-20)
=
32780
已经超过16bit signed最大值:
32767
如果直接:
corrected[15:0]
会发生:
回绕,而不是饱和。
波形会直接跳到负数附近。
所以正确做法是:
先扩位减法
↓
判断上下溢
↓
饱和到16bit
33. 一个简单的16bit饱和函数
例如:
function automatic signed [15:0] sat16;
input signed [17:0] din;
begin
if (din > 18'sd32767)
sat16 = 16'sh7FFF;
else if (din < -18'sd32768)
sat16 = 16'sh8000;
else
sat16 = din[15:0];
end
endfunction
内部校正减法建议使用18bit。
34. RTL:最终Sample校正关系
伪代码如下:
for (int i = 0; i < 16; i++) begin
// adc0
if ((i % 2) == 0) begin
// P0,参考相位
adc0_corr_sample[i] = adc0_sample[i];
end
else begin
// P2
adc0_corr_sample[i]
= sat16(adc0_sample[i] - offset2);
end
// adc1
if ((i % 2) == 0) begin
// P1
adc1_corr_sample[i]
= sat16(adc1_sample[i] - offset1);
end
else begin
// P3
adc1_corr_sample[i]
= sat16(adc1_sample[i] - offset3);
end
end
重新pack以后:
adc0_corr_data[255:0]
adc1_corr_data[255:0]
接口和原输入保持完全一致。
Offset模块不负责10GS/s交织。
35. 整个设计大概需要多少资源?
Estimator部分:
SUM8
4组 × 7个adder
=
28个加法器
Difference
3个20bit减法器
EMA
3个约27~37bit accumulator
+
3组移位
如果K固定:
移位基本等于接线
Correction
非参考相位共:
3 × 8
=
24个sample
所以需要大约:
24个16~18bit校正减法器
整个设计可以:
DSP = 0
BRAM = 0
主要消耗:
LUT
Carry Chain
Flip-Flop
实际上EMA估计器本身非常小。
真正占资源的主要是:
24路并行Offset减法
+
宽总线pipeline寄存器
所以前面把"Estimator和Correction Datapath拆开"很重要。
36. 如果一定要求矩形1024点窗口怎么办?
那也没必要保存1024个原始16bit sample。
仍然先计算:
D1
D2
D3
每个D已经代表8个采样点。
只保存:
最近128个D
维护:
Sk+1=Sk+Dk−Dk−128Sk+1 = Sk + Dk - Dk-128
然后:
offset = S >>> 10;
这种方案需要的历史数据约:
3 × 128 × 20bit
=
7680bit
仍然很小。
所以如果系统需求明确写着:
必须使用矩形窗口。
那我会选择这种实现,而不是保存4×1024个原始采样点。
但对于Offset后台跟踪,我仍然优先选择EMA。
37. 怎么验证这个模块?
这种算法最容易出现的问题通常不是:
代码编译不过
而是:
代码能跑
但方向、延迟、定点或者统计方法错了
所以验证最好分三层。
37.1 第一层:纯算法验证
自己构造四个逻辑相位:
P0 offset = 0
P1 offset = +12
P2 offset = -20
P3 offset = +7
再加同一个真实信号。
例如:
signal + phase_offset + noise
检查EMA收敛以后:
offset1 ≈ +12
offset2 ≈ -20
offset3 ≈ +7
同时四路校正后均值应该接近一致。
37.2 第二层:专门验证公共DC不会被消掉
例如人为加入:
signal_dc = +500
然后:
P0 = signal + 0
P1 = signal + 12
P2 = signal - 20
P3 = signal + 7
正确结果应该是:
四路校正后均值 ≈ +500
而不是:
≈ 0
这个测试可以直接证明:
当前算法是在消Offset mismatch,不是在做DC blocker。
37.3 第三层:验证Offset突变
运行一段时间以后:
P2 offset:
-20 → +30
观察EMA。
应该看到:
offset2
以指数形式逐渐从:
-20
收敛到:
+30
而不是突然跳过去。
38. Freeze测试也要单独做
例如EMA已经收敛:
offset1 = 12
然后:
cal_freeze = 1;
此时即使输入统计量发生变化:
offset1
也必须完全保持。
但是:
adc_corr_vld
adc0_corr_data
adc1_corr_data
仍然继续正常输出。
这是两个完全不同的概念。
39. MATLAB和RTL对比时最容易踩的坑
不要直接拿:
MATLAB 1024点moving mean
和:
EMA RTL
逐点做:
RTL == MATLAB
然后发现不一样就认为RTL错了。
因为两种滤波器本来就不是同一个系统。
正确方法有两个。
方法一
MATLAB也实现完全相同的EMA:
A = A + D - floor_shift(A, K);
offset = A >> (K+3);
然后进行bit-accurate比较。
方法二
如果MATLAB仍然使用1024点滑窗,就比较:
最终Offset估计值
收敛趋势
稳态方差
校正后的spur
不要比较每一个采样点完全相等。
40. FPGA定点模型必须和RTL使用同样的舍入方法
RTL里的:
>>>
本质是定点截断。
尤其对负数:
算术右移的行为
必须在MATLAB/Python模型里模拟一致。
否则可能出现:
算法趋势完全对
但始终差1LSB
然后查RTL查半天。
这种问题在定点DSP设计里非常常见。
41. 上板以后重点看什么?
我会重点抓四类数据。
1. 三个Offset系数
offset1
offset2
offset3
通过ILA或者寄存器读回。
看:
是否收敛
是否乱跳
温度变化时是否缓慢变化
2. 校正前四个逻辑相位的均值
mean(P0)
mean(P1)
mean(P2)
mean(P3)
3. 校正后的均值
理想情况下:
mean(P0)
≈
mean(P1_corr)
≈
mean(P2_corr)
≈
mean(P3_corr)
4. 最终10GS/s数据频谱
真正评价Offset校准有没有价值,最后还是要看:
交织相关的固定spur
是否下降。
不要只看:
offset寄存器看起来挺稳定
就认为算法有效。
42. 为什么简单EMA不是万能的?
本文这个方案的优点是:
简单
资源少
容易调试
容易高速流水
代价是:
它依赖"不同逻辑相位看到的真实输入统计均值长期一致"这个假设。
如果输入本身在交织相关频率存在强确定性成分:
P0/P1/P2/P3
的统计均值差未必全部来自ADC Offset。
此时简单EMA可能出现误估。
所以工程上最好配合:
Foreground训练
+
Background freeze机制
使用。
不要把一个简单均值估计器想成可以在任何输入条件下无条件工作的万能后台校准算法。
43. Foreground阶段最好在什么条件下做?
如果系统允许,最好选择:
无输入信号
前端静默
50Ω终端
已知训练状态
之一。
这时:
不同逻辑相位之间的均值差
更能直接反映ADC自身的Offset mismatch。
先运行128个有效beat:
1024 sample/phase
得到一个可靠初值。
然后正常工作时再切换到慢速EMA跟踪。
这个结构比"上电以后EMA从0开始一直跑"更加稳定。
44. 一个实际工程上更合理的参数配置
第一版RTL我会这样定:
DATA_WIDTH = 16
ADC_NUM = 2
SAMPLE_PER_CLK = 16
LOGICAL_PHASE = 4
PHASE_SAMPLE = 8
FG_BLOCK_NUM = 128
EMA_K = 6
并提供:
cal_enable
cal_start
cal_freeze
cal_done
以及调试寄存器:
offset1_readback
offset2_readback
offset3_readback
fg_cnt_readback
cal_state_readback
后面上板调试会方便很多。
45. 最终建议的模块划分
不要把所有逻辑塞进一个上千行模块。
可以拆成:
adc_offset_correct_top
│
├── phase_sum
│
│ ├── P0 SUM8
│ ├── P1 SUM8
│ ├── P2 SUM8
│ └── P3 SUM8
│
├── offset_estimator
│
│ ├── diff
│ ├── foreground accumulator
│ ├── background EMA
│ └── freeze/control
│
└── offset_corrector
│
├── P0 bypass
├── P1 correction
├── P2 correction
├── P3 correction
└── saturation + pack
这种结构有几个好处:
算法和数据通路分开
仿真方便
ILA定位方便
后面修改EMA不影响接口
后面修改饱和方式不影响估计器
对于刚开始做FPGA工程的人,这比"少写几个module"更重要。
46. 最后把整个算法浓缩成几行
每个有效时钟:
P0 = sum(adc0 even 8 samples)
P1 = sum(adc1 even 8 samples)
P2 = sum(adc0 odd 8 samples)
P3 = sum(adc1 odd 8 samples)
计算:
D1 = P1 - P0
D2 = P2 - P0
D3 = P3 - P0
Foreground:
累加128个有效beat
Offset1 = ACC1 >> 10
Offset2 = ACC2 >> 10
Offset3 = ACC3 >> 10
Background:
A1 = A1 + D1 - (A1 >>> K)
A2 = A2 + D2 - (A2 >>> K)
A3 = A3 + D3 - (A3 >>> K)
Offset1 = A1 >>> (K+3)
Offset2 = A2 >>> (K+3)
Offset3 = A3 >>> (K+3)
数据校正:
adc0 even:
不处理
adc0 odd:
sample - Offset2
adc1 even:
sample - Offset1
adc1 odd:
sample - Offset3
最后:
18bit扩位减法
→ 饱和
→ 16bit
→ 重新拼回两路256bit
47. 总结
这个工程真正值得记住的并不是"EMA公式怎么写",而是下面几个FPGA实现思路。
第一:
Offset mismatch校准应该优先估计相对Offset,而不是把每一路自己的DC全部减掉。
第二:
高速ADC虽然采样率很高,但FPGA看到的是并行数据。应该先利用并行结构做block统计,而不是按单sample思维设计。
第三:
EMA和1024点滑动平均不是完全等价,只是在本文这种慢参数估计问题中,可以用非常低的资源获得接近的平滑效果。
第四:
EMA一定要考虑定点精度。直接对整数Offset做
error >>> K容易形成死区,使用缩放累加器更合适。
第五:
估计器和数据校正主通路应该解耦。没必要为了等待当前拍的EMA结果,把512bit数据总线延迟五六拍。
第六:
两路ADC最终还要交织,所以只校正各自内部even/odd是不完整的;4个逻辑相位至少需要1个参考和3个相对Offset系数。
第七:
高速RTL里,算法正确只是第一步。signed、位宽、valid、最后一个累加样本、算术右移、溢出饱和和pipeline,任何一个没处理好,都可能让板上结果完全不对。
最终这套架构的Estimator部分只需要:
4组SUM8加法树
3个diff
3个EMA accumulator
少量状态机和移位逻辑
可以做到:
BRAM = 0
DSP = 0
同时保留:
Foreground快速初始化
Background慢速跟踪
Freeze
Offset系数读回
对于312.5MHz、两路256bit输入的ADC Offset校准,这比直接照搬MATLAB的1024点逐样点滑动mean更适合作为第一版FPGA工程实现。
参考资料
-
AMD Zynq UltraScale+ RFSoC RF Data Converter Product Guide,PG269
-
RF-ADC Calibration Mechanism
-
Time Interleaved Offset Calibration Block(OCB)
-
Foreground Calibration Process
-
Background Calibration Process