当前位置:   article > 正文

基于FPGA的数字信号处理(18)--半加器和全加器

基于FPGA的数字信号处理(18)--半加器和全加器

前言

        在数字系统中,加法运算是最常见的算术运算,同时它也是进行各种复杂运算的基础。

半加器

        最简单的加法器叫做 半加器Half Adder),它将2个输入1bit的数据相加,输出一个2bits的和,和的范围为0~2(10进制)。和的高位也被称为进位Carry),和的低位则通常被直接叫Sum)。例如:

  • 1 + 1 = 2 = 10,即进位carry是1,和sum是0

  • 1 + 0 = 1 = 01,即进位carry是0,和sum是1

  • 0 + 1 = 1 = 01,即进位carry是0,和sum是1

  • 0 + 0 = 0 = 00,即进位carry是0,和sum是0

        2个1bit数相加,最多只有4种情况(在上面已经例出来了),据此可以写出半加器的真值表:

加数1加数2结果进位
absumcarry
0000
0110
1010
1101

        从这个真值表,不难推断出两个输出的逻辑表达式:

  • 和sum在2个输入不同时为1,所以它是输入异或的结果,即 sum = a ^ b

  • 进位carry在2个输入都为1时才为1,所以它是输入相与的结果,即 carry = a & b

        有了逻辑表达式后,就很容易画出电路图了:

image-20240423194559926

        顺便提一句,虽然半加器的基本电路是上面这个样子的,但是在FPGA中,因为只有查找表LUT没有具体的门电路,所以如果用FPGA来综合半加器,它的电路应该是这个样子的(因为只有2个输入和2个输出,所以只要1个LUT6就可以覆盖到所有情况):

如果你不了解LUT,可以看看这篇文章:从底层结构开始学习FPGA(2)----LUT查找表

或者看看这个专栏:从底层结构开始学习FPGA

image-20240423202331743

        IBUF和OBUF是Vivado自动添加的对输入输出管脚的缓冲,尽管上图显示的是2个LUT2,但是实际上就是1个LUT6,只是这样的显示会更清晰一点。下面的资源显示情况证明了这一点:

image-20240423202457190

        用verilog实现半加器的方式有两种:

  • 用逻辑表达式来描述输出

  • 直接写加法

        因为电路非常简单,所以这两种方法综合出来的电路都是一样的(上面说了,就是1个LUT6)。第1种方法:

  1. //使用逻辑表达式来描述半加器
  2. module half_adder(
  3. input in1, //加数1
  4. input in2, //加数2
  5. output sum, //
  6. output cout //进位
  7. );
  8. //根据化简结果分别表示:和 与 进位
  9. assign sum  = in1 ^ in2;
  10. assign cout = in1 & in2;
  11. endmodule

        第2种方法:

  1. //直接使用加法(assign语句)进行计算
  2. module half_adder(
  3. input in1, //加数1
  4. input in2, //加数2
  5. output sum, //
  6. output cout //进位
  7. );
  8. //使用拼接运算符分别表示:和 与 进位
  9. assign {cout,sum} = in1 + in2;
  10. endmodule
  11. //使用always块
  12. module half_adder(
  13. input in1, //加数1
  14. input in2, //加数2
  15. output  reg sum, //加和
  16. output reg cout //进位
  17. );
  18. //使用拼接运算符分别表示:和 与 进位
  19. always@(*)begin
  20. {cout,sum} = in1 + in2;
  21. end
  22. endmodule

        上面分别用always语句和assign语句来描述半加器加法,但效果上二者是等价的,对于这种比较简单又比较少的语句描述,建议使用assign语句

        有了RTL,接下来就该要写1个对应的TB来测试电路功能是否正常。由于这个电路足够简单(一共只有4种情况),所以我们可以把所有可能的情况都穷举出来,然后观察输出是否符合预期即可。TB如下:

  1. `timescale 1ns/1ns //时间刻度:单位1ns,精度1ns
  2. module tb_half_adder();
  3. //定义变量
  4. reg in1;
  5. reg in2;
  6. wire cout;
  7. wire sum;
  8. //设置初始化条件
  9. initial begin
  10. //1种情况
  11. in1 =1'b0; //初始化为0
  12. in2 =1'b0; //初始化为0
  13. #10
  14. //2种情况
  15. in1 =1'b0;
  16. in2 =1'b1;
  17. #10
  18. //3种情况
  19. in1 =1'b1;
  20. in2 =1'b0;
  21. #10
  22. //4种情况
  23. in1 =1'b1;
  24. in2 =1'b1;
  25. #10 $stop(); //结束仿真
  26. end
  27. //例化被测试模块
  28. half_adder u_half_adder(
  29. .in1 (in1),
  30. .in2 (in2),
  31. .sum (sum),
  32. .cout (cout)
  33. );
  34. endmodule

        仿真结果如下:

image-20240423203701013

        通过和真值表的对比(或者验证逻辑表达式也可以),可以发现,电路的输出是符合预期的。

全加器

        虽然半加器可以实现2个1bit数的加法,但在实际应用中,更常见的是要实现多个bit的加法,那么该如何实现?以2个2bits数的加法为例:

先把低位和高位的加法先分开。

低位是2个1bit的加法,所以可以用1个HA(半加器)来实现,它产生的和就是最终结果的低位,它产生的进位要被送入到高位参与它们的加法。

高位除了要计算2个加数的高位外,还有1个来自低位的进位。

        问题是半加器没有设计来自低位的进位,所以它处理不了这种情况。为此,全加器被设计出来了,它在半加器的基础上,增加了来自低级的进位输入。这样多个全加器就可以级联起来实现多bits的加法了。

        全加器Full Adder),它将2个1bit的输入和来自低级的进位输入共3个数相加,输出一个2bits的和,和的范围为0~3(10进制)。和的高位也被称为进位Carry),和的低位则通常被直接叫Sum)。例如:

  • 1 + 1 + 1 = 3 = 11,即进位carry是1,和sum是1

  • 1 + 0 + 1 = 2 = 10,即进位carry是1,和sum是0

  • ·····

  • 0 + 1 + 0 = 1 = 01,即进位carry是0,和sum是1

  • 0 + 0 + 0 = 0 = 00,即进位carry是0,和sum是0

        3个输入一共只有8种情况,把所有情况都穷举出来,就可以列出全加器的真值表:

加数1加数2低位进位结果高位进位
abcinsumcout
00000
01010
10010
11001
00110
01101
10101
11111

        从这个真值表,不难推断出两个输出的逻辑表达式:

  • 和sum为1的4种情况,ab'cin' + a'bcin' +ab'cin + a'bcin = (ab'+ a'b)cin' + (ab'+ a'b)'cin = (a^b)cin' + (a^b)'cin = (a^b)^cin = a ^ b ^ cin

  • 进位cout为1的4种情况,abcin' + a'bcin + ab'cin + abcin = ab(cin + cin') + cin(a'b + ab') = ab + cin(a^b)

        有了逻辑表达式后,就很容易画出电路图了:

image-20240423220322126

        同样的,虽然全加器的基本电路是上面这个样子的,但是在FPGA中,因为只有查找表LUT没有具体的门电路,所以它的电路其实是这个样子的(因为只有3个输入和2个输出,所以只要1个LUT6就可以覆盖到所有情况):

image-20240423214526562

        尽管显示的也是2个LUT2,但实际上就是1个LUT6。同半加器一样,全加器的Verilog实现也可以用2种方式:

  • 用逻辑表达式来描述输出

  • 直接写加法

        因为电路非常简单,所以这两种方法综合出来的电路是一样的。第1种方法:

  1. //根据逻辑表达式来描述输出
  2. module full_adder(
  3. input a, //加数1
  4. input b, //加数2
  5. input cin, //低位向高位的进位
  6. output sum, //
  7. output cout //进位
  8. );
  9. assign sum  = a ^ b ^ cin;
  10. assign cout = (a & b) | cin & (a ^ b);
  11. endmodule

        第2种方法:

  1. //直接用加法来描述全加器
  2. module full_adder(
  3. input a, //加数1
  4. input b, //加数2
  5. input cin, //低位向高位的进位
  6. output sum, //
  7. output cout //进位
  8. );
  9. assign {cout,sum} = a + b + cin; //使用位拼接 和 加法运算
  10. endmodule

        接下来,也写1个TB来测试电路,因为输入一共只有8个,所以依然用穷举法来测试:

  1. `timescale 1ns/1ns //时间刻度:单位1ns,精度1ns
  2. module tb_full_adder();
  3. //定义变量
  4. reg a;
  5. reg b;
  6. reg cin;
  7. wire cout;
  8. wire sum;
  9. //设置初始化条件
  10. initial begin
  11. //1种情况
  12. a =1'b0;
  13. b =1'b0;
  14. cin =1'b0;
  15. #10
  16. //第2种情况
  17. a =1'b0;
  18. b =1'b1;
  19. cin =1'b0;
  20. #10
  21. //3种情况
  22. a =1'b1;
  23. b =1'b0;
  24. cin =1'b0;
  25. #10
  26. //第4种情况
  27. a =1'b1;
  28. b =1'b1;
  29. cin =1'b0;
  30. #10
  31. //5种情况
  32. a =1'b0;
  33. b =1'b0;
  34. cin =1'b1;
  35. #10
  36. //第6种情况
  37. a =1'b0;
  38. b =1'b1;
  39. cin =1'b1;
  40. #10
  41. //7种情况
  42. a =1'b1;
  43. b =1'b0;
  44. cin =1'b1;
  45. #10
  46. //第8种情况
  47. a =1'b1;
  48. b =1'b1;
  49. cin =1'b1;
  50. #10 $stop(); //结束仿真
  51. end
  52. //例化被测试模块
  53. full_adder u_full_adder(
  54. .a (a),
  55. .b (b),
  56. .sum (sum),
  57. .cin (cin),
  58. .cout (cout)
  59. );
  60. endmodule

        仿真结果如下所示:

image-20240423215226332

        通过和真值表的对比(或者验证逻辑表达式也可以),可以发现,电路的输出是符合预期的。

用半加器实现全加器

        如果你仔细看半加器和全加器的电路图,就会发现它们有很多重合的地方:

  • 半加器的组成:1个与门 + 1个异或门

  • 全加器的组成:2个与门 + 2个异或门 + 1个或门

        这么看,全加器似乎可以用2个半加器 + 1个或门组成,我们把全加器的电路图重新布局一下:

image-20240423221247345

        可以清晰地看到,全加器确实可以由2个半加器+1个或门组成:

  • 加数a和b作为第1个半加器的输入

  • 第1个半加器的输出sum1 和 进位输入cin作为第2个半加器的输入;第1个半加器的输出carry1作为或门的1个输入

  • 第2个半加器的输出sum2就是全加器的和sum; 第2个半加器的输出carry2作为或门的另1个输入

  • 或门的输出cout就是全加器的进位cout

        用Verilog来描述是这样的:

  1. //2个半加器级联实现全加器
  2. module full_adder(
  3. input a, //加数1
  4. input b, //加数2
  5. input cin, //低位向高位的进位
  6. output sum, //
  7. output cout //进位
  8. );
  9. //模块之间的连线,结合模块图理解
  10. wire hf1_cout; //1个半加器的进位输出
  11. wire hf2_cout; //2个半加器的进位输出
  12. wire hf1_sum; //1个半加器的和输出
  13. assign cout = hf1_cout || hf2_cout;
  14. //例化第1个半加器
  15. half_adder u1_half_adder(
  16. .a (a),
  17. .b (b),
  18. .sum (hf1_sum),
  19. .cout (hf1_cout)
  20. );
  21. //例化第2个半加器
  22. half_adder u2_half_adder(
  23. .a (hf1_sum),
  24. .b (cin),
  25. .sum (sum),
  26. .cout (hf2_cout)
  27. );
  28. endmodule
  29. module half_adder(
  30. input a, //加数1
  31. input b, //加数2
  32. output sum, //
  33. output cout //进位
  34. );
  35. //使用拼接运算符分别表示:和 与 进位
  36. assign {cout,sum} = a + b;
  37. endmodule

        生成的RTL视图如下:

image-20240423222355211

        这与理论上的框图一致:例化了2个半加器和1个或门。仿真的话用上面的同一个TB就行,仿真结果也和之前的结果一致:

image-20240423222613520

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/874683
推荐阅读
相关标签
  

闽ICP备14008679号