FPGA学习(三)——数码管实现四位分秒计数器
FPGA学习(三)——数码管实现四位分秒计数器
目录
- FPGA学习(三)——数码管实现四位分秒计数器
- 一、实验要求
- 二、实验思考
- 三、四位分秒计数器实现
- 1、代码实现
- 2、添加按键消抖模块
- 3、添加状态机思想
- 4、模块化重构代码
- 5、实现效果
- 四、总结
一、实验要求
1、用FPGA控制四个数码管,分别显示分钟、秒钟,计数范围都是0~59。
2、按键控制复位和暂停。当复位时所有计数器归零,当启动/暂停按下,暂停计数,再次接下时继续。
3、采用状态机思想实现。
二、实验思考
1、数码管显示。为了在数码管上显示四位分秒计数器,在DE2-115开发板上使用四位共阳极的七段数码管,如hex3-hex0,分别作为分钟的十位、个位,秒的十位、个位。
2、计数逻辑。秒的计数器是0到59,当秒到59后,分钟加一,秒归零。分钟同样到59后归零。用一个4位的BCD计数器,每个单位用4位表示。例如,秒的个位和十位各用4位二进制码,组合成0-59的范围。
reg [3:0] sec_one; // 0-9
reg [3:0] sec_ten; // 0-5
reg [3:0] min_one;
reg [3:0] min_ten;
/*详细的计数逻辑*/
3、分频逻辑。将50MHz的时钟分频成1Hz的信号,这样每秒钟计数器加一。分频的话,50MHz等于50,000,000 Hz,要得到1Hz,需要计数50,000,000次,需要26位计数器
reg [DIV_WIDTH-1:0] counter;
always @(posedge clk or posedge reset) begin
if (reset) begin
counter <= 0;
clk_out <= 0;
end else begin
counter <= counter + 1;
clk_out <= counter[DIV_WIDTH-1]; // 取最高位作为分频输出
end
end
4、复位/暂停。可以采用按键作为控制信号,当按键按下时,复位/启动/暂停计数。
if (reset) begin
{sec_ones, sec_tens, min_ones, min_tens} <= 16'd0; // 全部清零
end else if (!start_pause) begin // KEY1按下时暂停(低电平有效)
5、状态机。首先要定义系统可能达到的所有状态。可能的状态包有复位状态、运行状态、暂停状态以及空闲状态。此外考虑状态转移的条件和时机,将按键信号作为状态转移的触发条件,同时需要确保状态转移只在按键事件发生时进行一次切换,而不是持续切换。这可能需要边缘检测,即检测按键信号的上升沿或下降沿。同时,每个状态下的行为也需要明确,如:在RUNNING状态下,计数器每秒递增;在IDLE状态下,计数器保持不变,而复位信号无论当前处于什么状态,都应强制回到初始状态。
三、四位分秒计数器实现
1、代码实现
首先,在不考虑状态机的情况下,先来简单的实现一下这个分秒计数器。代码如下 :
module clock(
input clk_50m, // 50MHz系统时钟
input reset_btn, // 复位按键(直接连接)
input pause_btn, // 暂停按键(直接连接)
output [7:0] HEX3, // 分钟十位
output [7:0] HEX2, // 分钟个位
output [7:0] HEX1, // 秒十位
output [7:0] HEX0 // 秒个位
);
// 内部信号定义
wire clk_1hz;
wire [3:0] min_ten, min_one;
wire [3:0] sec_ten, sec_one;
wire carry;
reg pause_state; // 暂停状态寄存器
// 1Hz时钟生成(简化版)
reg [25:0] clk_div;
always @(posedge clk_50m or posedge reset_btn) begin
if (reset_btn) begin
clk_div <= 0;
end
else begin
clk_div <= clk_div + 1;
end
end
assign clk_1hz = clk_div[25] & ~pause_state; // 暂停时停止时钟
// 暂停状态控制
always @(posedge clk_50m or posedge reset_btn) begin
if (reset_btn) begin
pause_state <= 1'b0;
end
else if (pause_btn) begin
pause_state <= ~pause_state; // 每次按下切换暂停状态
end
end
// 秒计数器(0-59)
reg [3:0] sec_one, sec_ten;
always @(posedge clk_1hz or posedge reset_btn) begin
if (reset_btn) begin
sec_one <= 0;
sec_ten <= 0;
end else begin
if (sec_one == 9) begin
sec_one <= 0;
if (sec_ten == 5) sec_ten <= 0;
else sec_ten <= sec_ten + 1;
end else begin
sec_one <= sec_one + 1;
end
end
end
assign carry = (sec_one == 9) && (sec_ten == 5);
// 分钟计数器(0-59)
reg [3:0] min_one, min_ten;
always @(posedge carry or posedge reset_btn) begin
if (reset_btn) begin
min_one <= 0;
min_ten <= 0;
end else begin
if (min_one == 9) begin
min_one <= 0;
if (min_ten == 5) min_ten <= 0;
else min_ten <= min_ten + 1;
end else begin
min_one <= min_one + 1;
end
end
end
// 数码管显示驱动
function [7:0] digit_to_seg;
input [3:0] digit;
input dot;
begin
case(digit)
0: digit_to_seg = {dot, 7'b1000000};
1: digit_to_seg = {dot, 7'b1111001};
2: digit_to_seg = {dot, 7'b0100100};
3: digit_to_seg = {dot, 7'b0110000};
4: digit_to_seg = {dot, 7'b0011001};
5: digit_to_seg = {dot, 7'b0010010};
6: digit_to_seg = {dot, 7'b0000010};
7: digit_to_seg = {dot, 7'b1111000};
8: digit_to_seg = {dot, 7'b0000000};
9: digit_to_seg = {dot, 7'b0010000};
default: digit_to_seg = {dot, 7'b1111111};
endcase
end
endfunction
assign HEX3 = digit_to_seg(min_ten, 1'b1); // 分十位带小数点
assign HEX2 = digit_to_seg(min_one, 1'b0);
assign HEX1 = digit_to_seg(sec_ten, 1'b1); // 秒十位带小数点
assign HEX0 = digit_to_seg(sec_one, 1'b0);
endmodule
上述代码只是简单的实现,而在实际中,按键输入控制复位和启动/暂停,硬件会有机械抖动,可能导致误触发,因此我们要为上述代码添加上按键的消抖模块。
2、添加按键消抖模块
在编写消抖模块时,通常的做法是使用计数器来检测按键状态的稳定。当检测到按键状态变化时,启动一个计数器,如果在20ms内状态没有变化,则认为按键稳定,输出新的状态。
消抖模块代码如下:
module button_debounce(
input clk, // 50MHz时钟
input button_in, // 原始按键输入
output reg button_out // 消抖后的按键信号
);
reg [19:0] counter; // 20位计数器(50MHz/2^20 ≈ 21ms)
reg button_sync;
reg button_prev;
always @(posedge clk) begin
// 同步输入信号,防止亚稳态
button_sync <= button_in;
// 消抖处理逻辑
if (button_sync != button_prev) begin
button_prev <= button_sync;
counter <= 0;
end else if (counter[19]) begin // 计数器满(约21ms)
button_out <= button_prev;
end else begin
counter <= counter + 1;
end
end
endmodule
3、添加状态机思想
module fsm_controller(
input clk,
input reset,
input pause,
output reg running_o // 明确输出端口命名
);
// Verilog-2001兼容的状态定义
parameter [1:0] IDLE = 2'b00,
RUNNING = 2'b01,
PAUSED = 2'b10;
reg [1:0] current_state, next_state;
// 状态寄存器更新
always @(posedge clk or posedge reset) begin
if (reset) begin
current_state <= IDLE;
end else begin
current_state <= next_state;
end
end
// 状态转移逻辑
always @(*) begin
case (current_state)
IDLE: next_state = RUNNING;
RUNNING: next_state = pause ? PAUSED : RUNNING;
PAUSED: next_state = pause ? RUNNING : PAUSED;
default: next_state = IDLE;
endcase
end
// 输出逻辑
always @(*) begin
running_o = (current_state == RUNNING);
end
endmodule
4、模块化重构代码
将上述按键消抖模块及状态机思想加入,并模块化代码。完整代码如下:
clock.v
/*
* 顶层模块:clock_mmss_static_top
* 修改说明:明确声明running信号并统一连接
*/
module clock(
input clk_50m, // 50MHz系统时钟
input reset_btn, // 按键1 - 复位(KEY0)
input pause_btn, // 按键2 - 暂停/恢复(KEY1)
output [7:0] HEX3, // 分钟十位(最左数码管)
output [7:0] HEX2, // 分钟个位(带小数点)
output [7:0] HEX1, // 秒十位
output [7:0] HEX0 // 秒个位(最右数码管)
);
// 内部信号定义
wire clk_1hz;
wire reset_pulse, pause_pulse;
wire running; // 统一使用wire类型连接模块
wire [3:0] min_ten, min_one;
wire [3:0] sec_ten, sec_one;
// 实例化时钟分频模块
clock_divider #(
.DIV_WIDTH(25) // 50MHz -> 1Hz (2^25 = 33,554,432)
) u_clock_div (
.clk(clk_50m),
.reset(reset_pulse),
.clk_out(clk_1hz)
);
// 实例化按键消抖模块
button_debounce u_reset_debounce (
.clk(clk_50m),
.button_in(reset_btn),
.button_out(reset_pulse)
);
button_debounce u_pause_debounce (
.clk(clk_50m),
.button_in(pause_btn),
.button_out(pause_pulse)
);
// 实例化状态机控制器(关键修改)
fsm_controller u_fsm (
.clk(clk_50m),
.reset(reset_pulse),
.pause(pause_pulse),
.running_o(running) // 统一端口命名
);
// 实例化秒计数器
second_counter u_sec_counter (
.clk(clk_1hz),
.reset(reset_pulse),
.enable(running), // 直接使用running信号
.sec_ten(sec_ten),
.sec_one(sec_one),
.carry(carry_w)
);
// 实例化分钟计数器
minute_counter u_min_counter (
.clk(carry_w),
.reset(reset_pulse),
.enable(running), // 直接使用running信号
.min_ten(min_ten),
.min_one(min_one)
);
// 实例化静态显示驱动
static_display u_display (
.min_ten(min_ten),
.min_one(min_one),
.sec_ten(sec_ten),
.sec_one(sec_one),
.HEX3(HEX3),
.HEX2(HEX2),
.HEX1(HEX1),
.HEX0(HEX0)
);
endmodule
clock_divider.v
/*
* 模块名称:clock_divider
* 功能:将输入时钟分频为较低频率
*/
module clock_divider #(
parameter DIV_WIDTH = 25 // 分频计数器位宽
)(
input clk,
input reset,
output reg clk_out
);
reg [DIV_WIDTH-1:0] counter;
always @(posedge clk or posedge reset) begin
if (reset) begin
counter <= 0;
clk_out <= 0;
end else begin
counter <= counter + 1;
clk_out <= counter[DIV_WIDTH-1]; // 取最高位作为分频输出
end
end
endmodule
button_debounce.v
/*
* 模块名称:button_debounce
* 功能:对机械按键进行消抖处理(约20ms消抖时间)
*/
module button_debounce(
input clk, // 50MHz时钟
input button_in, // 原始按键输入
output reg button_out // 消抖后的按键信号
);
reg [19:0] counter; // 20位计数器(50MHz/2^20 ≈ 21ms)
reg button_sync;
reg button_prev;
always @(posedge clk) begin
// 同步输入信号,防止亚稳态
button_sync <= button_in;
// 消抖处理逻辑
if (button_sync != button_prev) begin
button_prev <= button_sync;
counter <= 0;
end else if (counter[19]) begin // 计数器满(约21ms)
button_out <= button_prev;
end else begin
counter <= counter + 1;
end
end
endmodule
second_counter.v
module second_counter(
input clk,
input reset,
input enable,
output reg [3:0] sec_ten,
output reg [3:0] sec_one,
output carry // 改为连续赋值方式
);
// 进位条件:59秒时产生1个时钟周期高电平
assign carry = (sec_one == 9) && (sec_ten == 5) && enable;
always @(posedge clk or posedge reset) begin
if (reset) {sec_ten, sec_one} <= 8'h0;
else if (enable) begin
if ({sec_ten, sec_one} == 8'h59)
{sec_ten, sec_one} <= 8'h0;
else if (sec_one == 9) begin
sec_one <= 0;
sec_ten <= sec_ten + 1;
end
else sec_one <= sec_one + 1;
end
end
endmodule
minute_counter.v
/*
* 模块名称:minute_counter
* 功能:实现0-59分钟计数
*/
module minute_counter(
input clk, // 由秒进位驱动
input reset,
input enable,
output reg [3:0] min_ten, // 分十位(0-5)
output reg [3:0] min_one // 分个位(0-9)
);
always @(posedge clk or posedge reset) begin
if (reset) begin
min_ten <= 0;
min_one <= 0;
end else if (enable) begin
if (min_one == 9) begin
min_one <= 0;
if (min_ten == 5) begin
min_ten <= 0;
end else begin
min_ten <= min_ten + 1;
end
end else begin
min_one <= min_one + 1;
end
end
end
endmodule
static_display.v
/*
* 模块名称:static_display
* 功能:驱动4位数码管静态显示MM:SS
* DE2-115数码管为共阳极,低电平点亮
*/
module static_display(
input [3:0] min_ten, // 分钟十位
input [3:0] min_one, // 分钟个位
input [3:0] sec_ten, // 秒钟十位
input [3:0] sec_one, // 秒钟个位
output [7:0] HEX3, // 分钟十位(最左)
output [7:0] HEX2, // 分钟个位(带小数点)
output [7:0] HEX1, // 秒钟十位
output [7:0] HEX0 // 秒钟个位(最右)
);
// 数字到7段码的转换(共阳极,低电平点亮)
function [7:0] digit_to_seg;
input [3:0] digit;
input dot; // 小数点控制
begin
case(digit)
0: digit_to_seg = {dot, 7'b1000000}; // 0
1: digit_to_seg = {dot, 7'b1111001}; // 1
2: digit_to_seg = {dot, 7'b0100100}; // 2
3: digit_to_seg = {dot, 7'b0110000}; // 3
4: digit_to_seg = {dot, 7'b0011001}; // 4
5: digit_to_seg = {dot, 7'b0010010}; // 5
6: digit_to_seg = {dot, 7'b0000010}; // 6
7: digit_to_seg = {dot, 7'b1111000}; // 7
8: digit_to_seg = {dot, 7'b0000000}; // 8
9: digit_to_seg = {dot, 7'b0010000}; // 9
default: digit_to_seg = {dot, 7'b1111111}; // 灭
endcase
end
endfunction
// 数码管连接(使用HEX2的小数点作为冒号)
assign HEX3 = digit_to_seg(min_ten, 1'b1); // 分钟十位,点亮小数点
assign HEX2 = digit_to_seg(min_one, 1'b0); // 分钟个位
assign HEX1 = digit_to_seg(sec_ten, 1'b1); // 秒钟十位,点亮小数点
assign HEX0 = digit_to_seg(sec_one, 1'b0); // 秒钟个位
endmodule
fsm_controller.v
/*
* 模块名称:fsm_controller
*/
module fsm_controller(
input clk,
input reset,
input pause,
output reg running_o // 明确输出端口命名
);
// Verilog-2001兼容的状态定义
parameter [1:0] IDLE = 2'b00,
RUNNING = 2'b01,
PAUSED = 2'b10;
reg [1:0] current_state, next_state;
// 状态寄存器更新
always @(posedge clk or posedge reset) begin
if (reset) begin
current_state <= IDLE;
end else begin
current_state <= next_state;
end
end
// 状态转移逻辑
always @(*) begin
case (current_state)
IDLE: next_state = RUNNING;
RUNNING: next_state = pause ? PAUSED : RUNNING;
PAUSED: next_state = pause ? RUNNING : PAUSED;
default: next_state = IDLE;
endcase
end
// 输出逻辑
always @(*) begin
running_o = (current_state == RUNNING);
end
endmodule
5、实现效果
分秒计数器
四、总结
在这次基于DE2-115开发板的四位分秒计数器设计实验中,我深刻体会到了数字系统设计的完整流程与工程思维的重要性。从最初简单的分频计数到最终的模块化重构,整个过程让我对FPGA开发有了更立体的认识。通过引入消抖模块并优化参数,最终实现了可靠的按键检测。而三段式状态机的设计,不仅让逻辑更清晰,调试效率也大幅提升。最让我受益的是最后的模块化重构过程,将系统拆分为功能独立的模块,提高了代码复用性。