2024年寒假练 - 基于小脚丫FPGA套件STEP BaseBoard V4.0实现两位十进制加、减、乘、除计算器
该项目使用了Verilog,WebIDE,实现了小脚丫FPGA套件STEP BaseBoard V4.0的设计,它的主要功能为:两位十进制加、减、乘、除计算器。
标签
FPGA
QingSpace
更新2024-03-29
322

1.项目需求

实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。

2.需求分析

首先需要接受矩阵键盘的按得位置,接下来要将位置信息解码为对应的按键内容,然后将按键信息传给计算模块进行对应的数据存储的计算,再把计算的二进制结果转码为十进制的BCD码,最后把BCD码传给数码管的显示模块将按下的数字和计算的结果显示在数码管上。

同时,由于在FPGA中直接用除号(/)和取余(%)会占用大量资源,所以需要考虑使用除法器。在除不尽时也要考虑保留几位小数,以及能除尽但含有小数的问题。

3.实现的方式

浏览小脚丫官方提供的例程,我们可以找到以下有用的模块帮助我们实现需求:

  • array_keyboard模块,这个模块使用了状态机,通过扫描的方法采样矩阵键盘,可以帮助我们识别按下的是哪个按键。
  • key_decode模块,这个模块接收上一个array_keyboard模块输出的脉冲信号,将其解码为对应的按键信息。
  • bin_to_bcd模块,通过满五加三的操作将二进制的数据转换为十进制的BCD码型数据让数码管显示。
  • segment_scan模块,底板的74HC595芯片对应的模块,将接受的十进制BCD数据传给74HC595芯片显示在数码管上。

在此之外,为了实现除法操作且节约资源,我选择使用了除法器div模块,它接受除数与被除数,并输出商和余数。而本项目的核心模块——calculator计算模块可以根据按下的符号进行加、减、乘、除计算,同时,计算模块还可以根据目前按下的是第几位数熄灭左边的数码管,只点亮需要显示数字的数码管。其次,计算模块也能在除法除得尽但是有小数(13/4)时自适应显示小数位数,最多显示四位小数,超过四位小数时四舍五入;当除不尽时,计算模块最多保留四位小数并且四舍五入最后一位。最后,计算模块也支持连续计算,即将计算结果自动当做第一个数据,只需按下符号和第二个数据即可继续进行计算。

4.功能框图

5.代码及说明

5.1 Top.v

module Top(
input clk,
input rst_n,
input [3:0] col,
output [3:0] row,
output seg_rck,
output seg_sck,
output seg_din,
output [8:0] seg_led_1,
output [8:0] seg_led_2
);

wire [15:0] key_out;
wire [15:0] key_pulse;

wire [3:0] key_num;
wire flag;

wire [26:0] result;

wire [31:0] display_bcd;

wire [7:0] dat_en;
wire [7:0] dot_en;

wire [3:0] seg_data_1;
wire [3:0] seg_data_2;

array_keyboard keyboard(
.clk (clk ),
.rst_n (rst_n ),
.col (col ),
.row (row ),
.key_out (key_out ),
.key_pulse (key_pulse )
);

key_decode decode(
.clk (clk ),
.rst_n (rst_n ),
.key_pulse (key_pulse ),
.seg_data (key_num ),
.flag (flag )
);

calculator calc(
.clk (clk ),
.rst_n (rst_n ),
.flag (flag ),
.key_data (key_num ),
.num1 (result ),
.dat (dat_en ),
.dot (dot_en ),
.seg_data_1 (seg_data_1 ),
.seg_data_2 (seg_data_2 )
);

bin_to_bcd bcd(
.rst_n (rst_n ),
.bin_code (result ),
.bcd_code (display_bcd)
);

segment_scan scan(
.clk (clk ), //系统时钟 12MHz
.rst_n (rst_n ), //系统复位 低有效
.dat_1 (display_bcd[31:28]), //SEG1 显示的数据输入
.dat_2 (display_bcd[27:24]), //SEG2 显示的数据输入
.dat_3 (display_bcd[23:20]), //SEG3 显示的数据输入
.dat_4 (display_bcd[19:16]), //SEG4 显示的数据输入
.dat_5 (display_bcd[15:12]), //SEG5 显示的数据输入
.dat_6 (display_bcd[11:8] ), //SEG6 显示的数据输入
.dat_7 (display_bcd[7:4] ), //SEG7 显示的数据输入
.dat_8 (display_bcd[3:0] ), //SEG8 显示的数据输入
.dat_en (dat_en ), //数码管数据位显示使能
.dot_en (dot_en ), //数码管小数点位显示使能
.seg_rck (seg_rck ), //74HC595的RCK管脚
.seg_sck (seg_sck ), //74HC595的SCK管脚
.seg_din (seg_din ) //74HC595的SER管脚
);

LED led(
.seg_data_1 (seg_data_1 ),
.seg_data_2 (seg_data_2 ),
.seg_led_1 (seg_led_1 ),
.seg_led_2 (seg_led_2 )
);

endmodule

本段代码作为顶层设计,负责将一系列必要的子模块巧妙地集成至一个统一的设计框架中。它精心地实例化了所有子模块,并按照严格的设计规范将它们相互连接,构建出一个完整而协调的系统。同时,该代码精心定义了设计的输入与输出接口,这些接口如同与外界沟通的桥梁,为系统提供了与外部世界交互的能力。例如,它包含了接收时钟信号和复位信号的接口,以及向数码管输出的接口,确保了与外部设备的顺畅通信。在连线方面,该代码巧妙地连接了各个子模块的输入与输出,确保了数据与控制信号能够在整个系统中流畅而准确地传递,为系统的稳定运行提供了坚实的保障。

5.2 array_keyboard.v

module array_keyboard #
(
parameter CNT_200HZ = 60000
)
(
input clk,
input rst_n,
input [3:0] col,
output reg [3:0] row,
output reg [15:0]key_out,
output [15:0]key_pulse
);

localparam STATE0 = 2'b00;
localparam STATE1 = 2'b01;
localparam STATE2 = 2'b10;
localparam STATE3 = 2'b11;

//计数器计数分频实现5ms周期信号clk_200hz,系统时钟12MHz
reg [15:0] cnt;
reg clk_200hz;
always@(posedge clk or negedge rst_n) begin //复位时计数器cnt清零,clk_200hz信号起始电平为低电平
if(!rst_n) begin
cnt <= 16'd0;
clk_200hz <= 1'b0;
end else begin
if(cnt >= ((CNT_200HZ>>1) - 1)) begin //数字逻辑中右移1位相当于除2
cnt <= 16'd0;
clk_200hz <= ~clk_200hz; //clk_200hz信号取反
end else begin
cnt <= cnt + 1'b1;
clk_200hz <= clk_200hz;
end
end
end

reg [1:0] c_state;
//状态机根据clk_200hz信号在4个状态间循环,每个状态对矩阵按键的行接口单行有效
always@(posedge clk_200hz or negedge rst_n) begin
if(!rst_n) begin
c_state <= STATE0;
row <= 4'b1110;
end else begin
case(c_state)
//状态c_state跳转及对应状态下矩阵按键的row输出
STATE0: begin c_state <= STATE1; row <= 4'b1101; end
STATE1: begin c_state <= STATE2; row <= 4'b1011; end
STATE2: begin c_state <= STATE3; row <= 4'b0111; end
STATE3: begin c_state <= STATE0; row <= 4'b1110; end
default:begin c_state <= STATE0; row <= 4'b1110; end
endcase
end
end

reg [15:0] key,key_r;
//因为每个状态中单行有效,通过对列接口的电平状态采样得到对应4个按键的状态,依次循环
always@(negedge clk_200hz or negedge rst_n) begin
if(!rst_n) begin
key_out <= 16'hffff; key_r <= 16'hffff; key <= 16'hffff;
end else begin
case(c_state)
//采集当前状态的列数据赋值给对应的寄存器位
//对键盘采样数据进行判定,连续两次采样低电平判定为按键按下
STATE0: begin key_out[ 3: 0] <= key_r[ 3: 0]|key[ 3: 0]; key_r[ 3: 0] <= key[ 3: 0]; key[ 3: 0] <= col; end
STATE1: begin key_out[ 7: 4] <= key_r[ 7: 4]|key[ 7: 4]; key_r[ 7: 4] <= key[ 7: 4]; key[ 7: 4] <= col; end
STATE2: begin key_out[11: 8] <= key_r[11: 8]|key[11: 8]; key_r[11: 8] <= key[11: 8]; key[11: 8] <= col; end
STATE3: begin key_out[15:12] <= key_r[15:12]|key[15:12]; key_r[15:12] <= key[15:12]; key[15:12] <= col; end
default:begin key_out <= 16'hffff; key_r <= 16'hffff; key <= 16'hffff; end
endcase
end
end

reg [15:0] key_out_r;
//Register low_sw_r, lock low_sw to next clk
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_out_r <= 16'hffff;
else key_out_r <= key_out; //将前一刻的值延迟锁存

//wire [15:0] key_pulse;
//Detect the negedge of low_sw, generate pulse
assign key_pulse= key_out_r & ( ~key_out); //通过前后两个时刻的值判断

endmodule

该模块是一个用于4x4矩阵键盘扫描和按键检测的Verilog模块。它通过一个两比特的状态机实现对四行键盘的轮流扫描,每个状态持续5毫秒,总共四个状态,每个状态下只有一行是有效的。在有效的行扫描期间,模块会读取列输入,以检测是否有按键按下。它使用一个20毫秒的时钟信号来驱动状态机,这个时钟信号是由12MHz的系统时钟分频得到的。模块会连续两次采样列输入,如果连续两次采样都是低电平,则判定对应的按键被按下。模块输出一个16比特的信号key_out,表示当前按键的状态,以及一个key_pulse信号,用来生成按键按下的脉冲。这个设计可以有效地减少I/O口的使用,同时能够检测多个按键。

5.3 key_decode.v

module key_decode(
input clk,
input rst_n,
input [15:0] key_pulse, //按键消抖后动作脉冲信号
output reg [3:0] seg_data,
output reg flag
);

//key_pulse transfer to seg_data
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
seg_data <= 4'd00;
end
else begin
case(key_pulse) //key_pulse脉宽等于clk_in周期
16'h0001: begin
seg_data <= 4'd07; flag <= 1'b1;
end
16'h0002: begin
seg_data <= 4'd08; flag <= 1'b1;
end
16'h0004: begin
seg_data <= 4'd09; flag <= 1'b1;
end
16'h0008: begin
seg_data <= 4'd10; flag <= 1'b1;
end
16'h0010: begin
seg_data <= 4'd04; flag <= 1'b1;
end
16'h0020: begin
seg_data <= 4'd05; flag <= 1'b1;
end
16'h0040: begin
seg_data <= 4'd06; flag <= 1'b1;
end
16'h0080: begin
seg_data <= 4'd11; flag <= 1'b1;
end
16'h0100: begin
seg_data <= 4'd01; flag <= 1'b1;
end
16'h0200: begin
seg_data <= 4'd02; flag <= 1'b1;
end
16'h0400: begin
seg_data <= 4'd03; flag <= 1'b1;
end
16'h0800: begin
seg_data <= 4'd12; flag <= 1'b1;
end
16'h1000: begin
seg_data <= 4'd00; flag <= 1'b1;
end
16'h2000: begin
seg_data <= 4'd00; flag <= 1'b1;
end
16'h4000: begin
seg_data <= 4'd14; flag <= 1'b1;
end
16'h8000: begin
seg_data <= 4'd13; flag <= 1'b1;
end
default: begin seg_data <= seg_data; flag <= 1'b0; //无按键按下时保持
end
endcase
end
end

endmodule

本模块用于将16位宽的按键脉冲信号key_pulse解码为4位宽的段数据seg_data以及一个标志位flag。该模块在时钟信号clk的上升沿或复位信号rst_n的下降沿触发。当复位信号激活时,段数据被清零。否则,根据不同的key_pulse输入值,seg_data被赋予不同的4位值,并且标志位flag被置为1,表示有按键被按下且已被解码。若key_pulse的值未在case语句中明确列出,则保持当前的seg_data值不变,并将flag置为0,表示无按键被按下或按键值未知。

5.4 calculator.v

module calculator (  
clk,
rst_n,
flag,
key_data,
num1,
dat,
dot,
seg_data_1,
seg_data_2
);

input clk;
input rst_n;
input flag;
input [3:0] key_data;

output reg [26:0] num1;
output reg [7:0] dat;
output reg [7:0] dot;
output reg [3:0] seg_data_1;
output reg [3:0] seg_data_2;

reg [1:0] state;
reg [26:0] num2;
reg [26:0] num3;
reg [3:0] opcode;
reg [7:0] datt;
reg [3:0] state1;
reg [3:0] d; //出现小数且商小于1时强行使能数码管位数,否则前几位0对应的数码管不会被使能​
reg f; //连续计算时判断上一步是否为除法,从而将num3或num1赋给num2​

localparam L_DIVN = 27 ;//被除数的位宽;
localparam L_DIVR = 27 ;//除数的位宽;;

reg start ;//开始计算信号,高电平有效,
reg [L_DIVN - 1 : 0] dividend ;//被除数输入;
reg [L_DIVR - 1 : 0] divisor ;//除数输入;

wire [L_DIVN - 1 : 0] quotient ;//商。
wire [L_DIVR - 1 : 0] remainder ;//余数,余数的大小不会超过除数大小。
wire quotient_vld ;//商和余数输出有效指示信号,高电平有效;
wire ready ;//高电平表示此模块空闲。
wire error ;//高电平表示输入除数为0,输入数据错误。

reg quotient_error ;
reg rem_error ;


div #(
.L_DIVN ( L_DIVN ),//被除数的位宽;
.L_DIVR ( L_DIVR ) //除数的位宽;
)
u_div(
.clk ( clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.start ( start ),//开始计算信号,高电平有效,
.dividend ( dividend ),//被除数输入;
.divisor ( divisor ),//除数输入;
.quotient ( quotient ),//商。
.remainder ( remainder ),//余数,余数的大小不会超过除数大小。
.ready ( ready ),//高电平表示此模块空闲。
.error ( error ),//高电平表示输入除数为0,输入数据错误。
.quotient_vld ( quotient_vld ) //商和余数输出有效指示信号,高电平有效;
);

always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
state <= 0;
state1 <= 0;
num1 = 0;
num2 = 0;
opcode <= 0;
dat <= 0;
datt <= 1;
dot = 0;
start = 0;
d = 0;
f = 0;
seg_data_1 <= 0;
seg_data_2 <= 0;
end
else
begin
case(state)
0 : begin
if (flag)
begin
if (key_data < 10)
begin
num1 = num1 * 10 + key_data;
dat <= dat + datt;
datt <= datt << 1;
dot = 0;
end
else
begin
if (key_data == 14)
begin
state <= 0;
end
else
begin
opcode <= key_data;
state <= 1;
state1 <= 0;
dat <= 0;
datt <= 1;
dot = 0;
start = 0;
d = 0;
if(f) begin
num2 = num3;
num1 = 0;
end
else begin
num2 = num1;
num1 = 0;
end
case(key_data)
10 : begin
seg_data_1 <= 1;
seg_data_2 <= 1;
end
11 : begin
seg_data_1 <= 2;
seg_data_2 <= 2;
end
12 : begin
seg_data_1 <= 3;
seg_data_2 <= 3;
end
13 : begin
seg_data_1 <= 4;
seg_data_2 <= 4;
end
endcase
end

end
end
else
begin
state <= 0;
end
end

1 : begin
if (flag || start)
begin
if (key_data < 10)
begin
num1 = num1 * 10 + key_data;
dat <= dat + datt;
datt <= datt << 1;
dot = 0;
end
else
begin
if (key_data == 14)
begin
seg_data_1 <= 5;
seg_data_2 <= 5;
case (opcode)
10 : begin
num1 = num2 + num1;
num2 = num1;
f = 0;
state <= 0;
end
11 : begin
num1 = num2 - num1;
num2 = num1;
f = 0;
state <= 0;
end
12 : begin
num1 = num2 * num1;
num2 = num1;
f = 0;
state <= 0;
end
13 : begin
case(state1)
0 : begin
dividend = num2;
divisor = num1;
start = 1;
state1 <= 1;
f = 1;
end

1 : begin
if(quotient_vld) begin
if(remainder) begin
num3 = quotient;
dividend = num2 * 10;
divisor = num1;
dot = 8'b0000_0010;
state1 <= 2;
if(num2 < num1) begin
d = 2;
end
end
else begin
num1 = quotient;
num2 = num1;
start = 0;
state <= 0;
end
end
end

2 : begin
if(quotient_vld) begin
state1 <= 3;
end
end

3 : begin
if(quotient_vld) begin
if(remainder) begin
dividend = num2 * 100;
divisor = num1;
dot = 8'b0000_0100;
state1 <= 4;
if(num2 < num1) begin
d = 3;
end
end
else begin
num1 = quotient;
num2 = num3;
start = 0;
state <= 0;
end
end
end

4 : begin
if(quotient_vld) begin
state1 <= 5;
end
end

5 : begin
if(quotient_vld) begin
if(remainder) begin
dividend = num2 * 1000;
divisor = num1;
dot = 8'b0000_1000;
state1 <= 6;
if(num2 < num1) begin
d = 4;
end
end
else begin
num1 = quotient;
num2 = num3;
start = 0;
state <= 0;
end
end
end

6 : begin
if(quotient_vld) begin
state1 <= 7;
end
end

7 : begin
if(quotient_vld) begin
if(remainder) begin
dividend = num2 * 10000;
divisor = num1;
dot = 8'b0001_0000;
state1 <= 8;
if(num2 < num1) begin
d = 5;
end
end
else begin
num1 = quotient;
num2 = num3;
start = 0;
state <= 0;
end
end
end

8 : begin
if(quotient_vld) begin
state1 <= 9;
end
end

9 : begin
if(quotient_vld) begin
if((remainder * 2) < num1) begin
num1 = quotient;
num2 = num3;
start = 0;
state <= 0;
end
else begin
num1 = quotient + 1;
num2 = num3;
start = 0;
state <= 0;
end
end
end
endcase
end
endcase
end
else
begin
state <= 1;
end
if(num1 < 10)
begin
dat <= 8'b0000_0001;
end
else if((num1 < 100 && num1 >= 10 && d == 0) || d == 2)
begin
dat <= 8'b0000_0011;
end
else if((num1 < 1000 && num1 >= 100 && d == 0) || d == 3)
begin
dat <= 8'b0000_0111;
end
else if((num1 < 10000 && num1 >= 1000 && d == 0) || d == 4)
begin
dat <= 8'b0000_1111;
end
else if((num1 < 100000 && num1 >= 10000 && d == 0) || d == 5)
begin
dat <= 8'b0001_1111;
end
else if(num1 < 1000000 && num1 >= 100000)
begin
dat <= 8'b0011_1111;
end
else if(num1 < 10000000 && num1 >= 1000000)
begin
dat <= 8'b0111_1111;
end
else if(num1 < 100000000 && num1 >= 10000000)
begin
dat <= 8'b1111_1111;
end
end
end
else
begin
state <= 1;
end
end
endcase
end
end

endmodule

本模块是该项目的核心模块——计算模块,该模块接收解码后的键盘内容,并使用状态机处理计算过程。在第一个状态中,先接收第一个数字,刚开始所有的数码管都没有被使能,所以都不会亮起,按下第一个数字时最右侧的数码管会亮起。如果继续按数字,则会将第一位数移至下一位并将这一个数字显示在第一位,同时使能相应的数码管,以此类推。当按下任意一个符号时会清空数码管的使能数据,将第一个数据存到num2中,并将按下的符号存储到opcode中,同时,会让开发板上的两个数码管对应显示1(+)、2(-)、3(*)、4(/)进入第二个计算状态。

在第二个计算状态中,刚开始先和上一个状态一样把第二个数据存储到num1中。一但按下等于(=),就会开始根据opcode的值进行相应的计算。当是加、减、乘使用+、-、*符号计算,当是除的时候,调用除法器模块,将num2和num1分别赋值给被除数和除数,启动除法器,等待除法计算完成后quotient_vld会变为1进行接下来的判断:如果余数是0则将把商quotient赋值给num1直接输出,如果余数不为零则不能整除,先用num3把整数部分暂存一下,用于后续的连续计算,然后把num2乘10再次进行除法,同时把右边第二位的小数点使能,再次判断余数是否为0……按此步骤进行到num2乘10000,如果还是不能整除则进行四舍五入。四舍五入很简单,只需要判断余数的2倍与除数的关系判断商是否需要加1即可。在计算结束显示结果到数码管同时会让开发板上的两个的数码管显示5(计算完成)。

在本模块的寄存器中有一个四位的“d”和一位的“f”。“d”用于处理被除数小于除数使商小于1时的数码管使能,“f”用于在连续计算时判断上一次是不是除法从而正确给num2赋值,在第7部分中会具体介绍。

5.5 div.v

//注意此模块默认被除数的位宽大于等于除数的位宽。
//当quotient_vld信号为高电平且error为低电平时,输出的数据是除法计算的正确结果。
//当输入除数为0时,error信号拉高,且商和余数为0;
//当ready信号为低电平时,不能将开始信号start拉高,此时拉高start信号会被忽略。
module div #(
parameter L_DIVN = 27 ,//被除数的位宽;
parameter L_DIVR = 27 //除数的位宽;
)(
input clk ,//时钟信号;
input rst_n ,//复位信号,低电平有效;

input start ,//开始计算信号,高电平有效,必须在ready信号为高电平时输入才有效。
input [L_DIVN - 1 : 0] dividend ,//被除数输入;
input [L_DIVR - 1 : 0] divisor ,//除数输入;

output reg ready ,//高电平表示此模块空闲。
output reg error ,//高电平表示输入除数为0,输入数据错误。
output reg quotient_vld ,//商和余数输出有效指示信号,高电平有效;
output reg [L_DIVR - 1 : 0] remainder ,//余数,余数的大小不会超过除数大小。
output reg [L_DIVN - 1 : 0] quotient //商。
);
localparam L_CNT = clogb2(L_DIVN) ;//利用函数自动计算移位次数计数器的位宽。
localparam IDLE = 3'b001 ;//状态机空闲状态的编码;
localparam ADIVR = 3'b010 ;//状态机移动除数状态的编码;
localparam DIV = 3'b100 ;//状态机进行减法计算和移动被除数状态的编码;

reg vld ;//
reg [2 : 0] state_c ;//状态机的现态;
reg [2 : 0] state_n ;//状态机的次态;
reg [L_DIVN : 0] dividend_r ;//保存被除数;
reg [L_DIVR - 1 : 0] divisor_r ;//保存除数。
reg [L_DIVN - 1 : 0] quotient_r ;//保存商。
reg [L_CNT - 1 : 0] shift_dividend ;//用于记录被除数左移的次数。
reg [L_CNT - 1 : 0] shift_divisor ;//用于记录除数左移的次数。

wire [L_DIVR : 0] comparison ;//被除数的高位减去除数。
wire max ;//高电平表示被除数左移次数已经用完,除法运算基本结束,可能还需要进行一次减法运算。

//自动计算计数器位宽函数。
function integer clogb2(input integer depth);begin
if(depth == 0)
clogb2 = 1;
else if(depth != 0)
for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
depth=depth >> 1;
end
endfunction

//max为高电平表示被除数左移的次数等于除数左移次数加上被除数与除数的位宽差;
assign max = (shift_dividend == (L_DIVN - L_DIVR) + shift_divisor);

//用来判断除数和被除数第一次做减法的高位两者的大小,当被除数高位大于等于除数时,comparison最高位为0,反之为1。
//comparison的计算结果还能表示被除数高位与除数减法运算的结果。
//在移动除数时,判断的是除数左移一位后与被除数高位的大小关系,进而判断能不能把除数进行左移。
assign comparison = ((divisor[L_DIVR-1] == 0) && ((state_c == ADIVR))) ?
dividend_r[L_DIVN : L_DIVN - L_DIVR] - {divisor_r[L_DIVR-2 : 0],1'b0} :
dividend_r[L_DIVN : L_DIVN - L_DIVR] - divisor_r;//计算被除数高位减去除数,如果计算结果最高位为0,表示被除数高位大于等于除数,如果等于1表示被除数高位小于除数。

//状态机次态到现态的转换;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为空闲状态;
state_c <= IDLE;
end
else begin//状态机次态到现态的转换;
state_c <= state_n;
end
end

//状态机的次态变化。
always@(*)begin
case(state_c)
IDLE : begin//如果开始计算信号为高电平且除数和被除数均不等于0。
if(start & (dividend != 0) & (divisor != 0))begin
state_n = ADIVR;
end
else begin//如果开始条件无效或者除数、被除数为0,则继续处于空闲状态。
state_n = state_c;
end
end
ADIVR : begin//如果除数的最高位为高电平或者除数左移一位大于被除数的高位,则跳转到除法运算状态;
if(divisor_r[L_DIVR-1] | comparison[L_DIVR])begin
state_n = DIV;
end
else begin
state_n = state_c;
end
end
DIV : begin
if(max)begin//如果被除数移动次数达到最大值,则状态机回到空闲状态,计算完成。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default : begin//状态机跳转到空闲状态;
state_n = IDLE;
end
endcase
end

//对被除数进行移位或进行减法运算。
//初始时需要加载除数和被除数,然后需要判断除数和被除数的高位,确定除数是否需要移位。
//然后根据除数和被除数高位的大小,确认被除数是移位还是与除数进行减法运算,注意被除数移动时,为了保证结果不变,商也会左移一位。
//如果被除数高位与除数进行减法运算,则商的最低位变为1,好比此时商1进行的减法运算。经减法结果赋值到被除数对应位。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
divisor_r <= 0;
dividend_r <= 0;
quotient_r <= 0;
shift_divisor <= 0;
shift_dividend <= 0;
end//状态机处于加载状态时,将除数和被除数加载到对应寄存器,开始计算;
else if(state_c == IDLE && start && (dividend != 0) & (divisor != 0))begin
dividend_r <= dividend;//加载被除数到寄存器;
divisor_r <= divisor;//加载除数到寄存器;
quotient_r <= 0;//将商清零;
shift_dividend <= 0;//将移位的被除数寄存器清零;
shift_divisor <= 0; //将移位的除数寄存器清零;
end//状态机处于除数左移状态,且除数左移后小于等于被除数高位且除数最高位为0。
else if(state_c == ADIVR && (~comparison[L_DIVR]) && (~divisor_r[L_DIVR-1]))begin
divisor_r <= divisor_r << 1;//将除数左移1位;
shift_divisor <= shift_divisor + 1;//除数总共被左移的次数加1;
end
else if(state_c == DIV)begin//该状态需要完成被除数移位和减法运算。
if(comparison[L_DIVR] && (~max))begin//当除数大于被除数高位时,被除数需要移位。
dividend_r <= dividend_r << 1;//将被除数左移1位;
quotient_r <= quotient_r << 1;//同时把商左移1位;
shift_dividend <= shift_dividend + 1;//被除数总共被左移的次数加1;
end
else if(~comparison[L_DIVR])begin//当除数小于等于被除数高位时,被除数高位减去除数作为新的被除数高位。
dividend_r[L_DIVN : L_DIVN - L_DIVR] <= comparison;//减法结果赋值给被除数进行减法运算的相应位。
quotient_r[0] <= 1;//因为做了一次减法,则商加1。
end
end
end

//生成状态机从计算除结果的状态跳转到空闲状态的指示信号,用于辅助设计输出有效指示信号。
always@(posedge clk)begin
vld <= (state_c == DIV) && (state_n == IDLE);
end

//生成商、余数及有效指示信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b0;
end//如果开始计算时,发现除数或者被除数为0,则商和余数均输出0,且将输出有效信号拉高。
else if(state_c == IDLE && start && ((dividend== 0) || (divisor==0)))begin
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b1;
end
else if(vld)begin//当计算完成时。
quotient <= quotient_r;//把计算得到的商输出。
quotient_vld <= 1'b1;//把商有效是指信号拉高。
//移动剩余部分以补偿对齐变化,计算得到余数;
remainder <= (dividend_r[L_DIVN - 1 : 0]) >> shift_dividend;
end
else begin
quotient_vld <= 1'b0;
end
end

//当输入除数为0时,将错误指示信号拉高,其余时间均为低电平。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
error <= 1'b0;
end
else if(state_c == IDLE && start)begin
if(divisor==0)//开始计算时,如果除数为0,把错误指示信号拉高。
error <= 1'b1;
else//开始计算时,如果除数不为0,把错误指示信号拉低。
error <= 1'b0;
end
end

//状态机处于空闲且不处于复位状态;
always@(*)begin
if(start || state_c != IDLE || vld)
ready = 1'b0;
else
ready = 1'b1;
end

endmodule

本模块参考了这篇文章:基于FPGA的高效除法器

这个模块用于实现被除数为L_DIVN位,除数为L_DIVR位的二进制除法运算。该模块可以处理被除数大于等于除数的情况,并且能够处理输入除数为0的情况,当输入除数为0时,模块会输出错误指示信号。

模块的主要输入信号包括时钟信号clk,复位信号rst_n,开始计算信号start,被除数dividend和除数divisor。模块的主要输出信号包括准备就绪信号ready,错误指示信号error,商和余数输出有效指示信号quotient_vld,余数remainder和商quotient。

该模块使用一个状态机来实现除法运算,包括空闲状态IDLE,移动除数状态ADIVR和进行除法运算状态DIV。在空闲状态时,如果开始计算信号为高电平且除数和被除数均不等于0,则模块会跳转到移动除数状态。在移动除数状态时,如果除数的最高位为高电平或者除数左移一位大于被除数的高位,则跳转到除法运算状态。在除法运算状态时,如果被除数移动次数达到最大值,则回到空闲状态,计算完成。

模块中还使用了移位和减法运算来实现除法运算,包括对被除数和除数进行移位,以及根据除数和被除数高位的大小进行减法运算。在减法运算后,会将减法结果赋值给被除数相应位,并将商最低位加1。

当计算完成后,模块会输出商和余数,并将输出有效指示信号拉高。如果输入除数为0,则模块会输出错误指示信号,并将输出有效指示信号拉低。

5.6 bin_to_bcd.v

module bin_to_bcd(
input rst_n, //系统复位,低有效
input [26:0] bin_code,//需要进行BCD转码的二进制数据
output [31:0] bcd_code //转码后的BCD码型数据输出
);

/*
此模块为了将ADC采样的数据转换为我们常用的十进制显示而存在,
主要知识涉及数学中不同制式数据的转换,详细原理这里不做介绍,去百度搜索<FPGA 二进制转BCD码>可得
*/

reg [31:0] bcd_code;
reg [58:0] shift_reg;
always@(bin_code or rst_n)begin
shift_reg = {32'h0,bin_code};
if(!rst_n)
bcd_code = 0;
else begin
repeat(27) begin //循环27次
//BCD码各位数据作满5加3操作,
if (shift_reg[30:27] >= 5) shift_reg[30:27] = shift_reg[30:27] + 2'b11;
if (shift_reg[34:31] >= 5) shift_reg[34:31] = shift_reg[34:31] + 2'b11;
if (shift_reg[38:35] >= 5) shift_reg[38:35] = shift_reg[38:35] + 2'b11;
if (shift_reg[42:39] >= 5) shift_reg[42:39] = shift_reg[42:39] + 2'b11;
if (shift_reg[46:43] >= 5) shift_reg[46:43] = shift_reg[46:43] + 2'b11;
if (shift_reg[50:47] >= 5) shift_reg[50:47] = shift_reg[50:47] + 2'b11;
if (shift_reg[54:51] >= 5) shift_reg[54:51] = shift_reg[54:51] + 2'b11;
if (shift_reg[58:55] >= 5) shift_reg[58:55] = shift_reg[58:55] + 2'b11;
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[58:27];
end
end

endmodule

本模块用于将27位的二进制数转换为32位的BCD(二进制编码的十进制)码。模块包含复位输入rst_n(低有效)、27位二进制输入bin_code和32位BCD输出bcd_code。在复位时,BCD输出被清零。非复位状态下,模块使用一个59位的移位寄存器shift_reg来执行二进制到BCD的转换。通过27次循环,每次对shift_reg中的特定段执行“满5加3”操作(实际上是加2'b11,即3),这是BCD编码的一个关键步骤,用于确保每4位BCD码表示一个十进制数字。然后,shift_reg左移一位,为下一次迭代准备。最终,转换后的32位BCD码从shift_reg中提取并赋值给输出bcd_code

5.7 LED.v

module LED (seg_data_1,seg_data_2,seg_led_1,seg_led_2);

input [3:0] seg_data_1; //数码管需要显示0~9十个数字,所以最少需要4位输入做译码
input [3:0] seg_data_2; //小脚丫上第二个数码管
output [8:0] seg_led_1; //在小脚丫上控制一个数码管需要9个信号 MSB~LSB=DIG、DP、G、F、E、D、C、B、A
output [8:0] seg_led_2; //在小脚丫上第二个数码管的控制信号 MSB~LSB=DIG、DP、G、F、E、D、C、B、A

reg [8:0] seg [9:0]; //定义了一个reg型的数组变量,相当于一个10*9的存储器,存储器一共有10个数,每个数有9位宽

initial //在过程块中只能给reg型变量赋值,Verilog中有两种过程块always和initial
//initial和always不同,其中语句只执行一次
begin
seg[0] = 9'h3f; //对存储器中第一个数赋值9'b00_0011_1111,相当于共阴极接地,DP点变低不亮,7段显示数字 0
seg[1] = 9'h06; //7段显示数字 1
seg[2] = 9'h5b; //7段显示数字 2
seg[3] = 9'h4f; //7段显示数字 3
seg[4] = 9'h66; //7段显示数字 4
seg[5] = 9'h6d; //7段显示数字 5
seg[6] = 9'h7d; //7段显示数字 6
seg[7] = 9'h07; //7段显示数字 7
seg[8] = 9'h7f; //7段显示数字 8
seg[9] = 9'h6f; //7段显示数字 9
end

assign seg_led_1 = seg[seg_data_1]; //连续赋值,这样输入不同四位数,就能输出对于译码的9位输出
assign seg_led_2 = seg[seg_data_2];

endmodule

模块来自WebIDE的示例,它接受两个4位输入(seg_data_1和seg_data_2),分别表示两个数码管要显示的0到9之间的数字。模块通过两个9位输出(seg_led_1和seg_led_2)控制这两个数码管的显示。代码中使用了一个10x9位的寄存器数组seg来存储0到9每个数字的7段数码管编码,加上数码管选择和小数点控制的两位。在initial块中,对数组进行了初始化,为每个数字指定了相应的编码。通过连续赋值语句,根据输入的数字选择相应的编码输出到数码管控制引脚,从而实现数字的显示。

6.仿真波形图

展示主要模块的仿真波形图

6.1 array_keyboard.v

6.2 key_decode.v

6.3 calculator.v

6.4 div.v

7.遇到的主要难题

7.1 结果显示时使能数码管的位数与第二个数据相同

在加入了自适应使能数码管功能的代码后,遇到了结果输出后被点亮的位数与第二个数据相同,而不是取决于结果的位数,比如70+55=125,最后只会显示25,因为第二个数据55只有两位。

问题的原因是计算结果赋值给num1时用的非阻塞赋值(<=),这样的话判断num1的位数时其实结果还没有被赋值给num1,此时num1里存的还是第二个数据,将num1的所有赋值符号换为阻塞赋值(=)就能让num1及时更新,但是同一个寄存器的所有赋值符号必须一致,不能阻塞赋值与非阻塞赋值混用。

7.2 除法器的使用

直接用除号(/)和取余(%)一句大概要占用30%的资源,这显然是不合适的,于是我选择使用除法器。但是除法器需要一段时间计算,程序里却不能直接延迟。在观察了除法器的仿真波形图后我发现了当结果计算完成后quotient_vld会被置1一次,于是我便利用这个特点,等到计算完成后再把商赋值给num1输出。

7.3 被除数小于除数时数码管使能位数错误

完成了除不尽的保留小数时,我在一次测试中发现如果被除数小于除数(3/43)前面的几位0时数码管不会被使能(0.0698显示为698),为了解决这个问题我新定义了一个“d”,当被除数小于除数时会把需要使能的位数传给d,比如保留三位小数时使能后四个数码管,并在num1位数判断的同时加上d的判断,如果d为0则不影响本次数码管使能,如果不为0则按d的值使能数码管。

8.FPGA的资源利用说明

Design Summary:
Number of registers: 456 out of 4635 (10%)
PFU registers: 456 out of 4320 (11%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1385 out of 2160 (64%)
SLICEs as Logic/ROM: 1385 out of 2160 (64%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 545 out of 2160 (25%)
Number of LUT4s: 2770 out of 4320 (64%)
Number used as logic LUTs: 1680
Number used as distributed RAM: 0
Number used as ripple logic: 1090
Number used as shift registers: 0
Number of PIO sites used: 31 + 4(JTAG) out of 105 (33%)
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%)
Notes:-
1. Total number of LUT4s = (Number of logic LUT4s) + 2*(Number of distributed RAMs) + 2*(Number of ripple logic)
2. Number of logic LUT4s does not include count of distributed RAM and ripple logic.
Number of clocks: 3
Net clk_c: 253 loads, 253 rising, 0 falling (Driver: PIO clk )
Net clk_200hz: 24 loads, 0 rising, 24 falling (Driver: keyboard/clk_200hz_38 )
Net scanclk_40khz_68 )
Number of Clock Enables: 41
Net rst_n_c: 1 loads, 1 LSLICEs
Net keyboard/clk_200hz_N_24_enable_48: 6 loads, 6 LSLICEs
Net keyboard/clk_200hz_N_24_enable_40: 6 loads, 6 LSLICEs
Net keyboard/clk_200hz_N_24_enable_47: 6 loads, 6 LSLICEs
Net clk_c_enable_208: 6 loads, 6 LSLICEs
Net keyboard/clk_200hz_N_24_enable_46: 6 loads, 6 LSLICEs
Net decode/seg_data_3__N_150: 2 loads, 2 LSLICEs
Net clk_c_enable_215: 3 loads, 3 LSLICEs
Net calc/clk_c_enable_140: 14 loads, 14 LSLICEs
Net calc/clk_c_enable_40: 25 loads, 25 LSLICEs
Net calc/clk_c_enable_168: 14 loads, 14 LSLICEs
Net calc/clk_c_enable_87: 27 loads, 27 LSLICEs
Net calc/clk_c_enable_57: 2 loads, 2 LSLICEs
Net calc/clk_c_enable_49: 3 loads, 3 LSLICEs
Net calc/clk_c_enable_44: 2 loads, 2 LSLICEs
Net calc/clk_c_enable_42: 2 loads, 2 LSLICEs
Net calc/clk_c_enable_142: 14 loads, 14 LSLICEs
Net calc/clk_c_enable_212: 5 loads, 5 LSLICEs
Net calc/clk_c_enable_52: 3 loads, 3 LSLICEs
Net calc/clk_c_enable_88: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_93: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_94: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_95: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_213: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_214: 1 loads, 1 LSLICEs
Net calc/clk_c_enable_216: 1 loads, 1 LSLICEs
Net calc/vld: 14 loads, 14 LSLICEs
Net calc/quotient_vld_N_1023: 18 loads, 18 LSLICEs
Net calc/clk_c_enable_75: 2 loads, 2 LSLICEs
Net calc/clk_c_enable_185: 1 loads, 1 LSLICEs
Net calcclk_c_enable_199: 13 loads, 13 LSLICEs
Net calcclk_c_enable_56: 1 loads, 1 LSLICEs
Net calcclk_c_enable_184: 2 loads, 2 LSLICEs
Net calcclk_c_enable_74: 1 loads, 1 LSLICEs
Net calcclk_c_enable_200: 1 loads, 1 LSLICEs
Net scan/state_0: 4 loads, 4 LSLICEs
Net scan/clk_40khz_enable_21: 9 loads, 9 LSLICEs
Net scan/clk_40khz_enable_2: 1 loads, 1 LSLICEs
Net scan/clk_40khz_enable_3: 1 loads, 1 LSLICEs
Net scan/clk_40khz_enable_4: 1 loads, 1 LSLICEs
Net scan/state_1: 2 loads, 2 LSLICEs
Number of LSRs: 15
Net clk_200hz_N_26: 8 loads, 8 LSLICEs
Net keyboard/n16566: 2 loads, 2 LSLICEs
Net calc/n32585: 1 loads, 1 LSLICEs
Net calc/n16536: 2 loads, 2 LSLICEs
Net calc/n43120: 4 loads, 4 LSLICEs
Net calc/state_c_2: 1 loads, 1 LSLICEs
Net calc/n47538: 23 loads, 23 LSLICEs
Net calc/n47540: 20 loads, 20 LSLICEs
Net calc/n32879: 4 loads, 4 LSLICEs
Net calcn32891: 1 loads, 1 LSLICEs
Net calcn32881: 2 loads, 2 LSLICEs
Net scan/n42880: 2 loads, 2 LSLICEs
Net scan/n42873: 3 loads, 3 LSLICEs
Net scan/cnt_9__N_2076: 5 loads, 5 LSLICEs
Net scan/n9: 1 loads, 1 LSLICEs
Number of nets driven by tri-state buffers: 0
Top 10 highest fanout non-clock nets:
Net calc/n14923: 208 loads
Net calc/n47531: 207 loads
Net rst_n_c: 156 loads
Net opcode_2: 88 loads
Net state_0: 71 loads
Net calc/state1_1: 70 loads
Net calc/n13: 63 loads
Net calc/num2_0: 60 loads
Net calc/n47595: 59 loads
Net n47592: 54 loads

附件下载
FPGA计算器完整代码.zip
完整代码以及可以直接烧录的.jet文件
FPGA计算器完整代码及主要模块仿真文件.zip
完整代码以及主要模块的仿真文件(不含.jet文件)
团队介绍
一位喜爱嵌入式和FPGA开发的大学生
团队成员
QingSpace
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号