--作者:小黑同学
本文为明德扬原创及录用文章,转载请注明出处!
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.1 系统结构框图
系统结构框图如下所示:
1.1.1 模块功能
Ø 键盘扫描模块实现功能
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.2 顶层信号
1.1.1 参考代码
1. module calc_project(
2. clk ,
3. rst_n ,
4. key_col ,
5. key_row ,
6. seg_sel ,
7. segment ,
8. beep
9. );
10.
11. parameter KEY_WID = 4 ;
12. parameter STATE_WID = 5 ;
13. parameter NUM_WID = 27 ;
14. parameter SEG_NUM = 8 ;
15. parameter SEG_WID = 8 ;
16.
17. input clk ;
18. input rst_n ;
19. input [KEY_WID-1:0] key_col ;
20. output [KEY_WID-1:0] key_row ;
21. output [SEG_NUM-1:0] seg_sel ;
22. output [SEG_WID-1:0] segment ;
23. output beep ;
24.
25. wire [KEY_WID-1:0] key_num ;
26. wire key_vld ;
27. wire [KEY_WID-1:0] key_num_out ;
28. wire [KEY_WID-1:0] key_vld_out ;
29. wire [STATE_WID-1:0] state_c ;
30. wire [NUM_WID-1:0] op_1 ;
31. wire op_1_err ;
32. wire [KEY_WID-1:0] oper ;
33. wire [NUM_WID-1:0] op_2 ;
34. wire op_2_err ;
35. wire [NUM_WID-1:0] result ;
36. wire result_err ;
37. wire result_neg ;
38. wire [SEG_NUM*4-1:0] display ;
39. wire display_vld ;
40.
41. key_scan key_scan_prj(
42. .clk (clk ) ,
43. .rst_n (rst_n ) ,
44. .key_col (key_col) ,
45. .key_row (key_row) ,
46. .key_out (key_num) ,
47. .key_vld (key_vld)
48. );
49.
50. work_state work_state_prj(
51. .clk (clk ) ,
52. .rst_n (rst_n ) ,
53. .key_num (key_num ) ,
54. .key_vld (key_vld ) ,
55. .result_err (result_err ) ,
56. .key_num_out(key_num_out) ,
57. .key_vld_out(key_vld_out) ,
58. .state_c (state_c )
59. );
60.
61. op_1 op_1_prj(
62. .clk (clk ) ,
63. .rst_n (rst_n ) ,
64. .key_num_out(key_num_out) ,
65. .key_vld_out(key_vld_out) ,
66. .state_c (state_c ) ,
67. .result (result ) ,
68. .op_1 (op_1 ) ,
69. .op_1_err (op_1_err )
70. );
71.
72. oper oper_prj(
73. .clk (clk ) ,
74. .rst_n (rst_n ) ,
75. .key_num_out(key_num_out) ,
76. .key_vld_out(key_vld_out) ,
77. .state_c (state_c ) ,
78. .oper (oper )
79. );
80.
81. op_2 op_2_prj(
82. .clk (clk ) ,
83. .rst_n (rst_n ) ,
84. .key_num_out(key_num_out) ,
85. .key_vld_out(key_vld_out) ,
86. .state_c (state_c ) ,
87. .op_1 (op_1 ) ,
88. .op_2 (op_2 ) ,
89. .op_2_err (op_2_err )
90. );
91.
92. result result_prj(
93. .clk (clk ) ,
94. .rst_n (rst_n ) ,
95. .key_num_out(key_num_out) ,
96. .key_vld_out(key_vld_out) ,
97. .state_c (state_c ) ,
98. .op_1 (op_1 ) ,
99. .oper (oper ) ,
100. .op_2 (op_2 ) ,
101. .result (result ) ,
102. .result_err (result_err ) ,
103. .result_neg (result_neg )
104. );
105.
106. display_sel display_sel_prj(
107. .clk (clk ) ,
108. .rst_n (rst_n ) ,
109. .state_c (state_c ) ,
110. .op_1 (op_1 ) ,
111. .op_2 (op_2 ) ,
112. .result (result ) ,
113. .result_neg (result_neg ) ,
114. .display (display ) ,
115. .display_vld(display_vld)
116. );
117.
118. segment segment_prj(
119. .rst_n (rst_n ) ,
120. .clk (clk ) ,
121. .display (display ) ,
122. .display_vld(display_vld) ,
123. .seg_sel (seg_sel ) ,
124. .segment (segment )
125. );
126.
127. beep beep_prj(
128. .clk (clk ) ,
129. .rst_n (rst_n ) ,
130. .op_1_err (op_1_err ) ,
131. .op_2_err (op_2_err ) ,
132. .result_err (result_err ) ,
133. .beep (beep )
134. );
135.
136. endmodule
1.1 键盘扫描模块设计
1.1.1 接口信号
1.1.1 设计思路
在前面的案例中已经有矩阵键盘的介绍,所以这里不在过多介绍
1.1.2 参考代码
1. always @(posedge clk or negedge rst_n)begin
2. if(rst_n==1b0)begin
3. key_col_ff0 <= 4b1111;
4. key_col_ff1 <= 4b1111;
5. end
6. else begin
7. key_col_ff0 <= key_col ;
8. key_col_ff1 <= key_col_ff0;
9. end
10. end
11.
12.
13. always @(posedge clk or negedge rst_n) begin
14. if (rst_n==0) begin
15. shake_cnt <= 0;
16. end
17. else if(add_shake_cnt) begin
18. if(end_shake_cnt)
19. shake_cnt <= 0;
20. else
21. shake_cnt <= shake_cnt+1 ;
22. end
23. end
24. assign add_shake_cnt = key_col_ff1!=4hf;
25. assign end_shake_cnt = add_shake_cnt && shake_cnt == TIME_20MS-1 ;
26.
27.
28. always @(posedge clk or negedge rst_n)begin
29. if(rst_n==1b0)begin
30. state_c <= CHK_COL;
31. end
32. else begin
33. state_c <= state_n;
34. end
35. end
36.
37. always @(*)begin
38. case(state_c)
39. CHK_COL: begin
40. if(col2row_start )begin
41. state_n = CHK_ROW;
42. end
43. else begin
44. state_n = CHK_COL;
45. end
46. end
47. CHK_ROW: begin
48. if(row2del_start)begin
49. state_n = DELAY;
50. end
51. else begin
52. state_n = CHK_ROW;
53. end
54. end
55. DELAY : begin
56. if(del2wait_start)begin
57. state_n = WAIT_END;
58. end
59. else begin
60. state_n = DELAY;
61. end
62. end
63. WAIT_END: begin
64. if(wait2col_start)begin
65. state_n = CHK_COL;
66. end
67. else begin
68. state_n = WAIT_END;
69. end
70. end
71. default: state_n = CHK_COL;
72. endcase
73. end
74. assign col2row_start = state_c==CHK_COL && end_shake_cnt;
75. assign row2del_start = state_c==CHK_ROW && row_index==3 && end_row_cnt;
76. assign del2wait_start= state_c==DELAY && end_row_cnt;
77. assign wait2col_start= state_c==WAIT_END && key_col_ff1==4hf;
78.
79. always @(posedge clk or negedge rst_n)begin
80. if(rst_n==1b0)begin
81. key_row <= 4b0;
82. end
83. else if(state_c==CHK_ROW)begin
84. key_row <= ~(1b1 << row_index);
85. end
86. else begin
87. key_row <= 4b0;
88. end
89. end
90.
91.
92. always @(posedge clk or negedge rst_n) begin
93. if (rst_n==0) begin
94. row_index <= 0;
95. end
96. else if(add_row_index) begin
97. if(end_row_index)
98. row_index <= 0;
99. else
100. row_index <= row_index+1 ;
101. end
102. else if(state_c!=CHK_ROW)begin
103. row_index <= 0;
104. end
105. end
106. assign add_row_index = state_c==CHK_ROW && end_row_cnt;
107. assign end_row_index = add_row_index && row_index == 4-1 ;
108.
109.
110. always @(posedge clk or negedge rst_n) begin
111. if (rst_n==0) begin
112. row_cnt <= 0;
113. end
114. else if(add_row_cnt) begin
115. if(end_row_cnt)
116. row_cnt <= 0;
117. else
118. row_cnt <= row_cnt+1 ;
119. end
120. end
121. assign add_row_cnt = state_c==CHK_ROW || state_c==DELAY;
122. assign end_row_cnt = add_row_cnt && row_cnt == 16-1 ;
123.
124.
125. always @(posedge clk or negedge rst_n)begin
126. if(rst_n==1b0)begin
127. key_col_get <= 0;
128. end
129. else if(state_c==CHK_COL && end_shake_cnt ) begin
130. if(key_col_ff1==4b1110)
131. key_col_get <= 0;
132. else if(key_col_ff1==4b1101)
133. key_col_get <= 1;
134. else if(key_col_ff1==4b1011)
135. key_col_get <= 2;
136. else
137. key_col_get <= 3;
138. end
139. end
140.
141.
142. always @(posedge clk or negedge rst_n)begin
143. if(rst_n==1b0)begin
144. key_out <= 0;
145. end
146. else if(state_c==CHK_ROW && end_row_cnt)begin
147. key_out <= {row_index,key_col_get};
148. end
149. else begin
150. key_out <= 0;
151. end
152. end
153.
154. always @(posedge clk or negedge rst_n)begin
155. if(rst_n==1b0)begin
156. key_vld <= 1b0;
157. end
158. else if(state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1b0)begin
159. key_vld <= 1b1;
160. end
161. else begin
162. key_vld <= 1b0;
163. end
164. end
1.2 工作状态选择模块设计
1.2.1 接口信号
1.1.1 设计思路
该模块的主要功能是根据按下的按键进行不同来判断和决定计算器的工作状态。一条等式可以写成:运算数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.1.2 参考代码
使用GVIM,在命令模式下输入如下内容,即可生成本模块所需要的状态机代码。
使用明德扬的状态机模板,可以很快速的写出此模块代码。
1. always @(*)begin
2. case(key_num)
3. 4d0 :key_num_chg = 4d7 ;
4. 4d1 :key_num_chg = 4d8 ;
5. 4d2 :key_num_chg = 4d9 ;
6. 4d3 :key_num_chg = 4d10 ;
7. 4d7 :key_num_chg = 4d11 ;
8. 4d8 :key_num_chg = 4d1 ;
9. 4d9 :key_num_chg = 4d2 ;
10. 4d10 :key_num_chg = 4d3 ;
11. 4d11 :key_num_chg = 4d14 ;
12. 4d12 :key_num_chg = 4d0 ;
13. 4d13 :key_num_chg = 4d12 ;
14. 4d14 :key_num_chg = 4d13 ;
15. default:key_num_chg = key_num;
16. endcase
17. end
18.
19. assign key_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;
20. assign key_op_en = (key_num_chg==10 || key_num_chg==11 || key_num_chg==12 || key_num_chg==13) && key_vld==1;
21. assign key_cal_en = key_num_chg==15 && key_vld==1;
22. assign key_back_en = key_num_chg==14 && key_vld==1;
23.
24.
25.
26. always @(posedge clk or negedge rst_n) begin
27. if (rst_n==0) begin
28. state_c <= OP_1 ;
29. end
30. else begin
31. state_c <= state_n;
32. end
33. end
34.
35. always @(*) begin
36. if(result_err)begin
37. state_n = ERROR;
38. end
39. else begin
40. case(state_c)
41. OP_1 :begin
42. if(op_12oper_start)
43. state_n = OPER ;
44. else if(op_12result_start)
45. state_n = RESULT ;
46. else
47. state_n = state_c ;
48. end
49. OPER :begin
50. if(oper2op_2_start)
51. state_n = OP_2 ;
52. else if(oper2result_start)
53. state_n = RESULT ;
54. else
55. state_n = state_c ;
56. end
57. OP_2 :begin
58. if(op_22oper_start)
59. state_n = OPER ;
60. else if(op_22result_start)
61. state_n = RESULT ;
62. else
63. state_n = state_c ;
64. end
65. RESULT :begin
66. if(result2op_1_start)
67. state_n = OP_1 ;
68. else if(result2oper_start)
69. state_n = OPER ;
70. else
71. state_n = state_c ;
72. end
73. ERROR :begin
74. if(error2op_1_start)
75. state_n = OP_1 ;
76. else
77. state_n = state_c ;
78. end
79. default : state_n = OP_1 ;
80. endcase
81. end
82. end
83.
84. assign op_12oper_start = state_c==OP_1 && key_op_en ;
85. assign op_12result_start = state_c==OP_1 && key_cal_en;
86. assign oper2op_2_start = state_c==OPER && key_num_en;
87. assign oper2result_start = state_c==OPER && key_cal_en;
88. assign op_22oper_start = state_c==OP_2 && key_op_en ;
89. assign op_22result_start = state_c==OP_2 && key_cal_en;
90. assign result2op_1_start = state_c==RESULT && key_num_en;
91. assign result2oper_start = state_c==RESULT && key_op_en ;
92. assign error2op_1_start = state_c==ERROR && key_num_en;
93.
94. always @(posedge clk or negedge rst_n)begin
95. if(rst_n==1b0)begin
96. key_num_out <= 0;
97. end
98. else begin
99. key_num_out <= key_num_chg;
100. end
101. end
102.
103. always @(posedge clk or negedge rst_n)begin
104. if(rst_n==1b0)begin
105. key_vld_out <= 0;
106. end
107. else begin
108. key_vld_out <= key_vld;
109. end
110. end
1.2 运算数1模块设计
1.2.1 接口信号
1.1.1 设计思路
该模块主要的作用是根据当前状态和输入的按键,来决定运算数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、 其他时刻保持为低电平。
1.1.2 参考代码
1. assign key_num_en = (key_num_out==0 || key_num_out==1 || key_num_out==2 || key_num_out==3 || key_num_out==4 || key_num_out==5 || key_num_out==6 || key_num_out==7 || key_num_out==8 || key_num_out==9) && key_vld_out==1;
2. assign key_op_en = (key_num_out==10 || key_num_out==11 || key_num_out==12 || key_num_out==13) && key_vld_out==1;
3. assign key_cal_en = key_num_out==15 && key_vld_out==1;
4. assign key_back_en = key_num_out==14 && key_vld_out==1;
5.
6. always @(posedge clk or negedge rst_n)begin
7. if(rst_n==1b0)begin
8. state_c_ff <= 0;
9. end
10. else begin
11. state_c_ff <= state_c;
12. end
13. end
14.
15. always @(posedge clk or negedge rst_n)begin
16. if(rst_n==1b0)begin
17. flag_calc <= 0;
18. end
19. else if(state_c==OP_1)begin
20. flag_calc <= 1b0;
21. end
22. else if(state_c_ff==OP_2 && state_c==OPER || state_c==RESULT)begin
23. flag_calc <= 1b1;
24. end
25. else begin
26. flag_calc <= flag_calc;
27. end
28. end
29.
30. always @(posedge clk or negedge rst_n)begin
31. if(rst_n==1b0)begin
32. op_1 <= 0;
33. end
34. else if(state_c==ERROR)begin
35. op_1 <= 10;
36. end
37. else if((state_c_ff==RESULT || state_c_ff==ERROR) && state_c==OP_1)begin
38. op_1 <= key_num_out;
39. end
40. else if(state_c==OP_1 && key_back_en==1)begin
41. op_1 <= op_1 / 10;
42. end
43. else if(state_c==OP_1 && key_num_en==1)begin
44. op_1 <= (op_1>9999999) ? op_1 : (op_1*10+key_num_out);
45. end
46. else if(flag_calc==1)begin
47. op_1 <= result;
48. end
49. else begin
50. op_1 <= op_1;
51. end
52. end
53.
54. always @(posedge clk or negedge rst_n)begin
55. if(rst_n==1b0)begin
56. op_1_err <= 0;
57. end
58. else if(state_c==OP_1 && state_c_ff==OP_1 && key_num_en==1 && op_1>9999999)begin
59. op_1_err <= 1b1;
60. end
61. else begin
62. op_1_err <= 1b0;
63. end
64. end
1.2 运算符模块设计
1.2.1 接口信号
1.1.1 设计思路
本模块的设计思路比较简单,只需要判断哪些按键是运算符,然后再这些运算符被按下的时候,将他们对应的值输出就可以了。
下面是运算符指示信号设计思路:
1、 当“加”“减”“乘”“除”四个按键的任意一个被按下之后,该信号置为高电平;
2、 当“加”“减”“乘”“除”四个按键没有一个被按下的时候,该信号置为低电平。
下面是运算符输出信号oper设计思路:
初始状态,该信号输出0;
1、 当处于操作数1状态时,输出0;
2、 当“加”“减”“乘”“除”任意按键被按下之后,输出该按键对应的值;
3、 其他时候保持不变;
1.1.2 参考代码
1. assign key_op_en = (key_num_out==10 || key_num_out==11 || key_num_out==12 || key_num_out==13) && key_vld_out==1;
2.
3. always @(posedge clk or negedge rst_n)begin
4. if(rst_n==1b0)begin
5. oper <= 0;
6. end
7. else if(state_c==OP_1)begin
8. oper <= 0;
9. end
10. else if(key_op_en==1)begin
11. oper <= key_num_out;
12. end
13. else begin
14. oper <= oper;
15. end
16. End
1.2 运算数2模块设计
1.2.1 接口信号
1.1.1 设计思路
该模块主要的作用是根据当前状态和输入的按键,来决定运算数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.1.2 参考代码
1. assign key_num_en = (key_num_out==0 || key_num_out==1 || key_num_out==2 || key_num_out==3 || key_num_out==4 || key_num_out==5 || key_num_out==6 || key_num_out==7 || key_num_out==8 || key_num_out==9) && key_vld_out==1;
2. assign key_op_en = (key_num_out==10 || key_num_out==11 || key_num_out==12 || key_num_out==13) && key_vld_out==1;
3. assign key_cal_en = key_num_out==15 && key_vld_out==1;
4. assign key_back_en = key_num_out==14 && key_vld_out==1;
5.
6. always @(posedge clk or negedge rst_n)begin
7. if(rst_n==1b0)begin
8. state_c_ff <= 0;
9. end
10. else begin
11. state_c_ff <= state_c;
12. end
13. end
14.
15. always @(posedge clk or negedge rst_n)begin
16. if(rst_n==1b0)begin
17. op_2 <= 0;
18. end
19. else if(state_c==OPER)begin
20. op_2 <= 0;
21. end
22. else if(state_c_ff==OPER && state_c==OP_2)begin
23. op_2 <= key_num_out;
24. end
25. else if(state_c==OP_2 && key_back_en==1)begin
26. op_2 <= op_2 / 10;
27. end
28. else if(state_c==OP_2 && key_num_en==1)begin
29. op_2 <= (op_2>9999999) ? op_2 : (op_2*10+key_num_out);
30. end
31. else begin
32. op_2 <= op_2;
33. end
34. end
35.
36. always @(posedge clk or negedge rst_n)begin
37. if(rst_n==1b0)begin
38. op_2_err <= 0;
39. end
40. else if(state_c==OP_2 && key_num_en==1 && op_2>9999999)begin
41. op_2_err <= 1b1;
42. end
43. else begin
44. op_2_err <= 1b0;
45. end
46. end
1.2 运算单元模块设计
1.2.1 接口信号
1.1.1 设计思路
本模块的作用是根据运算符,对运算数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.1.2 参考代码
1. assign key_op_en = (key_num_out==10 || key_num_out==11 || key_num_out==12 || key_num_out==13) && key_vld_out==1;
2. assign key_cal_en = key_num_out==15 && key_vld_out==1;
3. assign calculate = (state_c_ff==OP_2 && state_c==OPER || key_cal_en==1);
4.
5. always @(posedge clk or negedge rst_n)begin
6. if(rst_n==1b0)begin
7. state_c_ff <= 0;
8. end
9. else begin
10. state_c_ff <= state_c;
11. end
12. end
13.
14. always @(posedge clk or negedge rst_n)begin
15. if(rst_n==1b0)begin
16. result <= 0;
17. end
18. else if(calculate==1)begin
19. case(oper)
20. ADD:begin
21. if(result_neg==0)
22. result <= op_1 + op_2;
23. else
24. result <= (op_1>op_2) ? (op_1 - op_2) : (op_2 - op_1);
25. end
26. DEV:begin
27. if(result_neg==0)
28. result <= (op_1>op_2) ? (op_1 - op_2) : (op_2 - op_1);
29. else
30. result <= op_1 + op_2;
31. end
32. MUL:begin
33. result <= op_1 * op_2;
34. end
35. DIV:begin
36. result <= op_1 / op_2;
37. end
38. default:result <= op_1;
39. endcase
40. end
41. else begin
42. result <= result;
43. end
44. end
45.
46. always @(posedge clk or negedge rst_n)begin
47. if(rst_n==1b0)begin
48. result_neg <= 0;
49. end
50. else if(state_c==OP_1)begin
51. result_neg <= 1b0;
52. end
53. else if(state_c_ff==ERROR)begin
54. result_neg <= 1b0;
55. end
56. else if(calculate==1 && oper==DEV && op_1<op_2)begin
57. result_neg <= 1b1;
58. end
59. else if(calculate==1 && result_neg==1 && oper==ADD && op_1<=op_2)begin
60. result_neg <= 1b0;
61. end
62. else if(result==0)begin
63. result_neg <= 1b0;
64. end
65. else begin
66. result_neg <= result_neg;
67. end
68. end
69.
70. always @(posedge clk or negedge rst_n)begin
71. if(rst_n==1b0)begin
72. result_err <= 0;
73. end
74. else if(state_c==OP_1)begin
75. result_err <= 1b0;
76. end
77. else if((state_c_ff==OP_2 && state_c==OPER) || (key_cal_en==1 && state_c_ff!=ERROR))begin
78. case(oper)
79. ADD:begin
80. if(result_neg==0)
81. result_err <= (op_1+op_2)>9999_9999 ? 1b1 : 1b0;
82. else
83. result_err <= 1b0;
84. end
85. DEV:begin
86. if(result_neg==1)
87. result_err <= (op_1+op_2)>999_9999 ? 1b1 : 1b0;
88. else if(op_2>op_1)
89. result_err <= (op_2-op_1)>999_9999 ? 1b1 : 1b0;
90. else
91. result_err <= 1b0;
92. end
93. MUL:begin
94. if(result_neg==1)
95. result_err <= (op_1*op_2)>999_9999 ? 1b1 : 1b0;
96. else
97. result_err <= (op_1*op_2)>9999_9999 ? 1b1 : 1b0;
98. end
99. DIV:begin
100. if(op_2==0)
101. result_err <= 1b1;
102. else
103. result_err <= 1b0;
104. end
105. default:result_err <= 1b0;
106. endcase
107. end
108. else begin
109. result_err <= 1b0;
110. end
111. end
1.2 显示对象选择模块设计
1.2.1 接口信号
1.1.1 设计思路
该模块的作用是根据当前计算器的工作状态来选择数码管的显示内容。
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 Lite Edition 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 the denominator input”表示需要设置的分母的位宽这里设置为4。在下方分子和分母的表示都选用“Unsigned”无符号类型。然后点击Next
第四步、下图中的1处表示是否需要对输出进行打拍,这里选择打一拍之后输出。2处表示要进行的优化,这里选择默认优化。3处表示是否总是返回正余数,选择是。然后点击Next。
第五步、方框出表示该IP核在仿真的时候需要调用的库,直接点击Next即可。
第六步、这一界面是设置需要生成的文件,本工程只需要生成默认的即可,所以不用勾选。点击Finish。
1.1.1 参考代码
1. always @(posedge clk or negedge rst_n)begin
2. if(rst_n==1b0)begin
3. result_neg_tmp <= 0;
4. end
5. else begin
6. result_neg_tmp <= result_neg;
7. end
8. end
9.
10. always @(*)begin
11. if(state_c==OP_2)begin
12. dis_sel = op_2;
13. end
14. else begin
15. dis_sel = op_1;
16. end
17. end
18.
19. always @(posedge clk or negedge rst_n)begin
20. if(rst_n==1b0)begin
21. dis_sel_tmp <= 0;
22. end
23. else begin
24. dis_sel_tmp <= dis_sel;
25. end
26. end
27.
28.
29. div div_prj(
30. .clock (clk ) ,
31. .numer (dis_tmp ) ,
32. .denom (10 ) ,
33. .quotient (div_quo ) ,
34. .remain (div_rem )
35. );
36.
37. always @(posedge clk or negedge rst_n)begin
38. if(rst_n==1b0)begin
39. flag_add <= 0;
40. end
41. else if(dis_sel!=dis_sel_tmp || result_neg!=rssult_neg_tmp)begin
42. flag_add <= 1;
43. end
44. else if(end_dis_cnt)begin
45. flag_add <= 0;
46. end
47. end
48.
49.
50. always @(posedge clk or negedge rst_n) begin
51. if (rst_n==0) begin
52. dis_cnt <= 0;
53. end
54. else if(add_dis_cnt) begin
55. if(end_dis_cnt)
56. dis_cnt <= 0;
57. else
58. dis_cnt <= dis_cnt+1 ;
59. end
60. end
61. assign add_dis_cnt = flag_add && (dis_sel==dis_sel_tmp && result_neg==result_neg_tmp);
62. assign end_dis_cnt = add_dis_cnt && dis_cnt == 10-1 ;
63.
64.
65. assign dis_tmp = add_dis_cnt && dis_cnt==1 ? dis_sel : div_quo;
66.
67. always @(posedge clk or negedge rst_n)begin
68. if(rst_n==1b0)begin
69. display <= 4b0;
70. end
71. else if(state_c==ERROR)begin
72. display[4*(dis_cnt)-1 -:4] <= 4b1111;
73. end
74. else if(end_dis_cnt && result_neg==1 && state_c!=OP_2)begin
75. display[31:28] <= 4b1010;
76. end
77. else begin
78. display[4*(dis_cnt-1)-1 -:4] <= div_rem;
79. end
80. end
81.
82.
83. always @(posedge clk or negedge rst_n)begin
84. if(rst_n==1b0)begin
85. display_vld <= 0;
86. end
87. else begin
88. display_vld <= (dis_cnt==0 && (dis_sel==dis_sel_tmp)) ? 1b1 : 1b0;
89. end
90. end
1.2 数码管显示模块设计
1.2.1 接口信号
1.1.1 设计思路
本模块主要实现的功能是对显示对象选择模块的显示数据输出信号(display)进行数码管显示。
1、 复位后,数码管默认显示运算数1;
2、 当result_err有效时,数码管显示8个F;
3、 当result_neg有效时,第8个数码管显示“—”;
4、 数码管显示display;
由于数码管显示在前面已有案例介绍,所以这个就不做介绍。
1.1.2 参考代码
1. always @(posedge clk or negedge rst_n) begin
2. if (rst_n==0) begin
3. count_20us <= 0;
4. end
5. else if(add_count_20us) begin
6. if(end_count_20us)
7. count_20us <= 0;
8. else
9. count_20us <= count_20us+1 ;
10. end
11. end
12. assign add_count_20us = 1;
13. assign end_count_20us = add_count_20us && count_20us == TIME_20US-1 ;
14.
15.
16. always @(posedge clk or negedge rst_n) begin
17. if (rst_n==0) begin
18. sel_cnt <= 0;
19. end
20. else if(add_sel_cnt) begin
21. if(end_sel_cnt)
22. sel_cnt <= 0;
23. else
24. sel_cnt <= sel_cnt+1 ;
25. end
26. end
27. assign add_sel_cnt = end_count_20us;
28. assign end_sel_cnt = add_sel_cnt && sel_cnt == SEG_NUM-1 ;
29.
30.
31.
32. always @(posedge clk or negedge rst_n)begin
33. if(rst_n==1b0)begin
34. seg_sel <= {SEG_NUM{1b1}};
35. end
36. else begin
37. seg_sel <= ~(1b1 << sel_cnt);
38. end
39. end
40.
41. always @(posedge clk or negedge rst_n)begin
42. if(rst_n==1b0)begin
43. display_ff0 <= 0;
44. end
45. else begin
46. for(ii=0;ii<SEG_NUM;ii=ii+1)begin
47. if(display_vld==1)begin
48. display_ff0[(ii+1)*4-1 -:4] <= display[(ii+1)*4-1 -:4];
49. end
50. else begin
51. display_ff0[(ii+1)*4-1 -:4] <= display_ff0[(ii+1)*4-1 -:4];
52. end
53. end
54. end
55. end
56.
57. always @(*)begin
58. seg_tmp = display_ff0[(sel_cnt+1)*4-1 -:4];
59. end
60.
61.
62. always @(posedge clk or negedge rst_n)begin
63. if(rst_n==1b0)begin
64. segment <= NUM_0;
65. end
66. else begin
67. case(seg_tmp)
68. 0 :segment <=NUM_0 ;
69. 1 :segment <=NUM_1 ;
70. 2 :segment <=NUM_2 ;
71. 3 :segment <=NUM_3 ;
72. 4 :segment <=NUM_4 ;
73. 5 :segment <=NUM_5 ;
74. 6 :segment <=NUM_6 ;
75. 7 :segment <=NUM_7 ;
76. 8 :segment <=NUM_8 ;
77. 9 :segment <=NUM_9 ;
78. 10:segment <=NUM_10 ;
79. default:segment <= NUM_ERR;
80. endcase
81. end
82. end
1.2 蜂鸣器模块设计
1.2.1 接口信号
1.1.1 设计思路
该模块的主要功能是根据接收到的各个错误指示信号,进行报警提示。当接收到错误信号有效的时候,蜂鸣器报警,持续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.1.2 参考代码
1. always @(posedge clk or negedge rst_n)begin
2. if(rst_n==1b0)begin
3. flag_add <= 0;
4. end
5. else if(op_1_err || op_2_err || result_err)begin
6. flag_add <= 1;
7. end
8. else if(end_cnt_1s)begin
9. flag_add <= 0;
10. end
11. end
12.
13.
14. always @(posedge clk or negedge rst_n) begin
15. if (rst_n==0) begin
16. cnt_1s <= 0;
17. end
18. else if(add_cnt_1s) begin
19. if(end_cnt_1s)
20. cnt_1s <= 0;
21. else
22. cnt_1s <= cnt_1s+1 ;
23. end
24. end
25. assign add_cnt_1s = flag_add;
26. assign end_cnt_1s = add_cnt_1s && cnt_1s == CNT_1S-1 ;
27.
28.
29. always @(posedge clk or negedge rst_n)begin
30. if(rst_n==1b0)begin
31. beep <= 1b1;
32. end
33. else if(flag_add)begin
34. beep <= 1b0;
35. end
36. else begin
37. beep <= 1b1;
38. end
39. end
1.2 效果和总结
1.2.1 db603开发板
由于计算器的演示是一个动态的过程,所以从下面图片中看不出具体实现的效果,想要看上板效果的话可以看一下工程上板的视频。
1.1.1 ms980试验箱
由于计算器的演示是一个动态的过程,所以从下面图片中看不出具体实现的效果,想要看上板效果的话可以看一下工程上板的视频。