2022_update

pull/15/head
Zhiyuan Shao 3 years ago
parent dc86842936
commit ea496ce6c4

@ -12,6 +12,7 @@ Copyright (c) 2021, Zhiyuan Shao (zyshao@hust.edu.cn),
Zichen Xu (xuzichen@hust.edu.cn),
Wenzhuo Liu (mgt@oi-wiki.org),
Huan Luo (rogeeerluo@gmail.com)
Guo Li (2925441676@qq.com)
Huazhong University of Science and Technology
Permission is hereby granted, free of charge, to any person obtaining

@ -1,10 +1,12 @@
# 采用RISC-V代理内核的操作系统和系统能力培养实验
**操作系统部分的实验用课件PPT及视频讲解内容可通过[百度网盘](https://pan.baidu.com/s/1H3PWEGwTa_GVrhhLYDwCwg)下载提取码66a3**
[前言](preliminary.md)
[第一章. RISC-V体系结构](chapter1_riscv.md)
## 第一部分:操作系统实验
**操作系统部分的实验用课件PPT及视频讲解内容可通过[百度网盘](https://pan.baidu.com/s/1H3PWEGwTa_GVrhhLYDwCwg)下载提取码66a3**
[第一章. RISC-V体系结构](chapter1_riscv.md) ----- 课程资源: [PPT](./resources/第一章.RISC-V体系结构.pptx) [视频讲解](https://www.bilibili.com/video/BV1ca411K7Zy)
- [1.1 RISC-V发展历史](chapter1_riscv.md#history)
- [1.2 RISC-V汇编语言](chapter1_riscv.md#assembly)
@ -13,13 +15,14 @@
- [1.5 页式虚存管理](chapter1_riscv.md#paging)
- [1.6 相关工具软件](chapter1_riscv.md#toolsoftware)
[第二章. 实验环境配置与实验构成](chapter2_installation.md)
[第二章. 实验环境配置与实验构成](chapter2_installation.md) ----- 课程资源: [PPT](./resources/第二章.实验环境配置与实验构成.pptx) [视频讲解](https://www.bilibili.com/video/BV1hS4y147ZP)
- [2.1 实验环境安装](chapter2_installation.md#environments)
- [2.1 操作系统部分实验环境安装](chapter2_installation.md#environments)
- [2.2 riscv-pke实验代码的获取](chapter2_installation.md#preparecode)
- [2.3 PKE实验的组成](chapter2_installation.md#pke_experiemnts)
[第三章. 实验1系统调用、异常和外部中断](chapter3_traps.md)
[第三章. PKE实验1系统调用、异常和外部中断](chapter3_traps.md) ----- 课程资源: [PPT](./resources/第三章.实验1系统调用、异常和外部中断.pptx) [视频讲解](https://www.bilibili.com/video/BV1aW4y1a71T)
- [3.1 实验1的基础知识](chapter3_traps.md#fundamental)
- [3.2 lab1_1 系统调用](chapter3_traps.md#syscall)
- [3.3 lab1_2 异常处理](chapter3_traps.md#exception)
@ -27,7 +30,7 @@
- [3.5 lab1_challenge1 打印用户程序调用栈(难度:★★★☆☆](chapter3_traps.md#lab1_challenge1_backtrace)
- [3.6 lab1_challenge2 打印异常代码行(难度:★★★☆☆](chapter3_traps.md#lab1_challenge2_errorline)
[第四章. 实验2内存管理](chapter4_memory.md)
[第四章. PKE实验2内存管理](chapter4_memory.md) ----- 课程资源: [PPT](./resources/第四章.实验2内存管理.pptx) [视频讲解](https://www.bilibili.com/video/BV1yd4y1T77a)
- [4.1 实验2的基础知识](chapter4_memory.md#fundamental)
- [4.2 lab2_1 虚实地址转换](chapter4_memory.md#lab2_1_pagetable)
@ -36,7 +39,7 @@
- [4.5 lab2_challenge1 复杂缺页异常(难度:★☆☆☆☆](chapter4_memory.md#lab2_challenge1_pagefault)
- [4.6 lab2_challenge2 堆空间管理(难度:★★★★☆](chapter4_memory.md#lab2_challenge2_singlepageheap)
[第五章. 实验3进程管理](chapter5_process.md)
[第五章. PKE实验3进程管理](chapter5_process.md) ----- 课程资源: [PPT](./resources/第五章.实验3进程管理.pptx) [视频讲解](https://www.bilibili.com/video/BV1Qe4y1D7dv)
- [5.1 实验3的基础知识](chapter5_process.md#fundamental)
- [5.2 lab3_1 进程创建](chapter5_process.md#lab3_1_naive_fork)
@ -45,12 +48,21 @@
- [5.5 lab3_challenge1 进程等待和数据段复制(难度:★★☆☆☆](chapter5_process.md#lab3_challenge1_wait)
- [5.6 lab3_challenge2 实现信号量(难度:★★★☆☆](chapter5_process.md#lab3_challenge2_semaphore)
[第六章. 实验4设备和文件基于RISCV-on-PYNQ](chapter6_device.md)
## 第二部分:系统能力培养实验
[第六章. RISCV处理器在PYNQ上的部署和接口实验](chapter6_riscv_on_pynq.md) ----- 课程资源: [PPT](./resources/第六章.fpga实验.pptx) [视频讲解](https://www.bilibili.com/video/BV1nt4y1n7dm)
- [6.1 系统能力培养部分实验环境安装](chapter6_riscv_on_pynq.md#environments)
- [6.2 fpga实验1在Rocket Chip上添加uart接口](chapter6_riscv_on_pynq.md#hardware_lab1)
- [6.3 fpga实验2以中断方式实现uart通信](chapter6_riscv_on_pynq.md#hardware_lab2)
- [6.4 fpga实验3配置连接到PS端的USB设备](chapter6_riscv_on_pynq.md#hardware_lab3)
[第七章. PKE实验4设备和文件](chapter7_device.md) ----- 课程资源: [PPT](./resources/第七章.实验4设备管理.pptx) [视频讲解](https://www.bilibili.com/video/BV1LB4y157Rb)
- [6.1 实验4的基础知识](chapter6_device.md#fundamental)
- [6.2 lab4_1 轮询方式](chapter6_device.md#polling)
- [6.3 lab4_2 中断方式](chapter6_device.md#PLIC)
- [6.4 lab4_3 主机设备访问](chapter6_device.md#hostdevice)
- [7.1 实验4的基础知识](chapter7_device.md#fundamental)
- [7.2 lab4_1 轮询方式](chapter7_device.md#polling)
- [7.3 lab4_2 中断方式](chapter7_device.md#PLIC)
- [7.4 lab4_3 主机设备访问](chapter7_device.md#hostdevice)

@ -432,19 +432,19 @@ SUMpermit Supervisor User Memory access位用于控制S模式下的虚拟
实际系统中导致中断发生的事件往往是比较复杂的,它们的来源、处理时机和返回方式都不尽相同。为了便于读者对中断的理解以及表达的准确性,我们借鉴参考文献[SiFive Interrupt](#refenences)的中断分类标准将系统中发生的可能中断当前执行程序的事件分为3类
**Exception**(异常):这类中断是处理器在执行某条指令时,由于条件不满足而产生的。典型的异常有除零错误、缺页、执行当前特权级不支持的指令等。**相对于正在执行的程序而言exception是同步synchronous发生的。exception产生的时机是指令执行的过程中即处理器流水线的执行阶段在exception处理完毕后系统将返回发生exception的那条指令重新执行**。
**Exception**(异常):这类中断是处理器在执行某条指令时,由于条件不满足而产生的。典型的异常有缺页、执行当前特权级不支持的指令等。**相对于正在执行的程序而言exception是同步synchronous发生的。exception产生的时机是指令执行的过程中即处理器流水线的执行阶段在exception处理完毕后系统将返回发生exception的那条指令重新执行**。
**Trap**即我们通常理解的“系统调用”或者“软件中断”但是我们不建议把它翻译为“陷阱”因为“陷阱”这个词在中文语境的含义甚至和“中断”一样宽泛。RISC-V中trap等同于syscall这类中断是当前执行的程序主动发出的ecall指令类似8086中的int指令导致的。典型的trap有屏幕输出printf、磁盘文件读写read/write这些高级语言函数调用通过系统函数库libc的转换在RISC-V平台都会转换成ecall指令。**与exception类似相对于正在执行的程序而言trap也是同步synchronous发生的。但与exception不同的地方在于trap在处理完成后返回的是下一条指令。**
**Trap**即我们通常理解的“系统调用”或者“软件中断”但是我们不建议把它翻译为“陷阱”因为“陷阱”这个词在中文语境的含义甚至和“中断”一样宽泛。RISC-V中trap等同于syscall,为了与其他类型的中断进行区分,**我们更推荐使用syscall来指代这类中断**这类中断是当前执行的程序主动发出的ecall指令类似8086中的int指令导致的。典型的trap有屏幕输出printf、磁盘文件读写read/write这些高级语言函数调用通过系统函数库libc的转换在RISC-V平台都会转换成ecall指令。**与exception类似相对于正在执行的程序而言syscall也是同步synchronous发生的。但与exception不同的地方在于syscall在处理完成后返回的是下一条指令。**
**Interrupt**我们不建议对它进行任何形式的翻译因为“中断”在中文语境中的含义过于宽泛这类中断一般是由外部设备产生的事件而导致的。在Intel的x86系列处理器中interrupt也被称为IRQInterrupt ReQuest。典型的interrupt可编程时钟计时器PIT所产生的timer事件、DMA控制器发出的I/O完成事件、声卡发出的缓存空间用完事件等。**相对于正在执行的程序而言interrupt是异步asynchronous发生的。另外对于处理器流水线而言interrupt的处理时机是指令的间隙。不同于exception但与trap类似interrupt在处理完成后返回的是下一条指令**。
**Interrupt**我们不建议对它进行任何形式的翻译因为“中断”在中文语境中的含义过于宽泛这类中断一般是由外部设备产生的事件而导致的。在Intel的x86系列处理器中interrupt也被称为IRQInterrupt ReQuest,实际上,**我们更推荐使用IRQ来指代外部中断**用Interrupt往往导致指代范围太宽泛的问题。典型的IRQ可编程时钟计时器PIT所产生的timer事件、DMA控制器发出的I/O完成事件、声卡发出的缓存空间用完事件等。**相对于正在执行的程序而言interrupt是异步asynchronous发生的。另外对于处理器流水线而言interrupt的处理时机是指令的间隙。不同于exception但与trap类似interrupt在处理完成后返回的是下一条指令**。
表1.6 中断的分类
| 中断类型 | 产生时机 | 处理时机 | 返回地址 |
| --------- | ------------------ | ------------ | -------------- |
| Exception | 同步(于当前程序) | 指令执行阶段 | 发生异常的指令 |
| Trap | 同步(于当前程序) | - | 下一条指令 |
| Interrupt | 异步(于当前程序) | 指令执行间隙 | 下一条指令 |
| 中断类型 | 产生时机 | 处理时机 | 返回地址 |
| ------------------- | ------------------ | ------------ | -------------- |
| Exception | 同步(于当前程序) | 指令执行阶段 | 发生异常的指令 |
| Trap (**syscall**) | 同步(于当前程序) | - | 下一条指令 |
| Interrupt (**IRQ**) | 异步(于当前程序) | 指令执行间隙 | 下一条指令 |
表1.6对这3类中断进行了归纳和比较。需要注意的是不同文献特别是中文文献对于某个类型的中断可能用了不同的名字例如trap在很多文献和参考书中又被称为“陷阱”、“陷入”、“软件中断”或“系统调用”等等。
@ -514,9 +514,9 @@ SUMpermit Supervisor User Memory access位用于控制S模式下的虚拟
当发生一个中断假设其目标模式即执行中断例程的模式为机器模式RISC-V处理器硬件将执行以下动作
1保存发生中断前的pc如果是trap或者interrupt则保存下一条指令的pc到mepc寄存器
1保存进入中断处理历程之前的pc如果是trap或者interrupt则保存下一条指令的pc到mepc寄存器
2发生中断前的特权级保存到mstatus寄存器的MPP字段
2进入中断处理历程之前的特权级保存到mstatus寄存器的MPP字段
3将mstatus寄存器中的MIE字段保存到它自己的MPIE字段

@ -1,22 +1,21 @@
# 第二章.实验环境配置与实验构成
### 目录
- [2.1 实验环境安装](#environments)
- [2.1.1 安装操作系统环境](#subsec_osenvironments)
- [2.1.2 安装执行支撑软件](#subsec_softwarepackages)
- [2.1 操作系统部分实验环境安装](#environments)
- [2.1.1 安装开发环境](#subsec_osenvironments)
- [2.1.2 安装支撑软件](#subsec_softwarepackages)
- [2.1.3 头歌平台](#subsec_educoder)
- [2.2 riscv-pke实验代码的获取](#preparecode)
- [2.3 PKE实验构成](#pke_experiemnts)
<a name="environments"></a>
## 2.1 实验环境安装
## 2.1 操作系统部分实验环境安装
<a name="subsec_osenvironments"></a>
### 2.1.1 安装操作系统环境
### 2.1.1 安装开发环境
安装即将编译、构建build和执行riscv-pke的环境我们给出以下3种选择
@ -38,7 +37,7 @@
<a name="subsec_softwarepackages"></a>
### 2.1.2 安装执行支撑软件
### 2.1.2 安装支撑软件
riscv-pke的源代码的编译compile、构建build需要专门的risc-v交叉编译器所构建的代理内核及应用的执行也需要专门的risc-v虚拟机spike的支持。这一节将介绍这些支撑软件的安装过程需要说明的是该安装过程适合操作系统环境见[2.1.1节](#subsec_osenvironments))为**WSL**,或者**Ubuntu发行版**的情况。对于头歌实验平台,则无需对执行支撑软件进行任何安装。
@ -59,7 +58,7 @@ RISC-V交叉编译器的执行仍然需要一些本地支撑软件包可使
● 第二步,下载自包含交叉编译器+执行环境
到[这里](./resources/riscv64-elf-gcc-20210923.tgz)下载自包含交叉编译器+执行环境的tgz包大小约为72MB感谢张杰老师为大家制作采用以下命令解开tgz包
到[**这里**](./resources/riscv64-elf-gcc-20210923.tgz)下载自包含交叉编译器+执行环境的tgz包大小约为72MB感谢张杰老师为大家制作采用以下命令解开tgz包
$ tar xf riscv64-elf-gcc-20210923.tgz
@ -179,8 +178,6 @@ RISC-V交叉编译器的构建需要一些本地支撑软件包可使用以
“基于RISCV的操作系统实验”课程的设计充分考虑到了实验的模块化以及学制安排的问题。课程共设计了15个实验其中包含9个基础实验和6个挑战实验随着时间推移这个列表可能会进一步增加实验间的关联见[PKE实验的组成](#pke_experiemnts)。**教师开设教学课堂的时候,可以根据学生的水平、教学预期按需选择不同的实验内容,分期分批地给学生安排实验任务**。
<a name="preparecode"></a>
## 2.2 riscv-pke实验代码的获取

@ -30,8 +30,8 @@
- [实验内容](#lab1_challenge2_content)
- [实验指导](#lab1_challenge2_guide)
<a name="fundamental"></a>
## 3.1 实验1的基础知识
本章我们将首先[获得代码](#subsec_preparecode),接下来介绍[程序的编译链接和ELF文件](#subsec_elfload)的基础知识接着讲述riscv-pke操作系统内核的[启动原理](#subsec_booting)最后开始实验1的3个实验。
@ -607,53 +607,63 @@ $ riscv64-unknown-elf-objdump -D ./obj/riscv-pke | grep _mentry
28 call m_start
```
它的执行将机器复位16行为在不同处理器上我们在lab1_1中只考虑单个内核运行的内核分配大小为4KB的栈20--25行并在最后28行调用m_start函数。m_start函数是在kernel/machine/minit.c文件中定义的
它的执行将机器复位16行为在不同处理器上我们在PKE的基础实验中只考虑单个处理器的情况运行的内核分配大小为4KB的栈20--25行并在最后28行调用m_start函数。m_start函数是在kernel/machine/minit.c文件中定义的
```c
68 void m_start(uintptr_t hartid, uintptr_t dtb) {
69 // init the spike file interface (stdin,stdout,stderr)
70 spike_file_init();
71 sprint("In m_start, hartid:%d\n", hartid);
72
73 // init HTIF (Host-Target InterFace) and memory by using the Device Table Blob (DTB)
74 init_dtb(dtb);
75
76 // set previous privilege mode to S (Supervisor), and will enter S mode after 'mret'
77 write_csr(mstatus, ((read_csr(mstatus) & ~MSTATUS_MPP_MASK) | MSTATUS_MPP_S));
78
79 // set M Exception Program Counter to sstart, for mret (requires gcc -mcmodel=medany)
80 write_csr(mepc, (uint64)s_start);
81
82 // delegate all interrupts and exceptions to supervisor mode.
83 delegate_traps();
84
85 // switch to supervisor mode and jump to s_start(), i.e., set pc to mepc
86 asm volatile("mret");
87 }
77 void m_start(uintptr_t hartid, uintptr_t dtb) {
78 // init the spike file interface (stdin,stdout,stderr)
79 // functions with "spike_" prefix are all defined in codes under spike_interface/,
80 // sprint is also defined in spike_interface/spike_utils.c
81 spike_file_init();
82 sprint("In m_start, hartid:%d\n", hartid);
83
84 // init HTIF (Host-Target InterFace) and memory by using the Device Table Blob (DTB)
85 // init_dtb() is defined above.
86 init_dtb(dtb);
87
88 // set previous privilege mode to S (Supervisor), and will enter S mode after 'mret'
89 // write_csr is a macro defined in kernel/riscv.h
90 write_csr(mstatus, ((read_csr(mstatus) & ~MSTATUS_MPP_MASK) | MSTATUS_MPP_S));
91
92 // set M Exception Program Counter to sstart, for mret (requires gcc -mcmodel=medany)
93 write_csr(mepc, (uint64)s_start);
94
95 // delegate all interrupts and exceptions to supervisor mode.
96 // delegate_traps() is defined above.
97 delegate_traps();
98
99 // switch to supervisor mode (S mode) and jump to s_start(), i.e., set pc to mepc
100 asm volatile("mret");
101 }
```
它的作用是首先初始化spike的客户机-主机接口Host-Target InterFace简称HTIF以及承载于其上的文件接口70-74行人为的将上一个状态机器启动时的状态为M态即Machine态设置为SSupervisor并将“退回”到S态的函数指针s_start写到mepc寄存器中77--80行接下来将中断异常处理“代理”给S态83行最后执行返回动作86行。由于之前人为地将上一个状态设置为S态所以86行的返回动作将“返回”S态并进入s_start函数执行。
它的作用是首先初始化spike的客户机-主机接口Host-Target InterFace简称HTIF以及承载于其上的文件接口81-86人为的将上一个状态机器启动时的状态为M态即Machine态设置为SSupervisor并将“退回”到S态的函数指针s_start写到mepc寄存器中90--93行接下来将中断异常处理“代理”给S态97行最后执行返回动作100行。由于之前人为地将上一个状态设置为S态所以第100行的返回动作将“返回”S态并进入s_start函数执行。
s_start函数在kernel/kernel.c文件中定义
```c
30 int s_start(void) {
31 sprint("Enter supervisor mode...\n");
32 // Note: we use direct (i.e., Bare mode) for memory mapping in lab1.
33 // which means: Virtual Address = Physical Address
34 write_csr(satp, 0);
35
36 // the application code (elf) is first loaded into memory, and then put into execution
37 load_user_program(&user_app);
38
39 sprint("Switch to user mode...\n");
40 switch_to(&user_app);
41
42 return 0;
43 }
34 int s_start(void) {
35 sprint("Enter supervisor mode...\n");
36 // Note: we use direct (i.e., Bare mode) for memory mapping in lab1.
37 // which means: Virtual Address = Physical Address
38 // therefore, we need to set satp to be 0 for now. we will enable paging in lab2_x.
39 //
40 // write_csr is a macro defined in kernel/riscv.h
41 write_csr(satp, 0);
42
43 // the application code (elf) is first loaded into memory, and then put into execution
44 load_user_program(&user_app);
45
46 sprint("Switch to user mode...\n");
47 // switch_to() is defined in kernel/process.c
48 switch_to(&user_app);
49
50 // we should never reach here.
51 return 0;
52 }
```
该函数的动作也非常简单首先将地址映射模式置为34行直映射模式Bare mode接下来调用37行load_user_program()函数,将应用(也就是最开始的命令行中的./obj/app_helloworld载入内存封装成一个最简单的“进程process最终调用switch_to()函数,将这个简单得不能再简单的进程投入运行。
该函数的动作也非常简单首先将地址映射模式置为41直映射模式Bare mode接下来调用44load_user_program()函数,将应用(也就是最开始的命令行中的./obj/app_helloworld载入内存封装成一个最简单的“进程process最终调用switch_to()函数,将这个简单得不能再简单的进程投入运行。
以上过程中load_user_program()函数的作用是将我们的给定应用user/app_helloworld.c所对应的可执行ELF文件即./obj/app_helloworld文件载入spike虚拟内存这个过程我们将在3.1.5中详细讨论。另一个函数是switch_to()为了理解这个函数的行为需要先对lab1中“进程”的定义有一定的了解kernel/process.h
@ -691,49 +701,54 @@ s_start函数在kernel/kernel.c文件中定义
29 assert(proc);
30 current = proc;
31
32 write_csr(stvec, (uint64)smode_trap_vector);
33 // set up trapframe values that smode_trap_vector will need when
34 // the process next re-enters the kernel.
35 proc->trapframe->kernel_sp = proc->kstack; // process's kernel stack
36 proc->trapframe->kernel_trap = (uint64)smode_trap_handler;
37
38 // set up the registers that strap_vector.S's sret will use
39 // to get to user space.
40
41 // set S Previous Privilege mode to User.
42 unsigned long x = read_csr(sstatus);
43 x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
44 x |= SSTATUS_SPIE; // enable interrupts in user mode
45
46 write_csr(sstatus, x);
32 // write the smode_trap_vector (64-bit func. address) defined in kernel/strap_vector.S
33 // to the stvec privilege register, such that trap handler pointed by smode_trap_vector
34 // will be triggered when an interrupt occurs in S mode.
35 write_csr(stvec, (uint64)smode_trap_vector);
36
37 // set up trapframe values (in process structure) that smode_trap_vector will need when
38 // the process next re-enters the kernel.
39 proc->trapframe->kernel_sp = proc->kstack; // process's kernel stack
40 proc->trapframe->kernel_trap = (uint64)smode_trap_handler;
41
42 // SSTATUS_SPP and SSTATUS_SPIE are defined in kernel/riscv.h
43 // set S Previous Privilege mode (the SSTATUS_SPP bit in sstatus register) to User mode.
44 unsigned long x = read_csr(sstatus);
45 x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
46 x |= SSTATUS_SPIE; // enable interrupts in user mode
47
48 // set S Exception Program Counter to the saved user pc.
49 write_csr(sepc, proc->trapframe->epc);
48 // write x back to 'sstatus' register to enable interrupts, and sret destination mode.
49 write_csr(sstatus, x);
50
51 // switch to user mode with sret.
52 return_to_user(proc->trapframe);
53 }
51 // set S Exception Program Counter (sepc register) to the elf entry pc.
52 write_csr(sepc, proc->trapframe->epc);
53
54 // return_to_user() is defined in kernel/strap_vector.S. switch to user mode with sret.
55 return_to_user(proc->trapframe);
56 }
```
可以看到该函数的作用是初始化进程的process结构体并最终调用return_to_user(proc->trapframe)函数将载入的应用所封装的进程投入运行。return_to_user()函数在kernel/strap_vector.S文件中定义
```assembly
45 .globl return_to_user
46 return_to_user:
47 # save a0 in sscratch, so sscratch points to a trapframe now.
48 csrw sscratch, a0
49
50 # let [t6]=[a0]
51 addi t6, a0, 0
52
53 # restore all registers from trapframe, so as to resort the execution of a process
54 restore_all_registers
55
56 # return to user mode and user pc.
57 sret
49 .globl return_to_user
50 return_to_user:
51 # [sscratch]=[a0], save a0 in sscratch, so sscratch points to a trapframe now.
52 csrw sscratch, a0
53
54 # let [t6]=[a0]
55 addi t6, a0, 0
56
57 # restore_all_registers is a assembly macro defined in util/load_store.S.
58 # the macro restores all registers from trapframe started from [t6] to all general
59 # purpose registers, so as to resort the execution of a process.
60 restore_all_registers
61
62 # return to user mode and user pc.
63 sret
```
其作用是恢复进程的上下文(54到RISC-V机器的所有寄存器并调用sret指令从S模式“返回”应用模式即U模式。这样所载入的应用程序即obj/app_helloworld所对应的“进程”就投入运行了。
其作用是恢复进程的上下文(60到RISC-V机器的所有寄存器并调用sret指令从S模式“返回”应用模式即U模式。这样所载入的应用程序即obj/app_helloworld所对应的“进程”就投入运行了。
@ -744,60 +759,65 @@ s_start函数在kernel/kernel.c文件中定义
这里我们对load_user_program()函数进行讨论它在kernel/kernel.c中定义
```c
18 void load_user_program(process *proc) {
19 proc->trapframe = (trapframe *)USER_TRAP_FRAME;
20 memset(proc->trapframe, 0, sizeof(trapframe));
21 proc->kstack = USER_KSTACK;
22 proc->trapframe->regs.sp = USER_STACK;
23
24 load_bincode_from_host_elf(proc);
25 }
19 void load_user_program(process *proc) {
20 // USER_TRAP_FRAME is a physical address defined in kernel/config.h
21 proc->trapframe = (trapframe *)USER_TRAP_FRAME;
22 memset(proc->trapframe, 0, sizeof(trapframe));
23 // USER_KSTACK is also a physical address defined in kernel/config.h
24 proc->kstack = USER_KSTACK;
25 proc->trapframe->regs.sp = USER_STACK;
26
27 // load_bincode_from_host_elf() is defined in kernel/elf.c
28 load_bincode_from_host_elf(proc);
29 }
```
我们看到它的作用是首先对进程壳做了一定的初始化最后调用load_bincode_from_host_elf()函数将应用程序对应的二进制代码实际地载入。load_bincode_from_host_elf()函数在kernel/elf.c文件中实际定义
```c
103 void load_bincode_from_host_elf(struct process *p) {
104 arg_buf arg_bug_msg;
105
106 // retrieve command line arguements
107 size_t argc = parse_args(&arg_bug_msg);
108 if (!argc) panic("You need to specify the application program!\n");
107 void load_bincode_from_host_elf(process *p) {
108 arg_buf arg_bug_msg;
109
110 sprint("Application: %s\n", arg_bug_msg.argv[0]);
111
112 //elf loading
113 elf_ctx elfloader;
114 elf_info info;
110 // retrieve command line arguements
111 size_t argc = parse_args(&arg_bug_msg);
112 if (!argc) panic("You need to specify the application program!\n");
113
114 sprint("Application: %s\n", arg_bug_msg.argv[0]);
115
116 info.f = spike_file_open(arg_bug_msg.argv[0], O_RDONLY, 0);
117 info.p = p;
118 if (IS_ERR_VALUE(info.f)) panic("Fail on openning the input application program.\n");
119
120 // init elfloader
121 if (elf_init(&elfloader, &info) != EL_OK)
122 panic("fail to init elfloader.\n");
123
124 // load elf
125 if (elf_load(&elfloader) != EL_OK) panic("Fail on loading elf.\n");
126
127 // entry (virtual) address
128 p->trapframe->epc = elfloader.ehdr.entry;
116 //elf loading. elf_ctx is defined in kernel/elf.h, used to track the loading process.
117 elf_ctx elfloader;
118 // elf_info is defined above, used to tie the elf file and its corresponding process.
119 elf_info info;
120
121 info.f = spike_file_open(arg_bug_msg.argv[0], O_RDONLY, 0);
122 info.p = p;
123 // IS_ERR_VALUE is a macro defined in spike_interface/spike_htif.h
124 if (IS_ERR_VALUE(info.f)) panic("Fail on openning the input application program.\n");
125
126 // init elfloader context. elf_init() is defined above.
127 if (elf_init(&elfloader, &info) != EL_OK)
128 panic("fail to init elfloader.\n");
129
130 // close host file
131 spike_file_close( info.f );
130 // load elf. elf_load() is defined above.
131 if (elf_load(&elfloader) != EL_OK) panic("Fail on loading elf.\n");
132
133 sprint("Application program entry point (virtual address): 0x%lx\n", p->trapframe->epc);
134 }
133 // entry (virtual, also physical in lab1_x) address
134 p->trapframe->epc = elfloader.ehdr.entry;
135
136 // close the host spike file
137 spike_file_close( info.f );
138
139 sprint("Application program entry point (virtual address): 0x%lx\n", p->trapframe->epc);
140 }
```
该函数的大致过程是:
- 107--108行首先解析命令行参数获得需要加载的ELF文件文件名
- 113--122行接下来初始化ELF加载数据结构并打开即将被加载的ELF文件
- 125加载ELF文件
- 128通过ELF文件提供的入口地址设置进程的trapframe->epc保证“返回”用户态的时候所加载的ELF文件被执行
- 131--133关闭ELF文件并返回。
- 108--114首先解析命令行参数获得需要加载的ELF文件文件名
- 117--128行接下来初始化ELF加载数据结构并打开即将被加载的ELF文件
- 131加载ELF文件
- 134通过ELF文件提供的入口地址设置进程的trapframe->epc保证“返回”用户态的时候所加载的ELF文件被执行
- 137--139关闭ELF文件并返回。
该函数用到了同文件中的诸多工具函数,这些函数的细节请读者自行阅读相关代码,这里我们只贴我们认为重要的代码:
@ -806,28 +826,30 @@ s_start函数在kernel/kernel.c文件中定义
- elf_load读入ELF文件中所包含的程序段segment到给定的内存地址中。elf_load的具体实现如下
```c
51 elf_status elf_load(elf_ctx *ctx) {
52 elf_prog_header ph_addr;
53 int i, off;
54 // traverse the elf program segment headers
55 for (i = 0, off = ctx->ehdr.phoff; i < ctx->ehdr.phnum; i++, off += sizeof(ph_addr)) {
56 // read segment headers
57 if (elf_fpread(ctx, (void *)&ph_addr, sizeof(ph_addr), off) != sizeof(ph_addr)) return EL_EIO;
58
59 if (ph_addr.type != ELF_PROG_LOAD) continue;
60 if (ph_addr.memsz < ph_addr.filesz) return EL_ERR;
61 if (ph_addr.vaddr + ph_addr.memsz < ph_addr.vaddr) return EL_ERR;
53 elf_status elf_load(elf_ctx *ctx) {
54 // elf_prog_header structure is defined in kernel/elf.h
55 elf_prog_header ph_addr;
56 int i, off;
57
58 // traverse the elf program segment headers
59 for (i = 0, off = ctx->ehdr.phoff; i < ctx->ehdr.phnum; i++, off += sizeof(ph_addr)) {
60 // read segment headers
61 if (elf_fpread(ctx, (void *)&ph_addr, sizeof(ph_addr), off) != sizeof(ph_addr)) return EL _EIO;
62
63 // allocate memory before loading
64 void *dest = elf_alloc_mb(ctx, ph_addr.vaddr, ph_addr.vaddr, ph_addr.memsz);
65
66 // actual loading
67 if (elf_fpread(ctx, dest, ph_addr.memsz, ph_addr.off) != ph_addr.memsz)
68 return EL_EIO;
69 }
70
71 return EL_OK;
72 }
63 if (ph_addr.type != ELF_PROG_LOAD) continue;
64 if (ph_addr.memsz < ph_addr.filesz) return EL_ERR;
65 if (ph_addr.vaddr + ph_addr.memsz < ph_addr.vaddr) return EL_ERR;
66
67 // allocate memory block before elf loading
68 void *dest = elf_alloc_mb(ctx, ph_addr.vaddr, ph_addr.vaddr, ph_addr.memsz);
69
70 // actual loading
71 if (elf_fpread(ctx, dest, ph_addr.memsz, ph_addr.off) != ph_addr.memsz)
72 return EL_EIO;
73 }
74
75 return EL_OK;
76 }
```
这个函数里我们需要说一下elf_alloc_mb()函数该函数返回代码段将要加载进入的地址dest。由于我们在lab1全面采用了直地址映射模式Bare mode也就是说逻辑地址=物理地址对于lab1全系列的实验来说elf_alloc_mb()返回的装载地址实际上就是物理地址。
@ -957,7 +979,7 @@ lab1_1实验需要读者了解和掌握操作系统中系统调用机制的实
27 }
```
我们发现do_user_call函数是通过ecall指令完成系统调用的且在执行ecall指令前所有的参数即do_user_call函数的8个参数实际上都已经载入到RISC-V机器的a0到a7这8个寄存器中这一步是我们的编译器生成的代码帮我们完成的。ecall指令的执行将根据a0寄存器中的值获得系统调用号并使RISC-V转到S模式因为我们的操作系统内核启动时将所有的中断、异常、系统调用都代理给了S模式的trap处理入口执行在kernel/strap_vector.S文件中定义
我们发现do_user_call函数是通过ecall指令完成系统调用的且在执行ecall指令前所有的参数即do_user_call函数的8个参数实际上都已经载入到RISC-V机器的a0到a7这8个寄存器中这一步是我们的编译器生成的代码帮我们完成的。ecall指令的执行将根据a0寄存器中的值获得系统调用号并使RISC-V转到S模式因为我们的操作系统内核启动时将所有的中断、异常、系统调用都代理给了S模式的trap处理入口执行在kernel/strap_vector.S文件中定义
```assembly
16 .globl smode_trap_vector
@ -968,29 +990,33 @@ lab1_1实验需要读者了解和掌握操作系统中系统调用机制的实
21
22 # save the context (user registers) of current process in its trapframe.
23 addi t6, a0 , 0
24 store_all_registers
25
26 # come back to save a0 register before entering trap handling in trapframe
27 csrr t0, sscratch
28 sd t0, 72(a0)
29
30 # use the "user kernel" stack (whose pointer stored in p->trapframe->kernel_sp)
31 ld sp, 248(a0)
32
33 # load the address of smode_trap_handler() from p->trapframe->kernel_trap
34 ld t0, 256(a0)
35
36 # jump to smode_trap_handler() that is defined in kernel/trap.c
37 jr t0
24
25 # store_all_registers is a macro defined in util/load_store.S, it stores contents
26 # of all general purpose registers into a piece of memory started from [t6].
27 store_all_registers
28
29 # come back to save a0 register before entering trap handling in trapframe
30 # [t0]=[sscratch]
31 csrr t0, sscratch
32 sd t0, 72(a0)
33
34 # use the "user kernel" stack (whose pointer stored in p->trapframe->kernel_sp)
35 ld sp, 248(a0)
36
37 # load the address of smode_trap_handler() from p->trapframe->kernel_trap
38 ld t0, 256(a0)
39
40 # jump to smode_trap_handler() that is defined in kernel/trap.c
41 jr t0
```
从以上代码我们可以看到trap的入口处理函数首先将“进程”即我们的obj/app_helloworld的运行现场进行保存第24行接下来将a0寄存器中的系统调用号保存到内核堆栈第27--28行再将p->trapframe->kernel_sp指向的为应用进程分配的内核栈设置到sp寄存器第31行**该过程实际上完成了栈的切换**。完整的切换过程为:
从以上代码我们可以看到trap的入口处理函数首先将“进程”即我们的obj/app_helloworld的运行现场进行保存第27行接下来将a0寄存器中的系统调用号保存到内核堆栈第31--32再将p->trapframe->kernel_sp指向的为应用进程分配的内核栈设置到sp寄存器第35行),**该过程实际上完成了栈的切换**。完整的切换过程为:
- 1应用程序在U模式即应用态执行这个时候是使用的操作系统为其分配的栈称为用户栈这一部分参见`kernel/kernel.c`文件中`load_user_program`的实现;
- 2应用程序调用ecall后陷入内核开始执行`smode_trap_vector`函数,此时使用的是操作系统内核的栈。参见`kernel/machine/mentry.S`文件中的PKE入口`_mentry`
- 3中断处理例程`smode_trap_vector`函数执行到第31行时,将栈切换到用户进程“自带”的“用户内核栈“,也就是`kernel/process.c`文件中`switch_to`函数的第35行所引用的`proc->kstack`而不使用PKE内核自己的栈**这里请读者思考为何要这样安排**
- 3中断处理例程`smode_trap_vector`函数执行到第35行时,将栈切换到用户进程“自带”的“用户内核栈“,也就是`kernel/process.c`文件中`switch_to`函数的第39行所引用的`proc->kstack`而不使用PKE内核自己的栈**这里请读者思考为何要这样安排**
后续的执行将使用应用进程所附带的内核栈来保存执行的上下文如函数调用、临时变量这些最后将应用进程中的p->trapframe->kernel_trap写入t0寄存器第34行并最后第37调用p->trapframe->kernel_trap所指向的smode_trap_handler()函数。
后续的执行将使用应用进程所附带的内核栈来保存执行的上下文如函数调用、临时变量这些最后将应用进程中的p->trapframe->kernel_trap写入t0寄存器第38行并最后第41调用p->trapframe->kernel_trap所指向的smode_trap_handler()函数。
smode_trap_handler()函数的定义在kernel/strap.c文件中采用C语言编写
@ -1004,21 +1030,22 @@ smode_trap_handler()函数的定义在kernel/strap.c文件中采用C语言编
39 // save user process counter.
40 current->trapframe->epc = read_csr(sepc);
41
42 // if the cause of trap is syscall from user application
43 if (read_csr(scause) == CAUSE_USER_ECALL) {
44 handle_syscall(current->trapframe);
45 } else {
46 sprint("smode_trap_handler(): unexpected scause %p\n", read_csr(scause));
47 sprint(" sepc=%p stval=%p\n", read_csr(sepc), read_csr(stval));
48 panic( "unexpected exception happened.\n" );
49 }
50
51 // continue the execution of current process.
52 switch_to(current);
53 }
42 // if the cause of trap is syscall from user application.
43 // read_csr() and CAUSE_USER_ECALL are macros defined in kernel/riscv.h
44 if (read_csr(scause) == CAUSE_USER_ECALL) {
45 handle_syscall(current->trapframe);
46 } else {
47 sprint("smode_trap_handler(): unexpected scause %p\n", read_csr(scause));
48 sprint(" sepc=%p stval=%p\n", read_csr(sepc), read_csr(stval));
49 panic( "unexpected exception happened.\n" );
50 }
51
52 // continue (come back to) the execution of current process.
53 switch_to(current);
54 }
```
该函数首先在第36行对进入当前特权级模式S模式之前的模式进行判断确保进入前是用户模式U模式接下来在第40行保存发生系统调用的指令地址进一步判断第43--49行的if...else...语句导致进入当前模式的原因如果是系统调用的话read_csr(scause) == CAUSE_USER_ECALL就执行handle_syscall()函数但如果是其他原因对于其他原因的处理我们将在后续实验中进一步完善的话就打印出错信息并推出最后在第52行调用switch_to()函数返回当前进程。
该函数首先在第36行对进入当前特权级模式S模式之前的模式进行判断确保进入前是用户模式U模式接下来在第40行保存发生系统调用的指令地址进一步判断第44--50行的if...else...语句导致进入当前模式的原因如果是系统调用的话read_csr(scause) == CAUSE_USER_ECALL就执行handle_syscall()函数但如果是其他原因对于其他原因的处理我们将在后续实验中进一步完善的话就打印出错信息并推出最后在第53行调用switch_to()函数返回当前进程。
handle_syscall()函数的定义也在kernel/strap.c文件中
@ -1179,63 +1206,73 @@ lab1_2实验需要读者了解和掌握操作系统中异常exception
通过[3.1.5](#subsec_booting)节的阅读我们知道PKE操作系统内核在启动时会将部分异常和中断“代理”给S模式处理但是它是否将CAUSE_ILLEGAL_INSTRUCTION这类异常也进行了代理呢这就要研究m_start()函数在执行delegate_traps()函数时设置的代理规则了我们先查看delegate_traps()函数的代码在kernel/machine/minit.c文件中找到它对应的代码
```c
51 static void delegate_traps() {
52 if (!supports_extension('S')) {
53 // confirm that our processor supports supervisor mode. abort if not.
54 sprint("s mode is not supported.\n");
55 return;
56 }
57
58 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
59 uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) |
60 (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) |
61 (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_USER_ECALL);
55 static void delegate_traps() {
56 // supports_extension macro is defined in kernel/riscv.h
57 if (!supports_extension('S')) {
58 // confirm that our processor supports supervisor mode. abort if it does not.
59 sprint("S mode is not supported.\n");
60 return;
61 }
62
63 write_csr(mideleg, interrupts);
64 write_csr(medeleg, exceptions);
65 assert(read_csr(mideleg) == interrupts);
66 assert(read_csr(medeleg) == exceptions);
67 }
63 // macros used in following two statements are defined in kernel/riscv.h
64 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
65 uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) |
66 (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) |
67 (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_USER_ECALL);
68
69 // writes 64-bit values (interrupts and exceptions) to 'mideleg' and 'medeleg' (two
70 // priviledged registers of RV64G machine) respectively.
71 //
72 // write_csr and read_csr are macros defined in kernel/riscv.h
73 write_csr(mideleg, interrupts);
74 write_csr(medeleg, exceptions);
75 assert(read_csr(mideleg) == interrupts);
76 assert(read_csr(medeleg) == exceptions);
77 }
```
在第58--61行的代码中delegate_traps()函数确实将部分异常代理给了S模式处理但是里面并没有我们关心的CAUSE_ILLEGAL_INSTRUCTION异常这说明该异常的处理还是交给M模式来处理实际上对于spike模拟的RISC-V平台而言CAUSE_ILLEGAL_INSTRUCTION异常*必须*在M态处理所以我们需要了解M模式的trap处理入口以便继续跟踪其后的处理过程。M模式的trap处理入口在kernel/machine/mtrap_vector.S文件中PKE操作系统内核在启动时kernel/machine/minit.c文件的第125行`write_csr(mtvec, (uint64)mtrapvec);`已经将M模式的中断处理入口指向了该函数
在第64--67行的代码中delegate_traps()函数确实将部分异常代理给了S模式处理但是里面并没有我们关心的CAUSE_ILLEGAL_INSTRUCTION异常这说明该异常的处理还是交给M模式来处理实际上对于spike模拟的RISC-V平台而言CAUSE_ILLEGAL_INSTRUCTION异常*必须*在M态处理所以我们需要了解M模式的trap处理入口以便继续跟踪其后的处理过程。M模式的trap处理入口在kernel/machine/mtrap_vector.S文件中PKE操作系统内核在启动时kernel/machine/minit.c文件的第104行`write_csr(mtvec, (uint64)mtrapvec);`已经将M模式的中断处理入口指向了该函数
```assembly
8 mtrapvec:
9 # swap a0 and mscratch
10 # so that a0 points to interrupt frame
11 csrrw a0, mscratch, a0
12
13 # save the registers in interrupt frame
14 addi t6, a0, 0
15 store_all_registers
16 # save the user a0 in itrframe->a0
17 csrr t0, mscratch
18 sd t0, 72(a0)
19
20 # use stack0 for sp
21 la sp, stack0
22 li a3, 4096
23 csrr a4, mhartid
24 addi a4, a4, 1
25 mul a3, a3, a4
26 add sp, sp, a3
27
28 // save the address of interrupt frame in the csr "mscratch"
29 csrw mscratch, a0
30
31 call handle_mtrap
9 # mscratch -> g_itrframe (cf. kernel/machine/minit.c line 94)
10 # swap a0 and mscratch, so that a0 points to interrupt frame,
11 # i.e., [a0] = &g_itrframe
12 csrrw a0, mscratch, a0
13
14 # save the registers in g_itrframe
15 addi t6, a0, 0
16 store_all_registers
17 # save the original content of a0 in g_itrframe
18 csrr t0, mscratch
19 sd t0, 72(a0)
20
21 # switch stack (to use stack0) for the rest of machine mode
22 # trap handling.
23 la sp, stack0
24 li a3, 4096
25 csrr a4, mhartid
26 addi a4, a4, 1
27 mul a3, a3, a4
28 add sp, sp, a3
29
30 # pointing mscratch back to g_itrframe
31 csrw mscratch, a0
32
33 // restore all registers
34 csrr t6, mscratch
35 restore_all_registers
36
37 mret
33 # call machine mode trap handling function
34 call handle_mtrap
35
36 # restore all registers, come back to the status before entering
37 # machine mode handling.
38 csrr t6, mscratch
39 restore_all_registers
40
41 mret
```
可以看到mtrapvec汇编函数首先会将a0和mscratch交换而mscratch之前保护的是g_itrframe的地址g_itrframe的定义在kernel/machine/minit.c的第27行`struct riscv_regs g_itrframe;`也就是说g_itrframe是一个包含所有RISC-V通用寄存器的栈帧。接下来将t6赋值为a0的值第14行并将所有通用寄存器保存到t6寄存器所指定首地址的内存区域该动作由第15行的store_all_registers完成。这里因为t0=a0=mstratch所以通用寄存器最终是保存到了mstratch所指向的内存区域也就是g_itrframe中。第17-18行是保护进入中断处理前a0寄存器的值到g_itrframe。
可以看到mtrapvec汇编函数首先会将a0和mscratch交换而mscratch之前保护的是g_itrframe的地址g_itrframe的定义在kernel/machine/minit.c的第31行`riscv_regs g_itrframe;`也就是说g_itrframe是一个包含所有RISC-V通用寄存器的栈帧。接下来将t6赋值为a0的值第15并将所有通用寄存器保存到t6寄存器所指定首地址的内存区域该动作由第16行的store_all_registers完成。这里因为t0=a0=mstratch所以通用寄存器最终是保存到了mstratch所指向的内存区域也就是g_itrframe中。第18-19行是保护进入中断处理前a0寄存器的值到g_itrframe。
接下来mtrapvec汇编函数在第21--26行切换栈到stack0即PKE内核启动时用过的栈并在31行调用handle_mtrap()函数。handle_mtrap()函数在kernel/machine/mtrap.c文件中定义
接下来mtrapvec汇编函数在第23--28行切换栈到stack0即PKE内核启动时用过的栈并在34行调用handle_mtrap()函数。handle_mtrap()函数在kernel/machine/mtrap.c文件中定义
```c
20 void handle_mtrap() {
@ -1281,6 +1318,8 @@ lab1_2实验需要读者了解和掌握操作系统中异常exception
$ git commit -a -m "my work on lab1_2 is done."
```
<a name="irq"></a>
## 3.4 lab1_3 (外部)中断
@ -1411,154 +1450,163 @@ System is shutting down with exit code 0.
- 在m_start函数也就是机器模式的初始化函数中新增了timerinit()函数后者的函数定义在kernel/machine/minit.c文件
```c
72 void timerinit(uintptr_t hartid) {
73 // fire timer irq after TIMER_INTERVAL from now.
74 *(uint64*)CLINT_MTIMECMP(hartid) = *(uint64*)CLINT_MTIME + TIMER_INTERVAL;
75
76 // enable machine-mode timer irq in MIE (Machine Interrupt Enable) csr.
77 write_csr(mie, read_csr(mie) | MIE_MTIE);
78 }
82 void timerinit(uintptr_t hartid) {
83 // fire timer irq after TIMER_INTERVAL from now.
84 *(uint64*)CLINT_MTIMECMP(hartid) = *(uint64*)CLINT_MTIME + TIMER_INTERVAL;
85
86 // enable machine-mode timer irq in MIE (Machine Interrupt Enable) csr.
87 write_csr(mie, read_csr(mie) | MIE_MTIE);
88 }
```
该函数首先在74行设置了下一次timer触发的时间即当前时间的TIMER_INTERVAL即1000000周期后见kernel/config.h中的定义之后。另外77行设置了MIEMachine Interrupt Enable见本书的第一章的[1.3节](chapter1_riscv.md#machinestates)和[1.4节](chapter1_riscv.md#traps)寄存器中的MIE_MTIE位即允许我们的模拟RISC-V机器在M模式处理timer中断。
该函数首先在84行设置了下一次timer触发的时间即当前时间的TIMER_INTERVAL即1000000周期后见kernel/config.h中的定义之后。另外87行设置了MIEMachine Interrupt Enable见本书的第一章的[1.3节](chapter1_riscv.md#machinestates)和[1.4节](chapter1_riscv.md#traps)寄存器中的MIE_MTIE位即允许我们的模拟RISC-V机器在M模式处理timer中断。
时钟中断触发后kernel/machine/mtrap_vector.S文件中的mtrapvec函数将被调用
```assembly
8 mtrapvec:
9 # swap a0 and mscratch
10 # so that a0 points to interrupt frame
11 csrrw a0, mscratch, a0
12
13 # save the registers in interrupt frame
14 addi t6, a0, 0
15 store_all_registers
16 # save the user a0 in itrframe->a0
17 csrr t0, mscratch
18 sd t0, 72(a0)
19
20 # use stack0 for sp
21 la sp, stack0
22 li a3, 4096
23 csrr a4, mhartid
24 addi a4, a4, 1
25 mul a3, a3, a4
26 add sp, sp, a3
27
28 // save the address of interrupt frame in the csr "mscratch"
29 csrw mscratch, a0
30
31 call handle_mtrap
9 # mscratch -> g_itrframe (cf. kernel/machine/minit.c line 94)
10 # swap a0 and mscratch, so that a0 points to interrupt frame,
11 # i.e., [a0] = &g_itrframe
12 csrrw a0, mscratch, a0
13
14 # save the registers in g_itrframe
15 addi t6, a0, 0
16 store_all_registers
17 # save the original content of a0 in g_itrframe
18 csrr t0, mscratch
19 sd t0, 72(a0)
20
21 # switch stack (to use stack0) for the rest of machine mode
22 # trap handling.
23 la sp, stack0
24 li a3, 4096
25 csrr a4, mhartid
26 addi a4, a4, 1
27 mul a3, a3, a4
28 add sp, sp, a3
29
30 # pointing mscratch back to g_itrframe
31 csrw mscratch, a0
32
33 // restore all registers
34 csrr t6, mscratch
35 restore_all_registers
36
37 mret
33 # call machine mode trap handling function
34 call handle_mtrap
35
36 # restore all registers, come back to the status before entering
37 # machine mode handling.
38 csrr t6, mscratch
39 restore_all_registers
40
41 mret
```
和lab1_2一样最终将进入handle_mtrap函数继续处理。handle_mtrap函数将通过对mcause寄存器的值进行判断确认是时钟中断CAUSE_MTIMER将调用handle_timer()函数进行进一步处理:
```c
17 static void handle_timer() {
18 int cpuid = 0;
19 // setup the timer fired at next time (TIMER_INTERVAL from now)
20 *(uint64*)CLINT_MTIMECMP(cpuid) = *(uint64*)CLINT_MTIMECMP(cpuid) + TIMER_INTERVAL;
21
22 // setup a soft interrupt in sip (S-mode Interrupt Pending) to be handled in S-mode
23 write_csr(sip, SIP_SSIP);
24 }
25
26 //
27 // handle_mtrap calls cooresponding functions to handle an exception of a given type.
28 //
29 void handle_mtrap() {
30 uint64 mcause = read_csr(mcause);
31 switch (mcause) {
32 case CAUSE_MTIMER:
33 handle_timer();
34 break;
35 case CAUSE_FETCH_ACCESS:
36 handle_instruction_access_fault();
37 break;
38 case CAUSE_LOAD_ACCESS:
39 handle_load_access_fault();
40 case CAUSE_STORE_ACCESS:
41 handle_store_access_fault();
42 break;
43 case CAUSE_ILLEGAL_INSTRUCTION:
44 // TODO (lab1_2): call handle_illegal_instruction to implement illegal instruction
45 // interception, and finish lab1_2.
46 panic( "call handle_illegal_instruction to accomplish illegal instruction interception for lab1_2.\n" );
47
48 break;
49 case CAUSE_MISALIGNED_LOAD:
50 handle_misaligned_load();
51 break;
52 case CAUSE_MISALIGNED_STORE:
53 handle_misaligned_store();
54 break;
55
56 default:
57 sprint("machine trap(): unexpected mscause %p\n", mcause);
58 sprint(" mepc=%p mtval=%p\n", read_csr(mepc), read_csr(mtval));
59 panic( "unexpected exception happened in M-mode.\n" );
60 break;
61 }
62 }
18 static void handle_timer() {
19 int cpuid = 0;
20 // setup the timer fired at next time (TIMER_INTERVAL from now)
21 *(uint64*)CLINT_MTIMECMP(cpuid) = *(uint64*)CLINT_MTIMECMP(cpuid) + TIMER_INTERVAL;
22
23 // setup a soft interrupt in sip (S-mode Interrupt Pending) to be handled in S-mode
24 write_csr(sip, SIP_SSIP);
25 }
26
27 //
28 // handle_mtrap calls a handling function according to the type of a machine mode interrupt ( trap).
29 //
30 void handle_mtrap() {
31 uint64 mcause = read_csr(mcause);
32 switch (mcause) {
33 case CAUSE_MTIMER:
34 handle_timer();
35 break;
36 case CAUSE_FETCH_ACCESS:
37 handle_instruction_access_fault();
38 break;
39 case CAUSE_LOAD_ACCESS:
40 handle_load_access_fault();
41 case CAUSE_STORE_ACCESS:
42 handle_store_access_fault();
43 break;
44 case CAUSE_ILLEGAL_INSTRUCTION:
45 // TODO (lab1_2): call handle_illegal_instruction to implement illegal instruction
46 // interception, and finish lab1_2.
47 panic( "call handle_illegal_instruction to accomplish illegal instruction interception for lab1_2.\n" );
48
49 break;
50 case CAUSE_MISALIGNED_LOAD:
51 handle_misaligned_load();
52 break;
53 case CAUSE_MISALIGNED_STORE:
54 handle_misaligned_store();
55 break;
56
57 default:
58 sprint("machine trap(): unexpected mscause %p\n", mcause);
59 sprint(" mepc=%p mtval=%p\n", read_csr(mepc), read_csr(mtval));
60 panic( "unexpected exception happened in M-mode.\n" );
61 break;
62 }
63 }
```
而handle_timer()函数会在第20行先设置下一次timer再次触发的时间为当前时间+TIMER_INTERVAL并在23行对SIPSupervisor Interrupt Pending即S模式的中断等待寄存器寄存器进行设置将其中的SIP_SSIP位进行设置完成后返回。至此时钟中断在M态的处理就结束了剩下的动作交给S态继续处理。而handle_timer()在第23行的动作会导致PKE操作系统内核在S模式收到一个来自M态的时钟中断请求CAUSE_MTIMER_S_TRAP
而handle_timer()函数会在第21行先设置下一次timer再次触发的时间为当前时间+TIMER_INTERVAL并在24行对SIPSupervisor Interrupt Pending即S模式的中断等待寄存器寄存器进行设置将其中的SIP_SSIP位进行设置完成后返回。至此时钟中断在M态的处理就结束了剩下的动作交给S态继续处理。而handle_timer()在第23行的动作会导致PKE操作系统内核在S模式收到一个来自M态的时钟中断请求CAUSE_MTIMER_S_TRAP
那么为什么操作系统内核不在M态就完成对时钟中断的处理而一定要将它“接力”给S态呢这是因为对于一个操作系统来说timer事件对它的意义在于它是标记时间片的重要甚至是唯一手段而将CPU事件分成若干时间片的作用很大程度上是为了做进程的调度我们将在lab2_3中接触同时操作系统的功能大多数是在S态完成的。如果在M态处理时钟中断虽然说特权级上允许这样的操作但是处于M态的程序可能并不是非常清楚S态的操作系统的状态。如果贸然采取动作可能会破坏操作系统本身的设计。
接下来我们继续讨论时钟中断在S态的处理。我们直接来到S态的C处理函数即位于kernel/strap.c中的 smode_trap_handler函数
```c
45 void smode_trap_handler(void) {
46 // make sure we are in User mode before entering the trap handling.
47 // we will consider other previous case in lab1_3 (interrupt).
48 if ((read_csr(sstatus) & SSTATUS_SPP) != 0) panic("usertrap: not from user mode");
49
50 assert(current);
51 // save user process counter.
52 current->trapframe->epc = read_csr(sepc);
53
54 // if the cause of trap is syscall from user application
55 uint64 cause = read_csr(scause);
48 void smode_trap_handler(void) {
49 // make sure we are in User mode before entering the trap handling.
50 // we will consider other previous case in lab1_3 (interrupt).
51 if ((read_csr(sstatus) & SSTATUS_SPP) != 0) panic("usertrap: not from user mode");
52
53 assert(current);
54 // save user process counter.
55 current->trapframe->epc = read_csr(sepc);
56
57 if (cause == CAUSE_USER_ECALL) {
58 handle_syscall(current->trapframe);
59 } else if (cause == CAUSE_MTIMER_S_TRAP) { //soft trap generated by timer interrupt in M mode
60 handle_mtimer_trap();
61 } else {
62 sprint("smode_trap_handler(): unexpected scause %p\n", read_csr(scause));
63 sprint(" sepc=%p stval=%p\n", read_csr(sepc), read_csr(stval));
64 panic( "unexpected exception happened.\n" );
65 }
66
67 // continue the execution of current process.
68 switch_to(current);
69 }
57 // if the cause of trap is syscall from user application.
58 // read_csr() and CAUSE_USER_ECALL are macros defined in kernel/riscv.h
59 uint64 cause = read_csr(scause);
60
61 // we need to handle the timer trap @lab1_3.
62 if (cause == CAUSE_USER_ECALL) {
63 handle_syscall(current->trapframe);
64 } else if (cause == CAUSE_MTIMER_S_TRAP) { //soft trap generated by timer interrupt in M m ode
65 handle_mtimer_trap();
66 } else {
67 sprint("smode_trap_handler(): unexpected scause %p\n", read_csr(scause));
68 sprint(" sepc=%p stval=%p\n", read_csr(sepc), read_csr(stval));
69 panic( "unexpected exception happened.\n" );
70 }
71
72 // continue (come back to) the execution of current process.
73 switch_to(current);
74 }
```
我们看到该函数首先读取scause寄存器的内容如果内容等于CAUSE_MTIMER_S_TRAP的话说明是M态传递上来的时钟中断动作就调用handle_mtimer_trap()函数进行处理而handle_mtimer_trap()函数的定义为:
```c
31 static uint64 g_ticks = 0;
32 void handle_mtimer_trap() {
33 sprint("Ticks %d\n", g_ticks);
34 // TODO (lab1_3): increase g_ticks to record this "tick", and then clear the "SIP"
35 // field in sip register.
36 // hint: use write_csr to disable the SIP_SSIP bit in sip.
37 panic( "lab1_3: increase g_ticks by one, and clear SIP field in sip register.\n" );
38
39 }
32 //
33 // added @lab1_3
34 //
35 void handle_mtimer_trap() {
36 sprint("Ticks %d\n", g_ticks);
37 // TODO (lab1_3): increase g_ticks to record this "tick", and then clear the "SIP"
38 // field in sip register.
39 // hint: use write_csr to disable the SIP_SSIP bit in sip.
40 panic( "lab1_3: increase g_ticks by one, and clear SIP field in sip register.\n" );
41
42 }
```
至此,我们就知道为什么会在之前看到`lab1_3: increase g_ticks by one, and clear SIP field in sip register.`这样的输出了显然这是因为handle_mtimer_trap()并未完成。
那么handle_mtimer_trap()需要完成哪些“后续动作”呢首先我们看到在该函数上面定义了一个全局变量g_ticks用它来对时钟中断的次数进行计数而第33行会输出该计数。为了确保我们的系统持续正常运行该计数应每次都会完成加一操作。所以handle_mtimer_trap()首先需要对g_ticks进行加一其次由于处理完中断后SIPSupervisor Interrupt Pending即S模式的中断等待寄存器寄存器中的SIP_SSIP位仍然为1由M态的中断处理函数设置如果该位持续为1的话会导致我们的模拟RISC-V机器始终处于中断状态。所以handle_mtimer_trap()还需要对SIP的SIP_SSIP位清零以保证下次再发生时钟中断时M态的函数将该位置一会导致S模式的下一次中断。
那么handle_mtimer_trap()需要完成哪些“后续动作”呢首先我们看到在该函数上面定义了一个全局变量g_ticks用它来对时钟中断的次数进行计数而第36行会输出该计数。为了确保我们的系统持续正常运行该计数应每次都会完成加一操作。所以handle_mtimer_trap()首先需要对g_ticks进行加一其次由于处理完中断后SIPSupervisor Interrupt Pending即S模式的中断等待寄存器寄存器中的SIP_SSIP位仍然为1由M态的中断处理函数设置如果该位持续为1的话会导致我们的模拟RISC-V机器始终处于中断状态。所以handle_mtimer_trap()还需要对SIP的SIP_SSIP位清零以保证下次再发生时钟中断时M态的函数将该位置一会导致S模式的下一次中断。
**实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab1_3中所做的工作**
@ -1566,6 +1614,8 @@ System is shutting down with exit code 0.
$ git commit -a -m "my work on lab1_3 is done."
```
<a name="lab1_challenge1_backtrace"></a>
## 3.5 lab1_challenge1 打印用户程序调用栈(难度:&#9733;&#9733;&#9733;&#9734;&#9734;
@ -1717,8 +1767,8 @@ $ riscv64-unknown-elf-objdump -d obj/app_print_backtrace
在`elf_init`函数中完成了`elf header`的加载:
```C
42 // load the elf header
43 if (elf_fpread(ctx, &ctx->ehdr, sizeof(ctx->ehdr), 0) != sizeof(ctx->ehdr)) return EL_EIO;
41 // load the elf header
42 if (elf_fpread(ctx, &ctx->ehdr, sizeof(ctx->ehdr), 0) != sizeof(ctx->ehdr)) return EL_EIO;
```
Section Header Table可以认为是一个线性结构也就是说对于每一个Section Header地址的寻找是线性的换句话说每个Section Header都是都是紧密相连的。

@ -20,7 +20,6 @@
- [给定应用](#lab2_3_app)
- [实验内容](#lab2_3_content)
- [实验指导](#lab2_3_guide)
- [4.5 lab2_challenge1 复杂缺页异常(难度:&#9733;&#9734;&#9734;&#9734;&#9734;](#lab2_challenge1_pagefault)
- [给定应用](#lab2_challenge1_app)
- [实验内容](#lab2_challenge1_content)
@ -106,45 +105,45 @@ spike将创建一个模拟的RISC-V机器该机器拥有一个支持RV64G指
这样操作系统内核的逻辑地址和物理地址就有了一一对应的关系这也是我们在lab1中采用直模式Bare mode虚拟地址翻译机制也不会出错的原因。这里需要解释的是对内核的机器模式栈的处理。通过实验一我们知道机器模式栈是一个4KB的空间它位于内核数据段而不是专门分配一个额外的页面。这样简单处理的原因是PKE上运行的应用往往只有一个算是非常简单的多任务环境且操作系统利用机器模式栈的时机只有特殊的异常如lab1_2中的非法指令异常以及一些外部中断如lab1_3中的时钟中断
如图4.3b所示在spike将操作系统内核装入物理内存后剩余的内存空间应该是从内核数据段的结束_end符号到0xffffffff即4GB-1的地址。但是由于PKE操作系统内核的特殊性它只需要支持给定应用的运行lab2的代码将操作系统管理的空间进一步缩减定义了一个操作系统需要管理的最大内存空间kernel/config.h文件从而提升实验代码的执行速度
如图4.3(b)所示在spike将操作系统内核装入物理内存后剩余的内存空间应该是从内核数据段的结束_end符号到0xffffffff即4GB-1的地址。但是由于PKE操作系统内核的特殊性它只需要支持给定应用的运行lab2的代码将操作系统管理的空间进一步缩减定义了一个操作系统需要管理的最大内存空间kernel/config.h文件从而提升实验代码的执行速度
```c
10 // the maximum memory space that PKE is allowed to manage
10 // the maximum memory space that PKE is allowed to manage. added @lab2_1
11 #define PKE_MAX_ALLOWABLE_RAM 128 * 1024 * 1024
12
13 // the ending physical address that PKE observes
13 // the ending physical address that PKE observes. added @lab2_1
14 #define PHYS_TOP (DRAM_BASE + PKE_MAX_ALLOWABLE_RAM)
```
可以看到实验代码“人为”地将PKE操作系统所能管理的内存空间限制到了128MB即PKE_MAX_ALLOWABLE_RAM的定义同时定义了PHYS_TOP为新的内存物理地址上限。实际上kernel/pmm.c文件所定义的pmm_init()函数包含了PKE对物理内存进行管理的逻辑
```c
62 void pmm_init() {
63 // start of kernel program segment
64 uint64 g_kernel_start = KERN_BASE;
65 uint64 g_kernel_end = (uint64)&_end;
66
67 uint64 pke_kernel_size = g_kernel_end - g_kernel_start;
68 sprint("PKE kernel start 0x%lx, PKE kernel end: 0x%lx, PKE kernel size: 0x%lx .\n",
69 g_kernel_start, g_kernel_end, pke_kernel_size);
70
71 // free memory starts from the end of PKE kernel and must be page-aligined
72 free_mem_start_addr = ROUNDUP(g_kernel_end , PGSIZE);
73
74 // recompute g_mem_size to limit the physical memory space that PKE kernel
75 // needs to manage
76 g_mem_size = MIN(PKE_MAX_ALLOWABLE_RAM, g_mem_size);
77 if( g_mem_size < pke_kernel_size )
78 panic( "Error when recomputing physical memory size (g_mem_size).\n" );
79
80 free_mem_end_addr = g_mem_size + DRAM_BASE;
81 sprint("free physical memory address: [0x%lx, 0x%lx] \n", free_mem_start_addr,
82 free_mem_end_addr - 1);
83
84 sprint("kernel memory manager is initializing ...\n");
85 // create the list of free pages
86 create_freepage_list(free_mem_start_addr, free_mem_end_addr);
87 }
63 void pmm_init() {
64 // start of kernel program segment
65 uint64 g_kernel_start = KERN_BASE;
66 uint64 g_kernel_end = (uint64)&_end;
67
68 uint64 pke_kernel_size = g_kernel_end - g_kernel_start;
69 sprint("PKE kernel start 0x%lx, PKE kernel end: 0x%lx, PKE kernel size: 0x%lx .\n",
70 g_kernel_start, g_kernel_end, pke_kernel_size);
71
72 // free memory starts from the end of PKE kernel and must be page-aligined
73 free_mem_start_addr = ROUNDUP(g_kernel_end , PGSIZE);
74
75 // recompute g_mem_size to limit the physical memory space that our riscv-pke kernel
76 // needs to manage
77 g_mem_size = MIN(PKE_MAX_ALLOWABLE_RAM, g_mem_size);
78 if( g_mem_size < pke_kernel_size )
79 panic( "Error when recomputing physical memory size (g_mem_size).\n" );
80
81 free_mem_end_addr = g_mem_size + DRAM_BASE;
82 sprint("free physical memory address: [0x%lx, 0x%lx] \n", free_mem_start_addr,
83 free_mem_end_addr - 1);
84
85 sprint("kernel memory manager is initializing ...\n");
86 // create the list of free pages
87 create_freepage_list(free_mem_start_addr, free_mem_end_addr);
88 }
```
在76行pmm_init()函数会计算g_mem_size其值在PKE_MAX_ALLOWABLE_RAM和spike所模拟的物理内存大小中取最小值也就是说除非spike命令行参数中-m参数后面所带的数字小于128即128Mg_mem_size的大小将为128MB。
@ -168,33 +167,35 @@ spike将创建一个模拟的RISC-V机器该机器拥有一个支持RV64G指
操作系统内核建立页表的过程可以参考kernel/vmm.c文件中的kern_vm_init()函数的实现需要说明的是kern_vm_init()函数在PKE操作系统内核的S态初始化过程s_start函数中被调用
```c
119 void kern_vm_init(void) {
120 pagetable_t t_page_dir;
121
122 // allocate a page (t_page_dir) to be the page directory for kernel
123 t_page_dir = (pagetable_t)alloc_page();
124 memset(t_page_dir, 0, PGSIZE);
125
126 // map virtual address [KERN_BASE, _etext] to physical address [DRAM_BASE, DRAM_BASE+(_etext - KERN_BASE)],
127 // to maintain (direct) text section kernel address mapping.
128 kern_vm_map(t_page_dir, KERN_BASE, DRAM_BASE, (uint64)_etext - KERN_BASE,
129 prot_to_type(PROT_READ | PROT_EXEC, 0));
130
131 sprint("KERN_BASE 0x%lx\n", lookup_pa(t_page_dir, KERN_BASE));
132
133 // also (direct) map remaining address space, to make them accessable from kernel.
134 // this is important when kernel needs to access the memory content of user's app
135 // without copying pages between kernel and user spaces.
136 kern_vm_map(t_page_dir, (uint64)_etext, (uint64)_etext, PHYS_TOP - (uint64)_etext,
137 prot_to_type(PROT_READ | PROT_WRITE, 0));
138
139 sprint("physical address of _etext is: 0x%lx\n", lookup_pa(t_page_dir, (uint64)_etext));
140
141 g_kernel_pagetable = t_page_dir;
142 }
120 void kern_vm_init(void) {
121 // pagetable_t is defined in kernel/riscv.h. it's actually uint64*
122 pagetable_t t_page_dir;
123
124 // allocate a page (t_page_dir) to be the page directory for kernel. alloc_page is defined in kernel/pmm.c
125 t_page_dir = (pagetable_t)alloc_page();
126 // memset is defined in util/string.c
127 memset(t_page_dir, 0, PGSIZE);
128
129 // map virtual address [KERN_BASE, _etext] to physical address [DRAM_BASE, DRAM_BASE+(_etext - KERN_BASE)],
130 // to maintain (direct) text section kernel address mapping.
131 kern_vm_map(t_page_dir, KERN_BASE, DRAM_BASE, (uint64)_etext - KERN_BASE,
132 prot_to_type(PROT_READ | PROT_EXEC, 0));
133
134 sprint("KERN_BASE 0x%lx\n", lookup_pa(t_page_dir, KERN_BASE));
135
136 // also (direct) map remaining address space, to make them accessable from kernel.
137 // this is important when kernel needs to access the memory content of user's app
138 // without copying pages between kernel and user spaces.
139 kern_vm_map(t_page_dir, (uint64)_etext, (uint64)_etext, PHYS_TOP - (uint64)_etext,
140 prot_to_type(PROT_READ | PROT_WRITE, 0));
141
142 sprint("physical address of _etext is: 0x%lx\n", lookup_pa(t_page_dir, (uint64)_etext));
143
144 g_kernel_pagetable = t_page_dir;
145 }
```
我们看到kern_vm_init()函数会首先123从空闲物理内存中获取分配一个t_page_dir指针所指向的物理页该页将作为内核页表的根目录page directory对应图4.1中的VPN[2]。接下来将该页的内容清零124行、映射代码段到它对应的物理地址128--129行、映射数据段的起始到PHYS_TOP到它对应的物理地址空间136--137行最后记录内核页表的根目录页141行)。
我们看到kern_vm_init()函数会首先125从空闲物理内存中获取分配一个t_page_dir指针所指向的物理页该页将作为内核页表的根目录page directory对应图4.1中的VPN[2]。接下来将该页的内容清零127行、映射代码段到它对应的物理地址131--132行、映射数据段的起始到PHYS_TOP到它对应的物理地址空间139--140行最后记录内核页表的根目录页144行)。
#### 应用进程
@ -232,50 +233,56 @@ Program Headers:
PKE实验二中的应用加载是通过kernel/kernel.c文件中的load_user_program函数来完成的
```c
37 void load_user_program(process *proc) {
38 sprint("User application is loading.\n");
39 proc->trapframe = (trapframe *)alloc_page(); //trapframe
40 memset(proc->trapframe, 0, sizeof(trapframe));
41
42 //user pagetable
43 proc->pagetable = (pagetable_t)alloc_page();
44 memset((void *)proc->pagetable, 0, PGSIZE);
45
46 proc->kstack = (uint64)alloc_page() + PGSIZE; //user kernel stack top
47 uint64 user_stack = (uint64)alloc_page(); //phisical address of user stack bottom
48 proc->trapframe->regs.sp = USER_STACK_TOP; //virtual address of user stack top
49
50 sprint("user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n", proc->trapframe,
51 proc->trapframe->regs.sp, proc->kstack);
52
53 load_bincode_from_host_elf(proc);
38 void load_user_program(process *proc) {
39 sprint("User application is loading.\n");
40 // allocate a page to store the trapframe. alloc_page is defined in kernel/pmm.c. added @la b2_1
41 proc->trapframe = (trapframe *)alloc_page();
42 memset(proc->trapframe, 0, sizeof(trapframe));
43
44 // allocate a page to store page directory. added @lab2_1
45 proc->pagetable = (pagetable_t)alloc_page();
46 memset((void *)proc->pagetable, 0, PGSIZE);
47
48 // allocate pages to both user-kernel stack and user app itself. added @lab2_1
49 proc->kstack = (uint64)alloc_page() + PGSIZE; //user kernel stack top
50 uint64 user_stack = (uint64)alloc_page(); //phisical address of user stack bottom
51
52 // USER_STACK_TOP = 0x7ffff000, defined in kernel/memlayout.h
53 proc->trapframe->regs.sp = USER_STACK_TOP; //virtual address of user stack top
54
55 // map user stack in userspace
56 user_vm_map((pagetable_t)proc->pagetable, USER_STACK_TOP - PGSIZE, PGSIZE, user_stack,
57 prot_to_type(PROT_WRITE | PROT_READ, 1));
58
59 // map trapframe in user space (direct mapping as in kernel space).
60 user_vm_map((pagetable_t)proc->pagetable, (uint64)proc->trapframe, PGSIZE, (uint64)proc->trapframe,
61 prot_to_type(PROT_WRITE | PROT_READ, 0));
62
63 // map S-mode trap vector section in user space (direct mapping as in kernel space)
64 // we assume that the size of usertrap.S is smaller than a page.
65 user_vm_map((pagetable_t)proc->pagetable, (uint64)trap_sec_start, PGSIZE, (uint64)trap_sec_start,
66 prot_to_type(PROT_READ | PROT_EXEC, 0));
67 }
55 sprint("user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n", proc->trapframe,
56 proc->trapframe->regs.sp, proc->kstack);
57
58 // load_bincode_from_host_elf() is defined in kernel/elf.c
59 load_bincode_from_host_elf(proc);
60
61 // populate the page table of user application. added @lab2_1
62 // map user stack in userspace, user_vm_map is defined in kernel/vmm.c
63 user_vm_map((pagetable_t)proc->pagetable, USER_STACK_TOP - PGSIZE, PGSIZE, user_stack,
64 prot_to_type(PROT_WRITE | PROT_READ, 1));
65
66 // map trapframe in user space (direct mapping as in kernel space).
67 user_vm_map((pagetable_t)proc->pagetable, (uint64)proc->trapframe, PGSIZE, (uint64)proc->trapframe,
68 prot_to_type(PROT_WRITE | PROT_READ, 0));
69
70 // map S-mode trap vector section in user space (direct mapping as in kernel space)
71 // here, we assume that the size of usertrap.S is smaller than a page.
72 user_vm_map((pagetable_t)proc->pagetable, (uint64)trap_sec_start, PGSIZE, (uint64)trap_sec_start,
73 prot_to_type(PROT_READ | PROT_EXEC, 0));
74 }
```
load_user_program()函数对于应用进程逻辑空间的操作可以分成以下几个部分:
- 39--40行分配一个物理页面将其作为栈帧trapframe即发生中断时保存用户进程执行上下文的内存空间。由于物理页面都是从位于物理地址范围[_endPHYS_TOP]的空间中分配的它的首地址也将位于该区间。所以第60--61行的映射也是做一个proc->trapframe到所分配页面的直映射逻辑地址=物理地址)。
- 41--42分配一个物理页面将其作为栈帧trapframe即发生中断时保存用户进程执行上下文的内存空间。由于物理页面都是从位于物理地址范围[_endPHYS_TOP]的空间中分配的它的首地址也将位于该区间。所以第67--68行的映射也是做一个proc->trapframe到所分配页面的直映射逻辑地址=物理地址)。
- 43--44行分配一个物理页面作为存放进程页表根目录page directory对应图4.1中的VPN[2])的空间。
- 45--46分配一个物理页面作为存放进程页表根目录page directory对应图4.1中的VPN[2])的空间。
- 46分配了一个物理页面作为用户进程的内核态栈该栈将在用户进程进入中断处理时用作S模式内核处理函数使用的栈。然而这个栈并未映射到用户进程的逻辑地址空间而是将其首地址保存在proc->kstack中。
- 49分配了一个物理页面作为用户进程的内核态栈该栈将在用户进程进入中断处理时用作S模式内核处理函数使用的栈。然而这个栈并未映射到用户进程的逻辑地址空间而是将其首地址保存在proc->kstack中。
- 47--48行)再次分配一个物理页面,作为用户进程的用户态栈,该栈供应用在用户模式下使用,并在第56--57行映射到用户进程的逻辑地址USER_STACK_TOP。
- 53调用load_bincode_from_host_elf()函数该函数将读取应用所对应的ELF文件并将其中的代码段读取到新分配的内存空间物理地址位于[_endPHYS_TOP]区间)。
- 65--66将内核中的S态trap入口函数所在的物理页一一映射到用户进程的逻辑地址空间。
- 50--53行)再次分配一个物理页面,作为用户进程的用户态栈,该栈供应用在用户模式下使用,并在第63--64行映射到用户进程的逻辑地址USER_STACK_TOP。
- 59调用load_bincode_from_host_elf()函数该函数将读取应用所对应的ELF文件并将其中的代码段读取到新分配的内存空间物理地址位于[_endPHYS_TOP]区间)。
- 72--73将内核中的S态trap入口函数所在的物理页一一映射到用户进程的逻辑地址空间。
通过以上load_user_program()函数,我们可以大致画出用户进程的逻辑地址空间,以及该地址空间到物理地址空间的映射。
@ -559,28 +566,28 @@ System is shutting down with exit code 0.
一般来说应用程序执行过程中的动态内存分配和回收是操作系统中的堆Heap管理的内容。在本实验中我们实际上是为PKE操作系统内核实现一个简单到不能再简单的“堆”。为实现naive_free()的内存回收过程我们需要了解其对偶过程即内存是如何“分配”给应用程序并供后者使用的。为此我们先阅读kernel/syscall.c文件中的naive_malloc()函数的底层实现sys_user_allocate_page()
```c
43 uint64 sys_user_allocate_page() {
44 void* pa = alloc_page();
45 uint64 va = g_ufree_page;
46 g_ufree_page += PGSIZE;
47 user_vm_map((pagetable_t)current->pagetable, va, PGSIZE, (uint64)pa,
48 prot_to_type(PROT_WRITE | PROT_READ, 1));
49
50 return va;
51 }
42 uint64 sys_user_allocate_page() {
43 void* pa = alloc_page();
44 uint64 va = g_ufree_page;
45 g_ufree_page += PGSIZE;
46 user_vm_map((pagetable_t)current->pagetable, va, PGSIZE, (uint64)pa,
47 prot_to_type(PROT_WRITE | PROT_READ, 1));
48
49 return va;
50 }
```
这个函数在44行分配了一个首地址为pa的物理页面这个物理页面要以何种方式映射给应用进程使用呢第45行给出了pa对应的逻辑地址va = g_ufree_page并在46行对g_ufree_page进行了递增操作。最后在47--48将pa映射给了va地址。这个过程中g_ufree_page是如何定义的呢我们可以找到它在kernel/process.c文件中的定义
这个函数在43行分配了一个首地址为pa的物理页面这个物理页面要以何种方式映射给应用进程使用呢第44行给出了pa对应的逻辑地址va = g_ufree_page并在45行对g_ufree_page进行了递增操作。最后在46--47将pa映射给了va地址。这个过程中g_ufree_page是如何定义的呢我们可以找到它在kernel/process.c文件中的定义
```c
27 // start virtual address of our simple heap.
27 // points to the first free page in our simple heap. added @lab2_2
28 uint64 g_ufree_page = USER_FREE_ADDRESS_START;
```
而USER_FREE_ADDRESS_START的定义在kernel/memlayout.h文件
```c
17 // simple heap bottom, virtual address starts from 4MB
17 // start virtual address (4MB) of our simple heap. added @lab2_2
18 #define USER_FREE_ADDRESS_START 0x00000000 + PGSIZE * 1024
```
@ -732,44 +739,50 @@ System is shutting down with exit code 0.
另外lab1_2中处理的非法指令异常是在M模式下处理的原因是我们根本没有将该异常代理给S模式。但是对于本实验中的缺页异常是不是也是需要在M模式处理呢我们先回顾以下kernel/machine/minit.c文件中的delegate_traps()函数:
```c
51 static void delegate_traps() {
52 if (!supports_extension('S')) {
53 // confirm that our processor supports supervisor mode. abort if not.
54 sprint("s mode is not supported.\n");
55 return;
56 }
57
58 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
59 uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) |
60 (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) |
61 (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_USER_ECALL);
55 static void delegate_traps() {
56 // supports_extension macro is defined in kernel/riscv.h
57 if (!supports_extension('S')) {
58 // confirm that our processor supports supervisor mode. abort if it does not.
59 sprint("S mode is not supported.\n");
60 return;
61 }
62
63 write_csr(mideleg, interrupts);
64 write_csr(medeleg, exceptions);
65 assert(read_csr(mideleg) == interrupts);
66 assert(read_csr(medeleg) == exceptions);
67 }
63 // macros used in following two statements are defined in kernel/riscv.h
64 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
65 uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) |
66 (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) |
67 (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_USER_ECALL);
68
69 // writes 64-bit values (interrupts and exceptions) to 'mideleg' and 'medeleg' (two
70 // priviledged registers of RV64G machine) respectively.
71 //
72 // write_csr and read_csr are macros defined in kernel/riscv.h
73 write_csr(mideleg, interrupts);
74 write_csr(medeleg, exceptions);
75 assert(read_csr(mideleg) == interrupts);
76 assert(read_csr(medeleg) == exceptions);
77 }
```
而在本实验的应用中产生缺页异常的本质还是应用往未被映射的内存空间“写”以及后续的访问所导致的所以CAUSE_STORE_PAGE_FAULT是我们应该关注的异常。通过阅读delegate_traps()函数我们看到该函数显然已将缺页异常CAUSE_STORE_PAGE_FAULT代理给了S模式所以接下来我们就应阅读kernel/strap.c文件中对于这类异常的处理
```c
49 void handle_user_page_fault(uint64 mcause, uint64 sepc, uint64 stval) {
50 sprint("handle_page_fault: %lx\n", stval);
51 switch (mcause) {
52 case CAUSE_STORE_PAGE_FAULT:
53 // TODO (lab2_3): implement the operations that solve the page fault to
54 // dynamically increase application stack.
55 // hint: first allocate a new physical page, and then, maps the new page to the
56 // virtual address that causes the page fault.
57 panic( "You need to implement the operations that actually handle the page fault in lab2_3.\n" );
58
59 break;
60 default:
61 sprint("unknown page fault.\n");
52 void handle_user_page_fault(uint64 mcause, uint64 sepc, uint64 stval) {
53 sprint("handle_page_fault: %lx\n", stval);
54 switch (mcause) {
55 case CAUSE_STORE_PAGE_FAULT:
56 // TODO (lab2_3): implement the operations that solve the page fault to
57 // dynamically increase application stack.
58 // hint: first allocate a new physical page, and then, maps the new page to the
59 // virtual address that causes the page fault.
60 panic( "You need to implement the operations that actually handle the page fault in lab 2_3.\n" );
61
62 break;
63 }
64 }
63 default:
64 sprint("unknown page fault.\n");
65 break;
66 }
67 }
```
这里,我们找到了之前运行./obj/app_sum_sequence出错的地方我们只需要改正这一错误实现缺页处理使得程序获得正确的输出就好。实现缺页处理的思路如下
@ -912,30 +925,48 @@ $ git merge lab2_3_pagefault -m "continue to work on lab2_challenge1"
- user/app_singlepageheap.c
```c
1 /*
2 * Below is the given application for lab2_challenge2_singlepageheap.
3 * This app performs malloc memory.
4 */
5
6 #include "user_lib.h"
7 #include "util/types.h"
8 #include "util/string.h"
9 int main(void) {
10
11 char str[20] = "hello world.";
12 char *m = (char *)better_malloc(100);
13 char *p = (char *)better_malloc(50);
14 if((uint64)p - (uint64)m > 512 ){
15 printu("you need to manage the vm space precisely!");
16 exit(-1);
17 }
18 better_free((void *)m);
19
20 strcpy(p,str);
21 printu("%s\n",p);
22 exit(0);
23 return 0;
24 }
1 /*
2 * Below is the given application for lab2_challenge2_singlepageheap.
3 * This app performs malloc memory.
4 */
5
6 #include "user_lib.h"
7 //#include "util/string.h"
8
9 typedef unsigned long long uint64;
10
11 char* strcpy(char* dest, const char* src) {
12 char* d = dest;
13 while ((*d++ = *src++))
14 ;
15 return dest;
16 }
17 int main(void) {
18
19 char str[20] = "hello, world!!!";
20 char *m = (char *)better_malloc(100);
21 char *p = (char *)better_malloc(50);
22 if((uint64)p - (uint64)m > 512 ){
23 printu("you need to manage the vm space precisely!");
24 exit(-1);
25 }
26 better_free((void *)m);
27
28 strcpy(p,str);
29 printu("%s\n",p);
30 char *n = (char *)better_malloc(50);
31
32 if(m != n)
33 {
34 printu("your malloc is not complete.\n");
35 exit(-1);
36 }
37 // else{
38 // printu("0x%lx 0x%lx\n", m, n);
39 // }
40 exit(0);
41 return 0;
42 }
```
以上程序先利用better_malloc分别申请100和50个字节的一个物理页的内存然后使用better_free释放掉100个字节向50个字节中复制一串字符串进行输出。原本的pke中malloc的实现是非常简化的一次直接分配一个页面你的挑战任务是**修改内核(包括machine文件夹下)的代码使得应用程序的malloc能够在一个物理页中分配并对各申请块进行合理的管理**,如上面的应用预期输出如下:
@ -954,9 +985,9 @@ kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: obj/app_singlepageheap
Application program entry point (virtual address): 0x00000000000100b0
Application program entry point (virtual address): 0x000000000001008a
Switch to user mode...
hello world.
hello, world!!!
User exit with code:0.
System is shutting down with exit code 0.
```

@ -40,8 +40,8 @@
实验3跟之前的两个实验最大的不同在于在实验3的3个基本实验中PKE操作系统将需要支持多个进程的执行。为了对多任务环境进行支撑PKE操作系统定义了一个“进程池”见kernel/process.c文件
```C
34 // process pool
35 process procs[NPROC];
29 // process pool. added @lab3_1
30 process procs[NPROC];
```
实际上这个进程池就是一个包含NPROC=32见kernel/process.h文件个process结构的数组。
@ -49,7 +49,7 @@
接下来PKE操作系统对进程的结构进行了扩充见kernel/process.h文件
```C
58 // points to a page that contains mapped_regions
58 // points to a page that contains mapped_regions. below are added @lab3_1
59 mapped_region *mapped_info;
60 // next free mapped region in mapped_info
61 int total_mapped_region;
@ -59,9 +59,9 @@
65 // process status
66 int status;
67 // parent process
68 struct process *parent;
68 struct process_t *parent;
69 // next queue element
70 struct process *queue_next;
70 struct process_t *queue_next;
```
- 前两项mapped_info和total_mapped_region用于对进程的虚拟地址空间中的代码段、堆栈段等进行跟踪这些虚拟地址空间在进程创建fork将发挥重要作用。同时这也是lab3_1的内容。PKE将进程可能拥有的段分为以下几个类型
@ -104,63 +104,63 @@
PKE实验中创建一个进程需要先调用kernel/process.c文件中的alloc_process()函数:
```C
89 process* alloc_process() {
90 // locate the first usable process structure
91 int i;
92
93 for( i=0; i<NPROC; i++ )
94 if( procs[i].status == FREE ) break;
92 process* alloc_process() {
93 // locate the first usable process structure
94 int i;
95
96 if( i>=NPROC ){
97 panic( "cannot find any free process structure.\n" );
98 return 0;
99 }
100
101 // init proc[i]'s vm space
102 procs[i].trapframe = (trapframe *)alloc_page(); //trapframe, used to save context
103 memset(procs[i].trapframe, 0, sizeof(trapframe));
104
105 // page directory
106 procs[i].pagetable = (pagetable_t)alloc_page();
107 memset((void *)procs[i].pagetable, 0, PGSIZE);
108
109 procs[i].kstack = (uint64)alloc_page() + PGSIZE; //user kernel stack top
110 uint64 user_stack = (uint64)alloc_page(); //phisical address of user stack bottom
111 procs[i].trapframe->regs.sp = USER_STACK_TOP; //virtual address of user stack top
112
113 // allocates a page to record memory regions (segments)
114 procs[i].mapped_info = (mapped_region*)alloc_page();
115 memset( procs[i].mapped_info, 0, PGSIZE );
116
117 // map user stack in userspace
118 user_vm_map((pagetable_t)procs[i].pagetable, USER_STACK_TOP - PGSIZE, PGSIZE,
119 user_stack, prot_to_type(PROT_WRITE | PROT_READ, 1));
120 procs[i].mapped_info[0].va = USER_STACK_TOP - PGSIZE;
121 procs[i].mapped_info[0].npages = 1;
122 procs[i].mapped_info[0].seg_type = STACK_SEGMENT;
123
124 // map trapframe in user space (direct mapping as in kernel space).
125 user_vm_map((pagetable_t)procs[i].pagetable, (uint64)procs[i].trapframe, PGSIZE,
126 (uint64)procs[i].trapframe, prot_to_type(PROT_WRITE | PROT_READ, 0));
127 procs[i].mapped_info[1].va = (uint64)procs[i].trapframe;
128 procs[i].mapped_info[1].npages = 1;
129 procs[i].mapped_info[1].seg_type = CONTEXT_SEGMENT;
130
131 // map S-mode trap vector section in user space (direct mapping as in kernel space)
132 // we assume that the size of usertrap.S is smaller than a page.
133 user_vm_map((pagetable_t)procs[i].pagetable, (uint64)trap_sec_start, PGSIZE,
134 (uint64)trap_sec_start, prot_to_type(PROT_READ | PROT_EXEC, 0));
135 procs[i].mapped_info[2].va = (uint64)trap_sec_start;
136 procs[i].mapped_info[2].npages = 1;
137 procs[i].mapped_info[2].seg_type = SYSTEM_SEGMENT;
138
139 sprint("in alloc_proc. user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n",
140 procs[i].trapframe, procs[i].trapframe->regs.sp, procs[i].kstack);
96 for( i=0; i<NPROC; i++ )
97 if( procs[i].status == FREE ) break;
98
99 if( i>=NPROC ){
100 panic( "cannot find any free process structure.\n" );
101 return 0;
102 }
103
104 // init proc[i]'s vm space
105 procs[i].trapframe = (trapframe *)alloc_page(); //trapframe, used to save context
106 memset(procs[i].trapframe, 0, sizeof(trapframe));
107
108 // page directory
109 procs[i].pagetable = (pagetable_t)alloc_page();
110 memset((void *)procs[i].pagetable, 0, PGSIZE);
111
112 procs[i].kstack = (uint64)alloc_page() + PGSIZE; //user kernel stack top
113 uint64 user_stack = (uint64)alloc_page(); //phisical address of user stack bottom
114 procs[i].trapframe->regs.sp = USER_STACK_TOP; //virtual address of user stack top
115
116 // allocates a page to record memory regions (segments)
117 procs[i].mapped_info = (mapped_region*)alloc_page();
118 memset( procs[i].mapped_info, 0, PGSIZE );
119
120 // map user stack in userspace
121 user_vm_map((pagetable_t)procs[i].pagetable, USER_STACK_TOP - PGSIZE, PGSIZE,
122 user_stack, prot_to_type(PROT_WRITE | PROT_READ, 1));
123 procs[i].mapped_info[0].va = USER_STACK_TOP - PGSIZE;
124 procs[i].mapped_info[0].npages = 1;
125 procs[i].mapped_info[0].seg_type = STACK_SEGMENT;
126
127 // map trapframe in user space (direct mapping as in kernel space).
128 user_vm_map((pagetable_t)procs[i].pagetable, (uint64)procs[i].trapframe, PGSIZE,
129 (uint64)procs[i].trapframe, prot_to_type(PROT_WRITE | PROT_READ, 0));
130 procs[i].mapped_info[1].va = (uint64)procs[i].trapframe;
131 procs[i].mapped_info[1].npages = 1;
132 procs[i].mapped_info[1].seg_type = CONTEXT_SEGMENT;
133
134 // map S-mode trap vector section in user space (direct mapping as in kernel space)
135 // we assume that the size of usertrap.S is smaller than a page.
136 user_vm_map((pagetable_t)procs[i].pagetable, (uint64)trap_sec_start, PGSIZE,
137 (uint64)trap_sec_start, prot_to_type(PROT_READ | PROT_EXEC, 0));
138 procs[i].mapped_info[2].va = (uint64)trap_sec_start;
139 procs[i].mapped_info[2].npages = 1;
140 procs[i].mapped_info[2].seg_type = SYSTEM_SEGMENT;
141
142 procs[i].total_mapped_region = 3;
143 // return after initialization.
144 return &procs[i];
145 }
142 sprint("in alloc_proc. user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n",
143 procs[i].trapframe, procs[i].trapframe->regs.sp, procs[i].kstack);
144
145 procs[i].total_mapped_region = 3;
146 // return after initialization.
147 return &procs[i];
148 }
```
通过以上代码可以发现alloc_process()函数除了找到一个空的进程结构外还为新创建的进程建立了KERN_BASE以上逻辑地址的映射这段代码在实验3之前位于kernel/kernel.c文件的load_user_program()函数中),并将映射信息保存到了进程结构中。
@ -168,83 +168,91 @@ PKE实验中创建一个进程需要先调用kernel/process.c文件中的allo
对于给定应用PKE将通过调用load_bincode_from_host_elf()函数载入给定应用对应的ELF文件的各个段。之后被调用的elf_load()函数在载入段后,将对被载入的段进行判断,以记录它们的虚地址映射:
```c
62 elf_status elf_load(elf_ctx *ctx) {
63 elf_prog_header ph_addr;
64 int i, off;
65 // traverse the elf program segment headers
66 for (i = 0, off = ctx->ehdr.phoff; i < ctx->ehdr.phnum; i++, off += sizeof(ph_addr)) {
67 // read segment headers
68 if (elf_fpread(ctx, (void *)&ph_addr, sizeof(ph_addr), off) != sizeof(ph_addr)) return EL_EIO;
65 elf_status elf_load(elf_ctx *ctx) {
66 // elf_prog_header structure is defined in kernel/elf.h
67 elf_prog_header ph_addr;
68 int i, off;
69
70 if (ph_addr.type != ELF_PROG_LOAD) continue;
71 if (ph_addr.memsz < ph_addr.filesz) return EL_ERR;
72 if (ph_addr.vaddr + ph_addr.memsz < ph_addr.vaddr) return EL_ERR;
73
74 // allocate memory before loading
75 void *dest = elf_alloccb(ctx, ph_addr.vaddr, ph_addr.vaddr, ph_addr.memsz);
76
77 // actual loading
78 if (elf_fpread(ctx, dest, ph_addr.memsz, ph_addr.off) != ph_addr.memsz)
79 return EL_EIO;
80
81 // record the vm region in proc->mapped_info
82 int j;
83 for( j=0; j<PGSIZE/sizeof(mapped_region); j++ )
84 if( (process*)(((elf_info*)(ctx->info))->p)->mapped_info[j].va == 0x0 ) break;
70 // traverse the elf program segment headers
71 for (i = 0, off = ctx->ehdr.phoff; i < ctx->ehdr.phnum; i++, off += sizeof(ph_addr)) {
72 // read segment headers
73 if (elf_fpread(ctx, (void *)&ph_addr, sizeof(ph_addr), off) != sizeof(ph_addr)) return EL _EIO;
74
75 if (ph_addr.type != ELF_PROG_LOAD) continue;
76 if (ph_addr.memsz < ph_addr.filesz) return EL_ERR;
77 if (ph_addr.vaddr + ph_addr.memsz < ph_addr.vaddr) return EL_ERR;
78
79 // allocate memory block before elf loading
80 void *dest = elf_alloc_mb(ctx, ph_addr.vaddr, ph_addr.vaddr, ph_addr.memsz);
81
82 // actual loading
83 if (elf_fpread(ctx, dest, ph_addr.memsz, ph_addr.off) != ph_addr.memsz)
84 return EL_EIO;
85
86 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].va = ph_addr.vaddr;
87 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].npages = 1;
88 if( ph_addr.flags == (SEGMENT_READABLE|SEGMENT_EXECUTABLE) ){
89 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].seg_type = CODE_SEGMENT;
90 sprint( "CODE_SEGMENT added at mapped info offset:%d\n", j );
91 }else if ( ph_addr.flags == (SEGMENT_READABLE|SEGMENT_WRITABLE) ){
92 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].seg_type = DATA_SEGMENT;
93 sprint( "DATA_SEGMENT added at mapped info offset:%d\n", j );
94 }else
95 panic( "unknown program segment encountered, segment flag:%d.\n", ph_addr.flags );
96
97 ((process*)(((elf_info*)(ctx->info))->p))->total_mapped_region ++;
98 }
99
100 return EL_OK;
101 }
86 // record the vm region in proc->mapped_info. added @lab3_1
87 int j;
88 for( j=0; j<PGSIZE/sizeof(mapped_region); j++ ) //seek the last mapped region
89 if( (process*)(((elf_info*)(ctx->info))->p)->mapped_info[j].va == 0x0 ) break;
90
91 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].va = ph_addr.vaddr;
92 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].npages = 1;
93
94 // SEGMENT_READABLE, SEGMENT_EXECUTABLE, SEGMENT_WRITABLE are defined in kernel/elf.h
95 if( ph_addr.flags == (SEGMENT_READABLE|SEGMENT_EXECUTABLE) ){
96 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].seg_type = CODE_SEGMENT;
97 sprint( "CODE_SEGMENT added at mapped info offset:%d\n", j );
98 }else if ( ph_addr.flags == (SEGMENT_READABLE|SEGMENT_WRITABLE) ){
99 ((process*)(((elf_info*)(ctx->info))->p))->mapped_info[j].seg_type = DATA_SEGMENT;
100 sprint( "DATA_SEGMENT added at mapped info offset:%d\n", j );
101 }else
102 panic( "unknown program segment encountered, segment flag:%d.\n", ph_addr.flags );
103
104 ((process*)(((elf_info*)(ctx->info))->p))->total_mapped_region ++;
105 }
106
107 return EL_OK;
108 }
```
以上代码段中第86--97行将对被载入的段的类型ph_addr.flags进行判断以确定它是代码段还是数据段。完成以上的虚地址空间到物理地址空间的映射后将形成用户进程的虚地址空间结构如[图4.5](chapter4_memory.md#user_vm_space)所示)。
以上代码段中,第95--102行将对被载入的段的类型ph_addr.flags进行判断以确定它是代码段还是数据段。完成以上的虚地址空间到物理地址空间的映射后将形成用户进程的虚地址空间结构如[图4.5](chapter4_memory.md#user_vm_space)所示)。
接下来将通过switch_to()函数将所构造的进程投入执行:
```c
43 void switch_to(process* proc) {
44 assert(proc);
45 current = proc;
46
47 write_csr(stvec, (uint64)smode_trap_vector);
48 // set up trapframe values that smode_trap_vector will need when
49 // the process next re-enters the kernel.
50 proc->trapframe->kernel_sp = proc->kstack; // process's kernel stack
51 proc->trapframe->kernel_satp = read_csr(satp); // kernel page table
52 proc->trapframe->kernel_trap = (uint64)smode_trap_handler;
53
54 // set up the registers that strap_vector.S's sret will use
55 // to get to user space.
56
57 // set S Previous Privilege mode to User.
41 void switch_to(process* proc) {
42 assert(proc);
43 current = proc;
44
45 // write the smode_trap_vector (64-bit func. address) defined in kernel/strap_vector.S
46 // to the stvec privilege register, such that trap handler pointed by smode_trap_vector
47 // will be triggered when an interrupt occurs in S mode.
48 write_csr(stvec, (uint64)smode_trap_vector);
49
50 // set up trapframe values (in process structure) that smode_trap_vector will need when
51 // the process next re-enters the kernel.
52 proc->trapframe->kernel_sp = proc->kstack; // process's kernel stack
53 proc->trapframe->kernel_satp = read_csr(satp); // kernel page table
54 proc->trapframe->kernel_trap = (uint64)smode_trap_handler;
55
56 // SSTATUS_SPP and SSTATUS_SPIE are defined in kernel/riscv.h
57 // set S Previous Privilege mode (the SSTATUS_SPP bit in sstatus register) to User mode.
58 unsigned long x = read_csr(sstatus);
59 x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
60 x |= SSTATUS_SPIE; // enable interrupts in user mode
61
62 write_csr(sstatus, x);
63
64 // set S Exception Program Counter to the saved user pc.
65 write_csr(sepc, proc->trapframe->epc);
66
67 //make user page table
68 uint64 user_satp = MAKE_SATP(proc->pagetable);
69
70 // switch to user mode with sret.
71 return_to_user(proc->trapframe, user_satp);
72 }
62 // write x back to 'sstatus' register to enable interrupts, and sret destination mode.
63 write_csr(sstatus, x);
64
65 // set S Exception Program Counter (sepc register) to the elf entry pc.
66 write_csr(sepc, proc->trapframe->epc);
67
68 // make user page table. macro MAKE_SATP is defined in kernel/riscv.h. added @lab2_1
69 uint64 user_satp = MAKE_SATP(proc->pagetable);
70
71 // return_to_user() is defined in kernel/strap_vector.S. switch to user mode with sret.
72 // note, return_to_user takes two parameters @ and after lab2_1.
73 return_to_user(proc->trapframe, user_satp);
74 }
```
实际上,以上函数在[实验1](chapter3_traps.md)就有所涉及它的作用是将进程结构中的trapframe作为进程上下文恢复到RISC-V机器的通用寄存器中并最后调用sret指令通过return_to_user()函数)将进程投入执行。
@ -254,7 +262,7 @@ PKE实验中创建一个进程需要先调用kernel/process.c文件中的allo
```c
34 ssize_t sys_user_exit(uint64 code) {
35 sprint("User exit with code:%d.\n", code);
36 // in lab3 now, we should reclaim the current process, and reschedule.
36 // reclaim the current process, and reschedule. added @lab3_1
37 free_process( current );
38 schedule();
39 return 0;
@ -264,15 +272,15 @@ PKE实验中创建一个进程需要先调用kernel/process.c文件中的allo
可以看到如果某进程调用了exit()系统调用操作系统的处理方法是调用free_process()函数将当前进程也就是调用者进行“释放”然后转进程调度。其中free_process()函数kernel/process.c文件的实现非常简单
```c
150 int free_process( process* proc ) {
151 // we set the status to ZOMBIE, but cannot destruct its vm space immediately.
152 // since proc can be current process, and its user kernel stack is currently in use!
153 // but for proxy kernel, it (memory leaking) may NOT be a really serious issue,
154 // as it is different from regular OS, which needs to run 7x24.
155 proc->status = ZOMBIE;
156
157 return 0;
158 }
153 int free_process( process* proc ) {
154 // we set the status to ZOMBIE, but cannot destruct its vm space immediately.
155 // since proc can be current process, and its user kernel stack is currently in use!
156 // but for proxy kernel, it (memory leaking) may NOT be a really serious issue,
157 // as it is different from regular OS, which needs to run 7x24.
158 proc->status = ZOMBIE;
159
160 return 0;
161 }
```
可以看到,**free_process()函数仅是将进程设为ZOMBIE状态而不会将进程所占用的资源全部释放**这是因为free_process()函数的调用说明操作系统当前是在S模式下运行而按照PKE的设计思想S态的运行将使用当前进程的用户系统栈user kernel stack。此时如果将当前进程的内存空间进行释放将导致操作系统本身的崩溃。所以释放进程时PKE采用的是折衷的办法即只将其设置为僵尸ZOMBIE状态而不是立即将它所占用的资源进行释放。最后schedule()函数的调用,将选择系统中可能存在的其他处于就绪状态的进程投入运行,它的处理逻辑我们将在下一节讨论。
@ -510,56 +518,48 @@ user/app_naive_fork.c --> user/user_lib.c --> kernel/strap_vector.S --> kernel/s
直至跟踪到kernel/process.c文件中的do_fork()函数:
```c
167 int do_fork( process* parent)
168 {
169 sprint( "will fork a child from parent %d.\n", parent->pid );
170 process* child = alloc_process();
171
172 for( int i=0; i<parent->total_mapped_region; i++ ){
173 // browse parent's vm space, and copy its trapframe and data segments,
174 // map its code segment.
175 switch( parent->mapped_info[i].seg_type ){
176 case CONTEXT_SEGMENT:
177 *child->trapframe = *parent->trapframe;
178 break;
179 case STACK_SEGMENT:
180 memcpy( (void*)lookup_pa(child->pagetable, child->mapped_info[0].va),
181 (void*)lookup_pa(parent->pagetable, parent->mapped_info[i].va), PGSIZE );
182 break;
183 case CODE_SEGMENT:
184 // TODO (lab3_1): implment the mapping of child code segment to parent's
185 // code segment.
186 // hint: the virtual address mapping of code segment is tracked in mapped_info
187 // page of parent's process structure. use the information in mapped_info to
188 // retrieve the virtual to physical mapping of code segment.
189 // after having the mapping information, just map the corresponding virtual
190 // address region of child to the physical pages that actually store the code
191 // segment of parent process.
192 // DO NOT COPY THE PHYSICAL PAGES, JUST MAP THEM.
193 panic( "You need to implement the code segment mapping of child in lab3_1.\n" );
194
195 // after mapping, register the vm region (do not delete codes below!)
196 child->mapped_info[child->total_mapped_region].va = parent->mapped_info[i].va;
197 child->mapped_info[child->total_mapped_region].npages =
198 parent->mapped_info[i].npages;
199 child->mapped_info[child->total_mapped_region].seg_type = CODE_SEGMENT;
200 child->total_mapped_region++;
201 break;
202 }
203 }
204
205 child->status = READY;
206 child->trapframe->regs.a0 = 0;
207 child->parent = parent;
208 insert_to_ready_queue( child );
209
210 return child->pid;
211 }
170 int do_fork( process* parent)
171 {
172 sprint( "will fork a child from parent %d.\n", parent->pid );
173 process* child = alloc_process();
174
175 for( int i=0; i<parent->total_mapped_region; i++ ){
176 // browse parent's vm space, and copy its trapframe and data segments,
177 // map its code segment.
178 switch( parent->mapped_info[i].seg_type ){
179 case CONTEXT_SEGMENT:
180 *child->trapframe = *parent->trapframe;
181 break;
182 case STACK_SEGMENT:
183 memcpy( (void*)lookup_pa(child->pagetable, child->mapped_info[0].va),
184 (void*)lookup_pa(parent->pagetable, parent->mapped_info[i].va), PGSIZE );
185 break;
186 case CODE_SEGMENT:
187 // TODO (lab3_1): implment the mapping of child code segment to parent's
188 // code segment.
189 // hint: the virtual address mapping of code segment is tracked in mapped_info
190 // page of parent's process structure. use the information in mapped_info to
191 // retrieve the virtual to physical mapping of code segment.
192 // after having the mapping information, just map the corresponding virtual
193 // address region of child to the physical pages that actually store the code
194 // segment of parent process.
195 // DO NOT COPY THE PHYSICAL PAGES, JUST MAP THEM.
196 panic( "You need to implement the code segment mapping of child in lab3_1.\n" );
197
198 // after mapping, register the vm region (do not delete codes below!)
199 child->mapped_info[child->total_mapped_region].va = parent->mapped_info[i].va;
200 child->mapped_info[child->total_mapped_region].npages =
201 parent->mapped_info[i].npages;
202 child->mapped_info[child->total_mapped_region].seg_type = CODE_SEGMENT;
203 child->total_mapped_region++;
204 break;
205 }
206 }
```
该函数使用第172--202行的循环来拷贝父进程的逻辑地址空间到其子进程。我们看到对于trapframe段case CONTEXT_SEGMENT以及堆栈段case CODE_SEGMENTdo_fork()函数采用了简单复制的办法来拷贝父进程的这两个段到子进程中,这样做的目的是将父进程的执行现场传递给子进程。
该函数使用第175--205行的循环来拷贝父进程的逻辑地址空间到其子进程。我们看到对于trapframe段case CONTEXT_SEGMENT以及堆栈段case CODE_SEGMENTdo_fork()函数采用了简单复制的办法来拷贝父进程的这两个段到子进程中,这样做的目的是将父进程的执行现场传递给子进程。
然而对于父进程的代码段子进程应该如何“继承”呢通过第185--190行的注释,我们知道对于代码段,我们不应直接复制(减少系统开销),而应通过映射的办法,将子进程中对应的逻辑地址空间映射到其父进程中装载代码段的物理页面。这里,就要回到[实验2内存管理](chapter4_memory.md#pagetablecook)部分,寻找合适的函数来实现了。注意对页面的权限设置(可读可执行)。
然而对于父进程的代码段子进程应该如何“继承”呢通过第187--195行的注释我们知道对于代码段我们不应直接复制减少系统开销而应通过映射的办法将子进程中对应的逻辑地址空间映射到其父进程中装载代码段的物理页面。这里就要回到[实验2内存管理](chapter4_memory.md#pagetablecook)部分,寻找合适的函数来实现了。注意对页面的权限设置(可读可执行)。
@ -1121,8 +1121,7 @@ $ git merge lab3_3_rrsched -m "continue to work on lab3_challenge1"
- user/app_semaphore.c
```c
1 /*
2 * This app create two child process.
1 /* 2 * This app create two child process.
3 * Use semaphores to control the order of
4 * the main process and two child processes print info.
5 */

@ -0,0 +1,849 @@
## 第六章. RISCV处理器在PYNQ上的部署和接口实验
### 目录
- [6.1 系统能力培养部分实验环境安装](#environments)
- [6.1.1 Vivado开发环境](#subsec_vivadoenvironments)
- [6.1.2 fpga-pynq工程源码获取](#subsec_src)
- [6.1.3 工程编译环境配置](#subsec_projectenv)
- [6.1.4 蓝牙小车硬件组装](#subsec_hardware)
- [6.1.5 操作系统的替换修改](#subsec_system)
- [6.2 fpga实验1在Rocket Chip上添加uart接口](#hardware_lab1)
- [实验目的](#lab1_target)
- [实验内容](#lab1_content)
- [实验结果](#lab1_result)
- [6.3 fpga实验2以中断方式实现uart通信](#hardware_lab2)
- [实验目的](#lab2_target)
- [实验内容](#lab2_content)
- [实验结果](#lab2_result)
- [6.4 fpga实验3配置连接到PS端的USB设备](#hardware_lab3)
- [实验目的](#lab3_target)
- [实验内容](#lab3_content)
- [实验结果](#lab3_result)
在本章中我们将结合fpga-pynq开发板在PYNQ上部署RISCV处理器。并带领读者对rocket chip进行一些修改逐步为其添加uart外设支持和中断支持并搭载PKE内核运行小车控制程序实现一个可以通过蓝牙控制的智能小车。读者在完成6.1节和6.2节中的实验环境搭建后便可进行6.3节、6.4节和6.5节中的相关实验。
**需要特别注意的是本章中的三个fpga实验并不是独立设计的而是与第七章中的实验4_1、4_2和4_3依次对应。每一个fpga实验为对应的软件实现提供硬件支持。因此读者应该按照如下顺序来完成第六章和第七章的实验在完成“6.3 fpga实验1”后前往第七章完成对应的PKE软件实验——“7.2 lab4_1_POLL”在完成"6.4 fpga实验2"后前往第七章完成对应的PKE软件实验——“7.3 lab4_2_PLIC”在完成"6.5 fpga实验3"后前往第七章完成对应的PKE软件实验——“7.4 lab4_3_hostdevice”。**
<a name="environments"></a>
## 6.1 系统能力培养部分实验环境安装
<a name="subsec_vivadoenvironments"></a>
### 6.1.1 Vivado开发环境
Vivado设计套件是FPGA厂商赛灵思公司2012年发布的集成设计环境。本实验需要使用Vivado对FPGA电路进行修改。**Vivado设计套件在Windows平台与Linux平台都可安装使用且安装与使用过程基本相同读者可根据自身情况选择**。下面详细介绍这两种搭建方案:
**方案一Windows平台**
Vivado开发环境提供Windows平台支持该方案的安装过程简单**推荐读者优先选择此方案进行Vivado开发环境搭建**。具体步骤如下:
1. 下载安装程序。建议安装2016.2版本的Vivado进入[下载页面](https://china.xilinx.com/support/download/index.html/content/xilinx/zh/downloadNav/vivado-design-tools/archive.html)下拉找到2016.2版本,下载**Windows Web Installer**在弹出的Xilinx账号登录页面注册账号并登录后下载会自动开始。
<img src="pictures/vivado_17.png" alt="vivado_17" style="zoom:80%;" />
2. 启动安装程序。**注意安装过程需要保证网络畅通。以及30G以上的磁盘剩余空间**。
双击所下载的安装包直接运行即可。如下图窗口弹出时点击Continue。
<img src="pictures/vivado_1.jpg" alt="vivado_1" style="zoom:80%;" />
3. 进入欢迎页面点击next。
<img src="pictures/vivado_2.jpg" alt="vivado_2" style="zoom:80%;" />
4. 输入Xilinx账号密码并选中Download and Install Now后点击next。下载Vivado安装包时注册的账号可以在此处使用若无账号则可以点击上方“please create one”进行注册。
<img src="pictures/vivado_3.jpg" alt="vivado_3" style="zoom:80%;" />
5. 勾选同意用户协议后点击next。
<img src="pictures/vivado_4.jpg" alt="vivado_4" style="zoom:80%;" />
6. 勾选Vivado HL System Edition后点击next。
<img src="pictures/vivado_5.jpg" alt="vivado_5" style="zoom:80%;" />
7. 根据需要勾选安装项点击next。
<img src="pictures/vivado_6.jpg" alt="vivado_6" style="zoom:80%;" />
8. 选择安装位置。点击左上角的安装位置选择器右侧三点弹出目录选择窗口读者应在此处选择恰当的安装位置Vivado程序将会被安装在所选目录下。
**这里选择安装目录时,需要注意以下两点**
* 当前用户需要有安装目录的写入权限。
* 所选安装位置剩余空间必须足够读者可以查看左下角的Disk Space Required栏此处会显示安装所需空间以及磁盘剩余空间
当这两个条件不满足时,页面中会有红字提醒(如下图所示),读者应留意查看:
<img src="pictures/vivado_7.jpg" alt="vivado_7" style="zoom:80%;" />
根据提示更换适当的安装目录(下图是选择安装目录适当时的显示):
<img src="pictures/vivado_8.jpg" alt="vivado_8" style="zoom:80%;" />
确认无误后点击next继续下一步安装。
9. 检查安装信息无误后点击install。
<img src="pictures/vivado_9.jpg" alt="vivado_9" style="zoom:80%;" />
10. 等待下载安装完成(**耗时较长,务必保证下载时网络畅通**)。
<img src="pictures/vivado_10.jpg" alt="vivado_10" style="zoom:80%;" />
安装过程中弹出相关组件的安装窗口时,按照弹窗提示点击安装(根据所选组件不同,确认安装弹窗中的软件名称可能有变,一律点击安装即可)。
<img src="pictures/vivado_12.jpg" alt="vivado_12" style="zoom:80%;" />
11. 添加Vivado证书。读者应自行申请Vivado证书后导入。
<img src="pictures/vivado_13.jpg" alt="vivado_13" style="zoom:80%;" />
12. 出现如下的MATLAB选择窗口时直接点击右上角叉号关闭即可本实验中不会用到相关功能。
<img src="pictures/vivado_15.jpg" alt="vivado_15" style="zoom:80%;" />
13. 安装完成并导入证书后,会弹出如下窗口,表明安装成功。点击确认后安装过程结束。
<img src="pictures/vivado_16.jpg" alt="vivado_15" style="zoom:80%;" />
**方案二Linux平台Ubuntu桌面环境**
**注意本方案只在具有图形界面支持的Linux桌面环境中适用Ubuntu桌面环境或虚拟机等。若读者在前文中选择在WSL中安装相关实验环境则强烈建议直接参考前文的方案一在Windows系统中安装Windows版本的Vivado不要按照本方案进行安装。**
1. 下载安装程序。建议安装2016.2版本的Vivado进入[下载页面](https://china.xilinx.com/support/download/index.html/content/xilinx/zh/downloadNav/vivado-design-tools/archive.html)下拉找到2016.2版本,下载**Linux Web Installer**在弹出的Xilinx账号登录页面注册账号并登录后下载会自动开始。
<img src="pictures/vivado_18.png" alt="vivado_5" style="zoom:80%;" />
2. 启动安装程序。**注意安装过程需要保证网络畅通。以及30G以上的磁盘剩余空间**。
首先打开命令行,运行如下命令切换到安装包所在目录:
`$ cd [your.vivado.download.path]`
以上命令中,[your.vivado.download.path]指的是Vivado安装包的所在目录。接下来要为安装包添加可执行权 限,继续在命令行中执行如下命令:
`$ chmod +x Xilinx_Vivado_SDK_2016.2_0605_1_Lin64.bin`
上述命令执行完毕后,安装程序便具有了可执行权限,可以直接启动运行。执行如下命令:
`$ ./Xilinx_Vivado_SDK_2016.2_0605_1_Lin64.bin`
启动安装程序。
**3\~9步与上文方案一中Windows平台安装的3\~9步相同。**
10. 等待下载安装完成(**耗时较长,务必保证下载时网络畅通**。注意若安装时长时间卡在“generating installed device list”如下图
<img src="pictures/vivado_19.png" alt="vivado_5" style="zoom:80%;" />
可以先退出安装程序,**退出时仔细阅读弹窗内容(不要删除已经下载的文件缓存,下次运行安装程序时会自动跳过下载,节约读者安装时间)**。之后打开命令行,执行:
`$ sudo apt install libncurses5`
上述命令会安装缺失的ncurses库。ncurses库安装完成后需再次执行Vivado安装程序重做1~9步。
**接下来的11\~13步与上文方案一中Windows平台安装的11\~13步相同。**
<a name="subsec_src"></a>
### 6.1.2 fpga-pynq工程源码获取
为了方便大家快速获取源码,已将全部源码(包括子模块)打包上传到百度网盘,可以直接下载。
```
链接https://pan.baidu.com/s/1mTCcKG0EiFdxq4C5HTey3w
提取码1234
```
下载完成后,在命令行中执行如下命令,将源码进行解压。
```
$ cd 你的源码下载目录
$ cat fpga-pynq.0* > fpga-pynq.tar.gz #组装文件
$ md5sum fpga-pynq.tar.gz > md5 #计算MD5校验码
$ cmp md5 md5sum #比对校验码,如果此处没有任何输出,则为正确
$ tar -zxvf fpga-pynq.tar.gz #解压文件
```
或者从github获取源码
```
$ git clone https://github.com/huozf123/fpga-pynq.git
$ cd fpga-pynq
$ git submodule update --init --recursive
```
**Windows用户注意选择在WSL中完成实验的读者在完成上述步骤后还需要额外注释掉Makefrag文件的第108行在终端中继续输入以下命令**
```
$ cd fpga-pynq
$ sed -i "108s/^/#/" common/Makefrag
```
<a name="subsec_projectenv"></a>
### 6.1.3 工程编译环境配置
**注意该步骤中Linuxubuntu桌面环境与WindowsWSL环境下的操作存在一些差异读者应按照自己所用平台选择正确的步骤执行。**
**Linuxubuntu桌面环境**
1. 安装java jdk首先检查电脑中是否已安装java在中端输入以下命令
```
java -version
```
2. 若能显示java版本号则表明已安装java可以跳过java的安装。否则继续在终端执行如下命令安装java 1.8
```
sudo apt install openjdk-8-jre-headless
```
3. 添加Vivado相关的环境变量在命令行中执行替换掉“你的vivado安装目录”
```
$ source 你的vivado安装目录/Vivado/2016.2/settings64.sh
$ source 你的vivado安装目录/SDK/2016.2/settings64.sh
```
建议直接将上述两行命令添加到~/.bashrc文件的末端避免每次重新打开终端都需要重新执行。
4. 因为Vivado、SDK存在bug所以需要继续在命令行中执行以下命令替换“你的vivado安装目录”
```
$ sudo apt-get install libgoogle-perftools-dev
$ export SWT_GTK3=0
$ sudo sed -i "11,15s/^/#/" 你的vivado安装目录/Vivado/2016.2/.settings64-Vivado.sh #注释该文件第11-15行
```
5. 最后初始化子模块(若从网盘中下载源码,则此步无需执行),执行:
```
$ cd $REPO/pynq-z2/
$ make init-submodules
```
**WindowsWSL环境**
按照上文中第1、2步说明在WSL中安装java 1.8第3、4步在Windows中无需执行。最后在WSL中按第5步初始化子模块若从网盘中下载源码同样无需执行
<a name="subsec_hardware"></a>
### 6.1.4 蓝牙小车硬件组装
**组装蓝牙小车需要的材料清单**
pynq-z1开发板、sd卡、蓝牙模块、小车相关硬件电机、电机驱动板、底盘、配套总线、ttl转总线模块、以太网网线、7.4v电池、电池配套电源线、移动电源、usb供电线。
**蓝牙小车组装步骤**
1组装小车
首先完成小车底盘组装(**若采用已预先组装并连线的小车进行实验,则在该步骤中只需将剩下的四个车轮装上后,即可进行第二步**)。先将小车两个前轮上的电机驱动板用一根总线连接(每个电机驱动板上有两个总线接口,功能完全相同,两个电机各自任选一个总线接口用导线连接即可),另外再准备一根总线,一端插入两个电机中任一个的余下总线接口,另一端从小车底盘上方引出备用。再将两个后轮也按相同方式连接,引出另一根总线。小车底盘全部组装完成后,上方应该有两根引出的总线待连接(一根连接两个前轮,另一根连接两个后轮)。如下图所示:
<img src="pictures/hardware_2.jpg" alt="hardware_2" style="zoom:10%;" />
2将蓝牙模块连接到**PMODA**接口
将蓝牙模块的VCC、GND、TXD和RXD管脚连接到pynq-z1的**PMODA**接口,分别对应**PMODA**上面一排从左往右第1、2、3和4个管脚。如下图所示
<img src="pictures/hardware_1.jpg" alt="hardware_1" style="zoom:10%;" />
3ttl转总线模块连接
将从小车底盘上引出的两根总线插入到ttl转总线模块上的两个总线接口两个接口不必进行区分。然后用跳线帽将该模块上的**ttl-zx**针脚进行短接以便让模块工作在ttl转总线模式。之后取出与电池配套的电源线接到ttl转总线模块的电源输入上**注意查看电路板背面的正负极标志,红色导线接正极,黑色导线接负极**)。再将电源线上的电池插头与电池连接。
取三根公对母杜邦线下面为了叙述方便将这三根杜邦线分别命名为线1、线2和线3读者可通过杜邦线颜色进行区分将线1、线2和线3的母头分别连接到ttl转总线模块上的**RXD、TXD与GND注意ttl转总线模块上有两排黄色的ttl接口一排为公头一排为母头这里说的这三个接口是指下面一排的公头**。ttl转总线模块连接好后的部分连接如下图所示
<img src="pictures/hardware_6.jpg" alt="hardware_6" style="zoom:20%;" />
再将线1、线2和线3的公头连接到pynq-z1的**PMODB**接口,分别对应**PMODB**上面一排**从右往左第3、4、5个管脚**。
<img src="pictures/hardware_7.jpg" alt="hardware_7" style="zoom:10%;" />
注意在连线过程中三根杜邦线没有发生交叉始终是按照线123或线321的顺序插入到接口中的。因此在连接完成后读者可以通过检查线3的两端是否连接的都是GND一端为ttl转总线上的GND另一端为pynq-z1上的GND来判断接线是否正确。连接完成后如下图
<img src="pictures/hardware_3.jpg" alt="hardware_3" style="zoom:10%;" />
4为pynq-z1供电
首先应确保pynq-z1开发板处于从USB供电的状态这需要读者使用跳线帽将电路板左下角印有*USB*标志的两个针脚短接。然后将USB供电线一端接入开发板左侧的micro USB接口另一端接入移动电源5V 1A。如下图所示
<img src="pictures/hardware_4.jpg" alt="hardware_4" style="zoom:80%;" />
5将pynq-z1开发板、ttl转总线模块、电机电池以及开发板供电电源固定在小车底盘上
<a name="subsec_system"></a>
### 6.1.5 操作系统的替换修改
注意,下文中涉及到的指令较多,且需要在不同的地方进行输入。总的来说,**需要输入命令的地方大致有三处本地终端、ssh会话、sftp会话**。读者在操作过程中,务必要留意文中类似于: **“在终端输入”指本地计算机的终端、“在ssh会话中输入”、在“sftp会话中输入”这样的提示**。另外需要在这三处输入的命令可能无法避免的交替出现因此建议读者各打开一个窗口分别用来创建ssh连接与sftp连接创建方式在下文需要用到的ssh与sftp的地方有说明读者可以在遇到后再进行创建并在实验完成前不要关闭这样可以避免反复的进行ssh、sftp连接。若由于重启开发板或者其他原因导致连接断开则按照相关创建步骤的说明重新创建连接即可。
1下载[PYNQ v2.6启动镜像Boot Image](https://xilinx-ax-dl.entitlenow.com/dl/ul/2020/10/22/R210391224/pynq_z1_v2.6.0.zip?hash=LpAIQVeSunXk7KU0smi8Ag&expires=1649065517&filename=pynq_z1_v2.6.0.zip),然后解压得到.img镜像文件。
2把启动镜像写到sd卡中**此步Linuxubuntu桌面环境与WindowsWSL环境下操作不同**
**Linuxubuntu桌面环境**
* 将sd卡插入读卡器连上电脑。
* 打开终端输入lsblk命令找到刚刚插入的sd卡设备可通过输出的设备所对应的容量判断记下该设备的名称通常为**sd***)。
<img src="pictures/fpga_4.png" alt="fpga_4" style="zoom:80%;" />
* 继续在终端中输入如下命令进行写入,将*镜像文件路径*替换为**第1步**中解压得到的.img镜像文件所在路径将*设备名称*替换为上一步中记录下的设备名称(**该命令具有一定风险会清空指定设备上原有的所有文件不可逆输入该命令时务必仔细检查正确的名称一般为sdb或sdc等后面不带数字且容量与插入的sd卡设备相同**
```
sudo dd bs=4M if=镜像文件路径 of=/dev/设备名称
```
该命令执行耗时较长,且在执行完成前不会有任何输出,请读者耐心等待。
* 拔出sd卡。
**WindowsWSL环境**
* 在[这里](https://sourceforge.net/projects/win32diskimager/)下载Win32 Disk Imager软件并安装。
* 将sd卡插入读卡器连上电脑。
* 打开Win32 Disk Imager在**映像文件**处选择解压得到的img文件然后在**设备**选择处选择插入的sd卡最后点击**写入**按钮。
<img src="pictures/windows_1.jpg" alt="windows_1" style="zoom:80%;" />
* 拔出sd卡。
3将sd卡插入到pynq-z1开发板中检查pynq-z1左上角的跳线帽是否短接了靠上面的两个针脚这样做是为了确保pynq-z1处于从sd卡启动的状态。如下图所示
<img src="pictures/hardware_5.jpg" alt="hardware_5" style="zoom:80%;" />
打开电源左下方的开关等待系统启动。系统启动耗时较长启动完成后开发板下方的四个蓝色led灯会在数次闪烁后保持长亮此时表明系统已经启动完成。用网线连接电脑和开发板开发板上的网口在左侧
**注意若遇到以下情况开发板电源开关旁边的红灯闪烁或熄灭开发板无法正常启动开发板启动后随机发生自动重启表现为ssh会话断开或卡死则应该检查**
* USB电源输入是否正常
* 电源开关旁的跳线帽是否连接了USB标志对应的两个针脚
* 电源开关旁的跳线帽是否松动
4修改网卡IP地址**此步Linuxubuntu桌面环境与WindowsWSL环境下操作不同**
**Linuxubuntu桌面环境**
打开电脑终端,执行如下命令,需要将*有线网卡名称*替换为电脑上实际的有线网卡名称,可以通过执行*ifconfig*命令查看通常为eth0、enp7s0等。该命令将电脑有线网卡ip地址设置为192.168.2.13。**注意若在所使用电脑的有线网卡中配置了自动连接的PPPOE协议等手动指定的ip地址可能会在一段时间后就被自动覆盖导致实验失败读者应根据自己电脑的具体情况暂时关闭相关设置。**
```
sudo ifconfig 有线网卡名称 192.168.2.13
```
**WindowsWSL环境**
点击左下角Windows图标输入“控制面板”这时会看到控制面板点击打开控制面板。然后依次点击网络和Internet、网络和共享中心、更改适配器设置在窗口左侧找到以太网右键单击点击“属性”打开以太网属性设置窗口。
<img src="pictures/windows_2.jpg" alt="windows_2" style="zoom:80%;" />
找到*Internet协议版本4TCP/IPv4*双击打开。选择“使用下面的IP地址”并将IP地址改为192.168.2.13子网掩码改为255.255.255.0,最后点击确定保存。
<img src="pictures/windows_3.jpg" alt="windows_3" style="zoom:80%;" />
5下载[libfesvr.so](https://gitee.com/hustos/myriscv-fesvr/blob/modify/build/libfesvr.so)frontend server运行需要的动态链接库、[riscv-fesvr](https://gitee.com/hustos/myriscv-fesvr/blob/modify/build/riscv-fesvr)frontend server
6创建一个烧写bitstream的shell脚本
由于烧写bitstream的操作在之后的实验中会多次进行读者每次在对硬件进行修改后都需要重新烧写bitstream到pynq-z1开发板中才能生效因此可以将烧写bitstream的命令保存在一个脚本中便于后续使用。用文本编辑器创建一个文本文件命名为program.sh 输入如下内容后保存:
注意若读者在Windows平台进行实验**千万不要用系统自带的记事本创建此文件!**因为记事本会默认将文件保存成dos格式会导致将脚本传输到Linux系统中执行时报错。Windows用户可以选择**1. 在WSL中创建该文件2. 不要在Windows中创建该文件暂时跳过这一步当后文利用ssh协议连接到开发板后直接在ssh会话中启动vim创建该文件3. 若执意在Windows图形界面中创建该文件请下载[sublime文本编辑器](http://www.sublimetext.com/)进行创建**。
```
#! /bin/sh
cp rocketchip_wrapper.bit.bin /lib/firmware/
echo 0 > /sys/class/fpga_manager/fpga0/flags
echo rocketchip_wrapper.bit.bin > /sys/class/fpga_manager/fpga0/firmware
```
7将这三个文件通过SFTP传输到开发板的/home/xilinx目录下需要先将[libfesvr.so所在路径]、[riscv-fesvr所在路径]与[program.sh所在路径]分别替换成前两步中libfesvr.so、riscv-fesvr与program.sh文件所在的路径。**此步Linuxubuntu桌面环境与WindowsWSL环境下操作不同**
**Linuxubuntu桌面环境**
创建sftp连接
```
sftp xilinx@192.168.2.99
密码输入xilinx
```
在**sftp会话**中输入:
```
put [libfesvr.so所在路径] /home/xilinx
put [riscv-fesvr所在路径] /home/xilinx
put [program.sh所在路径] /home/xilinx
```
**WindowsWSL环境**
在Windows下进行fpga实验的读者既可以选择在WSL中执行上述Linux环境下的sftp连接命令从而直接在WSL中完成文件的传输操作也可以选择更加方便的带图形界面的sftp客户端程序来完成文件的传输操作。下面介绍如何使用MobaXterm软件来完成文件传输。**注意后文中的其他步骤仍然会用到sftp连接为避免文章篇幅过长Windows环境下图形界面的文件传输操作只会在此处介绍一次读者在后续步骤中碰到需要使用sftp传输文件的操作时可以参考此处的操作方式**
以传输文件libfesvr.so为例下面是具体步骤
打开MobaXterm点击左上角的“Session”。
<img src="pictures/windows_4.jpg" alt="windows_4" style="zoom:80%;" />
在弹出的窗口中选择“SFTP“然后在Remote host后填入192.168.2.99在Username后填入xilinx最后点击OK。
<img src="pictures/windows_5.jpg" alt="windows_5" style="zoom:80%;" />
输入密码xilinx后点击OK。
<img src="pictures/windows_6.jpg" alt="windows_6" style="zoom:80%;" />
在图中“1”处定位到libfesvr.so所在文件夹然后在图中“2”处找到文件libfesvr.so将该文件用鼠标拖动到图中“3”处等待文件传输完成即可。
<img src="pictures/windows_7.jpg" alt="windows_7" style="zoom:80%;" />
**以同样的方式再将riscv-fesvr文件与program.sh文件传输到pynq-z1开发板中。**
8用ssh远程登录192.168.2.99开发板的ip地址用户名密码都为xilinx。**此步Linuxubuntu桌面环境与WindowsWSL环境下操作不同**
**Linuxubuntu桌面环境**
在终端中输入如下命令来建立ssh连接
```
ssh xilinx@192.168.2.99
第一次运行时会提示确认连接信息输入yes后回车
密码输入xilinx
```
**WindowsWSL环境**
Windows用户既可以选择在WSL中用与上述Linux下相同的方式在终端中建立ssh连接也可以选择用带图形界面的ssh客户端来建立ssh连接。下面介绍如何在Windows环境中使用MobaXterm软件建立ssh连接的步骤。**注意后文中的其他步骤仍然会用到ssh连接为避免文章篇幅过长Windows环境下图形界面的ssh连接建立过程只会在此处介绍一次读者在后续步骤中碰到需要使用ssh与pynq-z1建立连接的操作时可以参考此处的操作方式**
打开MobaXterm点击左上角的“Session”。
<img src="pictures/windows_4.jpg" alt="windows_4" style="zoom:80%;" />
在弹出的窗口中选择“SSH“然后在Remote host后填入192.168.2.99勾选Specify username并在其后填入xilinx最后点击OK。
<img src="pictures/windows_8.jpg" alt="windows_8" style="zoom:80%;" />
输入密码xilinx后回车。
9在**ssh会话**中,依次执行以下命令:
```
$ sudo mv libfesvr.so /usr/local/lib/ #密码输入xilinx
$ sudo mkdir -p /lib/firmware
$ chmod +x program.sh
$ chmod +x riscv-fesvr
```
10修改完成断开ssh、sftp连接。
若是在图形窗口中创建的连接则直接关闭窗口即可若是在Linux终端或WSL中创建的连接则输入“exit”后回车。
<a name="hardware_lab1"></a>
## 6.2 fpga实验1在Rocket Chip上添加uart接口
<a name="lab1_target"></a>
#### 实验目的
对于前文中提到的蓝牙智能小车在其从蓝牙设备获取用户指令以及向电机驱动板发送电机驱动命令的过程都需要用到uart接口与外设进行通信。因此在本实验中首先带领读者为Rocket Chip添加uart外设的支持。
<a name="lab1_content"></a>
#### 实验内容
**为Rocket Chip添加uart支持**
Rocket Chip相关的verilog文件主要是由Chisel自动生成的这些verilog文件难以直接进行修改编辑。为了改变Rocket Chip的行为正确的修改方式是对common/src/main/scala下的文件进行修改并运行rocket chip生成器生成相关的verilog文件再将生成的verilog文件复制到src/verilog工程目录下。其中运行rocket chip生成器以及复制文件的相关操作都已经为读者写入了make file中并不需要手动执行读者只需按照以下步骤修改相关配置文件并按照说明执行编译命令即可。
**下面是具体的操作步骤**
为了能够在PKE中像访问普通内存一样对新增的uart外设进行访问我们需要对Rocket Chip进行修改为其添加MMIO的支持。然后需要在顶层verilog文件中增加MMIO接口和uart接口其中的MMIO接口用来将Rocket Chip中的MMIO接口与block design中添加的MMIO_s_axi接口进行连接uart接口则用来将在block design中添加的uart接口暴露出去。在完成顶层verilog文件中接口的添加后还需修改xdc约束文件将uart接口与fpga开发板上的物理接口进行绑定便于后续外设的连接。
1. 修改Rocket Chip以增加其对MMIOMemory-mapped I/O的支持
修改文件`$REPO/common/src/main/scala/Top.scala`。修改后的文件为[Top.scala](https://gitee.com/hustos/fpga-pynq/blob/uart-pynq/common/src/main/scala/Top.scala)修改过的地方均有modified标识。
2. 修改工程顶层verilog文件增加MMIO接口和uart接口的连接
修改文件`$REPO/common/rocketchip_wrapper.v`。修改后的文件为[rocketchip_wrapper.v](https://gitee.com/hustos/fpga-pynq/blob/uart-pynq/common/rocketchip_wrapper.v)修改过的地方均有modified标识。
3. 修改xdc约束文件
修改文件`$REPO/pynq-z2/src/constrs/base.xdc`。为了将`$REPO/common/rocketchip_wrapper.v`中定义的uart接口与fpga开发板上的IO管脚建立链接并定义IO管脚的电平标准我们需要在xdc约束文件末尾加入如下四行代码映射uart接口。这四行代码将uart_out、uart_in、uart2_out和uart2_in端口依次绑定到FPGA开发板的Y16、Y17、T11和T10管脚其中Y16与Y17管脚分别对应于pynq-z1开发板上**PMODA**接口中**上面一排的从左往右第三、第四个接口**这两个接口在后面会用来连接蓝牙模块的数据收发管脚具体的安装方式在本实验末尾会进行介绍。类似的T11和T10两个接口分别对应**PMODB**接口中**上面一排的从左往右第三、第四个接口**,用来连接电机驱动板的数据收发管脚。
```
set_property -dict { PACKAGE_PIN Y16 IOSTANDARD LVCMOS33 } [get_ports { uart_out }]; #IO_L7P_T1_34 Sch=ja_p[2]
set_property -dict { PACKAGE_PIN Y17 IOSTANDARD LVCMOS33 } [get_ports { uart_in }]; #IO_L7N_T1_34 Sch=ja_n[2]
set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { uart2_out }]; #IO_L1P_T0_34 Sch=jb_p[2]
set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { uart2_in }]; #IO_L1N_T0_34 Sch=jb_n[2]
```
修改后的文件为[base.xdc](https://gitee.com/hustos/fpga-pynq/blob/uart-pynq/pynq-z2/src/constrs/base.xdc)
**在完成上述三处文件修改后编译生成vivado工程**。
4. 生成vivado工程
**注意该步骤中Linuxubuntu桌面环境与WindowsWSL环境下的操作存在一些差异读者应按照自己所用平台选择正确的步骤执行。**
**Linuxubuntu桌面环境**
在命令行中执行如下命令生成并打开vivado工程。
```
$ cd $REPO/pynq-z2/
$ make project
$ make vivado
```
**WindowsWSL环境**
在WSL命令行中执行如下命令
```
$ cd $REPO/pynq-z2/
$ make project
```
在Windows中**第一次**生成工程时还需要进行以下操作后续在修改Rocket Chip后重新生成时只需要执行上述两条命令即可
按快捷键win+r打开运行窗口输入cmd后按回车键打开命令提示符窗口。在命令提示符窗口中执行如下命令设置vivado环境变量替换掉“你的vivado安装目录”
```
> 你的vivado安装路径\Vivado\2016.2\settings64.bat
```
然后切换盘符到源码所在盘比如若源码保存在d盘则在命令提示符窗口中输入`d:`,切换完成后继续在命令提示符窗口中输入以下命令(替换掉“你的源码所在目录”):
```
> cd 你的源码所在目录\fpga-zynq\pynq-z2\
> vivado -mode tcl -source src/tcl/pynq_rocketchip_ZynqFPGAConfig.tcl
```
双击桌面图标打开Windows中安装好的vivado点击Open Project找到`你的源码所在目录\fpga-zynq\pynq-z2\pynq_rocketchip_ZynqFPGAConfig\pynq_rocketchip_ZynqFPGAConfig.xpr`双击打开。
**然后我们需要在block design中添加uart相关的接口和IP核并完成地址分配。**
5. 修改block design
1打开vivado工程后在*Sourses*面板点击*rocketchip_wrapper*左边的加号,然后再双击*system_i*打开block design面板。在最左边找到*S_AXI*端口,点击选中,然后再按*ctrl+c*、*ctrl+v*,这样就复制出一个同样的端口*S_AXI1*,点击选中这个*S_AXI1*,找到左边*External Interface Properties*面板,把*Name*改为*MMIO_S_AXI**Clock Port*选择*ext_clk_in*;选中最左侧的*ext_clk_in*端口,找到左边*External Port Properties*面板,将*FrequencyMhz*修改为*50*。
<img src="pictures/fpga_1.png" alt="fpga_1" style="zoom:80%;" />
2在右侧电路图面板空白处点击右键再点击*Add IP...*然后输入axi interconnect然后双击出现的搜索结果把axi interconnect放入电路图中然后双击新出现的*axi_interconnect_2*,将*Number of Master Interface*改为2然后点击*OK*。
3在右侧电路图面板空白处点击右键再点击*Add IP...*然后输入axi uartlite然后双击出现的搜索结果把axi uartlite放入电路图中然后双击新出现的*axi_uartlite_0*,将*Baud Rate*改为115200然后点击*OK*。*axi_uartlite_0*用来与蓝牙模块进行串口通信,其波特率需要与蓝牙模块的波特率相同,若读者使用的蓝牙模块为其他的波特率,则应该将*Baud Rate*改为蓝牙模块的实际波特率(或通过串口调试软件改变蓝牙模块的波特率)。
4重复步骤3再添加一个axi uartlite该IP核会被自动命名为*axi_uartlite_1*,双击*axi_uartlite_1*,同样将其*Baud Rate*改为115200然后点击*OK*。*axi_uartlite_1*用来与电机驱动板进行串口通信其波特率需要与电机驱动板以串口方式进行控制的波特率相同在本实验用到的小车中电机驱动板的波特率为115200。
5在右侧电路图面板空白处点击右键再点击*Create Port...**Port name*为uart_in*Direction*为Input*Type*为Other点击OK。
6在右侧电路图面板空白处点击右键再点击*Create Port...**Port name*为uart2_in*Direction*为Input*Type*为Other点击OK。
7在右侧电路图面板空白处点击右键再点击*Create Port...**Port name*为uart_out*Direction*为Output*Type*为Other点击OK。
8在右侧电路图面板空白处点击右键再点击*Create Port...**Port name*为uart2_out*Direction*为Output*Type*为Other点击OK。
7按照如图方式接好电路图(注意uart_lite_0右侧的UART端口右边有个加号需要点一下加号展开再进行连线)。
<img src="pictures/fpga_2.png" alt="fpga_2" style="zoom:80%;" />
8点击上面的*Address Editor*,然后在未映射的单元上右键单击, 并在弹出的菜单中点击*assign address*。
<img src="pictures/fpga_11.jpg" alt="fpga_11" style="zoom:80%;" />
按照如图所示配置地址建议先修改Range属性再修改Offset Address属性否则Offset Address可能修改不成功该操作将蓝牙模块的地址映射到了0x6000_0000将电机驱动的地址映射到了0x6000_1000。
<img src="pictures/fpga_3.png" alt="fpga_3" style="zoom:80%;" />
6. 保存所有修改后的文件(也可以选择直接点击下一步中的*Generate Bitstream*vivado会对未保存内容弹窗提醒用户保存读者应留意相关弹窗内容
**上述修改全部完成后便可以进行bitstream文件的生成以及打包的工作。打包得到的rocketchip_wrapper.bit.bin文件最终是需要烧录到pynq-z1开发板中来实现相关硬件功能的。**
7. 生成bitstream文件
最后,点击左下角的*Generate Bitstream*生成比特流文件。(该步骤耗时较长)
当弹出如下对话框时点击yes
<img src="pictures/fpga_9.png" alt="fpga_9" style="zoom:80%;" />
当弹出如下对话框时表明bitstream生成完成**点击cancel**
<img src="pictures/fpga_10.png" alt="fpga_10" style="zoom:80%;" />
8. 打包bitstream文件为bin文件
**注意该步骤中Linuxubuntu桌面环境与WindowsWSL环境下的操作存在一些差异读者应按照自己所用平台选择正确的步骤执行。**
**Linuxubuntu桌面环境**
1创建Full_Bitstream.bif文件
该文件为纯文本文件,可以直接用记事本创建,并输入如下内容后保存:
```
all:
{
rocketchip_wrapper.bit /* Bitstream file name */
}
```
将该文件重命名为Full_Bitstream.bif然后将其复制到`$REPO/pynq-z2/pynq_rocketchip_ZynqFPGAConfig/pynq_rocketchip_ZynqFPGAConfig.runs/impl_1`目录下。
2生成比特流bin文件
在终端执行以下命令生成bin文件
```
$ cd $REPO/pynq-z2/pynq_rocketchip_ZynqFPGAConfig/pynq_rocketchip_ZynqFPGAConfig.runs/impl_1
$ bootgen -image Full_Bitstream.bif -arch zynq -process_bitstream bin -w on
```
**WindowsWSL环境**
1创建Full_Bitstream.bif文件
创建步骤与上文Linux桌面环境下基本相同读者根据自己的习惯在WSL或者Windows图形界面中创建该文件皆可创建完成后将其复制到`$REPO/pynq-z2/pynq_rocketchip_ZynqFPGAConfig/pynq_rocketchip_ZynqFPGAConfig.runs/impl_1`目录下。
2生成比特流bin文件
按快捷键win+r打开运行窗口输入cmd后按回车键打开命令提示符窗口。在命令提示符窗口中执行如下命令设置vivado环境变量替换掉“你的vivado安装目录”
```
> 你的vivado安装路径\Vivado\2016.2\settings64.bat
```
接着按照前文中所述方式切换盘符到源码所在盘,接着在命令提示符窗口中执行如下命令(替换掉“你的源码所在目录”):
```
> cd 你的源码所在目录\pynq-z2\pynq_rocketchip_ZynqFPGAConfig\pynq_rocketchip_ZynqFPGAConfig.runs\impl_1
> bootgen -image Full_Bitstream.bif -arch zynq -process_bitstream bin -w on
```
9. 硬件组装
按照前文6.1.4节的说明完成蓝牙小车的组装。
10. 操作系统的替换修改
按照前文6.1.5节的说明完成操作系统的替换修改。
11. 烧写bitstream
将`$REPO/pynq-z2/pynq_rocketchip_ZynqFPGAConfig/pynq_rocketchip_ZynqFPGAConfig.runs/impl_1`下的rocketchip_wrapper.bit.bin文件传输到pynq-z1开发板中后执行烧录脚本即可具体方法如下
1将rocketchip_wrapper.bit.bin传输到pynq-z1中
若使用sftp协议在命令行中传输则在**sftp会话**中输入如下命令:
```
put [rocketchip_wrapper.bit.bin所在路径] /home/xilinx
```
Windows图形界面下的传输方法不再赘述。
2 在**ssh会话**中,执行以下命令:
```
$ sudo ./program.sh #密码输入xilinx
```
<a name="lab1_result"></a>
#### 实验结果
在完成本实验后,一个基础的蓝牙小车所需要的硬件环境就搭建完成了。接下来,读者应该前往第七章完成**lab4_1_POLL**实验,在完成**lab4_1_POLL**并将riscv_pke与小车控制程序发送到pynq-z1上执行后小车便可以通过蓝牙app进行控制。
<a name="hardware_lab2"></a>
## 6.3 fpga实验2以中断方式实现uart通信
**该实验以fpga实验1的修改为基础读者可以先将fpga实验1中最后生成的`rocketchip_wrapper.bit.bin`文件复制到其他目录保存,然后直接在同一个项目文件夹下继续本实验**。
<a name="lab2_target"></a>
#### 实验目的
在上一个fpga实验中读者已经为Rocket Chip添加了uart通信功能但是目前在PKE中需要以轮询的方式实现uart通信这一点读者在完成lab4_1时应该有所体会程序需要不断读取蓝牙模块的状态寄存器来判断IO设备是否准备好发送或接收数据。本实验将会为uart添加中断机制以中断方式实现uart通信。
<a name="lab2_content"></a>
#### 实验内容
**以中断方式实现uart通信**
要想在Rocket Chip上实现对uart外设的中断通信支持一方面需要通过修改Rocket Chip的结构来为Rocket Chip添加中断引脚另一方面需要修改block design将uart产生的中断信号线连接到Rocket Chip的中断引脚。
**下面是具体的操作步骤:**
**首先我们需要修改Rocket Chip结构为其添加外部中断接口。**
1. 修改Rocket Chip结构暴露外部中断接口
修改`$REPO/common/src/main/scala/Top.scala`文件:
第16行加入
```
val interrupt_in = Input(UInt(2.W)) //给rocket-chip增加一个位宽为2的输入端口
```
第32行加入
```
io.interrupt_in <> target.interrupts //将rocket-chip内部的中断引脚和刚刚增加的端口连接起来
```
注释掉第40行该语句原本的作用是直接把中断引脚置为低电平以防止中断引脚悬空
```
//target.tieOffInterrupts()
```
这是[修改好的rocket-chip顶层文件](https://gitee.com/hustos/fpga-pynq/blob/uart-interrupt-pynq/other-files/Top.scala)。
2. 修改`$REPO/common/rocketchip_wrapper.v`文件
此处主要的改动是将block design引出的uart中断引脚连接到rocket chip对应端口。
这是修改好的[rocketchip_wrapper.v](https://gitee.com/hustos/fpga-pynq/blob/uart-interrupt-pynq/common/rocketchip_wrapper.v)文件。
**上述文件修改完成后需要删除之前编译工程时产生的临时文件并重新编译vivado工程。只有执行该步操作才能将上述修改引入到vivado工程中去。**
3. 重新生成vivado工程
在Linux终端或WSL中执行如下命令重新生成vivado工程。命令执行完成后会自动在vivado中打开项目。
```
$ cd $REPO/pynq-z2
$ rm -f src/verilog/Top.ZynqFPGAConfig.v
$ rm -f ../common/build/*
$ make project
```
**然后要将block design中的两个axi uartlite IP核的中断信号引出。**
4. 修改block design
1打开vivado工程后在*Sourses*面板点击*rocketchip_wrapper*左边的加号,然后再双击*system_i*打开block design面板。在右侧电路图面板空白处点击右键再点击Add IP...然后输入concat然后双击出现的搜索结果把concat放入电路图中然后双击新出现的xlconcat_0模块将Number of Ports设置为2In0 Width设置为1In1 Width设置为1然后点击OK。该concat IP核的作用是将axi_uartlite_0与axi_uartlite_1两个模块产生的一位中断信号进行拼接合并成一路位宽为2的中断信号该信号会被连接到上文中为Rocket Chip添加的位宽为2的中断输入端口。
2右键点击空白处点击create portport名称填写interruptsdirection选择outputtype选择interrupt勾选create vector填写from 1 to 0。
3按如图方式连线
<img src="pictures/fpga_5.png" alt="fpga_5" style="zoom:80%;" />
5. 保存所有修改后的文件(也可以选择直接点击下一步中的*Generate Bitstream*vivado会对未保存内容弹窗提醒用户保存读者应留意相关弹窗内容
**完成所有的修改后需要重新进行bitstream文件的生成以及打包的工作然后将新生成的rocketchip_wrapper.bit.bin烧录到pynq-z1开发板中。**
6. 生成bitstream文件
最后,点击左下角的*Generate Bitstream*生成比特流文件。(该步骤耗时较长)
7. 打包bitstream文件为bin文件
见上文**fpga实验1 步骤8**。若读者选择在与fpga实验1相同的项目文件夹下继续完成本次实验则Full_Bitstream.bif文件应该已经存在那么可以跳过**fpga实验1 步骤8**中**创建Full_Bitstream.bif文件**的步骤。
8. 硬件连接
本次实验的硬件连接与**fpga实验1**完全相同读者同样需要按照6.1.4节的说明完成蓝牙小车的硬件组装。
9. 操作系统的替换修改
若在进行本次实验时pynq-z1开发板中插入的sd卡与**fpga实验1**中相同,且没有删除过**fpga实验1**中创建的相关文件则该步骤可以直接跳过。否则读者应参考6.1.5节中的说明重新为sd卡写入操作系统并完成相关文件的替换和修改。
10. 烧写bitstream
见上文**fpga实验1 步骤11**。
<a name="lab2_result"></a>
#### 实验结果
在完成本实验后当蓝牙模块在收到蓝牙串口app发送的指令时会产生一个外部中断该中断能够在后续的PKE实验中被操作系统所捕获并做出相应的反应如唤醒一个处于阻塞状态的进程。读者在完成本次fpga实验后应该前往第七章完成PKE实验**lab4_2_PLIC**,该实验会通过中断的方式来通过外设向蓝牙小车发送控制指令。
<a name="hardware_lab3"></a>
## 6.4 fpga实验3配置连接到PS端的USB设备
**该实验以fpga实验2的修改为基础读者可以先将fpga实验2中最后生成的`rocketchip_wrapper.bit.bin`文件复制到其他目录保存,然后直接在同一个项目文件夹下开始本实验**。
<a name="lab3_target"></a>
#### 实验目的
读者在完成**fpga实验1**与**fpga实验2**以及第七章中对应的PKE实验**lab4_1 POLL**和**lab4_2_PLIC**后便能得到一个可以通过蓝牙串口助手进行控制的蓝牙小车。该蓝牙小车通过uart接口获取来自手机或其他蓝牙设备的控制指令并根据收到的不同指令选择做出前进、后退、左转和右转动之一。在这个控制过程中接收控制信号的蓝牙模块和发出控制指令的电机驱动模块都是连接在PL端的设备所谓PL端即为pynq开发板上搭载的一块FPGA可编程芯片在fpga实验中生成的比特流文件都需要烧录到PL端使其能够支持riscv-pke的运行在第7章会有关于PL端与PS端更详细的介绍。连接在PL端的设备通常功能比较简单可以由PKE直接进行访问控制。而其他一些功能更加复杂的外设例如USB摄像头则被连接到PS端。运行在PL端的PKE在访问PS端的USB设备时需要通过HTIF协议来调用ARM端的系统调用函数利用功能更加完整的PS端操作系统对其进行访问控制。
本实验的目的是对连接到PS端的USB设备进行一些配置便于USB摄像头的连接。在对应的第七章**lab4_3_hostdevice**中我们要为蓝牙小车连接一个USB摄像头让小车根据摄像头拍摄的画面识别前方出现的障碍物并做出自动避障动作。
<a name="lab3_content"></a>
#### 实验内容
**配置连接到PS端的USB设备**
zynq的PS端外设都被封装成了一个ZYNQ7 Processing System IP核因此修改PS端的USB外设连接需要通过配置该IP核进行。
**下面是具体的操作步骤:**
**该实验的操作步骤比较简单只需要在vivado的图形界面中对PS端的USB外设进行一些配置后重新生成bitstream文件即可**。
1. 修改block design
打开vivado工程后在*Sourses*面板点击*rocketchip_wrapper*左边的加号,然后再双击*system_i*打开block design面板。双击processing_system7_0进入PS端ARM核的配置界面如下图。
<img src="pictures/fpga_6.png" alt="fpga_6" style="zoom:80%;" />
<img src="pictures/fpga_7.png" alt="fpga_7" style="zoom:80%;" />
点击该窗口左侧的*MIO Configuration*之后我们可以在右侧的界面中将需要使用的PS端外设与复用IO相连并配置MIO的电平标准。
按如下图示点击首先确保USB 0前的复选框处于勾选状态然后将USB的12个管脚的speed属性由slow改为fast修改完成后点击OK保存并关闭窗口。
<img src="pictures/fpga_8.png" alt="fpga_8" style="zoom:80%;" />
2. 生成bitstream文件
最后,点击左下角的*Generate Bitstream*生成比特流文件。(该步骤耗时较长)
3. 打包bitstream文件为bin文件
见上文**fpga实验1 步骤8**。
4. 硬件连接
在完成6.1.4节中硬件连接的基础之上还需要将USB摄像头连接到pynq-z1开发板左上角的USB接口并将USB摄像头固定在小车前方指前进方向固定时建议将摄像头略微向上倾斜。另外需要注意的是为了与第七章中**lab4_3_hostdevice**的代码相匹配这里连接的摄像头必须支持YUYV格式的画面输出。
5. 操作系统的替换修改
若在进行本次实验时pynq-z1开发板中插入的sd卡与**fpga实验1**中相同,且没有删除过**fpga实验1**中创建的相关文件则该步骤可以直接跳过。否则读者应参考6.1.5节中的说明重新为sd卡写入操作系统并完成相关文件的替换和修改。
6. 烧写bitstream
见上文**fpga实验1 步骤11**。
<a name="lab3_result"></a>
#### 实验结果
在完成本次实验后USB摄像头便可以正确的连接到开发板的PS端。读者接下来应该前往第七章继续完成**lab4_3_hostdevice**,在完成**lab4_3_hostdevice**中PKE相关代码的修改后连接到PS端的USB外设便可以被运行在PL端的PKE调用从而在此基础上进一步为蓝牙小车增添自动避障的功能。

@ -0,0 +1,619 @@
# 第七章实验4设备管理基于[RISCV-on-PYNQ](https://gitee.com/hustos/fpga-pynq)
### 目录
- [7.1 实验4的基础知识](#fundamental)
- [7.1.1 pynq开发板介绍](#subsec_pynq)
- [7.1.2 内存映射I/O(MMIO)](#subsec_MMIO)
- [7.1.3 riscv-fesvr原理](#subsec_fesvr)
- [7.1.4 轮询I/O控制方式](#subsec_polling)
- [7.1.5 中断驱动I/O控制方式](#subsec_plic)
- [7.1.6 设备文件](#subsec_file)
- [7.2 lab4_1 POLL](#polling)
- [给定应用](#lab4_1_app)
- [实验内容](#lab4_1_content)
- [实验指导](#lab4_1_guide)
- [7.3 lab4_2_PLIC](#PLIC)
- [给定应用](#lab4_2_app)
- [实验内容](#lab4_2_content)
- [实验指导](#lab4_2_guide)
- [7.4 lab4_3_hostdevice](#hostdevice)
- [给定应用](#lab4_3_app)
- [实验内容](#lab4_3_content)
- [实验指导](#lab4_3_guide)
<a name="fundamental"></a>
## 7.1 实验4的基础知识
完成前面所有实验后PKE内核的整体功能已经得到完善。在实验四的设备实验中我们将结合fpga-pynq板在rocket chip上增加uart模块和蓝牙模块并搭载PKE内核实现蓝牙通信控制智能小车设计设备管理的相关实验。
<a name="subsec_pynq"></a>
### 7.1.1 pynq开发板介绍
本实验中我们所使用的pynq-z2开发板上搭载两块芯片一块为Arm架构32位芯片称为PS端我们能在上面运行Ubuntu另一块为FPGA可编程芯片称为PL端通过烧录Rocket chip电路使它能够运行Riscv架构的操作系统即riscv-pke。
![](pictures/fig6_4.png)
如上图在开发板上运行的时候PKE在PL端运行一方面它可以通过Rocket Chip电路的连线访问PL端的设备device如蓝牙、小车电机等另一方面在PS端运行的riscv-fesvr程序可以和PKE通过HTIF协议通信使得PKE可以读写PS端Linux操作系统下的设备文件host device比如摄像头、声卡等。也就是说PKE除了可以访问本身的设备还可以利用PS端操作系统的功能访问更复杂的设备这就是代理内核的特点。
实验四和前三个实验的关系如下:
![](pictures/fig6_5.png)
可见除了部分硬件相关的操作外PKE在Spike和开发板上的运行是完全等价的代理内核一方面让我们可以用最简单的方法访问两端的设备另一方面在不同地方运行基本不用改太多代码非常优越。
<a name="subsec_MMIO"></a>
### 7.1.2 内存映射I/O(MMIO)
内存映射(Memory-Mapping I/O)是一种用于设备驱动程序和设备通信的方式它区别于基于I/O端口控制的Port I/O方式。RICSV指令系统的CPU通常只实现一个物理地址空间这种情况下外设I/O端口的物理地址就被映射到CPU中单一的物理地址空间成为内存的一部分CPU可以像访问一个内存单元那样访问外设I/O端口而不需要设立专门的外设I/O指令。
在MMIO中内存和I/O设备共享同一个地址空间。MMIO是应用得最为广泛的一种IO方法它使用相同的地址总线来处理内存和I/O设备I/O设备的内存和寄存器被映射到与之相关联的地址。当CPU访问某个内存地址时它可能是物理内存也可以是某个I/O设备的内存。此时用于访问内存的CPU指令就可以用来访问I/O设备。每个I/O设备监视CPU的地址总线一旦CPU访问分配给它的地址它就做出响应将数据总线连接到需要访问的设备硬件寄存器。为了容纳I/O设备CPU必须预留给I/O一个地址映射区域。
在本章节中修改后的RocketChip将蓝牙控制寄存器和小车电机端接到固定的内存地址因此可以通过对这些地址进行读写控制蓝牙和小车电机。
<a name="subsec_fesvr"></a>
### 7.1.3 riscv-fesvr原理
riscv-fesvr是PKE在PYNQ开发板上使用的重要工具它是ARM端系统上运行的程序控制PKE的启动。除了启动功能riscv-fesvr程序主要分为两个模块系统调用模块块负责接受PKE对ARM端的系统调用请求在ARM端执行这些函数内存模块负责读写RISCV端的内存和PKE交换数据。
PKE调用宿主机/开发板ARM端的系统调用函数使用的是HTIF协议。协议要求内核保留两个地址作为和riscv-fesvr共享数据的地方。PKE要使用系统调用函数就需要将系统调用函数的编号和参数通过这个地址发送给riscv-fesvr方法是定义一个数组magic_mem该数组存储了调用号和参数再将数组的起始地址按一定的格式填入这个地址中如下图。
![](pictures/fig6_7.png)
打个比方我通过HTIF协议调用ARM端的write函数那么我先定义一个数组magic_memmagic_mem[0]为write的系统调用号magic_mem[1]为文件描述符magic_mem[2]为缓冲区地址magic_mem[3]为写入长度。然后把一个数写入共享地址这个数高8位和中间8位都是0低48位为magic_mem的地址。riscv-fesvr会首先读出magic_mem里的数据然后根据magic_mem[0]决定要调用write函数这时需要注意magic_mem[2]里的缓冲区地址是RISCV端内存的地址所以先把RISCV端内存里的这段数据读到外面内存再以外面内存地址为参数调用write函数。
riscv-fesvr的内存模块用来读写RISCV端的内存从而可以读取系统调用参数以及读写PKE的缓冲区。Pynq开发板把RISCV端的内存抽象成设备文件/dev/mem所以内存模块可以通过在固定偏移量读写该文件从而实现读写内存。
另外控制摄像头需要用到的ioctl、mmap、munmap三个系统调用函数是原版的riscv-fesvr不支持的所以我们对riscv-fesvr的系统调用模块进行了修改
* ioctl函数比较简单可以直接用PKE传过来的参数调用系统调用函数
* mmap函数比较麻烦因为ARM端通过mmap映射的是ARM端的内存RISCV端无法访问。所以再添加readmmap函数PKE可以通过HTIF调用此函数读取ARM端被映射的内存。将ARM端用mmap映射的所有内存用数组存储映射地址返回给PKE数组索引PKE向readmmap传入索引riscv-fesvr根据索引找到地址读取ARM端的内存数据返回给PKE。
<a name="subsec_polling"></a>
### 7.1.4 轮询I/O控制方式
在实验四中,我们设备管理的主要任务是控制设备与内存的数据传递,具体为从蓝牙设备读取到用户输入的指令字符(或传递数据给蓝牙在手机端进行打印),解析为小车前、后、左、右、停止等动作来传输数据给电机实现对小车的控制。在前两个实验中,我们分别需要对轮询控制方式和中断控制方式进行实现。
首先程序直接控制方式又称循环测试方式每次从外部设备读取一个字的数据到存储器对于读入的每个字CPU需要对外设状态进行循环检查直到确定该数据已经传入I/O数据寄存器中。
轮询I/O控制方式流程如图
<img src="pictures/fig6_1_polling.png" alt="fig6_1" style="zoom:100%;" />
<a name="subsec_plic"></a>
### 7.1.5 中断驱动I/O控制方式
在前一种轮询的控制方式中由于CPU的高速性和I/O设备的低速性导致CPU浪费绝大多数时间处于等待I/O设备完成数据传输的循环测试中会造成大量资源浪费。中断驱动的方式是允许请求I/O的进程在设备工作时进入休眠状态使CPU能够运行别的进程。直到设备工作完成时再由设备发出中断中断处理程序唤醒之前休眠的进程使其能够接受设备返回的数据继续执行。采用中断驱动的控制方式在I/O操作过程中CPU可以执行其他的进程CPU与设备之间达到了部分并行的工作状态从而提升了资源利用率。
Riscv包含三类中断软中断、时钟中断和外部中断。软中断和时钟中断我们在实验一已经接触过而设备触发的中断属于外部中断。在实验一中我们在机器态捕获了时钟中断然后将其转发成内核态的软中断交由中断处理程序处理本章则直接通过设置MIDELEG寄存器利用RISCV的中断委托机制直接将外部中断交由内核态的中断处理程序处理不用经过机器态的捕获。另外RISCV架构还指定了PLICPriority Level Interrupt Controller模块管理各级中断该设备使用MMIO控制PKE在处理中断之后通过读写指定的内存地址来获取触发中断的设备的编号以及通知PLIC本次中断是否处理成功。
中断驱动I/O方式流程如图
<img src="pictures/fig6_2_plic.png" alt="fig6_2" style="zoom:100%;" />
<a name="subsec_file"></a>
### 7.1.6 设备文件
用户程序访问外部设备通常有两种方式通过特定系统调用访问和通过设备文件访问。前者即操作系统提供专门的函数控制设备后者是操作系统把设备指定成一个文件通过通用的文件的读写函数控制设备。设备文件常用的函数除了open、read、write、close还有以下几种
* ioctl`int ioctl(int fd, unsigned long request, void *data);`用来设置设备参数。fd是文件描述符request是一个常数表示参数类型不同的类型对应不同的常数data通常是一个指向要设置的参数的值的指针参数的值可以是整数也可以是结构体等返回值为该函数是否执行成功。如摄像头设备我们就可以通过该函数设置摄像头的拍摄分辨率、颜色格式等音频设备我们可以设置采样率、数据格式等。
* mmap`void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);`该函数原本的作用是将虚拟地址和文件进行映射使得读写文件可以像读写内存一样方便同时也能节省物理内存但对于有些不支持read/write读写的设备就必须使用mmap函数将虚拟地址和设备文件映射才能读写设备的数据。addr参数表示映射的起始虚拟地址通常填NULL表示由操作系统自己指定lengthprot、flags分别表示映射地址空间的长度、权限和其他参数fd为文件描述符offset为文件偏移量返回值为映射的起始虚拟地址。
在本章节中将使用PKE通过HTIF协议和PS端的riscv-fesvr进行通信以读写PS端的摄像头设备文件进而控制摄像头设备。
<a name="polling"></a>
## 7.2 lab4_1 poll
<a name="lab4_1_app"></a>
#### **给定应用**
- user/app_poll.c
```c
1 /*
2 * Below is the given application for lab4_1.
3 * The goal of this app is to control the car via Bluetooth.
4 */
5
6 #include "user_lib.h"
7 #include "util/types.h"
8
9 int main(void) {
10 printu("please input the instruction through bluetooth!\n");
11 while(1)
12 {
13 char temp = (char)uartgetchar();
14 if(temp == 'q')
15 break;
16 car_control(temp);
17 }
18 exit(0);
19 return 0;
20 }
```
应用通过轮询的方式从蓝牙端获取指令,实现对小车的控制功能。
* 先提交lab3_3的答案然后切换到lab4_1继承lab3_3中所做的修改并make
```bash
//切换到lab4_1
$ git checkout lab4_1_poll
//继承lab3_3以及之前的答案
$ git merge lab3_3_rrsched -m "continue to work on lab4_1"
//重新构造
$ make clean; make
```
由于本实验给出的基础代码修改了硬件相关的部分代码所以无法在Spike上运行需在PYNQ开发板上进行验证。读者在进行本实验之前应该已经按照第六章中的说明完成了fpga实验1并将rocketchip_wrapper.bit.bin文件烧录到了开发板中。
make完成后需要将obj目录下编译生成的可执行文件传输到开发板中然后在开发板上运行程序。具体的做法如下
1. 首先将make命令编译生成的两个可执行文件obj/app_polling文件与obj/riscv-pke文件通过sftp协议传输到开发板的/home/xilinx目录下。至于如何向开发板中传送文件在第六章中已有详细的介绍读者可以参考之前的描述。
2. 通过ssh协议连接到开发板连接方法在第六章中同样有详细说明并在ssh客户端中输入如下命令执行app_polling
```bash
$ sudo ./program.sh # 若重启过开发板,则在执行程序前要先执行此脚本进行烧录
$ sudo ./riscv-fesvr riscv-pke app_poll
In m_start, hartid:0
(Emulated) memory size: 256 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x0000000080009000, PKE kernel size: 0x0000000000009000 .
free physical memory address: [0x0000000080009000, 0x00000000800fffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080005000
kernel page table is on
Switch to user mode...
in alloc_proc. user frame 0x00000000800f9000, user stack 0x000000007ffff000, user kstack 0x00000000800f8000
User application is loading.
Application: app_polling
CODE_SEGMENT added at mapped info offset:3
Application program entry point (virtual address): 0x0000000000010078
going to insert process 0 to ready queue.
going to schedule process 0 to run.
Ticks 0
please input the instruction through bluetooth!
You have to implement sys_user_uart_getchar to get data from UART using uartgetchar in lab4_1 and modify it in lab4_2.
System is shutting down with exit code -1.
```
从直接编译运行结果上来看蓝牙端端口获取用户输入指令的uartgetchar系统调用未完善所以无法进行控制小车的后续操作。按照提示我们需要实现蓝牙uart端口的获取和打印字符系统调用以及传送驱动数据给小车电机的系统调用实现对小车的控制。
<a name="lab4_1_content"></a>
#### **实验内容**
如输出提示所表示的那样需要找到并完成对uart_getchar的调用重新编译生成app_polling与riscv-pke文件并将这两个文件传输到开发板中在ssh会话中执行以下命令得到预期结果
``` bash
$ sudo ./program.sh # 若重启过开发板,则在执行程序前要先执行此脚本进行烧录
$ sudo ./riscv-fesvr riscv-pke app_poll
In m_start, hartid:0
(Emulated) memory size: 256 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x0000000080009000, PKE kernel size: 0x0000000000009000 .
free physical memory address: [0x0000000080009000, 0x00000000800fffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080005000
kernel page table is on
Switch to user mode...
in alloc_proc. user frame 0x00000000800f9000, user stack 0x000000007ffff000, user kstack 0x00000000800f8000
User application is loading.
Application: app_polling
CODE_SEGMENT added at mapped info offset:3
Application program entry point (virtual address): 0x0000000000010078
going to insert process 0 to ready queue.
going to schedule process 0 to run.
please input the instruction through bluetooth!
```
当程序正确执行时会在显示“please input the instruction through bluetooth!”后等待用户从蓝牙设备发送控制指令。保持程序运行,并按照下面的步骤通过蓝牙向小车发送指令:
1. 在手机端下载任意一种蓝牙串口通信APP提供一个在安卓12上测试无误的软件下载[蓝牙串口](https://www.coolapk.com/apk/com.orion.bluetoothserialtool)下面的步骤都以此APP为例进行说明其他蓝牙串口通信APP的使用方法与之类似
2. 打开手机蓝牙开关。
3. 点击蓝牙串口通信APP下方的蓝牙设备按钮找到蓝牙模块其名称通常是“BT04-A”点击进行配对。若提示输入PIN码则输入“1234”。
4. 点击蓝牙串口通信APP下方的收发数据按钮进入收发数据界面。在下方的对话框中即可输入控制指令具体命令如下
**注意,若软硬件都正确无误,在发送下方指令后小车会立即开始运动。请务必确保小车的运动在实验人员的控制之内!若小车上有连接用于调试的网线等设备,建议暂时使小车的四个车轮处于悬空状态。当功能全部测试无误后,可以在保持程序运行的情况下直接拔掉网线,并将小车放置在地面上移动**。
| 命令 | 效果 |
| :--: | :---------------------------------------------: |
| 1 | 小车前进 |
| 2 | 小车后退 |
| 3 | 小车左转 |
| 4 | 小车右转 |
| 0 | 小车停止 |
| q | 程序停止在ssh会话中输入ctrl+c也可以停止程序 |
<a name="lab4_1_guide"></a>
#### **实验指导**
基于实验lab1_1你已经了解和掌握操作系统中系统调用机制的实现原理。对于本实验的应用我们发现user/app_poll.c文件中有三个函数调用uart_getcharuart_putchar和uart2_putchar。UART是一种控制设备的端口协议但在本实验中它可以通过MMIO进行控制。对代码进行跟踪我们发现这三个函数都在user/user_lib.c中进行了实现对应于lab1_1的流程我们可以在kernel/syscall.h中查看新增的系统调用以及编号
```c
16 #define SYS_user_uart_putchar (SYS_user_base + 6)
17 #define SYS_user_uart_getchar (SYS_user_base + 7)
18 #define SYS_user_uart2_putchar (SYS_user_base + 8)
```
继续追踪我们发现在kernel/syscall.c的do_syscall函数中新增了对应系统调用编号的实现函数对于新增系统调用分别有如下函数进行处理
```c
133 case SYS_user_uart_putchar:
134 sys_user_uart_putchar(a1);return 1;
135 case SYS_user_uart_getchar:
136 return sys_user_uart_getchar();
137 case SYS_user_uart2_putchar:
138 sys_user_uart2_putchar(a1);return 1;
```
读者的任务即为在kernel/syscall.c中追踪并完善sys_user_uart_getchar。对于uart相关的函数我们给出uart端口的地址映射如图
<img src="pictures/fpga_3.png" alt="fpga_3" style="zoom:80%;" />
图中的axi_uartlite_0对应于蓝牙设备的uart端口aix_uartlite_1则对应于小车电机驱动板的uart端口。其中蓝牙设备的uart端口的偏移地址为0x60000000对应写地址为0x60000004读地址为0x60000000同时对0x60000008的状态位进行轮询检测到信号时进行读写操作。
在kernel/syscall.c中找到函数实现空缺并根据注释完成sys_user_uart_getchar系统调用由于lab4_2需要对lab4_1完成的代码进行修改所以这里一并给出了lab4_2的提示
```c
95 ssize_t sys_user_uart_getchar() {
96 // TODO (lab4_1 and lab4_2): implment the syscall of sys_user_uart_getchar and modify it in lab4_2.
97 // hint (lab4_1): The functionality of sys_user_uart_getchar is to get data from UART address.
98 // Therefore we should check the data from the address of bluetooth status repeatedly, until the data is ready.
99 // Then read the data from the address of bluetooth reading and return.
100 // hint (lab4_2): the functionality of sys_user_uart_getchar is let process sleep and wait for value. therefore,
101 // we should call do_sleep to let process 0 sleep.
102 // then we should get uartvalue and return.
103 panic( "You have to implement sys_user_uart_getchar to get data from UART using uartgetchar in lab4_1 and modify it in lab4_2.\n" );
104
105 }
```
**注意编写自己的代码时千万不要修改或删去lab4_2的提示即100行到102行防止后面实验的合并错误**
**实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_1中所做的工作**
```
$ git commit -a -m "my work on lab4_1 is done."
```
<a name="PLIC"></a>
## 7.3 lab4_2_PLIC
<a name="lab4_2_app"></a>
#### **给定应用**
- user/app_PLIC.c
```c
1 /*
2 * Below is the given application for lab4_2.
3 * The goal of this app is to control the car via Bluetooth.
4 */
5
6 #include "user_lib.h"
7 #include "util/types.h"
8 void delay(unsigned int time){
9 unsigned int a = 0xfffff ,b = time;
10 volatile unsigned int i,j;
11 for(i = 0; i < a; ++i){
12 for(j = 0; j < b; ++j){
13 ;
14 }
15 }
16 }
17 int main(void) {
18 printu("Hello world!\n");
19 int i;
20 int pid = fork();
21 if(pid == 0)
22 {
23 while (1)
24 {
25 delay(3);
26 printu("waiting for you!\n");
27 }
28
29 }
30 else
31 {
32 while(1)
33 {
34 char temp = (char)uartgetchar();
35 if(temp == 'q')
36 break;
37 car_control(temp);
38 }
39 }
40
41
42 exit(0);
43
44 return 0;
45 }
```
应用通过中断的方式从蓝牙端获取指令实现对小车的控制功能。在等待蓝牙的进程休眠的时候会执行delay进程可以看到waiting for you提示信息。
* 先提交lab4_1的答案然后切换到lab4_2继承lab4_1中所做的修改并make
```bash
//切换到lab4_2
$ git checkout lab4_2_PLIC
//继承lab4_1以及之前的答案
$ git merge lab4_1_poll -m "continue to work on lab4_2"
//重新构造
$ make clean; make
```
编译完成后同样需要将obj/riscv-pke与obj/app_PLIC文件传输到开发板中然后再通过ssh协议连接到开发板在ssh会话中执行以下命令烧录并运行程序
```bash
$ sudo ./program.sh # 若重启过开发板,则在执行程序前要先执行此脚本进行烧录
$ sudo ./riscv-fesvr riscv-pke app_PLIC
```
直接编译执行结果和完成后的lab4_1一致一直阻塞在这里等待蓝牙数据。需要修改lab4_1所写的代码并添加中断处理使得等待蓝牙的进程能够自动休眠执行delay进程直到发生外部中断后才继续执行。
<a name="lab4_2_content"></a>
#### **实验内容**
如输出提示所表示的那样需要修改lab4_1所写的代码并添加中断处理。完成后按lab4_1的方法执行程序在等待蓝牙的时候会不断输出waiting for you提示信息在手机上输入控制指令后小车应能根据指令反应。
<a name="lab4_2_guide"></a>
#### **实验指导**
在kernel/syscall.c中找到lab4_1写的代码并根据注释进行修改
```c
95 ssize_t sys_user_uart_getchar() {
96 // TODO (lab4_1 and lab4_2): implment the syscall of sys_user_uart_getchar and modify it in lab4_2.
97 // hint (lab4_1): The functionality of sys_user_uart_getchar is to get data from UART address.
98 // Therefore we should check the data from the address of bluetooth status repeatedly, until the data is ready.
99 // Then read the data from the address of bluetooth reading and return.
100 // hint (lab4_2): the functionality of sys_user_uart_getchar is let process sleep and wait for value. therefore,
101 // we should call do_sleep to let process 0 sleep.
102 // then we should get uartvalue and return.
103 panic( "You have to implement sys_user_uart_getchar to get data from UART using uartgetchar in lab4_1 and modify it in lab4_2.\n" ); // 该行已被你之前写的代码替换
104
105 }
```
当蓝牙有数据发送时pke会收到外部中断你需要完成接收到外部中断后的处理。
在kernel/strap.c中找到函数空缺并根据注释完成中断处理函数
```c
103 case CAUSE_MEXTERNEL_S_TRAP:
104 {
105 //reset the PLIC so that we can get the next external interrupt.
106 volatile int irq = *(uint32 *)0xc201004L;
107 *(uint32 *)0xc201004L = irq;
108 volatile int *ctrl_reg = (void *)(uintptr_t)0x6000000c;
109 *ctrl_reg = *ctrl_reg | (1 << 4);
110 // TODO (lab4_2): implment the case of CAUSE_MEXTERNEL_S_TRAP.
111 // hint: the case of CAUSE_MEXTERNEL_S_TRAP is to get data from UART address and wake the process. therefore,
112 // and you need to store the data in struct process.value.
113 panic( "You have to implement CAUSE_MEXTERNEL_S_TRAP to get data from UART and wake the process 0 in lab4_2.\n" );
114
115 break;
116 }
```
**实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_2中所做的工作**
```
$ git commit -a -m "my work on lab4_2 is done."
```
<a name="hostdevice"></a>
## 7.4 lab4_3_hostdevice
<a name="lab4_3_app"></a>
#### **给定应用**
- user/app_host_device.c
```c
1 #pragma pack(4)
2 #define _SYS__TIMEVAL_H_
3 struct timeval {
4 unsigned int tv_sec;
5 unsigned int tv_usec;
6 };
7
8 #include "user_lib.h"
9 #include "videodev2.h"
10 #define DARK 64
11 #define RATIO 7 / 10
12
13 int main() {
14 char *info = allocate_share_page();
15 int pid = do_fork();
16 if (pid == 0) {
17 int f = do_open("/dev/video0", O_RDWR), r;
18
19 struct v4l2_format fmt;
20 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
21 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
22 fmt.fmt.pix.width = 320;
23 fmt.fmt.pix.height = 180;
24 fmt.fmt.pix.field = V4L2_FIELD_NONE;
25 r = do_ioctl(f, VIDIOC_S_FMT, &fmt);
26 printu("Pass format: %d\n", r);
27
28 struct v4l2_requestbuffers req;
29 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
30 req.count = 1; req.memory = V4L2_MEMORY_MMAP;
31 r = do_ioctl(f, VIDIOC_REQBUFS, &req);
32 printu("Pass request: %d\n", r);
33
34 struct v4l2_buffer buf;
35 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
36 buf.memory = V4L2_MEMORY_MMAP; buf.index = 0;
37 r = do_ioctl(f, VIDIOC_QUERYBUF, &buf);
38 printu("Pass buffer: %d\n", r);
39
40 int length = buf.length;
41 char *img = do_mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, f, buf.m.offset);
42 unsigned int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
43 r = do_ioctl(f, VIDIOC_STREAMON, &type);
44 printu("Open stream: %d\n", r);
45
46 char *img_data = allocate_page();
47 for (int i = 0; i < (length + 4095) / 4096 - 1; i++)
48 allocate_page();
49 yield();
50
51 for (;;) {
52 if (*info == '1') {
53 r = do_ioctl(f, VIDIOC_QBUF, &buf);
54 printu("Buffer enqueue: %d\n", r);
55 r = do_ioctl(f, VIDIOC_DQBUF, &buf);
56 printu("Buffer dequeue: %d\n", r);
57 r = read_mmap(img_data, img, length);
58 int num = 0;
59 for (int i = 0; i < length; i += 2)
60 if (img_data[i] < DARK) num++;
61 printu("Dark num: %d > %d\n", num, length / 2 * RATIO);
62 if (num > length / 2 * RATIO) {
63 *info = '0'; car_control('0');
64 }
65 } else if (*info == 'q') break;
66 }
67
68 for (char *i = img_data; i - img_data < length; i += 4096)
69 free_page(i);
70 r = do_ioctl(f, VIDIOC_STREAMOFF, &type);
71 printu("Close stream: %d\n", r);
72 do_munmap(img, length); do_close(f); exit(0);
73 } else {
74 yield();
75 while(1)
76 {
77 char temp = (char)uartgetchar();
78 *info = temp;
79 if(temp == 'q')
80 break;
81 car_control(temp);
82 }
83 }
84 return 0;
85 }
```
该用户程序包含两个进程其中主进程和实验4_2类似负责接收蓝牙发送过来的数据根据数据控制小车行动前进、后退、左转、右转、停止子进程则负责拍摄和分析首先初始化摄像头设备然后是个死循环判断摄像头拍摄的图像数据如果当前小车处于前进状态则拍摄然后检查数据如果判断前面有障碍物则控制车轮停转刹车否则如果主进程退出了则自己进行释放文件、内存、关闭设备等操作再退出。在用户程序操控摄像头的过程中使用了ioctl、mmap、munmap等系统调用你需完善其中的open和ioctl两个系统调用对其进行完善从而实现小车的障碍识别和停止功能。
* 先提交lab4_2的答案然后切换到lab4_3继承lab4_2中所做的修改并make
```bash
//切换到lab4_3
$ git checkout lab4_3_hostdevice
//继承lab4_2以及之前的答案
$ git merge lab4_2_PLIC -m "continue to work on lab4_3"
//重新构造
$ make clean; make
```
编译完成后同样需要将obj/riscv-pke与obj/app_host_device文件传输到开发板中然后再通过ssh协议连接到开发板在ssh会话中执行以下命令烧录并运行程序
```bash
$ sudo ./program.sh # 若重启过开发板,则在执行程序前要先执行此脚本进行烧录
$ sudo ./riscv-fesvr riscv-pke app_host_device
```
<a name="lab4_3_content"></a>
#### 实验内容
如应用提示所表示的那样读者需要找到并完成对open和ioctl的调用使得用户能够设置设备参数从而控制摄像头实现拍照等功能获取图片后检查数据从而判断前方是否出现障碍物。
跟踪相关系统调用在kernel/file.c里可以看到需要补充的函数
```c
25 int do_open(char *pathname, int flags) {
26 // TODO (lab4_3): call host open through spike_file_open and then bind fd to spike_file
27 // hint: spike_file_dup function can bind spike_file_t to an int fd.
28 panic( "You need to finish open function in lab4_3.\n" );
29 }
30 int do_write(int fd, char *buf, uint64 count) {
31 spike_file_t *f = spike_file_get(fd);
32 return spike_file_write(f, buf, count);
33 }
34 int do_close(int fd) {
35 spike_file_t *f = spike_file_get(fd);
36 return spike_file_close(f);
37 }
38
39 int do_ioctl(int fd, uint64 request, char *data) {
40 // TODO (lab4_3): call host ioctl through frontend_sycall
41 // hint: fronted_syscall ioctl argument:
42 // 1.call number
43 // 2.fd
44 // 3.the order to device
45 // 4.data address
46 panic( "You need to call host's ioctl by frontend_syscall in lab4_3.\n" );
47 }
```
实验预期结果小车在前进过程中能够正常识别障碍物后并自动停车。测试时别忘记把摄像头接到USB接口否则系统会找不到设备文件。
<a name="lab4_3_guide"></a>
#### 实验指导
##### 摄像头控制
USB摄像头最基础的控制方法是使用读写设备文件的方式。拍摄一张照片包含以下过程
* 打开设备文件使用open函数
* 设置设备参数使用ioctl函数包括设置摄像头的图像分辨率和格式、读取方式、缓冲数量和索引等
* 映射内存由于USB摄像头对应的设备文件不支持直接用read函数进行读写所以需要用mmap函数将文件映射到一段虚拟地址通过虚拟地址进行读写
* 拍摄使用ioctl函数控制设置缓冲区的入队和出队为一个拍摄过程
* 结束和清理包含使用ioctl函数关闭设备使用munmap函数解映射使用close函数关闭设备文件
##### 图片解析
在本实验的用户程序中我们实现了一个非常简单的障碍物判断算法计算灰度小于64的像素点个数如果个数大于像素点总数的7/10即认为前方是障碍物。
应用第21行可以看到我们从摄像头获取的数据是YUYV格式读者可进行查阅它用灰度、蓝色色度、红色色度三个属性表示颜色每个像素点都有灰度属性。由于我们分析障碍物只需要灰度图所以取每个像素点的灰度属性即可。因此对于获取过来的数据删去奇数索引的数据就可以得到灰度图。
注意:对于灰度阈值的设定可根据环境亮度进行一定的调整,可以先根据摄像头返回的图像进行分析,计算出对应障碍物的灰度值;灰度阈值越精确,小车对于障碍物的识别将越灵敏,并能在合理的距离内识别到障碍物并停车。
**虽然如此,该算法仍不是非常精确。所以,这里给出的障碍物判断算法仅供参考,我们鼓励大家编写更高级的算法,实现更强大的功能。**
**实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_3中所做的工作**
```
$ git commit -a -m "my work on lab4_3 is done."
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@ -1,6 +1,8 @@
## 前言
本书的写作以及配套实验代码PKE见[riscv-pke](https://gitee.com/hustos/riscv-pke)的设计目标是给出一套采用代理内核Proxy Kernel思想在RISC-V平台上的一组由给定应用驱动的内核开发操作系统部分实验以及和操作系统相关联的软硬协同设计系统能力培养部分实验。通过完成本书所给出的操作系统部分系列实验读者能够建立《操作系统原理》课程中所学习到的概念对应的工程上的认识。进而通过完成本书所给出的系统能力培养部分系列实验读者能够进一步从工程角度对现代计算机的硬件系统结构和软件操作系统以及应用所构成的整体系统建立起较为完整的认识。
本书的写作以及配套实验代码的设计目标是给出一套在RISC-V平台上的一组由给定应用驱动的操作系统内核开发操作系统部分实验以及和操作系统相关联的软硬协同设计系统能力培养部分实验。通过完成本书所给出的操作系统部分系列实验读者能够建立与《操作系统原理》课程中所学习到的概念相对应的工程上的认识。进而通过完成本书所给出的系统能力培养部分系列实验读者能够从工程角度对现代计算机的硬件涉及《计算机组成原理》和《接口技术》等课程和底层软件涉及《操作系统原理》课程所构成的整体系统建立起较为完整的认识。
RISC-V是一套新兴的开放指令集系统采用RISC-V指令集设计的处理器由于没有指令集版权的羁绊正在成为处理器设计者的开发首选。我们的实验将围绕64位RISC-V机器支持RV64G指令集展开在操作系统实验部分读者将在一组给定应用的驱动下开发基于代理内核PKE见[riscv-pke](https://gitee.com/hustos/riscv-pke)思想的操作系统内核在系统能力培养实验部分我们在FPGA开发板PYNQ Z1上实际部署一个RISC-V处理器软核通过一组实验将处理器软核与外设PL端的马达、蓝牙进行互联继而扩展操作系统部分开发的代理内核通过一组实验使其能够控制连接到FPGA开发板上的设备包括PL端的马达、蓝牙以及PS端的设备如摄像头并在Arduino小车上最终让所有软、硬件代码都“跑”起来做完后的效果可以参考这个[视频](https://www.bilibili.com/video/BV1aL4y1A7RS/?vd_source=a17fc28107fa1bd622ace79a17bd6e54)。
代理内核Proxy Kernel是操作系统内核的一种但区别于传统的宏内核Monolithic kernel和微内核Microkernel它的实质是和主机“伴生”的操作系统其在计算机系统中的地位和逻辑结构如下图所示
@ -8,7 +10,7 @@
通过上图可以看到代理内核仅运行在目标机target支撑目标机上应用Application的执行。为了完成一些跟硬件相关的功能如显示、读取主机上的数据等代理内核可以通过HTIFHost-Target InterFace接口与主机上运行的前端服务器Front-end server进行通讯由后者完成所需的硬件功能。这样**代理内核的设计就可以无需自己去实现显示、访问数据这些与具体设备相关的琐碎的功能**。同时,**代理内核的设计目标从来都不是一个完整的操作系统**,而是只需要支撑所给定的应用的执行就好!这意味着随着应用的不同,代理内核的设计可以简单也可以复杂。
通过上图可以看到代理内核仅运行在目标机target支撑目标机上应用Application的执行。为了完成一些跟硬件相关的功能如显示、读取主机上的数据等代理内核可以通过HTIFHost-Target InterFace接口与主机上运行的前端服务器Front-end server进行通讯由后者完成所需的硬件功能。这样**代理内核的设计就可以无需自己去实现显示、访问数据这些与具体设备相关的琐碎的功能**。同时,**代理内核的设计目标从来都不是一个完整的操作系统,而是只需要支撑所给定的应用的执行就好**!这意味着随着应用的不同,代理内核的设计可以简单也可以复杂。
总结起来,代理内核具有以下特点:
@ -24,28 +26,26 @@
**代理内核在实现代码规模的极简化的同时,并未牺牲作为一个操作系统在概念和功能上的完整性**。这是因为虽然它的设计将I/O交给了HTIF和与主机进行的通讯代理内核仍然必须考虑对处理器、内存这些计算机“核心资产”的管理。同时在系统能力培养实验阶段代理内核还将管理连接到目标机target的定制化的设备的管理。所以代理内核的设计极大地保留了操作系统的完整性。
- **实用性**
**代理内核实际上是用于验证所开发的处理器的重要手段**。例如某公司开发了一款采用RISC-V指令集的处理器为了验证该处理器是否满足它所面向的应用场景就可以采用FPGA开发板如我们采用的PYNQ-Z1将所开发的处理器以软核的形式部署在开发板的PL端以类似于本书中系统能力培养实验的方法在所开发的处理器软核上部署代理内核和应用以检验系统整体的完整性。
从实验的构成来看PKE共开发了四组实验。其中前三组实验是为《操作系统原理》课程开发的配套实验这三组实验分别涵盖了中断、内存管理以及进程调度等操作系统原理的核心内容。PKE的第四组实验面向的是《系统能力培养》课程该实验将发挥代理内核的实用性特点在搭载了RISC-V处理器核的FPGA开发板我们采用的是PYNQ Z1实现所开发操作系统内核的部署以及软硬协同设计从而实现如[视频](https://www.bilibili.com/video/BV1aL4y1A7RS/?vd_source=a17fc28107fa1bd622ace79a17bd6e54)所示的采用蓝牙进行操控的Arduino小车。本书采用代理内核的思想来构建《操作系统原理》课程的实验内容并以此为基础进而构造《系统能力培养》课程的实验内容
本书所设计的实验分为两大部分分别用于满足《操作系统原理》课程和《系统能力培养》课程的教学需要。其中第一部分包含了PKE的前三组实验分别涵盖了中断、内存管理以及进程调度等《操作系统原理》课程的核心内容。第二部分采用软硬协同设计的办法构建《系统能力培养》课程实验硬件部分指导读者在FPGA开发板上部署RISC-V处理器核并完成的三个基础硬件实验UART支持、中断支持和蓝牙小车集成软件部分则包含与硬件部分配合的第四组PKE实验涵盖设备管理和文件访问。无论是操作系统部分还是系统能力培养部分每组PKE实验又包含了3个基础实验与一些挑战实验基础实验实现操作系统的基础功能挑战实验部分在基础实验的基础上扩展系统的功能使得系统能够处理更复杂的问题
从工程设计的角度来看,课程实验内容的设计具有以下特点:
从工程设计的角度来看,本书所设计的课程实验内容具有以下特点:
- **循序渐进、突出阶段性重点**
采用代理内核的概念设计的**操作系统实验就能够做到将重点放在处理器、内存这些核心资产的管理上**,同时,在**其扩展阶段系统能力培养实验再将重点转移到I/O设备上**,从而形成一个相对完整的计算机系统。
- **基础和挑战并存**
**在PKE的每组实验中我们将设计基础实验和挑战实验两个部分**。读者在完成基础试验后,可以选择自己感兴趣的挑战实验进一步加深对所学知识的理解。同时,也可以**发挥自己的想象力**,“发明”新的挑战实验,以获得更大的满足感(和更高的成绩)。
采用代理内核的概念设计的**操作系统实验就能够做到将重点放在处理器、内存这些核心资产的管理上**,同时,在**其扩展阶段系统能力培养实验将重点转移到I/O设备上**,从而培养学生对作为整体的计算机系统的认识。
- **教学性和实用性并存**
本书中代理内核的设计,**面向的运行环境为采用精简指令集RISC-V的64位机器支持RV64G**。通过操作系统和系统能力培养部分实际系统的设计读者能够在64位今天广为采用且将来不大可能过时的基础上了解RISC-V指令集以及采用该指令集的计算机设计相关的知识。同时由于代理内核本身有其实际工程意义读者能够在实验的基础上自己“发明”更有趣的计算机系统并对其进行验证。
- **基础和挑战并存**
**在PKE的每组实验中我们将设计基础实验和挑战实验两个部分**。读者在完成基础试验后,可以选择自己感兴趣的挑战实验进一步加深对所学知识的理解。同时,也可以**发挥自己的想象力**,“发明”新的挑战实验,以获得更大的满足感(和更高的成绩)。
完成操作系统部分的实验读者需要具备以下知识C语言熟练掌握、汇编语言了解基础知识、操作系统原理了解基础知识、计算机系统结构了解基础知识、Linux的使用了解完成系统能力培养部分的实验读者还需要具备以下知识Verilog程序设计语言了解基础知识、FPGA开发了解基础知识、计算机接口技术了解基础知识
本书的组织结构如下我们将在第一章介绍RISC-V计算机的体系结构的相关知识第二章介绍实验环境的安装和使用第三章到第六章为操作系统实验部分分别设计了中断、内存管理和进程管理三个操作系统原理基础知识点的相关实验第七章为系统能力培养部分实验通过软硬协同的设计实现一个真实可用的能够响应蓝牙控制信号的Arduino小车。
完成操作系统部分的实验读者需要具备以下知识C语言熟练掌握、汇编语言了解基础知识、操作系统原理了解基础知识、计算机系统结构了解基础知识、Linux的使用了解完成系统能力培养部分的实验读者还需要具备以下知识Verilog程序设计语言熟练掌握、FPGA开发了解基础知识、计算机接口技术了解基础知识

Loading…
Cancel
Save