逻辑简单的小程序模块时钟频率可达到上百兆甚至几百兆,而作为复杂系统的全局时钟通常只在几十兆,一般不超过100M。当然,对于一个小型CPLD程序,几兆或千赫兹时钟即能完成整个功能的情况就另当别论了。一般来说,把系统时钟设计在50M到100M这个范围是最佳选择。这个范围的时钟频率远远高于通常的低速信号,这在处理低速信号时有很大的自由。同时这个范围的时钟用16位并行总线即可以接管上吉速率的串行数据。这就是同步系统设计准则1,系统时钟选定在50M~100M范围。这是根据当前FPGA工艺水平和通常的逻辑功能确定的。接下来的问题是如何把不同接口同步到系统时钟上来。对于低于系统时钟一半以下的低速信号,建议采用的办法是把接口的时钟信号转化为系统时钟的使能信号,用使能信号来控制接口数据的读写节拍。而对于接近系统时钟的中速信号,先按照其时钟进行独立的接口功能处理,达到字(位宽随具体情况而定)同步后再转交系统时钟,同样以使能方式控制节拍。交给接口输出的数据,同样是在系统时钟下处理接近最后一步,再交由接口时钟跳变沿输出。对于高于系统时钟的高速信号,有时要使用FPGA专有的接口资源完成串并变换,甚至需要进一步扩大位宽来降速再转交系统时钟。由系统到接口的过程则相反。具体剖析这些方法之前,先强调几条同步设计准则,这些准则在一些FPGA新手指南中也会提到,本文结合个人的理解增加一些具体分析。
准则2,不依赖复位。这是从系统的健壮性角度提出的要求,意思是不能依赖按下一次复位键后系统才能正常工作。例如,对于一个4比特的伪随机序列prb(0 to 3)其生成多项式为1 + x + x^4,其电路为:在时钟进程里prb <= (prb(0) xor prb(3)) & prb(0 to 2);我们知道这个电路在prb不为全0时工作正常,序列周期长度为15。而当prb=0后,就停留在0这个状态了。依赖复位的做法是,按下复位键给prb赋一个非全0值。且不说复位键带来多余的操作,设想系统正在模15正常工作,因意外干扰是prb值变成0,那么干扰消失后系统确停留在死循环状态了。如果我们把电路写成prb <=( (prb(0) xor prb(3)) or prb=0) & prb(0 to 2);那么此时的电路就变成了自启动电路,不再存在上述死等顾虑。这种不依赖复位的自启动电路设计,可以从意外故障中自动恢复,在非全状态的状态机、移位计数器等电路中均应考虑。另外,这里以及下文说的复位,并不一定是指赋全0值,而是泛指装载初始值。
准则3,如非必须不使用异步复位。异步复位究竟会带来什么问题呢?例,一个计数器cn在rst为高则赋0值,否则在时钟上升沿cn <= cn + 9。这个电路在rst为0后,cn预期的计数结果是0,9,18,27,...按9的倍数增长直到溢出处理。假设cn是10位计数器cn[9..0],也即cn电路上是由十个触发器cn[9], cn[8], .. , cn[0]构成,复位信号rst分别引线到这10个触发器。那么这10条引线带来的延迟不可能绝对相等。设想cn[3]处延时较小,则存在cn[3]处观察到rst在时钟沿之前已由1跳变到0而在cn[0]处时钟沿之前仍为1在时钟沿之后才跳变到0的时序。此时,cn的计数结果是0, 8, 17 ,26, ...。反之,如果cn[0]出延时小,则有可能是0, 1, 10, 19, 28, ...。当然,若在电路设计时已考虑到这一极限情况,或者rst与时钟相位有确定的关系,使用异步复位也无妨。
准则4,异源信号不在多处逻辑使用。例,电路需要判断一个异源信号A由0变为1。也就是说,在前一个时钟上沿看到的A是0,当前一个时钟看到的是1。那么很容易想到的逻辑就是if rising_edge(clock) then S <= A; end if; S为前一个时钟读到的值,ifA=1 and S=0 then ...这一电路目的是实现A每次由0变1后做一次省略部分的后续处理。此电路在两处使用了异源信号A,分别是读取A和判断A两处。同样设想A在时钟上沿附近变化,读取A电路对A的延时较小,读取电路的当前周期S=0下一周期S=1,而判断电路对A的延时较大导致识别A同样是当前周期A=0,下一周期A=1。这样A=1 and S=0的条件电路就遗漏了一次A由0到1的跳变事件。反之,如果读取电路对A的延时较大而判断电路延时较小,又会把一次跳变事件判断为两次连续的跳变事件。这就导致了逻辑出错。正确的做法是,再延时一个周期处理if rising_edge(clock) then S0 <= A; S1 <= S0; endif; 然后判断电路相应修改为if S0=1 and S1=0 then ...。此时异源信号A只在一处使用。同准则3一样,举例为简单逻辑,对于复杂逻辑不同电路相对延时差增大,发生这种错误的可能进一步增大。还有一种隐性的异源信号在多处逻辑使用的情况,例如对信号A积分,当A为1时cn增计数,A为0时减计数。如果写成if A=1 then cn <= cn + 1; else cn <= cn-1; end if;,由于cn代表着多个触发器,就隐含着在多处逻辑使用了信号A。
准则5,慎用双时钟异步读写RAM。当出现跨时钟域数据交互的时候,有些FPGA程序设计者用IP核生成异步RAM,然后在不同的时钟域内处理,不同时钟的问题放心地交给IP核生成的逻辑就万事大吉了。当然,异步RAM本身没有问题。但是对RAM的读写寻址,判断RAM空满或已用RAM空间,同样要选定在同一个时钟下完成。读写地址是并行数据,每个比特跳变有先后,不作同步处理,直接在一个时钟下判断是危险的行为。这一点,我们在以后的具体事例中再作讨论。
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
准则6,尽量避免使用门控时钟。这就是上一帖的内容,不能随便拿来一个信号就用作时钟,特别是不要用组合逻辑产生时钟。通常只使用输入到FPGA时钟专用引脚的晶振信号和锁相环输出作为时钟,个别使用同一块电路板上,其他接口芯片送出的较高速时钟信号作为时钟。
有了这些准则,跨时钟域问题就变得简单了。
同源时钟之间数据交互
同源时钟,是指相位关系确定的时钟。比如同一个锁相环输出的不同频率的时钟。例,用50M晶振,产生一个62.5M的系统时钟sysClk和一个125M的接口时钟hClk,hClk用于和外部DDR交互数据。只需要把sysClk和hClk的相位都设成0,那么在sysClk下输出的数据可以直接在hClk下两个时钟读取一次,同样在hClk下两个时钟更新一次的数据可以直接在sysClk下读取。切忌画蛇添足把sysClk和hClk的相位设置成有半个或1/4时钟周期的延时错相。这种错相的行为等同于010帖提到的双沿交替使用时钟。
异源低速信号同步化
准则4的示例就是异源低速信号同步化的方法,可以把准则4中的A信号看作低速信号的时钟,然后把en <= S0=1 and S1=0;作为使能信号读写异源数据,而不是直接使用A作为时钟信号。A的一个周期包含了系统时钟的多个周期,多个周期意味着可以做更多的逻辑处理,因此这种同步处理也使得后续逻辑处理更加灵活。这个在系统时钟下的en,等效于A的上升沿。而有时我们需要用A的下降沿,那么就是en <= S0=0 and S1=1; 或者同时使用A的上升沿和下降沿en<= S0 xor S1; 这种同步化的行为在设计中经常遇到,我们希望更加突出地显示使用上升沿还是下降沿,专注于设计其他逻辑不用费神去思考S0和S1那个为0那个为1表示上升沿或者下降沿。就像使用rising_edge(clock)或者falling_edge(clock),不再使用clock’event and clock=’1’一样。这种突出显示提高了可读性是显而易见的。
上述判断上升沿的方式,本质上是对S0和S1的差分处理。所以定义两个函数,分别为正差分PosDiff(v) (Positive Difference)和负差分NegDiff(v)用来显式地表示上升沿或者下降沿。函数的定义非常简单:PosDiff(v):return not v(v’left) and v(v’right);这样,我们在做时钟处理的时候,可以不加思考地写成如下形式。
signal y_A :std_vector(0 to 1);
Process(clock) begin
if rising_edge(clock) then
ShiftLeft(y_A,A);
en <=PosDiff(y_A);
end if;
End process;
我们把早已把ShiftLeft,PosDiff和NegDiff写在自定义库里面了,en可以根据实际情况写成组合逻辑或时序逻辑。而对于双沿处理可以用PosDiff(y_A) or NegDiff(y_A),或者用异或逻辑写成y_A(0) xory_A(1),等同于使用函数b_xor(y_A)来实现。
中高速信号的同步化
对于中高速信号,以扩大位宽的方法即可降速为低速信号B。并产生一个类似时钟的信号A,A使用异源时钟在B值更新的时候翻转。然后再按照按照低速信号办法产生y_A,在b_xor(y_A)时刻读取B。从系统时钟到接口时钟过程类似,不作赘述。
有时信号B为异步通信信号,即发发停停,比如以太网数据。此时不一定要扩大位宽,采用异步RAM缓存的方法也可以解决问题。
小结
本文以简单实例分析了信号同步化的必要性和注意事项,强调了应尽量遵循的六条准则。希望此帖能给FPGA代码工程师一点启迪。
一周热门 更多>