思考题答案
System Verilog 中事件的发生顺序
这题我的想法是:希望让大家通过这道题把时序逻辑和 verilog 代码连起来进行理解。
实际上在实验的时候很多人在问两个独立的 always_ff
的执行顺序是什么?
答案是没有执行顺序,跟硬件对应到一起进行理解就很容易明白了。
我们先来分析一下给的代码。
代码里有两个 always_ff
子块,还有一个 always_comb
子块。
如果按照软件方式进行理解,这段代码可能有两种不同的执行结果。
一个是在 clk 上升沿到来时,先执行第一个修改 state_r
的 always_ff
。
这样的话,tmp_state_r
就会和 state_r
保持一样的值。
另一个是在上升沿到来时先执行第二个。这样的话,tmp_state_r
的值会比 state_r
晚一个周期。
按照软件中的并行对代码进行理解,上面两种顺序都有可能发生,导致 tmp_state_r
的值不确定。
现在在这里进行说明,这种理解是 完全错误 的。
经过仿真,得到的波形如下:
我们发现 tmp_state_r
的值永远比 state_r
晚一个周期。
那难道上面的第二种理解是对的,verilog 会根据代码顺序来执行 always
块吗?这种理解也是错误的。
可以在 vivado 中,将两个 always_ff
块交换顺序,你会发现仿真的结果没有变化。那我们应该如何理解 verilog 的行为呢?答案是结合硬件来进行解释。生成电路图如下:
图中寄存器的左侧为输入端,右侧为输出端。绿色的是数据选择器,为组合电路。蓝色的是寄存器(D 触发器)为时序电路。
我们考虑时钟上升沿到来之后,电路中各信号的变化情况。
首先发生的是寄存器的值变化。state_r
和 tmp_state_r
的输出分别变为自己输入端的值。
拿第一个时钟上升沿到来时的情况进行举例。在时钟上升沿到来之前,state_r
的值为 STATE_1
,tmp_state_r
的值为 STATE_0
。因此此时两个寄存器建立的信号值分别为 STATE_2
和 STATE_1
。在时钟上升沿到来之后,两个寄存器的值发生变化,变为自己的输入,即 state_r
变为 STATE_2
,tmp_state_r
变为 STATE_1
。
在经过短暂的电路延迟之后,4 选 1 选择器的选择信号端会发生变化,从之前的 STATE_1
变为 STATE_2
。由于组合逻辑的值随着输入的变化而变化,在经过一段时间的电路延迟后,4 选 1 选择器的输出变为 STATE_3
。
再经过一小段时间,4 选 1 选择器的输出传递到 state_r
寄存器的输入端,完成了输入值的建立。在下一个上升沿到来之后,state_r
会变成 STATE_3
。
因此,正确的电路事件顺序是:clk 上升沿到来 -> 寄存器值切换 -> 组合逻辑计算完毕 -> 寄存器输入准备完成 -> clk 上升沿到来
当然,tmp_state_r
的输入准备会在 4 选 1 选择器电路变化之前并行准备完毕,但我相信大家能够明白我想问什么。
现在,我们再将这个生成的电路对应回 verilog 中。
对于寄存器(即在 always_ff
中被赋值的信号)信号,当其作为左值被赋值时,引用的是寄存器的输入端。即对寄存器信号赋值是编写对应寄存器的输入逻辑。当引用其右值时,引用的是寄存器的输出端。即将寄存器的输出与组合逻辑连接,用于给其他信号赋值。
因此,在本题的代码之中,根据两个 always_ff
块会生成上面的电路,会有 tmp_state_r
比 state_r
晚一个周期的行为。
题外话,参考一下电路事件的顺序,我们可以尝试理解一下电路的时序要求。
在两次上升沿之间,电路的组合逻辑需要完成计算,在之后提供给寄存器输入,完成建立时间的等待。当时钟频率过高或者组合逻辑过于复杂时,电路就会发生时序违约:寄存器的输入端没准备好,时钟上升沿就来了。这样会导致电路的行为发生错误。因此,当组合逻辑过于复杂的时候,要么降低时钟的频率,要么简化组合逻辑(进行时序优化)。
手动 v.s. 自动
这题我的想法是:希望让大家通过这道题接触一下同步时序电路的设计。
在我们的实验当中,除了计数器的提高要求向设计中引入了时钟以外,其他的实验都是通过 CLK 按钮的上升沿进行驱动的,一般都是异步逻辑。
而在后续的实验当中,同步时序逻辑的设计往往更加常见。
本题的第一问实际上是一个很常用的场景。那么应该如何进行设计呢?
我们首先来对需求进行一下翻译:生成一个信号,当按钮信号从 0 变为 1 之后,保持高电平 1 个周期,其余时刻保持为低电平。
这样设计其实就很简单了。首先我们要解决如何检测 0 变为 1。当前周期的值实际上可以直接拿到。因此我们只需要保存一下上一个周期的信号,就可以完成 0 变为 1 事件的探测了。编写出来的代码如下:
// ...
input wire btn,
// ...
reg prev_btn;
always_ff @(posedge clk) begin
prev_btn <= btn;
end
// TODO: any type of always, depending on needs.
begin
if(prev_btn == 1'b0 && btn == 1'b1) begin
// we detected a rising edge
end
end
那如何让生成的信号只保持一个周期呢?只需要让寄存器的输出在上面的 if 里赋值为 1,else 的情况赋值为 0 就行了。那应该使用时序逻辑还是组合逻辑呢?实际上都可以,只不过信号会有 1 个周期的偏差。原因可以看一下上面那道思考题,这里只给出代码和波形,不再重复解释。
// ...
input wire btn,
// ...
reg prev_btn;
always_ff @(posedge clk) begin
prev_btn <= btn;
end
reg signal_r, signal_w;
always_ff @(posedge clk) begin
if(prev_btn == 1'b0 && btn == 1'b1) begin
signal_r <= 1;
end else begin
signal_r <= 0;
end
end
always_comb begin
if(prev_btn == 1'b0 && btn == 1'b1) begin
signal_w = 1;
end else begin
signal_w = 0;
end
end
实际上,还可以继续对本题进行扩展,完成按键的消抖,这里不再赘述。
对于第二问,我们只需要将判断 btn 的变化,改写为判断 mode 的变化的逻辑就可以了。根据功能需要,将其编写在对应类型的 always 块中即可,这里不多赘述。