前言
相比之前的秒表,这个题目的难度略有提升,虽然总体架构还是基于计数器的设计,但是需要添加其他的模块,还是有些挑战性的。
在代码实现部分会给出设计理念和分析,整体资源可以直接下载压缩包(手机端依然看不到,还是不知道为什么)。
题目需求及分析
需求
(1) 可以进行不同分值的得分计数;
(2) 用LED等表示裁判给出的犯规类型;
(3) 可以显示当前领先队伍编号;
(4) 用循环彩灯设计啦啦队加油信号。
分析
1、可以进行不同分值的得分计数,同时能实现两组分数的显示
1,2,3三种得分,三个key1,2,3对应,按一下加一次分数;
通过sw1,sw2的开关来分别显示两队的分值;
补充一下,本来想的是开则显示,关则不显示,但是这样会出现11的无效状态,00的状态也没有运用起来,比较浪费,所以这里扩展一下,合并3,sw1,2=00时清零分数,10时显示红队,01时显示蓝队,11时显示优先的队伍标号;
2、用LED等表示裁判给出的犯规类型
由于key键只有4个,所以这里需要分配一个sw,避免与得分冲突,3个key对应三种犯规,用三色灯表示;
3、可以显示当前领先队伍编号
sw分配一个,打开显示计数高的那一个;
4、用循环彩灯设计啦啦队加油信号
打开就启动流水灯(啦啦队加油没有限制);
5、辅助功能
显然对于按键,需要有按键消抖的功能;显示分数则需要数码管驱动模块;而关于时序电路中必不可少的分频器也是需要的;
代码实现
由于CSDN编辑文章工具中没有VDL语言的设置,这里就用C++来显示代码了(纯黑实属难看)。
1. 得分模块
(1) 计分及显示:scorer.v
module scorer(
input clk,
input rst, //重置分数键
input getscore_1, //得1分,同时在sw1,2=11时,控制R灯
input getscore_2, //得2分,同时在sw1,2=11时,控制G灯
input getscore_3, //得3分,同时在sw1,2=11时,控制B灯
//功能选择端,sw1,2=00时不工作,10时显示一队,01时显示二队,11时显示优先的队伍序号
input sw1,
input sw2,
input sw3,
input sw4,
output [8:0] segment_led_1,segment_led_2, //数码管输出
output [7:0] led,
output R_led,
output G_led,
output B_led
);
reg [7:0] r_score; //内部信号:红队分数
reg [7:0] b_score; //内部信号:蓝队分数
reg [7:0] display; //显示输出
//得分模块
always@(posedge clk_500hz)begin
//sw1,2=00,此时红队蓝队分数全部清零,显示FF,作为reset态
if(sw1 == 1'b0 && sw2 == 1'b0)begin
r_score[7:0] <= 8'h00;
b_score[7:0] <= 8'h00;
display[7:0] = 8'hff;
end
//sw1,2=10,此时显示红队分数,可以改变红队分数
else if(sw1 == 1'b1 && sw2 == 1'b0)begin
//清零
if(!rst)begin
r_score[7:0] <= 8'h00;
end
//得1分
else if(!getscore_1 && key_done)begin
if(r_score[3:0] < 4'd9)begin
r_score[3:0] <= r_score[3:0]+2'd1;
r_score[7:4] <= r_score[7:4];
end
else if(r_score[3:0] == 4'd9)begin
r_score[3:0] <= 4'h0;
r_score[7:4] <= r_score[7:4]+4'h1;
end
end
//得2分
else if(!getscore_2 && key_done)begin
if(r_score[3:0] < 4'd8)begin
r_score[3:0] <= r_score[3:0]+2'd2;
r_score[7:4] <= r_score[7:4];
end
else if(r_score[3:0] == 4'd8)begin
r_score[3:0] <= 4'h0;
r_score[7:4] <= r_score[7:4]+4'h1;
end
else if(r_score[3:0] == 4'd9)begin
r_score[3:0] <= 4'h1;
r_score[7:4] <= r_score[7:4]+4'h1;
end
end
//得3分
else if(!getscore_3 && key_done)begin
if(r_score[3:0] < 4'd7)begin
r_score[3:0] <= r_score[3:0]+2'd3;
r_score[7:4] <= r_score[7:4];
end
else if(r_score[3:0] == 4'd7)begin
r_score[3:0] <= 4'h0;
r_score[7:4] <= r_score[7:4]+4'h1;
end
else if(r_score[3:0] == 4'd8)begin
r_score[3:0] <= 4'h1;
r_score[7:4] <= r_score[7:4]+4'h1;
end
else if(r_score[3:0] == 4'd9)begin
r_score[3:0] <= 4'h2;
r_score[7:4] <= r_score[7:4]+4'h1;
end
end
//将红队分数赋值给显示
display[7:0] = r_score[7:0];
end
//sw1,2=01,此时显示蓝队分数,可以改变蓝队分数
else if((sw1 == 1'b0) & (sw2 == 1'b1))begin
//清零
if(!rst)begin
b_score[7:0] <= 8'h00;
end
//得1分
else if(!getscore_1 && key_done)begin
if(b_score[3:0] < 4'd9)begin
b_score[3:0] <= b_score[3:0]+2'd1;
b_score[7:4] <= b_score[7:4];
end
else if(b_score[3:0] == 4'd9)begin
b_score[3:0] <= 4'h0;
b_score[7:4] <= b_score[7:4]+4'h1;
end
end
//得2分
else if(!getscore_2 && key_done)begin
if(b_score[3:0] < 4'd8)begin
b_score[3:0] <= b_score[3:0]+2'd2;
b_score[7:4] <= b_score[7:4];
end
else if(b_score[3:0] == 4'd8)begin
b_score[3:0] <= 4'h0;
b_score[7:4] <= b_score[7:4]+4'h1;
end
else if(b_score[3:0] == 4'd9)begin
b_score[3:0] <= 4'h1;
b_score[7:4] <= b_score[7:4]+4'h1;
end
end
//得3分
else if(!getscore_3 && key_done)begin
if(b_score[3:0] < 4'd7)begin
b_score[3:0] <= b_score[3:0]+2'd3;
b_score[7:4] <= b_score[7:4];
end
else if(b_score[3:0] == 4'd7)begin
b_score[3:0] <= 4'h0;
b_score[7:4] <= b_score[7:4]+4'h1;
end
else if(b_score[3:0] == 4'd8)begin
b_score[3:0] <= 4'h1;
b_score[7:4] <= b_score[7:4]+4'h1;
end
else if(b_score[3:0] == 4'd9)begin
b_score[3:0] <= 4'h2;
b_score[7:4] <= b_score[7:4]+4'h1;
end
end
//将蓝队分数赋值给显示
display[7:0] = b_score[7:0];
end
//sw1,2=11,此时显示分数优先的队伍的序号,同时保持分数不变
else if(sw1 == 1'b1 && sw2 == 1'b1)begin
r_score[7:0] <= r_score[7:0];
b_score[7:0] <= b_score[7:0];
if(r_score[7:0] < b_score[7:0])begin //蓝队领先,显示01
display[7:0] = 8'h01;
end
else if(b_score[7:0] < r_score[7:0])begin //红队领先,显示10
display[7:0] = 8'h10;
end
else //打平,显示11
display[7:0] = 8'h11;
end
end
//例化数码管显示模块
segment
(
.seg_data_1 (display[7:4]), //seg_data input
.seg_data_2 (display[3:0]), //seg_data input
.seg_led_1 (segment_led_1), //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
.seg_led_2 (segment_led_2) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
//例化消抖module
wire key_done; //有按键按下
debounce //消抖模块
(
.clk (clk),
.rst_n (rst),
.key_in1 (getscore_1),
.key_in2 (getscore_2),
.key_in3 (getscore_3),
.clk_500hz (clk_500hz),
.key_done (key_done)
);
//实例化流水灯
flashled
(
.clk (clk),
.rst (rst),
.sw3 (sw3), //控制开关
.led (led)
);
//实例化三色灯
RGB_LED
(
.clk (clk),
.rst (rst),
.sw4 (sw4),
.key_R (getscore_1),
.key_G (getscore_2),
.key_B (getscore_3),
.R_led (R_led),
.G_led (G_led),
.B_led (B_led)
);
endmodule
(2) 按键消抖:debounce.v
module debounce
(
input clk , //时钟
input rst_n , //复位键
input key_in1, //对应得1分键
input key_in2, //对应得2分键
input key_in3, //对应得3分键
output reg clk_500hz, //分频出的500Hz时钟脉冲信号(该板使用的是12M晶振)
output key_done //按键按下动作完成标志
);
reg [25:0]div_cnt; //分频计数器
always@(posedge clk or negedge rst_n)begin //获得500Hz时钟脉冲信号
if(!rst_n)begin
div_cnt <= 0;
clk_500hz <= 0;
end
else if(div_cnt == 1999)begin //计数两千次反转状态
div_cnt <= 0;
clk_500hz <= ~clk_500hz;
end
else begin
div_cnt <= div_cnt + 1'b1;
clk_500hz <= clk_500hz;
end
end
reg qout;
reg key_tmp1,key_tmp2;
parameter n = 10;
reg [25:0] cnt;
always@(posedge clk_500hz or negedge rst_n) begin
if(!rst_n) begin
cnt <= 0;
qout <= 0;
end
else if(key_in1==0 || key_in2==0 || key_in3==0) begin //三个按键任意一个按下
if(cnt == n-1) begin //持续2ms的话判定按下
cnt <= cnt;
qout <= 1;
end
else begin
cnt <= cnt+1;
qout <= 0;
end
end
else begin
qout <= 0;
cnt <= 0;
end
end
//提取前后按键信号
always@(posedge clk_500hz or negedge rst_n) begin
if(!rst_n) begin
key_tmp1 <= 0;
key_tmp2 <= 0;
end
else begin
key_tmp1 <= qout;
key_tmp2 <= key_tmp1;
end
end
assign key_done = key_tmp1 & (~ key_tmp2);
endmodule
(3) 数码管驱动:segment.v
module segment
(
input wire [3:0] seg_data_1, //四位输入数据信号
input wire [3:0] seg_data_2, //四位输入数据信号
output wire [8:0] seg_led_1, //数码管1,MSB~LSB = SEG,DP,G,F,E,D,C,B,A
output wire [8:0] seg_led_2 //数码管2,MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
reg[8:0] seg [15:0]; //存储7段数码管译码数据
initial
begin
seg[0] = 9'h3f; // 0
seg[1] = 9'h06; // 1
seg[2] = 9'h5b; // 2
seg[3] = 9'h4f; // 3
seg[4] = 9'h66; // 4
seg[5] = 9'h6d; // 5
seg[6] = 9'h7d; // 6
seg[7] = 9'h07; // 7
seg[8] = 9'h7f; // 8
seg[9] = 9'h6f; // 9
seg[10]= 9'h77; // A
seg[11]= 9'h7C; // b
seg[12]= 9'h39; // C
seg[13]= 9'h5e; // d
seg[14]= 9'h79; // E
seg[15]= 9'h71; // F
end
assign seg_led_1 = seg[seg_data_1];
assign seg_led_2 = seg[seg_data_2];
endmodule
2. 流水灯模块
(1) 流水灯:flashled.v
module flashled (clk,rst,led,sw3);
input clk,rst,sw3;
output [7:0] led;
reg [2:0] cnt ; //定义了一个3位的计数器,输出可以作为3-8译码器的输入
wire clk1h; //定义一个中间变量,表示分频得到的时钟,用作计数器的触发
//例化module decode38,相当于调用
decode38 u1 (
.sw(cnt), //例化的输入端口连接到cnt,输出端口连接到led
.led(led)
);
//例化分频器模块,产生一个1Hz时钟信号
divide #(.WIDTH(32),.N(12000000)) u2 ( //传递参数
.clk(clk),
.rst_n(rst), //例化的端口信号都连接到定义好的信号
.clkout(clk1h)
);
//1Hz时钟上升沿触发计数器,循环计数
always @(posedge clk1h or negedge rst)
if (!rst)
cnt <= 0;
else
cnt <= cnt +1;
endmodule
(2) 分频器:divide.v
module divide ( clk,rst_n,clkout);
input clk,rst_n; //输入信号,其中clk连接到FPGA的C1脚,频率为12MHz
output clkout; //输出信号,可以连接到LED观察分频的时钟
//parameter是verilog里常数语句
parameter WIDTH = 3; //计数器的位数,计数的最大值为 2**WIDTH-1
parameter N = 5; //分频系数,请确保 N < 2**WIDTH-1,否则计数会溢出
reg [WIDTH-1:0] cnt_p,cnt_n; //cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
reg clk_p,clk_n; //clk_p为上升沿触发时分频时钟,clk_n为下降沿触发时分频时钟
//上升沿触发时计数器的控制
always @ (posedge clk or negedge rst_n ) //posedge和negedge是verilog表示信号上升沿和下降沿
//当clk上升沿来临或者rst_n变低的时候执行一次always里的语句
begin
if(!rst_n)
cnt_p<=0;
else if (cnt_p==(N-1))
cnt_p<=0;
else cnt_p<=cnt_p+1; //计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器
end
//上升沿触发的分频时钟输出,如果N为奇数得到的时钟占空比不是50%;如果N为偶数得到的时钟占空比为50%
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_p<=0;
else if (cnt_p<(N>>1)) //N>>1表示右移一位,相当于除以2去掉余数
clk_p<=0;
else
clk_p<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
//下降沿触发时计数器的控制
always @ (negedge clk or negedge rst_n)
begin
if(!rst_n)
cnt_n<=0;
else if (cnt_n==(N-1))
cnt_n<=0;
else cnt_n<=cnt_n+1;
end
//下降沿触发的分频时钟输出,和clk_p相差半个时钟
always @ (negedge clk)
begin
if(!rst_n)
clk_n<=0;
else if (cnt_n<(N>>1))
clk_n<=0;
else
clk_n<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
assign clkout = (N==1)?clk:(N[0])?(clk_p&clk_n):clk_p; //条件判断表达式
//当N=1时,直接输出clk
//当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p
//当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多所以是相与
endmodule
(3) 38译码器:decode38.v
module decode38 (sw,led);
input [2:0] sw; //开关输入信号,利用了其中3个开关作为3-8译码器的输入
output [7:0] led; //输出信号控制特定LED
reg [7:0] led; //定义led为reg型变量,在always过程块中只能对reg型变量赋值
//always过程块,括号中sw为敏感变量,当sw变化一次执行一次always中所有语句,否则保持不变
always @ (sw)
begin
case(sw) //case语句,一定要跟default语句
3'b000: led=8'b0111_1111; //条件跳转,其中“_”下划线只是为了阅读方便,无实际意义
3'b001: led=8'b1011_1111; //位宽'进制+数值是Verilog里常数的表达方法,进制可以是b、o、d、h(二、八、十、十六进制)
3'b010: led=8'b1101_1111;
3'b011: led=8'b1110_1111;
3'b100: led=8'b1111_0111;
3'b101: led=8'b1111_1011;
3'b110: led=8'b1111_1101;
3'b111: led=8'b1111_1110;
default: ;
endcase
end
endmodule
3. 三色灯模块
(1) 三色灯驱动:RGB_LED.v
module RGB_LED (
//这里之所以不采用连续赋值的方式来点亮LED的原因是,防止在整合模块时发生管脚冲突的问题
clk,
rst,
sw4,
key_R,
key_G,
key_B,
R_led,
G_led,
B_led
);
input clk, rst, sw4, key_R, key_G, key_B;
output reg R_led, G_led, B_led;
always@(posedge clk_500hz)begin
if(sw4 == 1'b1)begin
if (!rst) //复位时led熄灭
R_led = 1;
else if(key_R == 0)
R_led = ~R_led; //按键按下时led翻转
else
R_led = R_led;
if (!rst) //复位时led熄灭
G_led = 1;
else if(key_G == 0)
G_led = ~G_led; //按键按下时led翻转
else
G_led = G_led;
if (!rst) //复位时led熄灭
B_led = 1;
else if(key_B == 0)
B_led = ~B_led; //按键按下时led翻转
else
B_led = B_led;
end
end
//例化消抖module
wire key_done; //有按键按下
debounce //消抖模块
(
.clk (clk),
.rst_n (rst),
.key_in1 (key_R),
.key_in2 (key_G),
.key_in3 (key_B),
.clk_500hz (clk_500hz),
.key_done (key_done)
);
endmodule
(2) 按键消抖:debounce.v
(同得分模块下的按键消抖)
4. 管脚配置
作者使用的板子是“10M02SCM153C8G”核心板,用其他板子的话就换对应的管脚就行。其中sw键是拨码开关,getscore和rst键是按键,led[i]是led灯,RGB_led是三色灯,clk是时钟,segment是数码管。
5. 设计过程中的一些错误和不足
1. 按照最初的构想,作者是想要做如下图的一个结构的,顶层文件control.v做控制端,所有的子模块在该文件中都是例化的情况。
但是在实现过程中,在control.v文件实例化scorer.v时会出现如下报错,目前暂时不知道原因。处于“节省时间”的理由,转而做成了现在结构,即以scorer.v为顶层文件,在scorer.v中实例化三色灯和流水灯模块。
Error (10159): Verilog HDL error at control.v(22): "scorer" is not a task or void function
2. 如上文所言,在该项目中我们是无法控制流水灯的,但原本在设计时本来是准备用一个sw的开关来控制流水灯的开关。若在RGB_LED.v文件中添加如下控制代码:
if(sw3 == 1'b0)
//RGB_LED.v中always下的代码
则会出现如下报错:在verilog文件中使用按键判断时出现报错。
大概原因是quartus2中不允许出现“不完整的闭环结构”。这一点大概是代码的逻辑问题,但是目前也没有找出来具体是什么原因,所以干脆直接把控制端给删了,让流水灯一直亮着(啦啦队一直加油也没什么不对吧doge)。
Error (10200): Verilog HDL Conditional Statement error at flashled.v(27): cannot match operand(s) in the condition to the corresponding edges in the enclosing event control of the always construct
6. 可优化
显然,以上代码实现的只是实现了题目的基本要求,在此基础上还可以添加许多功能。sw_1,2,3,4四个拨码开关作为工作状态控制端,最多可以有16种功能。像篮球比赛中的24s倒计时,12min倒计时;大比分记录这些功能都可以加进去。
关于计时部分功能的添加,可以参考这个博主的,写的很好,架构也很清晰。
除去外加功能,已有功能也还有许多可以改进的地方,如:1. 裁判判决模块还可以加一个各种牌的累计数;2. 流水灯显然可以多几种样式,正传、反转、向中间和向两边都是可以的。
最后说一些关于项目开发的事儿,一个合格的项目,不仅不能有报错,warning也应该尽可能的少,但显然作者没能做到。
后记
一些感悟
1. 首先的首先,在一个完整的项目开发过程中,烧录前是有仿真测试的。这一步的目的在于:通过仿真波形来检查有没有什么地方通过了编译但是不符合使用要求的。比如在这次开发中,浪费了作者将近2个小时的一个点是:scorer.v中always用的时钟是直接接的clk,而非分频后的时钟,于是在烧录完后就出现了按下任意加分键,分数都会随机增加。其根本原因就是clk周期是1um,按一下的时间显然不会次次相同,所以增加的分数是随机的。但是话又说回来,很惭愧的是,最后发现问题也不是通过仿真找出来的,而是在上课途中突然灵光乍现,然后回来换了个时钟就解决了......
2. 这次项目开发对于个人而言是一个很特殊的经历。在往常的项目开发中,作者秉持着优先系统学习开发工具,在熟练运用后再来投入实践。诚然,这种方法的好处是知识体系健全,开发能力牢固,但相应的需要很长的学习周期,对于这个万物日新月异的时代来说不是一个好的方法。反而这次对于FPGA的学习中,“以需求为导向”的学习方式是个更好的方法,遇到什么不会的就去查什么,慢慢的一点一点减少未知,最后下来差不多也算是掌握如何使用了。
3. 分模块开发这一习惯一定要好好养成,完整来讲,以上代码都是第3还是第4版了,最初是按照得分模块(下面还有单队得分和双队得分两个版本),流水灯模块和三色灯模块分别开发的,最后经过整合后才做成了现在这个样子。所以在“三色灯驱动”中有写为什么不用数组来存状态,就是在整合中发现如果单独整出个数组,配置管脚时就会冲突,所以改成了单个按键控制单个灯。但总的来说,这种先开发各个小模块,最后整合的思路是极好的。
时间节点留言
2023.12.13
好好好历时12个小时,终于给做出来了。好的方面讲,实现了基本功能,至少是一个符合要求的,按照规定能够使用的篮球比赛计分器;坏的方面讲,其实还有很多地方时作者不太满意的,时间充裕的情况下应该好好优化的,比如:顶层控制文件,又比如流水灯的控制端等等......
但是把,还有一个月就要期末考了,作者那成绩属实是惨不忍睹,雪上加霜的是,一边还有个“智能储物柜系统”没整完,所以这个项目就只能草草了事了,实在是惭愧。
关于代码和其他感悟,应该能在明天晚上发出来,到时候会上传全部文件的,需要的可以自取。
2023.12.14
还差一点,下午就能出来。
2023.12.14
暂时就写这么多吧,之后有感悟了再回来补充。