FPGA实现脉冲信号MVT量化器

前言:采集脉冲信号,这个在嵌入式中相当常见的功能,很容易想到的方法有:定时器直接采PWM的脉宽、滤波后用ADC采能量(详见前文)。但是对于非常快速的尖脉冲信号,比如辐射粒子信号检测(能量511keV的粒子产生的脉宽时间为500ps左右),如果采用ADC方案则需要采样率达10G以上,这样的ADC价格极其昂贵而且还买不到!所以对于这样的信号,需要采用一种全新的思路,也就是接下来要讨论的MVT(多电压阈值Multi-Voltage Threshold)方法。

MVT Digitizer系统结构

system Architect

上图是从一篇做PET的论文中抽取的,除去后面的以太网通信部分,这就是一套完善的由FPGA实现的MVT Digitizer系统。

这个系统的核心组件是:

  1. LVDS Comparator:基于LVDS接收器的定阈值比较器
  2. TDC:基于加法器和传播延时设计的“时间数字转换器”
  3. Pack Module:对检测的阈值触发时间进行处理的算法逻辑实现

接下来,对这个系统的核心硬件部分依次进行原理分析。

LVDS

脉冲信号进入FPGA后的第一大关就是LVDS比较器。为了较好地说明实现原理,这里引用TI公司的SN65LVDS4芯片技术手册相关内容。该芯片是单路的LVDS接收器,下图是官方建议的电路结构。

LVDS

这边的100欧电阻是用来做传输线阻抗匹配,因为LVDS是被设计做高速通信(ANS/EIA/EIA-64定义中的LVDS标准理论极限速率为1.923Gbps),具体相关知识可以参考之前一篇讲传输线的文章

可见LVDS其实是一种差分信号,接收器以两差分线间电压差值为传输数据,如下图所示为其工作特性。

$$
V_{ID} = V_{IA}-V_{IB}
\\
V_O = \begin{cases}
1, & V_{ID} > 0 \\
0, & V_{ID} < 0
\end{cases}
$$

LVDS-signal

TI真的很贴心,图中还特别把LVDS门输出与输入间的延时特性画出来了。

所以回顾一下文初的系统架构图,当我们固定LVDS接受门的反向输入端电平为想比较的电平,那么多路这样的结构就形成多阈值比较器(MVT)。对一次输入的脉冲,MVT可以获得下图:

pulse-LVDSample

显然同一个阈值在输入脉冲上升沿触发后,肯定也会在下降沿触发,这就有了该阈值对应的一段时间调制数字脉冲。只要把这个脉冲的时间长短进行量化,再做拟合算法就可以获得该脉冲的估计波形,以及其他分析。

TDC

对该时间调制宽度的数字脉冲进行离散量化的期间,叫做TDC(Time-to-Digital Convertor)。当然这个非常像是用定时器去采PWM的脉冲宽度,但是这个时间太短了(ps级别),定时器的时钟起码要比这还快,显然在21世纪上叶这个时候来看是不可能的。所以采用如下结构的TDC:

TDC

首先这个结构核心器件就是Latch锁存器和Adder加法单元。锁存器由时钟总线触发,并且显然这个时钟是FPGA的时钟(0.1GHz~1GHZ数量级),相比要采集的信号非常慢。观察到所有的加法单元的AB输入是1和0,也就代表着当他们的输入Ci从0变成1时,S由1跳至0,Co由0跳至1。

注意,每个加法单元是由内部的若干CMOS组成的逻辑组合电路,所以从输入到输出是有时间延时的。而这个延时就是该电路的工作原理。

在FPGA的系统时钟两次上升沿之间,如果有数字脉冲信号进来,则加法单元会依次在延迟时间后翻转输出和触发下一个单元。

所以这个原理做的TDC,在FPGA上实现的时候,就要手动进行电路综合布线设计,以确保内部的各个门单元传播延时是一致的。

下图是TDC的输入信号。

TDCinput

代码实现

基于延迟链的TDC

以下Verilog代码为针对Zynq-7系列FPGA(Xilinx)设计的TDC,主要调用了Carry-4进位链和寄存器原语言。

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
module tdc_line#(
parameter STAGE = 128 // 延迟单元数量
)(
(* dont_touch="true" *)input wire sg_start, // 触发信号
input wire clk_bufg, // 锁存时钟
input wire reset, // 复位

(* dont_touch="true" *)output wire [STAGE - 1:0] value_latch // 锁存数据
);

(* dont_touch="true" *)wire [STAGE - 1:0] dat_reg0; // CARRY4的输出
(* dont_touch="true" *)wire [STAGE - 1:0] dat_reg1; // D触发器的输出

// --- CARRY4和FDRE等用法参见官方文件UG953 ---

genvar i; // 作为generate中的循环变量
generate
for (i = 0; i <= STAGE/4 - 1; i = i+1) begin // 一个carry4中出4个信号
if(i == 0) begin :carry4_first // 第一个carry4,输入信号是CYINIT
CARRY4 CARRY4_INST ( // 用MUX作为延迟单元
.CO (dat_reg0[3:0]), // 4-bit carry out ,MUX的出口
.O (), // 4-bit carry chain XOR data out,不用XOR
.CI (1'b0), // 1-bit carry cascade input,不用上一carry结果
.CYINIT (sg_start), // 1-bit carry initialization,外部输入信号
.DI (4'b0000), // 4-bit carry-MUX data in,初始化一个固定数
.S (4'b1111) // 4-bit carry-MUX select input,MUX数据选上一MUX
);
end
if (i > 0) begin :carry4_others // 后面的carry4,输入信号都是CIN,从前一个COUT进
CARRY4 CARRY4_OTHERS (
.CO (dat_reg0[4*(i+1)-1:4*i]), // 4-bit carry out
.O (), // 4-bit carry chain XOR data out
.CI (dat_reg0[4*i-1]), // 1-bit carry cascade input,从上一carry取数据
.CYINIT (1'b0), // 1-bit carry initialization,不用
.DI (4'b0000), // 4-bit carry-MUX data in
.S (4'b1111) // 4-bit carry-MUX select input
);
end
end
endgenerate

genvar j;
generate
for (j = 0; j <= STAGE - 1; j = j+1) begin:loop_fdre
FDRE #( // D 触发器 有时钟使能 和 同步复位
.INIT (1'b0) // Initial value of register (1'b0 or 1'b1)
) FDRE_INST0 ( // 锁存当前数据
.Q (dat_reg1[j]), // 1-bit Data output
.C (clk_bufg), // 1-bit Clock input
.CE (1'b1), // 1-bit Clock enable input
.R (reset), // 1-bit Synchronous reset input
.D (dat_reg0[j]) // 1-bit Data input
);

FDRE #(
.INIT (1'b0) // Initial value of register (1'b0 or 1'b1)
) FDRE_INST1 ( // 锁存上一时钟数据,相当于一级缓存
.Q (value_latch[j]), // 1-bit Data output
.C (clk_bufg), // 1-bit Clock input
.CE (1'b1), // 1-bit Clock enable input
.R (reset), // 1-bit Synchronous reset input
.D (dat_reg1[j]) // 1-bit Data input
);
end
endgenerate

endmodule

温度码编码器

用延迟链测量法得到的数据是温度码(数据形式111...100000),直接传输非常浪费带宽和Layout资源,所以一般捕获温度码后要马上转二进制码。笔者在设计的过程中,发现该译码器用Verilog写的话没有像上面设计TDC这样的快速生成语句可用,因此设计了一套用Python生成直接译码的工具。正如下方源码中注释的,设置相应参数后,即可自动生成温度码转二进制码的编码器Verilog文件。

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
import numpy as np

'''
参数设置
'''

genVerilogFilePath = '../thermal2BinCode.v'

thermalCodeWidth = 124 # 温度码 位宽
binaryCodeBusName = 'binaryCodeOut' # case语句中将会生成的 二进制码总线名字
binaryCodeDefaultValue = 0

binaryCodeWidth = int(np.log2(thermalCodeWidth))+1

genFile_Head = 'module thermal2BinCode(\n\
input wire [' + str(thermalCodeWidth-1) +\
':0] thermalCodeIn, // 输入的温度码,数据总线\n\
input wire clk, // 时钟\n\
input wire reset, // 同步复位\n\
\n\
output reg ['+str(binaryCodeWidth-1)+\
' : 0] binaryCodeOut // 输出的二进制码\n\
);\n\
\n\
always@(posedge clk) begin\n\
if(reset == 1\'b1) begin // 复位\n\
binaryCodeOut = \'d0;\n\
end\n\
else begin\n\
case(thermalCodeIn)\n'

genFile_Tail = ' endcase\n\
end\n\
end\n\
\n\
endmodule'


'''
开始生成代码
'''
verilogFile = open(genVerilogFilePath,'w')
verilogFile.write(genFile_Head)

thermalCodeWidthHeadStr = str(thermalCodeWidth)+'\'b'
equalBody = ': '+ binaryCodeBusName + ' = \'d'

for idx in range(1,thermalCodeWidth+1):
verilogFile.write(' '+thermalCodeWidthHeadStr+ idx*'1'+(thermalCodeWidth-idx)*'0'+equalBody+str(idx)+';\n')

verilogFile.write(' default: '+binaryCodeBusName +' = \'d'+str(binaryCodeDefaultValue)+';\n')
verilogFile.write(genFile_Tail)
verilogFile.close()

FPGA的LVDS输入

现代FPGA都有适用于LVDS差分输入的端口缓冲器,比如XILINX的FPGA中支持LVDS的IO BANK会给相应的引脚号后面加极性P或N。这样的一对口子在输入FPGA后,内部是有对应配套的差分变换缓冲器的(如下图)。

LVDSBuffer

要告诉FPGA综合出这样的的一对口子,在XILINX的Vidado平台中,可以使用下源语:

1
2
3
4
5
6
7
8
9
IBUFDS #(
.DIFF_TERM("FALSE"), // 差分输出,此处不用
.IBUF_LOW_PWR("FALSE"), // Low power="TRUE", Highest performance="FALSE"
.IOSTANDARD("DEFAULT") // Specify the input I/O standard
) IBUFDS_inst (
.O(channel_trig[i]), // 输出
.I(mLVDS_P[i]), // 差分正输入 Diff_p buffer input (connect directly to top-level port)
.IB(mLVDS_N[i]) // 差分负输入 Diff_n buffer input (connect directly to top-level port)
);

同时在约束文件中,也要对代码中相应的两个正负差分输入引脚做约束,注意:必须在FPGA内部是属于同一对的引脚!

1
2
set_property -dict {PACKAGE_PIN T11  IOSTANDARD  LVDS_25} [get_ports mLVDS_P_0[0] ]
set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVDS_25} [get_ports mLVDS_N_0[0] ]

Tip:在Zynq中实现LVDS时,注意只有LVDSLVDS_18LVDS_25这3种类型。并且当使用2.5V版本的LVDS输入的时候,Bank电压可以是3.3V;而做输出的时候,Bank电压必须是2.5V!

参考文献

  • FPGA-Only MVT Digitizer for TOF PET
  • 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-2023 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信