2024年寒假练 - 基于FPGA实现两位十进制加、减、乘、除计算器
该项目使用了STEP-MXO2-LPC 和 STEP-BASEBOARD-V4.0,实现了计算器的设计,它的主要功能为:实现一个两位十进制数加、减、乘、除运算的计算器。
标签
FPGA
EDA
数字电路
verilog
邓文聪
更新2024-04-02
北京理工大学
1114

一、项目需求

实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制,4×4键盘按键分配如下图所示。

 

4×4键盘按键分配图

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

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


二、需求分析

(一)功能需求

  • 四则运算支持:计算器必须能够执行两位十进制数的加、减、乘、除运算。
  • 输入方式:使用4×4键盘输入运算数和选择运算符。键盘的布局需确保可以输入数字0-9和选择加、减、乘、除运算。
  • 显示需求(基本):通过8个八段数码管显示输入的运算数和计算结果。每个运算数用两个数码管表示,其中一个显示十位数,另一个显示个位数。数码管应支持动态显示以便于用户输入和查看结果。
  • 显示过程:当输入两位数时,首先显示的数字应出现在个位数的位置,随后跳转到十位数的位置,新的数字则显示在个位数的位置。

(二)扩展功能需求

  • TFT LCD显示:除了基本的数码管显示,项目还应支持使用TFT LCD显示器来展示输入的运算数、运算符以及运算结果。这要求设计一个用户友好的界面,既能清楚展示所有必要的信息,也能提升用户体验。

(三)用户界面需求

  • 键盘交互:设计合理的键盘交互逻辑,确保用户能够直观地输入数字和选择运算符。
  • 显示界面设计(扩展):为TFT LCD显示器设计的用户界面需要直观、易用。应清晰显示运算数、运算符以及运算结果,同时保持界面的美观性。

(四)技术需求

  • Verilog实现:整个计算器的逻辑需要用Verilog语言实现,并在FPGA板上进行部署和测试。
  • 硬件兼容性:需要确保设计兼容基于LCMXO2-4000HC的FPGA板(STEP-MXO2-LPC)及其拓展板(STEP-BASEBOARD-V4.0),特别是对于数码管和TFT LCD显示器的接口和驱动。

三、实现方法

本计算器设计采用模块化方法,各功能模块通过Verilog HDL实现并在FPGA上实现硬件部署。设计中包括键盘输入、运算核心、BCD编码器和动态数码管显示模块,以及与595串转并模块的接口。以下是各个模块的功能与实现方法


(一)矩阵键盘输入模块(keyboard:keyboard_inst)

功能:处理4×4键盘的输入,对应十进制数字输入和四则运算符号的选择。

实现方法:通过扫描键盘矩阵,读取被按下的键,并将其编码为计算器可以识别的数字或运算符号信号。


(二) 计算核心模块(calculator:calculator_inst)

功能:执行加、减、乘、除运算。

实现方法:接收键盘模块的输入作为操作数和运算符,进行算术运算,并将结果输出至显示模块。


(三)BCD编码器模块(bcd8421:bcd8421_inst)

功能:将计算核心输出的二进制结果转换为BCD(二进制编码的十进制)格式,以适应数码管显示要求。

实现方法:实现二进制到BCD的转换算法,确保结果可以在数码管上正确显示。


(四)动态显示模块(dynamic:dynamic_inst)

功能:控制数码管的动态显示,确保输入和结果可以清晰显示。

实现方法:根据输入的BCD信号控制数码管,实现动态扫描,以实现连续和清晰的显示效果。


(五)595串转并模块接口(hc595:hc595_inst)

功能:控制数码管的实际显示,将内部数据线状态转换为数码管的控制信号。

实现方法:通过串转并的操作,将内部信号转换为数码管所需的并行信号,实现对数码管的控制。

四、功能框图

五、代码及其说明

(一)矩阵键盘模块

在这个键盘模块中,虽然没有明确用状态机的术语来定义各个状态,但可以从代码逻辑中解读出状态机的实现。实际上,模块的每个时钟周期都可以被看作是一个状态,而计数器的值和行控制信号`row`决定了模块处于扫描周期的哪个阶段。


让我们来详细分析这个过程:


(1)初始化状态:当复位信号`rst_n`为低时,系统处于初始化状态,所有寄存器(`cnt`, `row`, `key`, `key_r` 和 `keyboard_en`)被清零或设定为初始值。


(2)计数器状态:在初始化之后,系统进入计数器状态。在这个状态下,`cnt`寄存器开始计数直到达到`CNT_MAX`。这个计数器决定了键盘扫描的频率。每次计数结束后,`cnt_end`信号会为高,表示一个扫描周期的结束。


(3)行扫描状态:行控制`row`的初始值设置为`4'b1111`,意味着所有行都未激活。在每个扫描周期开始时(即`row == 4'b1111`),`row`被设置为`4'b1110`,激活第一行。 随后每当`cnt_end`为高,行控制`row`左移一位,意味着扫描下一行。因此,`row`会依次变为`4'b1101`, `4'b1011`, `4'b0111`,再回到`4'b1111`,这样就完成了对所有行的扫描。


(4)按键状态检测状态:在每次行扫描的状态中,通过检测`col`线的变化来确定是否有按键被按下。如果某一行被激活时,对应列线变为低,那么表示相应位置的按键被按下。对应的按键值将存储到`key`寄存器中。


(5)边沿检测状态:`key_r`寄存器在每个时钟周期记录上一次的`key`值。通过比较`key_r`和`key`的不同,可以检测到按键的按下和释放(正边沿和负边沿)。在这个模块中,我们通过`key_posedge`信号检测到按键释放的动作,因为这意味着用户完成了一个按键操作。


(6)输出编码状态:当检测到一个按键释放(`key_posedge`为高)时,模块将识别哪个按键被操作,并将相应的编码输出到`keyboard_num`。输出的编码与按键的物理位置相关。


(7)状态机的循环:上述状态在每个时钟周期中循环进行,从而实现了对键盘的连续扫描和按键操作的检测。


在这种实现中,并没有显式地使用状态机的特定语法结构(例如`case`语句或状态寄存器),但是通过逻辑控制和条件判断,模块的行为模拟了一个状态机的运作。每个时钟周期对应一个状态,而条件(如`cnt_end`, `row`, `col`的值)决定了转移到下一个状态的行为。这就是利用状态机和计数器概念来检测按键动作并输出按键编号的方法。

module	array_keyboard
#(
parameter CNT_MAX=119_999
)
(
input wire clk ,
input wire rst_n ,
input wire [3:0] col ,
output reg [3:0] row ,
output reg keyboard_en ,
output reg [3:0] keyboard_num
);

reg [31:0] cnt ;
wire cnt_end ;
reg [15:0] key ;
reg [15:0] key_r ;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
cnt<=0;
else if(cnt_end)
cnt<=0;
else
cnt <= cnt + 1;

assign cnt_end = (cnt == CNT_MAX);

always @(posedge clk or negedge rst_n)
if (rst_n==0)
row <= 4'b1111;
else if(row == 4'b1111)
row <= 4'b1110;
else if(cnt_end)
row <= {row[2:0], row[3]};
else
row<=row;

always @(posedge clk or negedge rst_n)
if(rst_n==0)
key <= 0;
else if(cnt_end)
begin
if (row[0] == 0) key[3:0] <= ~col;
if (row[1] == 0) key[7:4] <= ~col;
if (row[2] == 0) key[11:8] <= ~col;
if (row[3] == 0) key[15:12] <= ~col;
end

always @(posedge clk or negedge rst_n)
if(rst_n==0)
key_r <= 0;
else
key_r <= key;

wire[15:0] key_posedge = (~key_r) & key;

always @(posedge clk or negedge rst_n)
if (rst_n==0) begin
keyboard_num <= 0;
end else if (key_posedge) begin
if (key_posedge[0]) keyboard_num <= 'hd;
else if (key_posedge[1]) keyboard_num <= 'hf;
else if (key_posedge[2]) keyboard_num <= 'he;
else if (key_posedge[3]) keyboard_num <= 'h0;
else if (key_posedge[4]) keyboard_num <= 'hc;
else if (key_posedge[5]) keyboard_num <= 'h1;
else if (key_posedge[6]) keyboard_num <= 'h2;
else if (key_posedge[7]) keyboard_num <= 'h3;
else if (key_posedge[8]) keyboard_num <= 'hb;
else if (key_posedge[9]) keyboard_num <= 'h6;
else if (key_posedge[10]) keyboard_num <= 'h5;
else if (key_posedge[11]) keyboard_num <= 'h4;
else if (key_posedge[12]) keyboard_num <= 'ha;
else if (key_posedge[13]) keyboard_num <= 'h9;
else if (key_posedge[14]) keyboard_num <= 'h8;
else if (key_posedge[15]) keyboard_num <= 'h7;
end else begin
keyboard_num <= 0;
end

always @(posedge clk or negedge rst_n)
if(rst_n==0)
keyboard_en <= 0;
else if(key_posedge)
keyboard_en <= 1;
else
keyboard_en <= 0;

endmodule


(二)计算器模块

该计算器模块采用了有限状态机(FSM)的设计方法来处理从键盘模块接收到的输入,并根据这些输入执行算术运算。模块接收的输入包括操作数和运算符号,并在内部通过一系列状态对这些输入进行管理和处理。


在初始状态`IDLE`下,模块等待用户开始输入,一旦检测到键盘输入使能信号`keyboard_en`被激活,便开始接受操作数`a`的输入,并且随着进一步的数字输入,逐步构建出多位数的操作数。这一过程通过状态`NUM1`到`NUM4`进行管理,确保数字可以连续输入而不被中断。


当接收到运算符号时,模块进入`SYMBOL`状态。此状态将输入的运算符保存在`sign`变量中,并为输入第二个操作数`b`做准备。紧接着,模块通过状态`NUM5`到`NUM8`,采用与操作数`a`相同的方法来构建操作数`b`。


一旦检测到等号输入或确定操作数`b`的输入完成,模块进入`RESULT`状态,此时,根据之前保存的运算符号,进行相应的加、减、乘、除运算,并将结果存储回操作数`a`以便显示。之后,模块进入`WAIT`状态,在该状态下,显示当前计算结果,并等待进一步的用户输入。


在整个计算过程中,输出寄存器`show`负责在不同的状态下更新显示的值。它会根据当前的状态和已经输入的数值来刷新,以确保用户随时可以看到他们输入的数字或最终的计算结果。


综上所述,计算器模块的设计充分考虑了用户交互的流畅性和计算的准确性。通过有限状态机的运用,模块能够清晰地控制输入、处理和输出的逻辑流程,确保在每个步骤中都能给用户提供直观的反馈。这种方法的成功实现依赖于对状态之间转换逻辑的严密控制,以及对计算逻辑的精确编码。

module	calculator
(
input wire clk ,
input wire rst_n ,
input wire [3:0] keyboard_num ,
input wire keyboard_en ,
output reg [31:0] show
);

localparam IDLE = 4'b0000; //复位状态
localparam NUM1 = 4'b0001; //数字1
localparam NUM2 = 4'b0010; //数字2
localparam NUM3 = 4'b0011; //数字3
localparam NUM4 = 4'b0100; //数字4
localparam SYMBOL = 4'b0101; //加减乘除
localparam NUM5 = 4'b0110; //数字5
localparam NUM6 = 4'b0111; //数字6
localparam NUM7 = 4'b1000; //数字7
localparam NUM8 = 4'b1001; //数字8
localparam RESULT = 4'b1010; //等于号
localparam WAIT = 4'b1011; //等待

reg [3:0] state;
reg [31:0] a;
reg [31:0] b;
reg [3:0] sign;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
begin
state<=IDLE;
a<=0;
b<=0;
sign<=0;
end
else
case(state)
IDLE:
if(keyboard_en)
begin
state<=NUM1;
a<=keyboard_num;
end
else
begin
state<=IDLE;
a<=a;
end

NUM1:
if(keyboard_en)
begin
if(keyboard_num=='ha || keyboard_num=='hb || keyboard_num=='hc || keyboard_num=='hd)
begin
state<=SYMBOL;
sign<=keyboard_num;
end
else
begin
state<=NUM2;
a<=a*10+keyboard_num;
end
end
else
begin
state<=NUM1;
a<=a;
end

NUM2:
if(keyboard_en)
begin
if(keyboard_num=='ha || keyboard_num=='hb || keyboard_num=='hc || keyboard_num=='hd)
begin
state<=SYMBOL;
sign<=keyboard_num;
end
else
begin
state<=NUM3;
a<=a*10+keyboard_num;
end
end
else
begin
state<=NUM2;
a<=a;
end

NUM3:
if(keyboard_en)
begin
if(keyboard_num=='ha || keyboard_num=='hb || keyboard_num=='hc || keyboard_num=='hd)
begin
state<=SYMBOL;
sign<=keyboard_num;
end
else
begin
state<=NUM4;
a<=a*10+keyboard_num;
end
end
else
begin
state<=NUM3;
a<=a;
end

NUM4:
if(keyboard_en)
begin
if(keyboard_num=='ha || keyboard_num=='hb || keyboard_num=='hc || keyboard_num=='hd)
begin
state<=SYMBOL;
sign<=keyboard_num;
end
else
begin
state<=NUM4;
a<=a;
end
end
else
begin
state<=NUM4;
a<=a;
end

SYMBOL:
if(keyboard_en)
begin
state<=NUM5;
b<=keyboard_num;
end
else
begin
state<=SYMBOL;
b<=b;
end

NUM5:
if(keyboard_en)
begin
if(keyboard_num=='hf)
state<=RESULT;
else
begin
state<=NUM6;
b<=b*10+keyboard_num;
end
end
else
begin
state<=NUM5;
b<=b;
end

NUM6:
if(keyboard_en)
begin
if(keyboard_num=='hf)
state<=RESULT;
else
begin
state<=NUM7;
b<=b*10+keyboard_num;
end
end
else
begin
state<=NUM6;
b<=b;
end

NUM7:
if(keyboard_en)
begin
if(keyboard_num=='hf)
state<=RESULT;
else
begin
state<=NUM8;
b<=b*10+keyboard_num;
end
end
else
begin
state<=NUM7;
b<=b;
end

NUM8:
if(keyboard_en)
begin
if(keyboard_num=='hf)
state<=RESULT;
else
begin
state<=NUM8;
b<=b;
end
end
else
begin
state<=NUM8;
b<=b;
end

RESULT:
begin
state<=WAIT;
if(sign==4'ha)
a<=a/b;
else if(sign==4'hb)
a<=a*b;
else if(sign==4'hc)
a<=a-b;
else if(sign==4'hd)
a<=a+b;
end

WAIT:
if(keyboard_en)
begin
if(keyboard_num=='ha || keyboard_num=='hb || keyboard_num=='hc || keyboard_num=='hd)
begin
state<=SYMBOL;
sign<=keyboard_num;
end
else
begin
state<=NUM1;
a<=keyboard_num;
end
end
else
state<=WAIT;

default: state<=IDLE;

endcase

always@(posedge clk or negedge rst_n)
if(rst_n==0)
show<=0;
else if(state==NUM1||state==NUM2||state==NUM3||state==NUM4||state==RESULT||state==WAIT)
show<=a;
else if(state==NUM5||state==NUM6||state==NUM7||state==NUM8)
show<=b;
else
show<=show;

endmodule


(三)数码管动态模块

在这里,我将详细解释`dynamic_segment`模块的设计与实现,该模块负责在数码管上以动态方式显示数字。通过利用高速轮换显示各个数码管的方法,模块能够在人眼无法察觉的短时间内依次激活每个数码管,从而实现同时显示多个数字的错觉。该模块接受八个四位的二进制数字输入,并将这些数字轮流显示在数码管上。这种显示方式通过一个精心设计的定时器逻辑、数码管选择逻辑,以及数字到数码管显示码的映射逻辑共同实现。


(1) 定时器逻辑

核心的定时器由一个计数器组成,该计数器根据外部提供的时钟信号`clk`进行计数。从0开始,计数器一直增加直到达到预设阈值`CNT_MAX`,此时计数器重置并触发一次数码管的切换操作。这个循环确保了每个数码管都能在预定的时间间隔内被轮流激活。


(2) 数码管选择逻辑

模块通过维护一个选择寄存器`sel`来决定哪一个数码管被激活。每次计数器溢出时,`sel`的值会递增,以此选择下一个被激活的数码管。一旦所有数码管均已显示,`sel`寄存器的值会重置,从而开始新的显示循环。


(3)数字显示逻辑

基于`sel`寄存器当前的值,模块从八个数字输入中选择一个,并将其值存储到`data`寄存器中。`data`寄存器中的值随后用于查找特定的映射表,该表将二进制数字转换为数码管上相应的七段LED显示码。


最后,模块具有两个输出信号:`decdata`和`sel_smg`,分别用于控制数码管上的LED段和选择哪个数码管进行显示。`decdata`通过查找表得到,决定了数码管上哪些段被激活,以显示正确的数字。`sel_smg`则根据`sel`寄存器的值动态生成,通过对特定的位进行清零来激活对应的数码管。


通过上述的设计与实现,`dynamic_segment`模块不仅优化了对硬件资源的使用,还通过动态显示技术实现了功耗的有效管理。每个数码管在其对应的时间窗口中准确显示了指定的数字,向用户展示了完整的数字序列,从而在功能与效率之间取得了良好的平衡。

module	dynamic_segment														//数码管动态显示,10ms轮流一个数码管
#(
parameter CNT_MAX = 119_999 //10ms计数
)
(
input wire clk ,
input wire rst_n ,
input wire [3:0] num1 ,
input wire [3:0] num2 ,
input wire [3:0] num3 ,
input wire [3:0] num4 ,
input wire [3:0] num5 ,
input wire [3:0] num6 ,
input wire [3:0] num7 ,
input wire [3:0] num8 ,
output reg [7:0] decdata ,
output reg [7:0] sel_smg
);

reg [31:0] cnt ;
reg [2:0] sel ;
reg [3:0] data ;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
cnt<=0;
else if(cnt<CNT_MAX)
cnt<=cnt+1;
else
cnt<=0;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
sel<=0;
else if(cnt==CNT_MAX)
begin
if(sel==3'd7)
sel<=0;
else
sel<=sel+1;
end
else
sel<=sel;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
sel_smg<=8'b1111_1111;
else
case(sel)
3'b000 : sel_smg<=8'b1111_1110;
3'b001 : sel_smg<=8'b1111_1101;
3'b010 : sel_smg<=8'b1111_1011;
3'b011 : sel_smg<=8'b1111_0111;
3'b100 : sel_smg<=8'b1110_1111;
3'b101 : sel_smg<=8'b1101_1111;
3'b110 : sel_smg<=8'b1011_1111;
3'b111 : sel_smg<=8'b0111_1111;
default : sel_smg<=8'b1111_1111;
endcase

always@(posedge clk or negedge rst_n)
if(rst_n==0)
data<=4'b0000;
else
case(sel)
3'b000 : data<=num1 ;
3'b001 : data<=num2 ;
3'b010 : data<=num3 ;
3'b011 : data<=num4 ;
3'b100 : data<=num5 ;
3'b101 : data<=num6 ;
3'b110 : data<=num7 ;
3'b111 : data<=num8 ;
default : data<=4'b0000 ;
endcase

always@(data)
begin
case (data)
4'b0000 : decdata[7:0] <= 8'b00111111;
4'b0001 : decdata[7:0] <= 8'b00000110;
4'b0010 : decdata[7:0] <= 8'b01011011;
4'b0011 : decdata[7:0] <= 8'b01001111;
4'b0100 : decdata[7:0] <= 8'b01100110;
4'b0101 : decdata[7:0] <= 8'b01101101;
4'b0110 : decdata[7:0] <= 8'b01111101;
4'b0111 : decdata[7:0] <= 8'b00000111;
4'b1000 : decdata[7:0] <= 8'b01111111;
4'b1001 : decdata[7:0] <= 8'b01101111;
default : decdata[7:0] <= 8'b00000000;
endcase
end

endmodule


(四)BCD8421模块

这里将详细解释`bcd8421`模块的设计与实现,该模块的主要功能是将32位二进制数转换为BCD(二进制编码的十进制)格式,以便在数码管上显示。这一过程涉及到的核心技术是双重倍频技术(Double Dabble Algorithm),一个常用于二进制到BCD转换的算法。模块旨在接受一个32位的二进制输入(`data`),并将其转换为8个4位的BCD表示,这8个BCD数字(`num1`至`num8`)对应数码管上从左到右的显示顺序。实现这一目标需要一个精确的转换过程,保证二进制数准确地转换为其等效的BCD表示。


实现逻辑(转换过程):


(1)初始化:

  • 计数器`cnt_shift`跟踪转换过程中的位移操作次数。
  • 64位寄存器`data_shift`用于进行位操作,初始时将输入`data`左对齐到低32位。
  • `shift_flag`标志用于控制位移与加3条件的交替执行。


(2)转换操作:

  • 转换开始时,首先检查每个4位组(称为nibble)是否大于4。若大于4,则对该nibble加3,以符合BCD编码的要求。
  • 接下来,整个`data_shift`左移一位,并交替进行加3校正和位移操作,直到完成32次左移操作。


(3)位移与加3逻辑:

  • 每完成一次左移前,检查所有的nibble,并对大于4的nibble加3,以保证其符合BCD的要求。
  • 通过`shift_flag`控制交替执行加3和位移操作,确保每个操作步骤正确交替进行。


(4)输出更新:

  • 在完成所有位移操作后,`cnt_shift`达到33时(32次位移加上一次初始赋值),更新输出BCD数字`num1`至`num8`,这些数字直接从`data_shift`的高32位读取,因为这部分现在包含了转换后的BCD码。


通过以上设计,`bcd8421`模块能够有效地将32位二进制数转换成8个4位的BCD表示,为数码管显示提供了必要的数据格式。该模块的实现充分展示了在硬件设计中进行数字表示转换的方法,确保了数字信息能以人类可读的格式在数字显示设备上准确展示。此外,该设计方法展现了硬件级别算法的实现,提供了一种高效且可靠的二进制到BCD转换解决方案。

module	bcd8421										//二进制转BCD8421
(
input wire clk ,
input wire rst_n ,
input wire [31:0] data ,
output reg [3:0] num1 ,
output reg [3:0] num2 ,
output reg [3:0] num3 ,
output reg [3:0] num4 ,
output reg [3:0] num5 ,
output reg [3:0] num6 ,
output reg [3:0] num7 ,
output reg [3:0] num8
);

reg [7:0] cnt_shift;
reg [63:0] data_shift;
reg shift_flag;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
cnt_shift<=0;
else if(cnt_shift==33&&shift_flag==1)
cnt_shift<=0;
else if(shift_flag==1)
cnt_shift<=cnt_shift+1;
else
cnt_shift<=cnt_shift;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
data_shift<=0;
else if(cnt_shift==0)
data_shift<={32'b0,data};
else if(cnt_shift<=32&&shift_flag==0)
begin
data_shift[35:32]<=(data_shift[35:32]>4)?(data_shift[35:32]+2'd3):(data_shift[35:32]);
data_shift[39:36]<=(data_shift[39:36]>4)?(data_shift[39:36]+2'd3):(data_shift[39:36]);
data_shift[43:40]<=(data_shift[43:40]>4)?(data_shift[43:40]+2'd3):(data_shift[43:40]);
data_shift[47:44]<=(data_shift[47:44]>4)?(data_shift[47:44]+2'd3):(data_shift[47:44]);
data_shift[51:48]<=(data_shift[51:48]>4)?(data_shift[51:48]+2'd3):(data_shift[51:48]);
data_shift[55:52]<=(data_shift[55:52]>4)?(data_shift[55:52]+2'd3):(data_shift[55:52]);
data_shift[59:56]<=(data_shift[59:56]>4)?(data_shift[59:56]+2'd3):(data_shift[59:56]);
data_shift[63:60]<=(data_shift[63:60]>4)?(data_shift[63:60]+2'd3):(data_shift[63:60]);
end
else if(cnt_shift<=32&&shift_flag==1)
data_shift<=data_shift<<1;
else
data_shift<=data_shift;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
shift_flag<=0;
else
shift_flag<=~shift_flag;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
begin
num1 <=0 ;
num2 <=0 ;
num3 <=0 ;
num4 <=0 ;
num5 <=0 ;
num6 <=0 ;
num7 <=0 ;
num8 <=0 ;
end
else if(cnt_shift==33)
begin
num1 <=data_shift[35:32] ;
num2 <=data_shift[39:36] ;
num3 <=data_shift[43:40] ;
num4 <=data_shift[47:44] ;
num5 <=data_shift[51:48] ;
num6 <=data_shift[55:52] ;
num7 <=data_shift[59:56] ;
num8 <=data_shift[63:60] ;
end

endmodule


/*
module bcd8421
(
input wire [31:0] data ,
output wire [3:0] num1 ,
output wire [3:0] num2 ,
output wire [3:0] num3 ,
output wire [3:0] num4 ,
output wire [3:0] num5 ,
output wire [3:0] num6 ,
output wire [3:0] num7 ,
output wire [3:0] num8
);

assign num1 = data/1000_0000 ;
assign num2 = (data/100_0000)%10 ;
assign num3 = (data/10_0000)%10 ;
assign num4 = (data/1_0000)%10 ;
assign num5 = (data/1000)%10 ;
assign num6 = (data/100)%10 ;
assign num7 = (data/10)%10 ;
assign num8 = data%10 ;

endmodule
*/


(五)HC595芯片驱动模块

在本报告中,我们详细探讨`hc595`芯片驱动模块的设计和实现,这是一个串转并接口模块,主要用于控制595系列的移位寄存器。通过此模块,可以实现将串行数据转换为并行输出,进而驱动数码管显示或其他需要并行数据输入的设备。该模块接收8位的数码管解码数据(`decdata`)和8位的数码管选择信号(`sel_smg`),然后将这两个8位数据串联成16位数据(`data`),并通过595移位寄存器的串行数据输入(`sdio`)、移位寄存器时钟(`sclk`)和存储寄存器时钟(`rclk`)输出。


模块首先通过将8位数码管解码数据和8位数码管选择信号串联,形成16位的串行数据流。这一步骤的完成,依赖于对输入数据的精确组装,确保了数据的正确顺序和完整性。随后,模块内部通过精心设计的计数器和位控制逻辑,逐位地将这些数据输送到移位寄存器中。具体而言,内部实现了一个2位的周期计数器`cnt`来调节数据传输的节奏,以及一个4位的位计数器`bit`来跟踪当前正在传输的数据位。


在数据传输过程中,模块通过动态控制`sdio`(串行数据输入)信号,依次输出数据流中的每一位。与此同时,`sclk`(移位寄存器时钟)信号在每个数据位准备好时提供脉冲,触发移位寄存器的移位操作。数据传输完成后,`rclk`(存储寄存器时钟)信号负责触发一次存储操作,将移位寄存器中的数据锁存并并行输出。


通过这一系列精密的控制逻辑,hc595芯片驱动模块有效地实现了从串行到并行的数据转换,极大地增强了微控制器的I/O扩展能力。此模块的设计不仅体现了数字电路设计中的基本原理,也提供了一种高效、可靠的方式来满足高I/O需求设备的驱动需求。

module	hc595
(
input wire clk ,
input wire rst_n ,
input wire [7:0] decdata ,
input wire [7:0] sel_smg ,
output reg sdio ,
output reg sclk ,
output reg rclk
);

wire [15:0] data ;
reg [1:0] cnt ;
reg [3:0] bit ;

assign data={sel_smg,decdata[0],decdata[1],decdata[2],decdata[3],decdata[4],decdata[5],decdata[6],decdata[7]};

always@(posedge clk or negedge rst_n)
if(rst_n==0)
cnt<=0;
else if(cnt==3)
cnt<=0;
else
cnt<=cnt+1;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
bit<=0;
else if(bit==15&&cnt==3)
bit<=0;
else if(cnt==3)
bit<=bit+1;
else
bit<=bit;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
sdio<=0;
else if(cnt==0)
sdio<=data[bit];
else
sdio<=sdio;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
sclk<=0;
else if(cnt==2)
sclk<=1;
else if(cnt==0)
sclk<=0;
else
sclk<=sclk;

always@(posedge clk or negedge rst_n)
if(rst_n==0)
rclk<=0;
else if(bit==0&&cnt==0)
rclk<=1;
else if(bit==0&&cnt==2)
rclk<=0;
else
rclk<=rclk;

endmodule


(六)顶层文件

top模块的设计思路及其实现逻辑,该模块综合了前面提及的五个子模块,实现了一个完整的计算器功能,包括键盘输入、计算处理、二进制到BCD转换、动态数码管显示,以及595串转并接口的控制。top模块的设计目的是将一个基于键盘输入的计算器设计整合为一个单一的顶层模块,使其能在FPGA或其他硬件平台上实现。通过整合array_keyboardcalculatorbcd8421dynamic_segmenthc595模块,实现从用户输入到数字显示的全过程。


实现逻辑:


1. 键盘输入(`array_keyboard`模块):

  • 负责处理用户从4x4键盘的输入,包括数字和运算符。
  • 输出被激活的键的编码及激活信号。


2. 计算逻辑(`calculator`模块):

  • 接收键盘模块的输出作为输入,执行加、减、乘、除等运算。
  • 输出运算结果为32位二进制数据。


3. 二进制到BCD转换(`bcd8421`模块):

  • 将`calculator`模块的计算结果转换为BCD格式,以便在数码管上显示。
  • 输出为8个4位的BCD编码,代表数码管上显示的数字。


4. 动态数码管显示(`dynamic_segment`模块):

  • 接收BCD编码的数字,控制数码管动态显示,实现连续显示效果。
  • 生成数码管的解码数据和选择信号。


5. 595串转并接口(`hc595`模块):

  • 接收`dynamic_segment`模块生成的解码数据和选择信号,通过595移位寄存器进行串转并操作。
  • 输出控制信号到数码管或其他显示设备。


`top`模块通过精密设计的子模块协作,实现了一个功能完整的计算器系统。从用户输入到显示输出的每一步都被仔细地设计和实现,确保了系统的高效性和可靠性。通过这种模块化的设计方法,每个子模块都可以独立地进行开发和测试,大大提高了整个系统的开发效率和可维护性。此外,该设计展示了在数字系统设计中,如何通过综合各个功能模块来实现复杂的应用系统。

module	top
(
input wire clk ,
input wire rst_n ,
input wire [3:0] col , //col[3]是第一列
output wire [3:0] row , //row[3]是第一行
output wire sdio ,
output wire sclk ,
output wire rclk
);

wire keyboard_en ;
wire [3:0] keyboard_num ;
wire [31:0] data ;
wire [3:0] num1 ;
wire [3:0] num2 ;
wire [3:0] num3 ;
wire [3:0] num4 ;
wire [3:0] num5 ;
wire [3:0] num6 ;
wire [3:0] num7 ;
wire [3:0] num8 ;
wire [7:0] decdata ;
wire [7:0] sel_smg ;

array_keyboard
#(
.CNT_MAX(119_999)
)
array_keyboard_inst
(
.clk (clk ),
.rst_n (rst_n ),
.col (col ),
.row (row ),
.keyboard_en (keyboard_en ),
.keyboard_num (keyboard_num )
);

calculator calculator_inst
(
.clk (clk ) ,
.rst_n (rst_n ),
.keyboard_num (keyboard_num ),
.keyboard_en (keyboard_en ) ,
.show (data )
);

bcd8421 bcd8421_inst
(
.clk (clk ) ,
.rst_n (rst_n ),
.data (data ),
.num1 (num1 ),
.num2 (num2 ),
.num3 (num3 ),
.num4 (num4 ),
.num5 (num5 ),
.num6 (num6 ),
.num7 (num7 ),
.num8 (num8 )
);

dynamic_segment //数码管动态显示,10ms轮流一个数码管
#(
.CNT_MAX ( 11999 ) //10ms计数
)
dynamic_segment_inst
(
.clk (clk ) ,
.rst_n (rst_n ),
.num1 (num1 ),
.num2 (num2 ),
.num3 (num3 ),
.num4 (num4 ),
.num5 (num5 ),
.num6 (num6 ),
.num7 (num7 ),
.num8 (num8 ),
.decdata (decdata ),
.sel_smg (sel_smg )
);

hc595 hc595_inst
(
.clk (clk ) ,
.rst_n (rst_n ),
.decdata (decdata ),
.sel_smg (sel_smg ),
.sdio (sdio ),
.sclk (sclk ),
.rclk (rclk )
);

endmodule


六、FPGA的资源利用说明

这是在Diamond Lattice平台设计一个基于FPGA实现的计算器项目的占用情况的报告。

以下是对各项资源占用的简要解释:


1. 寄存器使用情况

  • 总共使用了365个寄存器,占总数4635个的8%,其中所有寄存器都被分类为PFU(可编程功能单元)寄存器。


2. SLICE使用情况

  • SLICE是FPGA内部的基本逻辑单元。本项目中共使用了1811个SLICE,占总数2160个的84%。所有这些SLICE都用作逻辑/ROM,其中1502个还被用作进位逻辑。


3. LUT4使用情况:

  • LUT(查找表)是实现组合逻辑的关键组件。本项目使用了3618个LUT4,占总数4320个的84%,其中614个用于逻辑LUT,3004个用于级联逻辑。


4. PIO站点使用

  • 使用了13个PIO(可编程输入输出)站点以及4个用于JTAG(用于调试)的,占总数105个的16%。


5. 块RAM使用情况

  • 没有使用任何块RAM(随机存取存储器),总共0个,占10个可用的0%。


6. 系统资源使用情况

  • 用了1个全局设置复位(GSR),即100%。
  • POR(上电复位)和Bandgap(基准电压发生器)均被开启。
  • 未使用EFB(嵌入式功能块)、JTAG、Readback(读回)、Oscillator(振荡器)、Startup(启动配置)等其他资源。


6. 其他资源使用情况

  • 动态银行控制器、DCCA(延迟配置时钟阵列)、DCMA(时钟多路复用器阵列)、PLL(相位锁定环)、DQSDLL(数据四分频延迟锁定环)、CLKDIVC(时钟分频器)、ECLKSYNCA(外部时钟同步阵列)和ECLKBRIDGECS(外部时钟桥接控制器)等资源均未使用。


这份报告显示,计算器项目在FPGA上的实现主要依赖于大量的SLICE和LUT资源,显示出了较高的逻辑资源占用率。同时,项目没有使用块RAM和其他复杂的系统资源,如PLL和DCCA,这表明设计主要集中在组合逻辑和简单的序列逻辑上,而非数据存储或复杂的时钟管理。此外,较低的PIO使用率表明与外部世界的接口相对有限。整体而言,该设计有效地利用了FPGA的逻辑资源来实现计算器功能,但在系统复杂性和外设接口方面保持了适度的简单性。


而这是每个模块对于FPGA资源的占用率。在这个FPGA资源占用率分析报告中,特别引人注意的是`calculator`模块,它使用了大量的LUTs、Register和Carry Cells。这是由于计算器模块涉及到复杂的算术运算,这些运算需要多位宽的操作数和结果,因此需要多个逻辑单元和寄存器来实现这些功能。

下面我们简单分析:


计算器模块 (`calculator`):

  • LUT4(查找表)使用328个,占用较多,因为计算器模块涉及许多逻辑判断和运算操作,这需要多个查找表来实现。
  • PFU寄存器有112个,寄存器被用来保存中间结果以及当前的状态。
  • 进位单元(Carry Cells)使用了1447个,这个数值非常高,反映了在计算过程中用于算术运算的逻辑。大量的进位单元使用通常与加法器和减法器等运算电路相关联。
  • SLICE占用为1600.25个,说明计算器模块在逻辑切片上的占用相当高,这也与它处理复杂运算和维护状态机有关。


为什么计算器模块会使用较多资源:


1. 多状态和中间值:计算器模块在执行计算时会经历多个状态(如输入、计算、显示结果等)。每个状态都可能需要保存数据,这就需要更多的寄存器。


2. 算术运算:加减乘除运算本身是资源密集型的,尤其是在乘法和除法这样的运算中,需要大量的逻辑单元来进行位操作和累加。


3. 进位链:算术操作通常涉及到进位链,这在FPGA中通常由专用的进位逻辑实现,称为Carry Chains。这就解释了为何会有这么多的进位单元被使用。


综合考虑模块内部的复杂逻辑、算术运算的资源需求以及FPGA的架构,可以理解为什么`calculator`模块占用了相当多的资源。尽管使用了大量资源,但考虑到它执行的运算功能,这种资源使用是合理的。我们可以优化设计时可能会寻求减少不必要的状态、简化运算逻辑或利用FPGA的专用硬件块(如DSP块),以降低资源占用并提高效率。


七、演示视频




 

附件下载
top.v
顶层文件
array_keyboard.v
矩阵键盘模块
calculator.v
计算模块
bcd8421.v
BCD8421转换模块
dynamic.v
数码管动态显示模块
hc595.v
HC595芯片驱动模块
FPGA_Calculator_impl1.jed
计算器程序JED文件
团队介绍
大家好,我是来自北京理工大学的大三本科生,专业是电子科学与技术
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号