[南大ICS-PA2] 输入输出
输入输出
设备与CPU
设备也有自己的状态寄存器(相当于CPU的寄存器),也有自己的功能部件(相当于CPU的运算器)。
在程序看来, 访问设备 = 读出数据 + 写入数据 + 控制状态。
那么在CPU看来, 这些行为究竟意味着什么呢? 具体要从哪里读数据? 把数据写入到哪里? 如何查询/设置设备的状态? 一个最本质的问题是, CPU和设备之间的接口, 究竟是什么?
既然设备也有寄存器, 一种最简单的方法就是把设备的寄存器作为接口, 让CPU来访问这些寄存器。
端口I/O
一种I/O编址方式是端口映射I/O(port-mapped I/O), CPU使用专门的I/O指令对设备进行访问, 并把设备的地址称作端口号。
事实上, 设备的API及其行为都会在相应的文档里面有清晰的定义, 在PA中我们无需了解这些细节, 只需要知道, 驱动开发者可以通过RTFM, 来编写相应程序来访问设备即可。
内存映射I/O
内存映射I/O这种编址方式非常巧妙, 它是通过不同的物理内存地址给设备编址的. 这种编址方式将一部分物理内存的访问"重定向"到I/O地址空间中, CPU尝试访问这部分物理内存的时候, 实际上最终是访问了相应的I/O设备, CPU却浑然不知。
内存映射I/O的编程模型和普通的编程完全一样: 程序员可以直接把I/O设备当做内存来访问。
理解volatile关键字
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
主要是避免其他位置改变了变量的值,而在本代码中,不认为它改变了,所以一直保存在寄存器中,没有去内存中读或者写入内存。
NEMU中的输入输出
NEMU的框架代码已经在nemu/src/device/
目录下提供了设备相关的代码,
映射和I/O方式
设备
将输入输出抽象成IOE
IOE(I/O Extension)
访问设备 = 读出数据 + 写入数据 + 控制状态 = 读/写操作
串口
实现printf
static char sprint_buf[1024];
/*可变函数在内部实现的过程中是从右向左压入堆栈,从而保证了可变参数的第一个参数始终位于栈顶*/
int printf(const char *fmt, ...)//可以有一个或多个固定参数
{
va_list args; //用于存放参数列表的数据结构
int n;
/*根据最后一个fmt来初始化参数列表,至于为什么是最后一个参数,是与va_start有关。*/
va_start(args, fmt);
n = vsprintf(sprint_buf, fmt, args);
va_end(args);//执行清理参数列表的工作
putstr(sprint_buf);
return n;
}
修改am-kernels/kernels/hello/hello.c
,加入头文件klib.h
,加入语句printf("Hello world!\n");
,成功输出"Hello world!"。
待解决的问题
如果向sprint_buf写入的超过1024怎么办?
时钟
实现IOE
在abstract-machine/am/src/platform/nemu/ioe/timer.c
中实现AM_TIMER_UPTIME
的功能. 在abstract-machine/am/src/platform/nemu/include/nemu.h
和 abstract-machine/am/src/$ISA/$ISA.h
中有一些输入输出相关的代码供你使用.
实现后, 在$ISA-nemu
中运行am-kernel/tests/am-tests
中的real-time clock test
测试. 如果你的实现正确, 你将会看到程序每隔1秒往终端输出一行信息. 由于我们没有实现AM_TIMER_RTC
, 测试总是输出1900年0月0日0时0分0秒, 这属于正常行为, 可以忽略.
void __am_timer_init() {
outl(RTC_ADDR, 0);
outl(RTC_ADDR + 4, 0);
}
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
uptime->us = inl(RTC_ADDR + 4);
uptime->us <<= 32;
uptime->us += inl(RTC_ADDR);
}
测试运行命令:make ARCH=riscv32-nemu run mainargs=t
看看NEMU跑多快
在测试microbench的test时,又补充了一个指令的实现
// mulhu rd, rs1, rs2 x[rd] = (x[rs1] 𝑢 ×𝑢 x[rs2]) ≫𝑢 XLEN
INSTPAT("0000001 ????? ????? 011 ????? 01100 11", mulhu , R, R(dest) = ((uint64_t)src1 * (uint64_t)src2) >> 32);
设备访问的踪迹 - dtrace
键盘
实现IOE(2)
在abstract-machine/am/src/platform/nemu/ioe/input.c
中实现AM_INPUT_KEYBRD
的功能. 实现后, 在$ISA-nemu
中运行am-tests
中的readkey test
测试. 如果你的实现正确, 在程序运行时弹出的新窗口中按下按键, 你将会看到程序输出相应的按键信息, 包括按键名, 键盘码, 以及按键状态.
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
int k = AM_KEY_NONE;
k = inl(KBD_ADDR);
kbd->keydown = (k & KEYDOWN_MASK ? true : false);
kbd->keycode = k & ~KEYDOWN_MASK;
}
VGA
事实上, VGA设备还有两个寄存器: 屏幕大小寄存器和同步寄存器. 我们在讲义中并未介绍它们, 我们把它们作为相应的练习留给大家. 具体地, 屏幕大小寄存器的硬件(NEMU)功能已经实现, 但软件(AM)还没有去使用它; 而对于同步寄存器则相反, 软件(AM)已经实现了同步屏幕的功能, 但硬件(NEMU)尚未添加相应的支持.
-
AM_GPU_CONFIG
, AM显示控制器信息, 可读出屏幕大小信息width
和height
-
AM_GPU_FBDRAW
, AM帧缓冲控制器, 可写入绘图信息, 向屏幕(x, y)
坐标处绘制w*h
的矩形图像. 图像像素按行优先方式存储在pixels
中, 每个像素用32位整数以00RRGGBB
的方式描述颜色. 若sync
为true
, 则马上将帧缓冲中的内容同步到屏幕上. -
屏幕大小寄存器在硬件中已经实现,在
nemu/src/device/vga.c/void init_vga()
中
vgactl_port_base[0] = (screen_width() << 16) | screen_height();
add_mmio_map("vgactl", CONFIG_VGA_CTL_MMIO, vgactl_port_base, 8, NULL);
-
同步寄存器,要在硬件中实现,即实现当同步寄存器为非零时,调用
update_screen()
,然后将同步寄存器清零。void vga_update_screen() { // TODO: call `update_screen()` when the sync register is non-zero, // then zero out the sync register #ifdef CONFIG_HAS_PORT_IO uint32_t sync = mmio_read(CONFIG_VGA_CTL_MMIO + 4, 4); if (sync != 0) { update_screen(); } else { mmio_write(CONFIG_VGA_CTL_MMIO + 4, 4, 0); } }
-
要修改
void __am_gpu_config(AM_GPU_CONFIG_T *cfg)
,不然测试中,总读到行列值为0void __am_gpu_config(AM_GPU_CONFIG_T *cfg) { uint32_t screen_size = inl(VGACTL_ADDR); int width = (screen_size >> 16) & 0xffff; // TODO: get the correct width int height = screen_size & 0xffff; // TODO: get the correct height *cfg = (AM_GPU_CONFIG_T) { .present = true, .has_accel = false, .width = width, .height = height, // 原本这里是0,导致错误 .vmemsz = 0 }; }