
本次课内容:
我们之前对sISA进行扩展, 缓解了部分问题
但扩展后的sISA v2.0仍然具有局限性:
要运行更多程序, 就要增加PC和GPR的位宽, 并添加更多功能的指令
RISC-V是近十年流行起来的开放指令集架构
有了sISA的认识, 可以很容易通过RISC-V官方手册了解RISC-V指令集
R[0]和sISA的R[0]有什么不同之处?add指令的格式具体是什么?RISC-V的32位整数基础指令集RV32I有42条指令
我们选取其中的一部分子集,
提出一个迷你RISC-V指令集minirv
0add, addi,
lui, lw, lbu, sw,
sb, jalr
我们在编译层次研发了一些黑科技
但带着编程任务去RTFM, 效果更好
#include <stdint.h>
#include <stdio.h>
uint8_t PC = 0;
uint8_t R[4];
uint8_t M[16] = {
0b10001010, // li r0, 10
0b10010000, // li r1, 0
0b10100000, // li r2, 0
0b10110001, // li r3, 1
0b00010111, // add r1, r1, r3
0b00101001, // add r2, r2, r1
0b11010001, // bner0 r1, 4
0b11011111, // bner0 r3, 7
};
void inst_cycle() {
uint8_t inst = M[PC];
if (((inst >> 6) & 0x3) == 0) { // add
R[(inst >> 4) & 0x3] = R[(inst >> 2) & 0x3] + R[inst & 0x3];
} else if (((inst >> 6) & 0x3) == 2) { // li
R[(inst >> 4) & 0x3] = inst & 0xf;
} else if (((inst >> 6) & 0x3) == 3) { // bner0
if (R[0] != R[inst & 0x3]) { PC = (inst >> 2) & 0xf; return; }
} else { printf("Unsupported instuction\n"); }
PC += 1;
}
int main() {
for (int i = 0; i < 100; i ++) { inst_cycle(); }
printf("r2 = %d\n", R[2]);
return 0;
}M的位宽\(w_{M} \ne w_{ISA}\),
则不能直接用PC进行寻址
R[0]和sISA不同, 需要考虑
+
各种细节的考虑, 作为编程作业留给大家思考
jalr
bner0更简单bner0不同
add
add类似
lui
li类似, 只是立即数的格式不同在ISA的概念中, 和GPR相比, 存储器空间更大, 但访问速度更慢
为了控制CPU访问存储器的数据, 需要添加访存指令
lw, lh, lb,
分别用于从存储器中读入4/2/1字节
lh和lb要对读入的数据进行符号扩展lhu和lbu, 对读入的数据进行零扩展sw, sh, sb,
分别用于向存储器中写入4/2/1字节
通常来说, ISA层次定义的存储器是按字节寻址的
小端的优势:
x x x+1 x+2 x+3
+------+ little-endian +------+------+------+------+
| 0x12 | ----------------> | 0x12 | 0x00 | 0x00 | 0x00 |
+------+ +------+------+------+------+
| 0x12 | ----------------> | 0x00 | 0x00 | 0x00 | 0x12 |
+------+ big-endian +------+------+------+------+
多数CPU采用小端的一个原因大端的优势:
小结:
数据的存储
指令的存储
16位包为单位的小端存储
+----------------+
|xxxxxxxxxxxxxxaa| 16-bit (aa != 11)
+----------------+----------------+
|xxxxxxxxxxxxxxxx|xxxxxxxxxxxbbb11| 32-bit (bbb != 111)
-------+----------------+----------------+
...xxxx|xxxxxxxxxxxxxxxx|xxxxxxxxxx011111| 48-bit
-------+----------------+----------------+
...xxxx|xxxxxxxxxxxxxxxx|xxxxxxxxx0111111| 64-bit
-------+----------------+----------------+
...xxxx|xxxxxxxxxxxxxxxx|xnnnxxxxx1111111| (80+16*nnn)-bit, nnn != 111
-------+----------------+----------------+
...xxxx|xxxxxxxxxxxxxxxx|x111xxxxx1111111| Reserved for >= 192-bits
-------+----------------+----------------+
base+4 base+2 base
和sCPU相比, minirv需要额外考虑访存指令的数据通路 - 需要添加RAM
作为编程作业留给大家思考
和sCPU不同, 将来还需要不断改进NPC, 添加更多功能
根据minirv指令的功能, 结合处理器的工作流程给NPC划分模块:
是RTL设计语言?
不, Verilog的本质是一门事件建模语言
synthesis
用Verilog来设计可综合电路是后来的事情
# 30可综合的含义*, /和%一般不用,
它们综合出的是在一个周期内得到结果的复杂电路, 面积, 功耗,
延迟都很大
但行为建模并不直观, 初学者很容易产生误解
行为建模的设计过程:
从第2步就已经开始偏离RTL设计的初衷:
行为建模的设计过程:
更可怕的是跳过第1步, 直接从第2步开始
你可以很容易在课本等学习资料中找到类似的描述:
在Verilog中,各语句是并发执行的,模块中所有的assign语句、always语句块和实例化语句,其执行顺序不分
先后。而if语句是顺序执行的语句,其执行过程中必须先判断if后的条件,如果满足条件则执行if后的语句,否则
执行else后的语句。Verilog语法规定,顺序执行的语句必须包含中always块中,always块中的语句按照它们
在代码中出现的顺序执行。灵魂拷问:
执行?if的条件满足, 就不执行else后的语句,
这里的不执行又是什么意思? 和电路有什么联系?并发执行, 又有顺序执行,
还有任何一个变量发生变化就立即执行,
以及在任何情况下都执行, 它们都是如何在电路中体现的?
估计大部分教Verilog的老师, 甚至是一些从业者, 都无法对这些问题作出明确的回答 😂
RTL设计 = 直接描述电路结构 = 实例化 + 连线
我们让大家在F阶段用Logisim设计电路, 就是希望大家理解上述本质
always实现(简单可控),
但通过结构化建模屏蔽实现细节always中的编码风格,
有的企业甚至不允许写always
always语句when和switch// 触发器模板
module Reg #(WIDTH = 1, RESET_VAL = 0) (
input clk,
input rst,
input [WIDTH-1:0] din,
output reg [WIDTH-1:0] dout,
input wen
);
always @(posedge clk) begin
if (rst) dout <= RESET_VAL;
else if (wen) dout <= din;
end
endmodule
// 使用触发器模板的示例
module example(
input clk,
input rst,
input [3:0] in,
output [3:0] out
);
// 位宽为1比特, 复位值为1'b1, 写使能一直有效
Reg #(1, 1'b1) i0 (clk, rst, in[0], out[0], 1'b1);
// 位宽为3比特, 复位值为3'b0, 写使能为out[0]
Reg #(3, 3'b0) i1 (clk, rst, in[3:1], out[3:1], out[0]);
endmodule| 软件设计 | 硬件设计 | |
|---|---|---|
| 图 | 算法流程图 | 电路结构图 |
| 代码 | 描述算法流程 | 描述电路结构 |
| 多行代码间的关系 | 反映流程的先后顺序 | 反映部件之间的连接关系 |
| 代码的作用 | 生成程序来执行 | 生成电路版图来制造 |
| 模块的作用 | 调用子程序 | 实例化子部件 |
* 在RTL设计中, 什么叫"执行"?
* 是谁在执行Verilog的语句? 是电路,综合器,还是其他的?
* "if的条件满足, 就不执行else后的语句", 这里的"不执行"又是什么意思? 和电路有什么联系?
* 有"并发执行", 又有"顺序执行", 还有"任何一个变量发生变化就立即执行", 以及"在任何情况下都执行",
它们都是如何在电路中体现的?行为建模的水很深, 我们会在D阶段进一步介绍
给初学者的建议:
如果设计电路时大脑蹦出执行这个词,
你就输了
我们会在将来接入SoC时使用可以流片的真实存储器
extern "C" int pmem_read(int raddr) {
// 总是读取地址为`raddr & ~0x3u`的4字节返回
}
extern "C" void pmem_write(int waddr, int wdata, char wmask) {
// 总是往地址为`waddr & ~0x3u`的4字节按写掩码`wmask`写入`wdata`
// `wmask`中每比特表示`wdata`中1个字节的掩码,
// 如`wmask = 0x3`代表只写入最低2个字节, 内存中的其它字节保持不变
}这两个内存读写函数模拟了32位总线的行为
这样做的原因:
完成逻辑设计后, 接下来就是要在NPC上运行程序
考虑求和程序, 如何将其运行在NPC上?
printf()库函数的功能?printf()可能需要调用malloc()main()函数返回后, 如何指示程序结束?
我们提供一个用于构建程序的项目abstract-machine(简称AM)
riscv64-linux-gnu-gcc将C代码编译成RISC-V汇编代码printf()库函数的功能?
klib库函数的实现,
以及putch()用于输出一个字符malloc()的实现main()函数返回后, 如何指示程序结束?
halt()中执行ebreak指令,
通知仿真环境结束仿真我们会在D阶段中进一步介绍AM的细节
借助AM, 在不超过内存容量的前提下, 你可以在NPC上运行各种纯计算的程序
但类似输出hello信息这种简单的程序, 却无法运行
你在F阶段已经了解, 用户是通过输入输出设备和处理器进行交互的
io指令内存映射I/O(Memory-mapped
I/O)方式来进行的考虑指定设备的方式:
io指令通过指令中的idx设备编号来指示一个设备
端口映射I/O(Port-mapped I/O)
假设地址0x10000000对应串口的输出数据寄存器
A:上述指令序列看上去和将字符A写入存储器并无本质区别
在C代码中, 可以通过指针来实现类似的功能:
volatile是C语言中的一个关键字
volatile的进一步细节, 我们会在后续阶段讲述为了让程序可以使用串口进行输出, 还需要在NPC中添加一个串口
上述行为模型通过if语句实现内存映射I/O的功能
0x10000000,
就写入串口
fputc()输出wdata低8位所表示的字符,
模仿UART输出的行为
有了串口, 就可以在NPC上运行各种需要输出信息的程序了
hello, 各种benchmark(只看对错, 不评分),
甚至是AI大模型(无交互)对于真实的UART来说, 在输出一个字符之前, 还需要检查状态是否就绪
查询UART是否就绪是通过读出UART的状态寄存器来实现的:
上述行为模型约定, 可以从地址0x10000004读出UART的状态
1,
表示就绪
还需要修改AM中putch()的实现,
在输出字符前查询UART状态
行为模型让仿真环境通过gettimeofday()来获取当前的系统时间
在AM中实现读取时钟的函数, 在函数中访问时钟的行为模型, 就可以让NPC上的程序具备计时功能
unsigned long long get_time() {
// 返回从仿真开始所经过的时间, 单位为微秒
// 可通过gettimeofday()来实现
return 0;
}
extern "C" int pmem_read(int raddr) {
// ...
// 读出时钟的低32位
else if (raddr == 0x20000000) {
return get_time() & 0xffffffff; }
// 读出时钟的高32位
else if (raddr == 0x20000004) {
return get_time() >> 32; }
// 读出存储器数组
}通常来说, 通过测试无法说明系统没有bug
什么是正确用命题表达出来目前我们采用简单的测试方法: 查看程序在NPC上的运行结果是否正确
如果程序的运行结果不正确, 如何高效地定位错误?
1000x100000=1亿个信号值启发: 在处理器上运行程序, 就是在处理器上执行一条条指令的过程
while (???) { // 以下为伪代码
dut_single_cycle();
ref_inst_cycle();
uint8_t *dut_regs = &top->rootp->NPC__DOT__gpr_ext__DOT__Memory;
uint8_t *ref_regs = ref_get_regs();
int is_diff = check_regs(dut_regs, ref_regs);
if (is_diff) {
printf("GPR different\n");
printf("Simulation stop\n");
break;
}
}基本思想: 对比每条指令执行后, NPC和模拟器的寄存器状态是否一致
实现模拟器不仅可以帮助我们理解指令行为, 还能作为测试的金标准