用Verilog编写“数显LED的亮度控制电路”

前言:

今天我将介绍的是Verilog的使用用法,然后如何用Verilog编写一个简单的小项目,项目的内容如标题所示——“数显LED的亮度控制电路”,本文章大致结构如下。

  • 介绍Verilog的基本语法
  • 介绍Verilog的基本底层电路
  • 总结归纳初学者可能犯得错误,并进行纠正

当我们对Verilog有了一定的了解后,就开始编写今天的项目了,思路如下:

  • 项目任务的解析
  • 设计思路
  • 编写代码
  • 成果展示

希望大家可以从易到难,慢慢的去理解、并熟练的运用这种语言。
那么任务开始了。

第一部分:Verilog硬件描述语言语法总结

一、摘录10条基本语法

1、module:模块的建立

module addr (a, b, cin, count, sum);  
endmodule 

每个模块以module开始,必须以endmodule结尾

2、input:数据的输入

input [2:0] a; 

使用input函数输入一个值,且位长为2-0+1=3

3、output:数据的输出

output [2:1] sum;

使用output函数输出一个值,且位长为2-1+1=2

4、assign:数据赋值

assign {count,sum} = a +b + cin;

注意:对于assign必须为wire型,并且{ }具有连接作用

5、assign:数据选择赋值

assign equare = (a == b) ? 1:0 ;

当满足(a==b)时,equare=1,反之为0

6、always:条件触发

always@(posedge clk or negedge key)begin 
end

如图为条件触发,当clk为高电平,且key为低电平时,满足条件进入always程序

7、wire ,reg:定义数据类型

  wire [3:0]q; 
  reg  [4:0]q2;

定义一个数据类型为wire且长度为4的q,和数据类型为reg长度为5的q2

8、if else:条件选择语句

  if (a>b)a=0;
  else a=1;

当满足括号内的条件时,则运行后面的程序,否则则运行else的程序。

9、case:多重条件选择语句

 case (key) begin 
 1:a=1;break;
 2:b=1;break;
 default :c=1;break;
 endcase

当key满足1,2,3的条件时,分别执行相应的语句,结尾记得用endcase结尾

10、task:与函数类似,用task进行包装

task q_sub();
begin 
q=1;
end
endtask

task的作用是,可以不必把所有的代码写到一个always中去,这样的代码看起来更加美观

二、摘录10条基本底层电路

1、数据选择器

module mux2(out,a,b,sel);
input a,b,sel;    
output out;    
reg out;
always @(sel or a or b) begin
    if (sel)  out = a;  else out = b;
end
endmodule;	

2、数据比较器

module compare(a,b,y);
input [2:0] a,b;  
output y;  
reg y; 
always @ (a or b)  begin
    if (a < b) y = 1;  
	else y = 0;
end  
endmodule

3、二进制计数器

module Bin_count(clr,clk,q);
input  clk,clr;
output [3:0]q;
reg    [3:0]q;
always @(negedge clk or posedge clr) begin
    if(clr) q=0;
    else q=q+1;
end
endmodule

4、100分频电路

module div100_clk(clk,q);
input  clk;    
output q;    
reg [5:0]cunt;    
reg q;
always @(posedge clk)  begin
    if(cunt<49) begin cunt=cunt+1; q=q; end
    else begin cunt=0;q=!q;end
end
endmodule

5、10进制计数器

module counter9(clr,clk,q);
input  clk,clr;
output [3:0]q;
reg    [3:0]q;
always @(posedge clk or negedge clr) begin
	if(!clr) q=0;
	else if(q==9) q=0;
	else q=q+1;
end
endmodule

6、加法器

module _2b_adder(IA,IB,SUM,C);
input [1:0]IA,IB;
output [1:0]SUM;
output C;
assign {C,SUM}=IA+IB;
endmodule

7、可使能3-8译码器

module _38_decode(EN,A,Y);
input EN;
input [2:0]A;
output [7:0]Y;
assign Y=(!EN)?8'B1111_1111 :
(
	(A==0)?8'B1111_1110 :
	(A==1)?8'B1111_1101 :
	(A==2)?8'B1111_1011 :
	(A==3)?8'B1111_0111 :
	(A==4)?8'B1110_1111 :
	(A==5)?8'B1101_1111 :
	(A==6)?8'B1011_1111 :  8'B0111_1111
);
endmodule

8、触发器设计

module dff(q,data,clk);
output q;
input data,clk;
reg q;
always@(posedge clk)
	begin 
		q<=data;
	end
endmodule

9、电平敏感型锁存器

module latch(q,data,clk);
output q;
input data,clk;
	assign q=clk?data:q;
endmodule 

10、三态输出驱动器

module trist(out,in,enable);
output out;
input in,enable;
	assign out=enable?in:’bz;
endmodule

三、错误总结

1、module后面缺少一个“;”,记得在每个module后面添加“;”。

2、对output的类型应该明确,reg或是wire。
在这里插入图片描述
在后面加一句reg cko; 给output一个明确的类型。

3、always后面缺少必要的end
在这里插入图片描述
找到相应的语句,添加一个end。

4、类型错误,always里面应该为reg型
在这里插入图片描述
将tag的类型改为reg型,因为是在always里面使用的。

5、模块的调用错误,调用是用“.”调用的,不能漏掉
在这里插入图片描述
改div_clk U0(.clk(clk), .fs(1), .cko(clk_up));

6、一个变量不能在多个always里面赋值
在这里插入图片描述
改:把两个always的变量赋值放到一个always里面去。

7、中文和英文符号的切换,圆角和半角
在这里插入图片描述
注意区别“;”和“;”

8、在module里面申明,在模块中却没有用input输入。
在这里插入图片描述
module dir(clk,rst);里面的都必须在模块中申明是input或是output,而不能直接使用。
9、top文件中的U3重复
在这里插入图片描述
改:按照顺序重新排列。

10、定义变量的范围
很多时候我们会忽略变量的定义范围,这样导致的问题是,举例数字钟只会显示010101,这就是我们忘记定义变量范围的后果

第二部分:自选题设计报告

选题名称:数显LED亮度控制电路

一、任务解析

数显LED亮度控制电路,实验功能:首先能够通过我们的按键控制灯的亮度,然后灯的亮度用数码管显示出来。
本次实验的要求:首先按键需要消抖,然后能够明显的观察到灯亮度的变化,最后亮度的变化可以通过数字直接的显示出来。

二、设计思路

在这里插入图片描述
首先通过原理图可知,我们需要按键及消抖、数码管显示、分频器、循环计数器、计数器、比较器这些功能函数。
经过进一步细化,流程图如下。
在这里插入图片描述
设计思路:通过比较99循环计数器每次通过99计数器的时间来控制LED的亮度变化,例如我们设置99计数器为80,且通过比较器设置小于80灯亮,那么99循环计数器小于80的值为0-79,同时灯亮的时间也为80个,这样灯就会很亮。
反之当我们设置99计数器很小,假设为20那么每次99循环计数器通过小于20的时间为0-19共20个时间单位,这样的话LED大多数时间属于不亮的状态,LED就会很暗。

三、实施步骤

1、按键消抖

module key_clean(clk,key_in,key_out);
input clk;      //1000Hz时钟
input key_in;   //物理按键输入
output key_out;//去抖之后输出
reg     [3:0]c; //计数变量
reg key_out;
always @(posedge clk) begin
    if(key_in) begin 
        if(c<10) c=c+1; //计数延迟消抖        
    end 
    else begin
        if(c>0) c=c-1;  
    end
    
    if(c>9) key_out=1;
    else if(c<1) key_out=0;
    else key_out=key_out;
end
endmodule 

2、分频电路

module div_clk(clk,fs,cko);
input clk; //输入时钟频率clk=50M
input [31:0]fs;
output cko; //输出时钟cko 
reg cko;
parameter N=50_000_000; //定义累加器上限,需要与基准频率f0相等
reg [31:0]ACC; //定义32位累加器ACC
always @(posedge clk) begin //累加器在clk上升沿触发完成累加
    if(ACC<N/2-1) ACC = ACC + fs; //累加器按参数步进值累加,注意不是+1
    else begin //满足累加器溢出条件
        ACC=0;
        cko=!cko; //这里cko自身二分频,所以上一句N需要除以2
    end
end
endmodule

3、数码管显示

module seg_drive (clk,DA,DB,DC,DD,DE,DF,DG,DH,a,b,c,d,e,f,g,p,sel); 
input clk;//数码管扫描时钟
input [3:0]DA,DB,DC,DD,DE,DF,DG,DH;
output a,b,c,d,e,f,g,p;
output [7:0]sel;

reg [2:0]scan;
reg[7:0]sel;
reg [3:0]num;

always @(posedge clk) begin
    scan=scan+1;
    case(scan) //3-8译码器 和 数据选择器
        0: begin sel=8'b1111_1110; num=DA; end
        1: begin sel=8'b1111_1101; num=DB; end
        2: begin sel=8'b1111_1011; num=DC; end
        3: begin sel=8'b1111_0111; num=DD; end
        4: begin sel=8'b1110_1111; num=DE; end
        5: begin sel=8'b1101_1111; num=DF; end
        6: begin sel=8'b1011_1111; num=DG; end
        7: begin sel=8'b0111_1111; num=DH; end
    endcase
end

assign {a,b,c,d,e,f,g,p} = //字符译码器
    (num==0)? 8'B0000_0011:        //"0"
    (num ==1)? 8'B1001_1111:       //"1"
    (num ==2)? 8'B0010_0101:       //"2"
    (num ==3)? 8'B0000_1101:       //"3"
    (num ==4)? 8'B1001_1001:       //"4"
    (num ==5)? 8'B0100_1001:       //"5"
    (num ==6)? 8'B0100_0001:       //"6"
    (num ==7)? 8'B0001_1111:       //"7"
    (num ==8)? 8'B0000_0001:       //"8"
    (num ==9)? 8'B0000_1001:       //"9"
    (num==10)? 8'B1111_1101:   //"-"
8'B1111_1111;   //熄灭

endmodule

4、99循环计数器

module cnt_9999(clk,out);
    input clk;
    output out;
    reg [6:0]out;
    always@(posedge clk)begin 
        if(out<100)out=out+1;
        else out=0; 
    end 
endmodule 

5、99计数器

module c9999(clk, rst, clk_1, kup, kdn, q);
input clk, rst;
input clk_1, kup, kdn;
output [23:0]q; //16bits计数器q,每4bits对应一位数码管

reg [23:0]q;
reg once, ku_f, kd_f; //合理利用标志位,实现单次加减1操作
always @(posedge clk or negedge rst) begin //此处的时钟不是计数时钟
    if(!rst) begin q=0; once=1; ku_f=1; kd_f=1; end
    else begin
        if(kup) begin 
            if(ku_f) begin 
                q_add(); //加1任务
                ku_f=0; //此标志位清零,可以实现按键1次就加1次
                once=0; //此标志位清零,可以实现按键不受计数时钟影响
            end 
        end
        else if(kdn) begin 
            if(kd_f) begin 
                q_sub(); //减1任务
                kd_f=0; //此标志位清零,可以实现按键1次就减1次
                once=0; //此标志位清零,可以实现按键不受计数时钟影响
            end 
        end
        else begin
            ku_f=1; //此标志位置1,允许快速响应“加”按键
            kd_f=1; //此标志位置1,允许快速响应“减”按键
        end
    end
end

//加1任务
task q_add();
begin
    if(q[15:0]==16'H9999)       q=0; //计数到了9999之后清零
else if(q[11:0]==12'H999)   q=q+16'H0667; //计数到了999之后进位清零
else if(q[07:0]== 8'H99)        q=q+16'H0067; //计数到了99之后进位清零
else if(q[03:0]== 4'H9)         q=q+16'H0007; //计数到了9之后进位清零
else                    q=q+16'H1; //加1
end
endtask

//对于16进制(二进制)数整体看,H9+H7=D16=H10,分高低4bit来看,就是H09变成H10
//对于16进制(二进制)数整体看,H99+H67=H100,道理同上
//对于16进制(二进制)数整体看,H999+H667=H1000

//减1任务
task q_sub();
begin
    if(q[3:0]>0)        q=q-16'H1;
    else if(q[7:4]>0)   q=q-16'H7;
    else if(q[11:8]>0)  q=q-16'H67;
    else if(q[15:12]>0)     q=q-16'H667;
    else            q=16'H9999; 
end
endtask
endmodule

6、数据比较器

module compare(a,b,y);
input [10:0] a,b; 
output y; 
reg y; 
always @ (a or b) begin
if (a < b) y = 1; 
    else y = 0;
end 
endmodule

7、top顶层函数

module top(clk,rst,a,b,c,d,e,f,g,p,sel,kup,kdn,led);
input clk,rst,kup,kdn; 
output a,b,c,d,e,f,g,p;
output [7:0]sel;
output led;

wire clk_up;       //计数时钟1hz
wire clk_scan; //扫描时钟500hz
wire clk_key;  //按键去抖时钟1000hz
wire    clk_xz;
wire     [3:0]A,B,C,D,E,F;     //数据节点
wire kup_reg, kdn_reg;     //消抖之后的按键w
wire [7:0]out;

div_clk        U0(.clk(clk), .fs(1), .cko(clk_up));
div_clk        U1(.clk(clk), .fs(500), .cko(clk_scan));
div_clk        U2(.clk(clk), .fs(1000), .cko(clk_key));
key_clean   U3(.clk(clk_key), .key_in(kup), .key_out(kup_reg));
key_clean   U4(.clk(clk_key), .key_in(kdn), .key_out(kdn_reg));
c9999      U5(.clk(clk), .rst(!rst), .clk_1(clk_up), .kup(kup_reg), .kdn(kdn_reg), .q({D,C,B,A,E,F}) );
cnt_9999        U7(.clk(clk_xz),.out(out));
seg_drive   U6
(
    .clk(clk_scan), //调用数码管驱动电路
    .DA(F), .DB(E), .DC(10), .DD(10), .DE(10), .DF(10), .DG(10), .DH(10), 
    .a(a), .b(b), .c(c), .d(d), .e(e), .f(f), .g(g), .p(p), .sel(sel)
);
compare     U8(.a(out),.b({E,F}),.y(led));
div_clk    U9(.clk(clk), .fs(50_000_000), .cko(clk_xz));
endmodule

四、结果展示

实验结果由暗到亮依次显示的结果如下,为了对比起来更加明显,我选择了02-20-99三种亮度便于比较。
在这里插入图片描述
图1:亮度为2,比较暗,无光圈
在这里插入图片描述
图2:亮度为20,较为明亮,外围蓝色光圈较小
在这里插入图片描述
图3:亮度为99,亮度很高,外围蓝色光圈很大很亮

五、经验总结

本次实验达到了预期的效果,可以通过消抖后的按键较好的控制灯的亮度,但是由于我们人眼本身观察不敏锐的原因,所以当灯在50亮度以后,我们人眼是无法察觉它的微小变化,主要靠我们的数码管进行显示。
解决办法:虽然实验存在上述问题,但是也不是不能解决的,我们可以让99计数器的值以指数的方式进行改变,因为指数的变化是非常大的,这样我们人眼的观察就能非常的清楚,能比较直观的感觉到灯亮度的变化。

六、学习体会

通过本次EDA的学习,我对硬件模块有了更深刻的理解,相比于我们正在学习的单片机、嵌入式,EDA的使用更加严谨周密,而且更加底层,例如时钟的快慢都是我们自己进行设置。记得在初学的时候它的规则一个变量不能在两个always赋值,让我觉得非常的奇怪、蹩脚甚至难以理解,但是在学习深入后我发现,这样确实能极大的增加硬件的稳定性,让电路更加的可靠,我也慢慢开始接受这种规则。
从语法上:它的语法属于C但是又不完全是C,拥有C语言基础基本可以驾驭这种语法,但是我发现在后面的小型项目的设计中,很多时候我们都要用到C语言的算法,当拥有C语言的算法思想后,我们的代码看起来不仅简洁而且有条理,这让我再次体会到了结构与算法的重要性。
今天的介绍就到这里啦,谢谢大家~~~

会水的芝麻
2019.8.4


版权声明:本文为qq_42369840原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。