2020 计算机组成原理问答
本文档包含了在计算机组成原理的讲课过程中以及实验过程中可能会出现的问题以及对应的解决办法,供同学们参考。特别感谢 2020 年秋计算机组成原理课堂上的同学们的总结。
由于问题是按照提问的时间顺序来整理的,因此问题之间无先后的顺序关系,建议各位同学先预先浏览一遍问题的描述。在实验中出现问题的时候,先找一下问题描述接近的来寻求答案。
1. 如何快速熟悉各种 Linux 环境的操作?
答:MIT 在去年提供了一个课程,名为 The Missing Semester of Your CS Education(2020)。这里面提供了一些基本的系统层面的操作以及相关的知识。从下面的网址可以看到课程。可以先学一下这几课:1,2,3,5,6,7。
https://www.bilibili.com/video/BV1x7411H7wa
2. 为什么每次修改完 verilog 文件之后,编译出来的结果不变?
答:注意,一定要把 thinpad_top.v 设置为 top 文件。右键点击这个文件,选择 set as top,否则在编译的时候依据的是其它文件作为 top,编译出来的东西还不变。top 的文件在显示上有黑体明显表示。
3. 实验指导书中有很多关于调试的内容,如何区分?
答:调试汇编语言程序的时候,按照第 6.3 节来,第 6.4 节是用于调试监控程序的,如果不需要自己修改监控程序,就不需要 6.4 节部分的内容。
4. 在编写汇编程序的时候,是否只能用给定的监控程序使用的 19 条指令还是可以使用其它的 RV32I 的指令?
答:QEMU 模拟的 RISC-V 的指令是完全的,可以使用任何的 RV32I 指令来完成汇编程序。实验 6 还需要运行编写的汇编语言程序,如果使用了监控程序所使用的指令之外的指令,那么最终在实验 6 中需要实现这些额外的指令。因此,尽量使用监控程序使用的 19 条指令。
5. 使用 Vivado 的版本问题。
答:强调一下,我们必须使用 2019.2 版本,否则 CI 自动综合会失败,同时云平台上的很多功能没法使用。
6. 监控程序 kernel,终端程序 term 和 qemu 之间的关系。
答:很多同学没有系统的经验,会误解 kernel 程序和 term 程序会通过网络连接在一起,或者通过某种方式连接在一起,这是一个误解。kernel 和 term 之间的连接关系是需要通过串口的,而串口有自己的协议,并且按照协议来工作。可以直接把串口想象成一条信号线。如果通过模拟器来运行 kernel,那么 qemu 模拟器就起到了模拟硬件的作用。qemu 模拟器会对内(针对 kernel)模拟出来一个串口,对外则启动了一个网络服务器,这样就使得监控程序可以和 qemu 模拟的串口进行交互。整个系统的连接情况如下图所示:
看一下 util.S 的代码。对于串口的访问来说,虽然是通过 sb,lb 等内存访问指令来进行的,而实际上是访问的外设寄存器(这样的访问方式叫做内存映射 IO,MMIO: Memory Mapped IO),对应的内存单元是不会被访问到的。每次访问之前都需要先读一下标志寄存器,看看是不是已经准备好。例如,term 发一个字符到串口的时候,流程如下所示:
(1)Term 发一个字符到 qemu。(qemu 会模拟出来一个 tcp 的服务端。)
(2)QEMU 收到一个字符后,放到模拟串口的缓冲器里面,一个字节(可以用一个内存变量来模拟串口的数据寄存器)
(3)QEMU 之后把对应的串口状态寄存器(可以用一个内存变量来模拟)标志位设置为 1。
上面就是模拟器的工作,而模拟器对应实际的硬件,实际硬件也是这么做的。
随后,kernel 程序会先对读测试一下 test_read,再去读的话就会读到状态寄存器的值。这个时候发现标志位为 1,下一步就可以直接从数据寄存器读数据了(使用 lb)。
有很多同学可能没有系统的经验,先理解上面的图以及工作流程之后再动手。和其它同学进行讨论是加快理解的好办法。
再加一个图,说明用户程序和系统剩下部分的关系:
用户程序需要执行的时候,需要通过 Kernel 来执行的。需要先把用户程序写入到内存中,然后再通过 G 命令执行。另外,基本版的监控程序没有保护,小心不要写入到内核的程序空间中,也要小心保护好 ra 寄存器,免得跳不会监控程序(一般不再调用新的函数不会)。
7. 串口的行为如何理解?
答:串口是一个外设。计算机处理器控制外设,一般是对外设寄存器进行读写。外设寄存器一般会不止一个,因此需要对外设寄存器进行编址(或者说,给不同的名称)。对于 x86 来说,对外设寄存器的读写是通过 in/out 指令来进行读写的。对于 mmio 这样的结构来说,就通过通常的内存访问指令来的。但是,无论是哪种情况,都是对外设寄存器的读写。外设寄存器一般有状态寄存器,控制寄存器,数据寄存器等。显然,状态寄存器是将自己的状态汇报给处理器的,是只读的。数据寄存器用来和处理器之间交换数据的。控制寄存器是控制外设的行为的。当外设看到控制寄存器发生变化的时候,就可以做出相应的动作。这些是基本的原理。上面部分的内容大家可以画个图,仔细体会一下(前面的图可以参考,在界面部分加上几个寄存器即可。)有这部分理解之后,就可以回答实验一的提问了。
关于串口部分的详细工作可以参考下面的网址:
https://www.lammertbies.nl/comm/info/serial-uart
代码部分有两个地方,一个地方在 include/serial.h
另外一个地方是 init.S 下面部分的代码
#ifdef ENABLE_UART16550
// 配置串口,见 serial.h 中的叙述进行配置
// 参考 ucore/kern/driver/console.c
li t0, COM1
li t1, COM_FCR_CONFIG // console.c:54
sb t1, %lo(COM_FCR_OFFSET)(t0) // :57
li t1, COM_LCR_DLAB
sb t1, %lo(COM_LCR_OFFSET)(t0) // :57
li t1, COM_DLL_VAL
sb t1, %lo(COM_DLL_OFFSET)(t0) // :58
sb x0, %lo(COM_DLM_OFFSET)(t0) // :59
li t1, COM_LCR_CONFIG
sb t1, %lo(COM_LCR_OFFSET)(t0) // :62
sb x0, %lo(COM_MCR_OFFSET)(t0) // :65
li t1, COM_IER_RDI
sb t1, %lo(COM_IER_OFFSET)(t0) // :67
#endif
QEMU 会完善模拟 uart 的工作,而 FPGA 是我们自己做的,配合 lb,sb 工作。
8. 汇编程序里面需要自己计算出相对跳转的距离吗?
答:不需要。例如下面的代码:
sum:
li a5,1
li a0,0
.L2:
li a4,10
bgt a5,a4,.L4
add a0,a0,a5
addi a5,a5,1
j .L2
.L4:
ret
可以看到,程序员是自己写标记的,而跳转指令的目标直接写上标记即可。而课堂上已经说过了,这里的跳转是相对跳转,需要计算偏移量。这个时候,汇编器可以发挥作用,将偏移计算出来,在生成机器码的时候填入偏移量。
9. 自己写程序的时候往串口发送了非显示字符,会发生什么情况?
答:参考 term 里面的 def run_G 函数来理解。
10. Kernel 有什么不同的版本啊?
答:kernel 有三个版本(只管 32 位的情况)。最基础的基本是实验 6 所需要支持的(基础版);在这个基础之上加上异常和中断的处理(异常处理版);然后在异常处理版的基础上加入虚拟内存的支持(虚拟内存版)。实验一要求对基础版进行分析,后面两个版本对于做扩展实验是必须的。另外,理解监控程序先要理解一下整个 ThinPAD 板子的工作原理,以及理解模拟环境是如何对硬件进行模拟的。
11. 要不要用 ecall?
答:取决于你对系统编程模式的理解。基础版本的监控程序不需要使用 ecall,直接写内存地址即可(实际是写外存寄存器)。
12. 伪指令是啥?
答:伪指令不是真正的机器指令。伪指令会通过汇编器翻译为真正的机器指令。伪指令的存在是为了方便程序员使用的。下面两条就是伪指令和真正机器指令的对应关系。
ret jalr x0, 0(x1)
j offset jal x0, offset
要理解上述关系的话,先要理解寄存器都有哪些。这里的 x0 是 0 号寄存器,永远是常数 0。x1 的别名是 ra,即 return address。注意:上述的一条指令实际上并不能完全代表 x86 下面的 ret。
13. 如何输入 jal 指令,为什么这样跳不到真正的位置:jal ra, 0x80003dc
答:汇编指令虽然和机器指令是一一对应的,但是汇编指令有汇编器的帮助,汇编器会帮助把汇编指令汇编为机器指令。汇编器期待的是标记,它其实不认得这里的绝对地址,因此不要在汇编语句中直接用 4 绝对地址。用标记就行,看下面的图中的使用方法,往前的标记和往后的标记都是可以的。(只是例子,不要试图去执行这里的代码)
往回 jal(低地址)
往前 jal(高地址)
14. .gdbinit 文件是个啥
答:如果有一些对 gdb 的默认配置,可以考虑保存到 ~/.gdbinit 文件中。在使用 gdb 调试程序的时候,有时候需要设定多个断点,重复执行某些操作,而这些操作写起来比较麻烦,这个时候就应该想起来用 gdb 命令脚本了,它能够很好的完成这些工作。gdb 在启动的时候,会在当前目录下查找 \".gdbinit\" 这个文件,并把它的内容作为 gdb 命令进行解释,所以如果我把脚本命名为 \".gdbinit\",这样在启动的时候就会处理这些命令。
在 UNIX(Linux 就是一种兼容 UNIX 的操作系统)下面,有很多各种 .xxxinit 的文件,都是对某一个程序的初始化脚本。注意,在 UNIX 下面以英文句号(.)开头的是隐藏文件,要用 ls -a 才能看到。
15. 汇编语言的注释用什么?
答:通常是使用 # 符号开始注释。当然,在其它的环境下面可能可以用 c++ 语言的注释方法。比如 .S 文件,可以使用 c++ 的注释,因为使用 gcc 编译的时候需要过一遍预处理,而预处理会把 C++ 风格的注释给处理掉。
http://labor-liber.org/en/gnu-linux/development/index.php?diapo=extensions
16. 编译期,链接期,汇编期优化的问题。
答:编译期会做很多优化,这个大家都知道。链接期也会做一些优化,参考下面的 url。
https://llvm.org/docs/LinkTimeOptimization.html
https://gcc.gnu.org/wiki/LinkTimeOptimization
https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain
汇编期是不会做优化的,因为没必要。汇编器就是将汇编语言一句一句汇编为机器代码。
17. 监控程序输入跳转等指令时,自动生成的地址异常?
答:这一问题可能是因为使用了 Python 2。使用 Python 3 运行 term,并给 py3 安装对应的 pyserial,更新监控程序,重新运行。
18. 关于用串口输出字符:想问一下我们有办法像 kernel.asm 的 .OP_G 里面一样使用类似 jal ra,800003dc\<WRITE_SERIAL> 的方法调用串口输出吗?
答:建议直接复制粘贴,不要尝试跳到内核里面的代码。当然是可以跳到内核代码的,但是需要确切知道内核的符号。如果一定要硬跳的话,那就自己找到地址,计算偏移,然后构造机器码,使用写内存的方式写入到代码内存。另外一个容易的方法是知道目标的位置,放到一个寄存器中,直接把绝对地址写进寄存器,jalr 就行了。
19. Bitstream 生成失败,错误类似于 [DRC NSTD-1] Unspecified I/O Standard
可能包括的现象:
Synthesis 阶段:CRITICAL WARNING: [Common 17-55] \'set_property\' expects at least one object.
Implementation 阶段:[DRC NSTD-1] Unspecified I/O Standard 或 [DRC UCIO-1] Unconstrained Logical Port。
现象的原因:
vivado 在指定的顶层模块文件(thinpad_top.v)有语法错误,或者文件不存在时,自动选择了错误的顶层模块,其端口与 xdc 中添加 IO 约束的名称不一致,导致约束应用失败。
或者对 thinpad_top 添加/修改了端口,导致端口名称发生变化。
解决方法:
如果 thinpad_top 模块有语法错误,先解决语法错误。之后在 Sources 窗口中将 thinpad_top 模块作为顶层(右键 thinpad_top,选择 Set as top)。
如果以上方法都不行,那么就:design runs 中找到 pll,右键 reset runs,然后再点击 Regenerate output products。
20. 每个实验具体的要求在什么地方?包括 ALU 操作码、之后实验的操作流程之类的。
在 ThinPAD-Cloud 实验平台 "自动评测" 栏目中,选择测例,有具体的描述。点击下方 Testcase 的编号,可以查看测试过程中使用的 Python 代码。
21. vivado 怎么清临时文件?
c 盘->属性->磁盘清理->临时文件
重启也可以清理临时文件。
还有,Vivado 各个阶段清临时文件,选择 Reset 菜单就可以清除临时文件。
上图中 Reset 了综合的结果,顺便把实现和 bitstream 也都给删了。
22. 未出现错误但是综合不过,咋办?
答:可能会出现各种情况,下面是一种在 Windows 下面的情况。
把里面提到的注册表里的 autorun 删了就能用了。这说的是每次会启动 cmd,然后 cmd 会启动 autorun,哪怕跟 vivado 没关系,也会导致综合失败。
23. CI 综合失败,报错 XML ERROR 什么的
答:这个问题大概率是因为你使用了 Vivado 2020.1 版本的软件,打开了工程并提交到了 git。CI 环境使用的 Vivado 版本为 2019.2,会导致工程不兼容。请重新安装 2019.2 版本的 Vivado,并将 xpr 等文件恢复到初始的状态,方法可以参考上一个问题。
24. vivado 查看综合后电路方法
如图点开相应的项目,查看综合的电路结果。
25. 仿真时总线出现奇怪的不定态
这是因为对于模块的使能信号,如果没有赋予一个合理的值(比如 0 或 1),则使能状态非法,就会导致总线处于不定的状态。
26. 求问如果仿真结果是正确的,但自动评测输出到串口全 timeout 有可能是什么原因?
有可能你的状态机还在等串口输入。
27. 遇到了打不开工程(例如:Can\'t open project file xxx. Please make sure the project still exists.)类似问题?
可能是你的工程路径中有非法字符(似乎\'&\'也不行)。
28. sram+uart 的实验中总线 base_ram_data 一直是 X 怎么回事?
当 sram 和 uart 同时给总线赋值时会出现多驱动问题,导致变为 X。
正确的做法是保证没有同时对总线的非 Z 赋值。例如在 uart_io.v 里有如下语句:
assign base_ram_data_wire = data_z ? 32\'bz : {24\'h00000, ram_data};
那么需要保证 sram 和 uart 模块的 data_z 不同时为 0。
29. io 模块不读写内存
在 io 模块中直接复制了模板代码,对 base_ram_ce_n 等使能信号进行 了 assign,而忘记在 io 模块的 output 中定义这些变量,综合也能通过,但信号无法传到 SRAM 上,因此无法读写内存。
30. 仿真 add to waveform
从左边 scope 里面一层层找到目标的信号,然后拖进 waveform
31. 仿真失败,报告错误:ERROR: [VRFC 10-845] illegal operand for operator &&
在 28F640P30.v 的最开头加一行 `default_nettype wire。
default_nettype 这个属性会影响当前文件开始之后的所有文件。所以解析.v 的顺序就有关系。加上上面的这一样就可以解决这样的一个问题。
32. 无法正确写入内存 (地址除以 4)
在确保控制信号 we_n, ce_n, data_z, address, data 都正确给到之后,检查一下地址是不是没有除 4。汇编程序中按照 RISC-V 的规范是按照字节寻址的。而我们的 SRAM 是按照字来寻址的,它们对于地址的认识是不一样的。
仿真时候的波形如下两图。(上方为正确,下方为错误。)
33. base_ram_data 出现 X,但所有代码位置给它正确赋值了
需要查看所有对于总线的驱动,发现同时串口对数据总线有驱动,在不需要串口的时候需要将串口的读写关掉,即 uart_rdn = 1'b1,uart_wrn=1'b1,可以将串口的读写关掉。
34. 常见的 Verilog 里不应使用的写法
以下写法不一定是错的,但是不要写就是了。
解释一下上面的三种情况:1)对于 来说,要么总是响应上升沿,要么总是响应下降沿,不能同时去响应上升沿和下降沿。我们的实验只需要响应上升沿就可以完成所有的实验功能。2)不要去响应除了时钟以外的信号的上升沿。3)不要使用 latch,即保存数据的器件使用边沿响应,不适用电平响应。
关于第三点:(1)不要用 latch。(2)判断语句上写完整,否则容易出 latch,if 要带 else,case 要带 default 等,保证综合出来组合逻辑。(3)最终波形说了算,一定要学会看仿真波形。(4)一定记得看 warning。
35. imm 的符号位
所有这里的 imm,如果没有写全 32 位,都是高位符号扩展,低位补零:
36. 仿真正确但上板子之后得 0 分时,我该做些什么
这其实说明了后仿真的重要性。可以看到,vivado 的 run simulation 第一次点击后有多个选项.除了我们经常使用的行为仿真外,还有后仿真.
首先要明确这两者的区别
\(1\) 行为仿真是仿真器直接执行你的 Verilog,像软件里面单步调试一样
\(2\) 后仿是综合、布局布线以后,仿真的 FPGA 里面的功能模块
> 尽管仿真的器件模型也不一定准确,但是后仿应该基本和实际行为一致
因此在仿真不能暴露问题时,可以借助后仿真。
另外还有一点就是 一定记得看 warning。
37. 新添加信号后记得重跑仿真
就这倒霉按钮,点它
38. 本地可以生成 .bit 文件,但是交到 gitlab 上综合跑挂了
下载 artifacts 看 runme.log
如果报错 ERROR: [Runs 36-527] DCP does not exist: /builds/cod-ta/cod21-xxxx/thinpad_top.srcs/sources_1/ip/pll_example/pll_example.dcp
就把 pll_example 目录下面除了 xci 以外的所有文件都删掉
39. 在综合的时候发现报错,但是实际上自己并没有这个错误
vivado 会留着上次综合时的错,这个时候应该先点一下垃圾桶,再重新跑综合
40. 实验 6 在仿真 kernel.bin 时,有一些 lb 指令会读出 X,其他看上去都对了
这个问题是 kernel.bin 长度不够导致的。需要自己在本地生成 kernel.bin,并将其补全到 4M 大小。
本地生成 kernel.bin 的方法是:从监控程序的仓库 (https://github.com/thu-cs-lab/supervisor-rv) 下载最新版本的监控程序,在 supervisor-rv/kernel文件夹下,Linux 环境下运行 make EN_UART16550=n,在 supervisor-rv/kernel 文件夹下就可以看到生成的 kernel.bin 文件,同时 kernel.asm 是对应的汇编代码。
Linux 环境下补全 kernel.bin 的指令是(在 kernel.bin 所在的目录下运行)
dd if=/dev/urandom of=random_4M.bin bs=1M count=4
dd if=kernel.bin of=random_4M.bin conv=notrunc
运行完这两条指令后,同目录下生成的 random_4M.bin 文件,就是把 kernel.bin 补全为 4M 的文件。(上面两条指令的意思是,第 1 条生成了大小为 4M 的随机数文件,第 2 条把 kernel.bin 覆盖到这个随机数文件的开头,效果等价于把 kernel.bin 补全到 4M)
41. 如何查看 term 给 kernel 发送了什么内容,以及在仿真中如何把 term 发给 kernel 的内容发送给仿真的代码
可以在 term 端的 tcp_wrapper 类的 write 函数中,打印出来 term 给 kernel 发送的信息,即 msg 变量。
从下图可以看到 term 给 kernel 发送的字节。在仿真代码中,可以也按照从左到右的方式,把每个字节逐个发送给串口。注意最后一个指令中的\xb3e,其实是\xb3 和\'e\'的 ASCII 码,即\xb3 和\x65
与上图对应的仿真代码发送字节的部分如下。注意每两个发送串口之间要有时间间隔。
42. jalr 指令涉及 0 号寄存器的读写(流水线)
RISC-V 的 jr、ret 等伪指令会(大概率被 gcc)翻译成 jalr,而且实现中大概率涉及到 0 号寄存器的写入。因此,如果实现了数据旁路,那么有可能会读取到被前推的错误的 0 号寄存器的值。
一个可能的解决办法是,在数据旁路中特判 0 号寄存器。
ret = jr ra
jr xn = jalr x0, 0(xn)
43. 读取 RAM 时,遇到某些 byte 上出现 z 的问题
要注意:Byte Enabling 信号是一个 reg [3:0],而不是一个单独的 reg。当成单独的 reg 使用的话,可能会出现这个信号变成 4\'b0001 的情况,导致出现最低八位数据为 z 的现象。
44. 使用了新的 Xilinx IP 以后,云平台综合报错 The given file isn't marked synthesis generation
解决方法:
在 Tcl Console 里运行下面的命令(把其中 xxx.xci 替换成报错的 IP 文件名,注意星号不能去掉)
set_property generate_synth_checkpoint true [get_files *xxx.xci]
运行后重新将整个工程交到云平台即可。
45. 加了页表选项编译出来的 bin,0x80004000 位置的页表项(用于返回内核态)没有数据怎么办?
先检查一下 kernel.bin 的大小,如果比较小(比如 5KB),那说明它本身都没有 0x80004000 地址,此时需要更新一下 supervisor-rv(makefile 里 kernel.bin 那一行需要有 .data 段,见下图),重新编译发现 kernel.bin 大小变成 28KB 左右才正常,可以使用 objdump 验证。