在7系列FPGA上用ISERDES2原语实现12:1串行数据解串

需求

最近在调索尼的IMX183传感器,使用的是LVDS格式传输,一共6个LVDS通道,每个通道串行输出12比特的像素信息。

我一看,这不直接级联两个6:1的ISERDES2就行了吗?然而在查看官方文档以后,却发现ISERDES2级联模式下,可以支持10、14比特模式,但却不支持12比特模式。

其实本来不支持也没啥大问题,我可以用4:1或者6:1的模式,配合移位寄存器,同样能够完成传感器数据接收。具体可见文章:在Lattice上实现IMX178传感器12比特接收

用4:1解串比+移位寄存器滑窗完成12比特传感器接收

可能有人会问为啥这么执着要ISERDES2工作在12:1模式下,你不是都说用4:1或者6:1也能完成任务吗?

主要还是因为12:1解串后,时钟速度大幅降低为源时钟的1/6,对时序有好处。另外后端接收、解码逻辑相比于4:1和6:1都能简化(省去了滑窗寄存器和计数器)

当然还有一点:FPGA在我手上,怎么用这玩意就由不得厂商了,走歪门邪道也得强行支持12:1😡。

Google一下相关问题,发现早在10年前就有人提出,并且还真给这个限制绕过去了。所以本文不是原创方法,只是将前人的方法重新讲述一遍。

原文章:12bit SERDES with ISERDESE2 (7series) possible?

思路

整体思路其实很简单,如下图所示:

12:1的DDR模式 = 两个6:1的SDR模式,一个在上升沿时采样,另一个在下降沿时采样。

不过在实现中,还是有一些地方需要注意,从图里就能看出来:

  1. LVDS的差分信号在FPGA内部并没有转化成单端信号后同时分配给两个ISERDES,而是经过差分缓冲器,正/负信号各自进入一个ISERDES,其中接入了负信号的ISERDES在输出端需要加反相器才能得到正确结果。

  2. 驱动ISERDES的时钟由MMCM或者PLL生成。

这一通操作下来看的确实让人迷糊,为啥要整这么麻烦呢?其实这一切都是为了绕过vivado限制,原帖中也进行了相应的解释。

第一点:如果使用IBUFDS将差分信号转成单端,那么这个信号只能由一个ISERDES独享,不能接入另一个ISERDES,而使用上面的方法就没这个限制。

另外,反相器不能在ISERDES输入端插入(解串前),因为此时数据速率很高,插入反相器会引入skew(歪斜),采样时机就会出问题。解串后插入反相器也会造成歪斜,但此时时钟速度已经降低,造成的影响不大。

第二点:可能有些人会用DCK转单端后直接驱动ISERDES,同时用BUFR从DCK分频CLKDIV出来,但BUFR的问题在于其相位不可控,如果系统发生干扰或者复位,BUFR分频出来的时钟相位可能发生错位,造成不良影响。

使用MMCM/PLL生成ISERDES采样时钟是官方推荐方法,这样生成出来的时钟更加稳定可控。将DCK作为参考时钟,生成一个与DCK同频的采样时钟CLK,低频数据时钟CLKDIV(频率:DCK = CLK = 6 * CLKDIV,相位:三者同相,不然PLL就不叫PLL了)

理论存在,实践开始

实践

根据上面的框图,可以写出如下代码:

`timescale 1ns / 1ps

module iserdes_12_to_1(
   input i_clk_highspeed,
   input i_clk_div,
   input i_rst,

   input i_din_p,
   input i_din_n,

   input i_bitslip,

   output [11:0] o_dout
);
//....完整源码在文末直接复制即可,放这里太影响观感

这个就是单个通道的12:1 LVDS解串代码,7系列FPGA可以直接拿去用。多个通道直接例化多个即可。

时钟:在外面用MMCM/PLL的IP生成,其中i_clk_highspeed是LVDS采样信号,i_clk_div是解串后的数据时钟信号。参考时钟是传感器发来的DCK

剩下的信号应该不用过多介绍了,bitslip功能也是能够正常工作的。

需要注意的一点,在原帖中也提到了:在上板使用时,DYNCLKDIVSEL和DYNCLKSEL这两个选项需要打开,并做对应调整。而仿真时需要关掉才能得到正确结果。

DYNCLKDIVSEL和DYNCLKSEL是控制时钟极性的,可能有眼尖的观众已经发现了,为什么在ISERDESE2_even中,我已经给输入时钟用“~”取反了,但又打开时钟极性翻转,这样一来不就等于没有翻转吗?

欸嘿,其实我也不懂我为啥要这样干,但只有这样子调才能得到正确的数据。只能说,这种Hack有时候确实就是比较玄学,It just works。

总之就是如果你得到的结果不正确,记得多调调这些选项。

测试

除了这个12:1 ISERDES模块,我还编写了一个上电自动校准字对其的模块,将这两个模块搭配使用,就可以保证与传感器的通信在上电时自动校准,并且运行过程中不会失去锁定。

下面是测试结果:

中间三个数据包是传感器6条LVDS通道发来的行结束码,并且在任何时候采集数据包的字节都是正确对齐的。长时间运行(指24小时)也没有出现问题。

并且时序完全符合要求(已进行时序约束)

可选改进

本文相对于原教程其实省略了IDELAY这个组件,因为传感器速率不高,并且PCB设计时严格进行了等长,从物理上保证所有通道相对于时钟的偏斜都在要求范围内。

如果有需要,可以在IBUFDS_DIFF_OUT缓冲与ISERDES模块间插入IDELAY原语即可。

源码

`timescale 1ns / 1ps

module iserdes_12_to_1(
   input i_clk_highspeed,
   input i_clk_div,
   input i_rst,

   input i_din_p,
   input i_din_n,

   input i_bitslip,

   output [11:0] o_dout
);

    wire data_in_buffered_p;
    wire data_in_buffered_n;

   IBUFDS_DIFF_OUT #(
      .DIFF_TERM("FALSE"),   // Differential Termination, "TRUE"/"FALSE" 
      .IBUF_LOW_PWR("FALSE"), // Low power="TRUE", Highest performance="FALSE" 
      .IOSTANDARD("LVDS_25") // Specify the input I/O standard
   ) IBUFDS_DIFF_OUT_inst (
      .O(data_in_buffered_p),   // Buffer diff_p output
      .OB(data_in_buffered_n), // Buffer diff_n output
      .I(i_din_p),   // Diff_p buffer input (connect directly to top-level port)
      .IB(i_din_n)  // Diff_n buffer input (connect directly to top-level port)
   );

   wire [5:0] data_output_odd;
   wire [5:0] data_output_even;
   assign o_dout = {data_output_odd[5], ~data_output_even[5], data_output_odd[4], ~data_output_even[4], data_output_odd[3], ~data_output_even[3], 
                    data_output_odd[2], ~data_output_even[2], data_output_odd[1], ~data_output_even[1], data_output_odd[0], ~data_output_even[0]};

   ISERDESE2 #(
      .DATA_RATE("SDR"),           // DDR, SDR
      .DATA_WIDTH(6),              // Parallel data width (2-8,10,14)
      .DYN_CLKDIV_INV_EN("TRUE"), // Enable DYNCLKDIVINVSEL inversion (FALSE, TRUE)
      .DYN_CLK_INV_EN("TRUE"),    // Enable DYNCLKINVSEL inversion (FALSE, TRUE)
      // INIT_Q1 - INIT_Q4: Initial value on the Q outputs (0/1)
      .INIT_Q1(1'b0),
      .INIT_Q2(1'b0),
      .INIT_Q3(1'b0),
      .INIT_Q4(1'b0),
      .INTERFACE_TYPE("NETWORKING"),   // MEMORY, MEMORY_DDR3, MEMORY_QDR, NETWORKING, OVERSAMPLE
      .IOBDELAY("NONE"),           // NONE, BOTH, IBUF, IFD
      .NUM_CE(1),                  // Number of clock enables (1,2)
      .OFB_USED("FALSE"),          // Select OFB path (FALSE, TRUE)
      .SERDES_MODE("MASTER"),      // MASTER, SLAVE
      // SRVAL_Q1 - SRVAL_Q4: Q output values when SR is used (0/1)
      .SRVAL_Q1(1'b0),
      .SRVAL_Q2(1'b0),
      .SRVAL_Q3(1'b0),
      .SRVAL_Q4(1'b0)
   )
   ISERDESE2_odd (
      .O(),                       // 1-bit output: Combinatorial output
      // Q1 - Q8: 1-bit (each) output: Registered data outputs
      .Q1(data_output_odd[0]),
      .Q2(data_output_odd[1]),
      .Q3(data_output_odd[2]),
      .Q4(data_output_odd[3]),
      .Q5(data_output_odd[4]),
      .Q6(data_output_odd[5]),
      .Q7(),
      .Q8(),
      // SHIFTOUT1, SHIFTOUT2: 1-bit (each) output: Data width expansion output ports
      .SHIFTOUT1(),
      .SHIFTOUT2(),
      .BITSLIP(i_bitslip),           // 1-bit input: The BITSLIP pin performs a Bitslip operation synchronous to
                                   // CLKDIV when asserted (active High). Subsequently, the data seen on the Q1
                                   // to Q8 output ports will shift, as in a barrel-shifter operation, one
                                   // position every time Bitslip is invoked (DDR operation is different from
                                   // SDR).

      // CE1, CE2: 1-bit (each) input: Data register clock enable inputs
      .CE1(1'b1),
      .CE2(1'b1),
      .CLKDIVP(1'b0),           // 1-bit input: TBD
      // Clocks: 1-bit (each) input: ISERDESE2 clock input ports
      .CLK(i_clk_highspeed),                   // 1-bit input: High-speed clock
      .CLKB(~i_clk_highspeed),                 // 1-bit input: High-speed secondary clock
      .CLKDIV(i_clk_div),             // 1-bit input: Divided clock
      .OCLK(1'b0),                 // 1-bit input: High speed output clock used when INTERFACE_TYPE="MEMORY" 
      // Dynamic Clock Inversions: 1-bit (each) input: Dynamic clock inversion pins to switch clock polarity
      .DYNCLKDIVSEL(1'b0), // 1-bit input: Dynamic CLKDIV inversion
      .DYNCLKSEL(1'b0),       // 1-bit input: Dynamic CLK/CLKB inversion
      // Input Data: 1-bit (each) input: ISERDESE2 data input ports
      .D(data_in_buffered_p),                       // 1-bit input: Data input
      .DDLY(1'b0),                 // 1-bit input: Serial data from IDELAYE2
      .OFB(1'b0),                   // 1-bit input: Data feedback from OSERDESE2
      .OCLKB(1'b0),               // 1-bit input: High speed negative edge output clock
      .RST(i_rst),                   // 1-bit input: Active high asynchronous reset
      // SHIFTIN1, SHIFTIN2: 1-bit (each) input: Data width expansion input ports
      .SHIFTIN1(1'b0),
      .SHIFTIN2(1'b0)
);

   ISERDESE2 #(
      .DATA_RATE("SDR"),           // DDR, SDR
      .DATA_WIDTH(6),              // Parallel data width (2-8,10,14)
      .DYN_CLKDIV_INV_EN("TRUE"), // Enable DYNCLKDIVINVSEL inversion (FALSE, TRUE)
      .DYN_CLK_INV_EN("TRUE"),    // Enable DYNCLKINVSEL inversion (FALSE, TRUE)
      // INIT_Q1 - INIT_Q4: Initial value on the Q outputs (0/1)
      .INIT_Q1(1'b0),
      .INIT_Q2(1'b0),
      .INIT_Q3(1'b0),
      .INIT_Q4(1'b0),
      .INTERFACE_TYPE("NETWORKING"),   // MEMORY, MEMORY_DDR3, MEMORY_QDR, NETWORKING, OVERSAMPLE
      .IOBDELAY("NONE"),           // NONE, BOTH, IBUF, IFD
      .NUM_CE(1),                  // Number of clock enables (1,2)
      .OFB_USED("FALSE"),          // Select OFB path (FALSE, TRUE)
      .SERDES_MODE("MASTER"),      // MASTER, SLAVE
      // SRVAL_Q1 - SRVAL_Q4: Q output values when SR is used (0/1)
      .SRVAL_Q1(1'b0),
      .SRVAL_Q2(1'b0),
      .SRVAL_Q3(1'b0),
      .SRVAL_Q4(1'b0)
   )
   ISERDESE2_even (
      .O(),                       // 1-bit output: Combinatorial output
      // Q1 - Q8: 1-bit (each) output: Registered data outputs
      .Q1(data_output_even[0]),
      .Q2(data_output_even[1]),
      .Q3(data_output_even[2]),
      .Q4(data_output_even[3]),
      .Q5(data_output_even[4]),
      .Q6(data_output_even[5]),
      .Q7(),
      .Q8(),
      // SHIFTOUT1, SHIFTOUT2: 1-bit (each) output: Data width expansion output ports
      .SHIFTOUT1(),
      .SHIFTOUT2(),
      .BITSLIP(i_bitslip),           // 1-bit input: The BITSLIP pin performs a Bitslip operation synchronous to
                                   // CLKDIV when asserted (active High). Subsequently, the data seen on the Q1
                                   // to Q8 output ports will shift, as in a barrel-shifter operation, one
                                   // position every time Bitslip is invoked (DDR operation is different from
                                   // SDR).

      // CE1, CE2: 1-bit (each) input: Data register clock enable inputs
      .CE1(1'b1),
      .CE2(1'b1),
      .CLKDIVP(1'b0),           // 1-bit input: TBD
      // Clocks: 1-bit (each) input: ISERDESE2 clock input ports
      .CLK(~i_clk_highspeed),                   // 1-bit input: High-speed clock
      .CLKB(i_clk_highspeed),                 // 1-bit input: High-speed secondary clock
      .CLKDIV(i_clk_div),             // 1-bit input: Divided clock
      .OCLK(1'b0),                 // 1-bit input: High speed output clock used when INTERFACE_TYPE="MEMORY" 
      // Dynamic Clock Inversions: 1-bit (each) input: Dynamic clock inversion pins to switch clock polarity
      .DYNCLKDIVSEL(1'b0), // 1-bit input: Dynamic CLKDIV inversion
      .DYNCLKSEL(1'b1),       // 1-bit input: Dynamic CLK/CLKB inversion
      // Input Data: 1-bit (each) input: ISERDESE2 data input ports
      .D(data_in_buffered_n),                       // 1-bit input: Data input
      .DDLY(1'b0),                 // 1-bit input: Serial data from IDELAYE2
      .OFB(1'b0),                   // 1-bit input: Data feedback from OSERDESE2
      .OCLKB(1'b0),               // 1-bit input: High speed negative edge output clock
      .RST(i_rst),                   // 1-bit input: Active high asynchronous reset
      // SHIFTIN1, SHIFTIN2: 1-bit (each) input: Data width expansion input ports
      .SHIFTIN1(1'b0),
      .SHIFTIN2(1'b0)
);

endmodule

发表回复