代码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-nemunative采用了不同的传递方法, 都值得你去了解一下.

答:

  • 这里应该是问怎么传递到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_readoffset=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.cbench_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_payloadaudio_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()显示结果, 如果速度为正数, 字母颜色为白色, 为负数, 字母颜色为绿色, 速度等于零, 为红色.

参考博客:

nju pa2 - NOSAE - 博客园 (cnblogs.com)

PA2实验笔记 | Anya の Blog

[南大ICS-PA2] 输入输出-CSDN博客

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐