首先为什么要定点化?
在性能可接受的范围内尽可能的的压缩数据bit位宽以便节省资源
一个扰码识别的定点流程
灰色的全精度
蓝色是定点截位以后的位宽,对于这个算法,定点以后在大大节省资源的情况下性能几乎没有任何损失。
我们在定点的时候不是说每一个节点都需要压缩bit,而是在关键的节点尽可能压缩位宽才能做到事半功倍,
比如以上例子中的求平方,和最后的累计和输出,这两个点对硬件来说非常敏感,一个是乘法器,另外一个是缓存MEM,是面积的开销大户,
所以我们在定点的时候要特别有意去照顾这些节点,在性能可接受的情况下,能省1bit是1bit
定点介绍(低截高饱)
一般我们会分为两步来操作
- 低位Round操作
低位的Round操作有非常多类型。参见
https://en.wikipedia.org/wiki/Rounding
其实 RoundHalfUp, RoundHalfDown, RoundHalfToZero,
RoundHalfToInf, RoundHalfToEven, RounHalfToOdd 非常接近,
RoundToInf,最为常见。其中”RoundToeven,RoundToOdd”比较特别,这种Round比RoundToInf还要公平一些,但是相对稍微复杂一下,一般用在打数据统计中对精度非常敏感的场景。
SpinalHDL目前不支持这两种类型,
不同语言的round函数实际用的Round类型其实也各有不同,SpinalHDL为了保持通用,默认round采用RoundToInf,
但是我们在一般的硬件设计当中
推荐大家使用RoundToUp,这种方式选择器更少,时序面积更好,性能几乎没有太大损失。
- 高位饱和操作
主要是饱和操作,但是有时候直接截掉的需求,高位直接截掉没有数学意义,我不清楚具体用在什么地方。
除此之外,高位还有一个对称操作,比如8bit的有符号数表示范围为(-128
~ 127)如果不对称取反的话会出现+128 如果有符号数表示就得扩展为9bit
为了避免这种情况,又是需要将(-128 ~127) 对称到(-127 ~ 127),
这样在取反的时候就不用扩展1bit,性能损失几乎忽略不计,在硬件设计当中非常常见。
Verilog是怎么做的?
手动书写定点电路 封装fixpoint代码
我们用Verilog来实现定点饱和截位时,起初都是手写饱和截位电路,包括现在我看很多人还是直接手写
这个是极其容易出错,也是我们和算法debug追问题的高发地带。我想很多人都会遇到跟算法比对数据是遇到
差1bit对不上(实际上就是+-1),追了半天发现定点方式理解不一致,或者电路里面写错了。这种问题反反复复的发生,非常耗时耗力,很没必要。
后来出现了一种相对稳定一点的方式: 封装为饱和截位
这个可能是在verilog上能做到的最好的方式了,但是不够完美 使用方式:
1 | fixpoint (15,11,4,1,1) u_add1_fix (.din(fixin),.dout(fixout)); |
源码
实际上这里面也就才实现了Floor,RoundToInf,RoundUp
三种Round方式,不得不用verilog generator配合一些宏,
代码看上去已经是灾难了,调试到稳定也是非常痛苦。
SpinalHDL定点如何操作
这一切到SpinalHDL上一句话就能搞定
1 | val A = SInt(10 bit) |
展开来等效于
1 | val B = A.roundToInf(3).sat(3) |
SpinalHDL完整的提供了8种Round Api
你可以直接像这样使用它
1 | val x = UInt(8 bits) |
当然我们鼓励你 使用fixTo ,
更直观也更安全,不需要你手动处理进位对齐的那些琐碎容易犯错的操作。
当然你愿意你完全还是可以用A.roundToInf(2).sat(3).symmetry 这种方式式去处理,不做限制。
只要你配置好roundType,以及symmetric是否对称, 就可以自由的使用fixTo,
一行搞定!