接口/总线/驱动
注意,这里的DDR指的是Double Data Rate,双倍数据速率。这篇文章并不是讲DDR存储器系列的东西。
不同于SDR,也就是单上升沿或下降沿,传输数据。DDR说我不想选择是上升沿还是下降沿传输数据,小孩子才做选择,大人只会说我全都要。
通过一组图片就可以看到SDR和DDR的区别:
SDR
DDR
可以看到经过DDR处理的数据在数据时钟上升沿和下降沿都有数据更新,对于如何完整的取出数据,我仔细思考了许久,经历了否定之否定的过程,最终才找到了通用的解决方案。现在写出解决方案的心路历程:
刚开始觉得,既然它上升沿和下降沿都有,不如用一个always检测时钟跳变,有跳变就开始取值。代码示例如下:
always @ (fb_clk)
begin
if (fb_clk == 1'b1) //上升沿跳变
i_data <= tx_frame ? {tx_d,6'd0} : {i_data[11:6], tx_d};// tx_frame为高代表高6位, 低为低8位
else
q_data <= tx_frame ? {tx_d,6'd0} : {q_data[11:6], tx_d};
end
但是这么做肯定是有问题的,我们本来是要描述一个时序电路,最后always的敏感列表里面是一个信号,这么做就成了组合逻辑了,这么做不稳定不可取。
2. 第二种方法是使用锁相环输出一个与原数据时钟同频但相位延后180度的时钟fb_clk_180, fb_clk负责采样data_I, fb_clk_180负责data_Q。这种方法可以,但感觉麻烦,因为后面还要使用DDR输出信号,时钟转来转去有点麻烦。
3. 第三种方法还是使用锁相环,输出一个同相但频率为原来频率2倍的时钟信号fb_clk_mul2。fb_clk_mul2的每次上升沿,对应着原时钟fb_clk的上升沿和下降沿,使用fb_clk_mul2就可以分离data_I和data_Q。但这种方法也有局限性,不仅增加时钟数量,当原时钟速率过高,这种方法的稳定性也将有待商榷。
最后,我们在Vivado里面找到了一种原语,完美解决这个问题。这就是IDDR和ODDR。
对于输入信号,我们使用IDDR解出原始数据,在Language Template找到IDDR原语示例,例子如下:
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE", "SAME_EDGE"
// or "SAME_EDGE_PIPELINED"
.INIT_Q1(1'b0), // Initial value of Q1: 1'b0 or 1'b1
.INIT_Q2(1'b0), // Initial value of Q2: 1'b0 or 1'b1
.SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC"
) IDDR_inst (
.Q1(rx_data_pos[i]), // 1-bit output for positive edge of clock
.Q2(rx_data_neg[i]), // 1-bit output for negative edge of clock
.C(data_clk), // 1-bit clock input
.CE(1'b1), // 1-bit clock enable input
.D(rx_data_dly[i]), // 1-bit DDR data input
// .D(rx_data[i]), // 1-bit DDR data input
.R(1'b0), // 1-bit reset
.S(1'b0) // 1-bit set
);
设置好IDDR的4个常量参数之后,将数据时钟接入C端口,时钟使能CE端口拉高,待转数据信号接入D端口,Q1端口将会输出时钟上升沿采样的数据,Q2端口将会输出时钟下降沿采样的数据。注意设置好复位R和置位S端口。
ODDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE"
.INIT(1'b0), // Initial value of Q: 1'b0 or 1'b1
.SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC"
) ODDR_inst (
.Q(p0_data[i]), // 1-bit DDR output
.C(data_clk), // 1-bit clock input
.CE(1'b1), // 1-bit clock enable input
.D1(idata[i]), // 1-bit data input (positive edge)
.D2(qdata[i]), // 1-bit data input (negative edge)
.R(1'b0), // 1-bit reset
.S(1'b0) // 1-bit set
);
设置好之后就可以在rx_data_pos,rx_data_neg看到数据。这里我使用了generate for生成块,所以出现了genvar变量i;
同样,对于DDR输出信号,使用ODDR原语解决:
设置好ODDR的3个常量参数之后,将数据时钟接入C端口,时钟使能CE端口拉高,Q端口输出DDR处理后的数据,数据时钟上升沿更新的数据接入D1端口,数据时钟下降沿更新的数据接入D2端口。注意设置好复位R和置位S端口。
ODDR还可以巧妙地输出时钟,在D1输入1'b1, D2输入1'b0,其他不变,则在数据时钟上升沿输出高电平,下降沿输出低电平。巧妙地输出了数据时钟。
注意,ODDR输出的数据只能经过IOBUF或者输出,曾经有人想使用ILA抓取ODDR的Q端口输出的数据,无奈Implemention总会报错。
总结:
对于DDR信号,不能直接用always @ (data_clk)的方法采样信号,详细见上述(1)内容
上述(2)和(3)的方法在一定范围内都有其可行性,但也有一些弊端,详细见上述(2)和(3)内容
使用IDDR和ODDR最为妥当,IDDR和ODDR的数据端口都是1bit,多bit可以使用generate for生成块
可以使用ODDR在普通IO上输出数据时钟
ODDR输出的数据只能经过IOBUF或者输出
如果是LVDS信号,需要先转单端再进IDDR;或者ODDR后再转差分输出;差分信号的处理方法可以看上一篇文章。
信号处理好之后,如果出现了时钟与数据对不上该怎么办,这个时候可以使用Idelay调整时序。我们下一篇文章可以谈论一下Idelay。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !