在做硕士毕设的过程中,写Verilog的时候遇到了一些问题,所以开一个记录贴记录一下。
1.组合逻辑always@(*)描述中,星号包含了这个程序块中的所有输入信号。举个例子:
always@(*)
if(a == 1)
b = c + 1;
这个代码中,因为对a要做判断,所以a也是输入信号,而b = c + 1,c自然也是b的输入信号,所以当a和c的信号变换时,这个程序块就会被执行。因此最好不要出现一下代码块:
always@(*)
if(b == 2)
b = b + 1
这种情况相当于在组合逻辑中,把输出又接回了输入,就会造成回环问题,可能会导致仿真仿不出来,而且对硬件设计也十分不友好。
2.当时钟频率太快的时候,写的组合逻辑如果不太好的话可能会导致仿真没问题,但上了板子有问题。举个例子:
always @(posedge video_clk) begin
case(current_state)
INIT:
cnt <= cnt + 1;
default: cnt <= 0;
endcase
end
always @(posedge video_clk) begin
case(current_state)
INIT: dia_in <= (1<<cnt) -1;
default: dia_in <= 0;
endcase
end
这个代码看上去没啥问题,当video_clk是33MHZ或者200MHZ的时候,仿真结果都是一样的。但是,当video_clk是33MHZ的时候,上板能够出现正确的结果;当video_clk是200MHZ的时候,上板就会发现dia_in的值不太正确。这就是因为(1<<cnt)-1这个语句所映射的硬件电路在时钟过快的时候可能组合逻辑做不完所出现的情况。所以后面我把这段代码改成了:
always @(posedge video_clk) begin
case(current_state)
INIT: dia_in[cnt] <= 1;
default: dia_in <= 0;
endcase
end
这样在任何时钟的情况下,上板后结果都是对的。
3.在复位的时候,不要把阻塞赋值和非阻塞赋值混用,不管是同步复位还是异步复位,这样综合以后的电路会十分奇怪。
首先同步复位写法1:
always @(posedge clk) begin
if(!rst_n)
cnt = 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
同步复位写法2:
always @(posedge clk) begin
if(!rst_n)
cnt <= 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
之前我还以为写法1可以比写法2快一点,但是跑了仿真才发现两者是一样快地,而且写法1综合的电路会十分大而奇怪,浪费资源。
接下来是异步复位写法1:
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt = 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
异步复位写法2:
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
可以看到和之前是一样的结果,但是在异步复位中,rst_n = 0与rst_n <= 0并没有区别。
4.阻塞赋值与非阻塞赋值
阻塞赋值就是立刻给,非阻塞赋值就是在满足触发条件的情况下直接给。
举个例子
always @(*) begin
case(current_state)
INIT: dia_in = 0;
TILE_R: dia_in = dob | ( {10{1'b1}} << (MIN_X - 1));
endcase
end
always @(posedge clk_b) begin
case(current_state)
INIT: addrb <= 0;
TILE_R:
if(!addrb)
addrb <= MIN_Y;
else if(addrb < MAX_Y)
addrb <= addrb + 1;
OUTPUT:
addrb <= active_y;
endcase
end
当状态机走到TILE_R状态的时候,dia_in能直接变成上述代码的对应值,但是另一个代码块中在TILE_R状态下addrb的变换要等过一个时钟,等到下一个时钟触发时,得到当前的状态为TILE_R,然后addrb才直接进行赋值。所以也就导致之前潜意识认为非阻塞赋值会比阻塞赋值慢一个时钟。
因为之前没想清楚,所以在异步赋值那块,我就对rst_n <= 0产生疑问,以为要等下一个时钟才会进行复位,其实不是的,复位信号触发的瞬间立刻复位。所以上面rst_n<=0和rst_n=0没有差别。
5.移位电路的综合结果
不同的移位电路的写法可能会导致不同,举个例子。
写法1:
always @(posedge clk) begin
if(!rst_n)
cnt <= 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
always @(*) begin
if(!rst_n)
dia_in = 0;
else
dia_in = (1<<cnt) - 1;
end
这种写法综合出来的电路是
而当我换一种写法的话:
always @(posedge clk) begin
if(!rst_n)
cnt <= 0;
else if(cnt == 7)
cnt <= 0;
else
cnt <= cnt + 1;
end
always @(*) begin
if(!rst_n)
dia_in = 0;
else if(cnt ==0) dia_in = (1 <<0)-1;
else if(cnt ==1) dia_in = (1 <<1)-1;
else if(cnt ==2) dia_in = (1 <<2)-1;
else if(cnt ==3) dia_in = (1 <<3)-1;
else if(cnt ==4) dia_in = (1 <<4)-1;
else if(cnt ==5) dia_in = (1 <<5)-1;
else if(cnt ==6) dia_in = (1 <<6)-1;
else dia_in = 0;
end
综合出来的结果是
6.一种状态机的写法可能出的BUG
之前状态机的写法如下:
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
always @(*) begin
case (current_state)
IDLE:
if (start) next_state = EVAL_START;
else next_state = IDLE;
EVAL_START: next_state = EVAL;
EVAL:
if(done_eval) next_state = JUDGE;
else next_state = EVAL;
JUDGE: next_state = OUTPUT;
OUTPUT: next_state = IDLE;
endcase
end
always @(*) begin
case (current_state)
EVAL_START : start_eval = 1;
default : start_eval = 0;
endcase
end
这种写法感觉也是比较常见的状态机写法,但是这种信号阻塞赋值的写法会导致上板以后,如果一旦出现毛刺或者建立时间和保持时间没做好,最终结果可能与仿真结果不一致。因此改了一种写法:
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
always @(*) begin
case (current_state)
IDLE:
if (start) next_state = EVAL_START;
else next_state = IDLE;
EVAL_START: next_state = EVAL;
EVAL:
if(done_eval) next_state = JUDGE;
else next_state = EVAL;
JUDGE: next_state = OUTPUT;
OUTPUT: next_state = IDLE;
endcase
end
always @(posedge clk ) begin
case (current_state)
EVAL_START : start_eval <= 1;
default : start_eval <= 0;
endcase
end
这种写法把信号用非阻塞赋值提供值,但是next_state的转换又是组合逻辑,综合来说时钟和信号用阻塞赋值进行传递的方式并未相差多少,之所以next_state的转换用非阻塞赋值就是因为过于浪费时钟了。但是这种方法进行仿真的发现有一个小小的BUG,如下图所示:
会发现next_state直接从0跳成2了,这里start信号我是从tb里给的,也就是在时钟的上升沿给一个start信号。之后我在这个模块之上添加一个顶层模块,让它去给start信号就发现没问题了,如下图:
想了想大概原因如下:
在tb中给start信号一般是这种形式,# xxx start = 1;这种情况下,由于可以看做是在这个上升沿前很小很小的一段时间,start信号拉高,由于next_state是组合逻辑,瞬间进入状态1,然后在上升沿时刻current_state进入状态1,之后因为在current_state为状态1的条件下,next_state = 2,所以瞬间又进入了状态2。因此如图1所示。
而在上个顶层模块给start信号的时候,一般是这种形式,xxx状态,start <= 1,这种情况下,是在时钟的上升沿触发后给一个start信号,所以next_state会在这个上升沿之后进入状态1,同时由于已经经过了上升沿,所以current_state只会在下个时钟的上升沿进入状态1,因此如图2所示。