前言
这是作者室友的项目,本来不管作者事儿的,但是后来听到说是室友去网上找人花了80块买了个劣质的,不仅是从CSDN上抄的,而且使用的板子还不符合室友的要求。可叹作者心软啊,顺便给室友做了。
在代码实现部分会给出设计理念和分析,整体资源可以直接下载压缩包(手机端依然看不到,还是不知道为什么)。
题目需求及分析
需求
基于双向计数器设计一个电梯楼层显示电路
说明:
设计多层电梯楼层显示电路。电梯每经过一层,“楼层信号”输入一个可逆计数脉冲电梯上升时“上升”为高电平,“下降”为低电平,下降时相反。
要求:
1、电梯楼层数为2(至少2层)
2、楼层数需使用7位译码器显示
3、可逆计数功能需用双向计数器实现 其输入端包含上升和下降操作信号、楼层信号、校正
4、上升下降状态过程中用小彩灯灯带变化体现
分析
1. sw1作为总开关,置0清零,置1工作
2. sw2作为楼层“上升”/“下降”控制开关:置0时呈下降状态,楼层信号发生后楼层数减一;置1时呈上升状态,楼层信号发生后楼层数加一
3. led流水灯作为上升下降的具象化表示:向上流水表示上升,向下流水表示下降
4. 辅助功能:显然对于按键,需要有按键消抖的功能;显示分数则需要数码管驱动模块;而关于时序电路中必不可少的分频器也是需要的
代码实现
由于CSDN编辑文章工具中没有VDL语言的设置,这里就用C++来显示代码了(纯黑实属难看)。
1. 楼层显示模块
(1) 中控:elevator.v
module elevator(
input clk,
input rst, //重置键
input floor_signal, //楼层信号
//功能选择端
input sw1, //sw1作为总开关,置0清零,置1工作
input sw2, //sw2作为楼层“上升”/“下降”控制开关:0下1上
output [8:0] segment_led_1,segment_led_2, //数码管输出
output [7:0] led
);
reg [7:0] floor_level; //内部信号:当前楼层数
reg [7:0] display; //显示输出
//电梯显示模块
always@(posedge clk_500hz)begin
//sw1=0,此时楼层数清零,显示FF,作为reset态
if(sw1 == 1'b0)begin
floor_level[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
floor_level[7:0] <= 8'h00;
end
//楼层下降
else if(!floor_signal && key_done)begin
if(floor_level[3:0] > 4'd0)begin
floor_level[3:0] <= floor_level[3:0]-2'd1;
floor_level[7:4] <= floor_level[7:4];
end
else if(floor_level[3:0] == 4'd0)begin
floor_level[3:0] <= 4'h9;
floor_level[7:4] <= floor_level[7:4]-4'h1;
end
end
//将当前楼层数赋值给显示
display[7:0] = floor_level[7:0];
end
//sw1,2=11,此时表示上升
else if(sw1 == 1'b1 && sw2 == 1'b1)begin
//清零
if(!rst)begin
floor_level[7:0] <= 8'h00;
end
//楼层上升
else if(!floor_signal && key_done)begin
if(floor_level[3:0] < 4'd9)begin
floor_level[3:0] <= floor_level[3:0]+2'd1;
floor_level[7:4] <= floor_level[7:4];
end
else if(floor_level[3:0] == 4'd9)begin
floor_level[3:0] <= 4'h0;
floor_level[7:4] <= floor_level[7:4]+4'h1;
end
end
//将当前楼层数赋值给显示
display[7:0] = floor_level[7:0];
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_in (floor_signal),
.clk_500hz (clk_500hz),
.key_done (key_done)
);
//例化流水灯
flashled u1 (
.clk (clk),
.rst (rst),
.led (led),
.sw (sw2)
);
endmodule
(2) 数码管驱动: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
(3) 按键消抖:debounce.v
module debounce
(
input clk , //时钟
input rst_n , //复位键
input key_in, //对应楼层信号
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_in==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
2. 流水灯模块
(1) 流水灯:flashled.v
module flashled (clk,rst,led,sw);
input clk,rst;
input sw; //控制正反转
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
if(sw == 1'b0)
cnt <= cnt +1; //向下流水,模拟下降
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. 管脚配置
作者使用的板子是“10M02SCM153C8G”核心板,用其他板子的话就换对应的管脚就行。其中sw键是拨码开关,floor_signal和rst键是按键,led[i]是led灯,clk是时钟,segment是数码管。
4. 可优化
显然,这是一个很简陋的显示系统。按照最终设想,设计出来的一个项目应该包含完整的电梯操作,包含内外两面显示:1.对于外显示,应该可以预设人所在楼层,让电梯显示逐层变化到指定楼层;2.对于内显示,应该可以设置想去的任何楼层,确定后让电梯显示逐层变化到指定楼层。这个功能其实也不难,添加一个变量用于存储即可,在加个时钟用于自动变化。但是作者手上的板子不具有输入功能(很麻烦,需要一下一下按),加之只是心血来潮帮室友做个期末大作业,所以只完成了基本要求。可能哪一天有兴趣了再来完善。
可以参考这个:使用Verilog实现FPGA双列电梯控制系统-阿里云开发者社区
后记
相比于第一次设计篮球比赛计分器,这次设计就要熟练的多,整体设计过程大约1h。之所以这一次开发能这么快,得益于分模块开发的思想。稍加注意即可发现,这一项目中的数码管驱动,按键消抖,分频器和38译码器都是现成的。其中流水灯模块稍加修改了一下,添加的正反装的功能。
所以说啊,电子这东西,入门难,但是入门之后很多东西都是相通的,稍加举一反三便可达到目的。最后提一嘴题外话,良好的开发习惯真的很造福开发过程,再接再厉。