一开始会想到写这样一个题目时,我正在青岛的海滩上玩耍。自从我上一次来到海边已经过了很久了。青岛那温暖而咸湿的海风,让我忍不住想起了曾经在南安普顿海岸沿线骑车飞驰的日子,那时也是这样一个九月多风的日子。远方山丘上的红瓦排屋,多么像我曾经在伊钦河口看到的那座梦想小屋。是的,我当时正在度假,忍不住回忆起了曾经上学时的美好时光,那为啥我就突然想到这个主题了呢?因为在南安普顿我曾经遇到过一位令我非常钦佩的老师 -- Iain McNally,而他曾经给我的代码指出过的第一个问题就是:
"不要在你的模块中生成任何时钟或者复位信号。"
I. 什么是本地时钟和复位
我打赌你们当中有很多人,就像我当年在学校的时候一样,并不明白究竟什么叫本地生成的时钟和复位信号。可能你们也正好奇为啥我的老师Iain会那样说我的代码。其实我当年在一份同步设计作业里上交的代码就有点像下面的例子这样。
[source lang="verilog"]
//==============================================================================
// Copyright (C) 2015 By Kellen.Wang
// mail@kellen.wang, All Rights Reserved
//==============================================================================
// Module : example
// Author : Kellen Wang
// Contact : mail@kellen.wang
// Date : Oct.02.2015
//==============================================================================
// Description :
//==============================================================================
module example (
input wire data_en ,
input wire data_in ,
output reg data_even ,
output reg data_odd
);
parameter DLY = 1'd1;
reg odd_en;
wire even_en = ~odd_en;
always @(posedge clk_in or negedge rst_n) begin
if (!rst_n) begin
odd_en <= #DLY 1'd0;
end
else if (!data_en) begin
odd_en <= #DLY 1'd0;
end
else begin
odd_en <= #DLY ~odd_en;
end
end
always @(posedge odd_en or negedge rst_n) begin
if (!rst_n) begin
data_odd <= #DLY 1'd0;
end
else begin
data_odd <= #DLY data_in;
end
end
always @(posedge even_en or negedge rst_n) begin
if (!rst_n) begin
data_even <= #DLY 1'd0;
end
else begin
data_even <= #DLY data_in;
end
end
endmodule
[/source]
在上面这份代码中,如果输入信号data_en变为高电平,则寄存器odd_en开始在时钟clk_in的上升沿进行翻转。 当odd_en变为高电平时,输入数据data_in被寄存到输出信号data_odd; 而当它变为低电平时,信号even_en变为高电平,同时输入数据data_en就被寄存到了输出信号data_even。
如果你给这个模块写一个测试单元,你会发现这个模块完全可以正常工作。但是,这是一个好的设计吗?这个设计有没有潜在的问题呢?这个真的可以算是“同步设计”吗?好好想想,接下来我将解释这些问题(哈哈哈,好奇怪的感觉,翻译自己写的博客居然还出现了翻译腔,好吧,希望大家原谅,因为我有直接用英文写作的习惯,如果觉得有奇怪的地方麻烦点击右上角切换英文模式)
首先,这是一个好设计吗?不是,尽管这个设计仿真看起来可以正常工作,但是实际变成电路以后就不一定了。第二个问题,有什么风险吗?是的。因为在这个设计中,使用了两个本地产生的时钟,具体而言,就是寄存器data_odd和data_even使用了本地产生的时钟odd_en和even_en。可能有人意识不到,当他们写下always@(posedge 某个东西 or negedge rst_n)的时候,实际上暗示了这里的“某个东西”是一个时钟。而在一个设计里,只要不是所有的模块都使用同一个信号作为时钟,那么这个设计就是一个多时钟设计(此处我的英文原文有不妥之处,实际上多时钟有可能是同步多时钟,也可能是异步多时钟,而同步多时钟的跨时钟问题与异步多时钟是不同的,当然同步多时钟也是有需要注意的问题的,与单时钟设计绝对不是一样的),因此跨时钟域的数据交互问题就是必须考虑的。
就拿上面的代码举例吧,输入信号data_in是同步于输入时钟clk_in的,而后这个data_in信号被两个不同的寄存器data_even和data_odd采样了,而这两个寄存器是被两个内部产生的时钟所驱动的。可是,data_in是与clk_in同步的,而不是与内部产生的两个时钟even_en和odd_en同步的,我们要如何才能保证data_in信号在even_en和odd_en信号上升沿发生的瞬间保持稳定(而不造成采样错误)呢?因此,data_odd就有可能出现亚稳态(既不是0也不是1,类似Z这样的高阻态),而这个状态还会顺着他输出到后级寄存器路径上。
有人可能觉得,odd_en和even_en都是从clk_in同步产生出来的,那他们的翻转边沿肯定自然是跟clk_in对齐的,所以他们实际上不就是“同步于”输入信号data_in的吗?这样问题就解决啦?诚然,我得承认(基于这个逻辑关系)让这些信号与输入时钟clk_in同步是有可能的,但是这意味着你要把这些额外的约束信息告诉综合电路的工具,让他对这两个内部产生的时钟进行同步约束(否则综合工具天然是无法意识到这个信号与clk_in之间有什么关系的),这不仅会带给我们很多额外工作量,同时也增加了时序约束失败的风险。
In VHDL or SOC system design projects, normally we divide modules not only by functions, but also by clock domains. By constraining each module with sole clock, clock-domain-crossing problems could be systematically solved in betweens of modules using different clocks, and all generated clocks in the whole system should be gathered in a dedicated module to ensure all timing constraints are met as expected.
Locally created resets are risky too.
II. How To Avoid Locally Created Clocks & Resets
III. The Correct Way To Create Clocks & Resets
在FPGA的设计中有种叫做异步复位同步释放的结构,这种复位信号是了内部产生的,不行吗?
我想你说的是,在FPGA内部对外部输入的复位信号进行处理,重新产生一个新的异步复位同步释放的新复位,然后再把这个复位信号拿去给每个模块使用对吧。这个复位信号是内部产生的没错。在复杂的SOC系统中,内部时钟有很多很多,所谓的“同步释放”必须要有一个参考的时钟,才能说“同步”,因此实际上,在一个复杂的系统中,这个复位信号在送给任何内部“模块”之前,都要重新跟这个“模块”的主时钟进行重新“异步复位同步释放”。这种复位的处理,一般都是集中在系统中的一个特定模块中统一完成,处理完的复位信号,才会送到真正使用该复位信号的模块去,送去之前就已经保证了“异步复位,与该模块时钟同步释放”。而本文的主要观点是:在模块的内部,不要生成“本地时钟和复位信号”。这里暗含的一个前提是,复位的同步处理应该被剥离到模块外部完成,在这个前提下,对于一个模块来说,输入给该模块的复位是已经保证与该模块的时钟“异步复位同步释放”了的,所以复位送进来之后,就不要再自己内部重新生成复位信号了。把整个系统的复位信号都集中到一个特定模块中完成同步处理有很多好处,一个好处是方便进行复位链的顺序控制和设计,另一方面是方便后端人员进行时序约束。
第一次来就迷上了这个网站!加油!
期待 II与III的更新