文章目录
代码github网址
https://github.com/xiao-tai/ics2021
ICS2021其他博客
南大-ICS2021 PA1~PA2.2 学习笔记&记录
南大-ICS2021 PA3.1 学习笔记&记录
南大ICS2021–实现库函数vsnprintf
PA2.3 笔记
输入输出
设备与CPU
在程序看来,访问设备=读出数据+写入数据+控制状态
访问设备最简单的方法就是将设备的寄存器作为接口,CPU来访问这些寄存器。比如CPU可以从/往设备的数据寄存器中读出/写入数据,进行数据的输入输出; 可以从设备的状态寄存器中读出设备的状态,询问设备是否忙碌; 或者往设备的命令寄存器中写入命令字,来修改设备的状态。
具体的,将允许CPU访问的寄存器逐一编号,然后通过这些指令来引用这些编号,这就是所谓的I/O编址方式。常用的有两种。
-
端口I/O: 把端口号作为I/O指令的一部分, 很简单, 但是指令集只能添加而不能进行修改, 端口映射I/O所能访问的I/O地址空间的大小, 在设计I/O指令的那一刻就已经决定下来了.
-
内存映射I/O: 通过不同的物理内存地址给设备编址的. 这种编址方式将一部分物理内存的访问"重定向"到I/O地址空间中. 内存映射I/O的一个例子是NEMU中的物理地址区间
[0xa1000000, 0xa1800000)
. 这段物理地址区间被映射到VGA内部的显存, 读写这段物理地址区间就相当于对读写VGA显存的数据.
volatile关键字: 作用为避免编译器对代码进行相应的优化.
NEMU中的输入输出
映射与I/O方式
定义了一个结构体类型IOMap
(在nemu/include/device/map.h
中定义)
在nemu/src/device/io/map.c
实现了映射的管理, 包括I/O空间的分配及其映射, 还有映射的访问接口.
add_pio_map()
用来注册I/O空间, 无论是端口映射, 还是内存映射, 最终都会执行map_read()
和map_write()
.
NEMU实现了串口, 时钟, 键盘, VGA, 声卡, 磁盘, SD卡七种设备. NEMU使用SDL库来实现设备的模拟, nemu/src/device/device.c
含有和SDL库相关的代码.
将输入输出抽象成IOE
本节测试命令均在am-kernels/tests/am-test
下进行.
bool ioe_init();
void ioe_read(int reg, void *buf);
void ioe_write(int reg, void *buf);
这里的reg
寄存器并不是上文讨论的设备寄存器, 因为设备寄存器的编号是架构相关的. 在IOE中, 我们希望采用一种架构无关的"抽象寄存器", 这个reg
其实是一个功能编号, 我们约定在不同的架构中, 同一个功能编号的含义也是相同的, 这样就实现了设备寄存器的抽象. 在abstract-machine/am/src/platform/nemu/ioe/ioe.c
中实现.
串口(实现)
起初, 执行make ARCH=riscv32-nemu run mainargs=I-love-PA
报错 address (XXXXX) is out of bound at pc = 0x8000XXX
, nemu会将am-kernels/tests/am-tests/src
下的main.c
作为源程序,所以如果要避免报错,需将main.c中printf涉及到的’格式控制符’都实现。
扩展printf功能之后(准确来说是vsprintf函数(abstract-machine/klib/src/stdio.c下
))
成功输出了I-love-PA
, 如下图
问:
请你通过RTFSC理解这个参数是如何从
make
命令中传递到hello
程序的,$ISA-nemu
和native
采用了不同的传递方法, 都值得你去了解一下.答:
这里应该是问怎么传递到
main
程序中的, 对于riscv32-nemu
,abstract-machine/scripts/platform/nemu.mk
中有一句CFLAGS += -DMAINARGS=\"$(mainargs)"\
, 即给出一个宏定义MAINARGS, 其值为$(mainargs)
.对于native本人没有了解…
时钟(实现)
注意: 后续测试要把abstract-machine/klib/include/klib.h
中的#define __NATIVE_USE_KLIB__
注释掉**.
abstract-machine/am/src/riscv/riscv.h
中in和out函数分别表示cpu读/写某一个I/O设备的寄存器. 示例: x86-qemu UART
设备的操作
在abstract-machine/am/src/platform/nemu/ioe/timer.c
中添加代码
void __am_timer_init() {
// i8253计时器会注册长度为8字节的MMIO空间, 又riscv.h中最大处理数据为32位, 所以分
// 高4字节和低4字节.
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跑多快
第一次运行, 报错没有实现slti
指令,添加代码实现如下
// 在def_THelper(cal_imm)中添加
def_INSTR_TAB("??????? ????? ????? 010 ????? ????? ??", slti);
// 在nemu/src/isa/riscv32/instr中添加
def_EHelper(slti) {
rtl_setrelopi(s, RELOP_LT, ddest, dsrc1, id_src2->imm);
}
// src/isa/riscv32/include/isa-all-instr.h中添加将新函数到INSTR_LIST
Dhrystone跑分结果
coremark跑分结果
microbench跑分结果, 因为klib库中的vsprintf函数的实现(见vsprintf详解)还存在一定缺陷, 所以无法显示出正确的毫秒数, (以后后续再改进)
问: 如何实现AM_TIMER_UPTIME?
答: 首先在
init_timer()
中确定好, 设备的名称, addr, 映射的目标空间, 空间长度和回调函数.相应函数:
rtc_io_handler()
会在offset=4
时获取一次时间, 获取时间其实就是执行io_read(AM_TIME_UPTIME)
. 最终还是会在map中执行这个callback, 在map_read
中offset=addr - map->low
.所以, 当
addr=map->low + 4
时. 执行一次get_time()
. 所以在__am_timer_uptime()
中先获取高32位数据(即RTC_ADDR + 4
的数据), 满足addr=low + 4
的条件, 从而更新rtc_port_base
.如果先获取低32位的数据, 此时不会执行该时刻的
get_time()
, 得到的还是上一次get_time()
所得到的数据.
运行马里奥
参考microbench/src/bench.c
中bench_alloc()
, 简单实现一下malloc
测试指令在fceux-am下执行: make ARCH=riscv32-nemu run mainargs=mario
// stdlib.c顶部
static char* start_addr; // addr的初始值
static bool init_flag = 0; // 初始化的标志, 初始化完成后置1
void *malloc(size_t size) {
if(!init_flag) {
start_addr = (void*)ROUNDUP(heap.start, 8);
init_flag = true;
}
size = (size_t)ROUNDUP(size, 8);
char* old = start_addr; // 获取addr
start_addr += size;
return old; // [addr, addr + size]
}
运行结果
设备访问的踪迹-dtrace
修改nemu/Kconfig
, 参考ITRACE
config DTRACE
depends on XXXX
bool "XXX"
default y
config DTRACE_COND
depends on DTRACE
string "XXX"
default "true"
在nemu/src/device/io/map.c
中添加
word_t map_read(paddr_t addr, int len, IOMap *map) {
// ..
+ #ifdef CONFIG_DTARCE_COND
+ log_write("dtrace: read %10s at " FMT_PADDR ",%d\n", map->name, addr, len);
+ #endif
return ret;
}
void map_write(paddr_t addr, int len, word_t data, IOMap *map) {
// ...
+ #ifdef CONFIG_DTARCE_COND
+ log_write("dtrace: write %10s at " FMT_PADDR ",%d with " FMT_WORD "\n",
map->name, addr, len, data);
+ #endif
}
键盘(实现)
在abstract-machine/am/src/platform/nemu/ioe/input.c
中实现, 参考native的ioe
void __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(实现)
VGA设备还有两个寄存器: 屏幕大小寄存器和同步寄存器.
-
屏幕大小寄存器的硬件(NEMU)功能已经实现, 但软件(AM)还没有去使用它;
-
同步寄存器软件(AM)已经实现了同步屏幕的功能, 但硬件(NEMU)尚未添加相应的支持.
问题一: 在abstract-machine/am/src/platform/nemu/ioe/gpu.c
中完善__am_gpu_config()
, 需要得到正确的.width
和.height
, RTFC可知, 分辨率的值保存在vgactl_port_base[0]
中, 高16位为width
, 低16位为height
.
问题二: 在nemu/src/device/vga.c
中完善vga_update_screen()
, sync
保存在vgactl_port_base[1]
中, 当sync=1
时, 执行一次update_screen()
.
解决问题之后并完成_am_gpu_init()
,此时结果还只是显示蓝绿渐变的图片, 不是测试程序所想要的, 需要进一步再完善__am_gpu_fbdraw()
, 将FB_ADDR空间的像素填充完整, 得到流动的渐变图像, 如下图(此时还没有注释掉_am_gpu_init()
的内容,屏幕左边缘和下边缘还有渐变的图案).
void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
int x = ctl->x, y = ctl->y, w = ctl->w, h = ctl->h;
if (!ctl->sync && (w == 0 || h == 0))
return;
uint32_t *pixels = ctl->pixels;
uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
uint32_t screen_w = inl(VGACTL_ADDR) >> 16;
for (int i = y; i < y+h; i++) {
for (int j = x; j < x+w; j++) {
fb[screen_w*i+j] = pixels[w*(i-y)+(j-x)]; //缓冲区是一个像素块
}
}
if (ctl->sync) {
outl(SYNC_ADDR, 1); //将sync置1,nemu会进行屏幕更新
}
}
fb[screen_w*i+j] = pixels[w*(i-y)+(j-x)];
这一句的理由可以看https://anya.cool/archives/81c5e64f
声卡(实现)
SDL音频播放流程
-
初始化音频系统, 填充好
SDL_AudioSpec
的音频信息 -
打开音频设备
-
开始播放:
SDL_PauseAudio(0)
-
回调函数: 开始播放后,会有音频其他子线程来调用回调函数,进行音频数据的补充,经过测试每次补充4096个字节。
具体实现
问: 为什么执行audio test会播放小星星?
答:
am-tests/src/tests/audio/audio-data.S
的功劳, 其中audio_payload
和audio_payload_end
分别标识了音频数据的开始位置和结束位置.
- 问题的关键在于维护流缓冲区, 程序通过
AM_AUDIO_PLAY
的抽象往流缓冲区里面写入音频数据, 而SDL库的回调函数则从流缓冲区里面读出音频数据.
AM(软件)的实现
static uint32_t sbuf_pos = 0; //重点,千万不能少
void __am_audio_config(AM_AUDIO_CONFIG_T *cfg) {
cfg->present = true;
}
void __am_audio_ctrl(AM_AUDIO_CTRL_T *ctrl) {
outl(AUDIO_FREQ_ADDR, ctrl->freq);
outl(AUDIO_CHANNELS_ADDR, ctrl->channels);
outl(AUDIO_SAMPLES_ADDR, ctrl->samples);
outl(AUDIO_INIT_ADDR, 1); //将init写入1,音频子系统进入初始化
}
void __am_audio_status(AM_AUDIO_STATUS_T *stat) {
stat->count = inl(AUDIO_COUNT_ADDR);
}
void __am_audio_play(AM_AUDIO_PLAY_T *ctl) {
uint8_t *audio_data = (ctl->buf).start;
uint32_t sbuf_size = inl(AUDIO_SBUF_SIZE_ADDR);
//uint32_t cnt = inl(AUDIO_COUNT_ADDR);
uint32_t len = (ctl->buf).end - (ctl->buf).start;
//while(len > buf_size - cnt);
uint8_t *ab = (uint8_t *)(uintptr_t)AUDIO_SBUF_ADDR; //参考GPU部分
for(int i = 0; i < len; i++){
ab[sbuf_pos] = audio_data[i];
sbuf_pos = (sbuf_pos + 1) % sbuf_size;
}
outl(AUDIO_COUNT_ADDR, inl(AUDIO_COUNT_ADDR) + len); //更新reg_count
}
NEMU(硬件的实现)
static uint32_t sbuf_pos = 0; //这一句非常重要
void sdl_audio_callback(void *userdata, uint8_t *stream, int len){
SDL_memset(stream, 0, len);
uint32_t used_cnt = audio_base[reg_count];
len = len > used_cnt ? used_cnt : len;
uint32_t sbuf_size = audio_base[reg_sbuf_size];
if( (sbuf_pos + len) > sbuf_size ){
SDL_MixAudio(stream, sbuf + sbuf_pos, sbuf_size - sbuf_pos , SDL_MIX_MAXVOLUME);
SDL_MixAudio(stream + (sbuf_size - sbuf_pos),
sbuf + (sbuf_size - sbuf_pos),
len - (sbuf_size - sbuf_pos),
SDL_MIX_MAXVOLUME);
}
else
SDL_MixAudio(stream, sbuf + sbuf_pos, len , SDL_MIX_MAXVOLUME);
sbuf_pos = (sbuf_pos + len) % sbuf_size;
audio_base[reg_count] -= len;
}
void init_sound() {
SDL_AudioSpec s = {};
s.format = AUDIO_S16SYS; // 假设系统中音频数据的格式总是使用16位有符号数来表示
s.userdata = NULL; // 不使用
s.freq = audio_base[reg_freq];
s.channels = audio_base[reg_channels];
s.samples = audio_base[reg_samples];
s.callback = sdl_audio_callback;
SDL_InitSubSystem(SDL_INIT_AUDIO);
SDL_OpenAudio(&s, NULL);
SDL_PauseAudio(0); //播放,可以执行音频子系统的回调函数
}
static void audio_io_handler(uint32_t offset, int len, bool is_write) {
if(audio_base[reg_init]==1){
init_sound();
audio_base[reg_init] = 0;
}
}
void init_audio() {
...
+ audio_base[reg_sbuf_size] = CONFIG_SB_SIZE; //确定流缓冲区的大小
}
继续运行马里奥make ARCH=riscv32-nemu run mainargs=mario
,有画面有声音,不过只能运行到20fps左右,可能是虚拟机的缘故
冯诺依曼计算机系统
游戏可以抽象成一个死循环:
while (1) {
等待新的一帧(); // AM_TIMER_UPTIME
处理用户按键(); // AM_INPUT_KEYBRD
更新游戏逻辑(); // TRM
绘制新的屏幕(); // AM_GPU_FBDRAW
}
游戏是如何运行的
首先
video_init()
确定了texture数组,确定了哪一个像素有颜色,哪一个没有颜色。
game_logic_update()
规定每秒生成FPS/CPS个字母,随机的字符,随机的速度. 每秒检查FPS次按键情况, 按键按下并且定义了按键符号, 程序开始执行check_hit
检查的是最下方的按下字母, 按键正确, 则字母速度向上.
最后
render()
显示结果, 如果速度为正数, 字母颜色为白色, 为负数, 字母颜色为绿色, 速度等于零, 为红色.