RISC-V Opcodes
riscv-opcodes
本仓库列举了标准的 RISC-V 指令操作码以及控制和状态寄存器。它还包含一个脚本,可将这些内容转换为多种格式(C、Scala、LaTeX)。
本仓库中的制品(如 encoding.h、latex-tables 等)被用于其他工具和项目,例如 Spike、PK、RISC-V 手册等。
项目结构
├── extensions # instruction opcodes
├── extensions/unratified # unratified instruction opcodes
├── encoding.h # the template encoding.h file
├── Makefile # makefile to generate artifacts
└── src/riscv_opcodes # python files to perform checks on
# the instructions and generate artifacts
文件命名规则
本项目采用非常特定的文件结构来定义指令编码。所有包含指令编码的文件均以前缀 rv 开头。这些文件可位于 extensions 目录(若指令已获得批准)或 extensions/unratified 目录(若指令未获得批准)。具体的文件命名规则和位置如下:
rv_x- 包含扩展 X 的 32 位和 64 位模式中通用的指令。rv32_x- 仅包含 rv32x 中的指令(在 rv64x 中不存在,例如 brev8)。rv64_x- 仅包含 rv64x 中的指令(在 rv32x 中不存在,例如 addw)。rv_x_y- 包含当扩展 X 和 Y 均可用/启用时的指令。建议此类文件名遵循规范中指定的标准顺序。unratified- 此目录也将包含符合上述规则的文件,但这些文件对应尚未获得批准的指令。
当一条指令存在于多个扩展中,且规范未明确界定该指令所属的扩展时,该指令的编码必须放置在按标准顺序排列的第一个扩展中,并应通过 $import 关键字在其余扩展中导入。
编码语法
编码语法使用 $ 来指示关键字。目前已确定的关键字有两个:$import 和 $pseudo_op(下文将进行说明)。该语法还使用 :: 来定义扩展与指令之间的关系。.. 用于定义位范围。我们使用 # 在文件中定义注释。所有注释必须单独成行,不支持行内注释。
本项目中使用的指令语法大致分为三类:
-
常规指令:这些指令在编码空间中拥有唯一的操作码。此类指令的通用语法指南如下:
<指令名称> <参数>其中,
<参数>可以是<位编码>或<变量参数>。示例:
lui rd imm20 6..2=0x0D 1..0=3 beq bimm12hi rs1 rs2 bimm12lo 14..12=0 6..2=0x18 1..0=3位编码通常有两种类型:
- 单比特赋值:使用语法
<bit-position>=<value>为单个比特赋值。例如,6=1表示第 6 位应为 1。此处的值必须为 1 或 0。 - 范围赋值:使用语法
<msb>..<lsb>=<val>为一个比特范围赋值。例如,31..24=0xab。此处的值可以是无符号整数、十六进制数(0x 开头)或二进制数(0b 开头)。
- 单比特赋值:使用语法
-
伪指令(也称为 pseudo_ops):这些指令是常规指令的别名。它们的编码对常规指令施加了特定的限制。此类指令的语法使用
$pseudo_op关键字,如下所示:$pseudo_op <extension>::<base-instruction> <instruction name> <instruction args> <bit-encodings>其中,
<extension>指定包含基础指令的扩展。<base-instruction>表示此伪指令所别名的指令名称。其余字段与常规指令语法相同,用于指定伪指令的所有参数和字段。示例:
$pseudo_op rv_zicsr::csrrs frflags rd 19..15=0 31..20=0x001 14..12=2 6..2=0x1C 1..0=3如果一条已批准的指令是某条常规未批准指令的伪指令,建议保持这种伪指令关系,即把新指令定义为该未批准常规指令的伪指令。这样可以避免那些同时试验未批准扩展的用户遇到操作码重叠的问题。
-
导入指令:这些指令是从一个扩展借用到新的/不同的扩展/子扩展中的指令。只有常规指令可以被导入,伪指令或已导入的指令不能被导入。示例:
$import rv32_zkne::aes32esmi
限制条件
在定义 $pseudo_ops 和 $imported_ops 时,需注意以下限制条件:
- 伪指令或已导入的指令不能在另一个文件中再次导入。应始终只导入基础指令。
- 定义 $pseudo_op 时,其基础指令本身不能是 $pseudo_op。
parse.py 的流程
parse.py Python 文件用于对当前指令编码集执行检查,并生成多种产物:LaTeX 表格、encoding.h 头文件等。本节将简要概述该 Python 文件内的流程。
首先,parse.py 创建当前已签入仓库的所有 rv* 文件的列表(包括 unratified 目录内的文件)。
然后,它开始逐行解析每个文件。在第一遍解析中,我们只捕获常规指令,忽略导入的指令或伪指令。
对于每条常规指令,会执行以下检查:
- 对于范围赋值语法,msb(最高有效位)位置必须高于 lsb(最低有效位)位置。
- 对于范围赋值语法,范围的值必须能在由 msb 和 lsb 确定的空间内表示。
- 不得对相同的位位置多次定义值。
- 所有位位置都必须被考虑到(无论是作为参数还是常量值字段)。
一旦常规指令通过上述检查,我们便为该指令创建一个字典,其中包含以下字段:
- encoding(编码):包含一个 32 位字符串,定义指令的编码。这里用
-表示指令参数字段。- extension(扩展):指示该指令来自哪个扩展/文件名的字符串。
- mask(掩码):一个 32 位十六进制值,指示为验证该指令的合法性必须检查的编码位。
- match(匹配值):一个 32 位十六进制值,指示在上述掩码中被设为 1 的位,其编码必须取的值。
- variable_fields(变量字段):该指令所需的参数列表。
上述字典元素会添加到主 instr_dict 字典的指令节点下。此过程持续进行,直至所有常规指令都处理完毕。在第二遍解析中,我们处理 $pseudo_op 指令。这里,我们首先检查该伪指令的 base-instruction(基础指令)是否存在于相关的扩展/文件名中。如果存在,则语法的其余部分会接受与上述相同的检查。检查通过后,如果 base-instruction 尚未添加到主 instr_dict 中,则将伪指令添加到列表中。在第三遍,也就是最后一遍解析中,我们处理导入的指令。
伪指令的 base-instruction 在第一遍解析后可能未出现在主 instr_dict 中的情况是:当仅处理一部分扩展,导致 base-instruction 未被包含时。
制品生成与使用
通过 parse.py 可生成以下制品:
- instr_dict.json:此文件由 parse.py 始终生成,包含完整的主字典
instr_dict的 JSON 格式内容。请注意,在此文件中,指令中的 点 被替换为 下划线。在本项目的早期版本中,生成的文件为 instr_dict.yaml。由于 JSON 是 YAML 的子集,因此任何 YAML 解析器仍可读取此文件。 - encoding.out.h:此头文件供 spike、pk 等工具使用。
- instr-table.tex:用于 riscv-unpriv 规范的 LaTeX 指令表格。
- priv-instr-table.tex:用于 riscv-priv 规范的 LaTeX 指令表格。
- inst.chisel:用于解码指令的 Chisel 代码。
- inst.sverilog:用于解码指令的 SystemVerilog 代码。
- inst.rs:包含所有指令的掩码和匹配变量的 Rust 代码。
- inst.spinalhdl:用于解码指令的 SpinalHDL 代码。
- inst.go:用于解码指令的 Go 代码。
要为当前已签入的所有指令生成上述所有制品,只需从根目录运行 make。需要 uv(参见简易安装说明)。
make 应在命令行输出以下日志:
Extensions selected : ['rv*', 'unratified/rv*']
INFO:: encoding.out.h generated successfully
INFO:: inst.chisel generated successfully
INFO:: inst.spinalhdl generated successfully
INFO:: inst.sverilog generated successfully
INFO:: inst.rs generated successfully
INFO:: inst.go generated successfully
INFO:: instr-table.tex generated successfully
INFO:: priv-instr-table.tex generated successfully
默认情况下,所有扩展均已启用。若要仅选择扩展的子集,您可以修改 makefile 中的 EXTENSIONS 变量,使其仅包含所需的文件名。
例如,如果您只需要 I 和 M 扩展,可以执行以下操作:
make EXTENSIONS='rv*_i rv*_m'
这将打印以下日志:
Extensions selected : ['rv32_i', 'rv64_i', 'rv_i', 'rv64_m', 'rv_m']
INFO:: encoding.out.h generated successfully
INFO:: inst.chisel generated successfully
INFO:: inst.spinalhdl generated successfully
INFO:: inst.sverilog generated successfully
INFO:: inst.rs generated successfully
INFO:: inst.go generated successfully
INFO:: instr-table.tex generated successfully
INFO:: priv-instr-table.tex generated successfully
如果您只需要特定的产物,可以使用以下一个或多个目标:c、rust、chisel、sverilog、latex。
例如,如果您想生成如前所示带有扩展的基于 c 的产物,可以使用以下命令:
uv run riscv_opcodes -c 'rv*_i' 'rv*_m'
这将打印以下日志:
Extensions selected : ['rv*_i', 'rv*_m']
INFO:: encoding.out.h generated successfully
或者你也可以使用 make 命令,如下所示:
make encoding.out.h EXTENSIONS='rv*_i rv*_m'
你可以使用 clean 目标来移除所有构建产物。
添加新扩展
要添加新的指令扩展,请根据 文件结构 中定义的规则创建相应的 rv* 文件。从根目录运行 make,确保所有检查都通过且所有构建产物都正确生成。成功运行后,终端应输出以下日志:
Extensions selected : ['rv*', 'unratified/rv*']
INFO:: encoding.out.h generated successfully
INFO:: inst.chisel generated successfully
INFO:: inst.sverilog generated successfully
INFO:: inst.rs generated successfully
INFO:: instr-table.tex generated successfully
INFO:: priv-instr-table.tex generated successfully
创建 PR 以供审核。
在 parse.py 中启用调试日志
要在 parse.py 中启用调试日志,请将 level=logging.INFO 修改为 level=logging.DEBUG,然后运行 python 命令。现在,您将在终端上看到如下调试语句:
DEBUG:: Collecting standard instructions first
DEBUG:: Parsing File: ./rv_i
DEBUG:: Processing line: lui rd imm20 6..2=0x0D 1..0=3
DEBUG:: Processing line: auipc rd imm20 6..2=0x05 1..0=3
DEBUG:: Processing line: jal rd jimm20 6..2=0x1b 1..0=3
DEBUG:: Processing line: jalr rd rs1 imm12 14..12=0 6..2=0x19 1..0=3
DEBUG:: Processing line: beq bimm12hi rs1 rs2 bimm12lo 14..12=0 6..2=0x18 1..0=3
DEBUG:: Processing line: bne bimm12hi rs1 rs2 bimm12lo 14..12=1 6..2=0x18 1..0=3
如何查找指令的定义位置?
从仓库根目录,您可以使用 grep "^\s*<instr-name>" extensions/rv* extensions/unratified/rv* 命令,或者运行 make 并打开 instr_dict.json,然后搜索您要查找的指令。在该指令中,extension 字段会指示该指令来自哪个文件。