写在前面的话
我大学本科学的是测控专业,2012年考取首都师范大学物理系研究生。我从未学习过数字电路设计,对FPGA和Verilog语言没有任何概念,更没有设计数字电路系统的基础和经验,也从未自己动手装配和完成过一台能实际运行的电子系统。但我从小就对电子设计有浓厚的兴趣。为什么小小的计算器按几下就能完成非常复杂的数学计算,一直困惑着我,激起我年轻的好奇心。大学四年里,虽然学习过“数字电路”和“模拟电路”课程,考试成绩也很不错,但对我而言,计算器是如何设计的,仍旧是一头雾水。
听同学们说,如果掌握了FPGA设计,这个谜就能找到答案。我用关键字“FPGA培训”在百度搜索,发现一个公司正在开设FPGA就业培训(100天)班,也知道这个班由北京航空航天大学的夏宇闻教授亲自讲授和管理。于是下定决心抽出3个月时间,认真学习一下FPGA。经过100天的学习和练习,我初步掌握了如何用FPGA芯片设计和搭建复杂数字系统。现在我有充分的信心,只要设计需求明确,我完全有能力独立设计并完成一个较复杂的数字系统,并能可靠地完成预先设定的数据 处理任务。这个阶段的学习给了我很多启发,也增强了我的信心,很想把自己的感受和学习心得编写成小册子与大家分享。我的想法得到夏宇闻教授的支持。于是我把学习期间的心路历程和学到的知识、经验略加整理,以日记的形式写出来,与大家分享,希望能给打算学习Verilog和FPGA设计的初学者一些帮助和启发,起到抛砖引玉的作用。
本书使用的硬件为至芯科技的四代开发板、Altera CycloneⅣ的芯片,软件为QuartusⅡ13.0 sp1。
——作者
1
设计需求讲解从今天开始就进入计算器内层的设计了,也就是计算器的输入状态机模块的设计。操作数输入模块用来把按键输入的每一位按键的键值“拼接”起来,按键每按下一次,输入一位十进制的数,输入的十进制数以 BCD 码的形式放到操作数寄存器的个位,同时将寄存器中原数据向高位移动,并且还要响应操作符,在操作符之后要重新显示数字。效果如下:
初始(显示0)→输入数1(显示1)→输入数2(显示12)→输入数3(显示123)→输入操作符(依然显示123)→输入数3(显示3)→输入数2(显示32)→……
2
我对状态机概念的理解像这种有着明显先后顺序的操作,一般情况下采用有限状态机(FSM) 来处理。其实大家昨天就已经在代码中看到了状态机,比如:
always @(posedge clk)
if( !(&keyin))
begin
AnyKeyPressed <= `OK ;
four_state <= 4'b0000;
endelse if(AnyKeyPressed)case(four_state)
4'b0000: begin AnyKeyPressed <= `OK ; four_state<=4'b0001; end
4'b0001: begin AnyKeyPressed <= `OK ; four_state<=4'b0010; end
4'b0010: begin AnyKeyPressed <= `OK ; four_state<=4'b0100; end
4'b0100: begin AnyKeyPressed <= `OK ; four_state<=4'b1000; end
4'b1000: begin AnyKeyPressed <= `NO ; end
default: AnyKeyPressed <= `NO ;
endcase
else
four_state <= 4'b0000;
这一小段代码就是一个状态机。在初始化时,将four_state值设置为0,在case(four_state)里会只执行第①行代码,此时four_state被赋值为1,那么下一次时钟沿到来的时候,在case(four_state)里便会执行第②行代码。以此类推,按照一定的顺序来执行case语句中这5行代码。因为硬件语言HDL中代码是并行执行的,所以想要执行一些有先后顺序的代码,就得用像状态机这种时序逻辑电路模块。简言之,状态机是有记忆功能的,它可以记住刚刚发生了什么,根据之前的情况来判断接下来会怎么做,实现一种因果的关系。
3
设计工具使用讲解1、范例代码解析
下面把夏老师在课堂上提供的示例代码分享给各位同学。夏老师说,给大家这个代码是为了抛砖引玉。这段代码十分粗浅,但容易理解,给出的目的是让大家彻底明白状态的意义。同学们在理解了粗浅的状态概念和实现方法之后,必须自己有所创新,想出更巧妙的办法来定义和记录更复杂的状态,实现更宽广的状态转移,以便更有效地控制系统的资源。
// 记录个、十、百位数字和四则算术运算符的输入,能执行最简四则算术运算控制的基础状态机
module gewshiwbaiw(clk,
rst_n,
realnumber, //已确认的键盘码输入
gew, //个位 输出
shiw, //十位 输出
baiw, //百位 输出
BCD_code, //BCD码输出
opcode, //运算符输出
BCD_state //状态记录
);
input clk,rst_n;
input [4:0]realnumber;
output [3:0]gew,
shiw,
baiw;
output [11:0] BCD_code;
output [3:0] opcode;
output [3:0] BCD_state;
reg [3:0] gew, gew_reg,
shiw, shiw_reg,
baiw, baiw_reg;
reg [3:0] opcode,
opcode_reg; //运算操作符寄存器
reg [11:0] BCD_code_reg;
reg [5:0] BCD_state;
reg datacoming_flag, datacoming_state;
assign BCD_code ={baiw_reg,shiw_reg,gew_reg};
//提取按键按下和释放跳变时刻的时钟信号,以掌握准确的状态转换时刻。
always @(posedge clk)
if (rst_n)
begin
datacoming_state <=0;
datacoming_flag <=0;
endelseif (realnumber!=17) //发现已有某个键被按下了case(datacoming_state)
0: begin
datacoming_flag <=1;
datacoming_state <=1;
end
1: begin
datacoming_flag <=0;
datacoming_state <=1;
end
endcase
else //该键已被放开,目前没有任何键被按下
begin
datacoming_state <= 0;
datacoming_flag <= 0;
end
// 控制两个由个位、十位、百位组成的BCD码
// 和相应运算操作符(加、减、乘、除)输入的状态机
always @(posedge clk)
if (rst_n)
begin
{gew,shiw,baiw} <=12'b0000_0000_0000;
{gew_reg,shiw_reg,baiw_reg} <=12'b0000_0000_0000;
BCD_state <= 4'b0000;
opcode <= 14;
opcode_reg<=14;
endelse if(datacoming_flag) //若有键已被按下case(BCD_state)
0: if (realnumber>=0 && realnumber<=9) //如键入码为操作数,而非操作码
begin
opcode_reg<=opcode;
gew<= realnumber;
BCD_state <= 1;
end
1: if(realnumber>=0 && realnumber<=9 )
begin
gew<=realnumber;
shiw<=gew;
BCD_state <= 2;
end else if(realnumber>=10 && realnumber<=13) //如键入码为操作码,而非操作数
begin
opcode <= realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 4;
end
2: if (realnumber>=0 && realnumber<=9)
begin
gew<=realnumber;
shiw<=gew;
baiw<=shiw;
BCD_state <= 3;
endelse if(realnumber>=10 && realnumber<=14) //如键入码为操作码,包括等号
begin
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 4;
end
3: if (realnumber>=10 && realnumber<=14 )
begin
opcode <= realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 4;
end
4: if (realnumber>=0 && realnumber<=9) //第二个操作数
begin
gew<= realnumber;
BCD_state <= 5;
opcode_reg<=opcode;
end
5: if (realnumber>=0 && realnumber<=9)
begin
gew<=realnumber;
shiw<=gew;
BCD_state <= 6;
endelse if(realnumber>=10 && realnumber<=14)
begin
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
6: if (realnumber>=0 && realnumber<=9)
begin
gew<=realnumber;
shiw<=gew;
baiw<=shiw;
BCD_state <=7;
end else if(realnumber>=10 && realnumber<=14)
begin
opcode_reg<=opcode;
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
7: if (realnumber>=10 && realnumber<=14)
begin
opcode_reg<=opcode;
opcode<= realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
default : BCD_state <= 0;
endcase
endmodule
下面是testbench。先看一下仿真的结果,然后再进行代码分析:
`timescale 10us/10us
module t;
reg clk,rst_n;
reg [4:0] realnumber;
wire [3:0] gew,
shiw,
baiw;
wire [11:0] BCD_code;
wire [3:0] BCD_state;
wire [3:0] opcode;
initial
begin
clk = 0;
rst_n = 0;
#20 rst_n = 1;
#120 rst_n = 0;
end
always #50 clk =~clk;
initial
begin repeat(50)
begin
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
realnumber = 2; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 6; #1000;
realnumber = 17; #5000;
//---------------------------
realnumber = 14; #1000; // 512 + 256 =
realnumber = 17; #5000;
//---------------------------
realnumber = 3; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 31 + 25 =
realnumber = 17; #5000;
//--------------------------
//---------------------------
realnumber = 3; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 6; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; //3 + 6 =
realnumber = 17; #5000;
//--------------------------
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
realnumber = 2; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 512 + 25 =
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 6; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 25 + 256 =
realnumber = 17; #5000;
end
$stop;
end
gewshiwbaiw m(.clk(clk),
.rst_n(rst_n),
.realnumber(realnumber),
.gew(gew),
.shiw(shiw),
.baiw(baiw),
.BCD_code(BCD_code),
.opcode(opcode),
.BCD_state(BCD_state));
endmodule
仿真波形如图3-1所示。
图3-1 仿真结果
我们关注一下波形中的gew(个位)、shiw(十位)、baiw(百位) 随着realnumber变化而变化的过程。一开始realnumber为17(无任何键按下),gew(个位)、shiw(十位)、baiw(百位)都为 0;按下数字5后 (realnumber等于5),gew变成5,shiw和baiw不变;接下来realnumber又等于17(表示抬起刚刚按下的按键);之后realnumber等于1(表示再一次按下数字1键),刚刚gew的数字5移到了shiw,而gew变成了刚刚按下的数字1,以此类推。我们自己规定realnumber为10表示加号(+),11代表减号(-),12代表乘号(×),13代表除号(÷),14代表等号(=),这些都可以按照自己的意愿随便定,只要别记混了就好。
通过波形可以看出,输入完第三个数之后,realnumber输入了加号(realnumber=10),此时gew、shiw、baiw变为0,并且将之前输入的第一个待运算数通过BCD_code输出,opcode(运算符) 也输出,等待接收第二个待运算数。两个待运算的数和运算符传输给后面的计算模块,就可以得到结果了。关于计算模块我们以后再讨论,先把今天夏老师讲的东西消化一下,经过了这两天读代码的练习,理解起来应该越来越容易了。首先看第一个always段:
always @(posedge clk)
if (!rst_n)
begin
datacoming_state <=0;
datacoming_flag <=0;
endelseif (realnumber!=17) //若有某个已键被按下case(datacoming_state)
0: begin
datacoming_flag <=1;
datacoming_state <=1;
end
1: begin
datacoming_flag <=0;
datacoming_state <=1;
end
endcase
else
begin
datacoming_state <= 0;
datacoming_flag <= 0;
end
复位之后把datacoming_state和 datacoming_flag清零,当有任何按键按下时(也就是realnumber不为17),运行case里的代码,使datacoming_flag置高,datacoming_state跳转到1;下一个时钟到来时,再把datacoming_flag拉低。产生的效果就是每当有按键输入,datacoming_flag会保持一个时钟周期的高电平,作为第二个模块也就是主状态机模块的使能信号,它要告诉状态机,有信号来了,让状态机动一下。没有datacoming_flag信号的时候状态机是不工作的。效果如图3-2所示。
图3-2 标志位信号
当realnumber由17变到5,datacoming_flag由低变高,保持一个时钟周期之后再变低,以此类推。
always @(posedge clk)
if (!rst_n) //复位时给输出赋初值
begin
{gew,shiw,baiw} <=12'b0000_0000_0000;
{gew_reg,shiw_reg,baiw_reg} <=12'b0000_0000_0000;
BCD_state <= 4'b0000;
opcode <= 14;
opcode_reg<=14;
endelse if(datacoming_flag) //如果有键按下,执行状态机case(BCD_state)
0: if (realnumber>=0 && realnumber<=9) //首先等待第一个数字按键0—9
begin
opcode_reg<=opcode;
gew<= realnumber; //将第一个按键数值赋给个位
BCD_state <= 1; //状态跳转到1
end
1: if(realnumber>=0 && realnumber<=9 ) //如果第二个按键还是数字
begin
gew<=realnumber; //将第二个按键数值赋给个位
shiw<=gew; //同时将之前的个位值移到十位
BCD_state <= 2; //状态跳转到2
end else if(realnumber>=10 && realnumber<=13) //如果第二个按键是操作符+-*/
begin
opcode <= realnumber; //将操作符记下
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
//将第一个操作数记下
{baiw,shiw,gew}<=0; //同时清掉之前的操作数
BCD_state <= 4; //状态跳转到4
end
2: if (realnumber>=0 && realnumber<=9) //流程同状态1
begin
gew<=realnumber;
shiw<=gew;
baiw<=shiw;
BCD_state <= 3;
endelse if(realnumber>=10 && realnumber<=14)
begin
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 4;
end
//由于此代码只支持3位数运算,所以输入3个数字后,只检测运算符,不检测数字
3: if (realnumber>=10 && realnumber<=14 ) //检测按下的运算符
begin
opcode <= realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 4;
end
4: if (realnumber>=0 && realnumber<=9) //接收第二个操作数
begin
gew<= realnumber;
BCD_state <= 5;
opcode_reg<=opcode;
end
5: if (realnumber>=0 && realnumber<=9) //后面的流程与之前类似
begin
gew<=realnumber;
shiw<=gew;
BCD_state <= 6;
endelse if(realnumber>=10 && realnumber<=14)
begin
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
6: if (realnumber>=0 && realnumber<=9)
begin
gew<=realnumber;
shiw<=gew;
baiw<=shiw;
BCD_state <=7;
end else if(realnumber>=10 && realnumber<=14)
begin
opcode_reg<=opcode;
opcode <=realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
7: if (realnumber>=10 && realnumber<=14)
begin
opcode_reg<=opcode;
opcode<= realnumber;
{baiw_reg,shiw_reg,gew_reg}<={baiw,shiw,gew};
{baiw,shiw,gew}<=0;
BCD_state <= 0;
end
default : BCD_state <= 0;
endcase
根据上述的一些注释,相信大家已经明白了这段代码的作用。夏老师给的是一个示例的代码,只能做3位数的运算,而且每一位都定义了一个变量来保存(gew,shiw,baiw),如果想扩展位数的话,还得增加变量,并且加入更多的状态,可扩展性不强。所以我打算重写这段代码,去掉每一位的变量,并区分出两个待运算的数,定义第一个操作数为 A,第二个操作数为B。这样整个模块的输出就只有A,B和运算符,输入为realnumber。
2、重写状态机代码
改写后的代码如下:
module key2bcd0(clk,real_number,opcode,rst_n,BCDa,BCDb);
input [4:0] real_number;
input rst_n,clk;
output reg [23:0] BCDa,BCDb;
output reg [3:0] opcode;
reg [3:0] opcode_reg;
reg [3:0] state;
reg datacoming_state,datacoming_flag;
always @(posedge clk)
if (!rst_n)
begin
datacoming_state <=0;
datacoming_flag <=0;
endelseif (real_number!=17) case(datacoming_state)
0: begin
datacoming_flag <=1;
datacoming_state <=1;
end
1: begin
datacoming_flag <=0;
datacoming_state <=1;
end
endcase
else
begin
datacoming_state <= 0;
datacoming_flag <= 0;
end
always @ (posedge clk or negedge rst_n)
beginif(!rst_n)
begin
BCDa <= 0;
BCDb <= 0;
state <= 0;
opcode <= 0;
end elseif(datacoming_flag)
begincase(state)
0: case(real_number) //接收第一个操作数A
0,1,2,3,4,5,6,7,8,9:
begin
BCDa[23:0] <= {BCDa[19:0],real_number[3:0]};
state <= 0;
end
10,11,12,13: //如果有运算符按下则状态跳转至状态1
begin
//因为还没有第二个操作数B,所以此时先记下操作符备用
opcode_reg <= real_number[3:0];
state <= 1;
end
default: state <= 0;
endcase
1: case(real_number) //接收第二个操作数B
0,1,2,3,4,5,6,7,8,9:
begin
//已经输入了第二个操作数B,所以将记下来的运算符输出给计算模块
opcode <= opcode_reg;
BCDb[23:0] <= {BCDb[19:0],real_number[3:0]};
state <= 1;
end
//如果操作符为等于号(=),则这一次计算结束,
//清空操作符,操作数A和B,跳回状态1
14: begin
BCDa <= 0;
BCDb <= 0;
opcode <= 0;
state <= 0;
end
default: state <= 1;
endcase
default : state <= 0;
endcase
end
end
endmodule
第一个always段没有变,还是为状态机提供使能信号,只是状态机进行了重写。我个人认为这个状态机比较容易理解,0状态为接收第一个操作数A,直到输入了运算符+,-,∗,/,之后再输入数字就是操作数B,再按下等号就表示这一次运算结束。这样我们就得到了一个可以运算8位数的计算器模块。然后将这个模块接到testbench上,测试一下。可以直接用夏老师提供的testbench文件进行修改。改后的代码如下:
`timescale 10us/10us
module key2bcd_tb0;
reg clk,rst_n;
reg [4:0] realnumber;
//输出变成了BCDa,BCDb和opcode,所以这里需要修改
wire [23:0] BCDa,BCDb;
wire [3:0] opcode;
initial
begin
clk = 0;
rst_n = 1;
#20 rst_n = 0;
#120 rst_n = 1;
end
always #50 clk =~clk;
initial
begin repeat(50)
begin
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
realnumber = 2; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 6; #1000;
realnumber = 17; #5000;
//---------------------------
realnumber = 14; #1000; // 512 + 256 =
realnumber = 17; #5000;
//---------------------------
realnumber = 3; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 31 + 25 =
realnumber = 17; #5000;
//--------------------------
//---------------------------
realnumber = 3; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 6; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; //3 + 6 =
realnumber = 17; #5000;
//--------------------------
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
realnumber = 1; #1000;
realnumber = 17; #5000;
realnumber = 2; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 512 + 25 =
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 10; #1000; //加
realnumber = 17; #5000;
//--------------------------
realnumber = 2; #1000;
realnumber = 17; #5000;
realnumber = 5; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 6; #1000;
realnumber = 17; #5000;
//--------------------------
realnumber = 14; #1000; // 25 + 256 =
realnumber = 17; #5000;
end
$stop;
end
//模块的实例化和接线重写即可
key2bcd0 key2bcd (
.clk(clk),
.real_number(realnumber),
.opcode(opcode),
.rst_n(rst_n),
.BCDa(BCDa),
.BCDb(BCDb)
);
endmodule
3、转到ModelSim仿真工具进行测试
写好后保存,别忘了在Settings进行 Simulation的设置,设置好后运行ModelSim,观察如图3-3所示波形。
图3-3 仿真结果
从波形可以看到 BCDa、BCDb和opcode均显示正确。连续按下5、1、2后,BCDa上为000512,按完加号(realnumber=10),按的数字保存在了BCDb上,同时输出了opcode。
4、下载程序到开发板进行调试
仿真通过了,下一步就可以在开发板上看一下效果了。把这个模块放在按键模块和显示模块之间,连接关系如图3-4所示。
图3-4 模块连接示意图
这样就得修改一下display模块。将输入改为BCDa和BCDb,并加上sel的所有情况,同时,我们还得选择到底显示第一个操作数A,还是第二个操作数B。可以这样判断,如果B=0,则认为还没输入数字B,所以此时就显示A。module display3(clk, rst_n, adata, bdata, sel, seg, clk_out);
input clk;
input rst_n;
input [23:0] adata,bdata;
output reg [2:0] sel;
output reg [7:0] seg;
wire [23:0] data;
reg [3:0] segdata;
reg [23:0] q;
output reg clk_out;
assign data = (bdata == 0) ? adata : bdata; //如果B为0,则输出A
always @ (posedge clk) //时钟分频,产生周期为毫秒级的慢时钟
begin if(!rst_n)
begin
q <= 24 'b0000_0000_0000_0000_0000_0000;
clk_out <= 1'b1;
end
else
begin
q <= q + 1;
clk_out <= q[12];
// 212个时钟后clk_out 从0变为1。再过212个时钟后又变为0
end
end
always @ (posedge clk_out or negedge rst_n)
// 产生sel扫描,依次控制6个数码管中LED共阳极与电源(高电平)的通/断
beginif(!rst_n)
begin
sel <= 0;
end
else
begin
sel <= sel + 1;
if (sel >= 5)
sel <= 0;
end
end
always @ (*) //将输入的24位数拆成6个数,每4位二进制表示1个十进制数(0—9)
beginif(!rst_n)
begin
segdata <= 0;
endelse
begincase(sel)
5: segdata <= data[3:0]; //个位
4: segdata <= data[7:4]; //十位
3: segdata <= data[11:8]; //百位
2: segdata <= data[15:12]; //千位
1: segdata <= data[19:16]; //万位
0: segdata <= data[23:20]; //十万位
default: segdata <= 0;
endcase
end
end
always @ (*) //把数字转换成seg对应的组合
beginif(!rst_n)
begin
seg <= 8'hff;
endelse
begincase(segdata)
0: seg <= 8'b11000000;
1: seg <= 8'b11111001;
2: seg <= 8'b10100100;
3: seg <= 8'b10110000;
4: seg <= 8'b10011001;
5: seg <= 8'b10010010;
6: seg <= 8'b10000010;
7: seg <= 8'b11111000;
8: seg <= 8'b10000000;
9: seg <= 8'b10010000;
default: seg <= 8'b11111111;
endcase
end
end
endmodule
稍微改动一下之前的代码就可以了,然后把顶层进行修改,包括显示模块接口的变化,并增加了一个新的模块。改后代码如下:
module calc1(clk,rst_n,seg,sel,keyin,keyscan);
input clk, rst_n;
input [3:0] keyin;
output [3:0] keyscan;
output [2:0] sel;
output [7:0] seg;
wire clk_slow;
wire [4:0] real_number;
wire [23:0] BCDa,BCDb;
wire [3:0] opcode;
//在第一天做的显示模块display1的基础上,修改出第2天显示模块display2
//下面的显示模块是在display2的基础上,修改得到的,其名为display3
display3 u1(
.clk(clk),
.adata(BCDa),
.bdata(BCDb),
.rst_n(rst_n),
.sel(sel),
.seg(seg),
.clk_out(clk_slow)
);
//第二天做的按键输入模块
keyscan u2(
.clk(clk_slow),
.rst_n(rst_n),
.keyscan(keyscan),
.keyin(keyin),
.real_number(real_number)
);
//今天做的状态机移位模块
key2bcd u3(
.clk(clk_slow),
.real_number(real_number),
.opcode(opcode),
.rst_n(rst_n),
.BCDa(BCDa),
.BCDb(BCDb)
);
endmodule
这样我们今天的任务就成功完成了,而且还把夏老师介绍的只能操作3位BCD数的模块修改为可操作6位BCD操作数的模块。
回顾一下今天所学到的内容:
1) 理解怎样根据电路功能的需求抽象出相应的状态机。
2) 用自己独特的思路大胆修改了老师提供的原始状态机,取得了很好的效果。
3) 了解多个模块之间的连接关系,利用顶层文件将它们整合。
4) 理解Verilog数字设计必须掌握硬件结构和部件工作原理的道理。
5) 修改了显示模块,使其能通过选择sel线的1/0,选择显示BCDa或者BCDb中的某一个。
接下来要做的工作就是设计计算模块部分,该模块可根据操作符opcode将操作数 A、B进行运算并输出结果。
5
夏老师评述赵然同学在输入状态机模块的设计中,勇敢开拓,很有创意。他设计的模块吸收了示范程序中所体现的记忆当前操作状态的思想,但他能开动脑筋,大胆改进,编写出有自己特色、性能稳定、功能更强、更加简洁的模块,说明他已经掌握状态机的实质和设计方法,掌握语法的程度也已达到并超过今天的教学要求。
老师建议读者在设计中还可以改进的地方是:当输入操作运算符时,数码显示区应该能显示操作运算符。此时,可以关闭第一个操作数的显示,也可以移位后继续在数码管上显示,直到第二个操作数开始输入后再关闭第一个操作数字和运算符的显示。选用这样的显示方案,对使用者而言,显然更加直观和方便。
通过修改状态机,增加几个控制信号,同一个显示模块只要补充少许硬件部件,如二选一多路器,便可以具有更强大显示功能。设计师必须学会如何利用现成模块,添加一些硬件和控制信号,在状态机的配合下,扩展其功能。数字系统设计的实质就是利用基础组件,通过创造性思维和现场调试,把它们构造成一个更加完整和理想的系统。
下期预告:
第4天——BCD码与二进制码转换模块的设计
往期回顾
10天学会四则运算小计算器设计之第2天
姿势已摆好
就等你点啦
你们的“在看”是小编连载的动力喔 ?