期末课设计,在这里记录一下开发思路。
1.摘要:
这是一款基于BASYS3开发板的秒表装置,其功能有:通过七段数码管显示当前秒表秒数,通过按键选择向上计时和向下计时,通过按键切换模式,实现左二数码管显示分钟,右二数码管显示秒和左二数码管显示秒,右二数码管显示百分之一秒两种模式的切换。设计亮点有,一、利用时钟使能得到不同的时钟而不是使用时钟分频来获得不同的时钟,提高了系统稳定性。二、显示界面符合实际。三、资源使用量少。
关键词:BASYS3 七段数码管 时钟使能
2.设计思路介绍
首先设计一个百分之一秒的时钟使能模块,其能记录主时钟上升沿并每百分之一秒输出一个使能信号,然后通过级联的方式,将百分之一秒使能信号作为一秒时钟使能模块的时钟触发信号,一秒时钟使能模块每秒输出秒使能信号,将秒使能信号作为分钟时钟使能模块的时钟触发信号,实现百分之一秒、一秒、一分钟定时器的设计。然后分别将三者的时钟使能信号传到三个计数器用于记录各个计时器使能信号的个数,接着通过一个选择器将需要显示的计数器的计数值传到显示模块,在显示模块中通过除数和取余的操作得出计数值的个位和十位,然后通过BCD译码器将相应信号给到相应数码管,同时设计一个200Hz的位选频率发生器,实现数码管的扫描显示。
(注:为了方便展示,这里并未给出reset信号和stop信号的功能,但实际项目中具有该功能)
3.模块介绍
3.1时钟模块
3.1.1 时钟分频和时钟使能模块介绍
板子上的晶振频率为100MHZ,但我们需要的信号是百分之一秒,即100Hz的时钟信号(给百分之一秒提供时钟信号)。实现的方法有两种,一种是使用分频器的方式,一种是使用时钟使能的方式。分频方式即在目标频率的前半个周期让时钟电平为正,后半个周期反转电平信号(以偶分频为例),此项目通过计算可知当计数100MHz的时钟的上升沿499999次时,100Hz的时钟信号取反,即可获得100Hz的时钟频率。而时钟使能方法是指在通过计数器记录时钟的次数,当计数器达到目标次数时输出一个使能信号,以此项目为例,当计数100MHz时钟信号999999次后输出使能信号,便可获得100Hz的信号。
时钟分频ASM图 时钟使能ASM图
3.1.2时钟分频和时钟使能的选择
在很多设计中,虽然内部不同模块的处理速度不同,但由于这些时钟是同源的,可以将它们转化为单一时钟处理;在ASIC中可以通过STA约束让分频始终和源时钟同相,但FPGA由于器件本身和工具的限制,分频时钟和源时钟的Skew不容易控制(使用锁相环分频是个例外),难以保证分频时钟和源时钟同相,因此推荐的方法是使用时钟使能,通过使用时钟使能可以避免时钟“满天飞”的情况,进而避免了不必要的亚稳态发生,在降低设计复杂度的同时也提高了设计的可靠性。[1]
3.1.3 时序测试:
1.时钟分频时序图
可见其达到计数值后翻转电平
注:由于测试需要,这里将计数初值设为4,
2.时钟使能时序图
可见其只有在计数值到达时会进行使能输出,其余时间输出为0
注:由于测试需要,这里将计数初值设为9,
3.总定时器时序
可见可以通过三者级联实现预设功能。
3.1.4 时钟使能模块代码
module clock_enable(
input wire clock,
input wire reset,
input wire stop,
input wire [31:0] number,
output reg enable
);
reg [31:0] counter;
always @(posedge clock or posedge reset or posedge stop)
begin
if (reset) begin //reset信号判断
counter <= 0; //有效清零
enable <= 0;
end
else if (stop) begin //stop信号判断
counter <= counter; //有效保持
enable <= enable;
end
else begin
if (counter >= number) begin //如果计数值大于给定值,使能输出
counter <= 0;
enable <= 1;
end
else begin
counter <= counter + 1; //未达到计数值,继续
enable <= 0;
end
end
end
endmodule
3.2 计数器模块
3.2.1 模块介绍
为了记录定时器记录的时间并把提供数码管将要显示的值,我这里使用三个计数器分别上面提到的三个计数器计数,以得到相应的时间。计数器内部设有进位判断逻辑,限制s、min的值在59内,百分之一秒的值在99内
计数器ASM图
3.2.2 时序测试
计数器时序图
可见其工作正常,每一个使能信号到达时,对于时钟计数器计数加1.
3.2.3 计数器代码
module display_number(
input wire clock,
input wire reset,
input wire stop,
input wire en_ms,
input wire up,
output reg [6:0] displayed_number
);
always @(posedge clock or posedge reset or posedge stop)
begin
if (reset)
displayed_number <= 0; //计数器清零
else if (stop)
displayed_number <= displayed_number; //计数器保持
else
begin
if (en_ms) begin //判断是否为百分之一秒计数,控制其值在100以下
case(up)
1'b1:
if(displayed_number>=99)
displayed_number <= 0;
else displayed_number <= displayed_number + 1'b1; //向上计数
1'b0:if(displayed_number<=0)
displayed_number <= 99;
else displayed_number <= displayed_number - 1'b1; //向下计数
default:displayed_number <= displayed_number;
endcase
end
else //若不是百分之一秒计数,控制值在60以下
case(up)
1'b1:
if(displayed_number>=59)
displayed_number <= 0;
else displayed_number <= displayed_number + 1'b1;//向上计数
1'b0:if(displayed_number<=0)
displayed_number <= 59;
else displayed_number <= displayed_number - 1'b1;//向下计数
default:displayed_number <= displayed_number;
endcase
end
end
endmodule
3.3 位选模块
3.3.1动态扫描原理介绍
动态扫描显示利用了时分原理和人的视觉暂留现象。例如,4位扫描数码显示.器将时间划分为4个扫描周期: 周期1一>周期2一>周期3一>周期4,每个周期只选通一位数据。在周期1显示第1个数码,周期2显示第2个数码管。在扫描四个周期后,又重新按顺序循环。如果扫描的速度足够快,人的感觉就象4个数码同时显示。
3.3.2模块设计
本项目数码管有四个,所以我定义了一个两位宽的位选信号LED_activating_counter来进行位选,这个位选模块的原理就是利用寄存器refresh的长度进行计时,这里refresh为18位寄存器,每1/100MHz*2^18次方刷新一次,即2.6ms刷新一次
3.3.3模块代码
always @(posedge clock_100Mhz or posedge reset) //refresh为18位寄存器
begin
if(reset==1)
refresh_counter <= 0;
else
refresh_counter <= refresh_counter + 1;
end
assign LED_activating_counter = refresh_counter[17:16];//每1/100MHz*2^18次方刷新一次,即2.6ms刷新一次
3.4取值模块
3.4.1取值原理介绍
因为数据为整型存储,将十位数除以10便能得到10位,以10取余便能得到个位,以27为例,除以10整数剩2,取出十位,以10取余得到7,取出个位
3.4.2 模块设计
首先利用选择器将需要显示的时间的计数器值传入取值模块中,然后取值模块通过取余和除10的方法获得计数值的个位和十位,然后将其值存到LED_BCD寄存器中,为后续的显示电路提供BCD码。
取值模块AMS图
3.4.3时序测试:
模块时序图
可看出段选信号变换正常。
(注:由于测试需要,这里测试时间只给了1ms,而位选信号变换是2.6ms,所以这里位选值为0是正常的)
多路选择器代码
always @(posedge clock_100Mhz)
begin
if(choice==1) //choice为1就数码管低两位显示百分之一秒,高两位显示秒。
begin temp_display[7:0]<= displayed_number_10ms;
temp_display[15:8]<=displayed_number_s;end
Else //否则数码管低两位显示秒,高两位显示分钟。
begin temp_display[7:0]<= displayed_number_s;
temp_display[15:8]<=displayed_number_min;end
End
取值模块代码
module display(
input wire [1:0] LED_activating_counter,
input wire [15:0] displayed_number,
output reg [3:0] Anode_Activate,
output reg [6:0] LED_BCD
);
always @(*)
begin
case (LED_activating_counter)
2'b00: begin
Anode_Activate = 4'b0111;
// 数码管1亮
LED_BCD = displayed_number[15:8]/10;
// 取高八位的十位
end
2'b01: begin
Anode_Activate = 4'b1011;
// 数码管2亮
LED_BCD = displayed_number[15:8]%10;
// 取高八位的个位
end
2'b10: begin
Anode_Activate = 4'b1101;
// 数码管3亮
LED_BCD = displayed_number[7:0]/10;
// 取低八位的十位
end
2'b11: begin
Anode_Activate = 4'b1110;
// 数码管4亮
LED_BCD = displayed_number[7:0]%10;
// 取低八位的个位
end
endcase
end
endmodule
3.5数码管显示模块
3.5.1七段数码管显示原理
数码管是一种半导体发光器件,其基本单元是发光二极管。数码管按段数分为七段数码管和八段数码管,八段数码管比七段数码管多一个发光二极管单元(多一个小数点显示);按发光二极管单元连接方式分为共阳极数码管和共阴极数码管。共阳数码管是指将所有发光二极管的阳极接到一起形成公共阳极(COM)的数码管。共阳数码管在应用时应将公共极COM接到+5V,当某一字段发光二极管的阴极为低电平时,相应字段就点亮。当某一字段的阴极为高电平时,相应字段就不亮。共阴数码管是指将所有发光二极管的阴极接到一起形成公共阴极(COM)的数码管。共阴数码管在应用时应将公共极COM接到地线GND上,当某一字段发光二极管的阳极为高电平时,相应字段就点亮。当某一字段的阳极为低电平时,相应字段就不亮。[2]
四位数码管引脚图
BCD-七段显示译码器
实验中运用到的数码管为八段共阳极数码管,当小数点对应的发光二极管为高电平时,小数点不显示。
3.5.2 七段数码管显示代码:
always @(*)
begin
case (LED_BCD_change)
4'b0000: LED_out = 7'b0000001; // "0"
4'b0001: LED_out = 7'b1001111; // "1"
4'b0010: LED_out = 7'b0010010; // "2"
4'b0011: LED_out = 7'b0000110; // "3"
4'b0100: LED_out = 7'b1001100; // "4"
4'b0101: LED_out = 7'b0100100; // "5"
4'b0110: LED_out = 7'b0100000; // "6"
4'b0111: LED_out = 7'b0001111; // "7"
4'b1000: LED_out = 7'b0000000; // "8"
4'b1001: LED_out = 7'b0000100; // "9"
default: LED_out = 7'b0000001; // "0"
endcase
end
`timescale 1ns/1ns
module Seven_segment_LED_Display_Controller_Test();
reg clock_100Mhz;
reg reset;
reg stop;
reg [1:0] choice;
reg up_down_choice;
wire [3:0] Anode_Activate;
wire [6:0] LED_out;
Seven_segment_LED_Display_Controller dut (
.clock_100Mhz(clock_100Mhz),
.reset(reset),
.stop(stop),
.choice(choice),
.up_down_choice(up_down_choice),
.Anode_Activate(Anode_Activate),
.LED_out(LED_out)
);
// Testbench code
initial begin
// Initialize inputs
clock_100Mhz = 0;
reset = 1;
stop = 0;
choice = 0;
up_down_choice=1;
#10 reset=0;// Test with 10ms time scale
#100000 up_down_choice=0;
#100010 reset=1;
#100020 reset=0;
#100030 stop=1;
#100040 stop=0;
//up_down_choice = 0;
end
always #10 clock_100Mhz=~clock_100Mhz;
endmodule
可看出其工作输出正常
//下排按键左起第一个控制reset信号,第二个控制stop信号,第三个模式切换,第四个向上向下计数模式切换
# Clock signal
set_property PACKAGE_PIN W5 [get_ports clock_100Mhz]
set_property IOSTANDARD LVCMOS33 [get_ports clock_100Mhz]
set_property PACKAGE_PIN R2 [get_ports reset]
set_property IOSTANDARD LVCMOS33 [get_ports reset]
set_property PACKAGE_PIN T1 [get_ports stop]
set_property IOSTANDARD LVCMOS33 [get_ports stop]
set_property PACKAGE_PIN U1 [get_ports choice]
set_property IOSTANDARD LVCMOS33 [get_ports choice]
set_property PACKAGE_PIN W2 [get_ports up_down_choice]
set_property IOSTANDARD LVCMOS33 [get_ports up_down_choice]
#seven-segment LED display
set_property PACKAGE_PIN W7 [get_ports {LED_out[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[6]}]
set_property PACKAGE_PIN W6 [get_ports {LED_out[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[5]}]
set_property PACKAGE_PIN U8 [get_ports {LED_out[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[4]}]
set_property PACKAGE_PIN V8 [get_ports {LED_out[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[3]}]
set_property PACKAGE_PIN U5 [get_ports {LED_out[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[2]}]
set_property PACKAGE_PIN V5 [get_ports {LED_out[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[1]}]
set_property PACKAGE_PIN U7 [get_ports {LED_out[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED_out[0]}]
set_property PACKAGE_PIN U2 [get_ports {Anode_Activate[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Anode_Activate[0]}]
set_property PACKAGE_PIN U4 [get_ports {Anode_Activate[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Anode_Activate[1]}]
set_property PACKAGE_PIN V4 [get_ports {Anode_Activate[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Anode_Activate[2]}]
set_property PACKAGE_PIN W4 [get_ports {Anode_Activate[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Anode_Activate[3]}]
现象正确,功能正常,可以实现清零,暂停,计时模式切换,上下计数模式切换
实验总结
本项目使用时钟使能信号对不同时间需求进行计数,效果很好,实现了预期功能,计时功能与手机计时器对比误差在可接受范围内,实验成功。
实验心得:这次实验是第一次进行这样大规模的硬件语言代码编写,遇到了很多问题,也对项目建设有了更深的理解。
- 体会到了寄存器位宽对工程的影响,有时一个位宽设置错误会导致很多预料不到的错误,需要花很多时间取排查,以后在位宽设置时一定会注意。
- 对阻塞赋值和非阻塞赋值有了更深入的理解,赋值方式错了真的会造成很多意外的bug。
- 设计时序逻辑时一定要清晰,不然实现不了功能也找不到原因。
- 分模块化编程可以使代码编写更加直观,也使逻辑更加清晰,同时增加了代码的复用性,减少了重复性工作。
参考文献
- verilog的时钟分频与时钟使能https://www.elecfans.com/d/1972527.html
- 七段数码管 知乎盐选 | Arduino 编程 (zhihu.com)