LVDS接收数据流(这一篇就够了)

本文将以驱动AFE5832高速模拟前端为开发目标,依次分析设计需求、方案原理、在Zynq Ultrascale+上的Verilog实现,最后扩展分享更通用、更有鲁棒性的LVDS Receiver。注:本文针对具有Verilog语言基础和FPGA基础概念的用户。

接口设计需求简析

AFE5832是TI公司的一款适用于超声应用的模拟前端芯片,内含从LNA、LPF、到ADC的完整信号链和其他超声需要的辅助模块。采样方面,总共有32路输入,16个ADC,每个ADC支持12bit@40MSPS或者10bit@50MSPS采样,每个ADC的结果分别用一个LVDS的Lane串行输出。配置方面使用SPI接口,可以直接用AXI-SPI的IP核控制,不作介绍,所以接下来的篇幅将开始集中在LVDS方面。

NOTE:笔者使用ZYNQ的PS的SPI,发现有BUG,只能EMIO出信号,MIO一样的代码不行

AFE5832

AFE5832的LVDS数据输出接口如下图,有16对Data Lane(DOUT),和1对串行数据时钟Serial Data CLK Lane(DCLK),以及1对用来划分数据帧的Frame CLK Lane(FCLK)。

AFE832LVDS

为了更好地理解这18对Lane的用途,下图截取了Datasheet上的时序说明。注意,这里手册上说明时序是使用单端信号表征,并且名字不是很统一(DCLK这里又变成Bit Clock output了,但这好像更好理解了)。很明显,数据Lane是工作在DDR(Double-Data Rate)的模式,在DCLK的时钟上下边沿抓数据。具体下图中的各参数,本文为规避风险不放,因为TI现在完整的Datasheet是用户签NDA才有的,想要具体了解,可以私信笔者学术交流。

LVDSTiming

所以,在FPGA侧的LVDS接收器的目标概括为:

  • ADC工作转换率:$f_c = 80$MSPS
  • ADC转换位数:$N_{ser}=12$
  • 帧时钟频率:$f_{Fclk}=0.5\times f_c = 40$MHz
  • 数据Lane的波特率:$f_D = N_{ser} \times f_c = 960$MHz
  • 时钟Lane频率:$f_{Dclk}=f_D/2=480$MHz
  • 16路LVDS receiver + 1:12 deserializer
  • 并帧识别划分后分双通道输出数据(如下图所示,每个ADC轮番转换两个通道,这也是FCLK帧信号的主要用途)

FCLK帧时钟用途

FPGA的LVDS接收器方案

LVDS接收器框图

FPGA关键原语

这里笔者使用的是AMD的 Ultrascale+ 系列FPGA,如果用Intel或国产的也是差不多的,读者应当注意对应移植细节。
这里做概念介绍方便理解代码,具体例化的接口定义请参考《UG571 UltraScale Architecture SelectIO Resources》

  • IBUFDS:差分输入信号转单端数字信号
    IBUFDS

  • IDELAYE3:任何输入信号都可以用IDELAYE3进行延迟,除了时钟信号(必须用PLL或MMCM)。里面的Tap Delay Line支持512级延迟调节,但是每一级的延迟时间是没校准的,需要使用 IDELAYCTR 组件来额外补偿控制。
    IDELAYE3

  • ISERDESE3:把串行输入(D口)的数据按一组并行输出。在SDR模式下支持1:2和1:4串转并,在DDR模式下支持1:4和1:8。

    ISERDESE3
    如下图,是ISERDES的DATA_WIDTH为8时,DDR模式的时序。可见使用ISERDES可以在电路初级就完成时钟的降频(降低4倍),这对应可以带来大量的功耗优化、资源节省和布线时序优化。
    ISERDES时序

关键模块Verilog实现

在给出具体代码前,我们先按照上面给出的框架,将接收器分为数个子模块:

  • 顶层封装 LVDS_recv
    • 时钟生成 clkgen
    • 帧信号采样器 sipo12
    • 数据串并转换器 sipo12
      • 位偏移器 bitSlip
      • ISERDES等FPGA资源模块
    • 位偏移对齐控制器 bitslip_ctrl
    • 帧识别及写入控制器 frame_ctrl
    • 多组输入单输出FIFO缓存器(MISO) FIFO_MISO
    • 切片组合器 gearbox

接下来,开始按照依赖关系,给出各个子模块的代码。最后给出顶层模块,和行为仿真的结果。这里上板的测试不方便给出,因为具体完整测试涉及到AFE5832的细节涉及保密条款。

时钟生成器

时钟恢复绝对是一切的基础,如果收不到时钟,那就是查硬件问题(顺序:查给AFE的$f_{adc}$正确输出 → 查AFE的时钟接收电路是否有正确的100Ω阻抗端接 → 查是否正确给了AFE初始化指令来让内部PLL正常工作 → 查供电 → 查FPGA的LVDS接收端是否有正确的100Ω阻抗端接)。

这里代码中没有什么复杂的操作,就是像框图里面由MMCM/PLL跟踪数据时钟。有一个细节点就是锁相环的反馈要来自过时钟驱动器(bufg)的信号,这样可以去除驱动器的延迟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
module clkgen #(
parameter real CLKIN_PERIOD = 2 // Clock period (ns) of input clock on clkin_p)
)
(
input lvds_bclkin_p, // LVDS 源同步数据时钟 差分+
input lvds_bclkin_n, // 差分-

input rstn, // 异步复位

output bps_div2, // DDR数据采样时钟,0相移
output bps_div8, // 字节(切片)时钟
output PLL_locked // PLL 锁定成功
);

wire bclkin_p, bclkin_n;

// 输入缓冲器
IBUFGDS_DIFF_OUT # (
.DIFF_TERM ("TRUE")
)
iob_clk_in (
.I (lvds_bclkin_p),
.IB (lvds_bclkin_n),
.O (bclkin_p),
.OB (bclkin_n)
);

// 用来防止VCO频率不够
// F_VCO 范围是 800MHz~1600MHz
// 本设计跟踪的 dclk 范围 120M~480M
// NOTE: 将VCO的比例因子缩放到 VCO频率接近1200MHz
// localparam VCO_MULTIPLIER = (CLKIN_PERIOD >11.666) ? 2 : 1 ;
localparam integer VCO_MULTIPLIER = (CLKIN_PERIOD /(1/1.200));
/****
** FB通道和输入时钟同频,也就是 f_fb = f_bps/2
****/
MMCME3_BASE # (
.CLKIN1_PERIOD (CLKIN_PERIOD),
.BANDWIDTH ("OPTIMIZED"),

.CLKFBOUT_MULT_F (VCO_MULTIPLIER),
.CLKFBOUT_PHASE (180.0),

.DIVCLK_DIVIDE (1),
.REF_JITTER1 (0.100)
)
rx_mmcm_adv_inst (
.CLKFBOUT (bps_d2_pll),
.CLKFBOUTB (),
.CLKOUT0 (),
.CLKOUT0B (),
.CLKOUT1 (),
.CLKOUT1B (),
.CLKOUT2 (),
.CLKOUT2B (),
.CLKOUT3 (),
.CLKOUT3B (),
.CLKOUT4 (),
.CLKOUT5 (),
.CLKOUT6 (),
.LOCKED (PLL_locked),
.CLKFBIN (bps_div2),
.CLKIN1 (bclkin_p),
.PWRDWN (1'b0),
.RST (~rstn)
);

// 时钟打出去
BUFG bg_2 (.I(bps_d2_pll), .O(bps_div2)) ;

// * 供给iserdes的bclk_div时钟要和bclk时钟同源
BUFGCE_DIV #(
.BUFGCE_DIVIDE(4)
) bg_8(
.I(bps_d2_pll),
.O(bps_div8)
);

endmodule

帧/数据采样器

和框图里面的设计一致,就是用原语来实现最基础的串并转换。这样比纯逻辑门做有更好的时序收敛性和更好的功耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
module sipo12(
input lvds_din_p, // LVDS 数据线 差分+
input lvds_din_n, // 差分-

input rstn, // 异步复位

input bps_div2, // DDR时钟
input bps_div8, // 字节时钟

input [2:0] Nslip, // 位滑动量

output [7:0] pdout // 字节输出
);


wire [7:0] des_out_curr;

// 差分转单端
IBUFDS # (
.DIFF_TERM ("TRUE")
)
iob_clk_in (
.I (lvds_din_p),
.IB (lvds_din_n),
.O (datain_i)
);

// DDR 串并转换 采样器
// 1:8
ISERDESE3 #(
.DATA_WIDTH (8), // DDR, 8位
.SIM_DEVICE ("ULTRASCALE_PLUS"), // FPGA器件
.FIFO_ENABLE ("FALSE"), // 不用FIFO
.FIFO_SYNC_MODE ("FALSE") )
iserdes_m (
.D (datain_i), // 串行数据线输入
.RST (~rstn), // 复位

.CLK ( bps_div2), // DDR采样时钟
.CLK_B (~bps_div2), // TODO : 这个或门要优化成BUF

.CLKDIV ( bps_div8), // 字节时钟
.Q (des_out_curr), // 字节数据

.FIFO_RD_CLK (1'b0),
.FIFO_RD_EN (1'b0),
.FIFO_EMPTY (),
.INTERNAL_DIVCLK ()
);

// 位滑动器
bitSlip slip_inst(
.Din(des_out_curr),
.clk(bps_div8),
.Nslip(Nslip),
.rstn(rstn),
.Dout(pdout)
);

endmodule

位偏移器

上面sipo12模块中,例化了一个位偏移器。这个模块的用途是,因为ISERDES被复位后开始抓数据的时间点,有非常大概率不是一个字节的第一个位,所以要对抓到的数据进行滑动匹配对齐。这里笔者使用的方案是比较笨但是好理解的,读者也可以升级为使用帧信号触发ISERDES复位逐渐逼近对齐的方案。偏移器的具体代码很简单,就是拼接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
module bitSlip(
input [7:0] Din,
input clk,
input [2:0] Nslip,
input rstn,
output [7:0] Dout
);

// *输入的Din默认在外部为REG驱动,如果不是这里要加一级
// *二级缓存REG
reg [7:0] pre_din;
// *输出REG
reg [7:0] concat_dout;
assign Dout = concat_dout;

always @(posedge clk or negedge rstn) begin
if(!rstn) begin
pre_din <= 0;
concat_dout <= 0;
end
else begin
//* 缓冲REG,就是直接拿
pre_din <= Din;

//* 根据位移量,拼接数据
// note: 下面的拼法,代表发射机先发低位再发高位
case(Nslip)
1: concat_dout <= {Din[0],pre_din[7:1]};
2: concat_dout <= {Din[1:0],pre_din[7:2]};
3: concat_dout <= {Din[2:0],pre_din[7:3]};
4: concat_dout <= {Din[3:0],pre_din[7:4]};
5: concat_dout <= {Din[4:0],pre_din[7:5]};
6: concat_dout <= {Din[5:0],pre_din[7:6]};
7: concat_dout <= {Din[6:0],pre_din[7]};
default : concat_dout <= pre_din;
endcase
end
end

endmodule

位偏移对齐控制器

识别位偏移的方法也很简单,就是把帧信号当作数据用sipo去抓,当正确对齐的情况下,抓出来的数据只可能是:1111_11111111_00000000_0000三种。基于这个思想,模块的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
module bitslip_ctrl(
input clk,
input rstn,

input [7:0] f8b_in, // 帧信号采样结果
output reg [2:0] Nslip, // 位滑动量
output reg [1:0] status, // 状态,见 BSLIP_[INIT/SLIPING/LOCK/FAIL]
output reg ERROR // 错误标志
);

// 状态机 4种工作状态
localparam BSLIP_INIT = 2'b00;
localparam BSLIP_SLIPING = 2'b01;
localparam BSLIP_LOCK = 2'b10;
localparam BSLIP_FAIL = 2'b11;

wire patternFit;
wire pattern_A,pattern_B,pattern_C;
assign pattern_A = f8b_in == 8'b1111_1111;
assign pattern_B = f8b_in == 8'b0000_1111;
assign pattern_C = f8b_in == 8'b0000_0000;
assign patternFit = pattern_A | pattern_B | pattern_C;

reg [1:0] pre_pattern; localparam PT_A = 0, PT_B=1, PT_C=2, PT_NO=3;
reg [1:0] fit_cnt;
reg [3:0] try_time; // 记录失败次数

// pre_pattern 观测器实现
always@(posedge clk or negedge rstn) begin
if(!rstn) begin
pre_pattern <= PT_NO;
end
else begin
if(pattern_A)
pre_pattern <= PT_A;
else if(pattern_B)
pre_pattern <= PT_B;
else if(pattern_C)
pre_pattern <= PT_C;
else
pre_pattern <= PT_NO;
end
end

// 滑动状态机
always@(posedge clk or negedge rstn) begin
if(!rstn) begin
try_time <= 0;
Nslip <=0;
status <= BSLIP_INIT;
fit_cnt <= 0;
end
else begin
case(status)

BSLIP_INIT : status <= BSLIP_SLIPING;

BSLIP_SLIPING : begin
// 只有匹配两次pattern且满足pattern的连续关系,才LOCK
if(patternFit) begin
if(fit_cnt == 2)
status <= BSLIP_LOCK;
else begin // fit_cnt = 0 or 1
case(pre_pattern)
PT_A: begin
if(pattern_B)
fit_cnt <= fit_cnt+1;
else begin
fit_cnt <=0;
end
end

PT_B: begin
if(pattern_C)
fit_cnt <= fit_cnt+1;
else begin
fit_cnt <=0;
end
end

PT_C: begin
if(pattern_A)
fit_cnt <= fit_cnt+1;
else begin
fit_cnt <=0;
end
end

default: begin
fit_cnt <= 0;
end
endcase
end
end
else if(Nslip == 7)
status <= BSLIP_FAIL;
else
Nslip <= Nslip+1;
// status <= BSLIP_INIT;
end

BSLIP_LOCK : begin
if(patternFit)
status <= status;
else
status <= BSLIP_FAIL;
end

// FAIL状态下,清空重新开始
default: begin
Nslip <=0;
status <= BSLIP_INIT;
fit_cnt <= 0;

try_time <= try_time +1;
end

endcase
end
end

// 错误位判断
// NOTE:清标志位只能通过子模块复位完成
always@(posedge clk or negedge rstn) begin
if(!rstn) begin
ERROR <=0;
end
else begin
if(try_time == 4'b1110)
ERROR <= 1;
else ERROR <= ERROR;
end
end
endmodule

切片组合器

我们使用的AFE采样精度是12位,也就是LVDS输出的一字有12位,但是我们用sipo抓的数据是8位,那么也就是我们需要对这些字节进行分切拼接。很明显,在帧信号的一个周期里面有2个采样数据(高电平一个、低电平一个),也就是24位,我们会抓到3个字节,其中,第一个字节和第二个字节的前半段拼起来是一个采样数据,第二个字节的后半段与第三个字节拼起来是第二个采样数据。因此,切片组合器实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
module gearbox(
input clk,
input rstn,

input [1:0] Nslice, // 切片编号
input [7:0] pdin, // 字节数据

output reg [11:0] pdout // 拼接结果
);

reg [7:0] store_last_des;

// 存储每一次的REG
always @(posedge clk or negedge rstn)
begin
if(!rstn) begin
store_last_des <= 0;
end
else begin
store_last_des <= pdin;
end
end

// 根据状态机拼接输出
always @(posedge clk or negedge rstn)
begin
if(! rstn)
begin
pdout <= 0;
end
else
begin
case(Nslice)
2'h1 : pdout <= pdout;
2'h2 : pdout <= {pdin[3:0],store_last_des[7:0]};
2'h3 : pdout <= {pdin[7:0],store_last_des[7:4]};
default : pdout <= 0;
endcase
end
end

endmodule

帧识别器及写入控制器

切片组合器的原理很简单,但前提是识别出切片的状态。前面说了帧信号是说明了采样数据的字划分的,并且我们也在位对齐环节对帧信号进行了采样,那么类似的采样的结果本质上就是切片编码。具体的,1111_1111就是第一个字节,1111_0000是第二个,0000_0000是第三个。但是要注意,3个字节切片,只有两个ADC采样结果,所以只有2/3的时钟点需要存数据(写FIFO),因此本控制同时根据切片位置给出FIFO使能信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module frame_ctrl(
input [7:0] f8b_in,
input init_seq_finish,

input recvEn, // 全局接收使能,用于控制FIFO写入

output reg [1:0] Nslice, // 切片号
output [1:0] FIFOen // FIFO写使能
);


reg [1:0] fifo_en_by_slice;

assign FIFOen = recvEn ? fifo_en_by_slice : 2'b00;

always @(*) begin
case(f8b_in)
8'hff : begin
Nslice <= 1;
fifo_en_by_slice <= 2'b00;
end
8'h0f : begin
Nslice <= 2;
fifo_en_by_slice <= 2'b10 & {2{init_seq_finish}};
end
8'h00 : begin
Nslice <= 3;
fifo_en_by_slice <= 2'b01 & {2{init_seq_finish}};
end
default : begin
Nslice <= 0;
fifo_en_by_slice <= 2'b00;
end
endcase
end

endmodule

MISO缓冲器

这个是为了方便读取数据到处理器的DDR设计的,因为使用的FPGA的PS与PL数据流交互是用的AXI,同时考虑到数据缓冲和多通道的需求,笔者设计了MISO这个缓冲器。具体思想是,首先例化了多个(通道数一致)跨时钟域的异步FIFO来缓冲数据,读写都是native端口,写入fifo的使能控制就是上面frame_ctrl来的;读fifo,则是设计了一个多通道轮询器来控制读使能,当用户连续读这个模块的端口时,实际在内部是轮流读各个通道数据FIFO。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
module FIFO_MISO#(
parameter N_CHANNEL = 16, // 需要缓冲数据的通道数
parameter WID_WORD = 12 // 数据宽度
)(
input rst,

// FIFO 写入接口
input wr_clk,
input [ (N_CHANNEL*WID_WORD -1 ) :0 ] wr_dataBus,
input [1:0] wr_enAB,
output wrst_busy,

// 读接口,与fifo native标准一致
input rd_clk,
output [ (WID_WORD-1) :0] rd_data,
input rd_en,
output [5:0] rd_cnt,
output rrst_busy,
output rd_empty
);

wire [( 2*N_CHANNEL -1 ):0] fifo_rd_en_bus;
wire [( 2*N_CHANNEL -1 ):0] fifo_full_bus;
wire [( 2*N_CHANNEL -1 ):0] fifo_empty_bus;
wire [5:0] fifo_cnt_bus [( 2*N_CHANNEL -1 ):0];
wire [( 2*N_CHANNEL -1 ):0] fifo_wrst_busy;
wire [( 2*N_CHANNEL -1 ):0] fifo_rrst_busy;

wire [ WID_WORD-1 :0 ] fifo_rdout_bus [ (2*N_CHANNEL-1) :0 ];

//* 例化FIFO,这里分AB是因为帧信号一周期有两个数据
generate
genvar gi;

for(gi = 0; gi < N_CHANNEL; gi = gi+1) begin:fifo_gen

fifo_generator_0 FIFO_channelA (
.rst(rst), // input wire rst
.wr_clk(wr_clk), // input wire wr_clk
.rd_clk(rd_clk), // input wire rd_clk
.din(wr_dataBus[gi*12 +: 12]), // input wire [11 : 0] din
.wr_en(wr_enAB[0]), // input wire wr_en
.rd_en(fifo_rd_en_bus[gi*2]), // input wire rd_en
.dout(fifo_rdout_bus[gi*2]), // output wire [11 : 0] dout
.full(fifo_full_bus[gi*2]), // output wire full
.empty(fifo_empty_bus[gi*2]), // output wire empty
.rd_data_count(fifo_cnt_bus[gi*2]), // output wire [5 : 0] rd_data_count
.wr_rst_busy(fifo_wrst_busy[gi*2]), // output wire wr_rst_busy
.rd_rst_busy(fifo_rrst_busy[gi*2]) // output wire rd_rst_busy
);

fifo_generator_0 FIFO_channelB (
.rst(rst), // input wire rst
.wr_clk(wr_clk), // input wire wr_clk
.rd_clk(rd_clk), // input wire rd_clk
.din(wr_dataBus[gi*12 +: 12]), // input wire [11 : 0] din
.wr_en(wr_enAB[1]), // input wire wr_en
.rd_en(fifo_rd_en_bus[gi*2+1]), // input wire rd_en
.dout(fifo_rdout_bus[gi*2+1]), // output wire [11 : 0] dout
.full(fifo_full_bus[gi*2+1]), // output wire full
.empty(fifo_empty_bus[gi*2+1]), // output wire empty
.rd_data_count(fifo_cnt_bus[gi*2+1]), // output wire [5 : 0] rd_data_count
.wr_rst_busy(fifo_wrst_busy[gi*2+1]), // output wire wr_rst_busy
.rd_rst_busy(fifo_rrst_busy[gi*2+1]) // output wire rd_rst_busy
);
end
endgenerate


assign rd_cnt = fifo_cnt_bus[0];
assign wrst_busy = (fifo_wrst_busy != {( 2*N_CHANNEL ){1'b0}} );
assign rrst_busy = (fifo_rrst_busy != {( 2*N_CHANNEL ){1'b0}} );
assign rd_empty = fifo_empty_bus[( 2*N_CHANNEL -1 )];

reg [ $clog2(N_CHANNEL*2)-1 : 0 ] reading_idx;
reg [( 2*N_CHANNEL -1 ):0] fifo_rd_en_ctrl;


generate
for(gi = 0; gi < (2*N_CHANNEL); gi = gi+1) begin: and_ren
assign fifo_rd_en_bus[gi] = rd_en & fifo_rd_en_ctrl[gi];
end
endgenerate
// assign fifo_rd_en_bus = {(2*N_CHANNEL){rd_en}} & fifo_rd_en_ctrl;
always @ (posedge rd_clk or posedge rst) begin
if(rst) begin
reading_idx <= 0;
fifo_rd_en_ctrl <= 1;
end
else begin
if(rd_en)
fifo_rd_en_ctrl <= {fifo_rd_en_ctrl[N_CHANNEL*2-2:0],fifo_rd_en_ctrl[N_CHANNEL*2-1]};
else
fifo_rd_en_ctrl <= fifo_rd_en_ctrl;

if(rd_en)
if(reading_idx == (2*N_CHANNEL-1))
reading_idx <= 0;
else
reading_idx <= reading_idx +1;
else
reading_idx <= reading_idx;
end
end


assign rd_data = fifo_rdout_bus[reading_idx];

endmodule

顶层封装及例化构建采样器

这里对各个子模块进行拼装,还有对外部输入的控制信号,做跨时钟域的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
module LVDS_recv #(
parameter real LVDS_DDR_CLK_MHZ = 120
) (
input lvds_fclk_p,
input lvds_fclk_n,
input lvds_dclk_p,
input lvds_dclk_n,
input [15:0] lvds_data_p,
input [15:0] lvds_data_n,

input recvEn, // 接收使能
input flush, // 清除FIFO中剩余数据
input rstn,

output reg ready,

(* X_INTERFACE_INFO = "xilinx.com:interface:fifo_read:1.0 GroupRD RD_EN" *)
input rd_en,
(* X_INTERFACE_INFO = "xilinx.com:interface:fifo_read:1.0 GroupRD EMPTY" *)
output empty,
(* X_INTERFACE_INFO = "xilinx.com:interface:fifo_read:1.0 GroupRD ALMOST_EMPTY" *)
output almost_empty,
(* X_INTERFACE_INFO = "xilinx.com:interface:fifo_read:1.0 GroupRD RD_DATA" *)
output [11:0]dout,

output [5:0] valid_cnt,
input rd_clk
);

wire bps_div2, bps_div8;
wire PLL_locked;
wire init_seq_finish;

//* 最重要的,不依赖任何子模块的:时钟恢复
clkgen #(
.CLKIN_PERIOD((1/LVDS_DDR_CLK_MHZ)*1000) // Clock period (ns) of input clock on clkin_p)
) clkgen_inst(
.lvds_bclkin_p(lvds_dclk_p),
.lvds_bclkin_n(lvds_dclk_n),
.rstn(rstn),

.bps_div2(bps_div2),
.bps_div8(bps_div8),
.PLL_locked(PLL_locked)
);

wire [2:0] Nslip;
wire [1:0] frame_slice_control_gearbox;
wire [1:0] fifo_write_en;

//* 帧采样模块
wire [7:0] frame_samp_signal;
sipo12 frame_samp_inst(
.lvds_din_p(lvds_fclk_p),
.lvds_din_n(lvds_fclk_n),

.rstn(rstn),

.bps_div2(bps_div2),
.bps_div8(bps_div8),

.Nslip(Nslip),

.pdout(frame_samp_signal)
);

//* 切片拼接测试-------------------------------------
// WARN!!!: 仅用于调试,使用时关闭!
wire [11:0] test_gearbox_out;
gearbox gb_test_inst(
.clk(bps_div8),
.rstn(rstn),

.Nslice(frame_slice_control_gearbox),
.pdin(frame_samp_signal),

.pdout(test_gearbox_out)
);
//* -------------------------------------------------

// 跨时钟域处理,从ui_clk 到 bps_div8
reg recv_en_buf, recv_en_buf_async; // 跨时钟域处理
always @(posedge bps_div8) begin
recv_en_buf_async <= recvEn;
recv_en_buf <= recv_en_buf_async;
end

// * 例化切片组合控制器 和 FIFO写切片控制器
frame_ctrl slice_concat_write_ctrl_inst(
.f8b_in(frame_samp_signal),
.Nslice(frame_slice_control_gearbox),

.recvEn(recv_en_buf),

.init_seq_finish(init_seq_finish),
.FIFOen(fifo_write_en)
);

//* 帧对齐(划位控制)模块
// tip: INIT(0), sliping(1), locked(2), error(3)
wire [1:0] slip_ctrl_status;
bitslip_ctrl slip_inst(
.clk(bps_div8),
.rstn(rstn),
.f8b_in(frame_samp_signal),
.Nslip(Nslip),
.status(slip_ctrl_status)
);

//* 例化data Lane采集通道
wire [7:0] dlane_des_curr[15:0];
wire [(12*16-1):0] word_get_from_lane_bus;
wire [11:0] word_get_from_lane[15:0];
generate
genvar gi;

for(gi = 0; gi < 16; gi = gi+1) begin: data_sipo_inst
assign word_get_from_lane_bus[gi*12 +: 12] = word_get_from_lane[gi];

sipo12 dsipo_inst(
.lvds_din_p(lvds_data_p[gi]),
.lvds_din_n(lvds_data_n[gi]),

.rstn(rstn),

.bps_div2(bps_div2),
.bps_div8(bps_div8),

.Nslip(Nslip),

.pdout(dlane_des_curr[gi])
);

gearbox gb_inst(
.clk(bps_div8),
.rstn(rstn),

.Nslice(frame_slice_control_gearbox),
.pdin(dlane_des_curr[gi]),

.pdout(word_get_from_lane[gi])
);
end

endgenerate

//* 多通道输入单通道输出FIFO模块例化
wire fifo_wrst_busy,fifo_rrst_busy,fifo_empty;

assign empty = fifo_empty;
// output almost_empty,
wire rst_fifo;

FIFO_MISO misoFIFO_inst(
.rst(rst_fifo),

.wr_clk(bps_div8),
.wr_dataBus(word_get_from_lane_bus),
.wr_enAB(fifo_write_en),
.wrst_busy(fifo_wrst_busy),

.rd_clk(rd_clk),
.rd_data(dout),
.rd_en(rd_en),
.rd_cnt(valid_cnt),
.rrst_busy(fifo_rrst_busy),
.rd_empty(fifo_empty)
);

//* 复位管理安全模块

lvds_rstCtrl safe_rst_inst(
.PLL_locked(PLL_locked),
.rst_in(~rstn),
.ui_clk(rd_clk),

.flush(flush),

.fifo_wrst_busy(fifo_wrst_busy),
.fifo_rrst_busy(fifo_rrst_busy),
.rst_o_FIFO(rst_fifo),
// .rst_o_SIPO,
// .rst_o_gearBox
.OK(init_seq_finish)
);

always @(posedge rd_clk) begin
if (~rstn) begin
ready <= 0;
end
else
ready <= init_seq_finish;
end

endmodule

仿真验证

先直接上仿真结果,再对着图给出仿真的思路和代码。

行为仿真结果

首先,我们仿真这个LVDS接收器,肯定是要先生成LVDS信号,以及随机的复位完成时间,来考察接收器锁定数据和帧切片的能力。下代码为数据生成器,笔者可以根据自己的应用大胆地更换数据生成器中的各个参数进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
module paternData_gen(
output fclk_p,
output fclk_n,

output dclk_p,
output dclk_n,

output [15:0] dLane_p,
output [15:0] dLane_n,

output [15:0] dLane,
output reg RST
);


reg BPS_CLK_960M;
reg BPS_CLK_DDR_480M;


/* 时钟生成 */
parameter real PERIOD = 1/0.96;

always begin
BPS_CLK_960M = 1'b0;
#(PERIOD/2) BPS_CLK_960M = 1'b1;
#(PERIOD/2);
end

always @ (posedge BPS_CLK_960M)
begin
if(RST)
BPS_CLK_DDR_480M <= 0;
else
BPS_CLK_DDR_480M <= ~BPS_CLK_DDR_480M;
end

OBUFDS #(
.IOSTANDARD("LVDS"), // 指定I/O标准为LVDS
.DIFF_TERM("TRUE") // 启用差分终端
) obufds_inst (
.I(BPS_CLK_DDR_480M), // 单端输入
.O(dclk_p), // 差分正端输出
.OB(dclk_n) // 差分负端输出
);


/* 生成测试数据 */
reg [3:0] ser_num;
reg [11:0] testData;
always@(negedge BPS_CLK_960M)
begin
if(RST)
testData <= 12'b0000_0000_0000;
else
begin
if(ser_num == 11)
testData <= testData + 12'b000_0001_0000;
else
testData <= testData;
end
end

/* 输出数据 */
reg [2:0] frame_status;
localparam [2:0] FRAME_READY = 0;
localparam [2:0] FRAME_CA = 1;
localparam [2:0] FRAME_CB = 2;

// 帧状态机
always @ (negedge BPS_CLK_960M)
begin
if(RST)
begin
frame_status <= FRAME_READY;
ser_num <= 0;
end
else
begin
if(ser_num == 11)
ser_num <= 0;
else
if( (frame_status == FRAME_CA) || (frame_status == FRAME_CB))
ser_num <= ser_num +1;
else
ser_num <= ser_num;


case(frame_status)
FRAME_READY : frame_status <= FRAME_CA;
FRAME_CA : begin
if(ser_num == 11)
frame_status <= FRAME_CB;
end
FRAME_CB : begin
if(ser_num == 11)
frame_status <= FRAME_CA;
end
default : frame_status <= FRAME_CA;
endcase
end
end

wire [11:0] testDataBus[15:0];

generate
genvar ci;
for(ci = 0; ci <16; ci = ci+1) begin: gen_output_channel
assign testDataBus[ci] = testData + ci;
assign dLane[ci] = testDataBus[ci][ser_num];

OBUFDS #(
.IOSTANDARD("LVDS"), // 指定I/O标准为LVDS
.DIFF_TERM("TRUE") // 启用差分终端
) dobufds_inst (
.I(dLane[ci]), // 单端输入
.O(dLane_p[ci]), // 差分正端输出
.OB(dLane_n[ci]) // 差分负端输出
);

end
endgenerate

wire frame_clk_s;
assign frame_clk_s = (frame_status == FRAME_CA);

OBUFDS #(
.IOSTANDARD("LVDS"), // 指定I/O标准为LVDS
.DIFF_TERM("TRUE") // 启用差分终端
) fobufds_inst (
.I(frame_clk_s), // 单端输入
.O(fclk_p), // 差分正端输出
.OB(fclk_n) // 差分负端输出
);

/* 初始化 */
initial begin
RST = 1;
# (PERIOD*5) RST = 0;
end

endmodule

生成器生成数据,被LVDS接收器收到后,我们还要对接收器进行读取操作,来验证数据是否正确。因此构建一个读取器,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

module fifo_reader(
output reg recvEn,
output reg flush,

input empty,
input almost_empty,
input [5:0] cnt,
input [11:0] rd_data,
output reg rd_en,
output reg rd_clk
);

/* 时钟生成 600MHz */
parameter real PERIOD = 1/0.600;

initial begin
recvEn = 0;
flush = 0;
#(PERIOD * 300) recvEn = 1;
#(PERIOD * 120) recvEn = 0;

#(PERIOD * 10) flush = 1;
#(PERIOD *1) flush = 0;

#(PERIOD *10) recvEn = 1;
end

always begin
rd_clk = 1'b0;
#(PERIOD/2) rd_clk = 1'b1;
#(PERIOD/2);
end

always@(posedge rd_clk) begin
if(!empty && cnt>2)
rd_en <= 1;
else
rd_en <= 0;
end

endmodule

最后,我们做一个testbench把上面几个测试的前后端拼接起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
`timescale 1ns / 1ps

module testBench_fifo_read(

);

wire fclk_p,fclk_n,dclk_p,dclk_n;
wire [15:0] dLane_p;
wire [15:0] dLane_n;

wire rst;

wire empty, almost_empty,fifo_ren,fifo_rclk;
wire [11:0] fifo_rd_data;
wire [5:0] fifo_valid_cnt;

paternData_gen data_generator(
.fclk_p(fclk_p),
.fclk_n(fclk_n),

.dclk_p(dclk_p),
.dclk_n(dclk_n),

.dLane_p(dLane_p),
.dLane_n(dLane_n),

.RST(rst)
);
//
LVDS_recv #(
.LVDS_DDR_CLK_MHZ(480)
) recv_inst(
.lvds_fclk_p(fclk_p),
.lvds_fclk_n(fclk_n),
.lvds_dclk_p(dclk_p),
.lvds_dclk_n(dclk_n),
.lvds_data_p(dLane_p),
.lvds_data_n(dLane_n),

.rstn(~rst),
.recvEn(recvEn),
.flush(flush),

.rd_en(fifo_ren),
.empty(empty),
.almost_empty(almost_empty),
.valid_cnt(fifo_valid_cnt),
.dout(fifo_rd_data),
.rd_clk(fifo_rclk)
);


fifo_reader reader_inst(
.recvEn(recvEn),
.flush(flush),

.empty(empty),
.almost_empty(almost_empty),
.cnt(fifo_valid_cnt),
.rd_data(fifo_rd_data),
.rd_en(fifo_ren),
.rd_clk(fifo_rclk)
);

endmodule

AMD官方推荐的LVDS接收器完整架构

见AMD的官方文档:《XAPP1315 LVDS Source Synchronous 7:1 Serialization and Deserialization Using Clock Multiplication》.

上面我们实现的接收器,直接基于DCLK恢复时钟采样,没有考虑skew的问题,是因为在板级设计时笔者严格缩短了AFE5832至FPGA之间的Layout距离,并且Lane对内做了差分等长,对间同样做了等长。在工程应用中,很可能是控不到这么好的条件,比如子模块板间跨过多个连接座和线缆或者Layout工程师无法压榨。这就要在每一对Lane进入FPGA后使用IDELAY调节skew,以及CLK进去后由PLL和校准算法匹配数据采样点。

下图是AMD给的DEMO推荐架构,加了IDELAYCTRLIDELAY。实际使用时,让LVDS前级出固定测试数据1010_1010...,然后FPGA工程师往延迟值小和延迟值大两个方向依次对每个通道测试延迟边缘值(也就是会看到采出来的数据开始一直跳),然后将中值配置为永久延迟设置值。

LVDS完整接收器架构

参考文档

  • XAPP1208 Bitslip in Logic
  • XAPP1315 LVDS Source Synchronous 7:1 Serialization and Deserialization Using Clock Multiplication
  • XAPP524 Serial LVDS High-Speed ADC Interface
  • XAPP1017 LVDS Source Synchronous DDR Deserialization (up to 1,600 Mb/s)
  • UG572 UltraScale Architecture Clocking Resources
  • UG571 UltraScale Architecture SelectIO Resources
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2024 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信