项目需求

  • 通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。
  • 使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。 秒表应从 0.0 秒计数到 9.9秒,然后翻转,计数值每0.1秒精确更新一次。
  • 秒表使用四个按钮输入:开始、停止、增量和清除(重置)。 开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次); 停止输入使计数器停止递增,但使数码管显示当前计数器值; 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间; 复位/清除输入强制计数器值为零。

需求分析

  • 需要用到小脚丫的两个数码管及按键,实现秒表功能。
  • 由于有计时功能,需要用到小脚丫的12M晶振作为时钟源,要求0.1s准确更新一次,即10Hz更新一次,理论实现方式为在12MHz的每个时钟上升沿计数一次,计到12*10^5后更新一次即为10Hz更新。
  • 用到四个按钮输入,由于秒表的高精度性,需要对按键进行消抖。
  • 计划利用三段式状态机进行编写

实现方式

  • 使用三段式状态机编写主模块,子模块分为seg数码管模块、debounce消抖模块(也使用了三段式状态机)、color_breath彩虹呼吸灯模块。
    *给出三段式状态机基本写法:
always @(posedge clk or negedge rst) begin
    if(!rst) begin  //异步复位
        STATE_C <= IDLE; 
    end
    else begin
        STATE_C <= STATE_N; //每个时钟上升沿到来时更新状态
    end
end

always @(*) begin
	case(STATE_C)
		STATE_1: begin
		…
		end
		STATE_2: begin
		…
		end
		…
		…
		…
	endcase
end

always @(posedge clk or negedge rst) begin
	if(!rst) begin
	…
	end
	else if(STATE_C == STATE_1) begin
	…
	end
	else if(…) begin
	…
	end
end
  • 此程序可分为6个状态,STOP暂停状态、STOP_PRESS按下暂停键后一直不放手的状态,COUNT计数状态、COUNT_PRESS按下开始键后一直不放手状态、INCRE增一键、S0按下增一键后不放手状态,将程序分好状态后续的实现方式可以很简单。
  • seg数码管模块直接定义每个数字对应的的数值,直接赋值即可。
  • color_breath彩虹呼吸灯模块,首先定义pwm发生器模块,再动态调整pwm的占空比,使其由低到高再由高到低循环,RGB端口调用pwm模块实现彩虹呼吸灯。
  • 彩虹呼吸灯为原创功能,其具体功能为,增加了三种模式,普通模式为0.1s更新,模式2为0.01s更新一次,模式3为1s更新一次,不同的模式闪烁不同的呼吸灯。
  • 原创增加了红色LED灯来扩展显示的秒数,以普通模式为例,显示时间从09.9s后会亮起一个LED灯,同时再次从09.9s循环,之后再次亮起一个LED灯,在8个LED灯亮满后且再次达到9.9s才算一次完整循环,即由基本要求的9.9s计时扩展到了89.9s计时,且保证精度不变。
  • 使用拨码开关控制模式的切换,且只有在STOP状态下可以进行模式切换,注意!switch为4’b0000时为普通模式,4’b0001时为高精度模式,精确到0.01s,4’b0011时为高范围模式,精确到1s,拨码为该三种情况外的其他情况时均为普通模式。

功能框图

状态转移关系如下图:

代码及说明

1.1顶层模块参数定义

//参数、状态定义
parameter STATE_WID = 6;    //状态位宽
parameter STOP = 6'b000_001; //松开后暂停状态(独热码)
parameter COUNT = 6'b000_010;    //松开后计数状态
parameter INCRE = 6'b000_100;    //增一状态
parameter S0 = 6'b001_000;   //增一后一直按住的状态(不计数)
parameter COUNT_PRESS = 6'b010_000;    //开始计数后一直按住的状态
parameter STOP_PRESS = 6'b100_000;   //暂停后一直按住的状态

parameter COUNT_CLK_1 = 1199999;  //计数1200000次,即0.1s
parameter COUNT_CLK_2 = 119999; //计数120000次,即0.01s
parameter COUNT_CLK_3 = 11999999;  //计数12000000次,即1s

parameter PRESS = 1'b1; //按钮按下
parameter UP = 1'b0;    //按钮抬起

1.2状态转换

always @(*) begin
    case(STATE_C)
        STOP: begin
            if(btn_de == PRESS) begin   //按下按键,状态转换
                STATE_N = COUNT_PRESS;
            end
            else if(increase_de == PRESS) begin //按下加一键,状态转换
                STATE_N = INCRE;
            end
            else begin  //保持状态
                STATE_N = STOP;
            end
        end
        COUNT_PRESS: begin
            if(btn_de == PRESS) begin
                STATE_N = COUNT_PRESS;  //一直按住就不会变动,也为计数状态
            end
            else if(btn_de == UP) begin
                STATE_N = COUNT;    //松开后进入计数状态
            end
            else begin
                STATE_N = COUNT_PRESS;
            end
        end
        COUNT: begin
            if(btn_de == PRESS) begin
                STATE_N = STOP_PRESS;
            end
            else begin
                STATE_N = COUNT;
            end
        end
        STOP_PRESS: begin
            if(btn_de == PRESS) begin
                STATE_N = STOP_PRESS;
            end
            else if(btn_de == UP) begin
                STATE_N = STOP; //松开后进入暂停状态
            end
            else begin
                STATE_N = STOP_PRESS;
            end
        end
        INCRE: begin
            if(increase_de == PRESS) begin
                STATE_N = S0;
            end
            else if(increase_de == UP) begin
                STATE_N  = STOP;
            end
            else begin
                STATE_N = INCRE;
            end
        end
        S0: begin
            if(increase_de == PRESS) begin
                STATE_N = S0;
            end
            else if(increase_de == UP) begin
                STATE_N = STOP;
            end
            else begin
                STATE_N = S0;
            end
        end
        default : begin //状态要完善
            STATE_N = STATE_C;
        end
    endcase
end

1.3定义计时器,定义每个状态下应当做些什么

always @(posedge clk or negedge rst) begin
    if(!rst) begin  //复位
        count <= 0;
        y1 <= 0;
        y0 <= 0;
        led_reg <= 0;
    end
    else if(STATE_C == COUNT || STATE_C == COUNT_PRESS) begin   //在COUNT、COUNT_PRESS启动计时
        if(count == count_choose) begin
            count <= 0; //计时器清零
            if(y0 == 9) begin
                y0 <= 0; //数字指示器个位清零
                if(y1 == 9) begin
                    y1 <= 0;    //计满,数字指示器十位清零
                    if(led_reg == 255) begin
                        led_reg <= 0;   //进位计满,进位指示器清零
                    end
                    else begin
                        led_reg <= (led_reg << 1) + 1; //进位,左移一位并加一
                    end
                end
                else begin
                    y1 <= y1 + 1;   //数字指示器十位进一
                end
            end
            else begin
                y0 <= y0 + 1; //每0.1s数字指示器个位加一
            end
        end
        else begin
            count <= count + 1; //在计时状态启动计时器
        end
    end
    else if(STATE_C == INCRE) begin //在INCRE状态数字指示器加一
        count <= 0; //计时器清零
        if(y0 == 9) begin
            y0 <= 0; //数字指示器个位清零
            if(y1 == 9) begin
                y1 <= 0;    //数字指示器十位清零
                if(led_reg == 255) begin
                    led_reg <= 0;   //进位计满,进位指示器清零
                end
                else begin
                    led_reg <= (led_reg << 1) + 1; //进位,左移一位并加一
                end
            end
            else begin
                y1 <= y1 + 1;   //数字指示器十位进一
            end
        end
        else begin
            y0 <= y0 + 1; //数字指示器个位加一
        end
    end
    else begin
        count <= 0; //在STOP、STOP_PRESS和S0状态计时器清零
        y1 <= y1;
        y0 <= y0;  //在STOP、STOP_PRESS和S0状态数字指示器不变
        led_reg <= led_reg;
        case(switch)    //仅可在STOP、STOP_PRESS和S0状态下改变模式
            4'b0000: begin
                count_choose <= COUNT_CLK_1; //普通秒表模式,精确到0.1s
                mod_reg <= 0;
            end
            4'b0001: begin
                count_choose <= COUNT_CLK_2; //模式2,精确到0.01s
                mod_reg <= 1;
            end
            4'b0011: begin
                count_choose <= COUNT_CLK_3; //模式3,精确到1s
                mod_reg <= 2;
            end
            default: begin
                count_choose <= COUNT_CLK_1;
                mod_reg <= 0;
            end
        endcase
    end
end

2.数码管控制

module seg(y1,y0,seg0,seg1);
input [3:0]y1;
input [3:0]y0;
output [8:0]seg1;
output [8:0]seg0;
reg [8:0]seg_reg [9:0];

initial begin
    seg_reg[0] = 9'h3f;
    seg_reg[1] = 9'h06;
    seg_reg[2] = 9'h5b;
	seg_reg[3] = 9'h4f;
	seg_reg[4] = 9'h66;
	seg_reg[5] = 9'h6d;
	seg_reg[6] = 9'h7d;
	seg_reg[7] = 9'h07;
	seg_reg[8] = 9'h7f;
    seg_reg[9] = 9'h6f;
end

assign seg1 = seg_reg[y1];
assign seg0 = seg_reg[y0];


endmodule

3.1PWM发生器

module pwm(out,duty,clk);
	input [7:0] duty;   // 输入,表示占空比,范围从0到255
	input clk;         // 输入,时钟信号用于同步
	output reg out;     // 输出信号,表示PWM波形
	reg [7:0] buffer;      // 8位寄存器,用于存储当前计数值
 
always @ (posedge clk)  begin 
	buffer <= buffer + 1;// 在每个时钟上升沿递增缓冲区值
	if (buffer < duty)// 将缓冲区值与占空比进行比较:如果缓冲区小于占空比,则将输出设置为0;否则,设置为1。
		begin
			out <= 0;
		end
		else begin
			out <= 1;
		end
	end
endmodule

3.2通过不断改变占空比实现呼吸效果

always @(posedge divide_clk) begin  //使RGB_buffer在0~255递增再从255~0递减
    if(wheel_position < 510 )   wheel_position <= wheel_position + 1;
    else wheel_position <= 0;
    if(wheel_position < 255) begin
        RGB_buffer <= wheel_position;
    end
    else begin
        RGB_buffer <= 510 - wheel_position;
    end
end

4.1消抖模块状态转换关系

//参数定义
localparam KEY_W = 1;
localparam TIME_20MS = 24_0000;

localparam IDLE  = 4'b0001;//初始状态 
localparam DOWN  = 4'b0010;//按键按下抖动
localparam HOLD  = 4'b0100;//按键按下后稳定
localparam UP    = 4'b1000;//按键上升抖动
//状态转移条件定义
wire idle2down; //初始转移到按下抖动
wire down2idle; //按下抖动转移到初始
wire down2hold; //按下抖动转移到按下稳定
wire hold2up  ; //按下稳定转移到上升抖动
wire up2idle  ; //上升抖动转移到初始

always@(*)begin
    case(state_c)
        IDLE:begin
            if(idle2down)begin
                state_n = DOWN;
            end
            else begin
                state_n = state_c;
            end
        end
        DOWN:begin
            if(down2idle)begin
                state_n = IDLE;
            end
            else if(down2hold)begin
                state_n = HOLD;
            end
            else begin
                state_n = state_c;
            end
        end
        HOLD:begin
            if(hold2up)begin
                state_n = UP;
            end
            else begin
                state_n = state_c;
            end
        end
        UP:begin
            if(up2idle)begin
                state_n = IDLE;
            end
            else begin
                state_n = state_c;
            end
        end
        default:state_n = state_c;
    endcase
end

4.2检测按键抖动来定义状态转换

assign idle2down = (state_c == IDLE) && nedge;//检测到下降沿
assign down2idle = (state_c == DOWN) && (pedge&& end_cnt_20ms);//计时未到20ms时且出现上升沿表示按键意外抖动,回到初始态
assign down2hold = (state_c == DOWN) && (~pedge && end_cnt_20ms);//计时到20ms时没有出现上升沿标志按键按下后保持稳定
assign hold2up   = (state_c == HOLD) && (pedge);//检测到上升沿跳转到上升态
assign up2idle   = (state_c == UP)   && end_cnt_20ms;//计数器计数到20ms跳转到初始态

4.3按键赋值

//按键赋值
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        key_out_r <= {KEY_W{1'b0}};
    end
    else if(state_c == HOLD)begin
        key_out_r <= {KEY_W{1'b1}};
    end
    else begin
        key_out_r <= {KEY_W{1'b0}};
    end
end
assign key_out = key_out_r;

仿真波形图

1.编写总体测试文件

`timescale 1ns/1ns
module counter_tb();
reg clk;
reg btn;
reg increase;
reg rst;
wire [8:0]seg1;
wire [8:0]seg0;

counter uut(
    .clk(clk),
    .btn(btn),
    .rst(rst),
    .increase(increase),
    .seg0(seg0),
    .seg1(seg1)
    );

always #(125/3) clk = ~clk;
// always #5 clk = ~clk;

initial begin
    rst = 1;
    btn = 1;
    increase = 1;
    clk = 1;
    
    rst = 0;
    #5
    rst = 1;    //初始复位
    #9000000

    //==============按下按钮开始计时=============
    #100
    btn = 0;
    #100
    btn = 1;
    // #900000000
    #9000000

    //==============按下按钮暂停====================
    btn = 0;
    #100
    btn = 1;
    #9000000

    //=================测试加一键========================
    increase = 0;
    #1000
    increase = 1;
    #9000000

    //====================测试复位键==================
    rst = 0;
    #100
    rst = 1;
    #4000000
    $stop;
    
end

endmodule

输出波形

2.测试消抖

`timescale 1ns/1ns
module debounce_tb();
reg clk;
reg rst;
reg key_in;
wire key_out;

debounce uut(
    .clk(clk),
    .rst_n(rst),
    .key_in(key_in),
    .key_out(key_out)
    );
always #(125/3) clk = ~clk;

initial begin
    rst = 1;
    key_in = 1;
    clk = 1;

    rst = 0;
    #5
    rst = 1;    //初始复位
    #1000000

    key_in = 0;
    #200000
    key_in = 1; //按下按键抖动
    #200000
    key_in = 0;
    #25000000

    key_in = 1;
    #200000
    key_in = 0; //抬起按键抖动
    #200000
    key_in = 1;
    #30000000
    $stop;
end

endmodule

输出波形:

可见,在输入模拟按下抖动信号大约20ms后才会输出消抖后信号,表明进入了稳定按下的状态,在输入模拟抬起抖动信号时,在上升沿出现一瞬间就输出了消抖后信号,表明已经不在按下状态。且由于小脚丫特性,输入信号中高电平代表未按下,低电平代表按下,而输出信号经过修正,高电平代表按下,低电平代表未按下。

FPGA资源利用说明

Design Summary
Number of registers: 241 out of 4635 (5%)
PFU registers: 241 out of 4320 (6%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 212 out of 2160 (10%)
SLICEs as Logic/ROM: 212 out of 2160 (10%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 116 out of 2160 (5%)
Number of LUT4s: 421 out of 4320 (10%)
Number used as logic LUTs: 189
Number used as distributed RAM: 0
Number used as ripple logic: 232
Number used as shift registers: 0
Number of PIO sites used: 40 + 4(JTAG) out of 105 (42%)
Number of block RAMs: 0 out of 10 (0%)
Number of GSRs: 1 out of 1 (100%)

Diamond MAP分析界面截图: