# rvs **Repository Path**: bds123/rvs ## Basic Information - **Project Name**: rvs - **Description**: 该文翻译自英文,介绍了一款RISC架构的模拟CPU工作原理。作者还设计了一个CPU仿真器,相当不错,可以帮助你理解CPU是如何工作的。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-04-28 - **Last Updated**: 2025-04-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 关于 Virgule 及其模拟器 emulsiV 作者:Guillaume Savaton, [ESEO](https://eseo.fr/) 翻译/校对/修改:Hao 原文链接:https://eseo-tech.github.io/emulsiV/doc/#textio CPU模拟器:https://eseo-tech.github.io/emulsiV/ GitHub:https://github.com/eseo-tech/emulsiV 以下是正文: [TOC] --- emulsiV 是名为 Virgule 的简单 RISC 处理器的可视化模拟器。 Virgule 是一个 32 位 RISC 处理器内核,它实现了 RISC-V 指令集的最小子集。这里的 "最小 "是指 Virgule 只接受 C 编译器从纯独立 (没有引入任何外部库,如) 程序中生成的指令。 ## 选择 Virgule 的理由与目的 Virgule 和 emulsiV 用于 ESEO 的计算机体系结构和数字设计初学者教学。在选择处理器架构之前,我们考虑了以下要求: * 开放式架构,我们可以使用和实施,而无需征得许可。 * 指令集简单而规范,但足够完整,可作为 C 编译器的目标。 * 符合市场现状的架构,最好是实际工业应用的架构。 * 免费开源工具链。 在候选架构中,RISC-V 满足了我们的所有要求: * 它的规格是开放的。 * 它是最先进的 RISC 架构,具有简洁的基本指令集。 * RISC-V 基金会拥有一份令人印象深刻的成员名单;RISC-V 内核和芯片现已面世。 * GNU 工具链支持 RISC-V 架构。 这些特性可用于多种教学场景。 在计算机体系结构课程中,学生将了解处理器是如何工作的,可以使用哪些语言和工具来创建底层程序(汇编、C)。模拟器是直观了解每条指令如何影响数据路径的好方法。 在数字电路设计课程中,学生可以用 VHDL 或 Verilog 实现处理器内核,也可以将处理器内核实例化,制作自己的片上系统。 ## 处理器架构 ### 与 RISC-V 规范的关系 Virgule 实现了 RISC-V 子集 RV32I 中的所有计算、传输控制和内存访问指令。它还提供了异常返回指令,可用于中断处理程序。 以下指令在 Virgule 不可用: * 内存序指令。 * 环境调用与断点。 * 控制与状态寄存器(CSR)指令。 RISC-V 架构定义了三个权限级别:用户、超级用户和机器。Virgule 仅支持机器级别。 ### 寄存器 (Registers) Virgule 包含以下 32 位寄存器: * 32 个名为 x0 至 x31 的通用寄存器。 * 程序计数器 pc 。该寄存器包含当前指令的地址。它的重置值为零,并且始终是 4 的倍数。 * 机器异常程序计数器 mepc 。当发生机器级异常时,该寄存器接收返回地址。 ### 内存组织(Memory organization) 内存加载和存储指令的数据格式遵循这些约定: | 名称 | 数据大小(位) | 地址是 4 的倍数 | | --- | --- | --- | | 字节 (B) | 8 | 1 | | 半字 (H) | 16 | 2 | | 字 (W) | 32 | 4 | 在内存中,16 位和 32 位数据将遵循 little-endian 排序。 以下两个地址具有特殊作用: * 复位时,执行从地址 0 开始。在这个地址上,我们通常会找到一条分支指令,指向程序的起点。 * Virgule 希望在地址 4 处找到中断处理程序。 ### 中断 (Interrupts) Virgule 在机器特权模式下实现了一个简单的硬件中断方案。处理器内核本身没有中断控制或状态寄存器。 收到中断请求后,Virgule 会执行以下操作: 1. 完成当前指令。 2. 切换到不可中断状态。 3. 将 mepc 设置为下一条指令的地址。 4. 将 pc 设置为 4,将控制权转移到中断处理程序。 使用 mret 指令从中断处理程序返回。该指令的作用如下: 1. 将 mepc 复制到 pc 。 2. 切换到可中断状态。 ### 指令集(Instruction set) 如下表所示: * rd 是目标通用寄存器。 * rs1 和 rs2 是源通用寄存器。 * imm 表示立即数。 | Instruction | Syntax | Operation | | :------------------------------- | :--------------------- | :---------------------------------------------------------------- | | Load Upper Immediate | `LUI rd, imm` | `rd <- imm << 12` (将 20 位 imm 放入 rd 的高位,低 12 位置零) | | Add Upper Immediate to PC | `AUIPC rd, imm` | `rd <- pc + (imm << 12)` (将 20 位 imm 左移 12 位加到 pc) | | Jump And Link | `JAL rd, imm` | `rd <- pc + 4; pc <- pc + imm` (imm 是符号扩展的偏移量) | | Jump And Link Register | `JALR rd, rs1, imm` | `rd <- pc + 4; pc <- rs1 + imm` (目标地址最低位置零) | | Branch if Equal | `BEQ rs1, rs2, imm` | `if rs1 == rs2: pc <- pc + imm else: pc <- pc + 4` | | Branch if Not Equal | `BNE rs1, rs2, imm` | `if rs1 != rs2: pc <- pc + imm else: pc <- pc + 4` | | Branch if Less Than | `BLT rs1, rs2, imm` | `if signed(rs1) < signed(rs2): pc <- pc + imm else: pc <- pc + 4` | | Branch if Greater or Equal | `BGE rs1, rs2, imm` | `if signed(rs1) >= signed(rs2): pc <- pc + imm else: pc <- pc + 4` | | Branch if Less Than Unsigned | `BLTU rs1, rs2, imm` | `if unsigned(rs1) < unsigned(rs2): pc <- pc + imm else: pc <- pc + 4` | | Branch if Greater or Equal Unsigned| `BGEU rs1, rs2, imm` | `if unsigned(rs1) >= unsigned(rs2): pc <- pc + imm else: pc <- pc + 4` | | Load Byte | `LB rd, imm(rs1)` | `rd <- sign_extend(mem[rs1+imm])` | | Load Half word | `LH rd, imm(rs1)` | `rd <- sign_extend(mem[rs1+imm+1] :: mem[rs1+imm])` | | Load Word | `LW rd, imm(rs1)` | `rd <- mem[rs1+imm+3] :: ... :: mem[rs1+imm]` | | Load Byte Unsigned | `LBU rd, imm(rs1)` | `rd <- zero_extend(mem[rs1+imm])` | | Load Half word Unsigned | `LHU rd, imm(rs1)` | `rd <- zero_extend(mem[rs1+imm+1] :: mem[rs1+imm])` | | Store Byte | `SB rs2, imm(rs1)` | `mem[rs1+imm] <- rs2[7:0]` | | Store Half word | `SH rs2, imm(rs1)` | `mem[rs1+imm+1] :: mem[rs1+imm] <- rs2[15:0]` | | Store Word | `SW rs2, imm(rs1)` | `mem[rs1+imm+3] :: ... :: mem[rs1+imm] <- rs2` | | Add Immediate | `ADDI rd, rs1, imm` | `rd <- rs1 + sign_extend(imm)` | | Set on Less Than Immediate | `SLTI rd, rs1, imm` | `if signed(rs1) < signed(imm): rd <- 1 else: rd <- 0` | | Set on Less Than Immediate Unsigned| `SLTIU rd, rs1, imm` | `if unsigned(rs1) < unsigned(imm): rd <- 1 else: rd <- 0` | | Exclusive Or Immediate | `XORI rd, rs1, imm` | `rd <- rs1 xor sign_extend(imm)` | | Shift Left Logical Immediate | `SLLI rd, rs1, shamt` | `rd <- rs1 << shamt` (`shamt` 来自 imm 低 5 位) | | Shift Right Logical Immediate | `SRLI rd, rs1, shamt` | `rd <- rs1 >> shamt` (逻辑右移, `shamt` 来自 imm 低 5 位) | | Shift Right Arithmetic Immediate | `SRAI rd, rs1, shamt` | `rd <- rs1 >> shamt` (算术右移, `shamt` 来自 imm 低 5 位) | | Or Immediate | `ORI rd, rs1, imm` | `rd <- rs1 or sign_extend(imm)` | | And Immediate | `ANDI rd, rs1, imm` | `rd <- rs1 and sign_extend(imm)` | | Add | `ADD rd, rs1, rs2` | `rd <- rs1 + rs2` | | Subtract | `SUB rd, rs1, rs2` | `rd <- rs1 - rs2` | | Shift Left Logical | `SLL rd, rs1, rs2` | `rd <- rs1 << rs2[4:0]` | | Set on Less Than | `SLT rd, rs1, rs2` | `if signed(rs1) < signed(rs2): rd <- 1 else: rd <- 0` | | Set on Less Than Unsigned | `SLTU rd, rs1, rs2` | `if unsigned(rs1) < unsigned(rs2): rd <- 1 else: rd <- 0` | | Exclusive Or | `XOR rd, rs1, rs2` | `rd <- rs1 xor rs2` | | Shift Right Logical | `SRL rd, rs1, rs2` | `rd <- rs1 >> rs2[4:0]` (逻辑右移) | | Shift Right Arithmetic | `SRA rd, rs1, rs2` | `rd <- rs1 >> rs2[4:0]` (算术右移) | | Or | `OR rd, rs1, rs2` | `rd <- rs1 or rs2` | | And | `AND rd, rs1, rs2` | `rd <- rs1 and rs2` | | Machine Return | `MRET` | `pc <- mepc` (切换到中断前的状态) | 在上表中,逻辑运算和移位操作的含义如下: | Operator | Effect | | --- | --- | | and | 按位与 | | or | 按位或 | | xor | 按位异或 | | sll | 逻辑左移 | | srl | 逻辑右移 | | sra | 算术右移(带符号扩展) | ### 指令编码 (Instruction encoding) 指令字可以由以下字段组成: * funct7 、 funct3 和 opcode 定义了要执行的操作。 * rd 是**目标通用寄存器**的索引(允许值范围为 0 至 15)。 * rs1 和 rs2 是**源通用寄存器**的索引(允许值范围为 0 至 15)。 * imm 表示一个 12bit 的**立即数**(immediate)。 RISC-V 指令集定义了六种指令格式: | Format / Instruction bits | 31:25 | 24:20 | 19:15 | 14:12 | 11:7 | 6:0 | | --- | --- | --- | --- | --- | --- | --- | | R | funct7 | rs2 | rs1 | funct3 | rd | opcode | | I | imm[11:5] `/` funct7 | imm[4:0] | rs1 | funct3 | rd | opcode | | S | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | | B | imm[12,10:5] | rs2 | rs1 | funct3 | imm[4:1,11] | opcode | | U | imm[31:25] | imm[24:20] | imm[19:15] | imm[14:12] | rd | opcode | | J | imm[20,10:5] | imm[4:1,11] | imm[19:15] | imm[14:12] | rd | opcode | > B指令中 imm[4:1,11] 含义:imm[11] - instruction [7],imm[4:1] - instruction [11:8] -- 译者注 ### 立即数的扩展(Immediate values) 立即数指的是直接写在指令中的数字。比如下面汇编指令: ```assembly addi x1, x0, 32 //x1 <- x0 + 32,其中32是立即数 ``` 因为 Virgule 是一个32bit处理器,而imm是12bit。因此 imm 会被扩展为32bit以参与运算。其中inst[31]表示imm的符号位(0为正,1为负),其他bit扩展方式遵循以下规则: | Format | imm[31:25] | imm[24:21] | imm[20] | imm[19:15] | imm[14:12] | imm[11] | imm[10:5] | imm[4:1] | imm[0] | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | I | inst[31] | inst[31] | inst[31] | inst[31] | inst[31] | inst[31] | inst[30:25] | inst[24:21] | inst[20] | | S | inst[31] | inst[31] | inst[31] | inst[31] | inst[31] | inst[31] | inst[30:25] | inst[11:8] | inst[7] | | B | inst[31] | inst[31] | inst[31] | inst[31] | inst[31] | inst[7] | inst[30:25] | inst[11:8] | 0 | | U | inst[31:25] | inst[24:21] | inst[20] | inst[19:15] | inst[14:12] | 0 | 0 | 0 | 0 | | J | inst[31] | inst[31] | inst[31] | inst[19:15] | inst[14:12] | inst[20] | inst[30:25] | inst[24:21] | 0 | 以S格式的指令为例,它的扩充规则如下: 1. imm[0] - inst[7] 2. imm[4:1] - inst[11:8] 3. imm[11:5] - inst[30:25] 4. imm的其他bit填充为inst[31],该位表示imm的符号位 假设imm = 1(0000 0000 0001),则被扩充为32bit后: > 0000 0000 0000 0000 0000 0000 1000 0000 要注意的是,在格式**B、J**格式指令(如if - else)中,imm 的值表示相对当前PC指针的地址偏移量。 ### 基本操作码(Base opcodes) 在 Virgule 中,我们保留了 RISC-V 规范中的以下基本操作代码。每个操作码对应一种特定的指令格式: | Name | opcode | Format | | --- | --- | --- | | LOAD | 0000011 | I | | OP-IMM | 0010011 | I | | AUIPC | 0010111 | U | | STORE | 0100011 | S | | OP | 0110011 | R | | LUI | 0110111 | U | | BRANCH | 1100011 | B | | JALR | 1100111 | I | | JAL | 1101111 | J | | SYSTEM | 1110011 | I | ### 各指令的字段值(Field values for each instruction) 在解码指令字时, Virgule 使用以下字段来识别实际指令。在此表中, opcode 列指的是上述基本操作代码表中的名称。 | Instruction | opcode | funct3 | funct7 | rs2 | | --- | --- | --- | --- | --- | | LUI | LUI | — | — | — | | AUIPC | AUIPC | — | — | — | | JAL | JAL | — | — | — | | JALR | JALR | 000 | — | — | | BEQ | BRANCH | 000 | — | — | | BNE | BRANCH | 001 | — | — | | BLT | BRANCH | 100 | — | — | | BGE | BRANCH | 101 | — | — | | BLTU | BRANCH | 110 | — | — | | BGEU | BRANCH | 111 | — | — | | LB | LOAD | 000 | — | — | | LH | LOAD | 001 | — | — | | LW | LOAD | 010 | — | — | | LBU | LOAD | 100 | — | — | | LHU | LOAD | 101 | — | — | | SB | STORE | 000 | — | — | | SH | STORE | 001 | — | — | | SW | STORE | 010 | — | — | | ADDI | OP-IMM | 000 | — | — | | SLLI | OP-IMM | 001 | 0000000 | — | | SLTI | OP-IMM | 010 | — | — | | SLTIU | OP-IMM | 011 | — | — | | XORI | OP-IMM | 100 | — | — | | SRLI | OP-IMM | 101 | 0000000 | — | | SRAI | OP-IMM | 101 | 0100000 | — | | ORI | OP-IMM | 110 | — | — | | ANDI | OP-IMM | 111 | — | — | | ADD | OP | 000 | 0000000 | — | | SUB | OP | 000 | 0100000 | — | | SLL | OP | 001 | 0000000 | — | | SLT | OP | 010 | 0000000 | — | | SLTU | OP | 011 | 0000000 | — | | XOR | OP | 100 | 0000000 | — | | SRL | OP | 101 | 0000000 | — | | SRA | OP | 101 | 0100000 | — | | OR | OP | 110 | 0000000 | — | | AND | OP | 111 | 0000000 | — | | MRET | SYSTEM | 000 | 0011000 | 00010 | > Virgule 指令共享了基本操作码,每种基本操作码下通过funct3和funct7来区分具体指令。这有点像计算机的二叉树。这种做法的好处是CPU的硬件电路更为简单,无关指令电路可以不使能,从而节省功耗。-- 译者注 ## 内存与外围设备(Memory and peripheral devices) ### 内存布局(Memory layout) 该CPU的寻找空间如下表所示: | Address (hex) | Device | | --- | --- | | 00000000 : 00000BFF | RAM (3072 bytes) | | 00000C00 : 00000FFF | Bitmap RAM (1024 bytes) | | B0000000 : B0000001 | Text input | | C0000000 | Text output | | D0000000 | General-purpose input/output | ### 文本输入/输出(Text input/output) 文本输入设备由模拟器用户界面上的一个文本字段表示。它有两个 8 位寄存器: | Address (hex) | Role | Value | | --- | --- | --- | | B0000000 | Control/Status | Bit 7: Interrupt enable
Bit 6: Character received | | B0000001 | Data | The ASCII code of the last input character. | 控制/状态寄存器的工作原理如下: * 复位时,第 6 位和第 7 位清零。 * 用户在文本字段中输入一个字符后,第 6 位被置位。清除该位的唯一方法是使用存储指令写入 0。 * 当第 7 位和第 6 位均被设置时,设备将向处理器发送中断请求。 文本输出设备由模拟器用户界面上的文本区域表示。它只有一个只写寄存器: | Address (hex) | Role | Value | | --- | --- | --- | | C0000000 | Data | The ASCII code of the character to display. | ### GPIO(General-purpose input/output) GPIO(通用输入/输出)外设最多可连接 32 个简单的用户输入/输出设备: * 按钮 * 拨动开关 * LEDs 输入以 8×4 的网格排列在模拟器 "通用输入/输出 "部分的底部。**右键单击**单元格可更改其类型。**左键单击**可更改按钮或开关的状态。 它有以下 32 位寄存器: | Address (hex) | Role | Value | | --- | --- | --- | | D0000000 | Direction (dir) | The configuration of each pin (0 for an output, 1 for an input). | | D0000004 | Interrupt enable (ien) | Enable interrupts on input events. | | D0000008 | Rising-edge events (rev) | Each bit is set to 1 if the corresponding input pin has changed from 0 to 1. | | D000000C | Falling-edge events (fev) | Each bit is set to 1 if the corresponding input pin has changed from 1 to 0. | | D0000010 | Value (val) | The current value of each input or output. | ### 位图输出(Bitmap output) 模拟器提供 32 行 32 个像素的图形显示区域。每个像素映射到一个 RAM 字节,其颜色的编码方式如下:
7 6 5 4 3 2 1 0
Red Green Blue
位图 RAM 采用光栅扫描排序(从上到下,从左到右)。该表显示了每个地址对应像素的(x、y)坐标:
+0 +1 +2 ... +1F
00000C00 (0,0) (1,0) (2,0) ... (31,0)
00000C20 (0,1) (1,1) (2,1) ... (31,1)
00000C40 (0,2) (1,2) (2,2) ... (31,2)
...
00000FE0 (0,31) (1,31) (2,31) ... (31,31)
## 使用GNU工具链创建 emulsiV 程序 模拟器允许通过在内存视图的汇编栏中输入指令来创建和编辑程序。另一种方法是在文本编辑器中键入程序,然后使用 GNU 工具链为 emulsiV 生成可执行文件。 ### 安装 如果使用的是 Ubuntu,可以使用此命令安装预置的 RISC-V 裸机工具链: ```bash sudo apt install gcc-riscv64-unknown-elf ``` 顾名思义,它安装的工具链同时支持 32 位和 64 位架构。 ### 编写启动代码 下面是一个典型的启动模块( startup.s ),您可以直接用于您的程序。 ```assembly .section vectors, "x" .global __reset __reset: j start __irq: j irq_handler .text .align 4 .weak irq_handler irq_handler: mret start: la gp, __global_pointer la sp, __stack_pointer la t0, __bss_start la t1, __bss_end bgeu t0, t1, memclr_done memclr: sw zero, (t0) addi t0, t0, 4 bltu t0, t1, memclr memclr_done: call main j . ``` 该如下命令将源文件 startup.s 汇编成目标文件 startup.o : ```bash riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -c -o startup.o startup.s ``` ### 编译C代码 下面是用 C 语言为 emulsiV 实现的 Hello World: ```c #define TEXT_OUT (*(char*)0xC0000000) void print(const char *str) { while (*str) { TEXT_OUT = *str++; } } void print(const char *str) { while (*str) { TEXT_OUT = *str++; } } void main(void) { print("Virgule says\n<< Hello! >>\n"); } ``` 以下命令将源文件 hello.c 编译成目标文件 hello.o : ```bash riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -ffreestanding -c -o hello.o hello.c ``` 如果想用 C 语言编写中断处理程序,可以覆盖 irq_handler 子程序,添加 interrupt 属性,如下所示: ```c __attribute__((interrupt("machine"))) void irq_handler(void) { // Insert your code here. } ``` ### 链接脚本 在生成最终的 emulsiV 可执行文件之前,需要创建一个链接脚本,脚本名称为:emulsiv.ld。 ```assembly ENTRY(__reset) MEM_SIZE = 4K; STACK_SIZE = 512; BITMAP_SIZE = 1K; SECTIONS { . = 0x0; .text : { *(vectors) *(.text) __text_end = .; } .data : { *(.data) } .rodata : { *(.rodata) } __global_pointer = ALIGN(4); .bss ALIGN(4) : { __bss_start = .; *(.bss COMMON) __bss_end = ALIGN(4); } . = MEM_SIZE - STACK_SIZE - BITMAP_SIZE; .stack ALIGN(4) : { __stack_start = .; . += STACK_SIZE; __stack_pointer = .; } .bitmap ALIGN(4) : { __bitmap_start = .; *(bitmap) } __bitmap_end = __bitmap_start + BITMAP_SIZE; } ``` 使用如下指令,链接 startup.o 、 hello.o 和 emulsiV.ld 生成可执行文件 hello.elf : ```bash riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -nostdlib -T emulsiv.ld -o hello.elf startup.o hello.o ``` ### 生成可执行文件 使用如下指令,将 ELF 二进制文件 hello.elf 转换为hex文件: ```bash riscv64-unknown-elf-objcopy -O ihex hello.elf hello.hex ``` 在模拟器中,点击 "Open an hex file from your computer" 按钮,加载 hello.hex 文件。