在线咨询
eetop公众号 创芯大讲堂 创芯人才网
切换到宽版

EETOP 创芯网论坛 (原名:电子顶级开发网)

手机号码,快捷登录

手机号码,快捷登录

找回密码

  登录   注册  

快捷导航
搜帖子
楼主: rosshardware

[原创] 数字典型电路知识结构地图,请大家参考,也希望积极补充!

[复制链接]
 楼主| 发表于 2018-9-6 14:15:09 | 显示全部楼层
本帖最后由 rosshardware 于 2018-9-6 15:09 编辑

5. 无符号乘法器
与无符号加法类似,无符号乘法器也要求两边的乘数是无符号的,一旦有一方为有符号数,则整个结果为有符号数,否则综合会出现不可预知的结果。与无符号加法不同的是,无符号的乘法,乘积结果位宽为两个乘数位宽相加,而非乘数最大位宽+1,其实从原理上是比较容易理解的,因为二进制乘法,就是几组二进制加法移位的结果,例如:
               1101              4位
   *            110              3位
---------------------------------
               0000
   +        1101
   +      1101
----------------------------------
          1001110             7位

乘法进行Verilog 编写,以前综合工具不是很优化,不能解析*,一般采用例会标准单元的方式,完成乘法运算:
传统古老方式Verilog 无符号乘法写法:

        localparam   A_WIDTH;
        localparam   B_WIDTH;
        localparam   PRDCT_WIDTH = A_WIDTH + B_WIDTH;
        
        reg [A_WIDTH-1:0]                 a;  // Default declaration type is unsigned
        reg [B_WIDTH-1:0]                 b;  // Default declaration type is unsigned
     
        wire [PRDCT_WIDTH-1:0]         prdct;
      
        DW02_MULT #(
                           .A_WIDTH       (A_WIDTH         ),
                           .B_WIDTH       (B_WIDTH         )
                          )
         U_DW_MULT
                          (
                           .TC                 (1'b0                 ), // 0 for unsigned, 1 for signed                       
                           .A                   (a                     ),
                           .B                   (b                     ),
                           .PRODUCT       (prdct               )
                          );

随着工具不断优化,包括Synplify也被synopsys收购后,FPGA综合工具也支持*乘法识别,只需要代码中申明乘法参数的符号属性既可。

推荐乘法运算Verilog 代码:
        localparam   A_WIDTH   =  8;
        localparam   B_WIDTH   =  16;
        localparam   PRDCT_WIDTH = A_WIDTH + B_WIDTH;
        
        reg unsigned [A_WIDTH-1:0]                a;  // Default declaration type is unsigned
        reg unsigned [B_WIDTH-1:0]                b;  // Default declaration type is unsigned
     
        reg unsigned [PRDCT_WIDTH-1:0]         prdct;

        always@(*) begin
              prdct = a * b;
        end
乘法不用显示把a和b位宽扩位到A_WIDTH+W_WIDTH,只要prdct 定义位宽为A_WIDTH+B_WIDTH,工具就不会报错。

以上讲解的是乘法器两边都是变量信号的无符号乘法运算,对于一个变量,一个常量的无符号运算,需要注意一下几点:
1. 常数的位宽要定义清楚;
2. 常数的符号类型要显示定义为无符号;
3. 对于常数无论是是否2的整数次幂,均按照* 写,不需要自己优化移位,因为综合的优化效果,不会比手动移位差。
示例:
        localparam                                            A_WIDTH   =  8;
        localparam                                            B_WIDTH   =  8;
        localparam   unsigned [B_WIDTH -1 : 0]  B              =  32;
        localparam                                             PRDCT_WIDTH = A_WIDTH + B_WIDTH;
        
        reg unsigned [A_WIDTH-1:0]                   a;  // Default declaration type is unsigned

     
        reg unsigned [PRDCT_WIDTH-1:0]         prdct;

        always@(*) begin
              prdct = a * B;
        end

强烈不推荐:
        always@(*) begin
              prdct = a << 5;
        end
原因: 1. 代码可扩展性上讲,后续常数B的值变化不是2的5次方,或者说不是2的整数次幂,这个地方就需要修改为*
         2.  代码可读性上讲,推荐的方式容易看懂,就是两个数相乘,不推荐的方式,还需要推敲一下,这行代码功能
         3.  代码可控性上讲,a往作移位,低位补0,还是补1,还是补a的最低位,工具都可以有不同理解,所以不同工具可能理解会不一样
         4.  两边位宽还不匹配,语法检查工具也会报Warning


顺便讲解一下这个章节代码规范一些细节:
1.  信号定义和申明,一行对应一个信号,不要多个信号定义在一行,否则修改其中一个信号,可能会影响其他信号,另外一行太长,也影响阅读,不建议定义方式:
              localparam   A_WIDTH = 8,B_WIDTH = 16;

2.  对于模块例化,建议按照名字进行例化,不要按照位置进行例化,否则被例化模块端口有修改,例化的上层文件就要重新修改,即不建议这样的例化代码风格:
          DW_MULT #(
                           A_WIDTH         ,
                           B_WIDTH         
                          )
         U_DW_MULT
                          (
                          1'b0                 ,
                           a                     ,
                           b                     ,
                           prdct               
                          );
     甚至很多教科书上的这种写法,可维护性更差,就更不推荐了哈:
       DW_MULT #( A_WIDTH ,  B_WIDTH ) U_DW_MULT(1'b0,a,b,prdct);
     原因很简答,如果a和b位置搞反了,a和b的位宽又不一样,就可能会报错,能够报错都算是不坏结果,就怕语法检查不跑错,最后仿真出错,定位问题会浪费较长时间。

3.  注意一下语法,例化赋值,或者用assign赋值的信号,我们定义为wire,在 always 块中的变量,无论是组合逻辑还是时序逻辑,都需要定义为reg。比如上面例子当中的prdct,第一个写法是通过例化模块得到值,所以定义为wire,第二个写法是在always块中得到,所以定义为reg。这个就是语法规定,没有什么理由,大家记住就行,否则工具就会报语法错误。

4. 在always 块中,组合逻辑采用非阻塞赋值 =, 时序逻辑采用阻塞赋值 <= ,具体原因,这里先不表述,前面章节主要讲组合逻辑,等讲到时序逻辑章节,会详细阐述原因,大家先有这个一个印象即可。

5.常数乘法给大家引申的一个写代码原则,尽量按照功能或者代码行为去写,只要是可综合风格即可,切忌自己觉得自己很聪明,对电路进行电路级的优化。这样会影响代码的可读性,扩展性以及可控性,同时现在综合工具优化功能很强,大家不必担心得不到最好的PPA(Power,Performance,Area),而且大家进行代码风格选择时,考虑的也不光是PPA这几个维度,也要从可以实现性,复杂度,可阅读星,开发周期多方面去考量。
发表于 2018-9-6 15:58:19 | 显示全部楼层
学习,很好的东西,感谢楼主的无私奉献!
发表于 2018-9-10 07:51:58 | 显示全部楼层
期待楼主继续 mark一下
 楼主| 发表于 2018-9-10 21:08:18 | 显示全部楼层
不好意思,最近有点忙,没有来得及补充,希望这周有时间能够把除法器以及有符号处理讲解一下
发表于 2018-9-11 09:25:41 | 显示全部楼层
顶!!!持续关注ing
发表于 2018-9-11 14:21:21 | 显示全部楼层
好文章,支持支持~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 楼主| 发表于 2018-9-11 15:56:49 | 显示全部楼层
对于除法的实现,相对于加减乘要麻烦一些。当然目前除法主要支持无符号数除法,我们分为两类进行介绍,一类是被除数是变量,即a/b这种,一类是被除数是常量,即a/B这种。
1. 被除数常量,方法一:长除法,即根据二进制手算除法,每次将被除数左移一位,每个周期得到一位商
    比如 11/4 = 2 于 3

           1011               --->11            
     -     100                ---->4
------------------------------------
           0011              101 > 100 商最高位1, 余数 0011,将0011左移一位
           011
     -     100                011 < 100 商次高位为0, 余数为011
  最终结果上为2'b10, 余数为011
  需要注意一点,如果除数的高位为0,则需要对被除数高位补0,比如1111/001 (15/1)
  由于001的bit2和bit1为0,因此1111需要补位为001111当作被除数,进行运算
   001111
- 001                                              商的bit3为1
------------------------------------
   0001
-    001                                            商的bit2为1
-------------------------------------
     0001
-      001                                          商的bit1为1
-------------------------------------
       0001
-        001                                        商的bit0为1
---------------------------------------
         000                                         余数为0

为了实现简便,我们对被除数的扩位进行归一化,统一扩位到被除数位宽+除数位宽,得到商取低位的被除数位宽即可。


根据这个思路,Verilog代码示意如下:

module SHIFT_DIV #(
                              parameter DIVIDEND_WIDTH = 16,
                              parameter DIVISOR_WIDTH  = 8,
                              parameter QUOTIENT_WIDTH = DIVIDEND_WIDTH,
                              parameter REMAINDER_WIDTH = DIVISOR_WIDTH - 1  
                             )
                            (
                              input                                                         clk_sys,
                              input                                                         rst_sys_n,
                              input                                                         div_strt,
                              input                                                         div_clr,
                              input  [DIVIDEND_WIDTH-1 : 0]                  dividend,
                              input  [DIVISOR_WIDTH-1 : 0]                    divisor,
                              output reg                                                 div_end,
                              output reg [QUOTIENT_WIDTH -1:0]           quotient,
                              output reg [REMAINDER_WIDTH-1:0]          remainder
                             );

    localparam  DIV_CNT_WIDTH = log2(QUOTIENT_WIDTH) + 1'b1;
    localparam  LSF_REG_WIDTH = DIVIDEND_WIDTH+DIVISOR_WIDTH;
/////////////////////////////////////////////////////////////////////////////////
    reg                                         div_cnt_en;
    reg [DIV_CNT_WIDTH-1 : 0]     div_cnt;

    reg [LSF_REG_WIDTH-1 : 0]   lsf_dividend;
   
    reg [DIVISOR_WIDTH-1 + 1 : 0]     sub_dividend_divsor;
     

/////////////////////////////////////////////////////////////////////////////////
    wire [DIVISOR_WIDTH-1 : 0]   divivend_cut;


////////////////////////////////////////////////////////////////////////////////////
//Generate counter to control calculation cycle
   always @(posedge clk_sys or negedge rst_sys_n) begin
        if (rst_sys_n == 1'b0) begin
              div_cnt_en <= 1'b0;
        end
        else begin
              if ((div_clr == 1'b1) ||
                  (div_cnt >= (QUOTIENT_WIDTH))) begin
                   div_cnt_en <= 1'b0;
              end
              else if (div_strt == 1'b1)begin
                    div_cnt_en <= 1'b1;
              end
        end
  end
  always @(posedge clk_sys or negedge rst_sys_n) begin
        if (rst_sys_n == 1'b0) begin
              div_cnt <= {DIV_CNT_WIDTH{1'b0}};
        end
        else begin
              if ((div_clr == 1'b1) || (div_strt == 1'b1) ||
                  (div_cnt >= (QUOTIENT_WIDTH))) begin
                  div_cnt <= {DIV_CNT_WIDTH{1'b0}};
              end
              else if (div_cnt_en == 1'b1)begin
                    div_cnt <= div_cnt + 1'b1;
              end
        end
  end


    assign  divivend_cut = lsf_divivend[DIVIDEND_WIDTH-1 -: DIVISOR_WIDTH];

    always @(*) begin
          sub_dividend_divsor = {1'b0,divivend_cut } - {1'b0, divisor};
    end

    always @(posedge clk_sys or negedge rst_sys_n) begin
          if (rst_sys_n == 1'b0) begin
                lsf_dividend <= {LSF_REG_WIDTH{1'b0}};
          end
          else if ((div_strt == 1'b1) || (div_clr == 1'b1))begin
                lsf_dividend <= {{DIVISOR_WIDTH{1‘b0}},dividend};
          end
          else if (div_cnt_en == 1'b1) begin
                if (sub_dividend_divsor[DIVISOR_WIDTH+1] == 1'b1 ) begin
                      lsf_dividend <= {lsf_dividend[LSF_REG_WIDTH-2:0],1'b0};
                end
                else begin
                     lsf_dividend <= {sub_dividend_divsor[DIVISOR_WIDTH-2:0],
                                              lsf_dividend[LSF_REG_WIDTH-DIVIDEND_WIDTH-1:0],1'b0};
                end
          end
    end


  always @(posedge clk_sys or negedge rst_sys_n) begin
          if (rst_sys_n == 1'b0) begin
                quotient <= {QUOTIENT_WIDTH{1'b0}};
          end
          else if ((div_strt == 1'b1) || (div_clr == 1'b1))begin
                quotient <= {QUOTIENT_WIDTH{1‘b0}};
          end
          else if (div_cnt_en == 1'b1) begin
                if (sub_dividend_divsor[DIVISOR_WIDTH+1] == 1'b1 ) begin
                      quotient <= {quotient[QUOTIENT_WIDTH-2:0],1'b0};
                end
                else begin
                      quotient <= {quotient[QUOTIENT_WIDTH-2:0],1'b1};
                end
          end
    end

    always @(posedge clk_sys or negedge rst_sys_n) begin
          if (rst_sys_n == 1'b0) begin
                remainder <= {REMAINDER_WIDTH{1'b0}};
          end
          else if ((div_strt == 1'b1) || (div_clr == 1'b1))begin
                remainder <= {REMAINDER_WIDTH{1'b0}};
          end
          else if (div_cnt_en == 1'b1 && (div_cnt >= (QUOTIENT_WIDTH)) begin
                remainder <= sub_dividend_divsor[REMAINDER_WIDTH-1:0] ;
          end
    end

always @(posedge clk_sys or negedge rst_sys_n) begin
          if (rst_sys_n == 1'b0) begin
                 div_end <= 1'b0;
          end
          else if ((div_strt == 1'b1) || (div_clr == 1'b1))begin
                 div_end <= 1'b0;
          end
          else if (div_cnt_en == 1'b1 && (div_cnt >= (QUOTIENT_WIDTH)) begin
                 div_end <= 1'b1 ;
          end
          else begin
                div_end <= 1'b0;
          end
    end

endmodule
 楼主| 发表于 2018-9-11 16:09:13 | 显示全部楼层
本帖最后由 rosshardware 于 2018-9-11 16:13 编辑

好像我写的二进制移位除法的帖子还是审核中,大家耐心等待一下。

上述二进制移位除法,最大的问题就是运算的Cycle会随被除数位宽增加而增加,因此在除法运算延时要求较高场景,可以使用DW的除法器,DW的除法器包括支持流水插拍的版本,可以帮助提升工作时钟频率。

几种除法器在统一工艺,按照500MHz目标综合,52bits/25bits,数据对比:
      除法器                         面积(um2)             最大Slack           工作时钟周期
  二级制移位除法                    1000                        -0.4                       53
     DW_div                          43692                      -15.42                    2
     DW_div_pipe                  50008                      -4.386                    4
     DW_div_seq                   11980                      -1.0348                  10  

综上,大家根据自己实际需求进行除法器实现策略。
 楼主| 发表于 2018-9-11 16:52:34 | 显示全部楼层
接下来讲讲,除数为常数的除法实现,当然除数为变量的方式是兼容常数的运算,只是针对常数运算,通常也有几种方法,供大家参考。
方法一:  把除数转化为小数,采用乘法运算,比如:
   a[15:0] / 8'd24;
   十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又 得到一个积,再将积的整数部分取出,如此进行,直到积中的整数部分为零,或者整数部分为1,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
  然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。 
  例如:0.7=(0.1 0110 0110...)B
  0.7*2=1.4========取出整数部分1
  0.4*2=0.8========取出整数部分0
  0.8*2=1.6========取出整数部分1
  0.6*2=1.2========取出整数部分1
  0.2*2=0.4========取出整数部分0 
  0.4*2=0.8========取出整数部分0
  0.8*2=1.6========取出整数部分1
  0.6*2=1.2========取出整数部分1
  0.2*2=0.4========取出整数部分0

     1/24 = 0.04167 = 00001010
     0.04167*2 = 0.08334  ========取出整数部分0
     0.08334*2 = 0.16698  ========取出整数部分0
     0.16698*2 = 0.33336  ========取出整数部分0
     0.33336*2 = 0.66672  ========取出整数部分0
     0.66672*2 = 1.33344  ========取出整数部分1
     0.33344*2 = 0.66688  ========取出整数部分0
     0.66688*2 = 1.33376  ========取出整数部分1
     0.33376*2 = 0.66752  ========取出整数部分0
   
     a/8'd24 = (a * (00001010)) >> 8

方法二: 现在DC综合工具对于/ 在常数运算时,是可以识别,并且优化效果与方法一相当,所以可以采用
quotient = a/B的方式进行常数除法运算。

方法三: 对于被除数除数范围比较小的情况,可以采用查找表的方式,比如被除数为a[2:0] 除数为3,则通过查找表方式完成,Verilog示例如下:

            always @(*) begin
                  case(a)
                     3'h0          :   quotient = 3'h0;
                     3'h1          :   quotient = 3'h0;
                     3'h2          :   quotient = 3'h0;
                     3'h3          :   quotient = 3'h1;
                     3'h4          :   quotient = 3'h1;
                     3'h5          :   quotient = 3'h1;
                     3'h6          :   quotient = 3'h1;
                     3'h7          :   quotient = 3'h1;
                     default      :    quotient = 3'h0;
                  endcase
            end
发表于 2018-9-13 09:34:39 | 显示全部楼层
楼主厉害了,持续关注中
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

站长推荐 上一条 /2 下一条

×

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-4-26 09:08 , Processed in 0.036634 second(s), 6 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
快速回复 返回顶部 返回列表