前言:
今天我将介绍的是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