作者:南京航空航天大学 计算机科学与技术学院 金航
未经作者允许,禁止转载
我们到现在的NEMU,只能进行计算,无法进行任何和外界的交互,因为不具有输入输出功能。
本节的任务:加入I/O设备以及I/O指令。
每个设备都位于微处理器的某个(或多个)端口上,微处理器通过这个端口和设备进行交互。
控制设备运行:由微处理器通过OUT指令
将数据送到设备的端口上,然后设备送该端口读走数据,得知CPU想让自己做什么事情。
反馈设备信息:设备总是将处理好的信息放到端口上,然后CPU通过IN指令
从该端口处读入数据。
由此可知,x86提供了一组专门用于读写外部设备的指令:IN
和OUT
。
如例子给出的代码:
movl $0x0, %al
movl $0x3f9, %edx // 0x3f9串口地址
outb %al, (%dx)
即是直接将数据,即AL寄存器的内容
,送到0x3f9号端口
上。这种编址方式诞生于8086时期,那个时候的许多设备,比如早期的打印机,以及早期8086机的若干周边控制器设备(如8254,8259)等都是直接使用IN
和OUT
指令直接编程控制的(现在有了驱动程序,由驱动程序控制外设)。
即将内存地址空间与I/O地址空间相关联
,最典型的例子就是我们的显存
。这种情况下,我们仅需要用正常的内存读写指令对内存进行操作,即在不知不觉中完成了对外部设备的操作(如绘图)。这种编程方式深受程序开发者喜爱。
我们知道,NEMU的实质是模拟的计算机硬件,那么在这个层面上,外部设备理所当然是连接在硬件上的。因此,NEMU的框架中已包含好了同样是被用代码模拟好的硬件。位于nemu/src/device
目录下。包括:串口、时钟、键盘、VGA
。
在nemu/src/device/io/port-io.c
中有对端口I/O的模拟,请自行阅读。需要注意的有:
- 结构体
PIO_t
,里面记录着端口I/O的映射关系,其实例化的结构体数组为maps[]
,指示当前有多少个映射关系的变量为nr_map
。 add_pio_map()
函数用于注册一个映射关系,其参数的意义依次为:端口映射关系的首地址
、注册的端口号个数(连续的)
、回调函数(即对该设备读写的时候调用的函数)
pio_read()
函数用于CPU微处理器从设备端口读取数据。pio_write()
函数用于CPU微处理器向设备端口写入数据。pio_space
数组用于存放端口上的数据。
nemu/src/device/device.c
是NEMU整个设备模块的核心。通过SDL库模拟计算机的标准输入输出(还记得SDL库吗?在PA0末尾我们首次编译NEMU的时候是不是很多人没有安装SDL组件?)需要注意的有:
init_device()
函数用于对设备进行初始化,因此需要在初始化NEMU的阶段调用。device_update()
负责设备的模拟操作,是整个NEMU控制设备的核心函数,该函数在cpu_exec()
中的每趟循环都被调用一次,以刷新设备。
稍后再来谈nemu/src/device/io/mmio.c
是对内存I/O的模拟,我们在下面讲VGA和显存的时候再讲。
- 认真读讲义 & 看Talk & 读框架代码 & 读近期问题汇总
- 定义宏
HAS_DEVICE
- 调用
init_device()
对设备进行初始化
- 输出的原理:通过串口输出,串口的实现见
nemu\src\device\serial.c
- 实现
IN
和OUT
两条指令(在nemu\src\cpu\exec\system.c
中) - 通过正常执行
_putc()
函数(该函数定义在trm.c
中)成功运行nexus-am\apps\hello
项目(记得先定义宏HAS_SERIAL
,然后再在该路径下使用make run
)
- 理解原理:
nemu/src/device/timer.c
中模拟好了时钟的功能,当CPU需要读取时即把当前的时间放在端口上等待CPU取走
(这里和真实机器的情况不同,真是机器是随时都把数据放到端口上,并不是要读的时候才计算并放置)。 - 我们需要在
nexus-am\am\arch\x86-nemu\src\ioe.c
实现_uptime()
函数,原始的样子:
unsigned long _uptime() {
return 0;
}
- 提示:把
0
修改为返回系统启动后经过的毫秒数
即为正确实现,思考一下:什么和什么做差就是系统启动后的毫秒数呢?系统启动的时间又记录在哪里呢?答案就在ioe.c
中,请自行思考。 - 成功运行
timetest
项目,让其每秒输出一句话。 - 成功运行跑分项目
dhrystone
,coremark
和microbench
。
- 理解键盘的
键盘码
,通码
和断码
的概念。 - 仔细阅读讲义关于NEMU中键盘设备的工作原理
- 理解NEMU中对键盘设备的实现(在``中)
- 实现
_read_key()
函数,框架已给出,在ioe.c
中:
int _read_key() {
return _KEY_NONE;
}
- 提示:最少三条语句就可以实现。参考操作:检查键盘状态寄存器状态,是否有按键按下;若没有,返回NONE;若有,从键盘的数据寄存器读取并返回键盘码。
- 成功运行测试项目
keytest
。
nemu/src/device/io/mmio.c
文件里面是对内存I/O的模拟,请自行阅读。需要注意的有:
- 函数
add_mmio_map()
在vga.c
中进行初始化的时候被调用,用于申请一段显存地址空间。 mmio_read()
和mmio_write()
是给CPU拿来访问内存I/O的空间使用的,需要在memory.c
的paddr_read()
和paddr_wirte()
函数中,先对要访问的地址进行判断(提示:使用is_mmio()
函数进行判断),然后再决定是否对应调用这两个函数进行显存I/O的访问。- 实现
_draw_rect()
函数,该函数用来画矩形。 - 成功运行测试项目
videotest
。
- 首先了解概念:我们通过在
fb指针上取偏移
找到该显存地址。 - 设置一个
指针p
指向fb内存空间
。 - 首先让
p
指向坐标(x,y)处。 - 写一个循环,我们按行来写显存数据,即
遍历h趟
(当然,有兴趣和耐心的同学可以考虑按列来写)。 - 每一趟我们把一行长度的显存数据复制到显存中对应区域。
- 注意时刻判断不要让你的x坐标或y坐标指向了屏幕大小以外的区域。
- 温馨提示:框架中带的那个画矩形函数中原有的内容可以全删了,然后再开始写。
打字小游戏位于nexus-am/apps/typing
目录,使用命令
make ARCH=x86-nemu run
来运行吧!
在nexus-am\am\arch\x86-nemu\include\x86.h
中提供了inb()
,inl()
,outb()
,outl()
四个函数帮大家通过函数的形式用内联汇编
封装好了IN
和OUT
指令,直接调用他们吧(他们在编译时会被编译成IN
或OUT
指令后给CPU进行执行指令)!