Verilog实现4位流水呼吸灯
(野火ALTERA FPGA 教程作业训练题)
在完成呼吸灯的基础上,尝试流水呼吸灯。
基本思路
首先流水灯部分代码基本上可以继续用,基本思路还是两个并行的部分,PWM呼吸灯的效果使用led变量赋值,一直运行;而流水部分,通过一个流水灯代码不停地移位,选择亮的那位,将led的值利用拼接符拼接即可。
关于流水灯部分的思路,是受了自己一年前用spartan6学习FPGA时的一个按键的教程:FPGA实战篇——【2】按键控制LED灯闪烁
先上代码再解释:
Verilog代码
module breath_led_flash
#(
parameter CNT_2US_MAX = 6'd99 ,
parameter CNT_2MS_MAX = 10'd999 ,
parameter CNT_2S_MAX = 10'd999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output reg [3:0] led_out
);
reg [6:0] cnt_2us;//0-99需要7位
reg [9:0] cnt_2ms;//0-999需要10位
reg [9:0] cnt_2s ;//0-999需要10位
reg cnt_en ;//cnt_1s_en = 0 时实现呼吸灯从灭到亮的过程
reg led ;//作为一位呼吸灯运行
reg [1:0] led_ctrl;//控制哪个led灯亮
reg cnt_en_1 ;//取en的下降沿来作为触发信号
wire negde ;//下降沿信号
//2us计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2us <= 7'b0;
else if(cnt_2us == CNT_2US_MAX)
cnt_2us <= 7'b0;
else
cnt_2us <= cnt_2us + 1'b1;
end
//2ms计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2ms <= 10'b0;
else if(cnt_2us == CNT_2US_MAX && cnt_2ms == CNT_2MS_MAX)
cnt_2ms <= 10'b0;
else if(cnt_2us == CNT_2US_MAX)
cnt_2ms <= cnt_2ms + 1'b1;
end
//2s计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2s <= 10'b0;
else if(cnt_2us == CNT_2US_MAX && cnt_2ms == CNT_2MS_MAX && cnt_2s == CNT_2S_MAX)
cnt_2s <= 10'b0;
else if(cnt_2us == CNT_2US_MAX && cnt_2ms == CNT_2MS_MAX)
cnt_2s <= cnt_2s + 1'b1;
end
//cnt_en:2s 计数器标志信号
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
begin
cnt_en <= 1'b0;
end
else if(cnt_2us == CNT_2US_MAX && cnt_2ms == CNT_2MS_MAX && cnt_2s == CNT_2S_MAX)
begin
cnt_en <= ~cnt_en;
end
end
//cnt_en_1 延一拍不能写在上一个2s的always块中,否则延迟的就不是一个sysclk而是2s
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_en_1 <= 1'b0;
else
cnt_en_1 <= cnt_en;
end
//led:负责呼吸灯控制
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led <= 1'b0;
else if((cnt_en && cnt_2ms < cnt_2s) || (!cnt_en && cnt_2ms > cnt_2s))
led <= 1'b0;
else
led <= 1'b1;
end
//取en的下降沿
assign negde = ~cnt_en && cnt_en_1;
//led_ctrl:作为led信号位的控制
always@(posedge negde or negedge sys_rst_n) begin
//如果使用sysclk的上升沿,计数满清零是不符合时序的,只会保持一个sysclk周期就由11变成00,
if(!sys_rst_n)
led_ctrl <= 2'd0;
else if(led_ctrl == 2'd3)
led_ctrl <= 2'd0;
else
led_ctrl <= led_ctrl + 1'b1;
end
//led_out:输出信号连接到外部的 led_out 灯
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_out <= 4'b1111;//全灭
else
case(led_ctrl)
2'b00: led_out <= {led , {3{1'b1}} };
2'b01: led_out <= {1'b1 , led , {2{1'b1}} };
2'b10: led_out <= {{2{1'b1}} , led , 1'b1 };
2'b11: led_out <= {{3{1'b1}} , led };
default:led_out <= 4'b1111; //全灭
endcase
//不能这样拼接 {led, 3’b111 };而且{led, 3{1'b1}};也不对,要给3{1'b1}也加{}
end
endmodule
代码思路解析
1.呼吸灯
设置reg led ;//作为一位呼吸灯运行
,至此呼吸灯部分代码可以看做封装好了,只需将其赋值给led_out对应的位即可。
2.流水灯的切换——拼接符{}的思路
受本文最开始提到自己的那篇博客的启发,对于呼吸灯位置的切换,使用这种拼接的方法进行
//led_out:输出信号连接到外部的 led_out 灯
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_out <= 4'b1111;//全灭
else
case(led_ctrl)
2'b00: led_out <= {led , {3{1'b1}} };
2'b01: led_out <= {1'b1 , led , {2{1'b1}} };
2'b10: led_out <= {{2{1'b1}} , led , 1'b1 };
2'b11: led_out <= {{3{1'b1}} , led };
default:led_out <= 4'b1111; //全灭
endcase
end
其实思路就像是一个简单的状态机,通过led_ctrl这个state标志位的不断变换,使用case语句进行状态的切换。
debug
拼接符号的使用出现了问题:
//不能这样拼接 {led, 3’b111 };而且{led, 3{1'b1}};也不对,要给3{1'b1}也加{}
详情见:
Verilog 位拼接运算符{}语法要点总结
3.流水灯的切换——led_ctrl的条件
在编写代码的时候,一个难点就是led_ctrl到底什么时候切换增加以及清零。
因为我们的PWM周期是2s*2 = 4s,也就是说,流水的时候一个led要亮4s再切换下一个led,但是我们只有一个2s的计数器,怎么实现呢,我们考虑到2s的标志位,cnt_en,它的一个周期是4s,那要怎么利用这个标志位呢?
cnt_en是一个电平信号,想要用他的高电平或者低电平是不可以的,那么我们就想到之前博客里写的,取下降沿的方式,只要将下降沿信号取出,下降沿信号negde==1的时候让led_ctrl切换下一个状态即可。
取下降沿思路:FPGA触摸按键控制LED——边沿检测——同步时钟的意识
//取en的下降沿 assign negde = ~cnt_en && cnt_en_1;
debug
这里怎么用negde呢?
如果是按下面这样写:(错误示例)
//led_ctrl:作为led信号位的控制
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_ctrl <= 2'd0;
else if(led_ctrl == 2'd3)
led_ctrl <= 2'd0;
else if(negde)
led_ctrl <= led_ctrl + 1'b1;
end
这样利用,当led_ctrl == 2’d3时,很快在下一个sysclk就变成0了,根本看不到最后一个led亮,如下图:
那怎么办呢,我们可以直接把触发条件更改,不用sys_clk的上升沿,而是直接使用negde的上升沿,他的上升沿就代表了4s已经结束了,led_ctrl需要变化了。所以如下:(正确代码)
//led_ctrl:作为led信号位的控制
always@(posedge negde or negedge sys_rst_n) begin
//如果使用sysclk的上升沿,计数满清零是不符合时序的,只会保持一个sysclk周期就由11变成00,
if(!sys_rst_n)
led_ctrl <= 2'd0;
else if(led_ctrl == 2'd3)
led_ctrl <= 2'd0;
else
led_ctrl <= led_ctrl + 1'b1;
end
Testbench
`timescale 1ns/1ns
module tb_breath_led_flash();
wire [3:0] led_out ;
reg sys_clk ;
reg sys_rst_n ;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
breath_led_flash
#(
.CNT_2US_MAX (7'd9 ),
.CNT_2MS_MAX (10'd9 ),
.CNT_2S_MAX (10'd9 )
)
breath_led_flash_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.led_out (led_out ) //output
);
endmodule
仿真图
通过下图中led_out的波形确实能看出来,1111中间夹杂的0111波形的时间确实在变化,这就是呼吸灯的效果,一段时间后又变为1011,以此类推