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