芯航线——普利斯队长精心奉献
实验目的:1.掌握BCD码的原理、分类以及优缺点
2.设计一个多位的8421码计数器并进行验证
3.学会基本的错误定位以及修改能力
实验平台:无
实验原理:
BCD码(Binary-Coded Decimal)又被称为二进码十进数、二-十进制代码是一种十进制的数字编码,用4位二进制数来表示十进制数中的0~9个十个数之一。BCD编码又可以分成有权码和无权码两种,其中有权码如:8421码、2421码以及5421等;无权码如:余3码、格雷码以及余3循环码等。
BCD码中最常用的是8421码,其四个bit权值分别是8,4,2,1;同理5421码各位的权依次为5,4,2,1,5421码特点是最高位连续5个0后连续5个1,故其当计数器采用这种编码时,最高位可产生对称方波输出;余3码是在8421码上加加0011的出来的;码格雷码的特点是任意两个相邻的代码只有一位二进制数不同,编码格式不唯一;余3循环具有格雷码的特点还具有编码的首尾可以连接来进行循环,这样可用反馈移位寄存器来实现,硬件实现简单。下面表6-1给出常见的几种编码格式:
十进制数 | 8421码 | 余3码 | 2421码 | 5421码 | 格雷码 | 余3循环码 |
0 | 0000 | 0011 | 0000 | 0000 | 0000 | 0010 |
1 | 0001 | 0100 | 0001 | 0001 | 0001 | 0110 |
2 | 0010 | 0101 | 0010 | 0010 | 0011 | 0111 |
3 | 0011 | 0110 | 0011 | 0011 | 0010 | 0101 |
4 | 0100 | 0111 | 0100 | 0100 | 0110 | 0100 |
5 | 0101 | 1000 | 1011 | 1000 | 0111 | 1100 |
6 | 0110 | 1001 | 1100 | 1001 | 0101 | 1101 |
7 | 0111 | 1010 | 1101 | 1010 | 0100 | 1111 |
8 | 1000 | 1011 | 1110 | 1011 | 1100 | 1110 |
9 | 1001 | 1100 | 1111 | 1100 | 1101 | 1010 |
表6-1 常见的BCD码
在实际使用中如不特指BCD码格式均为代指8421码。通过以上介绍将十进制895转换为BCD码就是1001_1001_0101,同理若将BCD码1001_0110_0100转换为十进制数即为964。
BCD码的运算规则:BCD码是十进制数,而运算器对数据做加减运算时,都是按二进制运算规则进行处理的。这样,当将BCD码传送给运算器进行运算时,其结果需要修正。修正的规则是:当两个BCD码相加,如果和等于或小于 1001(即十进制数9),不需要修正;如果相加之和在 1010 到1111(即十六进制数 0AH~0FH)之间,则需加 'd6也就是'b0110进行修正;如果相加时,本位产生了进位,也需加 6 进行修正。下面举例说明:计算5+8,将5和8转换为8421 BCD码后输入加法器,则运算如下:0 1 0 1 + 1 0 0 0 = 1 1 0 1 结果大于9,+ 0 1 1 0 即加 6 修正得出1 0 0 1 1,补充高位为0001_0011。5+8=13,结论正确。
BCD码的主要应用之一就是数码管,假设我们要将十进制数158显示,一般解决办法是先要除法运算158/100= 1得出百位,再取余158%100 = 58后继续进行除法运算58 / 10 = 5得出十位,再进行一次取余158%10 = 8,得到个位。以上过程可以看出需要除法,但是由于除法运算是比较消耗计算时间导致整体需要的指令周期太久。但是如果我们先将其转换为BCD码,则可大幅度减少运算时间。具体例子会在数码管一讲详细介绍。
实验步骤:
按照02章所讲,建立工程子文件夹后,新建一个以名为BCD_Counter的工程保存在prj下,并在本工程目录的rtl文件夹下新建verilog file文件在此文件下输入以下内容并以BCD_Counter.v保存。
module BCD_Counter(Clk, Cin, Rst_n, Cout, q);
input Clk;//计数基准时钟 input Cin; //计数器进位输入 input Rst_n; //系统复位
output reg Cout; //计数进位输出 output [3:0]q; //计数值输出
reg [3:0]cnt; //定义计数器寄存器
//执行计数过程 always@(posedge Clk or negedge Rst_n) if(Rst_n == 1'b0) cnt <= 4'd0; else if(Cin == 1'b1)begin if(cnt == 4'd9) cnt <= 4'd0; else cnt <= cnt + 1'b1; end else cnt <= cnt;
//产生进位输出信号 always@(posedge Clk or negedge Rst_n) if (!Rst_n) Cout <= 1'b0; else if(Cin == 1'b1 && cnt ==4'd9) Cout <= 1'b1; else Cout <= 1'b0;
assign q = cnt;
endmodule |
进行分析和综合直至没有错误以及警告。
为了测试仿真编写测试激励文件,新建BCD_Counter_tb.v文件保存到testbench文件夹下,输入以下内容再次进行分析和综合直至没有错误以及警告。本激励文件除产生正常的时钟以及复位信号外,还生成了重复30次的占空比为1:5周期为120ns的cin信号。
`timescale 1ns/1ns
`define clock_period 20
module BCD_Counter_tb;
reg Clk; reg Cin; reg Rst_n;
wire Cout; wire [3:0]q;
BCD_Counter BCD_Counter0( .Clk(Clk), .Cin(Cin), .Rst_n(Rst_n), .Cout(Cout), .q(q) );
initial Clk = 1'b1; always#(`clock_period/2) Clk = ~Clk;
initial begin Rst_n = 1'b0; Cin = 1'b0; #(`clock_period*200); Rst_n = 1'b1; #(`clock_period*20); repeat(30)begin Cin = 1'b1; #`clock_period; Cin = 1'b0; #(`clock_period*5); end #(`clock_period*20); $stop; end
endmodule |
设置好仿真脚本后进行功能仿真,可以看到如图6-1所示的波形文件,可以看出在复位信号置高后,每当进位输入信号cin为高时计数值输出q完成一次自加,直到计数值为9后清零重新计数并产生进位信号。
图6-1 功能仿真波形图
现在以上面的BCD计数器为基础设计级联的多位BCD计数器,这里我们将计数器位数设置为12,即3个BCD计数器级联既可以实现。新建verilog file文件在此文件下输入以下内容并以BCD_Counter_top.v保存至rtl文件夹下。本文件实现了例化与调用BCD_counter.v文件并将进位信号根据需要连接。
module BCD_Counter_top(Clk, Cin, Rst_n, Cout, q);
input Clk;//计数基准时钟 input Cin; //计数器进位输入 input Rst_n; //系统复位
output Cout; //计数进位输出 output [11:0]q; //计数值输出
wire Cout0,Cout1; wire [3:0]q0,q1,q2;
assign q = {q2,q1,q0};
BCD_Counter BCD_Counter0( .Clk(Clk), .Cin(Cin), .Rst_n(Rst_n), .Cout(Cout0), .q(q0) );
BCD_Counter BCD_Counter1( .Clk(Clk), .Cin(Cout0), .Rst_n(Rst_n), .Cout(Cout1), .q(q1) );
BCD_Counter BCD_Counter2( .Clk(Clk), .Cin(Cout1), .Rst_n(Rst_n), .Cout(Cout), .q(q2) );
endmodule |
将上述的文件设置为顶层,并再次进行分析和综合直至没有错误以及警告。点击RTL viewer,可以看到图6-2的模块结构,可以看出符合预期目的。
图6-2多级BCD计数器RTL视图
为了测试仿真编写测试激励文件,新建BCD_Counter_top_tb.v文件保存到testbench文件夹下,输入以下内容再次进行分析和综合直至没有错误以及警告。本激励文件为了简化分析,复位后将cin一直置高,并延迟一定的时间。由于现在为三级BCD计数器,计数器满值为十六进制的999,特将仿真时间进行了延长至5000个时钟周期。
`timescale 1ns/1ns
`define clock_period 20
module BCD_Counter_top_tb;
reg Clk; reg Cin; reg Rst_n;
wire Cout; wire [11:0]q;
BCD_Counter_top BCD_Counter_top0( .Clk(Clk), .Cin(Cin), .Rst_n(Rst_n), .Cout(Cout), .q(q) );
initial Clk = 1'b1; always#(`clock_period/2) Clk = ~Clk;
initial begin Rst_n = 1'b0; Cin = 1'b0; #(`clock_period*200); Rst_n = 1'b1; #(`clock_period*20); Cin = 1'b1; #(`clock_period*5000); $stop; end
endmodule |
设置好仿真脚本后进行功能仿真,可以看到如图6-3所示的波形文件,可以看到进位输出信号cout在计数值q变为十六进制999后延迟了两个系统周期才有输出,不符合既定设计,即设计存在错误。
图6-3 多级BCD计数器初次功能仿真
为了定位错误,将子模块的相关信号加入到wave栏,并再次仿真查看内部数据的信息进行分析解决。
在Modelsim找到Inatance窗口找到顶层文件,如图6-4-1所示点击加号后可以看到本顶层设计调用的模块。如图6-4-2所示单击不同的模块在Object栏可以看到其端口列表,选中需要的右键Add Wave,即可将内部信号加入到波形窗口。这里我门将每个模块的计数值输出信号q以及进位输出信号cout加入到波形窗口中。
图6-4-1 添加内部信号到wave窗口
图6-4-2 添加内部信号到wave窗口
单击工具栏中的Restart来复位仿真,在弹出对话框中全选后点击OK,点击Run-All来重启仿真。
图6-5-1 复位仿真
图6-5-2 复位仿真
图6-6 重启仿真
可以看到仿真后加入内部信号的波形较乱没有层次,这里介绍一个分组操作,我们首先ctrl+A选中所有wave窗口中的波形,后ctrl+G进行分组。分组后如图6-7所示,放大局部信号可以看到照成这个原因是由于每一级的BCD计数器的进位输出信号均延迟了一个时钟周期,从而导致顶层文件进位输出cout信号输出延迟了三个时钟周期的问题,此时的计数器值已经变为了十六进制的002,而不再是999。
图6-7 加入内部信号的功能仿真波形
这样我们就定位了错误,只需将修改进位输出信号cout修改为与计数值信号q计数到9时同时输出。将进位产生信号的逻辑修改为如下,且将Cout的类型改为普通wire型。
assign Cout = (Cin == 1'b1 && cnt == 4'd9); |
再次仿真后可以看到在图6-8-1中十六进制的999时输出进位信号,符合了原定的要求,我们可以再将显示格式修改为二进制数,如图6-8-2中在计数值q为1001_1001_1001产生进位信号,通过前面所讲的BCD码原理将其转换为BCD码的格式也是999。
图6-8-1 修改后的BCD计数器波形图
图6-8-2 修改后的BCD计数器波形图
至此,就完成了一个BCD计数器的设计,并且学会了基本的调试修改能力。具体BCD计数器的板级验证,可以参考后续文档关于芯航线数码管的驱动设计。
备注:在修改设计后如果进行单独的功能仿真,会发现在图6-9中进位信号存在毛刺(glitch),这里的解决办法很多,其中之一就是在后续设计中通过预估正常信号与毛刺信号的时间宽度来进行筛选,也可以通过相关约束来解决;硬件方面可以外接滤波电容来消除其影响。
图6-9 毛刺信号