1. 项目需求
实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制。
基本要求:
运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。
扩展要求:
使用TFTLCD来做显示,自行设计显示界面,代替数码管显示输入的参数、运算符以及计算后的结果。
2. 需求分析
要实现加减乘除计算功能,难点在于乘除的计算。
显示:
- 输出需要显示到数码管或者LCD上。这里先以数码管为例。
功能模块有:
- 计算,特别是除法
- 数码管显示
- 键盘的消抖和映射
状态跳转设计
键盘映射设计
1 2 3 +
4 5 6 -
7 8 9 *
0 C = /
3. 实现过程
1)数码管显示测试
参考这个 数码管模块
数据和位选组成16位,依次写入。
小脚丫底板中数码管的原理图:
其中,位的选择通过 DIG1~DIG8置位来选择:0xfe、0xfd……
程序代码:
// led 数码管显示
module led_segment
(
input clk, // 系统时钟
input rst_n, // 系统复位
input [4:0] seg_data_1, // seg1 数码管要显示的数据
input [4:0] seg_data_2, // seg2
input [4:0] seg_data_3, // seg3
input [4:0] seg_data_4, // seg4
input [4:0] seg_data_5, // seg5
input [4:0] seg_data_6, // seg6
input [4:0] seg_data_7, // seg7
input [4:0] seg_data_8, // seg8
input [7:0] seg_data_en, // 数码管数据使能[msb seg8 ~ lsb seg1]
input [7:0] seg_dot_en, // 数码管小数点使能
output reg rclk_out, // 74HC595 的 RCK 管脚
output reg sclk_out, // 74HC595 的 SCK 管脚
output reg sdio_out // 74HC595 的 SER 管脚
);
parameter CLK_DIV_PERIOD = 600; // 分频系数
// 状态机状态
localparam IDLE = 2'd0; // 空闲状态
localparam MAIN = 2'd1; // 主要工作状态,扫描刷新数码管
localparam WRITE= 2'd2; // 写状态
localparam LOW = 1'b0;
localparam HIGH = 1'b1;
// 数码管字库
reg [7:0] seg [18:0]; // seg 每个里面有8位
// 初始化
initial begin
seg[0] = 8'h3f; // 0
seg[1] = 8'h06; // 1
seg[2] = 8'h5b; // 2
seg[3] = 8'h4f; // 3
seg[4] = 8'h66; // 4
seg[5] = 8'h6d; // 5
seg[6] = 8'h7d; // 6
seg[7] = 8'h07; // 7
seg[8] = 8'h7f; // 8
seg[9] = 8'h6f; // 9
seg[10] = 8'h46; // + 的左半部分
seg[11] = 8'h70; // + 的右半部分
seg[12] = 8'h40; // -
seg[13] = 8'h76; // x
seg[14] = 8'h49; // /
seg[15] = 8'h79; // E
seg[16] = 8'h00; // 空
seg[17] = 8'h73; // R (显示是p)
seg[18] = 8'h48; // =
end
// 计数器对系统时钟进行计数
reg [9:0] cnt = 0;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 1'b0;
else begin
if(cnt >= (CLK_DIV_PERIOD-1))
cnt <= 1'b0;
else
cnt <= cnt + 1'b1;
end
end
// 根据分频计数产生分频脉冲信号
reg clk_div;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
clk_div <= 1'b0;
else
clk_div <= (cnt == (CLK_DIV_PERIOD-1)) ? 1'b1 : 1'b0;
end
// 使用状态机完成 数码管扫描和74HC595时序
reg [15:0] data_reg;
reg [2:0] cnt_main;
reg [5:0] cnt_write;
reg [1:0] state = IDLE;
always @(posedge clk or negedge rst_n) begin
// 复位状态,各寄存器赋初值
if(!rst_n) begin
state <= IDLE;
cnt_main <= 3'd0;
cnt_write <= 6'd0;
sdio_out <= 1'b0;
sclk_out <= LOW;
rclk_out <= LOW;
end else begin
case(state)
IDLE:begin
// 空闲状态,第一个状态,相当于软复位
state <= MAIN;
cnt_main <= 3'd0;
cnt_write <= 6'd0;
sdio_out <= 1'b0;
sclk_out <= LOW;
rclk_out <= LOW;
end
MAIN:begin
// cnt_main 累加
if(cnt_main >= 3'd7) cnt_main <= 3'b0;
else cnt_main <= cnt_main + 1'b1;
// 对8位数码管进行逐位扫描
case(cnt_main)
3'd0:begin
state <= WRITE; // 配置完后跳转至WRITE状态
// data_reg[15:8] 是段选(单个数码管显示的内容),data_reg[7:0] 是位选(选择哪一位数码管)
data_reg <= {
seg[seg_data_1] | (seg_dot_en[0]?8'h80:8'h00), // 段选中的seg_dot_en是小数点显示使能
seg_data_en[0] ? 8'hfe : 8'hff
};
end
3'd1:begin
state <= WRITE;
data_reg <= {
seg[seg_data_2] | (seg_dot_en[1]?8'h80:8'h00),
seg_data_en[1] ? 8'hfd : 8'hff
};
end
3'd2:begin
state <= WRITE;
data_reg <= {
seg[seg_data_3] | (seg_dot_en[2]?8'h80:8'h00),
seg_data_en[2] ? 8'hfb : 8'hff
};
end
3'd3:begin
state <= WRITE;
data_reg <= {
seg[seg_data_4] | (seg_dot_en[3]?8'h80:8'h00),
seg_data_en[3] ? 8'hf7 : 8'hff
};
end
3'd4:begin
state <= WRITE;
data_reg <= {
seg[seg_data_5] | (seg_dot_en[4]?8'h80:8'h00),
seg_data_en[4] ? 8'hef : 8'hff
};
end
3'd5:begin
state <= WRITE;
data_reg <= {
seg[seg_data_6] | (seg_dot_en[5]?8'h80:8'h00),
seg_data_en[5] ? 8'hdf : 8'hff
};
end
3'd6:begin
state <= WRITE;
data_reg <= {
seg[seg_data_7] | (seg_dot_en[6]?8'h80:8'h00),
seg_data_en[6] ? 8'hbf : 8'hff
};
end
3'd7:begin
state <= WRITE;
data_reg <= {
seg[seg_data_8] | (seg_dot_en[7]?8'h80:8'h00),
seg_data_en[7] ? 8'h7f : 8'hff
};
end
default: state <= IDLE;
endcase
end
WRITE:begin
if(clk_div) begin // 按照分频后的速度进行串行更新
if(cnt_write >= 6'd33) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
// 74HC595时序实现
6'd0: begin sclk_out <= LOW; sdio_out <= data_reg[15]; end
6'd1: begin sclk_out <= HIGH; end
6'd2: begin sclk_out <= LOW; sdio_out <= data_reg[14]; end
6'd3: begin sclk_out <= HIGH; end
6'd4: begin sclk_out <= LOW; sdio_out <= data_reg[13]; end
6'd5: begin sclk_out <= HIGH; end
6'd6: begin sclk_out <= LOW; sdio_out <= data_reg[12]; end
6'd7: begin sclk_out <= HIGH; end
6'd8: begin sclk_out <= LOW; sdio_out <= data_reg[11]; end
6'd9: begin sclk_out <= HIGH; end
6'd10: begin sclk_out <= LOW; sdio_out <= data_reg[10]; end
6'd11: begin sclk_out <= HIGH; end
6'd12: begin sclk_out <= LOW; sdio_out <= data_reg[9]; end
6'd13: begin sclk_out <= HIGH; end
6'd14: begin sclk_out <= LOW; sdio_out <= data_reg[8]; end
6'd15: begin sclk_out <= HIGH; end
6'd16: begin sclk_out <= LOW; sdio_out <= data_reg[7]; end
6'd17: begin sclk_out <= HIGH; end
6'd18: begin sclk_out <= LOW; sdio_out <= data_reg[6]; end
6'd19: begin sclk_out <= HIGH; end
6'd20: begin sclk_out <= LOW; sdio_out <= data_reg[5]; end
6'd21: begin sclk_out <= HIGH; end
6'd22: begin sclk_out <= LOW; sdio_out <= data_reg[4]; end
6'd23: begin sclk_out <= HIGH; end
6'd24: begin sclk_out <= LOW; sdio_out <= data_reg[3]; end
6'd25: begin sclk_out <= HIGH; end
6'd26: begin sclk_out <= LOW; sdio_out <= data_reg[2]; end
6'd27: begin sclk_out <= HIGH; end
6'd28: begin sclk_out <= LOW; sdio_out <= data_reg[1]; end
6'd29: begin sclk_out <= HIGH; end
6'd30: begin sclk_out <= LOW; sdio_out <= data_reg[0]; end
6'd31: begin sclk_out <= HIGH; end
6'd32: begin rclk_out <= HIGH; end
6'd33: begin rclk_out <= LOW; state <= MAIN; end
default: state <= IDLE;
endcase
end else begin
sclk_out <= sclk_out;
sdio_out <= sdio_out;
rclk_out <= rclk_out;
cnt_write <= cnt_write;
state <= state;
end
end
default:begin
state <= IDLE;
end
endcase
end
end
endmodule
仿真:
`timescale 1ns/1ns
module segment_tb;
reg clk, rst_n;
reg [3:0] seg_data_1; // seg1 数码管要显示的数据
reg [3:0] seg_data_2; // seg2
reg [3:0] seg_data_3; // seg3
reg [3:0] seg_data_4; // seg4
reg [3:0] seg_data_5; // seg5
reg [3:0] seg_data_6; // seg6
reg [3:0] seg_data_7; // seg7
reg [3:0] seg_data_8; // seg8
reg [7:0] seg_data_en; // 数码管数据使能[msb seg8 ~ lsb seg1]
reg [7:0] seg_dot_en; // 数码管小数点使能
wire rclk_out; // 74HC595 的 RCK 管脚
wire sclk_out; // 74HC595 的 SCK 管脚
wire sdio_out; // 74HC595 的 SER 管脚
initial begin
clk = 0;
rst_n = 0;
#20
rst_n = 1;
seg_data_1 = 4'd1;
seg_data_2 = 4'd2;
seg_data_3 = 4'd3;
seg_data_4 = 4'd4;
seg_data_5 = 4'd5;
seg_data_6 = 4'd7;
seg_data_7 = 4'd8;
seg_data_8 = 4'd0;
seg_data_en = 8'hfd; // 第二位数码管不使能
seg_dot_en = 8'h00;
end
always #10 clk = ~clk;
led_segment led_segment_inst(
.clk(clk),
.rst_n(rst_n),
.seg_data_1(seg_data_1),
.seg_data_2(seg_data_2),
.seg_data_3(seg_data_3),
.seg_data_4(seg_data_4),
.seg_data_5(seg_data_5),
.seg_data_6(seg_data_6),
.seg_data_7(seg_data_7),
.seg_data_8(seg_data_8),
.seg_data_en(seg_data_en),
.seg_dot_en(seg_dot_en),
.rclk_out(rclk_out),
.sclk_out(sclk_out),
.sdio_out(sdio_out)
);
endmodule
仿真波形
可观察到:
- clk_div 分频正常;
- 第一位数码管显示数字1(0x06),其他位数字类似;
- 第一位数码管使能,为0xfe,其他类似,只有第二位数码管不使能,因为传递的seg_data_en是0xfd
在板子上测试:
绑定管脚时候发现不能绑定,显示unconnected 不能绑定
lattice防止信号被优化(管脚分配时定义的引脚被分配到unconnected无关联引脚上导致无法配置) 中看到,加上 /* synthesis syn_force_pads = 1 */
防止被优化。
原因大概也是因为没有输出所致,最后有输出了也正常了。
顶层代码如下:
module top2(
input clk,
input rst_n,
output rclk_out, // 74HC595 的 RCK 管脚
output sclk_out, // 74HC595 的 SCK 管脚
output sdio_out // 74HC595 的 SER 管脚
);
/* synthesis syn_force_pads = 1 */
wire [3:0] seg_data_1; // seg1 数码管要显示的数据
wire [3:0] seg_data_2; // seg2
wire [3:0] seg_data_3; // seg3
wire [3:0] seg_data_4; // seg4
wire [3:0] seg_data_5; // seg5
wire [3:0] seg_data_6; // seg6
wire [3:0] seg_data_7; // seg7
wire [3:0] seg_data_8; // seg8
wire [7:0] seg_data_en; // 数码管数据使能[msb seg8 ~ lsb seg1]
wire [7:0] seg_dot_en; // 数码管小数点使能
assign seg_data_1 = 4'd1;
assign seg_data_2 = 4'd2;
assign seg_data_3 = 4'd3;
assign seg_data_4 = 4'd4;
assign seg_data_5 = 4'd5;
assign seg_data_6 = 4'd7;
assign seg_data_7 = 4'd8;
assign seg_data_8 = 4'd0;
assign seg_data_en = 8'hfd;
assign seg_dot_en = 8'h00;
led_segment inst(
.clk(clk),
.rst_n(rst_n),
.seg_data_1(seg_data_1),
.seg_data_2(seg_data_2),
.seg_data_3(seg_data_3),
.seg_data_4(seg_data_4),
.seg_data_5(seg_data_5),
.seg_data_6(seg_data_6),
.seg_data_7(seg_data_7),
.seg_data_8(seg_data_8),
.seg_data_en(seg_data_en),
.seg_dot_en(seg_dot_en),
.rclk_out(rclk_out),
.sclk_out(sclk_out),
.sdio_out(sdio_out)
);
endmodule
实际效果,
疑问项:为了数码管的时序,使用了600分频,暂不清楚原因和时间要求。
2)按键消抖
先试一下单个按键消抖
// 按键功能,消抖
/*
* led,0亮1灭
* key1,按下为0
*/
module key(
input clk,
input rst_n,
input key1,
output reg led0
);
// 系统时钟 50M,一个时钟周期是20ns
// 消抖时间 20ms = 20_000_000 / 20 = 1_000_000周期
// 系统时钟 12M,一个时钟周期是83.333333ns
// 消抖时间 20ms = 20_000_000/83.33333=240000周期,也可用 12M/50(hz)算=240000
parameter CNT_MAX = 20'd240000;
//parameter CNT_MAX = 20'd200;
reg [19:0] cnt;
reg key_d0; // 将按键信号延迟一个时钟周期
reg key_d1; // 将按键信号延迟两个时钟周期
reg key_flag; // 按键状态
// 延迟1,2周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_d0 <= 1'b1;
key_d1 <= 1'b1;
end
else begin
key_d0 <= key1;
key_d1 <= key_d0;
end
end
// 按键值消抖
/* 累加型的,会有问题,在保持一样时还会一直计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= 20'd0;
else begin
// 按键和前一个时刻一样,则计数器开始计数
cnt <= cnt + 1'b1;
end
end
*/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= CNT_MAX;
else //按键和前一个时刻一样,则计数器开始递减到 0
cnt <= (cnt>20'd0) ? (cnt-1'b1) : 20'd0;
end
reg key_flag2; // 按键状态变化,key_flag2 比 key_flag晚一个周期
// 消抖后的值
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_flag <= 1'b0;
key_flag2 <= 1'b0;
end
else if(cnt == 20'd1) begin
key_flag <= key_d1;
key_flag2 <= key_flag;
end
else begin
key_flag <= key_flag;
key_flag2 <= key_flag;
end
end
// 按下时点亮LED
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
led0 <= 1'b1;
else if(key_flag!=key_flag2) begin
if(key_flag==1'b0) // 下降沿,按下
led0 <= ~led0;
end
end
endmodule
仿真:
`timescale 1ns/1ns
module key_tb();
parameter CLK_PERIOD = 5'd20; // 时钟周期 20ns
parameter CNT_MAX = 20'd10; // 消抖时间 10 个周期,200ns
reg clk;
reg rst_n;
reg key;
wire led0;
initial begin
clk <= 1'b0;
rst_n <= 1'b0;
key <= 1'b1;
#200
rst_n <= 1'b1;
// key 信号变化
#20
key <= 1'b1;
#20
key <= 1'b0;
#50
key <= 1'b1;
#40
key <= 1'b0;
#20
key <= 1'b1;
#300
key <= 1'b0;
#50
key <= 1'b1;
#40
key <= 1'b0;
#300
key <= 1'b1;
#30
key <= 1'b0;
#300
key <= 1'b1;
#300
key <= 1'b0;
#40
key <= 1'b1;
#30
key <= 1'b0;
end
// 产生时钟
always #(CLK_PERIOD/2) clk = ~clk;
// 例化
key #(
.CNT_MAX(CNT_MAX)
) key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);
endmodule
顶层模块中调用测试:
module top(
input clk,
input rst_n,
// key
input key,
output led0
);
key key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);
endmodule
演示效果:
视频效果无。(绑定好led和按键之后,)每次点击按键时,led亮灭切换。
不过发现个bug:复位了之后,第一次点按钮,led不切换,之后第二次以及之后,才会正常切换。
但是我不能在线仿真,1是不会,2是现在是U盘模式,不知道JTAG如何连,所以不能调试。
细细分析了之后,觉得应该是key值初始为0(按下)的问题,修改后好了,并且修改了仿真文件(换了下信号变化的1<->0),问了把过程展示出来,上面的就不删除了,修改后的文件和tb文件如下:
// 按键功能,消抖
/*
* led,0亮1灭
* key1,按下为0
*/
module key(
input clk,
input rst_n,
input key1,
output reg led0
);
// 系统时钟 50M,一个时钟周期是20ns
// 消抖时间 20ms = 20_000_000 / 20 = 1_000_000周期
// 系统时钟 12M,一个时钟周期是83.333333ns
// 消抖时间 20ms = 20_000_000/83.33333=240000周期,也可用 12M/50(hz)算=240000
parameter CNT_MAX = 20'd240000;
//parameter CNT_MAX = 20'd200;
reg [19:0] cnt;
reg key_d0; // 将按键信号延迟一个时钟周期
reg key_d1; // 将按键信号延迟两个时钟周期
reg key_flag; // 按键状态
// 延迟1,2周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_d0 <= 1'b1;
key_d1 <= 1'b1;
end
else begin
key_d0 <= key1;
key_d1 <= key_d0;
end
end
// 按键值消抖
/* 累加型的,会有问题,在保持一样时还会一直计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= 20'd0;
else begin
// 按键和前一个时刻一样,则计数器开始计数
cnt <= cnt + 1'b1;
end
end
*/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 20'd0;
else if(key_d1 != key_d0) // 按键发生变化
cnt <= CNT_MAX;
else //按键和前一个时刻一样,则计数器开始递减到 0
cnt <= (cnt>20'd0) ? (cnt-1'b1) : 20'd0;
end
reg key_flag2; // 按键状态变化,key_flag2 比 key_flag晚一个周期
// 消抖后的值
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_flag <= 1'b1;
key_flag2 <= 1'b1;
end
else if(cnt == 20'd1) begin
key_flag <= key_d1;
key_flag2 <= key_flag;
end
else begin
key_flag <= key_flag;
key_flag2 <= key_flag;
end
end
// 按下时点亮LED
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
led0 <= 1'b1;
else if(key_flag!=key_flag2) begin
if(key_flag==1'b0) // 下降沿,按下
led0 <= ~led0;
end
end
endmodule
`timescale 1ns/1ns
module key_tb();
parameter CLK_PERIOD = 5'd20; // 时钟周期 20ns
parameter CNT_MAX = 20'd10; // 消抖时间 10 个周期,200ns
reg clk;
reg rst_n;
reg key;
wire led0;
initial begin
clk <= 1'b0;
rst_n <= 1'b0;
key <= 1'b1;
#200
rst_n <= 1'b1;
// key 信号变化
#20
key <= 1'b0;
#20
key <= 1'b1;
#50
key <= 1'b0;
#40
key <= 1'b1;
#20
key <= 1'b0;
#300
key <= 1'b1;
#50
key <= 1'b0;
#40
key <= 1'b1;
#300
key <= 1'b0;
#30
key <= 1'b1;
#300
key <= 1'b0;
#300
key <= 1'b1;
#40
key <= 1'b0;
#30
key <= 1'b1;
end
// 产生时钟
always #(CLK_PERIOD/2) clk = ~clk;
// 例化
key #(
.CNT_MAX(CNT_MAX)
) key_inst(
.clk(clk),
.rst_n(rst_n),
.key1(key),
.led0(led0)
);
endmodule
3)矩阵键盘
参考 小脚丫- 矩阵键盘键入系统设计
代码如下:(这一版采用的是键盘按下有个过程的时序,后面会改为一个脉冲)
`timescale 1ns/1ns
module keyboard_tb();
parameter CLK_CNT = 5'd20;
reg clk;
reg rst_n;
reg [3:0] col;
wire [3:0] row;
wire [15:0] key_out;
wire [15:0] key_pulse;
initial begin
clk <= 1'b1; // 仿真时候发现这里默认为0的时候,分频后的clk第一个不太准(本来是400结果是390)。这里默认位高,后面就整400了,看着舒服
rst_n <= 1'b0;
col <= 4'b1111;
#200
rst_n <= 1'b1;
// col输入
#100
col <= 4'b0111; // 这里是300ns时赋值的,时钟从400整开始
#1701 // 一个分频后的clk是400ns(20*20),状态机4次循环后是1600 + 100(时钟开始前提前按下了)+1(到这一次时钟这刻赋值,其实应该加一个大clk,但是这里仿真,走个极限)
col <= 4'b1111;
end
// 时钟
always #(CLK_CNT/2) clk = ~clk;
// 例化
keyboard #(
.NUM_FOR_200HZ(CLK_CNT)
) keyboard_inst(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_pulse(key_pulse)
);
endmodule
TestBench:
`timescale 1ns/1ns
module keyboard_tb();
parameter CLK_CNT = 5'd20;
reg clk;
reg rst_n;
reg [3:0] col;
wire [3:0] row;
wire [15:0] key_out;
wire [15:0] key_pulse;
initial begin
clk <= 1'b1; // 仿真时候发现这里默认为0的时候,分频后的clk第一个不太准(本来是400结果是390)。这里默认位高,后面就整400了,看着舒服
rst_n <= 1'b0;
col <= 4'b1111;
#200
rst_n <= 1'b1;
// col输入
#100
col <= 4'b0111; // 这里是300ns时赋值的,时钟从400整开始
#1701 // 一个分频后的clk是400ns(20*20),状态机4次循环后是1600 + 100(时钟开始前提前按下了)+1(到这一次时钟这刻赋值,其实应该加一个大clk,但是这里仿真,走个极限)
col <= 4'b1111;
end
// 时钟
always #(CLK_CNT/2) clk = ~clk;
// 例化
keyboard #(
.NUM_FOR_200HZ(CLK_CNT)
) keyboard_inst(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_pulse(key_pulse)
);
endmodule
仿真波形:
4)计算器
嫌麻烦不想写tb测试,结果状态机中的按键一直有问题,组合逻辑那边的赋值和比较有问题,准备测试一下,测试了两天~
经验:改好之后和同事聊说我昨晚把最后的结果用next就好了,同事说三段状态机中,第二个一般用组合逻辑描述,第三个可以是组合/时序逻辑,如果是时序逻辑就用next,组合逻辑就用curr。
终于是把输入(按键和算式)搞完了,看下(进行计算之前的)效果:
之后计算资源利用率的时候,先在不加计算之前的代码统计一次。
计算功能:
想着很简单,做起来却有点懵。个位和十位先组起来,能不能使用 v1*10+v2 呢,还有计算完成后,还得拆分十进制各位,又得用除法和取余,都是问题。
看了个B栈视频 https://www.bilibili.com/video/BV1ie411V7xv/?vd_source=695515d886ad153c2a2617c7f5903856
发现乘法可以通过移位和加来实现。和同事聊,同事搜到可以用每一位移位来运算,先搞这个
因为个位十位需要这个转换。
那么乘10就等于 <<3 + a +a
拼接
v11 是十位,v12是个位,v1是要拼接的结果数
v1 = v11*10 + v12
v1 = v118 + v112 + v12
v1 = v11<<3 + v11<<1 + v12
verilog: v1 <= {v11, 3'b0} + {v11, 1'b0} + v12;
仿真:
// 数据拼接
module calc_1(
input clk,
input rst_n,
input [3:0] v11,
input [3:0] v12,
output reg [7:0] v1
);
always @(posedge clk or negedge rst_n) begin
v1 <= {v11, 3'b0} + {v11, 1'b0} + v12;
end
endmodule
module calc_1_tb();
reg clk;
reg rst_n;
reg [3:0] v11;
reg [3:0] v12;
wire [7:0] vout;
initial begin
clk <= 1'b0;
rst_n <= 1'b0;
#100
rst_n <= 1'b1;
#200
v11 <= 4'd6;
v12 <= 4'd7;
end
always #(200) clk = ~clk;
calc_1 calc_1_inst(
.clk(clk),
.rst_n(rst_n),
.v11(v11),
.v12(v12),
.v1(vout)
);
endmodule
除法:
看着麻烦,其实写起来也很简单。
乘法和除法如下:
// 8位除法,有符号
module calc_div(
input [7:0] A, // 被除数
input [7:0] B, // 除数
output reg [7:0] Q, // 商
output reg [7:0] R, // 余数
output reg neg // 负号
);
// 除法
/*
reg [7:0] A; // 被除数
reg [7:0] B; // 除数
reg [7:0] R; // 余数
reg [7:0] Q; // 商
*/
reg [7:0] i;
reg [7:0] A2;
reg [7:0] B2;
always @(*) begin
/*
A = v1;
B = v2;
*/
R = 8'b0;
Q = 8'b0;
neg = 1'b0;
// 负数转正数
if(A[7] == 1'b1) begin
A2 = {1'b0, ~A[6:0]} + 1'b1;
end else
A2 = A;
if(B[7] == 1'b1)
A2 = {1'b0, ~B[6:0]} + 1'b1;
else
B2 = B;
for(i=1; i<=7; i=i+1) begin
R = {R[6:0], A2[7-i]}; // 从A中移一位到R中
if(R>=B) begin
Q[7-i] = 1'b1;
R = R - B;
end else begin
Q[7-i] = 1'b0;
end
end
// 负数再转回去
if(A[7]^B[7]) begin
// Q = {1'b1, ~Q[6:0]} + 1'b1; //不在商里面加负号,因为-0没法表示,0和-0补码一样
neg = 1'b1;
end
end
endmodule
// 8位乘法
module calc_mul(
input [7:0] A, // 被乘数
input [7:0] B, // 乘数
output reg [15:0] out // 乘积
);
integer i;
reg [15:0] A2; // 扩展
reg [15:0] B2;
always @(*) begin
out = 16'b0;
// 负数扩展
if(A[7]==1'b1)
A2 = {8'hff, A};
else
A2 = A;
if(B[7]==1'b1)
B2 = {8'hff, B};
else
B2 = B;
for(i=0; i<=15; i=i+1) begin
if(B2[i]==1) begin
out = out + (A2<<i);
end
end
end
endmodule
再把逻辑微调了几天,终于完成两位10进制四则运算计算器,虽然还有点瑕疵(对负数支持不到位等),但基本可用。除法也能显示一位小数了。
4. 实现结果
加法 12+86=98
减法
12-5=7
0-54 = -54
乘法 29*3=87
除法 12/5=2.4
5. 追加:加法和乘法逻辑完善
因为之前偷懒了,两位数的加法和乘法,结果最多为3或4位,我都丢弃掉了,之保留了两位。这样肯定是不行的。这里将之前除法、乘法(这两项只是扩展一下位数即可)、数据拆分都扩展一下就行了。
数据拆分代码如下:
// 最多四位十进制数字的(个位和十位)拆分
module num_split(
input [15:0] v,
output [3:0] thd,
output [3:0] hud,
output [3:0] ten,
output [3:0] one,
output neg // 负号
);
// 调用除法,将vout输出位两位
wire [7:0] Q,Q2,Q3;
wire [7:0] R,R2,R3;
wire neg,neg2,neg3;
calc_div calc_div_inst_numsplit(
.A(v),
.B(8'd10),
.Q(Q),
.R(R),
.neg(neg)
);
assign one = R[3:0];
calc_div calc_div_inst_numsplit2(
.A(Q),
.B(8'd10),
.Q(Q2),
.R(R2),
.neg(neg2)
);
assign ten = R2[3:0];
calc_div calc_div_inst_numsplit3(
.A(Q2),
.B(8'd10),
.Q(Q3),
.R(R3),
.neg(neg3)
);
assign hud = R3[3:0];
assign thd = Q3[3:0];
endmodule
除法代码
// 8位除法,有符号
module calc_div(
input [15:0] A, // 被除数
input [15:0] B, // 除数
output reg [15:0] Q, // 商
output reg [15:0] R, // 余数
output reg neg // 负号
);
// 除法
/*
reg [7:0] A; // 被除数
reg [7:0] B; // 除数
reg [7:0] R; // 余数
reg [7:0] Q; // 商
*/
reg [7:0] i;
reg [15:0] A2;
reg [15:0] B2;
always @(*) begin
/*
A = v1;
B = v2;
*/
R = 16'b0;
Q = 16'b0;
neg = 1'b0;
// 负数转正数
if(A[15] == 1'b1) begin
A2 = {1'b0, ~A[14:0]} + 1'b1;
end else
A2 = A;
if(B[15] == 1'b1)
B2 = {1'b0, ~B[14:0]} + 1'b1;
else
B2 = B;
for(i=1; i<=15; i=i+1) begin
R = {R[14:0], A2[15-i]}; // 从A中移一位到R中
if(R>=B) begin
Q[15-i] = 1'b1;
R = R - B;
end else begin
Q[15-i] = 1'b0;
end
end
// 负数再转回去
if(A[15]^B[15]) begin
neg = 1'b1;
end
end
endmodule
6. 资源使用情况
Design Summary
Number of registers: 244 out of 4635 (5%)
PFU registers: 244 out of 4320 (6%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1262 out of 2160 (58%)
SLICEs as Logic/ROM: 1262 out of 2160 (58%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 550 out of 2160 (25%)
Number of LUT4s: 2518 out of 4320 (58%)
Number used as logic LUTs: 1418
Number used as distributed RAM: 0
Number used as ripple logic: 1100
Number used as shift registers: 0
Number of PIO sites used: 13 + 4(JTAG) out of 105 (16%)
Number of block RAMs: 0 out of 10 (0%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 0 out of 2 (0%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)
参考
- 小脚丫- 小脚丫核心板上的实验例程
- 小脚丫- 基于小脚丫FPGA扩展底板的功能性项目
- 小脚丫- 数码管模块
- 小脚丫- 矩阵键盘键入系统设计, 矩阵按键模块
- 计算机组成原理 4 乘法器和除法器的原理
- FPGA设计之加减乘除四则运算
- 基于移位加法的乘法器---Verilog实现