一生一芯学习:PA2:输入输出
输入输出是计算机与外界交互的基本手段,只需要向设备发送一些有意义的数字信号,设备就会按照这些信号来工作。设备有自己的专属寄存器(如CPU的通用寄存器),也有自己的功能部件(如CPU的ALU)。以键盘外设为例,键盘有一个把按键的模拟信号转换成扫描码的部件,然后CPU根据扫描码就知道真实世界的用户按下了键盘的哪个按键了。除了纯粹的数据读写之外,我们还需要对设备进行控制,比如查看键盘是否有按键被按下。现在有个问题,CPU是如何访问设备的寄存器呢?答案是MMIO(內存映射I/O)。
MMIO我个人理解就是把内存中一段固定的地址作为访问寄存器的接口,需要有控制判断访问的是否是这段地址,是的话就等价于访问对应的IO。这样的话CPU就可以通过普通的访存指令来访问设备。
在map.h
中定义了设备映射的结构体
typedef struct {const char *name; // 设备名称paddr_t low; // 映射区起始地址paddr_t high; // 映射区结束地址void *space; // 设备实际存储空间指针io_callback_t callback; // 设备回调函数
} IOMap;
在map.c
中,实现了映射的管理,包括I/O空间的分配和映射,还有映射的访问接口。
在源码中定义了这样两边静态变量。
static uint8_t *io_space = NULL;
static uint8_t *p_space = NULL;
其中里面的io_space
是指向整个IO设备映射空间的起始地址。
p_space
是指向当前可分配空间的位置,每次分配设备空间后向后移动指针,类似堆指针。
paddr_read()和paddr_write()会判断地址addr落在物理内存空间还是设备空间, 若落在物理内存空间, 就会通过pmem_read()和pmem_write()来访问真正的物理内存; 否则就通过map_read()和map_write()来访问相应的设备. 从这个角度来看, 内存和外设在CPU来看并没有什么不同, 只不过都是一个字节编址的对象而已.map_read 和 map_write 可以用统一的方式模拟各种设备的寄存器访问,并通过回调函数实现设备的特殊行为,适合单线程仿真环境,非常方便地支持各种 I/O 设备的模拟。
设备
NEMU使用SDL库来实现设备的模拟, nemu/src/device/device.c含有和SDL库相关的代码. init_device()函数主要进行以下工作:
调用init_map()进行初始化.
对上述设备进行初始化, 其中在初始化VGA时还会进行一些和SDL相关的初始化工作, 包括创建窗口, 设置显示模式等;
然后会进行定时器(alarm)相关的初始化工作. 定时器的功能在PA4最后才会用到, 目前可以忽略它.
将输入输出抽象成IOE
IOE(抽象机 I/O 设备层)提供了三个统一的 API:
bool ioe_init();
用于初始化 IOE 相关的内容。void ioe_read(int reg, void *buf);
用于从编号为 reg 的“抽象寄存器”读取内容到 buf。void ioe_write(int reg, void *buf);
用于把 buf 的内容写入编号为 reg 的“抽象寄存器”。
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }
void ioe_write(int reg, void *buf) { ((handler_t)lut[reg])(buf); }
可以看到ioe_read``ioe_write
函数都调用了lut这个函数。
static inline void screen_refresh() {io_write(AM_GPU_FBDRAW, 0, 0, NULL, 0, 0, true);
}static inline int screen_tile_height() {return io_read(AM_GPU_CONFIG).height / TILE_W;
}static inline int screen_tile_width() {return io_read(AM_GPU_CONFIG).width / TILE_W;
}
一般用法就是这样,根据amdev.h
中定义的特殊寄存器及其该寄存器结构体中带的元素进行读取与写入等操作
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);
串口
在serial.c
函数中模拟了串口的功能。
static void serial_putc(char ch) {MUXDEF(CONFIG_TARGET_AM, putch(ch), putc(ch, stderr)); //如果没用am那就用标准io库,如果用了am那就用自己实现的putch
}
调用了putch
的函数。
putch
在trm.c
中定义了函数,
void putch(char ch) { //输出一个字符outb(SERIAL_PORT, ch);
}
static inline void outb(uintptr_t addr, uint8_t data) { *(volatile uint8_t *)addr = data; }
时钟
timer.c
模拟了i8253计时器的功能. 计时器的大部分功能都被简化, 只保留了"发起时钟中断"的功能(目前我们不会用到). 同时添加了一个自定义的时钟. i8253计时器初始化时会分别注册0x48处长度为8个字节的端口, 以及0xa0000048处长度为8字节的MMIO空间, 它们都会映射到两个32位的RTC寄存器. CPU可以访问这两个寄存器来获得用64位表示的当前时间.
amdev.h
为时钟定义了两个特殊寄存器,分别叫做AM_TIMER_RTC``AM_TIMER_UPTIME
分别用于读出AM实时时钟和AM系统启动时间可以用来读出系统启动的秒数。
dtrace-设备访问的痕迹
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); // 如果map->callback存在,调用它并传入参数(offset、len、false 表示读操作)。//callback用于模拟硬件设备的副作用(例如,读取某个寄存器可能自动清除状态位)。//map->space + offset:定位到映射区域中的目标地址。//host_read:从指针处读取 len 字节并返回 word_t 类型的地址。word_t ret = host_read(map->space + offset, len);//如果启用调试(CONFIG_DTRACE),记录读取操作的设备名、地址和长度。IFDEF(CONFIG_DTRACE, Log("read device %s : address in = " FMT_PADDR ", len = %d\n", map->name , addr, len));return ret;
}
//其中map_read()和map_write()用于将地址addr映射到map所指示的目标空间, 并进行访问.
//每次进行I/O读写的时候, 才会调用设备提供的回调函数(callback).
void map_write(paddr_t addr, int len, word_t data, IOMap *map) {assert(len >= 1 && len <= 8);check_bound(map, addr);paddr_t offset = addr - map->low;host_write(map->space + offset, len, data);invoke_callback(map->callback, offset, len, true);IFDEF(CONFIG_DTRACE, Log("write device %s : address in = " FMT_PADDR ", len = %d\n", map->name , addr, len));
}
在KCONFIG中定义变量然后在读写的时候LOG出设备名字即可。
键盘
学习native的写法即可。
#define KEYDOWN_MASK 0x8000void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {uint32_t kc = inl(KBD_ADDR);kbd->keydown = kc & KEYDOWN_MASK ? true : false;kbd->keycode = kc & ~KEYDOWN_MASK;
}
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, 则马上将帧缓冲中的内容同步到屏幕上.
也就是这两个寄存器,他带了一下这些参数。
AM_DEVREG( 9, GPU_CONFIG, RD, bool present, has_accel; int width, height, vmemsz);
AM_DEVREG(11, GPU_FBDRAW, WR, int x, y; void *pixels; int w, h; bool sync);