项目介绍
项目功能介绍
通过FPGA核心板实现一个秒表。该秒表具有启动,停止,递增和清除功能。
启动后,秒表从0开始计数,每0.1秒计数一次,最大值为9.9秒。当数值达到9.9之后会重新回到0.0开始新一轮的计数。按下停止键,则秒表停止计数并显示当前的数值。当停止计数后,按下递增键时,秒表计数增加0.1秒。按下清除键后,秒表归零。
设计思路
项目分为四个模块:主体逻辑实现,分频模块,消抖模块,递增模块
分频模块
将输入的时钟信号clk分频到1Hz(clk1h),通过一个计数器完成,当计数器达到一定计数值N=1200000时,将输出信号的状态翻转以生成更低的频率。
消抖模块
为start、stop和add提供消抖功能,即它们将忽略按键的快速变化(抖动),只有当按键状态稳定一段时间后才产生一个有效的信号脉冲,例如start_pulse、stop_pulse等。
递增模块
使用add_pulse的正脉冲触发且计数暂停时(由start_flag控制),调整cnt_num1和cnt_num2的值以实现计数的递增。
如下图下图为设计思路
硬件框图
以下为FOPA资源占用
硬件介绍
STEP小脚丫FPGA学习平台是苏州思得普信息科技公司专门针对FPGA初学者(尤其是学习数字电路的在校同学)打造的一系列性价比最高、学习门槛最低的学习模块系列。板上选用的芯片兼具了FPGA和CPLD的优点,瞬时上电启动,无需外部重新配置FPGA,是学习数字逻辑绝佳的选择。系列中所有板子的大小兼容标准的DIP40封装,尺寸只有52mm x 18mm,非常便于携带,而且能够直接插在面包板上或以模块的方式放置在其它电路板上以即插即用的方式,大大简化系统的设计。
最新推出的STEP-MXO2-LPC在易用性方面做了大幅升级:
- 使用了USB Type C接口提供板上+5V供电、FPGA的配置,并新增了UART通信的功能,因此无需再通过其它端口与PC进行数据通信
- 支持U盘模式(连接到上位机的USB端口,上位机自动弹出StepFPGA的U盘盘符)的下载,任何操作系统的电脑 - Windows、Mac OS以及Linux(包括树莓派)都可以在不安装任何驱动程序的情况下,直接将生成的jed配置文件发送到StepFPGA盘中即可完成编程
- 为配合这款小脚丫FPGA的使用,我们特别升级了Web IDE系统,用户不必再下载安装Diamond软件,即可在任何一款电脑上通过浏览器进行FPGA的编程和编译。图形化的界面使得操作非常直观、便捷
主要代码片段说明:
代码主体部分:
module counter (
input clk, // 时钟
input rst, // 复位
input sign, // 启动暂停按键
input step_up, // 递增按键
output [8:0] seg_led_1, // 数码管1
output [8:0] seg_led_2, // 数码管2
output reg [7:0] led // LED状态指示灯
);
wire clk1h; //1Hz时钟
wire sign_pulse; //按键消抖后信号
wire step_up_pulse;
reg sign_flag = 1'b0; //按键标志位
reg step_up_flag = 1'b0;
reg back_to_zero_flag ; //计时完成信号
reg [6:0] seg [9:0];
reg [3:0] cnt_num1; //个位
reg [3:0] cnt_num2; //十位
initial begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
end
wire step_up_pulse2;
reg step_up_pulse3;
reg [23:0] step_up_pulse3_cnt;
reg step_up_pulse3_cnt_en;
//启动/暂停按键进行消抖
debounce U2 (
.clk(clk),
.rst(rst),
.key(sign),
.key_pulse(sign_pulse)
);
//启动/暂停按键进行消抖
debounce2 U3 (
.clk(clk),
.rst(rst),
.key(step_up),
.key_sec(step_up_pulse),
.key_pulse(step_up_pulse2)
);
//用于分出一个1Hz的频率
divide #(.WIDTH(32),.N(1200000)) U1 (
.clk(clk),
.rst_n(rst),
.clkout(clk1h)
);
reg sign_pulse_dly;
always @ (posedge clk or negedge rst)begin
if(!rst==1)begin
sign_flag <= 1;
sign_pulse_dly <= 1'b0;
end
else begin
if(sign_pulse && ~sign_pulse_dly)begin
sign_flag <= ~sign_flag;
end
else begin
sign_flag <= sign_flag;
end
sign_pulse_dly <= sign_pulse;
end
end
always @ (posedge step_up_pulse)
if(!rst==1)
step_up_flag <= 0;
else begin
step_up_flag <= 1;
end
always@(posedge clk or negedge rst)begin
if(rst == 1'b0)begin
step_up_pulse3 <= 1'b0;
step_up_pulse3_cnt <= 24'd0;
step_up_pulse3_cnt_en <= 1'b0;
end
else begin
if(step_up_pulse2)begin
step_up_pulse3_cnt_en <= 1'b1;
step_up_pulse3_cnt <= step_up_pulse3_cnt + 1'b1;
step_up_pulse3 <= 1'b1;
end
else begin
if(step_up_pulse3_cnt_en)begin
step_up_pulse3 <= 1'b1;
if(step_up_pulse3_cnt == 24'd12_000_00)begin
step_up_pulse3_cnt <= 24'd0;
step_up_pulse3_cnt_en <= 1'b0;
end
else begin
step_up_pulse3_cnt <= step_up_pulse3_cnt + 1'b1;
step_up_pulse3_cnt_en <= step_up_pulse3_cnt_en;
end
end
else begin
step_up_pulse3_cnt <= step_up_pulse3_cnt;
step_up_pulse3 <= 1'b0;
end
end
end
end
//计时完成标志信号产生
always @(*) begin
// 默认情况下回到零标志为0
if (!rst) begin
back_to_zero_flag <= 0;
end
// 当计数器都为0时,设置回到零标志为1
else if ((cnt_num2 == 0) && (cnt_num1 == 0)) begin
back_to_zero_flag <= 1;
end
// 其他情况,保持回到零标志为0
else begin
back_to_zero_flag <= 0;
end
end
// 在时钟上升沿或者重置信号的下降沿进行计数操作
always @(posedge clk1h or negedge rst) begin
// 当重置非激活时
if (!rst) begin
cnt_num1 <= 4'd0; // 个位计数器清零
cnt_num2 <= 4'd0; // 十位计数器清零
end
// 正常计数操作
else begin
if (sign_flag) begin
if (step_up_pulse3) begin
// 个位满9进1
if (cnt_num1 == 4'd9) begin
cnt_num1 <= 4'd0;
// 十位满9则清零,否则加1
cnt_num2 <= (cnt_num2 == 4'd9) ? 4'd0 : cnt_num2 + 4'd1;
end
// 没有满9,个位数+1
else begin
cnt_num1 <= cnt_num1 + 4'd1;
end
end
end
// 如果不是持有标志,直接处理计数
else begin
if (cnt_num1 == 4'd9) begin
cnt_num1 <= 4'd0; // 个位清零
// 十位满9清零,否则加1
cnt_num2 <= (cnt_num2 == 4'd9) ? 4'd0 : cnt_num2 + 4'd1;
end
// 个位没有满9,直接加1
else begin
cnt_num1 <= cnt_num1 + 4'd1;
end
end
end
end
//计时完成点亮led
always @ ( back_to_zero_flag)
begin
if (back_to_zero_flag==1)
led = 8'b0;
else
led = 8'b11111111;
end
assign seg_led_1[8:0] = {2'b00,seg[cnt_num1]};
assign seg_led_2[8:0] = {1'b0, 1'b1, seg[cnt_num2]};
endmodule
这一部分主要实现的计数,递增,清除等功能。
分频部分
module divide (
input clk, rst_n, // 输入信号
output clkout // 输出信号
);
parameter WIDTH = 24; // 计数器的位数
parameter N = 12_000_000; // 分频系数
reg [WIDTH-1:0] cnt_p, cnt_n;
reg clk_p, clk_n;
always @(posedge clk or negedge rst_n) 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
// 上升沿触发的分频时钟输出控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
clk_p <= 0;
else if (cnt_p < (N >> 1)) // N除以2
clk_p <= 0;
else
clk_p <= 1; // 生成分频时钟
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
// 下降沿触发的分频时钟输出控制
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=1时,直接输出原时钟
(N[0]) ? (clk_p & clk_n) : // 当N为奇数时,利用clk_p和clk_n的与操作产生输出
clk_p; // 当N为偶数时,直接输出clk_p
endmodule
- 上升沿计数器: 在每个
clk的上升沿,如果满足分频条件(计数值cnt_p等于N-1),cnt_p会被清零;否则,cnt_p会自增。这是一个模N的计数器。 - 上升沿分频时钟: 同样在
clk的上升沿,如果计数器cnt_p小于N的一半,则clk_p被设为0,否则被设为1。这样产生的分频时钟,在计数器计数达到N的一半时切换状态,生成占空比为50%的时钟信号。 - 下降沿计数器: 在每个
clk的下降沿,行为与上升沿计数器相同,使用独立的计数器(cnt_n)来跟踪。 - 下降沿分频时钟: 与上升沿分频时钟相似,但响应
clk的下降沿。 - 最终分频输出: 通过
clkout输出。 - 当分频系数
N为1时,没有分频,直接将clk输出。 - 当
N为奇数时,通过对上升沿和下降沿产生的分频信号进行逻辑与操作(clk_p & clk_n)来得到输出。 - 当
N为偶数时,仅使用上升沿的分频时钟clk_p作为输出。
- 当分频系数
消抖模块
module debounce (
input clk,
input rst,
input [N-1:0] key, // 输入的按键
output [N-1:0] key_pulse // 按键动作产生的脉冲
);
parameter N = 1; // 要消除的按键的数量
reg [N-1:0] key_rst_pre; // 定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; // 定义一个寄存器变量储存储当前时刻触发的按键值
wire [N-1:0] key_edge; // 检测到按键由高到低变化时产生一个高脉冲
reg [N-1:0] key_sec;
reg [17:0] cnt; // 产生延时所用的计数器,至少需要18位计数器
// 利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst) begin
if (!rst) begin
key_rst <= {N{1'b1}};
key_rst_pre <= {N{1'b1}};
end else begin
key_rst <= key;
key_rst_pre <= key_rst;
end
end
assign key_edge = key_rst_pre & (~key_rst); // 脉冲边沿检测
// 产生20ms延时,当检测到key_edge有效时计数器清零开始计数
always @(posedge clk or negedge rst) begin
if (!rst) begin
cnt <= 18'h0;
end else if (key_edge) begin
cnt <= 18'h0;
end else begin
cnt <= cnt + 1'h1;
end
end
reg [N-1:0] key_sec_pre; // 延时后检测电平寄存器变量
// 延时后检测key,如果按键状态变低产生一个时钟的高脉冲
always @(posedge clk or negedge rst) begin
if (!rst) begin
key_sec <= {N{1'b1}};
end else if (cnt == 18'h3ffff) begin
key_sec <= key;
end
end
always @(posedge clk or negedge rst) begin
if (!rst) begin
key_sec_pre <= {N{1'b1}};
end else begin
key_sec_pre <= key_sec;
end
end
assign key_pulse = key_sec_pre & (~key_sec); // 最终消抖过的按键脉冲输出
endmodule
递增模块
module debounce2 (
input clk, // 系统时钟
input rst, // 异步复位,低电平有效
input [N-1:0] key, // 输入的按键数组
output [N-1:0] key_sec, // 消抖后的稳定按键信号
output [N-1:0] key_pulse // 检测到按键动作时产生的脉冲
);
parameter N = 1; // 要消除的按键的数量
// 内部寄存器声明
reg [N-1:0] key_rst_pre; // 上一个触发时的按键值存储寄存器
reg [N-1:0] key_rst; // 当前触发时的按键值存储寄存器
reg [N-1:0] key_sec; // 消抖后的稳定按键信号寄存器
reg [N-1:0] key_sec_pre; // 上一次稳定按键信号存储寄存器
// 检测键边沿
wire [N-1:0] key_edge; // 检测到按键由高到低变化时产生一个高脉冲
// 登记触发按键的状态,用于下降沿检测
always @(posedge clk or negedge rst) begin
if (!rst) begin
key_rst <= {N{1'b1}};
key_rst_pre <= {N{1'b1}};
end else begin
key_rst <= key;
key_rst_pre <= key_rst;
end
end
// 脉冲边沿检测
assign key_edge = key_rst_pre & (~key_rst);
// 延时计数器
reg [17:0] cnt; // 用于生成约20ms的延时
// 延时计数器逻辑
always @(posedge clk or negedge rst) begin
if (!rst)
cnt <= 18'h0;
else if (key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
// 延时后检测按键电平
always @(posedge clk or negedge rst) begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt == 18'h3FFFF)
key_sec <= key;
end
// 记录前一次的稳定按键信号
always @(posedge clk or negedge rst) begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
// 生成按键动作的脉冲
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
- 键状态登记与边缘检测:
- 在每个时钟上升沿,如果检测到复位信号(
rst)为低,则两个按键状态寄存器(key_rst,key_rst_pre)均置为高电平(1),表示没有按键按下。 - 之后,这两个寄存器分别保持当前和之前的按键状态,用于检测按键的边缘。
key_edge信号用于检测当从未按压状态到按压状态的变化时,产生一个高脉冲。
- 在每个时钟上升沿,如果检测到复位信号(
- 延时计数器:
- 用于生成约20ms的延时,这段时间内如果按键状态稳定,则认为是有效的按压(消除了抖动)。
- 如果
key_edge为true,或者在复位时,计数器重置。 - 否则,计数器每个时钟周期加1。
- 稳定按键信号产生:
- 当计数器达到指定值(
18'h3FFFF),即约20ms后,如果检测到按键仍处于按下状态,则更新key_sec为当前按键状态。
- 当计数器达到指定值(
- 脉冲生成:
- 通过比较前一次和当前的稳定状态(
key_sec_pre和key_sec),如果发现有变化,则在key_pulse上生成一个脉冲。这表示捕捉到了一个按键的稳定按下动作。
- 通过比较前一次和当前的稳定状态(
遇到的困难:
在测试多次后,发现所使用的板卡个位和十位数的位置相反,在示例项目中出现了同样的问题,因此在管脚分配中做了修改,满足了最终的要求。
未来计划:
尝试实现更多难度更高的项目,最终能够根据自己的实际需求实现较为实用的功能。