2024年寒假练 - 基于小脚丫FPGA套件V4.0制作两位十进制计算器
该项目使用了小脚丫开发板、Verilog语言,实现了两位十进制数计算器的设计,它的主要功能为:通过矩阵键盘的数字/运算符的输入,经过计算,从8段数码管输出计算结果。
标签
FPGA
开发板
计算器
参加活动
姜小豆
更新2024-04-01
273

1. 项目需求

实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制。

基本要求:

运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。

扩展要求:

使用TFTLCD来做显示,自行设计显示界面,代替数码管显示输入的参数、运算符以及计算后的结果。


2. 需求分析

要实现加减乘除计算功能,难点在于乘除的计算。

显示:

  • 输出需要显示到数码管或者LCD上。这里先以数码管为例。

功能模块有:

  • 计算,特别是除法
  • 数码管显示
  • 键盘的消抖和映射


状态跳转设计

image.png

键盘映射设计

1 2 3 +
4 5 6 -
7 8 9 *
0 C = /


3. 实现过程

1)数码管显示测试

参考这个 数码管模块

数据和位选组成16位,依次写入。

小脚丫底板中数码管的原理图:

image.png


其中,位的选择通过 DIG1~DIG8置位来选择:0xfe、0xfd……


程序代码:

// led 数码管显示



module led_segment
(

    input           clk,            // 系统时钟
    input           rst_n,          // 系统复位
    input   [4:0]   seg_data_1,     // seg1 数码管要显示的数据
    input   [4:0]   seg_data_2,     // seg2
    input   [4:0]   seg_data_3,     // seg3
    input   [4:0]   seg_data_4,     // seg4
    input   [4:0]   seg_data_5,     // seg5
    input   [4:0]   seg_data_6,     // seg6
    input   [4:0]   seg_data_7,     // seg7
    input   [4:0]   seg_data_8,     // seg8
    input   [7:0]   seg_data_en,    // 数码管数据使能[msb seg8 ~ lsb seg1]
    input   [7:0]   seg_dot_en,     // 数码管小数点使能
   
    output  reg     rclk_out,       // 74HC595 的 RCK 管脚
    output  reg     sclk_out,       // 74HC595 的 SCK 管脚
    output  reg     sdio_out        // 74HC595 的 SER 管脚
);

parameter   CLK_DIV_PERIOD = 600;   // 分频系数

// 状态机状态
localparam  IDLE = 2'd0;            // 空闲状态
localparam  MAIN = 2'd1;            // 主要工作状态,扫描刷新数码管
localparam  WRITE= 2'd2;            // 写状态

localparam  LOW = 1'b0;
localparam  HIGH = 1'b1;

// 数码管字库
reg [7:0] seg [18:0]; // seg 每个里面有8位

// 初始化
initial begin
    seg[0]  =   8'h3f;   // 0
    seg[1]  =   8'h06;   // 1
    seg[2]  =   8'h5b;   // 2
    seg[3]  =   8'h4f;   // 3
    seg[4]  =   8'h66;   // 4
    seg[5]  =   8'h6d;   // 5
    seg[6]  =   8'h7d;   // 6
    seg[7]  =   8'h07;   // 7
    seg[8]  =   8'h7f;   // 8
    seg[9]  =   8'h6f;   // 9
    seg[10] =   8'h46;   // + 的左半部分
    seg[11] =   8'h70;   // + 的右半部分
    seg[12] =   8'h40;   // -
    seg[13] =   8'h76;   // x
    seg[14] =   8'h49;   // /
    seg[15] =   8'h79;   // E
    seg[16] =   8'h00;   // 空
    seg[17] =   8'h73;   // R (显示是p)
    seg[18] =   8'h48;   // =
end

// 计数器对系统时钟进行计数
reg [9:0] cnt = 0;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cnt <= 1'b0;
    else begin
        if(cnt >= (CLK_DIV_PERIOD-1))
            cnt <= 1'b0;
        else
            cnt <= cnt + 1'b1;
    end

end

// 根据分频计数产生分频脉冲信号
reg clk_div;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        clk_div <= 1'b0;
    else
        clk_div <= (cnt == (CLK_DIV_PERIOD-1)) ? 1'b1 : 1'b0;
end

// 使用状态机完成 数码管扫描和74HC595时序
reg [15:0]  data_reg;
reg [2:0]   cnt_main;
reg [5:0]   cnt_write;
reg [1:0]   state = IDLE;
always @(posedge clk or negedge rst_n) begin
   
    // 复位状态,各寄存器赋初值
    if(!rst_n) begin
        state <= IDLE;
        cnt_main <= 3'd0;
        cnt_write <= 6'd0;
        sdio_out <= 1'b0;
        sclk_out <= LOW;
        rclk_out <= LOW;
    end else begin
       
        case(state)
            IDLE:begin
                // 空闲状态,第一个状态,相当于软复位
                state <= MAIN;
                cnt_main <= 3'd0;
                cnt_write <= 6'd0;
                sdio_out <= 1'b0;
                sclk_out <= LOW;
                rclk_out <= LOW;
            end
           
            MAIN:begin
                // cnt_main 累加
                if(cnt_main >= 3'd7) cnt_main <= 3'b0;
                else cnt_main <= cnt_main + 1'b1;
               
                // 对8位数码管进行逐位扫描
                case(cnt_main)
                    3'd0:begin
                        state <= WRITE; // 配置完后跳转至WRITE状态
                       
                        // data_reg[15:8] 是段选(单个数码管显示的内容),data_reg[7:0] 是位选(选择哪一位数码管)
                        data_reg <= {
                            seg[seg_data_1] | (seg_dot_en[0]?8'h80:8'h00), // 段选中的seg_dot_en是小数点显示使能
                            seg_data_en[0] ? 8'hfe : 8'hff
                        };
                    end

                    3'd1:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_2] | (seg_dot_en[1]?8'h80:8'h00),
                            seg_data_en[1] ? 8'hfd : 8'hff
                        };
                    end
                   
                    3'd2:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_3] | (seg_dot_en[2]?8'h80:8'h00),
                            seg_data_en[2] ? 8'hfb : 8'hff
                        };
                    end
                   
                    3'd3:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_4] | (seg_dot_en[3]?8'h80:8'h00),
                            seg_data_en[3] ? 8'hf7 : 8'hff
                        };
                    end
                   
                    3'd4:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_5] | (seg_dot_en[4]?8'h80:8'h00),
                            seg_data_en[4] ? 8'hef : 8'hff
                        };
                    end
                   
                    3'd5:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_6] | (seg_dot_en[5]?8'h80:8'h00),
                            seg_data_en[5] ? 8'hdf : 8'hff
                        };
                    end
                   
                    3'd6:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_7] | (seg_dot_en[6]?8'h80:8'h00),
                            seg_data_en[6] ? 8'hbf : 8'hff
                        };
                    end
                   
                    3'd7:begin
                        state <= WRITE;
                        data_reg <= {
                            seg[seg_data_8] | (seg_dot_en[7]?8'h80:8'h00),
                            seg_data_en[7] ? 8'h7f : 8'hff
                        };
                    end
                   
                    default: state <= IDLE;
                endcase
               
            end
           
            WRITE:begin
                if(clk_div) begin // 按照分频后的速度进行串行更新
                    if(cnt_write >= 6'd33) cnt_write <= 1'b0;
                    else cnt_write <= cnt_write + 1'b1;
                       
                    case(cnt_write)
                        // 74HC595时序实现
                        6'd0:   begin sclk_out <= LOW; sdio_out <= data_reg[15]; end
                        6'd1:   begin sclk_out <= HIGH; end
                        6'd2:   begin sclk_out <= LOW; sdio_out <= data_reg[14]; end
                        6'd3:   begin sclk_out <= HIGH; end
                        6'd4:   begin sclk_out <= LOW; sdio_out <= data_reg[13]; end
                        6'd5:   begin sclk_out <= HIGH; end
                        6'd6:   begin sclk_out <= LOW; sdio_out <= data_reg[12]; end
                        6'd7:   begin sclk_out <= HIGH; end
                        6'd8:   begin sclk_out <= LOW; sdio_out <= data_reg[11]; end
                        6'd9:   begin sclk_out <= HIGH; end
                        6'd10:  begin sclk_out <= LOW; sdio_out <= data_reg[10]; end
                        6'd11:  begin sclk_out <= HIGH; end
                        6'd12:  begin sclk_out <= LOW; sdio_out <= data_reg[9]; end
                        6'd13:  begin sclk_out <= HIGH; end
                        6'd14:  begin sclk_out <= LOW; sdio_out <= data_reg[8]; end
                        6'd15:  begin sclk_out <= HIGH; end
                        6'd16:  begin sclk_out <= LOW; sdio_out <= data_reg[7]; end
                        6'd17:  begin sclk_out <= HIGH; end
                        6'd18:  begin sclk_out <= LOW; sdio_out <= data_reg[6]; end
                        6'd19:  begin sclk_out <= HIGH; end
                        6'd20:  begin sclk_out <= LOW; sdio_out <= data_reg[5]; end
                        6'd21:  begin sclk_out <= HIGH; end
                        6'd22:  begin sclk_out <= LOW; sdio_out <= data_reg[4]; end
                        6'd23:  begin sclk_out <= HIGH; end
                        6'd24:  begin sclk_out <= LOW; sdio_out <= data_reg[3]; end
                        6'd25:  begin sclk_out <= HIGH; end
                        6'd26:  begin sclk_out <= LOW; sdio_out <= data_reg[2]; end
                        6'd27:  begin sclk_out <= HIGH; end
                        6'd28:  begin sclk_out <= LOW; sdio_out <= data_reg[1]; end
                        6'd29:  begin sclk_out <= HIGH; end
                        6'd30:  begin sclk_out <= LOW; sdio_out <= data_reg[0]; end
                        6'd31:  begin sclk_out <= HIGH; end
                        6'd32:  begin rclk_out <= HIGH; end
                        6'd33:  begin rclk_out <= LOW; state <= MAIN; end
                        default: state <= IDLE;
                    endcase
                end else begin
                    sclk_out <= sclk_out;
                    sdio_out <= sdio_out;
                    rclk_out <= rclk_out;
                    cnt_write <= cnt_write;
                    state <= state;
                end
            end
           
            default:begin
                state <= IDLE;
            end
        endcase
       
    end
   
   
   
end

endmodule



仿真:

`timescale 1ns/1ns

module segment_tb;

reg clk, rst_n;
reg [3:0] seg_data_1; // seg1 数码管要显示的数据
reg [3:0] seg_data_2; // seg2
reg [3:0] seg_data_3; // seg3
reg [3:0] seg_data_4; // seg4
reg [3:0] seg_data_5; // seg5
reg [3:0] seg_data_6; // seg6
reg [3:0] seg_data_7; // seg7
reg [3:0] seg_data_8; // seg8
reg [7:0] seg_data_en; // 数码管数据使能[msb seg8 ~ lsb seg1]
reg [7:0] seg_dot_en; // 数码管小数点使能

wire rclk_out; // 74HC595 的 RCK 管脚
wire sclk_out; // 74HC595 的 SCK 管脚
wire sdio_out; // 74HC595 的 SER 管脚


initial begin
clk = 0;
rst_n = 0;
#20
rst_n = 1;


seg_data_1 = 4'd1;
seg_data_2 = 4'd2;
seg_data_3 = 4'd3;
seg_data_4 = 4'd4;
seg_data_5 = 4'd5;
seg_data_6 = 4'd7;
seg_data_7 = 4'd8;
seg_data_8 = 4'd0;
seg_data_en = 8'hfd; // 第二位数码管不使能
seg_dot_en = 8'h00;

end
always #10 clk = ~clk;

led_segment led_segment_inst(
.clk(clk),
.rst_n(rst_n),
.seg_data_1(seg_data_1),
.seg_data_2(seg_data_2),
.seg_data_3(seg_data_3),
.seg_data_4(seg_data_4),
.seg_data_5(seg_data_5),
.seg_data_6(seg_data_6),
.seg_data_7(seg_data_7),
.seg_data_8(seg_data_8),
.seg_data_en(seg_data_en),
.seg_dot_en(seg_dot_en),
.rclk_out(rclk_out),
.sclk_out(sclk_out),
.sdio_out(sdio_out)
);

endmodule

仿真波形

image.png


可观察到:

  • clk_div 分频正常;
  • 第一位数码管显示数字1(0x06),其他位数字类似;
  • 第一位数码管使能,为0xfe,其他类似,只有第二位数码管不使能,因为传递的seg_data_en是0xfd

在板子上测试:

绑定管脚时候发现不能绑定,显示unconnected 不能绑定

image.png


lattice防止信号被优化(管脚分配时定义的引脚被分配到unconnected无关联引脚上导致无法配置) 中看到,加上 /* synthesis syn_force_pads = 1 */ 防止被优化。

原因大概也是因为没有输出所致,最后有输出了也正常了。

顶层代码如下:

module top2(
input clk,
input rst_n,

output rclk_out, // 74HC595 的 RCK 管脚
output sclk_out, // 74HC595 的 SCK 管脚
output sdio_out // 74HC595 的 SER 管脚



);
/* synthesis syn_force_pads = 1 */
wire [3:0] seg_data_1; // seg1 数码管要显示的数据
wire [3:0] seg_data_2; // seg2
wire [3:0] seg_data_3; // seg3
wire [3:0] seg_data_4; // seg4
wire [3:0] seg_data_5; // seg5
wire [3:0] seg_data_6; // seg6
wire [3:0] seg_data_7; // seg7
wire [3:0] seg_data_8; // seg8
wire [7:0] seg_data_en; // 数码管数据使能[msb seg8 ~ lsb seg1]
wire [7:0] seg_dot_en; // 数码管小数点使能

assign seg_data_1 = 4'd1;
assign seg_data_2 = 4'd2;
assign seg_data_3 = 4'd3;
assign seg_data_4 = 4'd4;
assign seg_data_5 = 4'd5;
assign seg_data_6 = 4'd7;
assign seg_data_7 = 4'd8;
assign seg_data_8 = 4'd0;
assign seg_data_en = 8'hfd;
assign seg_dot_en = 8'h00;


led_segment inst(
.clk(clk),
.rst_n(rst_n),
.seg_data_1(seg_data_1),
.seg_data_2(seg_data_2),
.seg_data_3(seg_data_3),
.seg_data_4(seg_data_4),
.seg_data_5(seg_data_5),
.seg_data_6(seg_data_6),
.seg_data_7(seg_data_7),
.seg_data_8(seg_data_8),
.seg_data_en(seg_data_en),
.seg_dot_en(seg_dot_en),
.rclk_out(rclk_out),
.sclk_out(sclk_out),
.sdio_out(sdio_out)
);
endmodule

实际效果,

image.png


疑问项:为了数码管的时序,使用了600分频,暂不清楚原因和时间要求。

2)按键消抖

先试一下单个按键消抖

// 按键功能,消抖

/*
* led,0亮1灭
* key1,按下为0
*/

module key(
input clk,
input rst_n,
input key1,
output reg led0
);

// 系统时钟 50M,一个时钟周期是20ns
// 消抖时间 20ms = 20_000_000 / 20 = 1_000_000周期
// 系统时钟 12M,一个时钟周期是83.333333ns
// 消抖时间 20ms = 20_000_000/83.33333=240000周期,也可用 12M/50(hz)算=240000
parameter CNT_MAX = 20'd240000;
//parameter CNT_MAX = 20'd200;

reg [19:0] cnt;
reg key_d0; // 将按键信号延迟一个时钟周期
reg key_d1; // 将按键信号延迟两个时钟周期
reg key_flag; // 按键状态

// 延迟1,2周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_d0 <= 1'b1;
key_d1 <= 1'b1;
end
else begin
key_d0 <= key1;
key_d1 <= key_d0;
end
end

// 按键值消抖
/* 累加型的,会有问题,在保持一样时还会一直计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= 20'd0;
else begin
// 按键和前一个时刻一样,则计数器开始计数
cnt <= cnt + 1'b1;
end
end
*/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= CNT_MAX;
else //按键和前一个时刻一样,则计数器开始递减到 0
cnt <= (cnt>20'd0) ? (cnt-1'b1) : 20'd0;
end

reg key_flag2; // 按键状态变化,key_flag2 比 key_flag晚一个周期
// 消抖后的值
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_flag <= 1'b0;
key_flag2 <= 1'b0;
end
else if(cnt == 20'd1) begin
key_flag <= key_d1;
key_flag2 <= key_flag;
end
else begin
key_flag <= key_flag;
key_flag2 <= key_flag;
end
end


// 按下时点亮LED
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
led0 <= 1'b1;
else if(key_flag!=key_flag2) begin
if(key_flag==1'b0) // 下降沿,按下
led0 <= ~led0;
end
end


endmodule

仿真:

`timescale 1ns/1ns

module key_tb();

parameter CLK_PERIOD = 5'd20; // 时钟周期 20ns
parameter CNT_MAX = 20'd10; // 消抖时间 10 个周期,200ns

reg clk;
reg rst_n;
reg key;

wire led0;

initial begin
clk <= 1'b0;
rst_n <= 1'b0;
key <= 1'b1;

#200
rst_n <= 1'b1;

// key 信号变化
#20
key <= 1'b1;
#20
key <= 1'b0;
#50
key <= 1'b1;
#40
key <= 1'b0;
#20
key <= 1'b1;
#300
key <= 1'b0;
#50
key <= 1'b1;
#40
key <= 1'b0;
#300
key <= 1'b1;
#30
key <= 1'b0;
#300
key <= 1'b1;
#300
key <= 1'b0;
#40
key <= 1'b1;
#30
key <= 1'b0;

end

// 产生时钟
always #(CLK_PERIOD/2) clk = ~clk;

// 例化
key #(
.CNT_MAX(CNT_MAX)
) key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);

endmodule


image.png



顶层模块中调用测试:

module top(
input clk,
input rst_n,

// key
input key,
output led0

);

key key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);
endmodule

演示效果:

视频效果无。(绑定好led和按键之后,)每次点击按键时,led亮灭切换。

不过发现个bug:复位了之后,第一次点按钮,led不切换,之后第二次以及之后,才会正常切换。

但是我不能在线仿真,1是不会,2是现在是U盘模式,不知道JTAG如何连,所以不能调试。

细细分析了之后,觉得应该是key值初始为0(按下)的问题,修改后好了,并且修改了仿真文件(换了下信号变化的1<->0),问了把过程展示出来,上面的就不删除了,修改后的文件和tb文件如下:

// 按键功能,消抖

/*
* led,0亮1灭
* key1,按下为0
*/

module key(
input clk,
input rst_n,
input key1,
output reg led0
);

// 系统时钟 50M,一个时钟周期是20ns
// 消抖时间 20ms = 20_000_000 / 20 = 1_000_000周期
// 系统时钟 12M,一个时钟周期是83.333333ns
// 消抖时间 20ms = 20_000_000/83.33333=240000周期,也可用 12M/50(hz)算=240000
parameter CNT_MAX = 20'd240000;
//parameter CNT_MAX = 20'd200;

reg [19:0] cnt;
reg key_d0; // 将按键信号延迟一个时钟周期
reg key_d1; // 将按键信号延迟两个时钟周期
reg key_flag; // 按键状态

// 延迟1,2周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_d0 <= 1'b1;
key_d1 <= 1'b1;
end
else begin
key_d0 <= key1;
key_d1 <= key_d0;
end
end

// 按键值消抖
/* 累加型的,会有问题,在保持一样时还会一直计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= 20'd0;
else begin
// 按键和前一个时刻一样,则计数器开始计数
cnt <= cnt + 1'b1;
end
end
*/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= CNT_MAX;
else //按键和前一个时刻一样,则计数器开始递减到 0
cnt <= (cnt>20'd0) ? (cnt-1'b1) : 20'd0;
end

reg key_flag2; // 按键状态变化,key_flag2 比 key_flag晚一个周期
// 消抖后的值
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_flag <= 1'b1;
key_flag2 <= 1'b1;
end
else if(cnt == 20'd1) begin
key_flag <= key_d1;
key_flag2 <= key_flag;
end
else begin
key_flag <= key_flag;
key_flag2 <= key_flag;
end
end


// 按下时点亮LED
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
led0 <= 1'b1;
else if(key_flag!=key_flag2) begin
if(key_flag==1'b0) // 下降沿,按下
led0 <= ~led0;
end
end


endmodule
`timescale 1ns/1ns

module key_tb();

parameter CLK_PERIOD = 5'd20; // 时钟周期 20ns
parameter CNT_MAX = 20'd10; // 消抖时间 10 个周期,200ns

reg clk;
reg rst_n;
reg key;

wire led0;

initial begin
clk <= 1'b0;
rst_n <= 1'b0;
key <= 1'b1;

#200
rst_n <= 1'b1;

// key 信号变化
#20
key <= 1'b0;
#20
key <= 1'b1;
#50
key <= 1'b0;
#40
key <= 1'b1;
#20
key <= 1'b0;
#300
key <= 1'b1;
#50
key <= 1'b0;
#40
key <= 1'b1;
#300
key <= 1'b0;
#30
key <= 1'b1;
#300
key <= 1'b0;
#300
key <= 1'b1;
#40
key <= 1'b0;
#30
key <= 1'b1;

end

// 产生时钟
always #(CLK_PERIOD/2) clk = ~clk;

// 例化
key #(
.CNT_MAX(CNT_MAX)
) key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);

endmodule


3)矩阵键盘

参考 小脚丫- 矩阵键盘键入系统设计

代码如下:(这一版采用的是键盘按下有个过程的时序,后面会改为一个脉冲)

`timescale 1ns/1ns

module keyboard_tb();

parameter CLK_CNT = 5'd20;

reg clk;
reg rst_n;
reg [3:0] col;
wire [3:0] row;
wire [15:0] key_out;
wire [15:0] key_pulse;

initial begin
clk <= 1'b1; // 仿真时候发现这里默认为0的时候,分频后的clk第一个不太准(本来是400结果是390)。这里默认位高,后面就整400了,看着舒服
rst_n <= 1'b0;
col <= 4'b1111;

#200
rst_n <= 1'b1;

// col输入
#100
col <= 4'b0111; // 这里是300ns时赋值的,时钟从400整开始
#1701 // 一个分频后的clk是400ns(20*20),状态机4次循环后是1600 + 100(时钟开始前提前按下了)+1(到这一次时钟这刻赋值,其实应该加一个大clk,但是这里仿真,走个极限)
col <= 4'b1111;
end

// 时钟
always #(CLK_CNT/2) clk = ~clk;

// 例化
keyboard #(
.NUM_FOR_200HZ(CLK_CNT)
) keyboard_inst(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_pulse(key_pulse)
);

endmodule

TestBench:

`timescale 1ns/1ns

module keyboard_tb();

parameter CLK_CNT = 5'd20;

reg clk;
reg rst_n;
reg [3:0] col;
wire [3:0] row;
wire [15:0] key_out;
wire [15:0] key_pulse;

initial begin
clk <= 1'b1; // 仿真时候发现这里默认为0的时候,分频后的clk第一个不太准(本来是400结果是390)。这里默认位高,后面就整400了,看着舒服
rst_n <= 1'b0;
col <= 4'b1111;

#200
rst_n <= 1'b1;

// col输入
#100
col <= 4'b0111; // 这里是300ns时赋值的,时钟从400整开始
#1701 // 一个分频后的clk是400ns(20*20),状态机4次循环后是1600 + 100(时钟开始前提前按下了)+1(到这一次时钟这刻赋值,其实应该加一个大clk,但是这里仿真,走个极限)
col <= 4'b1111;
end

// 时钟
always #(CLK_CNT/2) clk = ~clk;

// 例化
keyboard #(
.NUM_FOR_200HZ(CLK_CNT)
) keyboard_inst(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_pulse(key_pulse)
);

endmodule

仿真波形:

image.png


4)计算器

嫌麻烦不想写tb测试,结果状态机中的按键一直有问题,组合逻辑那边的赋值和比较有问题,准备测试一下,测试了两天~

经验:改好之后和同事聊说我昨晚把最后的结果用next就好了,同事说三段状态机中,第二个一般用组合逻辑描述,第三个可以是组合/时序逻辑,如果是时序逻辑就用next,组合逻辑就用curr。

终于是把输入(按键和算式)搞完了,看下(进行计算之前的)效果:

image.png


之后计算资源利用率的时候,先在不加计算之前的代码统计一次。

计算功能

想着很简单,做起来却有点懵。个位和十位先组起来,能不能使用 v1*10+v2 呢,还有计算完成后,还得拆分十进制各位,又得用除法和取余,都是问题。

看了个B栈视频 https://www.bilibili.com/video/BV1ie411V7xv/?vd_source=695515d886ad153c2a2617c7f5903856

发现乘法可以通过移位和加来实现。和同事聊,同事搜到可以用每一位移位来运算,先搞这个

因为个位十位需要这个转换。

那么乘10就等于 <<3 + a +a

拼接

v11 是十位,v12是个位,v1是要拼接的结果数

v1 = v11*10 + v12

v1 = v118 + v112 + v12

v1 = v11<<3 + v11<<1 + v12

verilog: v1 <= {v11, 3'b0} + {v11, 1'b0} + v12;

仿真:

// 数据拼接

module calc_1(
input clk,
input rst_n,
input [3:0] v11,
input [3:0] v12,
output reg [7:0] v1
);

always @(posedge clk or negedge rst_n) begin
v1 <= {v11, 3'b0} + {v11, 1'b0} + v12;
end

endmodule

module calc_1_tb();

reg clk;
reg rst_n;
reg [3:0] v11;
reg [3:0] v12;
wire [7:0] vout;

initial begin
clk <= 1'b0;
rst_n <= 1'b0;

#100
rst_n <= 1'b1;

#200
v11 <= 4'd6;
v12 <= 4'd7;
end
always #(200) clk = ~clk;

calc_1 calc_1_inst(
.clk(clk),
.rst_n(rst_n),
.v11(v11),
.v12(v12),
.v1(vout)
);

endmodule

image.png


除法:

image.png


看着麻烦,其实写起来也很简单。

乘法和除法如下:

// 8位除法,有符号

module calc_div(
input [7:0] A, // 被除数
input [7:0] B, // 除数
output reg [7:0] Q, // 商
output reg [7:0] R, // 余数
output reg neg // 负号
);

// 除法
/*
reg [7:0] A; // 被除数
reg [7:0] B; // 除数
reg [7:0] R; // 余数
reg [7:0] Q; // 商
*/
reg [7:0] i;
reg [7:0] A2;
reg [7:0] B2;
always @(*) begin
/*
A = v1;
B = v2;
*/
R = 8'b0;
Q = 8'b0;
neg = 1'b0;

// 负数转正数
if(A[7] == 1'b1) begin
A2 = {1'b0, ~A[6:0]} + 1'b1;

end else
A2 = A;
if(B[7] == 1'b1)
A2 = {1'b0, ~B[6:0]} + 1'b1;
else
B2 = B;

for(i=1; i<=7; i=i+1) begin
R = {R[6:0], A2[7-i]}; // 从A中移一位到R中
if(R>=B) begin
Q[7-i] = 1'b1;
R = R - B;

end else begin
Q[7-i] = 1'b0;
end
end

// 负数再转回去
if(A[7]^B[7]) begin
// Q = {1'b1, ~Q[6:0]} + 1'b1; //不在商里面加负号,因为-0没法表示,0和-0补码一样
neg = 1'b1;
end
end

endmodule
// 8位乘法

module calc_mul(
input [7:0] A, // 被乘数
input [7:0] B, // 乘数
output reg [15:0] out // 乘积
);

integer i;
reg [15:0] A2; // 扩展
reg [15:0] B2;
always @(*) begin

out = 16'b0;

// 负数扩展
if(A[7]==1'b1)
A2 = {8'hff, A};
else
A2 = A;
if(B[7]==1'b1)
B2 = {8'hff, B};
else
B2 = B;

for(i=0; i<=15; i=i+1) begin
if(B2[i]==1) begin
out = out + (A2<<i);
end
end

end

endmodule

再把逻辑微调了几天,终于完成两位10进制四则运算计算器,虽然还有点瑕疵(对负数支持不到位等),但基本可用。除法也能显示一位小数了。



4. 实现结果

加法 12+86=98

image.png


减法

12-5=7

image.png


0-54 = -54

image.png

乘法 29*3=87

image.png


除法 12/5=2.4

image.png

5. 追加:加法和乘法逻辑完善

因为之前偷懒了,两位数的加法和乘法,结果最多为3或4位,我都丢弃掉了,之保留了两位。这样肯定是不行的。这里将之前除法、乘法(这两项只是扩展一下位数即可)、数据拆分都扩展一下就行了。

数据拆分代码如下:

// 最多四位十进制数字的(个位和十位)拆分

module num_split(
input [15:0] v,
output [3:0] thd,
output [3:0] hud,
output [3:0] ten,
output [3:0] one,
output neg // 负号
);

// 调用除法,将vout输出位两位
wire [7:0] Q,Q2,Q3;
wire [7:0] R,R2,R3;
wire neg,neg2,neg3;

calc_div calc_div_inst_numsplit(
.A(v),
.B(8'd10),
.Q(Q),
.R(R),
.neg(neg)
);
assign one = R[3:0];

calc_div calc_div_inst_numsplit2(
.A(Q),
.B(8'd10),
.Q(Q2),
.R(R2),
.neg(neg2)
);
assign ten = R2[3:0];

calc_div calc_div_inst_numsplit3(
.A(Q2),
.B(8'd10),
.Q(Q3),
.R(R3),
.neg(neg3)
);
assign hud = R3[3:0];
assign thd = Q3[3:0];



endmodule

除法代码

// 8位除法,有符号

module calc_div(
input [15:0] A, // 被除数
input [15:0] B, // 除数
output reg [15:0] Q, // 商
output reg [15:0] R, // 余数
output reg neg // 负号
);

// 除法
/*
reg [7:0] A; // 被除数
reg [7:0] B; // 除数
reg [7:0] R; // 余数
reg [7:0] Q; // 商
*/
reg [7:0] i;
reg [15:0] A2;
reg [15:0] B2;
always @(*) begin
/*
A = v1;
B = v2;
*/
R = 16'b0;
Q = 16'b0;
neg = 1'b0;

// 负数转正数
if(A[15] == 1'b1) begin
A2 = {1'b0, ~A[14:0]} + 1'b1;
end else
A2 = A;

if(B[15] == 1'b1)
B2 = {1'b0, ~B[14:0]} + 1'b1;
else
B2 = B;

for(i=1; i<=15; i=i+1) begin
R = {R[14:0], A2[15-i]}; // 从A中移一位到R中
if(R>=B) begin
Q[15-i] = 1'b1;
R = R - B;

end else begin
Q[15-i] = 1'b0;
end
end

// 负数再转回去
if(A[15]^B[15]) begin
neg = 1'b1;
end
end

endmodule

6. 资源使用情况

Design Summary
Number of registers: 244 out of 4635 (5%)
PFU registers: 244 out of 4320 (6%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1262 out of 2160 (58%)
SLICEs as Logic/ROM: 1262 out of 2160 (58%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 550 out of 2160 (25%)
Number of LUT4s: 2518 out of 4320 (58%)
Number used as logic LUTs: 1418
Number used as distributed RAM: 0
Number used as ripple logic: 1100
Number used as shift registers: 0
Number of PIO sites used: 13 + 4(JTAG) out of 105 (16%)
Number of block RAMs: 0 out of 10 (0%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 0 out of 2 (0%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)


参考



附件下载
calc_02优化加法和乘法位数.rar
团队介绍
姜小豆(即本人),FPGA初学者,对FPGA以及能做的项目特别感兴趣。
团队成员
姜小豆
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号