IC大牛10多年的设计分享:数字典型电路知识结构地图及代码实现

来源:EETOP 论坛   作者:rosshardware

链接:http://bbs.eetop.cn/thread-768668-1-1.html

以下是我过去10多年设计过的电路罗列总结,请大家补充,如果大家有兴趣(请反馈),后续可以针对每个点单独进行详细的阐述。

同步电路设计:

数学运算&数字信号处理类:

无符号运算-比较,加,减,乘,除

有符号运算符号-比较,加,减,乘,除,复数加法,复数乘法

绝对值,最大值,最小值

饱和,截位运算

NCO

滤波器

AGC

上变频,下变频

上采样,下采样

削波

DPD

QMC

FFT

LDPC

RS

维特比

控制类逻辑电路:

与或非,选择器,译码器

计数器

状态机

移位控制器

拼位设计

Leading One,Leading zero

握手控制

同步FIFO

仲裁调度(RR,WRR,WFQ)

流量整形(shaping)

报文头同步

BITMAP

乒乓流水设计

配置寄存器设计(RW,RO,RC,WC,W1_PULSE)

共享RAM的链表设计

异步电路设计

单bits异步处理

打三拍

异步握手

多bits异步处理

D-MUX

格雷码

异步FIFO

SOC系统集成相关设计

系统级顶层设计:

CRG设计

低功耗设计&Power Domain 规划

IO 复用(IO MAPPING)&排布

地址空间划分(Memory Maping)

Paper Floorplan

系统控制器设计

核集成:

ARM  Cortex A系列

ARM  Cortex R系列

ARM  Cortex M系列

      存储系统

SRAM,ROM,Flash

DDR

      总线

AMBA AXI,AHB,APB

AMBA ACE

      外设&加速器

DMA

PCIE

USB

MPI

NANDC

NORC

LOCAL BUS

UART

I2C

SPI

JTAG

TIMER

RTC

WDT

     模拟IP

ADC,DAC

TSENSOR

USB PHY

DDR PHY

Serdes

知识结构地图-同步电路设计-运算类电路设计-无符号加法

学习加法运算之前,先谈几个概念:

知识结构地图-同步电路设计-运算类电路设计-无符号加法

学习加法运算之前,先谈几个概念:

1. 有符号和无符号

说到运算,我们首先介绍一下无符号和有符号数在数字电路的二进制表示方法,MSB(Most Significant Bit)代表最高位,LSB(Least Significant Bit)代表最低位。

在二进制运算里面,无符号数即所有bits位都代码实际的数据内容,dec代表十进制,计算公式:

Value(dec)=(2^MSB)*bit(MSB)+(2^MSB-1)*bit(MSB-1) +  ....+ (2^0)*bit0

有符号数通常会把MSB当作符号位,0代表正数,1代表负数,其余MSB-1 ~ 0 当作实际数据内容的补码,当符号位为0,实际值=补码值,当符号位为1,实际值=2^符号位bit位-补码值,计算公式:

Value(dec) = (MSB == 1'b0) ?

(2^MSB-1)*bit(MSB-1) +  ....+ (2^0)*bit0   :

-1* ((2^MSB)*bit(MSB)- ((2^MSB-1)*bit(MSB-1) +  ....+ (2^0)*bit0))

以3bits的二进制数为例,示意分别代表有符号数和无符号数的值:

小结一下,对于一个3bits的二级制数,如果代表无符号数,则表示范围为0~7, 如果表示有符号数,则表示范围为-4~3,即对于相同位宽的二进制数据,如果是无符号数,则能够表示范围为0~(2^MSB)-1, 如果是有符号数,则范围为-2^(MSB-1) ~ (2^(MSB-1)) -1, 由此可见,无符号数的范围是非对称的,即最小的复数值绝对值不等于最大整数的绝对值。

2. 定点数据和浮点数据

数字信号处理的输入源通常都是物理世界的模拟信号,其电平的表示是连续,数字处理会对其进行抽样,在算法阶段,会按照浮点运算的方式进行算法性能仿真,以便于评估最优性能边界。但是由于浮点运算硬件实现代价较大,且算法进行定点化以后的性能劣化通常也在实际使用可以接受范围,因此实际工程实现通常都采用定点化方式,实现算法链路。

浮点格式可以参考IEEE 754,由于实际工程使用不多,因此这里不做过多叙述,主要介绍定点的方法:

在定点数中,定义小数点的位置,把一个定点数分为两个部分,小数点左边部分的位宽为整数位宽,右边部分为小数位宽,小数点右边为0~1之间的小数,小数位宽则代表精度,比如(16,4)表示定点数位宽为16,整数位宽为4,小数位宽为12。当然可能不同公司会有不同定义,但是小数位宽和整数位宽的概念是相同的。

以(4,2) 为例,4位位宽,2位小数位

bit3       bit2      bit1       bit0

0            1           1           0

整数位   整数位  小数位    小数位

bit1      bit0      bit-1        bit-2

value(dec) = (2^1) * 0 + (2^0)* 1 + (2^-1) * 1 + (2^-2) * 0 = 1.5

3. 无符号二进制加法

无符号二进制加法,需要保证两个相加的加数均为无符号数,如果有一个位有符号数,则均为有符号运算,结果为有符号数,即对于减法来讲,不存在无符号减法。

无符号A+无符号B = 无符号C

无符号A+有符号B = 有符号C

有符号A+有符号B = 有符号C

有符号A+有符号B = 有符号C

二进制加法,动态范围会增加,精度保持不变,因此加法的结果需要扩一位,用于存放进位。

1011.1000            =》 8位

+  0101.1101            =》 8位

------------------

10001.0101            =》 9位

无符号加法Verilog 编码实现

localparam A_WIDTH = 16;

localparam B_WIDTH = 8;

// Sumation result width should be 1 bit more than biggest widht of adder factor

localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;

reg [A_WIDTH-1  : 0]      a;

reg [B_WIDTH-1  : 0]      b;

reg [C_WIDTH-1  : 0]      c;

always @(*) begin

c = {1'b0,a} + {1'b0,{A_WIDTH-B_WIDTH{1'b0}},b};

end

无符号加法编码要点:

1. 和c需要定位位宽比加数最大位宽大1位;

2. 加数a和b需要扩展位宽,扩展到与c位宽相等,且扩展位补0,否则有很多语法检查工具会报位宽不匹配错误,同时不同工具理解不一致,如果自动补1或者补最高位,就功能出问题了;

4. 无符号比较器

无符号比较器,需要比较两边信号均为无符号类型,同时如果位宽不匹配,需要扩展位宽进行匹配,对于无符号数,扩展位补0即可。当然,Synposys,推荐的时候,在定义信号时,把信号符号类型定义清楚,默认定位为无符号,可以不作位宽匹配,工具自动优化。不过本人还是建议,按照位宽扩位方式进行代码编写,一个是电路表达最清晰和可控,不依赖于工具的理解,因为可能synopsys综合最优,但可能FPGA综合就有问题,二个是,作工具语法检查,可以省去很多位宽不匹配的Warning的检查,防止Warning过多,检查疏忽,反而把真正有位宽匹配的问题漏掉。

个人推荐无符号比较器 Verilog写法:

localparam   A_WIDTH;

localparam   B_WIDTH;

reg [A_WIDTH-1:0]  a;  // Default declaration type is unsigned

reg [B_WIDTH-1:0]  b;  // Default declaration type is unsigned

reg                          c;

// A_WIDTH is bigger than B_WIDTH

always @(*) begin

if (a > {{(A_WIDTH-B_WIDTH){1'b0}},b}) begin

c = 1'b1;

end

else begin

c = 1'b0;

end

end

sysnopsys 推荐写法:

localparam   A_WIDTH;

localparam   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                          c;

// A_WIDTH is bigger than B_WIDTH

always @(*) begin

if (a > b) begin

c = 1'b1;

end

else begin

c = 1'b0;

end

end

顺便再啰嗦一下,无不会单独讲Verilog编码规范和技巧,但是都会融入到我讲解的每个电路实现的列子里面。比如在无符号的加法和比较里面,我想传递给大家编码规范是:

1. 尽量参数化,这样便于代码的IP化,我们写的代码,后续如果有位宽变化的应用,只需要例化时更改参数即可,不需要大规模的修改代码,可以减少重复工作量,同时也减少犯错误,埋Bug的机会。

2. 代码要整洁,清晰易懂,行与行之间要有间隔,可以间隔4个Space,也可以2个Space,这个根据自己审美以及各个公司的要求来定。

3. 组合逻辑,采用Verilog 2001语法,即always @(*),  Verilog 95 写组合语法,很多IP,尤其老外的IP,还用的这种语法,不推荐,因为需要把敏感信号列表写全,往往有时候笔误容易写漏,而且代码有修改,也可能忘记把新增信号加到信号敏感列表。 后续针对新的电路类型,给家讲新的代码编码规范和要求。

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这几个维度,也要从可以实现性,复杂度,可阅读星,开发周期多方面去考量。

对于除法的实现,相对于加减乘要麻烦一些。当然目前除法主要支持无符号数除法,我们分为两类进行介绍,一类是被除数是变量,即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

上述二进制移位除法,最大的问题就是运算的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

综上,大家根据自己实际需求进行除法器实现策略。

接下来讲讲,除数为常数的除法实现,当然除数为变量的方式是兼容常数的运算,只是针对常数运算,通常也有几种方法,供大家参考。

方法一:  把除数转化为小数,采用乘法运算,比如:

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

网友提问:

这个无符号加法在实现功能的时候,没有考虑a和b的那个数据位宽大,在进行功能实现的时候是不是应该考虑进去?不能直接默认a比较大把?

回答:

是要考虑进去,在我写的Demo 代码里面是考虑了的哈,参考第4行,当然实现的代码,是可以优化,作为兼容A和B任意的位宽为最大的情况,我更新一下,参见第12行:

和C_WIDTH是根据A和B的位宽比较后决定的,如果A > B,则是A_WIDTH+1,否则就是B_WIDTH+1

1    localparam A_WIDTH = 16;
2    localparam B_WIDTH = 8;
3  // Sumation result width should be 1 bit more than biggest widht of adder factor
4    localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;
5
6   reg [A_WIDTH-1  : 0]      a;
7   reg [B_WIDTH-1  : 0]      b; 
8   
9    reg [C_WIDTH-1  : 0]      c; 
10
11    always @(*) begin
12          c = {{(C_WIDTH-A_WIDTH){1'b0}},a} + 
                   {{C_WIDTH-B_WIDTH{1'b0}},b};
13    end

今天来讲讲有符号数的加法,从无符号的加法章节就提及过,只要加数有一方为有符号数,则和一定是有符号数,重点强调一下,大家千万不要从场景上分析,认为C = A+B一定是>0,则及时A和B有一个是有符号数,那么和就是无符号数,我们只能从电路结构上决定C是无符号,还是有符号,原因是,大家场景分析,往往只是从正常功能场景分析,而忽略了异常场景。 比如

A[1:0]:作为无符号数

2     |        .                    .

1     |   .         .          .

0  —|.——————.————————

|0 1   2   3   4    5   6

B[1:0]:作为有符号数

2     |

1     |.                       .

0  —|——.————.————————

-1     |             .

0     1     2    3    4

C正常为:正常场景,C被当做无符号数,没有问题,与有符号数值一样。

2     |

1     |.    .      .     .     .

0  —|——————————————

-1    |

0     1     2    3    4

异常场景,或者说未来B[1:0]信号相位和幅度发生了变化

B[1:0]:作为有符号数

2     |

1     |            .

0  —|——.————.————————

-1     |.                       .

0     1     2    3    4

C[2:0]:作为有符号数(-4~3)波形

3     |            .

2     |

1     |     .             .

0  —|——————————————

-1     |.                        .

0     1     2    3    4

C[2:0]:作为无符号数(0~8)波形, 0,1 两个坐标点,3,4两个坐标点,就存在很大幅度跳变

7     |.                         .

6     |

5     |

4     |

3     |            .

2     |

1     |     .             .

0  —|——————————————

-1     |

0     1     2    3    4

所以C应该按照有符号处理,即便,从算法角度,希望C后续按照无符号进行后续计算处理,

也应该是做一个C的有符号到无符号转换,专访方式其实很简单就是,把C最高位取反,上面的里面即

C_UNSIGN = {~C[2],C[1:0]}

这样异常场景,C_UNSIGN的波形为,这样,只是增加直流分量,其幅度仍然没有变化:

7     |            .

6     |

5     |     .            .

4     |

3     |.                        .

2     |

1     |

0  —|——————————————

-1    |

0     1     2    3    4

上面小节,主要跟大家强调,进行有符号运算,其和一定是有符号的,按照电路结构进行设计,如果根据场景需要把和作为无符号数使用,需要单独进行有符号到无符号转换,这个是电路结构的转换,不是简单定一个$signed去转换类型。有符号加法的Verilog实现形式,推荐两种方式:

方式一:传统方式,手动扩位,实现左右位宽匹配,扩位为符号位,另外信号输入有符号数,一定要显示定义,Verilog默认不定义就是无符号类型
1    localparam A_WIDTH = 16;
2    localparam B_WIDTH = 8;
3  // Sumation result width should be 1 bit more than biggest widht of adder factor
4    localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;
5
6   reg signed [A_WIDTH-1  : 0]      a;
7   reg signed [B_WIDTH-1  : 0]      b; 
8   
9    reg signed [C_WIDTH-1  : 0]      c; 
10  reg unsigned [C_WIDTH-1  : 0]      c_unsigned; 
11
12   always @(*) begin
13         c = {(C_WIDTH-A_WIDTH){a[A_WIDTH-1]}},a} + 
                    {{C_WIDTH-B_WIDTH{b[B_WIDITH-1]}},b};
14    end
15 
16   always @(*) begin
17         c_unsigned = {~c[C_WIDTH-1],c[C_WIDTH-2:0]};
18   end

方式二: Synopsys推荐,直接定义好符号类型,和的位宽按照运算法则定义好,实际+地方不作位宽匹配,工具自动识别
1    localparam A_WIDTH = 16;
2    localparam B_WIDTH = 8;
3  // Sumation result width should be 1 bit more than biggest widht of adder factor
4    localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;
5
6   reg signed [A_WIDTH-1  : 0]      a;
7   reg signed [B_WIDTH-1  : 0]      b; 
8   
9    reg signed [C_WIDTH-1  : 0]      c; 
10  reg unsigned [C_WIDTH-1  : 0]      c_unsigned; 
11
12   always @(*) begin
13         c = a + b;
14    end
15 
16   always @(*) begin
17         c_unsigned = {~c[C_WIDTH-1],c[C_WIDTH-2:0]};
18   end   
另种方式,综合效果是一样,个人还是推荐方式一,虽然写代码时间多花一点,但是整个代码更干净整洁,后续工具检查的Warning少,便于从LOG中检查出真正位宽不匹配的点,否则有很多这种伪不匹配Warning,LOG查看会非常费劲。 另外,强调一点,代码的编写从来都不是我们集成电路设计真正的瓶颈,真正时间是用于场景分析,需要分析,数据流分析,电路实现。代码编写只是我们设计思路的映射,所以初学者切忌不要被一些语言工具厂商或者教科书忽悠,认为作集成电路就是写Verilog,花大量时间学习和记忆一些枯燥的语法,大家会从我给的Demo看到,RTL 设计实现用的Verilog 语法都非常简单。我们核心是作逻辑时序和电路实现。

忘了介绍有符号的比较器的实现,这里给补充一下,有符号比较,两边一定是有符号数,需要统一处理,上一节讲了有符号到无符号的转换,因此,我们可以通过把有符号数,转换成无符号数,然后进行无符号的比较,结果应该是一致的,当然,目前Synopsys的工具也非常先进,我们自动把数据定义为有符号数,在比较时候,加上系统函数$signed就可以自动实现有符号数的比较。Verilog Demo:

有符号比较器 Verilog写法一,(通过作有符号到无符号转换实现):
        localparam   A_WIDTH;
        localparam   B_WIDTH;        
        reg signed [A_WIDTH-1:0]  a;  // Default declaration type is unsigned
        reg signed [B_WIDTH-1:0]  b;  // Default declaration type is unsigned
     
        reg                          c;
       // A_WIDTH is bigger than B_WIDTH
       always @(*) begin
             if ((~a[A_WIDTH-1],a[A_WIDTH-2:0]} > {~b[B_WIDTH-1],b[B_WIDTH-2:0]}) begin
                   c = 1'b1;
             end
             else begin
                  c = 1'b0;
             end
       end

sysnopsys 推荐写法:
       localparam   A_WIDTH;
        localparam   B_WIDTH;
        
        reg signed [A_WIDTH-1:0]  a;  // Default declaration type is unsigned
        reg signed [B_WIDTH-1:0]  b;  // Default declaration type is unsigned
     
        reg                          c;
       // A_WIDTH is bigger than B_WIDTH
       always @(*) begin
             if ($signed(a) > $signed(b)) begin
                   c = 1'b1;
             end
             else begin
                  c = 1'b0;
             end
       end

下面在聊聊有符号减法,从电路结构上讲,只要涉及到减法,理论上其得到的结果就是应该是一个有符号数,所以大家按照这个原则进行设计就行,如果需要对结果作转换,进行有符号到无符号转换即可,Verilog代码也推荐两种风格:
方式一:传统方式,手动扩位,实现左右位宽匹配,扩位为符号位,另外信号输入有符号数,一定要显示定义,Verilog默认不定义就是无符号类型

1    localparam A_WIDTH = 16;
2    localparam B_WIDTH = 8;
3  // Sumation result width should be 1 bit more than biggest widht of adder factor
4    localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;
5
6   reg signed [A_WIDTH-1  : 0]      a;
7   reg signed [B_WIDTH-1  : 0]      b; 
8   
9    reg signed [C_WIDTH-1  : 0]      c; 
10  reg unsigned [C_WIDTH-1  : 0]      c_unsigned; 
11
12   always @(*) begin
13         c = {(C_WIDTH-A_WIDTH){a[A_WIDTH-1]}},a} - 
                     {{C_WIDTH-B_WIDTH{b[B_WIDITH-1]}},b};
14    end
15 
16   always @(*) begin
17         c_unsigned = {~c[C_WIDTH-1],c[C_WIDTH-2:0]};
18   end

方式二: Synopsys推荐,直接定义好符号类型,和的位宽按照运算法则定义好,实际+地方不作位宽匹配,工具自动识别
1    localparam A_WIDTH = 16;
2    localparam B_WIDTH = 8;
3  // Sumation result width should be 1 bit more than biggest widht of adder factor
4    localparam C_WIDTH = if (A_WIDTH > B_WIDTH) ? A_WIDTH + 1'b1 : B_WIDTH + 1'b1;
5
6   reg signed [A_WIDTH-1  : 0]      a;
7   reg signed [B_WIDTH-1  : 0]      b; 
8   
9    reg signed [C_WIDTH-1  : 0]      c; 
10  reg unsigned [C_WIDTH-1  : 0]      c_unsigned; 
11
12   always @(*) begin
13         c = $signed(a) - $signed(b);
14    end
16   always @(*) begin
17         c_unsigned = {~c[C_WIDTH-1],c[C_WIDTH-2:0]};
18   end

另外补充一下有符号运算容出现的问题,这种写法是有问题的,
6   reg signed [A_WIDTH-1  : 0]      a;
7   reg signed [B_WIDTH-1  : 0]      b; 
8   
9    reg signed [C_WIDTH-1  : 0]      c;

12   always @(*) begin
13         c = $signed(a-b) ;
14    end
这种写法是先不扩位,相减,然后补充符号位,和我们本意为违背的。

另外对于信号+常量的有符号运算,需要把常量类型前面+s 比如
12   always @(*) begin
13         c = $signed(a)-11'sd1024 ;
14    end

(0)

相关推荐