写在前面的话
我大学本科学的是测控专业,2012年考取首都师范大学物理系研究生。我从未学习过数字电路设计,对FPGA和Verilog语言没有任何概念,更没有设计数字电路系统的基础和经验,也从未自己动手装配和完成过一台能实际运行的电子系统。但我从小就对电子设计有浓厚的兴趣。为什么小小的计算器按几下就能完成非常复杂的数学计算,一直困惑着我,激起我年轻的好奇心。大学四年里,虽然学习过“数字电路”和“模拟电路”课程,考试成绩也很不错,但对我而言,计算器是如何设计的,仍旧是一头雾水。
听同学们说,如果掌握了FPGA设计,这个谜就能找到答案。我用关键字“FPGA培训”在百度搜索,发现一个公司正在开设FPGA就业培训(100天)班,也知道这个班由北京航空航天大学的夏宇闻教授亲自讲授和管理。于是下定决心抽出3个月时间,认真学习一下FPGA。经过100天的学习和练习,我初步掌握了如何用FPGA芯片设计和搭建复杂数字系统。现在我有充分的信心,只要设计需求明确,我完全有能力独立设计并完成一个较复杂的数字系统,并能可靠地完成预先设定的数据 处理任务。这个阶段的学习给了我很多启发,也增强了我的信心,很想把自己的感受和学习心得编写成小册子与大家分享。我的想法得到夏宇闻教授的支持。于是我把学习期间的心路历程和学到的知识、经验略加整理,以日记的形式写出来,与大家分享,希望能给打算学习Verilog和FPGA设计的初学者一些帮助和启发,起到抛砖引玉的作用。
本书使用的硬件为至芯科技的四代开发板、Altera CycloneⅣ的芯片,软件为QuartusⅡ13.0 sp1。
——作者
1
设计需求讲解今天的设计任务有两项:1)完成计算器中负责二进制四则运算的算术逻辑模块(即alu0)。2)改写计算控制状态机模块(即key2bcd1)。算术逻辑模块是计算机设计中最后一个小模块。它运行所需的准备工作几乎都是由以前调试通过的模块完成的。操作数a和b,以及操作符opcode都可以从键盘输入,并转换为二进制数存入寄存器A、B。算术逻辑模块 alu0只需做相应的算术运算即可。
2
设计工具使用讲解我们需要新建一个文件来作为计算模块。
选择File–New–Verilog HDL File,如图5-1所示,然后点击下面的OK按钮。
图5-1 新建文件
1、计算模块的可综合代码
计算模块的可综合代码如下:
module alu0(a, b, clk, rst_n, opcode, bin_data);
input [23:0] a,b;
input clk;
input rst_n;
input [3:0] opcode;
output reg [24:0] bin_data;
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
bin_data <= 0;
end
else
begin
case(opcode)
10: begin bin_data <= a + b; end
11: begin bin_data <= a - b; end
12: begin bin_data <= a * b; end
13: begin bin_data <= a / b; end
endcase
end
end
endmodule
经过这些天的练习,写起这段代码来一定很轻松了。我们只需要判断opcode的值来运行不同的运算即可。
2、计算模块的测试
计算模块的测试代码如下:
`timescale 1ns/1ns
module alu_tb0;
reg [23:0] a,b;
reg clk;
reg rst_n;
reg [3:0] opcode;
wire [23:0] bin_data;
alu0 u1(.a(a),.opcode(opcode),.b(b),.clk(clk),
.rst_n(rst_n),.bin_data(bin_data));
initial
begin
a = 0;b = 0;rst_n = 0; clk = 1; opcode = 0; //复位,初始化
#101 rst_n = 1; //复位结束
#1000 a = 11; //11 + 2
opcode = 10;
b = 2;
#1000 a = 13; //13 - 3
opcode = 11;
b = 3;
#1000 a = 4; //4 × 5
opcode = 12;
b = 5;
#1000 a = 10; //10 ÷ 2
opcode = 13;
b = 2;
#1000
$stop;
end
always #10 clk = ~clk;
endmodule
因为模块比较简单,逻辑不易出错,所以简单地做一遍测试即可。写好之后将alu.v和alu_tb.v加入到仿真文件中,如图5-2所示。
设置完毕后启动RTL仿真。
图5-2 新建仿真设置
3、转到ModelSim仿真工具进行仿真
将计算模块的测试代码转到ModelSim仿真工具进行仿真,得到的仿真波形如图5-3所示。观察时可以将所有的变量格式设置为unsigned,方便观察。
图5-3 仿真结果
复位后的第一组运算,a为11,b为2,opcode为10(加法),结果bin_data输出13,没有问题。同样,13-3=10,4×5=20,10÷2=5均运算正确。
4、模块连接关系
目前我们的顶层连接还是以前做的如图5-4所示这种形式,后来编写的BCD码与二进制数之间的转换模块以及计算模块还都没有添加进去,所以还需要修改顶层代码。
图5-4 模块连接图
前面曾经讨论过,由于计算模块需要二进制数,而其它模块均以BCD码传输,所以计算模块alu前后必须紧跟BCD转二进制,以及二进制转BCD。所以把这3个模块接在按键转BCD码key2bcd后面最合适不过了。而值得考虑的是,计算完的结果输出到哪里。这里有两种选择,一种是输出到显示模块display,另一种是输出到按键转BCD的模块key2bcd。一般情况下我们会先想到要直接输出到显示模块,这样计算的结果就直接显示出来了。但是这样就又牵扯到一个问题,那就是显示模块只能输出一个数据,也就是说,只能输出A,B,或者结果,到底要显示哪一个数,显示模块需要做出判断。而我们之前已经在显示模块里加了判断A或者B的代码,不如就在这个基础上进行改造,将输出结果赋值给A或者B,这样显示模块就可以不用改动,继续判断A,B进行输出即可。而这一做法还有另外一个好处,就是为了改进计算器实现连续运算提供了方便。当我们想实现连续运算A+B+C+D……的时候,我们不可能会设置N多个变量来保存这些操作数,可以将每一步的运算结果赋值回A,这样就可以简化为用A一直在与另外一个数B进行运算,只需要两个操作数。关于连续运算以后再进行讨论。下面先修改key2bcd中的代码。
module key2bcd1 (clk,
rst_n,
real_number,
opcode,
BCDa,
BCDb,
result ); //端口加上计算结果result
input [4:0] real_number;
input rst_n,clk;
input [23:0] result; //result定义为输入
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)
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:
begin
opcode_reg <= real_number[3:0];
state <= 1;
end
default:state <= 0;
endcase
1: case(real_number)
0,1,2,3,4,5,6,7,8,9:
begin
opcode <= opcode_reg;
BCDb[23:0] <= {BCDb[19:0],real_number[3:0]};
state <= 1;
end
14: begin
BCDa <= result; //在按完等号之后将结果赋值给A
BCDb <= 0; //同时清空B的值,数码管就可以显示A
opcode <= 0;
state <= 2;
end
default:state <= 1;
endcase
//加上一个状态,表示上一次运算结束后,重新输入数值A时,不受到运算结果的影响
2: case(real_number)
0,1,2,3,4,5,6,7,8,9:
begin
BCDa <= real_number;
state <= 0; //输入完一位数后,状态跳回0,便又可以移位显示
end
10,11,12,13:
begin
opcode_reg <= real_number[3:0];
state <= 1;
end
default:state <= 2;
endcase
default : state <= 0;
endcase
end
end
endmodule
同时,顶层模块的连接关系也就产生了,如图5-5所示。
图5-5 模块连接图
按照这个连接关系把可综合顶层修改如下:
module calc2(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,a,b,bin_data,result;
wire [3:0] opcode;
display3 u1( .clk(clk),
.adata(BCDa),
.bdata(BCDb),
.rst_n(rst_n),
.sel(sel),
.seg(seg),
.clk_out(clk_slow)
);
keyscan0 u2(
.clk(clk_slow),
.rst_n(rst_n),
.keyscan(keyscan),
.keyin(keyin),
.real_number(real_number)
);
key2bcd1 u3(
.clk(clk_slow),
.real_number(real_number),
.opcode(opcode),
.rst_n(rst_n),
.BCDa(BCDa),
.BCDb(BCDb),
.result(result)
);
bcd2bin0 u4(
.BCDa(BCDa),
.BCDb(BCDb),
.a(a),
.b(b)
);
bin2bcd0 u5(
.bin(bin_data),
.bcd(result)
);
alu0 u6(
.a(a),
.b(b),
.clk(clk_slow),
.rst_n(rst_n),
.opcode(opcode),
.bin_data(bin_data)
);
endmodule
写好之后将calc2模块设置为顶层,然后进行分析综合。通过之后可以单击RTL Viewer看一下综合后各个模块的连接关系,是否和之前设想的一样,如图5-6所示,在Analysis & Synthesis下找到RTL Viewer,双击即可。
图5-6 打开RTL视图
打开之后会弹出一个窗口如图5-7所示窗口,里面绘制了各个模块之间的连线和输入输出端口。
图5-7 RTL模型图
可以看出,程序综合出了我们想要的模块连接,接下来可以进行全编译,然后将代码下载到开发板里了。
5、下载程序到开发板进行调试
由于之前已经将管脚分配好了,所以等待编译成功之后,打开Programmer直接下载程序即可。下载成功后,初始状态数码管显示6个0,顺序输入数字、操作符、数字、等号,即可得到正确的结果,继续按下数字或者操作符,可以进行下一次运算。
3
今天工作总结经过5天的努力,终于完成了可以实现基本功能的6位计算器了,赶快享受一下劳动成果吧!
今天通过上机仿真,不断地代码修改,逐步掌握了电路构造的思想,理解了模块划分,逐块调试和整合调试的概念。回顾一下今天所做的工作和学到的内容总结如下:
1)计算模块及其测试代码的编写。
2)多个模块之间数据和控制连接的思路。
3)通过观察RTL Viewer学会如何查看综合生成的逻辑电路。
4) 修改了key2bcd模块,增加了一个运算结果输入接口。
5)通过result接口,可将运算结果result从binary2bcd模块输出至key2bcd模块中的BCDa寄存器,直接输出display模块显示。
到此为止,我已达到了夏老师提出的最基本要求。从明天开始,要对这个计算器进行改进,包括占用逻辑资源的优化、运算功能的添加、显示效果的美化(消去有效数字前面的0)、连续运算等。
4
夏老师评述赵然同学今天完成的工作非常出色,计算器四则运算的功能基本实现了。当然在资源消耗方面尚未做深入思考。他之所以能顺利地完成今天的工作,跟他前四天的工作基础是不能分割的。今天通过综合和布局布线后下载的模块calc2中,大部分是前几天经过测试成熟的模块。键盘扫描分析模块keyscan0不用做任何修改,显示模块display3、数制转换模块bcd2bin0和bin2bcd0也不用做任何修改,计算模块alu0与教材上的例子相似;计算控制状态机key2bcd1是在第三天编写的key2bcd0基础上修改的。该模块是赵然同学在彻底理解了老师课堂上介绍的gewshiwbaiw模块原理的基础上,自己动手编写的。key2bcd1模块思路巧妙,可以实现连续计算操作。该计算控制状态机的成功是赵然同学今天工作顺利的主要原因。首先他把数字0~9的的输入和四则运算操作符号的输入分别用不同的状态记录,他还把等号的输入与四则运算操作符、0~9数字的输入区分开,用不同状态记录,以此来管理参与运算数据的位数确定、中间结果的寄存和最后运算结果的确认。最后他还把计算结果输入到该状态机中的BCDa寄存器,使得显示模块不必做任何修改,而且还能实现连续计算,一箭双雕。他之所以能写出这个状态机,与他勤于思考,较深刻地理解状态的含义和状态在控制中的作用有关,也跟他敢于实践,勇于尝试新的途径 、不怕失败、相信自己有关。
下期预告:
第6天——可进行连续运算的状态机改进
往期回顾
10天学会四则运算小计算器设计之第4天
姿势已摆好
就等你点啦
你们的“在看”是小编连载的动力喔 ?