输入输出
在真实的计算机中, 输入输出都是通过访问I/O设备来完成的。我们希望计算机能够控制设备, 让设备做我们想要做的事情, 这一重任毫无悬念地落到了CPU身上. CPU除了进行计算之外, 还需要访问设备, 与其协作来完成不同的任务. 那么在CPU看来, 这些行为究竟意味着什么呢? 具体要从哪里读数据? 把数据写入到哪里? 如何查询/设置设备的状态? 一个最本质的问题是, CPU和设备之间的接口, 究竟是什么?
设备通过寄存器实现对设备的控制,CPU通过端口号实现端口映射后,能够对设备寄存器进行访问
端口映射I/O
在设计I/O指令时,我们已经确定了I/O的地址空间,不方便拓展
内存映射
内存映射I/O这种编址方式非常巧妙, 它是通过不同的物理内存地址给设备编址的. 这种编址方式将一部分物理内存的访问"重定向"到I/O地址空间中, CPU尝试访问这部分物理内存的时候, 实际上最终是访问了相应的I/O设备, CPU却浑然不知.
缺点:
CPU无法通过正常渠道直接访问那些被映射到I/O地址空间的物理内存了 (被用来做映射 实现折别的输入输出)
举例,时钟:
src/device/timer.c
void init_timer() {
rtc_port_base = (uint32_t *)new_space(8);
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("rtc", CONFIG_RTC_PORT, rtc_port_base, 8, rtc_io_handler);
#else
add_mmio_map("rtc", CONFIG_RTC_MMIO, rtc_port_base, 8, rtc_io_handler);
#endif
IFNDEF(CONFIG_TARGET_AM, add_alarm_handle(timer_intr));
}
在时钟进行初始化时,通过add_mmio_map实现了内存映射,将0xa0000048 8个字节映射为rtc_port_base,由于此时rtc_port_base为两个四字节的数组组成如下:
static uint32_t *rtc_port_base = NULL;
rtc_port_base[0] = (uint32_t)us;
rtc_port_base[1] = us >> 32;
所以在内存中需要读取两次然后进行拼接实现对时间的读取,并且每次在进行map_read时会调用rtc_io_handler,map_read如下所示:
word_t map_read(paddr_t addr, int len, IOMap *map) {
assert(len >= 1 && len <= 8);
check_bound(map, addr);
paddr_t offset = addr - map->low;
invoke_callback(map->callback, offset, len, false); // prepare data to read
word_t ret = host_read(map->space + offset, len);
return ret;
}
invoke_callback(map->callback, offset, len, false); 实际就是在调用rtc_io_handler,该函数为:
static void invoke_callback(io_callback_t c, paddr_t offset, int len, bool is_write) {
if (c != NULL) { c(offset, len, is_write); }
}
那么此时的时间被刷新,所以每次进行timer读取的时候都会刷新时间,
所以在__am_timer_uptime中我们只需要对指定地址的值进行读取 与 拼接就能够得到时间。
ioe.c 文件解析
typedef void (*handler_t)(void *buf);定义函数指针类型,该类型输出为void,输入为void *类型
创建函数指针数组如下:
static void *lut[128] = {
[AM_TIMER_CONFIG] = __am_timer_config,
[AM_TIMER_RTC ] = __am_timer_rtc,
[AM_TIMER_UPTIME] = __am_timer_uptime,
[AM_INPUT_CONFIG] = __am_input_config,
[AM_INPUT_KEYBRD] = __am_input_keybrd,
[AM_GPU_CONFIG ] = __am_gpu_config,
[AM_GPU_FBDRAW ] = __am_gpu_fbdraw,
[AM_GPU_STATUS ] = __am_gpu_status,
[AM_UART_CONFIG ] = __am_uart_config,
[AM_AUDIO_CONFIG] = __am_audio_config,
[AM_AUDIO_CTRL ] = __am_audio_ctrl,
[AM_AUDIO_STATUS] = __am_audio_status,
[AM_AUDIO_PLAY ] = __am_audio_play,
[AM_DISK_CONFIG ] = __am_disk_config,
[AM_DISK_STATUS ] = __am_disk_status,
[AM_DISK_BLKIO ] = __am_disk_blkio,
[AM_NET_CONFIG ] = __am_net_config,
};
上述数组中 AM_TIMER_CONFIG、AM_TIMER_RTC 在amdev.h中定义,定义的方法如下:
#define AM_DEVREG(id, reg, perm, ...) \
enum { AM_##reg = (id) }; \
typedef struct { __VA_ARGS__; } AM_##reg##_T;
AM_DEVREG( 1, UART_CONFIG, RD, bool present);
AM_DEVREG( 2, UART_TX, WR, char data);
AM_DEVREG( 3, UART_RX, RD, char data);
AM_DEVREG( 4, TIMER_CONFIG, RD, bool present, has_rtc);
AM_DEVREG( 5, TIMER_RTC, RD, int year, month, day, hour, minute, second);
AM_DEVREG( 6, TIMER_UPTIME, RD, uint64_t us);
AM_DEVREG( 7, INPUT_CONFIG, RD, bool present);
AM_DEVREG( 8, INPUT_KEYBRD, RD, bool keydown; int keycode);
AM_DEVREG( 9, GPU_CONFIG, RD, bool present, has_accel; int width, height, vmemsz);
AM_DEVREG(10, GPU_STATUS, RD, bool ready);
AM_DEVREG(11, GPU_FBDRAW, WR, int x, y; void *pixels; int w, h; bool sync);
AM_DEVREG(12, GPU_MEMCPY, WR, uint32_t dest; void *src; int size);
AM_DEVREG(13, GPU_RENDER, WR, uint32_t root);
AM_DEVREG(14, AUDIO_CONFIG, RD, bool present; int bufsize);
AM_DEVREG(15, AUDIO_CTRL, WR, int freq, channels, samples);
AM_DEVREG(16, AUDIO_STATUS, RD, int count);
AM_DEVREG(17, AUDIO_PLAY, WR, Area buf);
AM_DEVREG(18, DISK_CONFIG, RD, bool present; int blksz, blkcnt);
AM_DEVREG(19, DISK_STATUS, RD, bool ready);
AM_DEVREG(20, DISK_BLKIO, WR, bool write; void *buf; int blkno, blkcnt);
AM_DEVREG(21, NET_CONFIG, RD, bool present);
AM_DEVREG(22, NET_STATUS, RD, int rx_len, tx_len);
AM_DEVREG(23, NET_TX, WR, Area buf);
AM_DEVREG(24, NET_RX, WR, Area buf);
上述宏中,实现AM_UART_CONFIG = 1,AM_UART_TX = 2 依次类推,创建结构体AM_UART_CONFIG_T\AM_UART_TX_T,依次类推。
回到ioe,c中,ioe_init()函数如下:
bool ioe_init() {
for (int i = 0; i < LENGTH(lut); i++)
if (!lut[i]) lut[i] = fail;
__am_gpu_init();
__am_timer_init();
__am_audio_init();
return true;
}
if (!lut[i]) lut[i] = fail; 表示如果某个函数不存在或未声明则在调用的时候出错,并对指定的初始化,gpu\timer\audio
time-test中是如何实现时间的调用的,test的函数如下所示:
#include <amtest.h>
void rtc_test() {
AM_TIMER_RTC_T rtc;
int sec = 1;
while (1) {
while(io_read(AM_TIMER_UPTIME).us / 1000000 < sec) ;
rtc = io_read(AM_TIMER_RTC);
printf("%d-%d-%d %02d:%02d:%02d GMT (", rtc.year, rtc.month, rtc.day, rtc.hour, rtc.minute, rtc.second);
if (sec == 1) {
printf("%d second).\n", sec);
} else {
printf("%d seconds).\n", sec);
}
sec ++;
}
}
此时调用了 io_read
io_read是什么时候实现的呢?
RTFSC,klib中提供了io_read()和io_write()这两个宏.
#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })
ioe_read()的函数如下:
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }
假设此时我们读取uptime,实际上应该有如下代码:
io_read(AM_TIMER_UPTIME);
上述代码首先创建了 AM_TIMER_UPTIME __io_param;
创建了一个__io_param实例,并执行对应的函数此时为,__am_timer_uptime(__io_param),在io_read中返回值为__io_param,所以此时我们得到了更新后的__io_param。 很绕需要多思考 自己推导一下
键盘
键盘是最基本的输入设备. 一般键盘的工作方式如下: 当按下一个键的时候, 键盘将会发送该键的通码(make code); 当释放一个键的时候, 键盘将会发送该键的断码(break code).
此时我们需要学习nemu中键盘是如何模拟的,复盘 keyboard.c的源码如下:
/***************************************************************************************
* Copyright (c) 2014-2022 Zihao Yu, Nanjing University
*
* NEMU is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*
* See the Mulan PSL v2 for more details.
***************************************************************************************/
#include <device/map.h>
#include <utils.h>
#define KEYDOWN_MASK 0x8000
#ifndef CONFIG_TARGET_AM
#include <SDL2/SDL.h>
// Note that this is not the standard
#define _KEYS(f) \
f(ESCAPE) f(F1) f(F2) f(F3) f(F4) f(F5) f(F6) f(F7) f(F8) f(F9) f(F10) f(F11) f(F12) \
f(GRAVE) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(0) f(MINUS) f(EQUALS) f(BACKSPACE) \
f(TAB) f(Q) f(W) f(E) f(R) f(T) f(Y) f(U) f(I) f(O) f(P) f(LEFTBRACKET) f(RIGHTBRACKET) f(BACKSLASH) \
f(CAPSLOCK) f(A) f(S) f(D) f(F) f(G) f(H) f(J) f(K) f(L) f(SEMICOLON) f(APOSTROPHE) f(RETURN) \
f(LSHIFT) f(Z) f(X) f(C) f(V) f(B) f(N) f(M) f(COMMA) f(PERIOD) f(SLASH) f(RSHIFT) \
f(LCTRL) f(APPLICATION) f(LALT) f(SPACE) f(RALT) f(RCTRL) \
f(UP) f(DOWN) f(LEFT) f(RIGHT) f(INSERT) f(DELETE) f(HOME) f(END) f(PAGEUP) f(PAGEDOWN)
#define _KEY_NAME(k) _KEY_##k,
// It's conflicted on macos with sys/_types/_key_t.h
#ifdef __APPLE__
#undef _KEY_T
#endif
enum {
_KEY_NONE = 0,
MAP(_KEYS, _KEY_NAME)
};
#define SDL_KEYMAP(k) keymap[concat(SDL_SCANCODE_, k)] = concat(_KEY_, k);
static uint32_t keymap[256] = {};
static void init_keymap() {
MAP(_KEYS, SDL_KEYMAP)
}
#define KEY_QUEUE_LEN 1024
static int key_queue[KEY_QUEUE_LEN] = {};
static int key_f = 0, key_r = 0;
static void key_enqueue(uint32_t am_scancode) {
key_queue[key_r] = am_scancode;
key_r = (key_r + 1) % KEY_QUEUE_LEN;
Assert(key_r != key_f, "key queue overflow!");
}
static uint32_t key_dequeue() {
uint32_t key = _KEY_NONE;
if (key_f != key_r) {
key = key_queue[key_f];
key_f = (key_f + 1) % KEY_QUEUE_LEN;
}
return key;
}
void send_key(uint8_t scancode, bool is_keydown) {
if (nemu_state.state == NEMU_RUNNING && keymap[scancode] != _KEY_NONE) {
uint32_t am_scancode = keymap[scancode] | (is_keydown ? KEYDOWN_MASK : 0);
key_enqueue(am_scancode);
}
}
#else // !CONFIG_TARGET_AM
#define _KEY_NONE 0
static uint32_t key_dequeue() {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
uint32_t am_scancode = ev.keycode | (ev.keydown ? KEYDOWN_MASK : 0);
return am_scancode;
}
#endif
static uint32_t *i8042_data_port_base = NULL;
static void i8042_data_io_handler(uint32_t offset, int len, bool is_write) {
assert(!is_write);
assert(offset == 0);
i8042_data_port_base[0] = key_dequeue();
}
void init_i8042() {
i8042_data_port_base = (uint32_t *)new_space(4);
i8042_data_port_base[0] = _KEY_NONE;
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("keyboard", CONFIG_I8042_DATA_PORT, i8042_data_port_base, 4, i8042_data_io_handler);
#else
add_mmio_map("keyboard", CONFIG_I8042_DATA_MMIO, i8042_data_port_base, 4, i8042_data_io_handler);
#endif
IFNDEF(CONFIG_TARGET_AM, init_keymap());
}
如何理解下面的宏定义:
#define _KEYS(f) \
f(ESCAPE) f(F1) f(F2) f(F3) f(F4) f(F5) f(F6) f(F7) f(F8) f(F9) f(F10) f(F11) f(F12) \
f(GRAVE) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(0) f(MINUS) f(EQUALS) f(BACKSPACE) \
f(TAB) f(Q) f(W) f(E) f(R) f(T) f(Y) f(U) f(I) f(O) f(P) f(LEFTBRACKET) f(RIGHTBRACKET) f(BACKSLASH) \
f(CAPSLOCK) f(A) f(S) f(D) f(F) f(G) f(H) f(J) f(K) f(L) f(SEMICOLON) f(APOSTROPHE) f(RETURN) \
f(LSHIFT) f(Z) f(X) f(C) f(V) f(B) f(N) f(M) f(COMMA) f(PERIOD) f(SLASH) f(RSHIFT) \
f(LCTRL) f(APPLICATION) f(LALT) f(SPACE) f(RALT) f(RCTRL) \
f(UP) f(DOWN) f(LEFT) f(RIGHT) f(INSERT) f(DELETE) f(HOME) f(END) f(PAGEUP) f(PAGEDOWN)
#define _KEY_NAME(k) _KEY_##k,
首先我们要知道MAP宏定义是如何实现的:在源码macro.h中实现
#define MAP(c, f) c(f)
那么实现后的_KEYS实际上就是:
_KEY_ESCAPE _KEY_F1 _KEY_F2 _KEY_F3 等等并且为美剧类型,_KEY_ESCAPE为起始值为1 KEY_NONE为0
第二个宏定义:
SDL_KEYMAP(k) keymap[concat(SDL_SCANCODE_, k)] = concat(_KEY_, k);
static void init_keymap() {
MAP(_KEYS, SDL_KEYMAP)
}
表示的是什么呢?
同理,在macro.h中存在如下宏定义:
#define concat_temp(x, y) x ## y
实际上就是,key_map[SDL_SCANCODE_ESCAPE] = _KEY_ESCAPE , 那么此时就实现了. 随后和上述相同的分析方法,此时我们知道 在进行io_read时,实际上就是在调用i8042_data_io_handler,并且返回变化的结构体,该结构体中存储的实际上就是i8042_data_port_base[0],这个值是哪里来的呢?
来自于如下函数:
static uint32_t key_dequeue() {
uint32_t key = _KEY_NONE;
if (key_f != key_r) {
key = key_queue[key_f];
key_f = (key_f + 1) % KEY_QUEUE_LEN;
}
return key;
}
上述函数的意思如下:如果两次读取到的值不相同那么这个时候执行key = key_queue[key_f];,并返回,那么我们要知道key_queue是怎么定义的,有如下函数:
static void key_enqueue(uint32_t am_scancode) {
key_queue[key_r] = am_scancode;
key_r = (key_r + 1) % KEY_QUEUE_LEN;
Assert(key_r != key_f, "key queue overflow!");
}
那么am_cancode是什么呢?
如下函数:
void send_key(uint8_t scancode, bool is_keydown) {
if (nemu_state.state == NEMU_RUNNING && keymap[scancode] != _KEY_NONE) {
uint32_t am_scancode = keymap[scancode] | (is_keydown ? KEYDOWN_MASK : 0);
key_enqueue(am_scancode);
}
}
上述函数的意思是:在NEMU处于执行状态且扫描的键值不为0,那么此时扫描的键值为,当按下的时候,让键值与掩码实现或操作得到最后的键值
所以在实现IOE时需要进行如下操作:
#include <am.h>
#include <nemu.h>
#define KEYDOWN_MASK 0x8000
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
uint32_t key = inl(KBD_ADDR);
if(key != AM_KEY_NONE) {
kbd->keycode = ~KEYDOWN_MASK & key;
kbd->keydown = true;
} else {
kbd->keycode = AM_KEY_NONE;
kbd->keydown = false;
}
}
此时读取keycode 需要解码,将按下的值去掉,得到按键的真实值
VGA
VGA可以用于显示颜色像素, 是最常用的输出设备. nemu/src/device/vga.c模拟了VGA的功能. VGA初始化时注册了从0xa1000000开始的一段用于映射到video memory(显存, 也叫frame buffer, 帧缓冲)的MMIO空间. 代码只模拟了400x300x32的图形模式, 一个像素占32个bit的存储空间, R(red), G(green), B(blue), A(alpha)各占8 bit, 其中VGA不使用alpha的信息. 如果你对VGA编程感兴趣, 这里有一个名为FreeVGA的项目, 里面提供了很多VGA的相关资料.
abstract-machine/am/include/amdev.h中为GPU定义了五个抽象寄存器, 在NEMU中只会用到其中的两个:
- AM_GPU_CONFIG, AM显示控制器信息, 可读出屏幕大小信息width和height. 另外AM假设系统在运行过程中,
屏幕大小不会发生变化. - AM_GPU_FBDRAW, AM帧缓冲控制器, 可写入绘图信息, 向屏幕(x, y)坐标处绘制w*h的矩形图像.
图像像素按行优先方式存储在pixels中, 每个像素用32位整数以00RRGGBB的方式描述颜色. 若sync为true,
则马上将帧缓冲中的内容同步到屏幕上.
同理先分析nemu中的文件:
vgactl_port_base = (uint32_t *)new_space(8);
vgactl_port_base[0] = (screen_width() << 16) | screen_height();
根据这行代码我们知道 vgactl开辟了一个两个四字节空间,一个四字节空间用来存储两个十六位的值放置屏幕的宽与高, 另外一个四字节表示颜色 在vga_update_screen中定义
FBDRAW寄存器对FB_ADDR进行赋值,就可以进行帧读取,在VAGCTL_ADDR+4处进行输出,在nemu中进行判断该位置是否为1,进行刷新屏幕:
void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
uint32_t* fb = (uint32_t*)(uintptr_t)FB_ADDR;
uint32_t* pixels = (uint32_t*)(ctl->pixels);
int x = ctl->x, y = ctl->y;
int w = ctl->w, h = ctl->h;
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
fb[(y + j) * W + (x + i)] = *(pixels + j * w + i);
}
}
if (ctl->sync) {
outl(SYNC_ADDR, 1);
}
}