概括
Cpu控制外设以及读写数据,核心是通过指令对内存/端口的访问来实现的。
1. 在真实的硬件设备上,Cpu通过执行指令时访问内存/端口,这是,有一个硬件设备()将指令进行译码、并通过总线将控制字\数据发送到相应的外设寄存器中,进而实现cpu对外设的控制。
2. nemu控制外设的原理也是如此,但要注意的是,nemu在对外设寄存器进行读取时是对真实硬件的抽象,此时还未实现总线。抽象核心如下:
nemu采用内存映射的方式将外设的地址空间映射到pmem,同时,程序会在初始化外设时会为外设开辟一个大小为2*1024*1024的空间(space),每当添加一个新的外设时,会按照分页管理机制在space为该设备分配一个n*page大小的地址空间,同时为了满足在访问pmem时可以映射到space的要求,会使用结构体数组maps[i]将该设备的name、left、right、&space存放在该数组中(left与right是提前在pmem中划分好的外设地址空间)。至此,当cpu对外设进行操作时,首先cpu会访问pmem,然后根据在pmem中划分的外设空间的偏移进而访问space空间。
内存映射:
3. 对设备寄存器的抽象:
定义寄存器:
#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);
......
读写寄存器,在我看来,inl()\outl()相当于是一个桥梁,软件层面在使用io_read/io_write操作外设寄存器时,最终会调用inl()\outl(),inl()\outl()在汇编后会被会变成load\store指令,这样cpu会访问pmem,接下来过程与上述一致。
#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })
#define io_write(reg, ...) \
({ reg##_T __io_param = (reg##_T) { __VA_ARGS__ }; \
ioe_write(reg, &__io_param); })
void __am_input_keybrd(AM_INPUT_KEYBRD_T *);
void __am_timer_rtc(AM_TIMER_RTC_T *);
......
typedef void (*handler_t)(void *buf);
static void *lut[128] = {
[AM_TIMER_CONFIG] = __am_timer_config,
[AM_TIMER_RTC ] = __am_timer_rtc,
[AM_TIMER_UPTIME] = __am_timer_uptime,
......
};
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }
void ioe_write(int reg, void *buf) { ((handler_t)lut[reg])(buf); }
设备的实现
准备
定义pmem中设备的起始地址,操作寄存器最终是对这些地址的读写:
#define SERIAL_PORT (DEVICE_BASE + 0x00003f8)
#define KBD_ADDR (DEVICE_BASE + 0x0000060)
#define RTC_ADDR (DEVICE_BASE + 0x0000048)
#define VGACTL_ADDR (DEVICE_BASE + 0x0000100)
#define AUDIO_ADDR (DEVICE_BASE + 0x0000200)
#define DISK_ADDR (DEVICE_BASE + 0x0000300)
#define FB_ADDR (MMIO_BASE + 0x1000000)
#define AUDIO_SBUF_ADDR (MMIO_BASE + 0x1200000)
准备寄存器:
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);
1.时钟
计时器初始化时会分别注册0x48
处长度为8个字节的端口, 以及0xa0000048
处长度为8字节的MMIO空间, 它们都会映射到两个32位的RTC寄存器. CPU可以访问这两个寄存器来获得用64位表示的当前时间.
NEMU:
static uint32_t *rtc_port_base = NULL;
static void rtc_io_handler(uint32_t offset, int len, bool is_write) {
assert(offset == 0 || offset == 4);
if (!is_write && offset == 4) {
uint64_t us = get_time();
rtc_port_base[0] = (uint32_t)us;
rtc_port_base[1] = us >> 32;
}
}
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));
}
AM:
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
uint32_t high = inl(RTC_ADDR + 4);
uint32_t low = inl(RTC_ADDR);
uint64_t current_time = ((uint64_t)high << 32) | low;
uptime->us = current_time;
}
这里要注意的是,AM中要先读取高字节的寄存器数据,因为在NEMU中offset==4时才可以将数据写到对应的内存中,这样在读取低字节的寄存器时,数据已经写好了,直接读取即可。
2.键盘
键盘是最基本的输入设备. 一般键盘的工作方式如下: 当按下一个键的时候, 键盘将会发送该键的通码(make code); 当释放一个键的时候, 键盘将会发送该键的断码(break code)。每当用户敲下/释放按键时, 将会把相应的键盘码放入数据寄存器, CPU可以访问数据寄存器, 获得键盘码; 当无按键可获取时, 将会返回AM_KEY_NONE。
NEMU:
#define NEMU_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 NEMU_KEY_NAME(k) NEMU_KEY_ ## k,
enum {
NEMU_KEY_NONE = 0,
MAP(NEMU_KEYS, NEMU_KEY_NAME)
};
#define SDL_KEYMAP(k) keymap[SDL_SCANCODE_ ## k] = NEMU_KEY_ ## k;
static uint32_t keymap[256] = {};
static void init_keymap() {
MAP(NEMU_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 = NEMU_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] != NEMU_KEY_NONE) {
uint32_t am_scancode = keymap[scancode] | (is_keydown ? KEYDOWN_MASK : 0);
key_enqueue(am_scancode);
}
}
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] = NEMU_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());
}
在每次执行完一条指令后,会进入device_updata()函数,该函数会根据SDL_PollEvent(&event)得到event.type该事件的类型,将通码/断码的值存到k中,设置is_keydown的值。将这两个值传入send_key函数中,通过掩码区分up和down,以通码作为下标,将符号作为值,存入队列中。
AM:
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
uint32_t code = inl(KBD_ADDR);
if ((code & 0xfffff000) == KEYDOWN_MASK) {
kbd->keydown = true;
kbd->keycode = code & 0xffff00ff;
}
else {
kbd->keydown = false;
kbd->keycode = AM_KEY_NONE;
}
}
首先通过判断是否有掩码进而判断是up还是down,其次,清除掩码。
3.VGA
- 一个像素信息占四个字节
RED |
GREEN |
BLUE |
ALPHA |
-
显存大小为400*300个字节
AM:
void __am_gpu_init() {
// int i;
// int w = io_read(AM_GPU_CONFIG).width;
// int h = io_read(AM_GPU_CONFIG).height;
// uint32_t *fb= (uint32_t *)(uintptr_t)FB_ADDR;
// for(i = 0; i < w * h; i++) fb[i] = i;
// outl(SYNC_ADDR,1);
}
void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
uint32_t height_and_width = inl(VGACTL_ADDR);
*cfg = (AM_GPU_CONFIG_T) {
.present = true, .has_accel = false,
.width = height_and_width >> 16, .height = height_and_width & 0x0000FFFF,
.vmemsz = 300 * 400 * 4
};
}
void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
int x = ctl->x;
int y = ctl->y;
int w = ctl->w;
int h = ctl->h;
int screen_width = io_read(AM_GPU_CONFIG).width;
uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
uint32_t *pixel_data = (uint32_t *)ctl->pixels;
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
fb[(y + j) * screen_width + (x + i)] = pixel_data[j * w + i];
}
}
if (ctl->sync) {
outl(SYNC_ADDR, 1);
}
}
void __am_gpu_status(AM_GPU_STATUS_T *status) {
status->ready = true;
}
NEMU:
#include <common.h>
#include <device/map.h>
#define SCREEN_W (MUXDEF(CONFIG_VGA_SIZE_800x600, 800, 400))
#define SCREEN_H (MUXDEF(CONFIG_VGA_SIZE_800x600, 600, 300))
static uint32_t screen_width() {
return MUXDEF(CONFIG_TARGET_AM, io_read(AM_GPU_CONFIG).width, SCREEN_W);
}
static uint32_t screen_height() {
return MUXDEF(CONFIG_TARGET_AM, io_read(AM_GPU_CONFIG).height, SCREEN_H);
}
static uint32_t screen_size() {
return screen_width() * screen_height() * sizeof(uint32_t);
}
static void *vmem = NULL;
static uint32_t *vgactl_port_base = NULL;
#ifdef CONFIG_VGA_SHOW_SCREEN
#ifndef CONFIG_TARGET_AM
#include <SDL2/SDL.h>
static SDL_Renderer *renderer = NULL;
static SDL_Texture *texture = NULL;
static void init_screen() {
SDL_Window *window = NULL;
char title[128];
sprintf(title, "%s-NEMU", str(__GUEST_ISA__));
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(
SCREEN_W * (MUXDEF(CONFIG_VGA_SIZE_400x300, 2, 1)),
SCREEN_H * (MUXDEF(CONFIG_VGA_SIZE_400x300, 2, 1)),
0, &window, &renderer);
SDL_SetWindowTitle(window, title);
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STATIC, SCREEN_W, SCREEN_H);
SDL_RenderPresent(renderer);
}
static inline void update_screen() {
SDL_UpdateTexture(texture, NULL, vmem, SCREEN_W * sizeof(uint32_t));
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
#else
static void init_screen() {}
static inline void update_screen() {
io_write(AM_GPU_FBDRAW, 0, 0, vmem, screen_width(), screen_height(), true);
}
#endif
#endif
void vga_update_screen() {
// TODO: call `update_screen()` when the sync register is non-zero,
// then zero out the sync register
if(vgactl_port_base[1] != 0) {
update_screen();
vgactl_port_base[1] = 0;
}
}
void init_vga() {
vgactl_port_base = (uint32_t *)new_space(8);
vgactl_port_base[0] = (screen_width() << 16) | screen_height();
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("vgactl", CONFIG_VGA_CTL_PORT, vgactl_port_base, 8, NULL);
#else
add_mmio_map("vgactl", CONFIG_VGA_CTL_MMIO, vgactl_port_base, 8, NULL);
#endif
vmem = new_space(screen_size());
add_mmio_map("vmem", CONFIG_FB_ADDR, vmem, screen_size(), NULL);
IFDEF(CONFIG_VGA_SHOW_SCREEN, init_screen());
IFDEF(CONFIG_VGA_SHOW_SCREEN, memset(vmem, 0, screen_size()));
}