迷你RISC-V指令集
“迷你RISC-V”指令集minirv
- PC初值为0
- GPR数量与RV32E中定义的GPR数量一致
- 支持如下8条指令:
add,addi,lui,lw,lbu,sw,sb,jalr - 其他的ISA细节与RV32I相同
编译黑科技:将其他常用34条指令编译到上述8条指令
只有两条指令的minirv处理器
实现addi指令
取指
称RISC-V手册中的定义的存储器宽度为
在电路层次, 如果ROM的宽度和不一致, 则不能直接用PC值对ROM进行寻址
译码
操作码译码
指令数量少,编码稀疏,用译码器不方便
通过以下操作判断一条指令是否为addi指令:
is_addi = (inst[6:0] == ?) && (inst[14:12] == ?)
其中inst表示取出的指令, ?需要根据你查阅手册的结果来决定
在元件库的Arithmetic(运算器)类别下找到Comparator(比较器), 用于方便地实现比较功能
操作数译码
立即数需要符号扩展(扩展成与GPR位宽一致的数据)
- 一种是零扩展(zero-extend), 这种方式总是在高位添加
0 - 另一种是符号扩展(sign-extend), 这是方式是在高位添加补码的符号位
- 在符号扩展前后, 补码的真值保持一致
R[0]永远为0
执行
加法器
更新PC
指令编码的位宽与sISA不同,需要考虑如何更新
实现jalr指令
比bner0更简单,跳转目标作为新PC的数据选择端
其他和addi类似
实现完整的minirv处理器
实现add指令
和sISA的add类似
实现lui指令
和sISA中的li指令很相似, 不过要考虑不同类型的立即数格式
四条访存指令
| 指令 | 英文全称 | 类型 | Opcode (7位) | 功能简述 |
|---|---|---|---|---|
lw | Load Word | I-Type | 0000011 | 从内存读 32位字 (有符号扩展) |
lbu | Load Byte Unsigned | I-Type | 0000011 | 从内存读 8位字节 (零扩展) |
sw | Store Word | S-Type | 0100011 | 向内存写 32位字 |
sb | Store Byte | S-Type | 0100011 | 向内存写 8位字节 |
需要访问存储器
访存操作分为**读内存(load)和写内存(store)**两种. 由于store指令需要写入内存, 因此ROM无法满足这一要求, 我们需要采用RAM.
使用Logisim提供的
RAM组件, 你可以在元件库的Memory(存储库)类别下找到它. 实例化后, 你需要按照以下配置修改其中的一些关键参数:
- Address Bit Width(地址位宽) - 根据后续的程序大小和你的理解进行配置
- Data Bit Width(数据位宽) - 32
- Enables(启用方式) - Use byte enables(使用字节启用)
- Ram type(RAM型) - non volatile(非易失性)
- Use clear pin(使用清除销) - No(否)
- Trigger(触发器) - Rising Edge(上升沿)
- Asynchronous read(异步读取) - Yes(是)
- Read write control(读写控制) - Use byte enables(使用字节启用)
- Data bus implementation(数据总线实现) - Separate data bus for read and write(用于读写的独立数据总线)
RAM组件的端口包括: 读写地址A, 写使能WE, 读使能OE, 字节写使能BE0, BE1, BE2, BE3, 写数据D(输入), 读数据D(输出), 以及时钟
实现lw指令
计算出访存地址后, 将其接入到RAM, 并使RAM的读使能OE有效
实现sw指令
需要连接写数据, 写使能和字节写使能
只有执行store指令时, 才能将写使能置为有效
实现lbu指令
lbu指令只需要读出一个字节, 但RAM的宽度比一个字节大. 你需要根据具体的访存地址, 从读出的数据中选择出相应的字节, 并写回目的寄存器
实现sb指令
只需要往目标地址写入一个字节, 因此需要通过具体的访存地址生成合适的字节写使能信号, 从而控制哪一个字节被写入.
在minirv处理器上执行C程序
.hex文件中不仅仅包含程序的指令序列, 还包含程序所处理的数据, 程序会通过访存指令访问这些数据, 因此我们还需要将.hex文件加载到RAM中
1. 怎么理解”同一个 .hex 文件既加载到 ROM 又加载到 RAM”?
这其实是为了在 Logisim 这种哈佛架构(指令和数据分开存储的硬件)上,模拟现实中的冯·诺依曼架构(指令和数据都在同一个内存里)。
- 编译器的视角 (.hex 文件): 编译器生成的这个
.hex文件,是一个完整的内存快照。里面既包含了代码段 (Code/Text),也包含了全局变量/静态数据段 (Data)。- 比如:地址
0~100是指令,地址200~300是数据。- 硬件的现状 (Logisim): 你有两个独立的存储器组件:ROM(专供 PC 取指)和 RAM(专供
lw/sw读写数据)。- “双胞胎”加载法的妙处: 我们将同一个
.hex文件同时塞进 ROM 和 RAM。- ROM 里:虽然也有数据段,但 PC 永远只会指向代码段的地址,所以多余的数据段被忽略了,不影响运行。
- RAM 里:虽然也有代码段,但
lw/sw指令只会访问数据段的地址,所以多余的代码段也被忽略了(除非程序自己作死改写自己的代码)。结论:这是一种简单粗暴但有效的工程手段,让你不用手动去拆分 hex 文件。
debug方法
1. 分而治之 (Divide and Conquer)
- 方法:我们没有一上来就跑 6000 行的
mem.hex。 - 实践:
- 先跑
addi测试。 - 再跑
sb专项测试(90ABCDEF)。 - 再跑
lbu专项测试。 - 价值:隔离变量。如果
sb测试挂了,我们就不用去管 ALU 或 跳转逻辑,只盯着 RAM 看就行。
2. 静态分析与反推 (Static Analysis)
- 方法:对着
.txt反汇编文件看代码意图。 - 实践:
- 当 PC 跑到
0x8204时,我们没有瞎猜,而是去反汇编里找”哪个指令会让 PC 变成这样?” - 当 PC 跑到
0x2448时,我们计算出0x2448 = 0x1224 * 2,从而立刻锁定了”翻倍”这个特征。 - 价值:数据敏感度。数字本身往往包含着 Bug 的特征(全0、翻倍、加1、高位全F)。
3. 动态探针 (Probe & Drill Down)
- 方法:Logisim 的”手指工具” (Poke) 和子电路透视。
- 实践:
- 钻进 GPR 子电路看
x1到底是 10 还是 8。 - 把鼠标悬停在 MUX 的输入线上,看数据是不是
12345678。 - 价值:所见即所得。不要相信你的逻辑推导,要相信电路里的电压。
为minirv处理器添加图形显示功能
给这个minirv处理器添加一个”屏幕”, 并通过运行程序在这个屏幕上显示一张图片
在Logisim中实例化一个屏幕组件, 你可以在元件库的
Input/Output(输入/输出)类别下找到RGB Video(RGB视频)组件. 实例化后, 你需要按照以下配置修改其中的一些关键参数:
- Cursor(光标) - No Cursor
- Reset Behavior(重置行为) - Asynchronous
- Color Model(颜色模式) - 888 RGB (24 bit)
- Width(宽度) - 256
- Height(高度) - 256
类似RGB Video这样的部件称为外部设备, 简称**“外设”**
访问外设是通过**“内存映射I/O”(Memory-mapped I/O)**方式来进行的(根据访存地址的范围来决定处理器的访问对象是内存还是外设)
RGB Video整个屏幕所存储的像素数据大小为256x256x4B=256KB
划分出一段连续的地址区域, 例如[0x20000000, 0x20040000)
- 当访存指令的目标地址落在这个范围之内, 相应指令将访问
RGB Video
实现内存映射I/O的关键:添加一个地址译码器
输入访存地址, 输出两个控制信号isVGA和isMem
通过这两个控制信号来控制相应组件的访问行为
X 和 Y 坐标怎么从地址里算出来?(核心难点)
这是最巧妙的地方。屏幕是 256x256 的,每个像素占 4 字节。
- 地址 0x20000000:对应 (X=0, Y=0)。
- 地址 0x20000004:对应 (X=1, Y=0)。
- 地址 0x20000400:这是第 256 个像素 (),对应 (X=0, Y=1)。
转换公式:
- 首先减去基地址,得到偏移量:
Offset = Addr - 0x20000000。 - X 坐标:
(Offset / 4) % 256。 - Y 坐标:
(Offset / 4) / 256。
在 Logisim 中如何”白嫖”这个计算? 利用分线器(Splitter)直接截取位!
Offset的二进制是:00000000 0000yyyy yyyyxxxx xxxx00- Bit 0-1:是 0(因为按 4 字节对齐)。
- Bit 2-9 (共8位):刚好就是 X 坐标 ()。
- Bit 10-17 (共8位):刚好就是 Y 坐标 ()。
迈向现代化的处理器设计
Logisim中设计处理器的缺陷
- 设计繁琐
- 仿真速度慢
- 调试困难
现代的处理器设计方式
采用代码开发
- 通过硬件描述语言(Hardware Description Language, HDL)描述硬件组件之间如何连接, 来给出处理器的逻辑结构
- 完成代码开发后, 需要通过仿真工具来检查代码所给出的逻辑结构是否符合预期
- 通过EDA工具将代码转变成版图
现代的处理器设计流程
- 架构设计: 给定一个新特性(可能是添加新指令等来自ISA规范的功能, 也可能是处理器层次上的功能优化方案), 如何给出一个设计方案, 将其分解成合适的硬件模块来实现它?
- 逻辑设计: 有了设计方案, 如何通过HDL在电路层次将设计方案中的硬件模块实现出来?
- 功能验证: 如何验证HDL所描述的电路满足新特性所期望的功能?
- 性能验证: 如何保证处理器的性能符合预期?
- 电路评估: 如何评估并优化处理器的频率, 面积和功耗等指标?
- 物理设计: 如何将HDL代码转变成可流片的版图?
- 性能优化: 如何发现并定位处理器中性能瓶颈, 并设计出相应的优化方案?

交流与留言
这里使用无需登录的轻量评论系统。你可以留下问题、反馈、勘误或交流想法。