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个浮点寄存器,叫f0f31。每个寄存器在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。验证时一定要测这些边界情况。

警告:浮点转整数的饱和行为在不同架构上可能不一样。RISC-V规范里明确写了饱和值,但有些老架构会返回0。如果你在移植代码,一定要检查这个行为。

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是目标浮点寄存器。

重点:浮点指令的opcode都是0b1010011(0x53)。区分具体指令靠funct7和funct3字段。验证时,解码逻辑是最容易出错的地方之一。

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 知识体系结构图

下面这张图展示了本章的知识脉络,方便你理解各个知识点之间的关系:

RISC-V浮点指令集知识体系 F扩展(单精度) D扩展(双精度) 指令编码格式 算术指令 比较指令 转换指令 算术指令 精度转换 舍入模式 R-type格式 I-type格式 编码速查 验证要点 覆盖所有舍入模式 | 测试NaN边界 | 随机指令生成
验证小贴士:我建议你在搭建验证环境时,把F扩展和D扩展的指令解码器做成参数化的。这样同一个解码模块可以复用于两种精度,减少代码量,也降低出错概率。

好了,浮点指令集这部分就讲到这里。记住,验证浮点单元的关键在于覆盖所有边界情况——NaN、无穷大、正负零、非规格化数,一个都不能少。下一章咱们会聊浮点运算的数据通路设计,到时候再细说这些特殊值在硬件里是怎么处理的。


公众号:蓝海资料掘金营,微信deep3321