diff --git a/docs/docs/TODO.md b/docs/docs/TODO.md new file mode 100644 index 0000000..bcf6280 --- /dev/null +++ b/docs/docs/TODO.md @@ -0,0 +1,32 @@ +重整大纲: + +principles of writing: + +- 及时提示:使用技巧在第一次使用时强调 +- 延后困难:尽量延后部分困难,减轻前期难度 + + +1. 项目介绍 + 1. 项目总览 + 1. 项目目的 + 2. 实验目标和完成方式 + 3. +2. 基础知识引入 + 1. 理论知识 + - [1] CPU 架构 + - [1] 指令集架构 + - [1] 中断和异常 + - 编译与链接 + 2. 工程知识技能 + - [1] shell、系统变量、常用指令 + - [1] scala and chisel3 + - 项目构建工具链:gcc, cmake;反汇编工具的使用:objdump, or godbolt + - IDEA 和 vscode 的使用(以 cheat sheet 形式给出,需要时查询) + - gtkwave 的使用 +3. 准备实验环境 + + +4. 实验内容 + 1. [1] 实验1 + 2. 实验2 + diff --git a/docs/docs/better-tut/intro.md b/docs/docs/better-tut/intro.md new file mode 100644 index 0000000..15751a9 --- /dev/null +++ b/docs/docs/better-tut/intro.md @@ -0,0 +1,30 @@ +# 欢迎来到 YatCPU 教程仓库! + + +YatCPU (Yet another toy CPU,逸芯) 是一款开源、开发中的教学用 RISC-V 处理器,基于 Chisel 硬件设计语言实现,并用于中山大学 (Sun Yat-sen University) 计算机学院冯班组成原理实验课程的教学。同样欢迎其他高校相关课程使用! + + +YatCPU 目前由 4 个步进式实验组成,您将分别实现一个单周期CPU、添加中断机制、实现多周期CPU 及最后实现总线功能。 +实验中您将运用 Chisel 硬件设计语言完成 CPU 设计,通过测试以验证正确性,并最终烧录至开发版上运行程序。 +在此过程中,您必然会遇到诸多困难,包括基础的计算机知识、环境配置和工具上手,以及理解 CPU 设计中的核心难点。我们针对 2023、2024年情况改进了该教程,为计算机新手提供尽可能易懂的额外知识补充和讲解;若您遇到教程无法解决的困难,您可以尝试使用搜索引擎、大语言模型问答解决。如果这些方法最终都不奏效,请及时请教老师和助教。 + + + + + + +--- + + + + + +本仓库由 [Tokisakix](https://github.com/Tokisakix)、[PurplePower](https://github.com/PurplePower)、[Han Huang](https://github.com/HHTheBest) 在 [2022-fall-yatcpu-repo](https://github.com/hrpccs/2022-fall-yatcpu-repo) 的基础上结合 2023 计组教学实情整理而来,有较多原创内容 + +非常感谢 [xy3](https://github.com/xy3xy3) 在此仓库开发期间多次帮我测试代码文件。 + + + + + + diff --git a/docs/docs/better-tut/labs/drawio-shapes/adder.xml b/docs/docs/better-tut/labs/drawio-shapes/adder.xml new file mode 100644 index 0000000..7946cf4 --- /dev/null +++ b/docs/docs/better-tut/labs/drawio-shapes/adder.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/docs/better-tut/labs/drawio-shapes/mux.xml b/docs/docs/better-tut/labs/drawio-shapes/mux.xml new file mode 100644 index 0000000..8ffb671 --- /dev/null +++ b/docs/docs/better-tut/labs/drawio-shapes/mux.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/docs/better-tut/labs/drawio-shapes/mux4.xml b/docs/docs/better-tut/labs/drawio-shapes/mux4.xml new file mode 100644 index 0000000..dca34cf --- /dev/null +++ b/docs/docs/better-tut/labs/drawio-shapes/mux4.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/docs/better-tut/labs/lab1/assets/.$single_cycle_cpu.drawio.bkp b/docs/docs/better-tut/labs/lab1/assets/.$single_cycle_cpu.drawio.bkp new file mode 100644 index 0000000..69863c4 --- /dev/null +++ b/docs/docs/better-tut/labs/lab1/assets/.$single_cycle_cpu.drawio.bkp @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/docs/better-tut/labs/lab1/assets/connect.png b/docs/docs/better-tut/labs/lab1/assets/connect.png new file mode 100644 index 0000000..8d4070d Binary files /dev/null and b/docs/docs/better-tut/labs/lab1/assets/connect.png differ diff --git a/docs/docs/better-tut/labs/lab1/assets/single_cycle_CPU.png b/docs/docs/better-tut/labs/lab1/assets/single_cycle_CPU.png new file mode 100644 index 0000000..f1416fc Binary files /dev/null and b/docs/docs/better-tut/labs/lab1/assets/single_cycle_CPU.png differ diff --git a/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio b/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio new file mode 100644 index 0000000..ad95ab4 --- /dev/null +++ b/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio @@ -0,0 +1,910 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio.svg b/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio.svg new file mode 100644 index 0000000..b59b95e --- /dev/null +++ b/docs/docs/better-tut/labs/lab1/assets/single_cycle_cpu.drawio.svg @@ -0,0 +1,4 @@ + + + +
InstructionFetch
4
ADD
0
InstAddr
PC
MUX
Reg1RA
Reg1RA
Reg2RA
Reg2RA
ALUOp1Src
ALUOp2Src
1
Immediate
RegWriteSrc
Decoder
Reg1RD
Register
File
ALU
Control
ALU
Jump
Judge
JumpFlag
JumpFlag
Addr
Memory
Control
Instruction
Instruction
Memory
InstAddr
RegWD
Funct3
Opcode
Funct7
ALUFunct
Mux
Mux
RegWE
RegWA
MemoryRE
MemRE
MemoryWE
MemWE
Instruction
Funct3
Reg2RD
Reg2RD
MemAddr
Mux
Opcode
Funct3
1
Reg1RD
Reg2RD
1
JumpAddr
1
2
0
WriteBack
Single-cycle RISC-V Core
Clk_p
WD
WE
Strobe
RD
Character
Display
HDMI
Display
Clk_n
Data_p
Data_n
Addr = Address
Inst = Instruction
Int = Interrupt
Src = Source
RE = ReadEnable
RA = ReadAddress
RD = ReadData
WE = WriteEnable
WA = WriteAddress
WD = WriteData


Data
Control
Execute
RegWE
RegWA
4
ADD
3
\ No newline at end of file diff --git a/docs/docs/better-tut/labs/lab1/lab1-single-cycle-cpu.md b/docs/docs/better-tut/labs/lab1/lab1-single-cycle-cpu.md new file mode 100644 index 0000000..2efa2a0 --- /dev/null +++ b/docs/docs/better-tut/labs/lab1/lab1-single-cycle-cpu.md @@ -0,0 +1,311 @@ +# 实验一 单周期CPU + +单周期 CPU 一条指令的执行在一个时钟周期内完成。由于时钟周期是固定的,所以执行所有指令都必须和执行最慢指令耗费一样的时间,这导致单周期 CPU 性能很差。单周期 CPU 同一时刻只运行一条指令,指令间不会产生冲突,所以实现起来是最简单的。 + +本实验的目的是让大家理解 CPU 的基本结构以及 CPU 是如何执行指令的。我们会先向大家介绍一些基本概念,然后会按照指令执行的步骤逐步构造数据通路和控制单元(期间会留有填写代码的任务,请记得完成),最终构造成一个简单的单周期 RISC-V 处理器。 + +不管使用 IDE 还是执行命令,根目录是 `lab1` 文件夹。 + +## 单周期 CPU 结构图 + +数据通路用蓝线标识,控制信号用红线标识。线路和模块的命名和代码有差异。 + + +![](assets/single_cycle_cpu.drawio.svg) + + +## 数据通路 + +数据在功能部件之间传送的路径称为数据通路。路径上操作或保存数据的部件称为数据通路部件,如 ALU、通用寄存器、内存等。数据通路显示了数据从一个组件流向另一个组件的所有方式。 + +## 控制信号 + +顾名思义,控制信号控制数据通路。每当有决定要做出时,控制器单元就必须做出正确的决定并将控制信号发给相应的数据通路部件。例如:ALU 是进行加法还是减法?我们是要从内存中读取还是写入? + +那么控制器如何弄清楚需要做什么呢?这完全取决于我们正在执行的指令。在RISC-V指令格式中,控制器通过指令的 `opcode`、`funct3`、`funct7` 字段得知该做出什么决定,从而发出正确的控制信号。 CPU 原理图中的 Decoder、ALUControl、JumpJudge 三个元件都可以看作控制单元。他们接收指令并输出控制信号。控制信号引导数据通过数据路径,以便指令正确执行。 + +## 组合单元与状态单元 + +我们知道数字电路里有两大类型的电路,一种是组合逻辑电路,另外一种是时序逻辑电路。在 CPU 设计中,这两种电路构成的单元分别叫做组合单元和状态单元。本实验中只有寄存器属于状态单元(内存不属于 CPU 内核的范畴),其余的均为组合单元。 + +- 组合单元:输出只取决于当前的输入,并且不需要时钟作为触发条件,输入会立即(不考虑延时)反映到输出 +- 状态单元:存储了状态,并且以以时钟作为触发条件,时钟的上升沿到来时输入才会反映到输出 + + +## 实现方式概述 + +我们设计的 RISC-V CPU 能执行 RISC-V 指令的一个核心子集(RV32I): + +- 算术逻辑指令:`add`、`sub`、`slt` 等 +- 存储器访问指令:`lb`、`lw`、`sb` 等 +- 分支指令:`beq`、`jar` 等 + +我们将执行指令分为五个不同的阶段: + +- 取指:从内存中获取指令数据 +- 译码:弄清楚这条指令的意义,并读取寄存器数据 +- 执行:用 ALU 计算结果 +- 访存(`load`/`store` 指令):读写内存 +- 回写(除了 `store` 指令外所有指令):将结果写回寄存器 + +下面我们先按照上述步骤逐步构建数据通路部件,然后在 CPU 顶层模块将这些数据通路部件实例化并且连接起来。(下面涉及的代码都位于 `lab1/src/main/scala/riscv` 目录下) + +### 取指 + +代码位于 `core/InstructionFetch.scala` + +取指阶段要做的: + +- 根据当前 PC 寄存器的地址从内存中取出指令 +- 修改 PC 寄存器的值使其指向下一条指令 + +```scala + val pc = RegInit(ProgramCounter.EntryAddress) + + when(io.instruction_valid) { + io.instruction := io.instruction_read_data + // lab1(InstructionDecode) + // ... +``` + + +首先 PC 寄存器的值被初始化为程序的入口地址。当指令有效时,先取出当前 PC 指向的指令,然后如果需要跳转则 PC 指向跳转地址,否则指向 PC+4,请你将修改 PC 寄存器的代码补充完整。 + +!!! note "实验任务" + 请在`core/InstructionFetch.scala` 的 `// lab1(InstructionFetch)` 注释处填入代码,使其能通过 `InstructionFetchTest` 单元测试。 + +???+tips "无需显式定义时钟信号" + Chisel 3 默认每个模块都有一个隐藏的时钟信号 `clock`,模块中的每个寄存器都默认使用这个时钟信号。 + + +### 译码 + +代码位于 `core/InstructionDecode.scala` + +译码阶段要做的: + +- 读取寄存器操作数以及立即数 +- 输出控制信号 + +```scala +val rs1 = io.instruction(19, 15) +val rs2 = io.instruction(24, 20) + +io.regs_reg1_read_address := Mux(opcode === Instructions.lui, 0.U(Parameters.PhysicalRegisterAddrWidth), rs1) +io.regs_reg2_read_address := rs2 +``` +上面的代码从指令中获取寄存器操作数的编号。除了当指令为 `lui` 时寄存器 1 为 0 号寄存器以外,其他情况寄存器 1 和寄存器 2 编号均分别为指令的 `rs1` 字段(19:15)和 `rs2` 字段(24:20) + +???+tips "0 号寄存器" + 为常量 0 单独分配一个寄存器使得 RISC-V ISA 更为简单,例如赋值指令可以用一个操作数为 0 的加法代替。 + +```scala +val immediate = MuxLookup( + opcode, + Cat(Fill(20, io.instruction(31)), io.instruction(31, 20)), + IndexedSeq( + InstructionTypes.I -> Cat(Fill(21, io.instruction(31)), io.instruction(30, 20)), + InstructionTypes.L -> Cat(Fill(21, io.instruction(31)), io.instruction(30, 20)), + // ... +``` + +上面的代码获取立即数。由于不同指令类型立即数的位置不同,所以要利用 `opcode` 区分指令类型,然后获取对应位置的立即数。 + +```scala +object ALUOp1Source { + val Register = 0.U(1.W) + val InstructionAddress = 1.U(1.W) +} +// ... + +io.ex_aluop1_source := Mux( + opcode === Instructions.auipc || opcode === InstructionTypes.B || opcode === Instructions.jal, + ALUOp1Source.InstructionAddress, + ALUOp1Source.Register +) +``` +以 `ex_aluop1_source` 控制信号为例。该控制信号控制 ALU 的第一个操作数的输入。通过 `opcode` 识别指令类型,从而为 `ex_aluop1_source` 赋值。当指令类型属于 `auipc`、`jal` 或 B 类时,`ex_aluop1_source` 置为 `0`,控制 ALU 第一个操作数的输入为指令地址;其他情况 `ex_aluop1_source` 置为 `1`,控制 ALU 第一个操作数的输入为寄存器。 + +可见译码单元的设计也是很简单的组合逻辑,知晓控制信号与指令的映射关系即可完成。下面请同学们补充为 `ex_aluop2_source`、`io.memory_read_enable`、`io.memory_write_enable`、`io.wb_reg_write_source` 四个控制信号赋值的代码。 + +| 控制信号 | 含义 | +| ----------- | ----------- | +| `ex_aluop2_source` | ALU 输入来源选择 | +| `memory_read_enable` | 内存读使能 | +| `memory_write_enable` | 内存写使能 | +| `wb_reg_write_source` | 写回数据来源选择 | + +!!! note "实验任务" + 请在`core/InstructionDecode.scala` 的 `// lab1(InstructionDecode)` 注释处填入代码,使其能通过 `InstructionDecoderTest` 单元测试。 + + +### 执行 + +代码位于 `core/Execute.scala` + +执行阶段要做的是: + +- 进行ALU计算 +- 判断是否跳转 + +```scala +val alu = Module(new ALU) +val alu_ctrl = Module(new ALUControl) +// ... +io.mem_alu_result := alu.io.result +``` +上面的代码在 `Execute` 模块内实例化了 `ALU` 和 `ALUcontrol`。具体的 ALU 计算逻辑在 `ALU` 模块进行,此处只需你在 `Execute` 模块内为 `ALU` 的输入端口赋值。`ALU` 的代码位于`core/ALU.scala`。 + +!!! note "实验任务" + 请在`core/Execute.scala` 的 `// lab1(Execute)` 注释处填入代码,使其能通过 `ExecuteTest` 单元测试。 + +```scala +io.if_jump_flag := + (opcode === Instructions.jal) || + (opcode === Instructions.jalr) || + (opcode === InstructionTypes.B) && + MuxLookup( + funct3, + false.B, + IndexedSeq( + InstructionsTypeB.beq -> (io.reg1_data === io.reg2_data), + // ... +``` + +上面的代码判断是否跳转。判断是否跳转的逻辑是:如果是无条件跳转指令则直接跳转(上面代码中的 `jal`、`jalr` 指令),如果是分支指令则根据相应的跳转条件判断是否跳转(例如上面代码中的 `beq` 指令,当 `io.reg1_data === io.reg2_data` 时跳转)。跳转时将控制信号 `if_jump_flag` 置 `1`。 + + +### 访存 + +代码位于 `core/MemoryAccess.scala` + +只有 `load`/`store` 指令有访存阶段。访存阶段,读取是将内存中的数据赋给寄存器,写入反之。 + +在译码阶段,如果是 L 型指令,`memory_read_enable` 被置 `1`;如果是 S 型指令,`memory_write_enable` 被置 `1`。这两个控制信号决定了本阶段是进行读取还是写入。 + +```scala +val mem_address_index = io.alu_result(log2Up(Parameters.WordSize) - 1, 0).asUInt +``` + +首先获取读写内存的地址。 + +```scala +when(io.memory_read_enable) { + val data = io.memory_bundle.read_data + io.wb_memory_read_data := MuxLookup( + io.funct3, + 0.U, + IndexedSeq( + InstructionsTypeL.lb -> MuxLookup( + mem_address_index, + Cat(Fill(24, data(31)), data(31, 24)), + IndexedSeq( + 0.U -> Cat(Fill(24, data(7)), data(7, 0)), + 1.U -> Cat(Fill(24, data(15)), data(15, 8)), + 2.U -> Cat(Fill(24, data(23)), data(23, 16)) + ) + ), + InstructionsTypeL.lbu -> MuxLookup( + // ... +``` + +上面的代码用于读取内存。首先从总线读取数据 `data`,然后根据指令的不同对数据进行不同的处理(例如 `lb` 指令进行符号扩展,`lbu` 指令进行 0 扩展,`lh` 读取双字节等)并赋给 `io.wb_memory_read_data`,用于写回。 + +```scala +.elsewhen(io.memory_write_enable) { + io.memory_bundle.write_data := io.reg2_data + io.memory_bundle.write_enable := true.B + io.memory_bundle.write_strobe := VecInit(Seq.fill(Parameters.WordSize)(false.B)) + when(io.funct3 === InstructionsTypeS.sb) { + io.memory_bundle.write_strobe(mem_address_index) := true.B + io.memory_bundle.write_data := io.reg2_data(Parameters.ByteBits, 0) << (mem_address_index << log2Up(Parameters.ByteBits).U) + }.elsewhen(io.funct3 === InstructionsTypeS.sh) { + // ... +``` + +上面的代码用于写入内存。根据指令的不同,写入的数据会经过不同的处理(`sw` 取 32 位、`sh` 取 16 位、`sb` 取 8 位)。 + + +### 写回 + +代码位于 `core/WriteBack.scala` + +写回阶段,将计算得到的数据或内存读取的数据写入寄存器。 + +写回模块只是一个多路选择器,代码非常简单,不再赘述。不过请你思考一个问题:写使能信号在译码阶段就产生了,此时正确的写回数据还没有算出(或从内存读出),那么错误的写回数据会被写入寄存器堆吗?为什么? + +## 组合成 CPU + +代码位于 `core/CPU.scala` + +我们已经实现了构建 CPU 所需要的所有部件。下面我们需要按照单周期 CPU 结构图将上面的部件实例化并且连接起来。 + +```scala +class CPU extends Module { + val io = IO(new CPUBundle) +``` +CPUBundle 是 CPU 和内存等外设进行数据交换的通道。 + +```scala + val regs = Module(new RegisterFile) + val inst_fetch = Module(new InstructionFetch) + val id = Module(new InstructionDecode) + val ex = Module(new Execute) + val mem = Module(new MemoryAccess) + val wb = Module(new WriteBack) +``` + +这里实例化了不同执行阶段的各个模块。 + +```scala + inst_fetch.io.jump_address_id := ex.io.if_jump_address + inst_fetch.io.jump_flag_id := ex.io.if_jump_flag +``` +以上面两个线路为例,大家在 CPU 原理图中可以看到相应的线路。 +` +![](assets/connect.png) + +请你观察 `Execute` 模块输入端口代码以及 CPU 结构图,填写 `Execute` 模块的输入与其他模块输出的连线。 + +!!! note "实验任务" + 请在 `core/CPU.scala` 的 `// lab1(CPU)` 注释处填入代码,并在lab1下运行 `sbt test`。 + +`sbt test` 会执行包括 CPUTest 以及上述所有单元测试,这一步完成后我们就成功构造出了一个 RISC-V 单周期 CPU 了! + +## 使用自定义应用测试 + +TODO: 更新链接 + +参考[编译和链接的过程](../tutorial/compile-and-link.md)以及[CMake 入门](../tutorial/cmake.md),编写并编译自己的程序后,修改 `CPUTest` 中的 `InstructionROM` 的指令文件地址,即可运行你自己的程序。 + +如果想要烧录自己的程序到 FPGA 板子上,只需要修改 `board/<板子型号>/Top.scala` 中的 `binaryFilename` 为你生成的程序二进制文件名即可。 + +## 实验报告 + + + + + +1. `src\test\scala\riscv\singlecycle` 中的测试文件 `ExecuteTest.scala`, `InstructionDecoderTest.scala` 和 `InstructionFetch.scala` 分别对相应部件进行单元测试,以确保单个部件的正确性。请您选择其中至少一个测试,并 + 1. 简述测试文件中的语句是如何测试部件正确性的; + 2. 在测试波形图上说明部件运行过程中关键信号的变化。 +2. 测试文件 `CPUTest.scala` 测试了整个 CPU 运行三个程序的正确性。请您选择其中至少一个程序的运行,并 + 1. 概述该程序做了什么,执行结果如何被检查; + 2. 在测试波形图上简单分析其执行,并说明检查执行结果时的波形。 +3. 说明您在完成实验的过程中,遇到的实验指导不足或改进建议。 + + diff --git a/docs/docs/better-tut/labs/lab2/lab2-interrupt.md b/docs/docs/better-tut/labs/lab2/lab2-interrupt.md new file mode 100644 index 0000000..227bd7c --- /dev/null +++ b/docs/docs/better-tut/labs/lab2/lab2-interrupt.md @@ -0,0 +1,178 @@ +# 实验二 中断 + +[//]: # (完成流水线 CPU 实验后,你就已经对基于流水线 CPU 的原理和设计有初步认识了。但是这个简单的 CPU 只能按照预先的程序指令一直运行,无法中途打断。然而,我们生活的世界充满了不确定性,一个实用的 CPU 需要能够时刻准备好处理来自外部的事件,及时处理中断,并返回到原来的程序中继续执行。) + +完成单周期 CPU 实验后,你获得了一个可以简单的按照预期指令执行的处理器。但是这个简单的 CPU 只能按照预先的程序指令一直运行,无法中途打断。然而,我们生活的世界充满了不确定性,一个实用的 CPU 需要能够时刻准备好处理来自外部的事件,及时处理中断,并返回到原来的程序中继续执行。 + +在本实验中,你将学习到: + +- CSR 寄存器以及其操作命令 +- 中断控制器的原理和设计 +- 编写一个简单的定时中断发生器 + +下面的内容中,不管使用 IDE 还是执行命令,根目录是 `lab2` 文件夹。 + +## CSR指令支持 + +从预备知识[中断与异常](../../theory/interrupt-and-exception.md)我们已经学习到了 CSR 寄存器的基本概念。本实验里我们在单周期 CPU 的基础上增加对 CSR 寄存器的操作指令的支持。 + +CSR相关操作指令集的细节,包括指令的语义,编码等,都可以通过阅读[非特权集手册](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf)的第九章获得。 +中断相关的具体CSR寄存器的内容与对应含义,请查阅[特权级手册](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf) + +有了实现单周期 CPU 的经验,我们可以把对 CSR 指令的支持分解为: + +- **CSR 寄存器组**:CSR 寄存器是一组类似于 RegisterFile 寄存器组的,地址空间大小为 4096 字节,独立编址的寄存器。 + 从指令手册可以看到对CSR寄存器的操作都是原子读写的,CSR指令具体的语义请查阅手册。 + CSR寄存器组需要根据ID模块译码后给出的控制信号和CSR寄存器地址,来对内部寄存器进行寻址,获取其内容并且修改。 +- **ID 译码单元**:ID 译码单元需要识别 CSR 指令,根据手册里描述的指令语义和编码规范,产生相应的传给其它模块的控制信号与数据。 +- **EX 执行单元**:CSR 指令都是原子读写的,即一条指令的执行结果中,既要把目标 CSR 寄存器原来的内容写入到目标通用寄存器中,还要按指令语义把从目标 CSR 寄存器读出来的内容修改之后再写回给该CSR 寄存器。此时 EX 里面的 ALU 单元是空闲的,要得到写入 CSR 寄存器的值,可以复用 ALU,也可以不复用。 +- **WB 写回单元**:支持 CSR 相关操作指令后,写回到目标通用寄存器的数据来源就多了一个从目标 CSR 寄存器读出来的修改前的值。 + + + + + +## 中断控制器(CLINT) + +CLINT(Core-Local Interrupt)是 RISC-V 架构中提供简单中断和定时器功能的中断处理器,其在中断或异常发生且中断开启时,将暂停CPU当前执行流,设置好相关 CSR 寄存器信息后跳转到中断处理程序中执行中断处理程序。 + +关键就是该保存哪些信息到对应的CSR寄存器中,答案就是 CPU 执行完当前指令后的下一个状态,比如当前指令是跳转指令,那么 `mepc` 保存的应该是当前跳转指令的跳转目标地址;以及一些关于中断发生原因的信息。 + +本实验中,我们希望实现一个支持基本功能的、简单的 CLINT,对于一些特殊情况我们规定: + +1. 中断到来时,我们认为应该让当前指令执行完后,再跳转到中断处理程序。 +2. `mstatus` 的 `mie`位(machine interrupt enable)记录中断使能,即是否响应中断。如果当前正在执行一条指令以关闭中断使能,但此刻发生外部中断,则规定这种情况不应响应中断。 +3. 我们不考虑嵌套中断的情况,即中断处理过程中忽略到来的中断。 +4. 仅考虑在 Machine 特权级下的情况。 + + + + +接下来,我们讨论 CLINT 要处理的情况,包括 进入中断处理 和 完成中断处理。 + + +### 进入中断处理 + +#### 1. 响应硬件中断 + +此处将外部设备和定时器的中断归为一类讨论。响应硬件中断时,CPU的工作具体分为两部分: + +保存现场,记录CPU当前正在执行程序的下一条指令地址,以便中断处理后恢复执行;以及记录中断信息和设置相应权限。这对应了下面 CSR 上的操作: + +- `mepc`:保存的是中断或者异常处理完成后,CPU返回并开始执行的地址。所以对于异常和中断,`mepc` 的保存内容需要注意。 +- `mcause`:保存的是导致中断或者异常的原因,具体代码请查阅特权级手册。 +- `mstatus`:在响应中断时,需要将 `mstatus` 寄存器中的 `MPIE` 标志位设置为 `0`,禁用中断。 + +然后从 `mtvec` 获取中断处理程序的地址,跳转到该地址执行中断处理程序。 + + + +!!! info "扩展知识:操作系统与硬件配合处理中断" + 事实上,中断的处理需要操作系统和硬件的紧密配合。在中断发生前,操作系统就要将中断处理程序写入内存并设置中断跳转向量到 `mtvec` 。 + 中断发生时,CPU保存现场并设置中断信息,操作系统还需保存进程上下文并进行资源调度等工作,完成中断处理也类似。 + 后续操作系统课程中你将对此有更全面深入的了解。 + + + + + + +#### 2. 响应软件中断 + +`ecall` 和 `ebreak` 指令是触发软件中断的两条 environment 指令。`ecall` 常用于系统调用,例如请求磁盘读写,`ebreak` 更多用于调试。 +详见非特权级手册第2章的 Environment Call and Breakpoints 。 + +这两条指令都将触发中断,CSR 的设置操作与响应硬件中断的相似。不同的是,`mcause` 的设置应参照 environment 相应的代码设置。 + + + +### 完成中断处理 + +不论任何原因进入到中断处理程序后,都需要使用 `mret` 指令以恢复 CPU 原先程序的执行。 +这时候其实干的事与响应中断是大致相同的,只不过需要写入的寄存器只有 `mstatus`,跳转的目标地址则是从 `mepc` 获取。 + + +为简单起见,对于 `mret` 时 `mstatus` 的要写入的值,就把 MIE 位置为 MPIE 位(Machine Previous Interrupt Enable)。若 MPIE 为 1 的话 `mret` 就会恢复中断,如果 MPIE 为 0 的话,`mret` 则不改变 `mstatus` 的值。 + +上述实现没有考虑嵌套中断,且以后实现特权级切换的时侯,`mstatus` 的改变更为复杂。中断的实现在手册中有明确的标准,想进一步了解中断机制的实现可以看特权级手册的 3.1.6.1 小节(Privilege and Global Interrupt-Enable Stack in mstatus register) 以及 9.6节(Traps)。 + +而异常的现场保存和恢复与中断处理差不多,只不过保存和恢复的内容上有所不同而已,感兴趣的同学可以自行摸索。 + + +### 关于 CLINT 的实现 + +CLINT 具体的实现方法很多,我们采用纯组合逻辑实现这个中断控制器。由于基于单周期 CPU 且 CLINT 是组合逻辑,所以外部中断到来时,CLINT 会马上响应。 +目前 YatCPU 的主仓库的多周期CPU中的 CLINT 采用状态机来实现。 + +CLINT 需要一个周期就把多个寄存器的内容修改的功能,而正常的 CSR 指令只能对一个寄存器读-修改-写(Read-Modify-Write, RMW)。所以 CLINT 和 CSR 之间有独立的优先级更高的通路,用来快速更新 CSR 寄存器的值。 + + + + + +*** + + + + +## 简单的定时中断发生器 + +我们要实现一个 MMIO 的定时中断发生器——Timer。 + +MMIO (Memory Mapped I/O)简单来说就是:该外设用来和 CPU 交互的寄存器是与内存一起编址的,所以 CPU 可以通过访存指令(load/store)来修改这些寄存器的值,从而达到 CPU 和外设交互的目的。 + +目前在没有实现总线的情况下,使用多路选择器实现 MMIO 即可达到目的。原因主要是我们把取指令的操作和 load/store 访存操作分开了,让 Memory 有单独的一个通路进行取指令操作。 +因此我们的模型还是一个 CPU 对多个外围设备,不会出现 CPU 的取指操作与访存操作冲突争抢外设的情况。 + +所以我们 CPU 发出的逻辑地址要发送到哪个设备,就由逻辑地址的高位作为外围设备的位选信号即可,低位则用于设备内部的寻址。 + +此外还有定时中断发生器的内部逻辑:两个控制寄存器 `enable` 寄存器和 `limit` 寄存器。 + +- `enable` 寄存器用来控制定时中断发生器的使能,为 `false` 则不产生中断, 映射到地址空间的逻辑地址为 0x80000008。 +- `limit` 寄存器用来控制定时器的中断发生间隔,映射到地址空间的逻辑地址为 0x80000004。中断发生器内部有一个加一计数器,当计数器的值到达 `limit` 为标准的界限时,定时器会发生一次中断信号(`enable` 使能情况下)。注:产生中断信号的时长没有太大关系,但是至少应该大于一个 CPU 时钟周期,确保 CPU 能够正确捕捉到该信号即可。 + +## 实验任务 + +!!! note "实验任务:支持中断处理" + + 1. 使 EX 单元支持 CSR 指令的运算。 + 2. 使 CSR 寄存器组支持 CLINT 和来自 CSR 指令的读写操作。 + 3. 使 CLINT 支持响应中断并且在中断结束后回到原来的执行流。 + 4. 使定时中断发生器可以正确产生中断信号,并且实现 Timer 寄存器的 MMIO。 + + 请在 `// lab2(CLINTCSR)` 注释处完成上述支持,并通过 `CPUTest`、`ExecuteTest`、`CLINTCSRTest`、`TimerTest` 测试。 + + + +EX 执行单元的代码文件位于 `src/main/scala/riscv/core/Execute.scala` + +CSR 寄存器组的代码文件位于 `src/main/scala/riscv/core/CSR.scala` + +CLINT 的代码文件位于 `src/main/scala/riscv/core/CLINT.scala` + +Timer 的代码位于 `src/main/scala/riscv/peripheral/Timer.scala` + +!!! tip + IDEA 可以双击 shift,vscode 可以 shift+P 以打开文件快速搜索面板。 + + + + + +*** + +## 实验报告 + + +1. `CLINTCSRTest.scala` 中添加了 CLINT 处理硬件终端和软件中断的两个测试,请您选择至少一个,并: + 1. 简述这个测试通过给部件输入什么信号,以测试 CLINT 的哪些功能? + 2. 在测试波形图上,找到一次从开始处理中断到中断处理完成的波形图,并挑选其中关键的信号说明其过程。例如硬件中断的测试中,有在跳转指令和非跳转指令下的两次中断处理测试;软件测试则分别测试了 `ecall` 和 `ebreak` 两次中断,选择其中一次即可。 +2. `CPUTest.scala` 中新增了 `SimpleTrapTest`,其执行 `csrc/simpletest.c` 的程序。请您: + 1. 简述该测试程序如何测试 CPU 的中断处理正确性。 + 2. 在测试波形图上找出说明该程序成功执行的信号。 +3. 说明您在完成实验的过程中,遇到的实验指导不足或改进建议。 + +## CPU架构图 + +![](images/single_cycle_CPU_with_interrupt_support.png) diff --git a/docs/docs/better-tut/practice/assets/bash-echo-path.png b/docs/docs/better-tut/practice/assets/bash-echo-path.png new file mode 100644 index 0000000..6a3f86b Binary files /dev/null and b/docs/docs/better-tut/practice/assets/bash-echo-path.png differ diff --git a/docs/docs/better-tut/practice/assets/cheatsheet-page1.png b/docs/docs/better-tut/practice/assets/cheatsheet-page1.png new file mode 100644 index 0000000..958c13a Binary files /dev/null and b/docs/docs/better-tut/practice/assets/cheatsheet-page1.png differ diff --git a/docs/docs/better-tut/practice/assets/cheatsheet-page2.png b/docs/docs/better-tut/practice/assets/cheatsheet-page2.png new file mode 100644 index 0000000..1907f89 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/cheatsheet-page2.png differ diff --git a/docs/docs/better-tut/practice/assets/cmd-echo-path.png b/docs/docs/better-tut/practice/assets/cmd-echo-path.png new file mode 100644 index 0000000..ac63d2b Binary files /dev/null and b/docs/docs/better-tut/practice/assets/cmd-echo-path.png differ diff --git a/docs/docs/better-tut/practice/assets/open-cmd-windows.png b/docs/docs/better-tut/practice/assets/open-cmd-windows.png new file mode 100644 index 0000000..33c4851 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/open-cmd-windows.png differ diff --git a/docs/docs/better-tut/practice/assets/open-pwsl-windows.png b/docs/docs/better-tut/practice/assets/open-pwsl-windows.png new file mode 100644 index 0000000..ab5e31f Binary files /dev/null and b/docs/docs/better-tut/practice/assets/open-pwsl-windows.png differ diff --git a/docs/docs/better-tut/practice/assets/open-terminal-linux.png b/docs/docs/better-tut/practice/assets/open-terminal-linux.png new file mode 100644 index 0000000..171efa0 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/open-terminal-linux.png differ diff --git a/docs/docs/better-tut/practice/assets/powershell-echo-path.png b/docs/docs/better-tut/practice/assets/powershell-echo-path.png new file mode 100644 index 0000000..9b82090 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/powershell-echo-path.png differ diff --git a/docs/docs/better-tut/practice/assets/win-modify-envvar-1.png b/docs/docs/better-tut/practice/assets/win-modify-envvar-1.png new file mode 100644 index 0000000..7512760 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/win-modify-envvar-1.png differ diff --git a/docs/docs/better-tut/practice/assets/win-modify-envvar-2.png b/docs/docs/better-tut/practice/assets/win-modify-envvar-2.png new file mode 100644 index 0000000..4918dc5 Binary files /dev/null and b/docs/docs/better-tut/practice/assets/win-modify-envvar-2.png differ diff --git a/docs/docs/better-tut/practice/envvar-and-cmd.md b/docs/docs/better-tut/practice/envvar-and-cmd.md new file mode 100644 index 0000000..61c4126 --- /dev/null +++ b/docs/docs/better-tut/practice/envvar-and-cmd.md @@ -0,0 +1,135 @@ + + +# 环境变量、Shell 与 常用指令 + +Writen By: [PurplePower](https://github.com/PurplePower) + +本节将介绍使用计算机的基本技巧:了解并设置环境变量及常用的指令。这些是计算机学科实践的必要技能,也在后面实验中频繁用到。 + + +## 环境变量 + +环境变量(Environment Variables)是一系列用户定义的变量,可以影响进程的运行。环境变量就如同 C++ 中声明一个变量,它可以被操作系统和各种程序读取和修改,以帮助程序顺利执行。 + +环境变量的概念在 Windows 和 Linux 上都存在,但它们的语法和使用略有区别,我们通过一些实际操作来认识他们。 + +首先我们来打开一个终端窗口(Terminal): + +???+ abstract "实践:打开一个终端窗口" + + === "Windows CMD" + ![打开一个Windows CMD 窗口](assets/open-cmd-windows.png){width=70%} + + 如图,使用 Win+R 快捷键调出运行窗口,输入 cmd 即可打开命令行窗口。 + === "Windows Powershell" + ![打开一个 Powershell 终端窗口](assets/open-pwsl-windows.png){width=80%} + + 如图,使用 Win+R 快捷键调出运行窗口,输入 powershell 即可打开命令行窗口。您也可以使用其他终端软件,如 Windows Terminal等。 + === "Linux" + ![打开一个 Linux 终端窗口](assets/open-terminal-linux.png){align=left, width=70%} + + 如图,如果您使用带 GUI 界面的 Linux,如 Ubuntu,则可以在桌面栏点击 Terminal 图标以打开一个可以输入文字的终端窗口。 + +下一步我们将使用 `echo` 指令,一条在上述三种 shell 中都可使用的打印指令,我们直接看在不同 shell 下打印的指令,及得到的结果: + +???+ abstract "实践:打印 PATH 环境变量" + + === "Windows CMD" + ```bat + echo %PATH% + ``` + + ![cmd 输出 PATH 环境变量内容](assets/cmd-echo-path.png) + + === "Windows Powershell" + ```powershell + echo $env:PATH + ``` + + ![powershell 输出 PATH 环境变量内容](assets/powershell-echo-path.png) + + === "Linux" + ```bash + echo $PATH + ``` + + ![bash 输出 PATH 环境变量内容](assets/bash-echo-path.png) + +可以看到,在这几种 shell 中,如何指代环境变量的方法各有不同:Windows 上的 `cmd` 使用百分号来包裹环境变量名 `%%`,`powershell` 则使用 `$env:`,而 Linux 上的 `bash` 则使用 `$`。另外,Windows 和 Linux 的环境变量中多个值的分隔符也不同,Windows 使用分号 `;`,因为引号会作为盘符后的分隔符而存在于路径中,Linux 则使用引号 `:`。此外还有一个需要注意的点是,Windows上的环境变量名不区分大小写,而 Linux 上则是大小写敏感的。 + +--- + +## PATH 环境变量及其修改 + +您已经学会使用 echo 指令查看环境变量名了,那么这些环境变量具体有何用途呢?下面将介绍 PATH 环境变量,该变量在系统运行中起着至关重要的作用,包括后面您安装软件后如何在终端直接调用该软件。 + +PATH 环境变量是系统保留的环境变量名,它包含了一系列目录路径,当您在输入一个不是完整绝对路径的指令时,系统将会在 PATH 包含的目录下寻找该指令对应的程序。 +例如在 Windows 中输入 `notepad` 指令时,系统会在 PATH 包含的目录中寻找名为 `notepad` 的可执行程序,并最终在 `C:\Windows\system32\` 目录下找到 `notepad.exe` 的记事本程序,并最终启动。如果未找到,则系统会提示输入的内容无法被识别为指令,此时您需要在 PATH 中添加对应的路径,或者使用完整的路径名以执行该程序: `C:\Windows\system32\notepad.exe` 。 + + +为了能正确运行某些程序,必须对 PATH 系统变量进行修改。一般只在 Windows 上需要,Linux 在安装大多数软件时已经由系统的软件管理器完成修改。下面介绍永久修改系统变量的方法,使该修改在关闭终端或重启后仍然生效。 + + +???+ abstract "实践:永久修改系统变量" + + === "Windows" + + 您可按下图步骤打开系统变量编辑窗口: + + ![打开环境变量面板](assets/win-modify-envvar-1.png) + + 随后,您可以修改系统变量 PATH。例如,当您希望能直接在命令行键入 `vivado` 以正确调用 `E:\Xilinx\Vivado\2020.1\bin\vivado.bat` 时,您应添加该文件所在路径 `E:\Xilinx\Vivado\2020.1\bin`,而非包含文件名的路径。 + + ![修改系统变量 PATH](assets/win-modify-envvar-2.png) + + 随后,您需要逐步点击“确定”以保存修改。系统变量修改后,您需要重新打开命令行或 powershell 以使其生效。您可以重新打印 PATH 变量以检查是否正确设置。 + + === "Linux" + + !!! warning + + 本课程中,若您使用 Linux 环境,您***无需***修改 PATH 变量。 + Linux 软件安装可以通过软件管理器,如 Ubuntu 的 apt、CentOS 的 yum 等,或从源文件安装(install from source)。前者会自动管理相应的 PATH 变量,后者大多使用 make 构建系统中会包含自动设置的内容。 + + 除非您清楚您确需修改 PATH 变量,否则您不应修改以免导致意外的故障。您可以 Google 搜索 How to permanently set $PATH on Linux ,或询问大语言模型(注意辨别其回复的正确性)以操作。 + + +但有时我们只希望对某些系统变量的修改是暂时的,比如在进行环境配置的调试时,此时可以在 shell 中直接修改系统变量的值,该修改在只会在这个终端窗口的 shell 中生效,且关闭后就失效。 + +???+ abstract "实践:暂时修改系统变量" + + === "Windows CMD" + ```bat + set PATH=%PATH%;D:\new\added\path + ``` + === "Windows Powershell" + ```powershell + $env:PATH+=";D:new\added\path" + ``` + === "Linux" + ```bash + export PATH=$PATH:/home/new/added/path + ``` + + +--- + +## 常用指令 + +接下来您将了解常见的指令。 + +在您学习这门课前,您可能极少使用纯文本的 shell 界面进行操作,而是使用 GUI (Graphical User Interface)通过鼠标直观地操作。两种方式各有其优劣,但作为计算机专业的学生,在 shell 中使用各种指令进行操作是不可或缺的技能,熟练后这将比使用 GUI 更高效。 + +以下常见功能在不同系统上的指令如下表所示,您可以在终端中尝试输入并查看结果。 + + +| 功能 | Windows cmd | Powershell | Linux | +| :--- | ----------- | --------- | ---------- | +| 列出目录下内容 | `dir` | `ls` | `ls` | +| 打印变量或字符串 | `echo ` {colspan=3, align=center} | +| 修改当前目录(change directory)| `cd `,可用 `cd ..` 快速返回上一级目录 {colspan=3, align=center} | +| 设置变量 | `set MYVAR="VALUE"` | `$MYVAR="Value"` | `export MYVAR=Value` | +| 清空当前显示内容 | `cls` | `cls` | `clear` | +| 查询文件路径 | `where gcc` | `Get-Command gcc` | `which -a gcc` 或 `whereis gcc` | + +当然,还有许多指令选项及搭配组合未列出。若您想要实现某些功能,但不知道要使用哪些指令,则您可以向 Google 或大语言模型提问: how to ... in linux bash / cmd 。若您已经知道使用何种指令,但需要调整指令的选项,例如上方 linux 里 `which` 的 `-a` 选项是显示所有结果,则您可以查询该指令的 manual ,一些网页记录了这些内容,如 [Man7 linux manual](https://man7.org/linux/man-pages/index.html) 。 diff --git a/docs/docs/better-tut/practice/scala-and-chisel.md b/docs/docs/better-tut/practice/scala-and-chisel.md new file mode 100644 index 0000000..41665d0 --- /dev/null +++ b/docs/docs/better-tut/practice/scala-and-chisel.md @@ -0,0 +1,108 @@ +# Scala 和 Chisel 编程语言 + +Writen By: [Howard Lau], [Tiger9Tu], [PurplePower](https://github.com/PurplePower) + +> 工欲善其事,必先利其器。 + + +## Chisel 3 的基本语法和功能 + +Chisel 3 是使用 Scala 语言的**宏**特性实现的一门领域特定语言(Domain Specific Language)。所以,所有涉及到电路逻辑的编程,都需要使用 Chisel 3 包中提供的宏定义实现,而不能直接使用 Scala 语言的关键词。具体而言,如果想要在电路中使用一个常量,例如 `12`,并将其与电路 `value` 比较,我们不可以写 `if (value == 12)`,而是要写 `when(value === 12.U)`。熟记这一最基本的概念,才能正确编写出合法的 Chisel 3 代码。 + +### 基本类型 + +和其他编程语言类似,Chisel 3 也支持一些基本类型,包括整数、布尔值等。 + +### 无符号整数与有符号整数 + +Chisel 3 中无符号整数类型为 `UInt`,有符号整数类型为 `SInt`。在声明整数的时候,可以通过 `.W` 来指定整数的位宽,例如 `UInt(8.W)`。如果想将一个 Scala 中的整数转换为 Chisel 3 的硬件整数,可以使用 `.U` 操作符,例如,`12.U`。`.U` 操作符同样支持通过 `.W` 指定位宽,例如 `12.U(8.W)`。 + +### 布尔值 + +Chisel 3 中布尔值类型为 `Bool`,可以通过 `.B` 来将 Scala 布尔值转换为硬件布尔值,例如 `true.B`。 + +### 模块 + +在 Chisel 3 中,声明一个模块需要继承 `Module` 类,并通过 `io` 成员声明模块的输入输出端口。 + +```scala +import chisel3._ + +class MyModule extends Module { + val io = IO(new Bundle { + val in = Input(UInt(8.W)) + val out = Output(UInt(8.W)) + }) +} +``` + +在 Chisel 3 中,每个模块除了声明的 IO 端口,还隐含了时钟信号 `clock` 和复位信号 `reset`。 + +### 组合逻辑 + +```scala +val wire = Wire(UInt(8.W)) +val wireinit = WireInit(0.U(8.W)) +``` + +### 时序电路 + +### 寄存器 + +```scala +val reg = Reg(UInt(8.W)) +val reginit = RegInit(0.U(8.W)) +``` + +Chisel 3 中的寄存器默认也带有 `clock` 和 `reset` 信号,所有对于寄存器的写入操作都将在时钟的上升沿生效,并在复位信号为 `true` 时复位为初始值(如果指定了的话)。 + +### Chisel 3 与 Verilog 的关系 + +Chisel 3 严格意义上并不是 Verilog 的等价替代,而是一个生成器语言。使用 Chisel 3 编写的硬件电路,需要编译成 Verilog 文件,然后再通过 EDA 软件综合实现出真正的电路。而且,为了通用性,一些 Verilog 中的特性,如负边缘触发、多时钟仿真等在 Chisel 3 中并不支持,或支持程度有限。 + + +## Chisel 3 项目结构 + +同学们在进行本次实验的过程中无需了解整个项目的所有文件,只需要专注于 `src/main/scala` 下的 `.scala` 代码文件即可。不过为了避免困惑,还是在此为大家介绍一下本项目的结构。 +``` +- labx + - coremark # CPU 性能测试 + - csrc # 存放汇编语言和 C 语言源代码 + - project # sbt 的插件以及配置文件 + - src + - main + - scala # Chisel 3 源代码 <-- + - resources # 资源文件 + - test + - scala # Chisel 3 测试代码 + - target # sbt 生成的文件 + - test_run_dir # 运行测试时生成的文件 + - verilog # Verilog 代码 + - vivado # tcl 脚本以及约束文件(用于自动化烧板) + - build.sbt # sbt 配置文件 +``` + +## 练习 + +!!! abstract "实践:动手练习 Chisel" + + 完成 [Chisel 3 在线实验](https://mybinder.org/v2/gh/freechipsproject/chisel-bootcamp/master) 的 `3.6_types` 以及之前的所有练习,这样您将具备基本的硬件设计能力。 + +## 参考资源和 Cheat Sheet + +尽管上网搜索或询问 LLM 可以很快找到需要的 Scala 和 Chisel 语法,但可能更快的方法是查找 Cheat Sheet,里面用简短的数页列出最常用的操作用法。以下是一些 Cheat Sheet,您可以点击下载或上网搜索其他版本: + +[free chips project Chisel 3.6.0 Cheat Sheet](https://github.com/freechipsproject/chisel-cheatsheet/releases/download/3.6.0/chisel_cheatsheet.pdf) + + +![Cheatsheet1](assets/cheatsheet-page1.png) + +![Cheatsheet2](assets/cheatsheet-page2.png) + + + +若您需要深入了解 Chisel 中的相关内容,您可以查阅 Chisel Book,改书适合新手入门 Chisel 并通过实践逐步深入。 +在本课程的学习中,您可以结合前述在线练习,简要地从本书了解 Chisel;或在遇到语法困难时才查询此书。 +[Chisel Book(有中文版)](https://www.imm.dtu.dk/~masca/chisel-book.html) + + diff --git a/docs/docs/better-tut/theory/cpu-arch.md b/docs/docs/better-tut/theory/cpu-arch.md new file mode 100644 index 0000000..497b40b --- /dev/null +++ b/docs/docs/better-tut/theory/cpu-arch.md @@ -0,0 +1,85 @@ +# 处理器架构 + +By: [:material-github: howardlau1999](https://github.com/howardlau1999) + +## 微架构 + +本项目支持的指令集是 [RISC-V](https://riscv.org/) 的 RV32I 整数运算指令集。微架构设计上,有两种可供选择:一种是和 [Z-scale](https://github.com/ucb-bar/zscale) 以及 [riscv-mini](https://github.com/ucb-bar/riscv-mini) 类似的三级流水线单发射顺序执行架构,另一种是和《Computer Organization and Design, RISC-V edition》中描述的 CPU 类似的五级流水线单发射顺序执行架构。 + +### 三级流水线 + +流水线总体架构分为取指(IF)、译码(DE)、执行/写回/访存(EX、WB、MEM)三个阶段,如下图所示: + +![pipeline](images/pipeline.png) + +### 五级流水线 + +流水线总体架构分为取指(IF)、译码(DE)、执行(EX)、访存(MEM)和写回(WB)五个阶段,如下图所示: + +![pipeline](images/five_stage_pipeline_cpu_structure.png) + +#### 取指(InstructionFetch) + +取指单元包含程序计数器(PC)。在每个时钟上升沿,若 PC 阻塞信号 `PCStall` 有效,则 PC 的值保持不变;否则若跳转标志 `JumpFlag` 有效,则 PC 的值变为跳转目标地址 `JumpAddress`;否则 PC 的值加 4。 + +取指阶段根据 PC 的值通过 AXI4Lite 总线从内存中读取指令并将指令和地址向后传递。由于 CPU 复位时将指令从 ROM 载入内存、通过总线从内存读取每条指令都需要若干时钟周期,在指令未就绪期间,它向后传递空指令 `nop`,并且阻塞 PC(令 `StallFlagIF` 有效)。 + +#### 译码(InstructionDecode) + +译码单元根据指令产生控制信号、拓展立即数并且从寄存器堆或 CSR 读取数据。另外,为了减少跳转指令的代价,将分支指令的判断提前到译码阶段,并且在本阶段向取指单元传递跳转目标地址 `JumpAddress` 和跳转控制 `JumpFlag`。译码单元对中断信号的处理可以视为在原本处理的指令之上“叠加了一条无条件跳转指令”:若 `IntAssert` 信号有效,则将 `JumpAddress` 设为 `IntHandlerAddress`,且令 `JumpFlag` 有效。 + +#### 执行(Execute) + +执行单元包含 ALU 和 ALUControl。ALUControl 根据指令的 `opcode`、`funct3` 和 `funct7` 字段产生 ALU 控制信号,ALU 根据控制信号 `ALUOp1Src` 和 `ALUOp2Src` 对两个操作数进行相应的运算。ALU 的操作数 1 从寄存器 `rs1` 数据 `Reg1Data`、指令地址 `InsAddr`、来自访存单元的转发 `ForwardFromMEM` 和来自写回单元的转发 `ForwardFromWB` 中选取;ALU 的操作数 2 从寄存器 `rs2` 数据 `Reg2Data`、立即数 `Immediate`、来自访存单元的转发 `ForwardFromMEM` 和来自写回单元的转发 `ForwardFromWB` 中选取。对于读写 CSR 的指令,在执行阶段还有对 CSR 的写入。 + +#### 访存(MemoryAccess) + +对于 `load` 或 `store` 指令,访存单元根据存储器读使能 `MemoryRE` 和存储器写使能 `MemoryWE` 通过 AXI4Lite 总线对内存进行读或写,在总线通信期间需要阻塞流水线(令 `StallFlagMEM` 有效)。另外,访存单元还要产生转发给执行单元的数据 `ForwardFromMEM`。 + +#### 写回(WriteBack) + +写回单元根据控制信号 `RegsWriteSrc` 从 ALU 运算结果 `ALUResult`、内存读取的数据 `MemoryReadData`、CSR 读取的数据 `CSRRD`、当前指令地址 `InsAddr` +4 中选择写回寄存器堆的数据 `RegsWD`,这个数据也作为转发给执行单元的数据 `ForwardFromWB`。 + +#### 流水线寄存器(XX2XX) + +流水线寄存器位于两个流水级之间,用于暂存数据和控制信号。有两个信号用于控制流水线寄存器:当 `XXStall` 为高电平时,该级流水线寄存器的值不变;当 `XXFlush` 为高电平时,该级流水线寄存器的值清零;当这两个信号都为低电平时,流水线寄存器在时钟上升沿锁存输入值。 + +#### 转发(Forwarding) + +转发单元根据执行单元、访存单元和写回单元的控制信号判断是否需要将访存单元或写回单元的数据转发到执行单元,判断过程可以用伪代码描述如下: + +``` +if (RegWEFromMEM && RdFromMEM != 0 && RdFromMEM == Reg1RA) { + ALUOp1Forward = ForwardingType.ForwardFromMEM +} else if (RegWEFromWB && RdFromWB != 0 && RdFromWB == Reg1RA) { + ALUOp1Forward = ForwardingType.ForwardFromWB +} else { + ALUOp1Forward = ForwardingType.NoForward +} + +if (RegWEFromMEM && RdFromMEM != 0 && RdFromMEM == Reg2RA) { + ALUOp2Forward = ForwardingType.ForwardFromMEM +} else if (RegWEFromWB && RdFromWB != 0 && RdFromWB == Reg2RA) { + ALUOp2Forward = ForwardingType.ForwardFromWB +} else { + ALUOp2Forward = ForwardingType.NoForward +} +``` + +通过转发可以解决执行单元处理的指令的源操作数是前两条指令的 ALU 运算结果或前面第二条指令的访存结果而产生的数据冒险。若本条指令的源操作数是上一条指令的访存结果,无法通过转发消除数据冒险,需要将流水线阻塞一个时钟周期,由下文叙述的控制单元进行检测和处理。 + +#### 控制(Control) + +控制单元接收来自 PC、取指单元、译码单元和执行单元的阻塞请求 `StallFlagXX` 并阻塞相应的流水级。另外,控制单元若检测到执行单元处理的指令的源操作数是上一条指令访存结果,将把流水线阻塞一个时钟周期,并且清除流水线寄存器 ID2EX (`IDFlush`)。对于跳转指令,若 `JumpFlag` 有效,还需清除流水线寄存器 IF2ID(`IFFlush`)。 + +#### 中断(CLINT) + +内核中断控制器 (CLINT)接收来自外设的中断信号 `InterruptFlag`,向控制单元发出阻塞流水线的请求 `StallFlagClint`,并将取指单元中当前 PC 值 `InsAddr`(若来自译码单元的 `JumpFlag` 有效,则是跳转指令的目标地址 `JumpAddress`)保存到 `MEPC`,将中断原因保存到 `MCAUSE`,修改 `MSTATUS`;然后向译码单元发送中断信号 `IntAssert` 和中断处理程序地址 `IntHandlerAddress`(`mtvec` 的值)。如上文所述,译码单元接收到 `IntAssert` 和 `IntHandlerAddress` 后会向取指单元传递跳转信号和跳转地址。这样,位于译码、执行、访存、写回阶段的指令继续正常执行,取指单元取出的指令将被清除,PC 被设置为中断服务程序入口地址,下面将开始执行中断服务程序。 + +内核中断控制器还会检测当前正在译码的指令,若该指令为 `ecall` 或 `ebreak` 则引发一次异常,处理流程与中断类似;若该指令为 `mret`(即从中断或异常处理程序中返回),则修改 `mstatus`,然后将 `mepc` 的值作为“中断处理程序地址 `IntHandlerAddress`” 和“中断信号 `IntAssert`” 传递给译码单元,巧妙地利用进入中断的形式退出中断。 + +## 访存模型 + +除寄存器堆以外,本项目默认外部存储设备为**同步读写**模式,也就是只有在时钟上升沿到来时数据才会发生变化,因此所有访存指令都需要提前一个时钟周期发出地址信号,并最快在下一个时钟周期取得数据。 + +同时,本项目采用冯·诺伊曼架构,也就是指令与数据存放在同一个存储器中,使用相同的地址空间。 \ No newline at end of file diff --git a/docs/docs/better-tut/theory/images/axi-fsm.png b/docs/docs/better-tut/theory/images/axi-fsm.png new file mode 100644 index 0000000..21fbf35 Binary files /dev/null and b/docs/docs/better-tut/theory/images/axi-fsm.png differ diff --git a/docs/docs/better-tut/theory/images/axi.png b/docs/docs/better-tut/theory/images/axi.png new file mode 100644 index 0000000..8992479 Binary files /dev/null and b/docs/docs/better-tut/theory/images/axi.png differ diff --git a/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.eddx b/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.eddx new file mode 100644 index 0000000..276d441 Binary files /dev/null and b/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.eddx differ diff --git a/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.png b/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.png new file mode 100644 index 0000000..d6f9566 Binary files /dev/null and b/docs/docs/better-tut/theory/images/five_stage_pipeline_cpu_structure.png differ diff --git a/docs/docs/better-tut/theory/images/pipeline.png b/docs/docs/better-tut/theory/images/pipeline.png new file mode 100644 index 0000000..26ec6c7 Binary files /dev/null and b/docs/docs/better-tut/theory/images/pipeline.png differ diff --git a/docs/docs/better-tut/theory/images/rv-asm-refcard-page1.png b/docs/docs/better-tut/theory/images/rv-asm-refcard-page1.png new file mode 100644 index 0000000..c1d2857 Binary files /dev/null and b/docs/docs/better-tut/theory/images/rv-asm-refcard-page1.png differ diff --git a/docs/docs/better-tut/theory/images/rv-refcard-page1.png b/docs/docs/better-tut/theory/images/rv-refcard-page1.png new file mode 100644 index 0000000..7f3fd5c Binary files /dev/null and b/docs/docs/better-tut/theory/images/rv-refcard-page1.png differ diff --git a/docs/docs/better-tut/theory/interrupt-and-exception.md b/docs/docs/better-tut/theory/interrupt-and-exception.md new file mode 100644 index 0000000..2a2731f --- /dev/null +++ b/docs/docs/better-tut/theory/interrupt-and-exception.md @@ -0,0 +1,73 @@ +# 中断和异常 + +By: [:material-github: howardlau1999](https://github.com/howardlau1999) + +## 简介 + +在使用电脑的时候,你可以一边听音乐一边上网冲浪、一边在线聊天一边看视频,后台可能还挂着东西在下载。然而,真正同时运行的程序数量最多不会超过 CPU 的核心数,一般的电脑有 4 核,所以最多只能同时运行 4 个程序。另外,当你写的程序出 Bug 的时候,操作系统会帮你结束程序,并将程序结束的原因报告给你,而不会影响到其他程序的运行。 + +你或许已经知道,我们日常所使用的操作系统实际上是会不停地切换程序。然而,一旦程序开始运行,除非它主动跳转到其他程序,否则将永远独占一个核心,哪怕它是在死循环。这样的情况下,操作系统又如何获得 CPU 核心的使用权呢?答案是,我们日常所使用的 Windows 和 Linux 等操作系统是**抢占式调度**的。在操作系统启动的时候,它们会打开 CPU 上的定时器中断,也就是说,每隔一段时间,CPU 就会强行暂停当前运行的程序,并跳转到操作系统设置的中断处理程序中。这时,操作系统就有机会保存好进程的状态,并执行调度算法选择下一个需要运行的程序了。当然,除了抢占式调度,还有协作式调度,在这种调度方式中,需要程序员在代码中手动插入跳转到操作系统的代码,否则程序将一直运行下去。 + +而在程序出 Bug 的时候,例如访存越界、除零错误等,则会触发 CPU 中的异常,这时,CPU 同样会强行暂停目前的程序,跳转到操作系统预先设置好的异常处理程序中。这样,操作系统就可以记录下错误的原因,并调用程序员设置好的异常处理程序来处理错误。当然,你也很少编写自己的异常处理程序,那么,操作系统便会默认地将你的进程结束掉了。 + +在本实验中,你将通过实践了解 CPU 的中断和异常是如何实现的,CPU 是如何打断正常的执行流进入中断和异常处理程序的,又是如何告诉中断处理程序必要的信息,以及如何从中断处理程序返回到正常的执行流中。下面将带你初步了解 RISC-V 中中断和异常的相关概念知识。 + + + + +## 中断、异常和陷入的定义 + +本实验中中断、异常和陷入的定义都以 RISC-V 非特权级手册中第一章的定义为准: + +!!! quote "1.6. Exceptions, Traps, and Interrupts" + We use the term exception to refer to an unusual condition occurring at run time associated with an instruction in the current RISC-V hart. + We use the term interrupt to refer to an external asynchronous event that may cause a RISC-V hart to experience an unexpected transfer of control. + We use the term trap to refer to the transfer of control to a trap handler caused by either an exception or an interrupt. + +异常(exception)指运行时与某一指令相关的不寻常情况,例如整数除以零异常、指令地址未对齐异常等。 + +中断(interrupt)指来自CPU外部的事件,例如键鼠输入、定时器事件等。 + +陷入(trap)指由异常或中断导致的 CPU 控制权切换,例如键鼠中断发生后 CPU 暂时放下当前程序,陷入至处理键鼠输入的例程,以完成键鼠操作。 + + + +## 控制和状态寄存器 + +CSR (Control and Status Registers, 控制和状态寄存器) 用来控制和保存 CPU 其他功能的状态,例如中断使能状态、特权等级等。在非特权级手册的 "Zicsr" 扩展部分讲述了与 CSR 相关的指令,在特权级手册则详细描述了各个 CSR 的定义及作用。 + +RISC-V 中为每个CPU核心保留了 4096 个 CSR 寄存器地址(12 bits),这些地址中有的是 RISC-V 必须的,有些是可选实现的,还有些由CPU厂商自行选择。为简单期间,实验中我们仅考虑CPU处于 Machine 特权级,其拥有最高特权。User 和 Supervisor 特权级不做讨论。Machine 特权级下,相应的 CSR 名称以 m 开头,一些 RISC-V 必须的 CSR 如下所示。 + +mstatus, mtvec, mip&mie, mepc, mcause, + +### `mstatus` 寄存器 + +`mstatus` 寄存器用于记录机器模式下的状态,例如中断是否启用等。 + +### `mepc` 寄存器 + +`mepc` 寄存器保存了中断返回后需要执行的指令地址,当 CPU 执行中断时,`mepc` 寄存器被自动设置为当前指令的地址,如果 `EX` 阶段正在执行跳转,则设置为跳转的目标地址。 + +### `mcause` 寄存器 + +`mcause` 寄存器保存了中断的原因。 +### `mtvec` 寄存器 + +`mtvec` 寄存器保存了陷入处理程序的地址和设置,当 CPU 执行中断时,`pc` 寄存器被自动设置为 `mtvec` 寄存器中保存的陷入处理程序的地址。 + +在中断发生的时候,CPU 需要清空并阻塞流水线,并在 CSR 寄存器写入中断相关的信息。由于 CSR 寄存器堆实现只有一个读写端口,因此,CPU 需要多个周期才能完成 CSR 寄存器的写入。在设置完 CSR 寄存器后,发出控制信号,CPU 跳转到 `mtvec` 中保存的指令地址,开始执行中断处理程序。 + +## 中断处理程序 + +为了尽快处理中断,CPU 仅会将最基本的中断发生地址和中断原因等保存到 CSR 寄存器中。更复杂的功能由中断处理程序实现。 + +程序正常执行的过程中,会使用到通用寄存器堆。为了能够让中断返回后,原来的执行流程不受影响,中断处理程序需要立刻先将当前所有寄存器的内容写入内存中,然后再调用我们自定义的函数处理中断。在函数返回后,我们需要将原来内存中的寄存器内容恢复到 CPU 中,并调用 `mret` 指令从中断中返回,恢复原来程序的执行。 + +此外,中断处理程序本身也需要栈来保存函数栈帧,所以中断处理程序中还需要切换栈。为了安全考虑,中断处理程序使用单独的内存空间作为其运行栈。上面所说的保存寄存器内容,可以直接保存在中断栈上。 + + +## CSR 相关指令 + +`csrrw`、`csrrs`、`csrrc` 及其相应立即数版本指令(带 i 后缀),都能读出指定 CSR 的值并同时修改 CSR 的内容。相应的指令格式和具体作用可查询 非特权级手册 的 "Zicsr" 扩展部分。 + + diff --git a/docs/docs/better-tut/theory/riscv-isa.md b/docs/docs/better-tut/theory/riscv-isa.md new file mode 100644 index 0000000..8850983 --- /dev/null +++ b/docs/docs/better-tut/theory/riscv-isa.md @@ -0,0 +1,45 @@ +# RISC-V 指令集架构 + +By: [:material-github: howardlau1999](https://github.com/howardlau1999), Purple Power + +RISC-V 是一种精简指令集架构,其支持32位和64位,及各种扩展。如 `rv32im` 指 32位 RISC-V 架构并支持 I拓展(整数运算,所有 RISC-V 架构必须支持) 和 M拓展(乘法支持)。 +本实验中,我们将实现最基础的 RISC-V 32位架构的 CPU,并支持整数运算。 +在此之前,您需要先了解 RISC-V 指令的格式。 + + + + +## 指令格式 + +RISC-V 有六种基本的指令类型: + +- R 类型指令,用于寄存器-寄存器操作 +- I 类型指令,用于短立即数和访存 load 操作 +- S 类型指令,用于访存 store 操作 +- B 类型指令,用于条件跳转操作 +- U 类型指令,用于长立即数 +- J 类型指令,用于无条件跳转 + +这些指令支持基本的整数运算,您可以通过 risc-v reference card 以快速查阅其格式或汇编指令语法,下面给出的两个 refcards 第一页就能基本覆盖实验所需,您也可以在 Google 搜索 RISC-V reference card 等关键词以查找。 + +[RISC-V ISA Reference Card](https://github.com/jameslzhu/riscv-card/releases/download/latest/riscv-card.pdf) + +[RISC-V Assembly Reference Card](https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/RISCVGreenCardv8-20151013.pdf) + +??? abstract "RISC-V 基础整数指令格式及汇编语法 refcard" + ![](images/rv-refcard-page1.png) + + ![](images/rv-asm-refcard-page1.png) + +TODO: asm example and godbolt showcase + +## Spike 模拟器 + +TODO: more + +[Spike](https://github.com/riscv-software-src/riscv-isa-sim) 是一个 RISC-V ISA 模拟器,支持多种 RISC-V 扩展,通常被人们拿来作为参考实现,以验证自己实现的正确性。 + + + + + diff --git a/docs/docs/run_os_try.md b/docs/docs/run_os_try.md new file mode 100644 index 0000000..3e6fa79 --- /dev/null +++ b/docs/docs/run_os_try.md @@ -0,0 +1,50 @@ +2025-02-19 01:19:45 + +# Tried to run egos-2000 on Single-Cycle RV32IM_Zicsr processor + +## Results + +**FAILED**. Verilator simulation shows some correct output but loops infinitely in `disk_init()`. Vivado synthesis failed. + +## EGOS-2000 + +egos provides basic, runnable OS without any modification on qemu SiFive multi-core system. + +## Efforts + +1. M-extension support, not tested + 1. see `ALU`, `ALUControl` +2. added MMIO regs `mtime` and `mtimecmp` for egos-2000 timing, not tested + 1. see `CLINT` +3. modify memory map in `Top.scala`, assign 0x8xxxxxxx for memory; egos uses 16MB for RAM +4. implement neccessary CSRs, `mvendorid`, `mhartid`, `marchid`, etc, to disguise CPU as ARTY platform in egos +5. modify egos UART address in `tty.c`, `NCORE` in `egos.h` +6. modify `boot.S` amoswap.w.aq instruction to simple loop only suitable for single-core (the only place using atomic operations) + ```asm + boot_aq_again: + lw t2, 0(t0) + bnez t2, boot_aq_again + sw t1, 0(t0) + li t1, 0 + ``` +7. use xpack on Linux to build egos-2000 and copy out the .text, .data, etc. sections to assembly binary file. +8. tried to implement A-extension for Atomic Memory Operation. However, `amoswap` requires read and write to memory in the same CPU cycle. + 1. detached `MemoryAccess` read and write signal processing, so read and write can run concurrently + 2. `SyncMem` provides sequential read, where the data is available at the next cycle of the request. + 3. Current memory and CPU interaction is tricky and unstable, CPU is div-freq so a CPU cycle is longer than memory read/write. This causes mem-arith-mem loop. + 4. Some read done mechanism should be added, and single cycle CPU should stall until memory read data available. This can help later labs for adding cache. + + +## Reasons + +1. Vivado Synthesis failed due to insufficient RAMB18: egos uses 16MB memory, which is now synthesised on PL. +2. Cannot handle file system, it requires SPI to SD card, or read only Flash ROM + + +## TODO + +1. rebase project on lab4 with AXI4 support +2. connect to Zynq board memory through AXI4 +3. connect to Flash ROM or SD +4. [optional] add memory read done and stall mechanism + diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 508ba3b..8556e18 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -7,6 +7,39 @@ nav: - 如何一键环境配置: env.md - 如何一键烧板: board.md - 最终验收: test.md +- better-tut/intro.md +- 理论知识基础: + - better-tut/theory/cpu-arch.md + - better-tut/theory/riscv-isa.md + - better-tut/theory/interrupt-and-exception.md +- 基础工程知识与技能: + - better-tut/practice/envvar-and-cmd.md + - better-tut/practice/scala-and-chisel.md +- 实验: + - better-tut/labs/lab1/lab1-single-cycle-cpu.md + - better-tut/labs/lab2/lab2-interrupt.md + + theme: - name: material \ No newline at end of file + name: material + features: + - content.tabs.link + - navigation.expand + + +markdown_extensions: + - toc: + permalink: "🚁" + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + - attr_list + - md_in_html + - pymdownx.blocks.caption + - tables + - def_list + - pymdownx.tasklist: + custom_checkbox: true \ No newline at end of file