3. 浮点指令集:F扩展指令详解、D扩展指令详解、指令编码格式
好,咱们今天聊聊RISC-V的浮点指令集。说实话,这部分内容在验证工作中特别容易踩坑。我刚开始做浮点单元验证那会儿,总觉得指令集嘛,看看手册就行了。结果呢?第一次仿真就跑飞了——因为没搞懂F扩展和D扩展的精度转换规则。
浮点运算,说白了就是让硬件帮你算小数。RISC-V的浮点扩展分两档:F扩展是单精度(32位),D扩展是双精度(64位)。咱们一个一个说。
3.1 F扩展指令详解
F扩展,也叫单精度浮点扩展。它操作的是32位的浮点数,对应C语言的float类型。指令集里大概有30多条指令,我挑几个重点的讲。
3.1.1 基本算术指令
这些指令跟整数指令长得挺像,但操作的是浮点寄存器。RISC-V有32个浮点寄存器,叫f0到f31。每个寄存器在F扩展里是32位宽。
| 指令 | 功能 | 格式示例 |
|---|---|---|
| FADD.S | 单精度浮点加法 | fadd.s f1, f2, f3 |
| FSUB.S | 单精度浮点减法 | fsub.s f1, f2, f3 |
| FMUL.S | 单精度浮点乘法 | fmul.s f1, f2, f3 |
| FDIV.S | 单精度浮点除法 | fdiv.s f1, f2, f3 |
| FSQRT.S | 单精度浮点开方 | fsqrt.s f1, f2 |
嗯,这里要注意一点:浮点除法比整数除法慢得多。我在项目中遇到过,有人用浮点除法做循环内的计算,结果性能直接崩了。后来改成查表加插值,速度提升了5倍。
3.1.2 比较指令
浮点比较跟整数比较不太一样。浮点数有NaN(Not a Number)这种特殊值,比较的时候要小心。
// 浮点比较指令示例
feq.s f1, f2, f3 // 如果f2 == f3,设置f1为1.0,否则为0.0
flt.s f1, f2, f3 // 如果f2 < f3,设置f1为1.0,否则为0.0
fle.s f1, f2, f3 // 如果f2 <= f3,设置f1为1.0,否则为0.0
你想想看,如果比较结果里有NaN参与,feq.s永远返回假。这个特性在验证时特别容易漏掉。我曾经吃过这个亏——写测试用例时没覆盖NaN比较,结果芯片回来后客户报了个bug,查了两天才定位到。
3.1.3 转换指令
浮点和整数之间经常需要转换。F扩展提供了专门的转换指令:
FCVT.W.S:把单精度浮点转成32位有符号整数FCVT.S.W:把32位有符号整数转成单精度浮点FCVT.WU.S:转成32位无符号整数FCVT.S.WU:从无符号整数转过来
这里有个坑:浮点转整数时,如果浮点数太大或者是个NaN,结果会饱和到2^31 - 1或者-2^31。验证时一定要测这些边界情况。
3.2 D扩展指令详解
D扩展是双精度浮点,操作64位的数。它的指令集跟F扩展几乎一一对应,只是操作码不同。指令后缀从.S变成了.D。
3.2.1 指令对照表
| F扩展指令 | D扩展指令 | 功能 |
|---|---|---|
| FADD.S | FADD.D | 双精度加法 |
| FMUL.S | FMUL.D | 双精度乘法 |
| FDIV.S | FDIV.D | 双精度除法 |
| FSQRT.S | FSQRT.D | 双精度开方 |
| FCVT.S.D | FCVT.D.S | 精度转换 |
我个人习惯,在验证D扩展时,会特别关注精度转换指令。比如FCVT.S.D是把双精度转成单精度,这个过程会损失精度。验证时要检查舍入模式是否正确。
3.2.2 精度转换的细节
从双精度转单精度,说白了就是扔掉一半的尾数位。但怎么扔?IEEE 754标准定义了四种舍入模式:
- 就近舍入(默认):四舍五入,遇到正好一半时取偶数
- 向零舍入:直接截断
- 向上舍入:往正无穷方向
- 向下舍入:往负无穷方向
验证时,每种舍入模式都要测。我记得有一次,验证环境只测了默认模式,结果综合后发现在向上舍入模式下,某个边界值算错了。从那以后,我写验证计划时一定会把四种舍入模式都列进去。
3.3 指令编码格式
RISC-V的指令编码非常规整。浮点指令主要用R-type和I-type两种格式。
3.3.1 R-type格式
大部分浮点算术指令都用R-type。它的编码如下:
31 27 26 25 24 20 19 15 14 12 11 7 6 0
+--------+--------+--------+--------+--------+--------+--------+
| funct7 | rs2 | rs1 | funct3 | rd | opcode |
+--------+--------+--------+--------+--------+--------+
7位 5位 5位 3位 5位 7位
举个例子,FADD.S的编码:
- opcode = 0b1010011 (浮点操作)
- funct7 = 0b0000000
- funct3 = 0b000
- rs1, rs2, rd 分别是源和目标寄存器编号
3.3.2 I-type格式
浮点加载指令和部分转换指令用I-type:
31 20 19 15 14 12 11 7 6 0
+--------+--------+--------+--------+--------+
| imm[11:0] | rs1 | funct3 | rd | opcode |
+--------+--------+--------+--------+--------+
12位 5位 3位 5位 7位
比如FLW(加载单精度浮点)就用这个格式。imm是偏移量,rs1是基址寄存器,rd是目标浮点寄存器。
3.3.3 编码速查表
| 指令 | opcode | funct7 | funct3 | 格式 |
|---|---|---|---|---|
| FADD.S | 0x53 | 0x00 | 0x0 | R-type |
| FSUB.S | 0x53 | 0x01 | 0x0 | R-type |
| FMUL.S | 0x53 | 0x02 | 0x0 | R-type |
| FDIV.S | 0x53 | 0x03 | 0x0 | R-type |
| FSQRT.S | 0x53 | 0x2C | 0x0 | R-type |
| FLW | 0x07 | — | 0x2 | I-type |
| FSW | 0x27 | — | 0x2 | S-type |
你可能会问,为什么FSQRT.S的funct7是0x2C?因为它是单操作数指令,编码上跟双操作数指令做了区分。验证时,我建议用随机指令生成器来覆盖所有编码组合,光靠手写测试用例肯定不够。
3.4 知识体系结构图
下面这张图展示了本章的知识脉络,方便你理解各个知识点之间的关系:
好了,浮点指令集这部分就讲到这里。记住,验证浮点单元的关键在于覆盖所有边界情况——NaN、无穷大、正负零、非规格化数,一个都不能少。下一章咱们会聊浮点运算的数据通路设计,到时候再细说这些特殊值在硬件里是怎么处理的。
公众号:蓝海资料掘金营,微信deep3321