明德扬吴老师 发表于 2020-8-28 00:11:41

【每周FPGA案例】 至简设计系列_简易计算器 编号:000600000010

至简设计系列_简易计算器

本案例的编号为:000600000010如果有疑问,请按编号在下面贴子查找答案:MDY案例交流【汇总贴】_FPGA-明德扬科教 (mdy-edu.com)
本文为明德扬原创及录用文章,转载请注明出处!简易计算器

【上板现象】简易计算器在点拨开发板的上板现象
https://www.bilibili.com/video/BV1Af4y117H4?p=31

简易计算器在实验箱的上板现象
https://www.bilibili.com/video/BV1Af4y117H4?p=32--作者:小黑同学
本文为明德扬原创及录用文章,转载请注明出处!
1.1 总体设计

1.1.1 概述

计算器是近代人发明的可以进行数字运算的机器。现代的电子计算器能进行数学运算的手持电子机器,拥有集成电路芯片,但结构比电脑简单得多,可以说是第一代的电子计算机,且功能也较弱,但较为方便与廉价,可广泛运用于商业交易中,是必备的办公用品之一。除显示计算结果外,还常有溢出指示、错误指示等。计算器电源采用交流转换器或电池。为了节省电能,计算器都采用CMOS工艺制作的大规模集成电路。
计算器一般由运算器、控制器、存储器、键盘、显示器、电源和一些可选外围设备及电子配件,通过人工或机器设备组成,抵挡计算器的运算器、控制器由数字逻辑电路实现简单的串行运算计算器是最早的计算工具,例如:古代印加人利用许多颜色的绳结来计数或者记录历史,还有古希腊人的安提凯希拉装置,中国的算盘等。中国古代最早采用的一种计算工具叫筹策,又被叫做算筹。
1.1.2 设计目标简易计算器支持简单的四则运算(支持负数),在此基础上,添加了连续运算功能。计算器面板如下:
1、计算器通过矩阵键盘模拟按键输入,并通过数码管显示。2、计算器有“0、1、2、3、4、5、6、7、8、9、+、-、*、/、C、=”共16个按键。3、计算器不支持输入负数,运算结果支持负数但不支持小数。4、运算数1、运算数2以及运算结果最大支持8位。其中,运算数1和运算结果的位数包括符号位“-”。5、运算数1和运算数2的默认值为0.6、计算器支持连续运算,允许在输入运算数2后按下运算符,或者得出运算结果后按下运算符。7、当运算结果溢出时,数码管显示8个F。8、当操作数1或者操作数2的长度溢出时,蜂鸣器会响。
1.1.3 系统结构框图
系统结构框图如下所示:
图一1.1.4模块功能


键盘扫描模块实现功能
1、将外来异步信号打两拍处理,将异步信号同步化。2、实现20ms按键消抖功能。3、实现矩阵键盘的按键检测功能,并输出有效按键信号。

工作状态选择模块实现功能
1、根据接收的不同的按键信号,判断和决定计算器的工作状态。共有5种状态,输入运算数1(OP_1)、运算符(OPER)、输入运算数2(OP_2)、输出结果(RESULT)、结果错误(ERROR)
Ø运算数1模块实现功能1、当计算器处于运算数1状态下,任何连续输入的数字(不超过8位)都将存放在该模块中,作为运算数1.2、当运算数已经到达8位时,此时无论输入任何数字,运算数1不变。3、当计算器经过一次运算后(按下等号或者在运算数2状态下按下运算符),运算数去存放结果result。
Ø运算符模块实现功能
1、保存最新按下的运算符。
Ø运算数2模块实现功能
1、当计算器处于运算数2状态下,任何连续输入的数字(不超过8位)都将存放在该模块中,作为运算数2,默认值为0。2、当运算数2已经到达8(包括符号位“-”),此时无论输入任何数字,运算数2不变。
Ø运算单元模块实现功能
1、当计算器处于运算数2状态下按下运算符或者在任何状态下按下等号时,该模块根据此时运算数1、运算数2以及运算符的值,进行运算。2、若运算结果溢出,或者长度大于8位(包括符号位“-”)或者除数为0时,输出8个F。3、最多保留运算结果整数部分的8个有效数字,不保留任何小数。
Ø显示对象选则模块实现功能
1、该模块的作用是根据当前计算器的工作状态来选择数码管的显示内容。
Ø数码管显示模块实现功能
1、该模块的作用是对显示对象选择模块的显示数据输出信号进行数码管显示。
Ø蜂鸣器模块实现功能
1、该模块的作用是对各种错误输入或输出进行响铃警告。
1.1.5顶层信号


信号名接口方向定义
clk输入系统时钟,50Mhz
rst_n输入低电平复位信号
Key_col输入4位矩阵键盘列信号,默认高电平,开发板按键为普通按键时,不需要该信号
Key_row输出4位矩阵键盘行信号,默认低电平,开发板按键为普通按键时,不需要该信号
Segment输出8位数码管段选信号
Seg_sel输出8位数码管位选信号
beep输出1位蜂鸣器控制信号

1.1.6参考代码modulecalc_project(
    clk   ,
    rst_n   ,
    key_col ,
    key_row ,
    seg_sel ,
    segment ,
    beep   
    );

    parameter   KEY_WID   =   4   ;
    parameter   STATE_WID   =   5   ;
    parameter   NUM_WID   =   27;
    parameter   SEG_NUM   =   8   ;
    parameter   SEG_WID   =   8   ;

    input                     clk         ;
    input                     rst_n       ;
    input          key_col   ;
    output       key_row   ;
    output       seg_sel   ;
    output       segment   ;
    output                      beep      ;

    wire           key_num   ;
    wire                        key_vld   ;
    wire           key_num_out ;
    wire           key_vld_out ;
    wire       state_c   ;
    wire           op_1      ;
    wire                        op_1_err    ;
    wire           oper      ;
    wire           op_2      ;
    wire                        op_2_err    ;
    wire           result      ;
    wire                        result_err;
    wire                        result_neg;   
    wire       display   ;
    wire                        display_vld ;

    key_scan   key_scan_prj(
                            .clk      (clk    )   ,
                            .rst_n      (rst_n)   ,
                            .key_col    (key_col)   ,
                            .key_row    (key_row)   ,
                            .key_out    (key_num)   ,
                            .key_vld    (key_vld)
                            );

    work_statework_state_prj(
                              .clk      (clk      )   ,
                              .rst_n      (rst_n      )   ,
                              .key_num    (key_num    )   ,
                              .key_vld    (key_vld    )   ,
                              .result_err (result_err )   ,
                              .key_num_out(key_num_out)   ,
                              .key_vld_out(key_vld_out)   ,
                              .state_c    (state_c    )
                              );

    op_1    op_1_prj(
                        .clk      (clk      )   ,
                        .rst_n      (rst_n      )   ,
                        .key_num    (key_num_out)   ,
                        .key_vld    (key_vld_out)   ,
                        .state_c    (state_c    )   ,
                        .result   (result   )   ,
                        .op_1       (op_1       )   ,
                        .op_1_err   (op_1_err   )
                  );

    oper    oper_prj(
                        .clk      (clk      )   ,
                        .rst_n      (rst_n      )   ,
                        .key_num    (key_num_out)   ,
                        .key_vld    (key_vld_out)   ,
                        .state_c    (state_c    )   ,
                        .oper       (oper       )
                  );
   
    op_2    op_2_prj(
                        .clk      (clk      )   ,
                        .rst_n      (rst_n      )   ,
                        .key_num    (key_num_out)   ,
                        .key_vld    (key_vld_out)   ,
                        .state_c    (state_c    )   ,
                        .op_2       (op_2       )   ,
                        .op_2_err   (op_2_err   )
                  );

    resultresult_prj(
                        .clk      (clk      )   ,
                        .rst_n      (rst_n      )   ,
                        .key_num    (key_num_out)   ,
                        .key_vld    (key_vld_out)   ,
                        .state_c    (state_c    )   ,
                        .op_1       (op_1       )   ,
                        .oper       (oper       )   ,
                        .op_2       (op_2       )   ,
                        .result   (result   )   ,
                        .result_err (result_err )   ,
                        .result_neg (result_neg )
                  );

    display_seldisplay_sel_prj(
                              .clk      (clk      )   ,
                              .rst_n      (rst_n      )   ,
                              .state_c    (state_c    )   ,
                              .op_1       (op_1       )   ,
                              .op_2       (op_2       )   ,
                              .result_neg (result_neg )   ,
                              .display    (display    )   ,
                              .display_vld(display_vld)
                              );

    segmentsegment_prj(
                            .rst_n      (rst_n      )   ,
                            .clk      (clk      )   ,
                            .display    (display    )   ,
                            .display_vld(display_vld)   ,
                            .seg_sel    (seg_sel    )   ,
                            .segment    (segment    )
                        );

    beep    beep_prj(
                        .clk      (clk      )   ,
                        .rst_n      (rst_n      )   ,
                        .op_1_err   (op_1_err   )   ,
                        .op_2_err   (op_2_err   )   ,
                        .result_err (result_err )   ,
                        .beep       (beep       )
                  );

endmodule

1.2 键盘扫描模块设计1.2.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
key_col输入矩阵键盘列输入信号
Key_row输出矩阵键盘行输出信号
Key_out输出按键位置输出信号,key_vld有效时,该信号有效。
Key_vld输出按键有效指示信号,高电平有效


1.2.2 设计思路
在前面的案例中已经有矩阵键盘的介绍,所以这里不在过多介绍,详细介绍请看下方链接:http://fpgabbs.com/forum.php?mod=viewthread&tid=310
1.2.3参考代码always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_col_ff0 <= 4'b1111;
      key_col_ff1 <= 4'b1111;
    end
    else begin
      key_col_ff0 <= key_col    ;
      key_col_ff1 <= key_col_ff0;
    end
end


always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      shake_cnt <= 0;
    end
    else if(add_shake_cnt) begin
      if(end_shake_cnt)
            shake_cnt <= 0;
      else
            shake_cnt <= shake_cnt+1 ;
   end
end
assign add_shake_cnt = key_col_ff1!=4'hf;
assign end_shake_cnt = add_shake_cnt&& shake_cnt == TIME_20MS-1 ;


always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      state_c <= CHK_COL;
    end
    else begin
      state_c <= state_n;
    end
end

always@(*)begin
    case(state_c)
      CHK_COL: begin
                     if(col2row_start )begin
                         state_n = CHK_ROW;
                     end
                     else begin
                         state_n = CHK_COL;
                     end
               end
      CHK_ROW: begin
                     if(row2del_start)begin
                         state_n = DELAY;
                     end
                     else begin
                         state_n = CHK_ROW;
                     end
               end
      DELAY :begin
                     if(del2wait_start)begin
                         state_n = WAIT_END;
                     end
                     else begin
                         state_n = DELAY;
                     end
               end
      WAIT_END: begin
                     if(wait2col_start)begin
                         state_n = CHK_COL;
                     end
                     else begin
                         state_n = WAIT_END;
                     end
                  end
       default: state_n = CHK_COL;
    endcase
end
assign col2row_start = state_c==CHK_COL&& end_shake_cnt;
assign row2del_start = state_c==CHK_ROW&& row_index==3 && end_row_cnt;
assign del2wait_start= state_c==DELAY    && end_row_cnt;
assign wait2col_start= state_c==WAIT_END && key_col_ff1==4'hf;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_row <= 4'b0;
    end
    else if(state_c==CHK_ROW)begin
      key_row <= ~(1'b1 << row_index);
    end
    else begin
      key_row <= 4'b0;
    end
end





always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      row_index <= 0;
    end
    else if(add_row_index) begin
      if(end_row_index)
            row_index <= 0;
      else
            row_index <= row_index+1 ;
   end
   else if(state_c!=CHK_ROW)begin
       row_index <= 0;
   end
end
assign add_row_index = state_c==CHK_ROW && end_row_cnt;
assign end_row_index = add_row_index&& row_index == 4-1 ;


always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      row_cnt <= 0;
    end
    else if(add_row_cnt) begin
      if(end_row_cnt)
            row_cnt <= 0;
      else
            row_cnt <= row_cnt+1 ;
   end
end
assign add_row_cnt = state_c==CHK_ROW || state_c==DELAY;
assign end_row_cnt = add_row_cnt&& row_cnt == 16-1 ;



always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_col_get <= 0;
    end
    else if(state_c==CHK_COL && end_shake_cnt ) begin
      if(key_col_ff1==4'b1110)
            key_col_get <= 0;
      else if(key_col_ff1==4'b1101)
            key_col_get <= 1;
      else if(key_col_ff1==4'b1011)
            key_col_get <= 2;
      else
            key_col_get <= 3;
    end
end


always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_out <= 0;
    end
    else if(state_c==CHK_ROW && end_row_cnt)begin
      key_out <= {row_index,key_col_get};
    end
    else begin
      key_out <= 0;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_vld <= 1'b0;
    end
    else if(state_c==CHK_ROW && end_row_cnt && key_col_ff1==1'b0)begin
      key_vld <= 1'b1;
    end
    else begin
      key_vld <= 1'b0;
    end
end



1.3 工作状态选择模块设计1.3.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
key_vld输入按键按下指示信号
Key_num输入按键位置输入信号,key_vld有效时,该信号有效
Result_err输入运算结果错误指示信号
Key_num_out输出计算器按下位置输出信号,key_vld_out有效时,该信号有效。
Key_vld_out输出计算器按键按下有效指示信号,高电平有效。
State_c输出计算器工作状态指示信号

1.3.2设计思路
该模块的主要功能是根据按下的按键进行不同来判断和决定计算器的工作状态。一条等式可以写成:运算数1+操作符+运算数2+等号+结果的形式。考虑到结果错误的情况,我将本模块的状态划分为5个,分别是:输入运算数1(OP_1)、运算符(OPER)、输入运算数2(OP_2)、输出结果(RESULT)、结果错误(ERROR)。
下图为本模块的状态跳转图:

复位后,状态机进入OP_1状态,即初始状态为OP_1;
在OP_1状态下:A.按下等号,跳到RESULT状态;B.按下运算符,跳到OPER状态;在OPER状态下:A.按下数字,跳到OP_2状态;B.按下等号,跳到RESULT状态;在OP_2状态下:A.按下等号,跳到RESULT状态;B.按下运算符,跳到OPER状态;在RESULT状态下:A.按下数字,跳到OP_1状态;B.按下运算符,跳到OPER状态;C.按下等号,停留在RESULT状态;在ERROR状态下:A.按下数字,跳到OP_1状态;B.按下其他按键,停留在ERROR状态;无论当前处于什么状态,只要检测到运算结果错误指示信号有效,即刻跳转到ERROR状态。
1.3.3参考代码
使用GVIM,在命令模式下输入如下内容,即可生成本模块所需要的状态机代码。

使用明德扬的状态机模板,可以很快速的写出此模块代码。
always@(*)begin
    case(key_num)
      4'd0   :key_num_chg = 4'd7   ;
      4'd1   :key_num_chg = 4'd8   ;
      4'd2   :key_num_chg = 4'd9   ;
      4'd3   :key_num_chg = 4'd10;
      4'd7   :key_num_chg = 4'd11;
      4'd8   :key_num_chg = 4'd1   ;
      4'd9   :key_num_chg = 4'd2   ;
      4'd10:key_num_chg = 4'd3   ;
      4'd11:key_num_chg = 4'd14;
      4'd12:key_num_chg = 4'd0   ;
      4'd13:key_num_chg = 4'd12;
      4'd14:key_num_chg = 4'd13;
      default:key_num_chg = key_num;
    endcase
end

assignkey_num_en = (key_num_chg==0 || key_num_chg==1 || key_num_chg==2 || key_num_chg==3 || key_num_chg==4 || key_num_chg==5 || key_num_chg==6 || key_num_chg==7 || key_num_chg==8 || key_num_chg==9) && key_vld==1;
assignkey_op_en = (key_num_chg==10 || key_num_chg==11 || key_num_chg==12 || key_num_chg==13) && key_vld==1;
assignkey_cal_en = key_num_chg==15 && key_vld==1;
assignkey_back_en = key_num_chg==14 && key_vld==1;



always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      state_c <= OP_1 ;
    end
    else begin
      state_c <= state_n;
   end
end

always @(*) begin
    if(result_err)begin
       state_n = ERROR;
   end
   else begin
       case(state_c)
      OP_1 :begin
            if(op_12oper_start)
                state_n = OPER ;
            else if(op_12result_start)
                state_n = RESULT ;
            else
                state_n = state_c ;
      end
      OPER :begin
            if(oper2op_2_start)
                state_n = OP_2 ;
            else if(oper2result_start)
                state_n = RESULT ;
            else
                state_n = state_c ;
      end
      OP_2 :begin
            if(op_22oper_start)
                state_n = OPER ;
            else if(op_22result_start)
                state_n = RESULT ;
            else
                state_n = state_c ;
      end
      RESULT :begin
            if(result2op_1_start)
                state_n = OP_1 ;
            else if(result2oper_start)
                state_n = OPER ;
            else
                state_n = state_c ;
      end
      ERROR :begin
            if(error2op_1_start)
                state_n = OP_1 ;
            else
                state_n = state_c ;
      end
      default : state_n = OP_1 ;
    endcase
end
end

assign op_12oper_start   = state_c==OP_1   && key_op_en ;
assign op_12result_start = state_c==OP_1   && key_cal_en;
assign oper2op_2_start   = state_c==OPER   && key_num_en;
assign oper2result_start = state_c==OPER   && key_cal_en;
assign op_22oper_start   = state_c==OP_2   && key_op_en ;
assign op_22result_start = state_c==OP_2   && key_cal_en;
assign result2op_1_start = state_c==RESULT && key_num_en;
assign result2oper_start = state_c==RESULT && key_op_en ;
assign error2op_1_start= state_c==ERROR&& key_num_en;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_num_out <= 0;
    end
    else begin
      key_num_out <= key_num_chg;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      key_vld_out <= 0;
    end
    else begin
      key_vld_out <= key_vld;
    end
end



1.4 运算数1模块设计1.4.1接口信号
信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Key_num_out输入计算器按下位置输出信号,key_vld_out有效时,该信号有效。
Key_vld_out输入计算器按键按下有效指示信号,高电平有效。
State_c输入计算器工作状态指示信号
Op_1输出运算数1输出信号
Result输入运算结果输出信号
Op_1_err输出运算数1溢出信号

1.4.2设计思路该模块主要的作用是根据当前状态和输入的按键,来决定运算数1要输出的结果。由于本工程需要实现连续运算的功能,所以在这个模块中要区分是否已经得出了运算结果。
下面是计算完成指示信号flag_calc的设计思路:1、 该信号为高时,表示完成一次计算过程得到了结果。初始状态为低电平;2、 当输入操作数2后又按下了等号或者其他操作符的时候,变为高电平,所以变高的条件为(state_c_ff==OP_2 && state_c==OPER) || state_c==RESULT;3、 当处在操作数1状态时,为低电平,所以变低的条件为state_c==OP_1。其他情况保持不变。
下面是运算数1输出信号op_1的设计思路:1、 该信号表示运算数1要输出的值。初始状态为0;2、 在结果错误状态的时候,给一个不超过范围的任意值,此代码中给的10;3、 在得到计算结果或者计算结果错误的时候,输入数字,输出为按下按键的对应值(key_num_out);4、 在输入操作数1之后,按下退格键,op_1输出的值除以10进行取整;5、 在输入操作数1状态下通过键盘输入数字,需要判断是否超过显示范围,如果没有超过的话就需要将当前op_1的值乘以10,然后加上按下的数字的值,进行输出;6、 当计算完成时,即flag_calc==1,操作数1输出计算的结果result;7、 其他时侯操作数1保持不变。
下面是运算数1溢出信号op_1_err的设计思路:1、 初始状态为0,表示没有溢出。2、 当一直处于操作数1状态,按下键盘输入数字之后,操作数1的值溢出了,则将运算数1溢出信号拉高。3、 其他时刻保持为低电平。assignkey_num_en = (key_num==0 || key_num==1 || key_num==2 || key_num==3 || key_num==4 || key_num==5 || key_num==6 || key_num==7 || key_num==8 || key_num==9) && key_vld==1;
assignkey_op_en = (key_num==10 || key_num==11 || key_num==12 || key_num==13) && key_vld==1;
assignkey_cal_en = key_num==15 && key_vld==1;
assignkey_back_en = key_num==14 && key_vld==1;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      state_c_ff <= 0;
    end
    else begin
      state_c_ff <= state_c;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      op_2 <= 0;
    end
    else if(state_c==OPER)begin
      op_2 <= 0;
    end
    else if(state_c_ff==OPER && state_c==OP_2)begin
      op_2 <= key_num;
    end
    else if(state_c==OP_2 && key_back_en==1)begin
      op_2 <= op_2 / 10;
    end
    else if(state_c==OP_2 && key_num_en==1)begin
      op_2 <= (op_2>9999999) ? op_2 : (op_2*10+key_num);
    end
    else begin
      op_2 <= op_2;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      op_2_err <= 0;
    end
    else if(state_c==OP_2 && key_num_en==1 && op_2>9999999)begin
      op_2_err <= 1'b1;
    end
    else begin
      op_2_err <= 1'b0;
    end
end

endmodule
1.4.3参考代码

1.5 运算符模块设计1.5.1接口信号
信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Key_num_out输入计算器按下位置输出信号,key_vld_out有效时,该信号有效。
Key_vld_out输入计算器按键按下有效指示信号,高电平有效。
State_c输入计算器工作状态指示信号
oper输出运算符输出信号

1.5.2设计思路本模块的设计思路比较简单,只需要判断哪些按键是运算符,然后再这些运算符被按下的时候,将他们对应的值输出就可以了。下面是运算符指示信号设计思路:1、 当“加”“减”“乘”“除”四个按键的任意一个被按下之后,该信号置为高电平;2、 当“加”“减”“乘”“除”四个按键没有一个被按下的时候,该信号置为低电平。下面是运算符输出信号oper设计思路:初始状态,该信号输出0;1、 当处于操作数1状态时,输出0;2、 当“加”“减”“乘”“除”任意按键被按下之后,输出该按键对应的值;3、 其他时候保持不变;
1.5.3参考代码
assignkey_op_en = (key_num==10 || key_num==11 || key_num==12 || key_num==13) && key_vld==1;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      oper <= 0;
    end
    else if(state_c==OP_1)begin
      oper <= 0;
    end
    else if(key_op_en==1)begin
      oper <= key_num;
    end
    else begin
      oper <= oper;
    end
end

1.6 运算数2模块设计1.6.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Key_num_out输入计算器按下位置输出信号,key_vld_out有效时,该信号有效。
Key_vld_out输入计算器按键按下有效指示信号,高电平有效。
State_c输入计算器工作状态指示信号
Op_2输出运算数2输出信号
Op_2_err输出运算数2溢出信号

1.6.2设计思路
该模块主要的作用是根据当前状态和输入的按键,来决定运算数2要输出的结果。下面是运算数2输出信号op_2的设计思路:
1、 该信号表示运算数2要输出的值。初始状态为0;2、 在运算符状态下,此时数码管不显示运算数2的值,让它输出0;3、 输入运算符之后,之后再输入的就是运算数2的值,此时运算数2就等于按下按键所对应的数值。4、 在输入运算数2之后,按下退格键,运算数2的值除以10进行取整;5、 在输入运算数2状态下通过键盘输入数字,需要判断是否超过显示范围,如果没有超过的话就需要将当前运算数2的值乘以10,然后加上按下的数字的值,进行输出;6、 其他时侯运算数2保持不变。
下面是运算数2溢出信号op_2_err的设计思路:
1、 初始状态为0,表示没有溢出。2、 当一直处于运算数2状态,按下键盘输入数字之后,运算数2的值溢出了,则将运算数2溢出信号拉高。3、 其他时刻保持为低电平。
1.6.3参考代码
assignkey_num_en = (key_num==0 || key_num==1 || key_num==2 || key_num==3 || key_num==4 || key_num==5 || key_num==6 || key_num==7 || key_num==8 || key_num==9) && key_vld==1;
assignkey_op_en = (key_num==10 || key_num==11 || key_num==12 || key_num==13) && key_vld==1;
assignkey_cal_en = key_num==15 && key_vld==1;
assignkey_back_en = key_num==14 && key_vld==1;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      state_c_ff <= 0;
    end
    else begin
      state_c_ff <= state_c;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      flag_calc <= 0;
    end
    else if(state_c==OP_1)begin
      flag_calc <= 1'b0;
    end
    else if(state_c_ff==OP_2 && state_c==OPER || state_c==RESULT)begin
      flag_calc <= 1'b1;
    end
    else begin
      flag_calc <= flag_calc;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      op_1 <= 0;
    end
    else if(state_c==ERROR)begin
      op_1 <= 10;
    end
    else if((state_c_ff==RESULT || state_c_ff==ERROR) && state_c==OP_1)begin
      op_1 <= key_num;
    end
    else if(state_c==OP_1 && key_back_en==1)begin
      op_1 <= op_1 / 10;
    end
    else if(state_c==OP_1 && key_num_en==1)begin
      op_1 <= (op_1>9999999) ? op_1 : (op_1*10+key_num);
    end
    else if(flag_calc==1)begin
      op_1 <= result;
    end
    else begin
      op_1 <= op_1;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      op_1_err <= 0;
    end
    else if(state_c==OP_1&& key_num_en==1 && op_1>9999999)begin
      op_1_err <= 1'b1;
    end
    else begin
      op_1_err <= 1'b0;
    end
end
1.7 运算单元模块设计1.7.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Key_num_out输入计算器按下位置输出信号,key_vld_out有效时,该信号有效。
Key_vld_out输入计算器按键按下有效指示信号,高电平有效。
State_c输入计算器工作状态指示信号
oper输出运算符输出信号
Op_1输入运算数1输入信号
Op_2输入运算数2输入信号
Result输出运算结果输出信号
Result_err输出运算结果错误信号,运算结果溢出或者除数为0时,该信号输出一个时钟周期的高电平
Result_neg输出运算结果符号位指示信号,当运算结果为负数时,该信号为高电平

1.7.2设计思路
本模块的作用是根据运算符,对运算数1和运算数2进行操作得出结果。由于再进行计算的时候考虑小数减去大数的情况,所以运算结果允许为负数,因此需要有符号位指示信号,下面是运算结果符号位指示信号result_neg的设计思路:
1、 只有当运算结果为负数的时候,才显示“负号”,因此初始状态为低电平;2、 当算式输入完成按下等号之后,如果运算符是“减”,并且运算数1小于运算数2,则运算结果为负数,将result_neg信号拉高。3、 由于该计算器支持连续输入,如果当前计算的结果为负数,接着输入的运算符为“加”,下一次进行加法运算,并且运算数1(此时比较不考虑符号位)小于或等于运算数2,则表示运算结果为正数,此时将result_neg信号拉低。4、 在进行连续计算的时候,如果得到的结果超过显示上限,要进入错误状态,这个时候符号位指示信号应该为低电平。5、 无论在计算中得到的结果是正还是负,如果下次输入的为运算数1,都需要result_neg信号为低电平。6、 由于除法不支持小数显示,只取整数部分,所以当运算结果为负数,并进行除法运算的时候,如果得到的结果为0,不应该显示为“负0”,应当将符号位指示信号置为低电平。
本模块主要的功能是实现加减乘除运算,下面是对运算结果输出信号result的设计思路:
1、 初始状态没有经过计算,自然输出为0。2、 在进行加法的时候,由于存在连续计算的情况,需要考虑符号位。当符号位指示信号为0,直接将运算数1和运算数2相加即可;当符号位指示信号为1,则需要判断运算数1和运算数2的大小,确保是大的减去小的。3、 在进行减法的时候,同样需要考虑符号位。当符号位指示信号为0的时候,需要判断运算数1和运算数2的大小,保证大的减去小的;当符号位指示信号位1的时候,直接将运算数1和运算数2相加即可。4、 乘法运算直接将运算数1和运算数2相乘即可。5、 在进行除法运算时,由于无法表示小数,因此这里需要采用运算数1除以运算数2取整的方法,即op_1/op_2。
在计算过程中,如果得到的结果超过显示上限或者错误的计算方法,需要做出错误提示,下面是对于运算结果错误信号result_err的设计思路:
1、初始状态下,该信号为0,表示没有错误。2、得到运算结果后,若继续输入数字,则会进入到运算数1状态,这个时候不进行错误提示。3、在运算数2状态下输入运算符,或者在结果不是错误的状态下输出“等号”(表示进行连续计算)。根据输入的运算符进行相应的判断:加:如果运算结果为正数,则判断运算数1加上运算数2之后会不会溢出,若溢出则做出错误提示;如果运算结果为负数,则不进行错误提示。减:如果运算结果为负数,则判断运算数1加上运算数2之后会不会溢出,若溢出则做出错误提示;如果运算结果为正数,则判断两个数相减之后的结果是否会溢出。乘:无论运算结果为何值,都只需要判断两数相乘之后的的结果会不会溢出就可以了。除:在进行除法运算的时候,需要避免出现除数为0的情况,如果出现此情况,则进行错误指示。

1.7.3参考代码assignkey_op_en = (key_num==10 || key_num==11 || key_num==12 || key_num==13) && key_vld==1;
assignkey_cal_en = key_num==15 && key_vld==1;
assigncalculate = (state_c_ff==OP_2 && state_c==OPER || key_cal_en==1);

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      state_c_ff <= 0;
    end
    else begin
      state_c_ff <= state_c;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      result <= 0;
    end
    else if(calculate==1)begin
      case(oper)
            ADD:begin
                if(result_neg==0)
                  result <= op_1 + op_2;
                else
                  result <= (op_1>op_2) ? (op_1 - op_2) : (op_2 - op_1);
            end
            DEV:begin
                if(result_neg==0)
                  result <= (op_1>op_2) ? (op_1 - op_2) : (op_2 - op_1);
                else
                  result <= op_1 + op_2;
            end
            MUL:begin
                result <= op_1 * op_2;
            end
            DIV:begin
                result <= op_1 / op_2;
            end
            default:result <= op_1;
      endcase
    end
    else begin
      result <= result;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      result_neg <= 0;
    end
    else if(state_c==OP_1)begin
      result_neg <= 1'b0;
    end
    else if(state_c==ERROR)begin
      result_neg <= 1'b0;
    end
    else if(calculate==1 && oper==DEV && op_1<op_2)begin
      result_neg <= 1'b1;
    end
    else if(calculate==1 && result_neg==1 && oper==ADD && op_1<=op_2)begin
      result_neg <= 1'b0;
    end
    else if(result==0)begin
      result_neg <= 1'b0;
    end
    else begin
      result_neg <= result_neg;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      result_err <= 0;
    end
    else if(state_c==OP_1)begin
      result_err <= 1'b0;
    end
    else if((state_c_ff==OP_2 && state_c==OPER) || (key_cal_en==1 && state_c_ff!=ERROR))begin
      case(oper)
            ADD:begin
                if(result_neg==0)
                  result_err <= (op_1+op_2)>9999_9999 ? 1'b1 : 1'b0;
                else
                  result_err <= 1'b0;
            end
            DEV:begin
                if(result_neg==1)
                  result_err <= (op_1+op_2)>999_9999 ? 1'b1 : 1'b0;
                else if(op_2>op_1)
                  result_err <= (op_2-op_1)>999_9999 ? 1'b1 : 1'b0;
                else
                  result_err <= 1'b0;
            end
            MUL:begin
                if(result_neg==1)
                  result_err <= (op_1*op_2)>999_9999 ? 1'b1 : 1'b0;
                else
                  result_err <= (op_1*op_2)>9999_9999 ? 1'b1 : 1'b0;
            end
            DIV:begin
                if(op_2==0)
                  result_err <= 1'b1;
                else
                  result_err <= 1'b0;
            end
            default:result_err <= 1'b0;
      endcase
    end
    else begin
      result_err <= 1'b0;
    end
end

1.8 显示对象选择模块设计1.8.1接口信号
信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Op_1输入运算数1输入信号
Op_2输入运算数2输入信号
State_c输入计算器工作状态指示信号
Result_neg输入运算结果符号位指示信号
Disply输出显示数据输出信号
Display_vld输出显示数据有效指示信号

1.8.2设计思路该模块的作用是根据当前计算器的工作状态来选择数码管的显示内容。
1、复位后,该模块输出0;2、当计算器处于OP_1状态下,该模块选择输出运算数1。3、当计算器处于OPER状态下,该模块选择输出运算数1。4、当计算器处于OP_2状态下,该模块选择输出运算数2。5、当计算器处于RESULT状态下,该模块选择输出运算数1。6、当计算器处于ERROR状态下,该模块选择输出8个F。
要将数据送到数码管显示,需要将收到的数据进行拆分,比如输入进来的是“12”,需要拆成一个4bit的“1”和一个4bit的“2”送给数码管显示模块。因此设计一个计数器的架构,如下图所示:
架构中使用到了一个时钟计数器dis_cnt、一个采集状态指示信号flag_add、dis_sel为输入要显示的数据、dis_sel_tmp为输入数据打一拍之后的数据、result_neg为运算结果符号位指示信号、result_neg_tmp为运算结果符号位指示信号打一拍之后的信号。下面分别介绍一下这些信号的设计思路:
采集状态指示信号flag_add:初始状态为0,表示不对数据进行采集显示。如果检测到输入的数据或者符号位发生变化,表示要在数码管上显示的数据有变化,该信号拉高,计数器可以进行计数,所以由0变1的条件为dis_sel!=dis_sel_tmp || result_neg!=result_neg_tmp。当计数器数完之后,表示要显示的数据已经全部显示,则将此信号拉低,所以由1变0的条件是end_dis_cnt。
显示数据dis_sel:该信号根据工作状态进行选择,当目前处于OP_2状态时,选择运算数2输入数据,其他情况都选择运算数1输入数据。
显示数据打一拍之后的信号dis_sel_tmp:该信号存在的目的就是为了检测显示数据是否发生变化。
运算结果符号位指示信号result_neg:输入信号。
符号位指示信号打一拍之后的信号result_neg_tmp:该信号存在的意义就是为了检测符号位是否发生变化。
时钟计数器dis_cnt:该计数器的作用有两个,延时和控制输入数据赋值给显示数据输出信号的对应位。加一条件为flag_add && (dis_sel==dis_sel_tmp &&result_neg==result_neg_tmp),表示处在采集状态时,如果显示数据和符号位指示信号稳定,则开始计数。结束条件为数10个,由于计数器刚开始计数的时候,显示数据存在变化的可能,因此这里选择延迟两个时钟在对显示数据输出信号进行赋值(由于后面数据都是保持不变的,因此这个延时时间不是固定的,可以多延时一些),共有8个数码管,因此要赋值8次,所以计数器共需要数10个。
前面提到过,需要将显示数据显示到数码管上的话,需要将每一个数字进行拆分,一般采用除以10取余和取整的方法,本工程使用除法器的IP核,该IP核的作用就是将输入的数据除以10,得到商和余数。生成过程如下:
第一步、使用软件为Quartus Prime LiteEdition 18.1版本。首先打开软件之后,在主页面的右边找到“IP Catalog”窗口,在搜索栏中输入“div”进行搜索,然后双击“LPM_DIVIDE”。如果没有找到“IP Catalog”窗口,可在上方工具栏“Tools”中选择“IP Catalog”调出。

第二步、选择IP核生成的路径,并将其命名为“div”,注意这里的名字不能有中文字符或者全数字。在下方文件类型中选择“Verilog”,然后点击OK。


第三步、在之后出现的IP核设置界面中,“How wide should the numerator input”表示需要设置的分子的位宽,这里设置为27。“How wide should thedenominator input”表示需要设置的分母的位宽这里设置为4。在下方分子和分母的表示都选用“Unsigned”无符号类型。然后点击Next

第四步、下图中的1处表示是否需要对输出进行打拍,这里选择打一拍之后输出。2处表示要进行的优化,这里选择默认优化。3处表示是否总是返回正余数,选择是。然后点击Next。

第五步、方框出表示该IP核在仿真的时候需要调用的库,直接点击Next即可。


第六步、这一界面是设置需要生成的文件,本工程只需要生成默认的即可,所以不用勾选。点击Finish。


1.8.3参考代码
always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      result_neg_tmp <= 0;
    end
    else begin
      result_neg_tmp <= result_neg;
    end
end

always@(*)begin
    if(state_c==OP_2)begin
      dis_sel = op_2;
    end
    else begin
      dis_sel = op_1;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      dis_sel_tmp <= 0;
    end
    else begin
      dis_sel_tmp <= dis_sel;
    end
end




div div_prj(
            .clock      (clk      )   ,
            .numer      (dis_tmp    )   ,
            .denom      (10         )   ,
            .quotient   (div_quo    )   ,
            .remain   (div_rem    )
            );

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      flag_add <= 0;
    end
    else if(dis_sel!=dis_sel_tmp || result_neg!=result_neg_tmp)begin
      flag_add <= 1;
    end
    else if(end_dis_cnt)begin
      flag_add <= 0;
    end
end


always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      dis_cnt <= 0;
    end
    else if(add_dis_cnt) begin
      if(end_dis_cnt)
            dis_cnt <= 0;
      else
            dis_cnt <= dis_cnt+1 ;
   end
end
assign add_dis_cnt = flag_add && (dis_sel==dis_sel_tmp && result_neg==result_neg_tmp);
assign end_dis_cnt = add_dis_cnt&& dis_cnt == 10-1 ;


assigndis_tmp = add_dis_cnt && dis_cnt==1 ? dis_sel : div_quo;

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      display <= 4'b0;
    end
    else if(state_c==ERROR)begin
      display <= 4'b1111;
    end
    else if(end_dis_cnt && result_neg==1 && state_c!=OP_2)begin
      display <= 4'b1010;
    end
    else begin
      display <= div_rem;
    end
end



always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      display_vld <= 0;
    end
    else begin
      display_vld <= (dis_cnt==0 && (dis_sel==dis_sel_tmp)) ? 1'b1 : 1'b0;
    end
end

1.9 数码管显示模块设计1.9.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Display输入显示数据输入信号
Display_vld输入显示数据有效指示信号
Seg_sel输出数码管位选信号
Segment输出数码管段选信号

1.9.2设计思路
本模块主要实现的功能是对显示对象选择模块的显示数据输出信号(display)进行数码管显示。1、复位后,数码管默认显示运算数1;2、当result_err有效时,数码管显示8个F;3、当result_neg有效时,第8个数码管显示“—”;4、数码管显示display;由于数码管显示在前面已有案例介绍,所以这个就不做介绍。感兴趣的同学可以看一下往期的文章:【每周FPGA案例】至简设计系列_7段数码管显示
1.9.3参考代码always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      count_20us <= 0;
    end
    else if(add_count_20us) begin
      if(end_count_20us)
            count_20us <= 0;
      else
            count_20us <= count_20us+1 ;
   end
end
assign add_count_20us = 1;
assign end_count_20us = add_count_20us&& count_20us == TIME_20US-1 ;


always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      sel_cnt <= 0;
    end
    else if(add_sel_cnt) begin
      if(end_sel_cnt)
            sel_cnt <= 0;
      else
            sel_cnt <= sel_cnt+1 ;
   end
end
assign add_sel_cnt = end_count_20us;
assign end_sel_cnt = add_sel_cnt&& sel_cnt == SEG_NUM-1 ;



always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      seg_sel <= {SEG_NUM{1'b1}};
    end
    else begin
      seg_sel <= ~(1'b1 << sel_cnt);
    end
end

always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      display_ff0 <= 0;
    end
    else begin
      for(ii=0;ii<SEG_NUM;ii=ii+1)begin
            if(display_vld==1)begin
                display_ff0[(ii+1)*4-1 -:4] <= display[(ii+1)*4-1 -:4];
            end
            else begin
                display_ff0[(ii+1)*4-1 -:4] <= display_ff0[(ii+1)*4-1 -:4];
            end
      end
    end
end

always@(*)begin
    seg_tmp = display_ff0[(sel_cnt+1)*4-1 -:4];
end


always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      segment <= NUM_0;
    end
    else begin
      case(seg_tmp)
            0 :segment <=NUM_0;
            1 :segment <=NUM_1;
            2 :segment <=NUM_2;
            3 :segment <=NUM_3;
            4 :segment <=NUM_4;
            5 :segment <=NUM_5;
            6 :segment <=NUM_6;
            7 :segment <=NUM_7;
            8 :segment <=NUM_8;
            9 :segment <=NUM_9;
            10:segment <=NUM_10 ;
         default:segment <= NUM_ERR;
       endcase
    end
end

1.10 蜂鸣器模块设计1.10.1接口信号

信号接口方向定义
clk输入系统时钟
rst_n输入低电平复位信号
Op_1_err输入运算数1溢出信号,高电平有效
Op_2_err输入运算数2溢出信号,高电平有效
Result_err输入运算结果错误信号,高电平有效
Beep输出蜂鸣输出信号,高电平有效

1.10.2设计思路
该模块的主要功能是根据接收到的各个错误指示信号,进行报警提示。当接收到错误信号有效的时候,蜂鸣器报警,持续1秒的时间,因此提出一个计数器的架构,如下图所示:


主要由时钟计数器cnt_1s和蜂鸣器输出组成,下面时两个信号的设计思路:时钟计数器cnt_1s:该计数器的作用是计时1秒的时间。加一条件为flag_add,表示进入报警状态的时候便开始计数。结束条件为数5000_0000个,系统时钟为50M,一个时钟周期为20ns,5000_0000个时钟周期就是1秒。蜂鸣器输出信号beep:初始状态为1,表示不报警。从1变0的条件为op_1_err || op_2_err ||result_err,表示接收到这些错误指示信号之后,开始报警。从0变1的条件为end_cnt_1s,表示报警时间持续1秒,之后结束。
1.10.3参考代码
always @(posedge clk or negedge rst_n) begin
    if (rst_n==0) begin
      cnt_1s <= 0;
    end
    else if(add_cnt_1s) begin
      if(end_cnt_1s)
            cnt_1s <= 0;
      else
            cnt_1s <= cnt_1s+1 ;
   end
end
assign add_cnt_1s = beep==0;
assign end_cnt_1s = add_cnt_1s&& cnt_1s == CNT_1S-1 ;


always@(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
      beep <= 1'b1;
    end
    else if(op_1_err || op_2_err || result_err)begin
      beep <= 1'b0;
    end
    else if(end_cnt_1s)begin
      beep <= 1'b1;
    end
end

1.11 效果和总结


1.11.1db603开发板
由于计算器的演示是一个动态的过程,所以从下面图片中看不出具体实现的效果,想要看上板效果的话可以看一下工程上板的视频。
1.11.2ms980试验箱
由于计算器的演示是一个动态的过程,所以从下面图片中看不出具体实现的效果,想要看上板效果的话可以看一下工程上板的视频。



感兴趣的朋友也可以访问明德扬论坛(http://www.fpgabbs.cn/)进行FPGA相关工程设计学习,也可以看一下我们往期的文章:
《基于FPGA的密码锁设计》《波形相位频率可调DDS信号发生器》《基于FPGA的曼彻斯特编码解码设计》《基于FPGA的出租车计费系统》《数电基础与Verilog设计》《基于FPGA的频率、电压测量》《基于FPGA的汉明码编码解码设计》《关于锁存器问题的讨论》《阻塞赋值与非阻塞赋值》《参数例化时自动计算位宽的解决办法》1.12 公司简介明德扬是一家专注于FPGA领域的专业性公司,公司主要业务包括开发板、教育培训、项目承接、人才服务等多个方向。点拨开发板——学习FPGA的入门之选。
MP801开发板——千兆网、ADDA、大容量SDRAM等,学习和项目需求一步到位。网络培训班——不管时间和空间,明德扬随时在你身边,助你快速学习FPGA。周末培训班——明天的你会感激现在的努力进取,升职加薪明德扬来助你。就业培训班——七大企业级项目实训,获得丰富的项目经验,高薪就业。专题课程——高手修炼课:提升设计能力;实用调试技巧课:提升定位和解决问题能力;FIFO架构设计课:助你快速成为架构设计师;时序约束、数字信号处理、PCIE、综合项目实践课等你来选。项目承接——承接企业FPGA研发项目。人才服务——提供人才推荐、人才代培、人才派遣等服务。
【设计教程下载】


【设计视频教程】

至简设计系列_简易计算器_01_模块划分_32分
https://www.bilibili.com/video/BV1Af4y117H4?p=26

至简设计系列_简易计算器_02_工作状态选择模块设计_19分

https://www.bilibili.com/video/BV1Af4y117H4?p=27


至简设计系列_简易计算器_03_运算数模块设计_22分

https://www.bilibili.com/video/BV1Af4y117H4?p=28

至简设计系列_简易计算器_04_运算符与运算单元模块设计_22分



https://www.bilibili.com/video/BV1Af4y117H4?p=29


至简设计系列_简易计算器_05_显示对象选择与蜂鸣器模块划分_27分
https://www.bilibili.com/video/BV1Af4y117H4?p=30

【工程源码】



我才是郭劲拔 发表于 2023-5-21 18:23:58

这个有仿真吗
页: [1]
查看完整版本: 【每周FPGA案例】 至简设计系列_简易计算器 编号:000600000010