跳转至

思考题答案

System Verilog 中事件的发生顺序

这题我的想法是:希望让大家通过这道题把时序逻辑和 verilog 代码连起来进行理解。

实际上在实验的时候很多人在问两个独立的 always_ff 的执行顺序是什么?

答案是没有执行顺序,跟硬件对应到一起进行理解就很容易明白了。

我们先来分析一下给的代码。

代码里有两个 always_ff 子块,还有一个 always_comb 子块。

如果按照软件方式进行理解,这段代码可能有两种不同的执行结果。

一个是在 clk 上升沿到来时,先执行第一个修改 state_ralways_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_rtmp_state_r 的输出分别变为自己输入端的值。

拿第一个时钟上升沿到来时的情况进行举例。在时钟上升沿到来之前,state_r 的值为 STATE_1tmp_state_r 的值为 STATE_0。因此此时两个寄存器建立的信号值分别为 STATE_2STATE_1。在时钟上升沿到来之后,两个寄存器的值发生变化,变为自己的输入,即 state_r 变为 STATE_2tmp_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_rstate_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 块中即可,这里不多赘述。


最后更新: 2023年6月1日
作者:cuibst (99.37%), Jiajie Chen (0.63%)