2024年寒假练 - 基于小脚丫FPGA套件STEP BaseBoard V4.0制作一个计算器
该项目使用了小脚丫FPGA套件STEP BaseBoard V4.0,实现了计算器的设计,它的主要功能为:两位十进制加、减、乘、除计算。
标签
FPGA
寒假一起练
小脚丫STEP FPGA套件
SyntaxError
更新2024-04-01
314

非常荣幸可以参与到这次寒假一起练活动,作为一个FPGA新手,也是在活动中学习到了许多,下面是具体的活动报告。

硬件介绍

项目使用的是小脚丫STEP-MXO2-LPC主控板子,个人认为最有特点的地方在于支持U盘模式(连接到上位机的USB端口,上位机自动弹出StepFPGA的U盘盘符)的下载,任何操作系统的电脑 - Windows、Mac OS以及Linux(包括树莓派)都可以在不安装任何驱动程序的情况下,直接将生成的jed配置文件发送到StepFPGA盘中即可完成编程。板子硬件规范如下所示,搭载了非常丰富的资源和外设。

- 核心器件:Lattice LCMXO2-4000HC-4MG132

- - 132脚BGA封装,引脚间距0.5mm,芯片尺寸8mm x 8mm;

- 上电瞬时启动,启动时间<1ms;

- 4320个LUT资源, 96Kbit 用户闪存,92Kbit RAM;

- 2+2路[PLL](https://www.eetree.cn/wiki/pll)+DLL;

- 嵌入式功能块(硬核):一路[SPI](https://www.eetree.cn/wiki/spi)、一路定时器、2路[I2C](https://www.eetree.cn/wiki/i2c)

- 支持DDR/DDR2/LPDDR存储器;

- 104个可热插拔I/O;

- 内核电压2.5-3.3V;

- 板载资源:

- - 两位7段数码管;

- 两个RGB三色LED;

- 8路用户LED;

- 4路拨码开关;

- 4路按键;

- 36个用户可扩展I/O(其中包括一路SPI硬核接口和一路I2C硬核接口)

- 支持的开发工具思德普开发的[Web IDE](https://www.stepfpga.com/)以及Lattice官方提供的[Diamond](https://www.stepfpga.com/doc/diamond)

- 支持MICO32/8软核处理器以及RISC-V软核

- 板上集成FPGA编程器,采用U盘的模式

- 一路USB Type C接口,可用于给核心板供电、给FPGA下载JED文件以及同上位机通过UART通信

- 板卡尺寸52mm x 18mm


同时也搭配了STEP BaseBoard V4.0的扩展底板进行开发,方便在数码管进行显示同时也可以通过矩阵键盘键入数字进行操作,板载资源如下:

- E2PROM芯片AT24C02

- 温湿度传感器SHT-20

- 加速度计

- 环境光和接近式传感器

- 气压计

- 4×4矩阵键盘

- 旋转编码器

- 电位计

- HDMI接口

- RGB LCD液晶屏

- 8位7段数码管

- 蜂鸣器模块

- UART通信模块CH340C

- ADC功能模块ADC081S101

- DAC功能模块DAC081S101

- WIFI功能模块ESP8266-12F

- PMOD接口


其他具体特性以及资料均可以在此获取:[小脚丫FPGA套件STEP BaseBoard V4.0 (eetree.cn)](https://class.eetree.cn/p/t_pc/goods_pc_detail/goods_detail/SPU_ENT_1703146423pPrXBgJxAVJXY?fromH5=true)

开发平台

官方推荐的在线WebIDE和Diamond我都有体验过,综合使用下来,还是主要使用的WebIDE平台进行开发,个人感觉对新手很友好,简化了很多功能,并且设置管脚的可视化界面也很利于新人上手,附两张开发平台的界面图。

Diamond:

WebIDE:

硬件框图以及软件流程

框图:


流程:

FPGA电路以及所用资源

电路:

资源占用:

数码管显示模块

要制作计算器并且方便后面开发debug或者进行其他操作,私以为首先搞定显示模块会便捷许多,官方有提供数码管的例程,链接:https://www.stepfpga.com/doc/%E6%95%B0%E7%A0%81%E7%AE%A1%E6%A8%A1%E5%9D%97


不过这个例程可能是3.0版本的底板,不能直接驱动新版底板的8个数码管,不过教程里的说明十分清晰易懂,并且数码管的驱动芯片也都是74HC595,因此可以基于已有例程扩展来驱动8个数码管,修改后的代码如下:

module Segment_scan_8
(
input clk_in, // 系统时钟
input rst_n_in, // 系统复位,低有效
input [3:0] seg_data_1, // SEG1 数码管要显示的数据
input [3:0] seg_data_2, // SEG2 数码管要显示的数据
input [3:0] seg_data_3, // SEG3 数码管要显示的数据
input [3:0] seg_data_4, // SEG4 数码管要显示的数据
input [3:0] seg_data_5, // SEG5 数码管要显示的数据
input [3:0] seg_data_6, // SEG6 数码管要显示的数据
input [3:0] seg_data_7, // SEG7 数码管要显示的数据 - 新增
input [3:0] seg_data_8, // SEG8 数码管要显示的数据 - 新增
input [7:0] seg_data_en, // 各位数码管数据显示使能,扩展为8位 [SEG8~SEG1]
input [7:0] seg_dot_en, // 各位数码管小数点显示使能,扩展为8位 [SEG8~SEG1]
output reg rclk_out, // 74HC595的RCK管脚
output reg sclk_out, // 74HC595的SCK管脚
output reg sdio_out // 74HC595的SER管脚
);

parameter CLK_DIV_PERIOD = 600; // 分频系数
localparam LOW = 1'b0;
localparam HIGH = 1'b1;
localparam IDLE = 3'd0;
localparam MAIN = 3'd1;
localparam WRITE = 3'd2;

// 创建数码管的字库
reg [7:0] seg [15:0];
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'h77; // A
seg[11] = 8'h7c; // b
seg[12] = 8'h39; // C
seg[13] = 8'h5e; // d
seg[14] = 8'h79; // E
seg[15] = 8'h71; // F
end

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

// 根据计数器计数的周期产生分频的脉冲信号
reg clk_div;
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
clk_div <= 1'b0;
end else begin
if(cnt==(CLK_DIV_PERIOD-1)) clk_div <= 1'b1;
else clk_div <= 1'b0;
end
end

// 使用状态机完成数码管的扫描和74HC595时序的实现
reg [15:0] data_reg;
reg [2:0] cnt_main;
reg [7:0] cnt_write;
reg [2:0] state = IDLE;
always @(posedge clk_in or negedge rst_n_in) begin
if (!rst_n_in) begin
state <= IDLE;
cnt_main <= 3'd0;
cnt_write <= 8'd0;
sdio_out <= 1'b0;
sclk_out <= 1'b0;
rclk_out <= 1'b0;
end else begin
case (state)
IDLE: begin
state <= MAIN;
cnt_main <= 3'd0;
cnt_write <= 8'd0;
sdio_out <= 1'b0;
sclk_out <= 1'b0;
rclk_out <= 1'b0;
end
MAIN: begin
if(cnt_main >= 3'd7) cnt_main <= 1'b0;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
//对6位数码管逐位扫描
3'd0: begin
state <= WRITE; //在配置完发给74HC595的数据同时跳转至WRITE状态,完成串行时序
data_reg <= {seg[seg_data_1]|(seg_dot_en[0]?8'h80:8'h00),seg_data_en[0]?8'h7f:8'hff};
//data_reg[15:8]为段选,data_reg[7:0]为位选
//seg[seg_data_1] 是根据端口的输入获取相应字库数据
//seg_dot_en[0]?8'h80:8'h00 是根据小数点显示使能信号 控制SEG1数码管的小数点DP段的电平
//seg_data_en[0]?8'hfe:8'hff 是根据数据显示使能信号 控制SEG1数码管的位选引脚的电平
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'hbf: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'hdf: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'hef: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'hf7: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'hfb: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'hfd: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'hfe:8'hff};
end
default: state <= IDLE;
endcase
end
WRITE:begin
if(clk_div) begin
if(cnt_write >= 8'd33) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
8'd0: begin sclk_out <= LOW; sdio_out <= data_reg[15]; end //SCK下降沿时SER更新数据
8'd1: begin sclk_out <= HIGH; end //SCK上升沿时SER数据稳定
8'd2: begin sclk_out <= LOW; sdio_out <= data_reg[14]; end
8'd3: begin sclk_out <= HIGH; end
8'd4: begin sclk_out <= LOW; sdio_out <= data_reg[13]; end
8'd5: begin sclk_out <= HIGH; end
8'd6: begin sclk_out <= LOW; sdio_out <= data_reg[12]; end
8'd7: begin sclk_out <= HIGH; end
8'd8: begin sclk_out <= LOW; sdio_out <= data_reg[11]; end
8'd9: begin sclk_out <= HIGH; end
8'd10: begin sclk_out <= LOW; sdio_out <= data_reg[10]; end
8'd11: begin sclk_out <= HIGH; end
8'd12: begin sclk_out <= LOW; sdio_out <= data_reg[9]; end
8'd13: begin sclk_out <= HIGH; end
8'd14: begin sclk_out <= LOW; sdio_out <= data_reg[8]; end
8'd15: begin sclk_out <= HIGH; end
8'd16: begin sclk_out <= LOW; sdio_out <= data_reg[7]; end
8'd17: begin sclk_out <= HIGH; end
8'd18: begin sclk_out <= LOW; sdio_out <= data_reg[6]; end
8'd19: begin sclk_out <= HIGH; end
8'd20: begin sclk_out <= LOW; sdio_out <= data_reg[5]; end
8'd21: begin sclk_out <= HIGH; end
8'd22: begin sclk_out <= LOW; sdio_out <= data_reg[4]; end
8'd23: begin sclk_out <= HIGH; end
8'd24: begin sclk_out <= LOW; sdio_out <= data_reg[3]; end
8'd25: begin sclk_out <= HIGH; end
8'd26: begin sclk_out <= LOW; sdio_out <= data_reg[2]; end
8'd27: begin sclk_out <= HIGH; end
8'd28: begin sclk_out <= LOW; sdio_out <= data_reg[1]; end
8'd29: begin sclk_out <= HIGH; end
8'd30: begin sclk_out <= LOW; sdio_out <= data_reg[0]; end
8'd31: begin sclk_out <= HIGH; end
8'd32: begin rclk_out <= HIGH; end //当16位数据传送完成后RCK拉高,输出生效
8'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: state <= IDLE;
endcase
end
end
endmodule

分析一下这部分代码,大致可以分为时钟分频、数据准备、状态机控制以及数据通过74HC595发送到数码管几个部分:

代码逻辑主要包括几个部分:时钟分频、数据准备、状态机控制以及数据通过74HC595发送到数码管。下面是逐部分的分析:

时钟分频

  • 参数定义:`CLK_DIV_PERIOD`设置为600,用作时钟分频系数。
  • 计数器(cnt):用于对系统时钟(`clk_in`)的脉冲计数,当计数达到`CLK_DIV_PERIOD-1`时重置为0,实现分频效果。
  • 分频脉冲信号(clk_div):利用上述计数器产生的分频脉冲信号,用于控制74HC595的数据传输时序。

数据准备

  • 数码管字库(seg):定义了一个数组,包含0~F十六进制值对应的数码管显示编码,用于将输入的4位二进制数转换为数码管上显示的数字或字母。
  • 输入信号:接收8个数码管的显示数据(`seg_data_1`到`seg_data_8`)、8位数据显示使能(`seg_data_en`)和8位小数点显示使能(`seg_dot_en`)。

状态机控制

状态机有三个状态:IDLE、MAIN、WRITE,用于控制数据的准备和通过74HC595发送到数码管的流程。

  • IDLE:初始状态,准备开始数据传输。
  • MAIN:在此状态下,根据`cnt_main`的值选择当前要显示的数码管数据,准备对应的数据发送给74HC595。
  • WRITE:在此状态下,将准备好的数据通过序列接口发送给74HC595。使用`cnt_write`控制数据位的发送,包括段选择和位选择。

数据通过74HC595发送到数码管

通过控制`sdio_out`(数据线)、`sclk_out`(时钟线)和`rclk_out`(锁存器更新信号)实现数据的串行输入和输出更新。数据发送利用状态机中的WRITE状态,通过分频脉冲信号`clk_div`同步,确保数据稳定地发送到74HC595,再通过74HC595输出到数码管显示。

简单例化一下,就可以让几个数码管显示数字,比如后面需要用到的全都初始化为0



仿真波形:



矩阵键盘模块

官方同样给出了一个教学性的例程:https://www.stepfpga.com/doc/%E7%9F%A9%E9%98%B5%E6%8C%89%E9%94%AE%E6%A8%A1%E5%9D%97


不过我在实践后发现,这个例程里没有做按键消抖动,结合我自己项目的实现逻辑与需求,我基于此进行修改,加入了按键消抖功能,最终代码如下:


module Array_KeyBoard #
(
parameter NUM_FOR_200HZ = 60000
)
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
input [3:0] col, //矩阵按键列接口
output reg [3:0] row, //矩阵按键行接口
output reg [7:0] seg_data,
output [15:0] key_pulse
);
localparam STATE0 = 2'b00;
localparam STATE1 = 2'b01;
localparam STATE2 = 2'b10;
localparam STATE3 = 2'b11;

//计数器计数分频实现5ms周期信号clk_200hz
reg [15:0] cnt;
reg clk_200hz;
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin //复位时计数器cnt清零,clk_200hz信号起始电平为低电平
cnt <= 16'd0;
clk_200hz <= 1'b0;
end else begin
if(cnt >= ((NUM_FOR_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_in) begin
if(!rst_n_in) begin
c_state <= STATE0;
row <= 4'b1110;
end else begin
case(c_state)
STATE0: begin c_state <= STATE1; row <= 4'b1101; end //状态c_state跳转及对应状态下矩阵按键的row输出
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;
reg [15:0] key_r;
reg [15:0] key_out;
//因为每个状态中单行有效,通过对列接口的电平状态采样得到对应4个按键的状态,依次循环
always@(negedge clk_200hz or negedge rst_n_in) begin
if(!rst_n_in) begin
key_out <= 16'hffff;
end else begin
case(c_state)
STATE0:
begin
key[3:0] <= col; //矩阵键盘采样
key_r[3:0] <= key[3:0]; //键盘数据锁存
key_out[3:0] <= key_r[3:0]|key[3:0]; //连续两次采样判定
end
STATE1:
begin
key[7:4] <= col; //矩阵键盘采样
key_r[7:4] <= key[7:4]; //键盘数据锁存
key_out[7:4] <= key_r[7:4]|key[7:4]; //连续两次采样判定
end
STATE2:
begin
key[11:8] <= col; //矩阵键盘采样
key_r[11:8] <= key[11:8]; //键盘数据锁存
key_out[11:8] <= key_r[11:8]|key[11:8]; //连续两次采样判定
end
STATE3:
begin
key[15:12] <= col; //矩阵键盘采样
key_r[15:12] <= key[15:12]; //键盘数据锁存
key_out[15:12] <= key_r[15:12]|key[15:12]; //连续两次采样判定
end
default:key_out <= 16'hffff;
endcase
end
end

reg [15:0] key_out_r;
//Register low_sw_r, lock low_sw to next clk
always @ ( posedge clk_in or negedge rst_n_in )
if (!rst_n_in) 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); //通过前后两个时刻的值判断

//key_pulse transfer to seg_data
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
seg_data <= 8'h00;
end else begin
case(key_pulse) //key_pulse脉宽等于clk_in的周期
16'h0001: seg_data <= 8'h07; //编码
16'h0002: seg_data <= 8'h08;
16'h0004: seg_data <= 8'h09;
16'h0008: seg_data <= 8'h10; //%
16'h0010: seg_data <= 8'h04;
16'h0020: seg_data <= 8'h05;
16'h0040: seg_data <= 8'h06;
16'h0080: seg_data <= 8'h11; //x
16'h0100: seg_data <= 8'h03;
16'h0200: seg_data <= 8'h02;
16'h0400: seg_data <= 8'h01;
16'h0800: seg_data <= 8'h12;//-
16'h1000: seg_data <= 8'h00;
16'h2000: seg_data <= 8'h14;//.
16'h4000: seg_data <= 8'h15;//=
16'h8000: seg_data <= 8'h13;//+
default: seg_data <= 8'h00; //无按键按下时保持
endcase
end
end
endmoudle​

其中代码的实现逻辑分析如下:


参数和信号定义

- `NUM_FOR_200HZ`:计数器上限,用于生成200Hz的信号,控制键盘扫描频率。这个参数可以在实例化模块时修改,以适应不同的系统时钟频率。

- 输入信号:

- `clk_in`:系统时钟。

- `rst_n_in`:低电平有效的复位信号。

- `col`:4位宽的输入信号,表示键盘的4列。

- 输出信号:

- `row`:4位宽的输出信号,用于控制键盘的4行。

- `seg_data`:8位宽的输出信号,用于数码管显示。

- `key_pulse`:16位宽的输出信号,表示按键动作的脉冲信号。

主要功能模块


  1. 时钟分频器:利用`cnt`计数器和系统时钟`clk_in`生成一个频率为200Hz的时钟信号`clk_200hz`,用于控制键盘扫描的速度。
  2. 状态机:状态机有四个状态(STATE0到STATE3),分别对应键盘扫描的四行。状态机以`clk_200hz`为触发信号,循环通过这四个状态,每个状态对应于键盘的一行被激活,同时读取对应列的状态。
  3. 键盘扫描与消抖:在每个状态下,读取列线`col`的状态,映射到对应的4个按键上。为了消除按键的抖动,使用两次采样确认按键状态的技术。即,在两个连续的`clk_200hz`下降沿采样相同值时,确认按键动作。这一过程分别在STATE0到STATE3状态中进行,覆盖了4x4键盘的所有按键。
  4. 按键脉冲生成:使用寄存器`key_out_r`锁存上一次的按键状态,并与当前状态`key_out`比较,通过边缘检测生成按键动作的脉冲信号`key_pulse`。
  5. 数码管编码:最后,根据`key_pulse`的值,选择相应的数码管显示编码`seg_data`。这里使用了一个case语句,将每个按键映射到一个特定的数码管显示值上。

简单测试两个键按下,仿真波形图:



除法器

直接在代码里使用除法或者取余会出现问题,因此我参考了网上几个大佬的设计,简单按照自己的项目需求进行修改,同时调整下位宽等参数,最后代码如下:

module div #(
parameter L_DIVN = 8 ,//被除数的位宽;
parameter L_DIVR = 4 //除数的位宽;
)(
input clk ,//时钟信号;
input rst_n ,//复位信号,低电平有效;

input start ,
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。

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;

//状态机次态到现态的转换;
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

//对被除数进行移位或进行减法运算。
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

通过一个状态机控制除法过程,包括初始化、除数左移(以匹配被除数的位宽)、和除法运算三个主要状态。在除法运算中,如果被除数大于或等于除数,就执行减法并将结果记录为新的被除数,同时商加1;如果不足,被除数左移准备下一轮比较。除法完成后,输出商和余数。

仿真波形图:



主控程序与总结

先放代码:

module TopModule(
input clk, // 系统时钟
input rst_n, // 系统复位,低有效
input [3:0] col, // 矩阵键盘的列线
output [3:0] row, // 矩阵键盘的行线
output rclk_out, // 74HC595的RCK管脚
output sclk_out, // 74HC595的SCK管脚
output sdio_out // 74HC595的SER管脚
);

// 数码管显示使能和小数点显示使能
wire [7:0] seg_data_en = 8'b11111111; // 激活所有8个数码管的显示
wire [7:0] seg_dot_en = 8'b00000000; // 所有小数点均不显示

// 矩阵键盘扫描结果
//wire [15:0] key_out;
wire [7:0] key_data;
wire [15:0] key_pulse;


// 数码管要显示的数据
reg [3:0] seg_data[7:0];
// 除法器要用的数据
reg start ;//开始计算信号,高电平有效,
reg [15 : 0] dividend ;//被除数输入;
reg [15 : 0] divisor ;//除数输入;

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

reg quotient_error ;
reg rem_error ;

// 输入的数字和运算结果
reg [15:0] num1 = 0, num2 = 0;
reg [7:0] d_num1 = 0, d_num2 = 0, d_num3 = 0,d_num4 = 0;
reg [15:0] qianwei,baiwei, shiwei, gewei; // 分别用于存储千位、百位、十位和个位数
reg calculate_done = 0; // 用于指示乘法及除法计算是否完成
reg [15:0] sum; // 用于存储乘法的结果


reg [3:0] digit_count = 0; // 用于跟踪当前输入的是第一个还是第二个数字

// 状态机状态定义
localparam INPUT_NUM1 = 1, INPUT_NUM2 = 2, INPUT_NUM3 = 3, INPUT_NUM4 = 4,CALCULATE = 5,MODE=6,READY_DIV=8,WAIT_FOR_DIV=7;
localparam STATE_IDLE = 0, STATE_CALC_MULTIPLY = 1, STATE_CALC_DIVIDE_HUNDRED = 2, STATE_CALC_DIVIDE_TEN = 3, STATE_CALC_DONE = 4;
localparam JI=0,SUAN=1;
reg [2:0] mod=0;
reg [2:0] state = INPUT_NUM1;
reg [2:0] state_div = INPUT_NUM1;
reg [2:0] state_cc = STATE_IDLE;
reg [2:0] jisuan = JI;
reg [2:0] temp_r =0,temp_t=0;
// 实例化除法器
div #(
.L_DIVN ( 16 ),//被除数的位宽;
.L_DIVR ( 16 ) //除数的位宽;
)
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 ) //商和余数输出有效指示信号,高电平有效;
);

// 实例化矩阵键盘模块
Array_KeyBoard #(
.NUM_FOR_200HZ(60000)
) keyboard_inst (
.clk_in(clk),
.rst_n_in(rst_n),
.col(col),
.row(row),
.seg_data(key_data),
.key_pulse(key_pulse)
);

// 实例化数码管显示模块
Segment_scan_8 segment_scan_inst(
.clk_in(clk),
.rst_n_in(rst_n),
.seg_data_1(seg_data[0]),
.seg_data_2(seg_data[1]),
.seg_data_3(seg_data[2]),
.seg_data_4(seg_data[3]),
.seg_data_5(seg_data[4]),
.seg_data_6(seg_data[5]),
.seg_data_7(seg_data[6]),
.seg_data_8(seg_data[7]),
.seg_data_en(seg_data_en),
.seg_dot_en(seg_dot_en),
.rclk_out(rclk_out),
.sclk_out(sclk_out),
.sdio_out(sdio_out)
);

// 数字-按键映射,假设数字0-9映射到按键0-9
// 注意:这里的映射需要根据实际情况调整
reg [3:0] key_to_num[0:13]; // 映射数组


always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
seg_data[0] <= 0;
seg_data[1] <= 0;
seg_data[2] <= 0;
seg_data[3] <= 0;
seg_data[4] <= 0;
seg_data[5] <= 0;
seg_data[6] <= 0;
seg_data[7] <= 0;
num1 <= 0;
num2 <= 0;
sum <= 0;
digit_count <= 0;
state <= INPUT_NUM1;
start<=0;
quotient_error <= 1'b0;
rem_error <= 1'b0;
state_div <= INPUT_NUM1;
state_cc <= STATE_IDLE;
calculate_done <= 0;



end else begin
// 根据当前状态处理键盘输入和计算
case (state)
INPUT_NUM1:
begin
if(key_data)
begin
seg_data[0] <= 0;
seg_data[1] <= 0;
seg_data[2] <= 0;
seg_data[3] <= 0;
seg_data[4] <= 0;
seg_data[5] <= 0;
seg_data[6] <= 0;
seg_data[7] <= 0;
seg_data[6]<=key_data;
d_num1 <= key_data; //第一个十位数
if(key_data==8'h20)begin
d_num1 <= 0;
end
state <= INPUT_NUM2;
end
end
INPUT_NUM2:
begin
if(key_data)
begin
seg_data[7]<=d_num1;
seg_data[6]<=key_data;
d_num2 <= key_data; //第一个的个位数
if(key_data==8'h20)begin
d_num2 <= 0;
end
state <= MODE;
end
end

MODE:
begin
if(key_data)
begin
if(key_data==8'h13) //加
begin mod<=1;state <= INPUT_NUM3;end
if(key_data==8'h12) //减
begin mod<=2;state <= INPUT_NUM3;end
if(key_data==8'h11) //乘
begin mod<=3;state <= INPUT_NUM3;end
if(key_data==8'h10) //除
begin mod<=4;state <= INPUT_NUM3;end
end
end

INPUT_NUM3:
begin
if(key_data)
begin
seg_data[4]<=key_data;
d_num3 <= key_data; //第2个十位数
if(key_data==8'h20)begin
d_num3 <= 0;
end
state <= INPUT_NUM4;
end
end
INPUT_NUM4:
begin
case(jisuan)
JI:
if(key_data)
begin
seg_data[5]<=d_num3;
seg_data[4]<=key_data;
d_num4 <= key_data; //第2个的个位数
if(key_data==8'h20)begin
d_num4 <= 0;
end
jisuan <= SUAN;
end
SUAN:
if(key_data==8'h15) //"="
begin
state <= CALCULATE;
jisuan<= JI;
end
endcase
end

CALCULATE:
begin
if(mod==1) //加
begin
num1 = d_num1+d_num3;
num2 = d_num2+d_num4;
if(num2>10 | num2==10)
begin
seg_data[0]<=num2-10;
seg_data[1]<=num1+1;
if(num1+1>=10)
begin
seg_data[2]<=1;
seg_data[1]<=num1-9;
end
end
if(num2<10)
begin
seg_data[0]<=num2;
seg_data[1]<=num1;
if(num1>=10)
begin
seg_data[2]<=1;
seg_data[1]<=num1-10;
end
end

state <= INPUT_NUM1;
end
if(mod==2) //减
begin
seg_data[1] <= d_num1-d_num3;
seg_data[0] <= d_num2-d_num4;
if(d_num2<d_num4)
begin
seg_data[0] <= 10+d_num2-d_num4;
seg_data[1] <= d_num1-d_num3-1;
end
state <= INPUT_NUM1;
end

if(mod==3) //乘
begin
num1 = d_num1*10+d_num2;
num2 = d_num3*10+d_num4;
sum = num1*num2;
case (state_cc)
STATE_IDLE:
begin
if (sum)
begin
num1 = d_num1*10 + d_num2;
num2 = d_num3*10 + d_num4;
sum = num1 * num2;
state_cc <= STATE_CALC_MULTIPLY;
end
end
STATE_CALC_MULTIPLY: begin
// 确保乘法结果已准备好
if (ready == 1 && !calculate_done) begin
dividend <= sum; // 设置被除数为乘法结果
divisor <= 1000; // 设置除数为1000,用于计算千位
start <= 1;
calculate_done <= 1; // 标记开始除法计算
end else if (quotient_vld == 1) begin
qianwei <= quotient; // 保存百位结果
start <= 0;
state_cc <= STATE_CALC_DIVIDE_HUNDRED; // 转移到计算十位的状态
calculate_done <= 0; // 重置计算完成标志
end
//state_cc <= STATE_CALC_DIVIDE_HUNDRED;
end
STATE_CALC_DIVIDE_HUNDRED: begin
if (ready == 1 && !calculate_done) begin
dividend <= sum-1000*qianwei; // 设置被除数为乘法结果
divisor <= 100; // 设置除数为100,用于计算百位
start <= 1;
calculate_done <= 1; // 标记开始除法计算
end else if (quotient_vld == 1) begin
baiwei <= quotient; // 保存百位结果
start <= 0;
state_cc <= STATE_CALC_DIVIDE_TEN; // 转移到计算十位的状态
calculate_done <= 0; // 重置计算完成标志
end
end
STATE_CALC_DIVIDE_TEN: begin
if (ready == 1 && !calculate_done) begin
dividend <= sum - 100*baiwei-1000*qianwei ; // 仅用乘法结果的后两位进行除法
divisor <= 10; // 设置除数为10,用于计算十位
start <= 1;
calculate_done <= 1; // 标记开始除法计算
end else if (quotient_vld == 1) begin
shiwei <= quotient; // 保存十位结果
gewei <= remainder; // 同时得到个位结果
start <= 0;
state_cc <= STATE_CALC_DONE; // 转移到计算完成的状态
calculate_done <= 0; // 重置计算完成标志
end
end
STATE_CALC_DONE: begin
// 显示baiwei, shiwei, gewei的值...
seg_data[3] <= qianwei;
seg_data[2] <= baiwei;
seg_data[1] <= shiwei;
seg_data[0] <= gewei;
if (1) begin
sum<=0;
state_cc <= STATE_IDLE;
state <= INPUT_NUM1; // 重置状态机等待下一次计算
end
end
endcase
end

if(mod==4) //除
begin
num1 = d_num1*10+d_num2;
num2 = d_num3*10+d_num4;
temp_r=1;
//seg_data[5] <= 5;
if(temp_r)begin
case(state_div)
INPUT_NUM1:
begin
if(temp_r)begin
//seg_data[5] <= 6;
if (ready == 1)
begin
//seg_data[5] <= 7;
dividend <= num1; // 设置被除数
divisor <= num2; // 设置除数
start <= 1; // 启动除法运算
state_div <= WAIT_FOR_DIV; // 转移到等待除法完成的状态
end
end
end
WAIT_FOR_DIV:
begin
start <= 0; // 清除启动信号
if (quotient_vld == 1)
begin // 检查除法是否完成
seg_data[0] <= quotient; // 更新显示数据
//seg_data[1] <= remainder;
state_div <= INPUT_NUM1;
//seg_data[5] <= 8;
temp_r=0;
state <= INPUT_NUM1; // 保持在计算状态直到新的输入
end
end
endcase
end
end


//end
end
endcase
end
end



endmodule

主要就是对上述几个模块进行例化,然后通过状态机进行切换,最终实现功能。其中比较复杂或者有趣的点在于计算的处理,如数据从键盘读入后,如何存储(比如一个变量存十位,一个变量存个位),如何在数码管进行显示(不同码制的转换),如何把读入的数据进行加减乘除运算得到正确的结果,并且合理地处理其中涉及的溢出等情况,这些都需要不断实验,一点点调试。


当然了,其中我遇到的最多的问题,一个是时序处理,特别是不同模块间的时序特别按键的时序,有时候就会遇到输入完数字没有显示计算结果,要到下一次输入数字才会显示上一组运算的结果。然后遇到的最搞心态的问题就是变量的位宽以及赋值方法,阻塞or 非阻塞,毕竟作为一个fpga初学者,之前只接触过单片机、单板计算机等的硬件,对这些还是不太熟悉的,特别是要自己设置的时候,经常茫茫然而不知所措。当然了,一点点调试,一点点研究逻辑,总会找到bug的,初学者就是要在不断的试错中学习进步。


最后感谢活动的官方,提供了这么一个宝贵的机会可以学习初步入门FPGA,了解一点点皮毛,我也会继续加油学习的!

附件下载
chufa.v
shuma.v
calculate.zip
calculate_impl1.jed
keyboard.v
top.v
团队介绍
个人开发者
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号