基于riscv32的nemu
RTFSC(2)
运行第一个客户程序
这一部分主要靠RTFM
,我们需要根据反汇编的结果来查看缺少的指令:
Disassembly of section .text:
80000000 <_start>:
80000000: 00000413 li s0,0
80000004: 00009117 auipc sp,0x9
80000008: ffc10113 addi sp,sp,-4 # 80009000 <_end>
8000000c: 00c000ef jal ra,80000018 <_trm_init>
80000010 <main>:
80000010: 00000513 li a0,0
80000014: 00008067 ret
80000018 <_trm_init>:
80000018: ff010113 addi sp,sp,-16
8000001c: 00000517 auipc a0,0x0
80000020: 01c50513 addi a0,a0,28 # 80000038 <_etext>
80000024: 00112623 sw ra,12(sp)
80000028: fe9ff0ef jal ra,80000010 <main>
8000002c: 00050513 mv a0,a0
80000030: 00100073 ebreak
80000034: 0000006f j 80000034 <_trm_init+0x1c>
注意一些伪指令如
li
是一些基础指令的组合实现的,需要添加那些基础指令才能实现伪指令的功能。
通过阅读代码可以发现,一些类型指令的添加并不困难,但是像J-type
指令中的立即数实现还是值得注意的。
由于添加指令的流程基本相同,所以就以jal
指令来简要介绍一下:
首先,添加J-type
指令立即数的宏定义(类比于已给出immS()
来写):
#define immJ() do { *imm = SEXT((BITS(i, 31, 31) << 19) | \
(BITS(i, 30, 21)) | (BITS(i, 20, 20) << 10) | \
(BITS(i, 19, 12) << 11), 20) << 1; } while(0)
由于J-type
指令并未用到源操作数,所以在对指令进行解析时,只需要解析立即数即可:
switch (type) {
case TYPE_I: src1R(); immI(); break;
case TYPE_U: immU(); break;
case TYPE_S: src1R(); src2R(); immS(); break;
case TYPE_J: immJ();
///for(int i = 0; i < 5; i++)
// printf("0x%05x\n", *imm);
break;
case TYPE_Rtype: src1R(); src2R(); break;
case TYPE_B: src1R(); src2R(); immB(); break;
}
最后再添加jal
指令的模式匹配就可以了。
//J-type
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, R(rd) = s->pc + 4, s->dnpc = s->pc + imm);
正如
PA
指导书所说,我们在添加指令进行测试时,最好不要先把指令全部添加之后再进行统一测试,我们应该利用所给的测试用例,逐一测试,如遇到缺少的指令,再进行添加,这样在出错时,也能缩小出错指令的范围,节省debug
时间。
实现更多的指令
根据以上流程,对所有测试用例逐一测试,添加缺少的指令。
程序, 运行时环境与AM
通过批处理模式运行NEMU
RTFSC
(其实是直接查找的mode
关键词)发现,在nemu/src/monitor.c
已经指出批处理模式的启动是由-b
参数控制的,所以我们需要在nemu.mk
中添加NEMUFLAGS += -b
来实现批处理模式运行nemu
。
实现字符串处理函数
这里面的函数实现还是需要注意的,很容易出现一些小问题,在这里只介绍几个易出错的函数。
strcpy
在测试check(strcmp( strcat(strcpy(str, str1), s[3]), s[4]) == 0)
时,出现了问题,我当时认为出问题的可能性:strcat
> strcmp
> strcpy
,结果最后逐一排查,是strcpy
出现了问题。果然,正如jyy
所说,往往你认为最不可能出问题的地方,bug
就躺在那里。
错误版:
char *strcpy(char *dst, const char *src) {
//panic("Not implemented");
assert(dst != NULL && src != NULL);
while(*src != '\0')
{
*dst++ = *src++;
}
*dst = '\0';
return dst;
}
正确版:
char *strcpy(char *dst, const char *src) {
//panic("Not implemented");
assert(dst != NULL && src != NULL);
char *tmp = dst;
while(*src != '\0')
{
*tmp++ = *src++;
}
*tmp = '\0';
return dst;
}
问题所在:返回的指针指错了位置
strcat
注意,需要在末尾加上
\0
char *strcat(char *dst, const char *src) {
//panic("Not implemented");
assert(dst != NULL && src != NULL);
char *tmp = dst;
while(*tmp != '\0') tmp++;
while(*src != '\0') *tmp++ = *src++;
*tmp = '\0';
return dst;
}
memmove
关于这个函数讲解,有一篇很好的blog:memmove函数实现
memmove函数用于处理重叠拷贝的情况。
重叠情况:
解决方案:
但是呢,也不能一概而论,就全部都是从后往前拷贝,还是得分情况的,具体就是看destination
和source
的位置关系。
根据以上分析,便可以实现此函数了:
void *memmove(void *dst, const void *src, size_t n) {
//panic("Not implemented");
assert(dst && src);
char *tmp = dst;
const char *s = src;
if(dst > src)
{
s += n - 1;
tmp += n - 1;
while(n--)
{
*tmp-- = *s--;
}
}
else
{
while(n--)
{
*tmp++ = *s++;
}
}
return dst;
}
断言指针不为空是个好习惯
实现sprintf
想要实现一个功能比较全的sprintf
函数,还是比较复杂的,由于测试用例hello-str
在这里只用到了%s
和%d
,所以目前只实现了一个简易的sprintf
。
static char *number(char *str, int num, int base)
{
int tmp[100], i = 0;
if(num < 0)
{
*str++ = '-';
num = -num;
}
while(num)
{
tmp[i++] = num % base;
num /= base;
}
i--;
while(i >= 0) *str++ = tmp[i--] + '0'; //注意要转化为字符型数字
return str;
}
int vsprintf(char *out, const char *fmt, va_list ap) {
//panic("Not implemented");
char *str = out;
const char *s;
int base;
int num;
for(; *fmt != '\0'; ++fmt)
{
if(*fmt != '%')
{
*str++ = *fmt;
continue;
}
//*fmt == '%'
++fmt;
base = 10; //默认为10进制
switch(*fmt)
{
case 'c':
*str++ = (unsigned char) va_arg(ap, int);
continue;
case 's':
s = va_arg(ap, char *);
/**** Plan A ****/
//for(int i = 0; s[i]; i++)
// *str++ = s[i];
/**** Plan B ****/
strcat(str, s);
str += strlen(s);
continue;
case 'd':
break; //用于跳出switch语句
default:
*str++ = '%';
if(*fmt)
*str++ = *fmt;
else
--fmt; //防止溢出
continue;
}
num = va_arg(ap, int);
str = number(str, num, base);
}
*str = '\0';
return str - out; //返回写入的字符数
}
/*
stdarg中部分宏的解释
va_list 是用于存放参数列表的数据结构。
va_start 函数根据初始化last来初始化参数列表,last为固定参数中的最后一个固定参数.(原因在后面的宏解析中)
va_arg 函数用于从参数列表中取出一个参数,参数类型由type指定。
va_copy 函数用于复制参数列表。
va_end 函数执行清理参数列表的工作。
*/
int sprintf(char *out, const char *fmt, ...) {
//panic("Not implemented");
assert(out);
va_list args;
int i;
va_start(args, fmt);
i = vsprintf(out, fmt, args);
va_end(args);
return i;
}
基础设施(2)
实现iringbuf
在nemu/src/utils
下新建trace.c
#include <common.h>
#include <elf.h>
#include <device/map.h>
#define INST_NUM 16
// iringbuf
typedef struct
{
word_t pc;
uint32_t inst;
}InstBuf;
InstBuf iringbuf[INST_NUM];
int cur_inst = 0;
int func_num = 0;
void trace_inst(word_t pc, uint32_t inst)
{
iringbuf[cur_inst].pc = pc;
iringbuf[cur_inst].inst = inst;
cur_inst = (cur_inst + 1) % INST_NUM;
}
void display_inst()
{
/*** 注意出错的是前一条指令,当前指令可能由于出错已经无法正常译码 ***/
int end = cur_inst;
char buf[128];
char *p;
int i = cur_inst;
if(iringbuf[i+1].pc == 0) i = 0;
do
{
p = buf;
//if(i == end) p += sprintf(buf, "-->");
p += sprintf(buf, "%s" FMT_WORD ": %08x\t", (i + 1) % INST_NUM == end ? "-->" : " ", iringbuf[i].pc, iringbuf[i].inst);
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
disassemble(p, buf + sizeof(buf) - p, iringbuf[i].pc, (uint8_t *)&iringbuf[i].inst, 4);
puts(buf);
i = (i + 1) % INST_NUM;
} while (i != end);
}
将trace_inst()
放在取指令inst_fetch
的后面,用于跟踪指令,将指令放入缓冲区中。
display_inst()
函数放在assert_fail_msg()
中,当程序出错时,展示寄存器的值与出错的指令。
void assert_fail_msg() {
isa_reg_display();
IFDEF(CONFIG_IRINGBUF,display_inst());
statistic();
}
在nemu/Kconfig
中参考着ITRACE定义IRINGBUF
,这样我们就可以在menuconfig
中控制iringbuf
的开启与关闭。
实现mtrace
同样的,先在nemu/Kconfig
中定义MTRACE
实现mtrace
是比较简单的,只需要在访问paddr_read()
和paddr_write()
时进行记录即可,在paddr_read()
和paddr_write()
中分别添加
IFDEF(CONFIG_MTRACE, display_memory_read(addr, len));
IFDEF(CONFIG_MTRACE, display_memory_write(addr, len, data));
然后在trace.c
中添加这两个函数即可:
void display_memory_read(paddr_t addr, int len)
{
printf(ANSI_FMT("read memory: ", ANSI_FG_BLUE) FMT_PADDR ", the len is %d\n", addr, len);
}
void display_memory_write(paddr_t addr, int len, word_t data)
{
printf(ANSI_FMT("write memory: ", ANSI_FG_YELLOW) FMT_PADDR ", the len is %d, the written data is " FMT_WORD "\n", addr, len, data);
}
需要注意的是开启
mtrace
后会有大量的输出,如果开启时,运行红白机模拟器,可能会无法正常运行,所以在不使用mtrace
时,我们可以将其关闭,或者将printf
换为log_write
,将这些信息输出到日志中。
实现ftrace
这个实现就很复杂了,首先在parse_args()
添加相关代码,其实也就是添加一个-e
的选项:
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"elf" , required_argument, NULL, 'e'},
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:e:", table, NULL)) != -1) {
switch (o) {
case 'e': elf_file = optarg; break;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-e,--elf=FILE parse the elf file\n");
printf("\n");
exit(0);
}
}
return 0;
}
void init_monitor(int argc, char *argv[]) {
/* Perform some global initialization. */
parse_elf(elf_file);
}
在nemu.mk
中添加NEMUFLAGS += -e $(IMAGE).elf
,用于开启解析elf
文件功能,接下来,我们的目标就是获取到elf
文件中的符号表,将其保存,供后续使用。elf
文件的介绍网上有很多,所以STFW
吧。
typedef struct {
char name[64];
paddr_t addr; //the function head address
Elf32_Xword size;
} Symbol;
Symbol *symbol = NULL; //dynamic allocate memory or direct allocate memory (Symbol symbol[NUM])
void parse_elf(const char *elf_file)
{
if(elf_file == NULL) return;
FILE *fp;
fp = fopen(elf_file, "rb");
if(fp == NULL)
{
printf("failed to open the elf file!\n");
exit(0);
}
Elf32_Ehdr edhr;
//读取elf头
if(fread(&edhr, sizeof(Elf32_Ehdr), 1, fp) <= 0)
{
printf("fail to read the elf_head!\n");
exit(0);
}
if(edhr.e_ident[0] != 0x7f || edhr.e_ident[1] != 'E' ||
edhr.e_ident[2] != 'L' ||edhr.e_ident[3] != 'F')
{
printf("The opened file isn't a elf file!\n");
exit(0);
}
fseek(fp, edhr.e_shoff, SEEK_SET);
Elf32_Shdr shdr;
char *string_table = NULL;
//寻找字符串表
for(int i = 0; i < edhr.e_shnum; i++)
{
if(fread(&shdr, sizeof(Elf32_Shdr), 1, fp) <= 0)
{
printf("fail to read the shdr\n");
exit(0);
}
if(shdr.sh_type == SHT_STRTAB)
{
//获取字符串表
string_table = malloc(shdr.sh_size);
fseek(fp, shdr.sh_offset, SEEK_SET);
if(fread(string_table, shdr.sh_size, 1, fp) <= 0)
{
printf("fail to read the strtab\n");
exit(0);
}
}
}
//寻找符号表
fseek(fp, edhr.e_shoff, SEEK_SET);
for(int i = 0; i < edhr.e_shnum; i++)
{
if(fread(&shdr, sizeof(Elf32_Shdr), 1, fp) <= 0)
{
printf("fail to read the shdr\n");
exit(0);
}
if(shdr.sh_type == SHT_SYMTAB)
{
fseek(fp, shdr.sh_offset, SEEK_SET);
Elf32_Sym sym;
size_t sym_count = shdr.sh_size / shdr.sh_entsize;
symbol = malloc(sizeof(Symbol) * sym_count);
for(size_t j = 0; j < sym_count; j++)
{
if(fread(&sym, sizeof(Elf32_Sym), 1, fp) <= 0)
{
printf("fail to read the symtab\n");
exit(0);
}
if(ELF32_ST_TYPE(sym.st_info) == STT_FUNC)
{
const char *name = string_table + sym.st_name;
strncpy(symbol[func_num].name, name, sizeof(symbol[func_num].name) - 1);
symbol[func_num].addr = sym.st_value;
symbol[func_num].size = sym.st_size;
func_num++;
}
}
}
}
fclose(fp);
free(string_table);
}
需要注意符号表中的
st_name
(符号名称),它其实不是一个字符串,而是一个数值,代表的是目标文件中字符串表(.strtab
节)中的一个索引值, 那里才真正存储着该符号的名称对应的字符串,也正是这个原因,我们需要读取字符串表的首地址,并将其存入string_table
变量中。
通过上述代码,也可以看出,在了解了
elf
文件的一些结构体后,解析elf
文件就主要靠fread
函数与fseek
函数,fseek
函数用于不断调整fp
文件指针的位置,然后fread
再读取对应的节头表、字符串表、符号表等,并将其保存在所定义的变量中,最后再通过此变量,获取到我们需要的数据。
几个c语言相关函数:
int fread(void *buffer,int size,int count,FILE *fp);
fread()──从fp所指向文件的当前位置开始,一次读入size个字节,重复count次,并将读入的数据存放到从buffer开始的内存中; buffer是存放读入数据的起始地址(即存放何处)。
int fseek(FILE *stream, long offset, int fromwhere);
第一个参数
file
指针 第二个参数移动的偏移量 第三个参数移动到哪里 分别用3
个宏
SEEK_SET 即0 文件开头
SEEK_CUR 即1 文件当前位置
SEEK_END 即2 文件结尾
fseek(fp,100L,SEEK_SET);把fp指针移动到离文件开头100字节处;
fseek(fp,100L,SEEK_CUR);把fp指针移动到离文件当前位置100字节处;
fseek(fp,100L,SEEK_END);把fp指针退回到离文件结尾100字节处。
接着,在jal
与jalr
指令中识别函数调用和函数返回指令:
可以看出,函数返回指令的特点是
rd
为x0
,rs1
为x1
,这也是判断是否是函数返回指令的条件。
INSTPAT("??????? ????? ????? 000 ????? 11001 11", jalr , I, R(rd) = s->pc + 4; s->dnpc = ((src1 + imm)) & ~1;
IFDEF(CONFIG_FTRACE, if(rd == 1){
display_call_func(s->pc, s->dnpc);
}
else if(rd == 0 && src1 == R(1)){
display_ret_func(s->pc);
}));
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, R(rd) = s->pc + 4; s->dnpc = s->pc + imm;
IFDEF(CONFIG_FTRACE, if(rd == 1){
display_call_func(s->pc, s->dnpc);
}));
最后,在trace.c
中添加display_call_func()
和display_ret_func()
就可以了。
int rec_depth = 1;
void display_call_func(word_t pc, word_t func_addr)
{
/*for(int i = 0; i < func_num; i++)
{
printf("%s\t0x%08x\t%lu\n", symbol[i].name, symbol[i].addr, symbol[i].size);
}
exit(0);*/
int i = 0;
for(; i < func_num; i++)
{
if(func_addr >= symbol[i].addr && func_addr < (symbol[i].addr + symbol[i].size))
{
break;
}
}
printf("0x%08x:", pc);
for(int k = 0; k < rec_depth; k++) printf(" ");
rec_depth++;
printf("call [%s@0x%08x]\n", symbol[i].name, func_addr);
}
void display_ret_func(word_t pc)
{
int i = 0;
for(; i < func_num; i++)
{
if(pc >= symbol[i].addr && pc < (symbol[i].addr + symbol[i].size))
{
break;
}
}
printf("0x%08x:", pc);
rec_depth--;
for(int k = 0; k < rec_depth; k++) printf(" ");
printf("ret [%s]\n", symbol[i].name);
}
编写更多的测试
在这里发现了memcmp
的实现出现了问题,记录一下:
错误实现:
int memcmp(const void *s1, const void *s2, size_t n) {
//panic("Not implemented");
assert(s1 && s2);
while(n-- && s1 && s2 && *s1 == *s2)
{
s1++;
s2++;
}
return *s1 - *s2;
}
如果
n
为5
的话,而且s1
与s2
前5
位相同,那么其实返回的是s1[5] - s2[5]
,也就是第六位的差值。
正确实现:
int memcmp(const void *s1, const void *s2, size_t n) {
//panic("Not implemented");
assert(s1 && s2);
const char *tmp1 = s1;
const char *tmp2 = s2;
for(size_t i = 0; i < n; i++)
{
if(tmp1[i] < tmp2[i])
return -1;
else if(tmp1[i] > tmp2[i])
return 1;
}
return 0;
}
实现DiffTest
native
作REF
确实是一个很有力的工具,之前还认为测试用例有问题,后续也是通过native
排除了这个错误的想法,后续native
也派上了很大的用场。
将ref_c
的寄存器与dut_cpu
的寄存器逐一进行比较即可。
bool isa_difftest_checkregs(CPU_state *ref_r, vaddr_t pc) {
bool sign = true;
int i = 0;
for(; i < 32; i++)
{
if(cpu.gpr[i] != ref_r->gpr[i])
{
sign = false;
break;
}
}
if(sign && cpu.pc == ref_r->pc)
{
return true;
}
pc = ref_r->pc;
return false;
}
一键回归测试
在一键回归测试时,又找到了两处错误,分别是load-store.c
和mul-longlong.c
,发现是load
指令和mulh
指令的实现出现了问题。
load
指令在取出数据时,要进行符号位拓展,存在寄存器中的数是有符号数。
INSTPAT("??????? ????? ????? 000 ????? 00000 11", lb , I, R(rd) = SEXT(Mr(src1 + imm, 1), 8)); //注意要加SEXT进行符号位扩展
INSTPAT("??????? ????? ????? 001 ????? 00000 11", lh , I, R(rd) = SEXT(Mr(src1 + imm, 2), 16));
INSTPAT("??????? ????? ????? 010 ????? 00000 11", lw , I, R(rd) = SEXT(Mr(src1 + imm, 4), 32));
mulh
指令表示的是两个有符号数相乘,将高字节存在寄存器中,但是不知道为什么将操作数转为有符号数时,要先转为32
位有符号数,再转为64
位有符号数,如果直接转为64
位有符号数会出现问题。
INSTPAT("0000001 ????? ????? 001 ????? 01100 11", mulh , Rtype, R(rd) = (((int64_t)(int32_t)src1 * (int64_t)(int32_t)src2) >> 32));
输入输出
实现printf
在实现sprintf
时,已经实现了vsprintf
,所以利用vsprintf
实现printf
也比较简单,之前实现的vsprintf
比较简单,好多输出格式都未实现,还需要进一步完善。
int printf(const char *fmt, ...) {
//panic("Not implemented");
int i;
char buf[256];
memset(buf, 0, sizeof(buf));
//va_list arg = (va_list)((char*)(&fmt) + 4);
va_list args;
va_start(args, fmt);
i = vsprintf(buf, fmt, args);
va_end(args);
char *tmp = buf;
while(*tmp != 0)
{
putch(*tmp);
tmp++;
}
return i;
}
时钟(实现IOE)
参考riscv/spike/timer.c
的实现:
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
uint32_t hi = inl(MMIO_BASE + 0x4c);
uint32_t lo = inl(MMIO_BASE + 0x48);
uint64_t time = ((uint64_t)hi << 32) | lo;
uptime->us = time - boot_time;
}
在这里需要先读取高位,因为在
rtc_io_handler
中具有offset==4
的判断,在读取高位时,offfset
为4
,才会触发get_time()
获取最新的时间,如果先读取低位的话,那么读取的是上次的时间,导致错误。
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;
}
}
看看NEMU跑多快
在虚拟机上得分为170
多分,更改定时器差距很小。
实现dtrace
同样的,先在nemu/Kconfig
中定义DTRACE
在nemu/src/device/io/map.c
中的map_read
和map_wirte
函数中分别添加
//map_read
IFDEF(CONFIG_DTRACE, display_device_read(addr, len, map));
//map_write
IFDEF(CONFIG_DTRACE, display_device_write(addr, len, data, map));
接着在trace.c
中实现display_device_read()
与display_device_write()
函数
// dtrace
void display_device_read(paddr_t addr, int len, IOMap *map)
{
log_write(ANSI_FMT("read memory: ", ANSI_FG_BLUE) FMT_PADDR ", the len is %d, the read device is "
ANSI_FMT(" %s ", ANSI_BG_BLUE) "\n", addr, len, map->name);
}
void display_device_write(paddr_t addr, int len, word_t data, IOMap *map)
{
log_write(ANSI_FMT("write memory: ", ANSI_FG_YELLOW) FMT_PADDR ", the len is %d, the written data is " FMT_WORD
", the written device is "ANSI_FMT(" %s ", ANSI_BG_YELLOW)"\n", addr, len, data, map->name);
}
由于是根据内存访问来判断识别的是哪一个设备的,所以当开启
dtrace
时,输出的信息也很多,在这里使用log_write
将其输出在log
文件中,如果想要减少输出,继续使用printf
,可以在输出前加一个判断,当上次输出的map->name
与此次的map->name
不同时,才输出信息,这样就可以减少大量的输出信息。
键盘(实现IOE)
参考native
的实现即可。
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
uint32_t code = inl(MMIO_BASE + 0x60);
kbd->keydown = (code & KEYDOWN_MASK ? true : false);
kbd->keycode = code & ~KEYDOWN_MASK;
}
VGA(实现IOE①)
和之前实现的方式类似,都是读取某一内存的值,然后进行赋值操作。
void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
uint32_t space = inl(VGACTL_ADDR);
int w = space >> 16;
int h = space & 0x0000ffff;
*cfg = (AM_GPU_CONFIG_T) {
.present = true, .has_accel = false,
.width = w, .height = h,
.vmemsz = 0
};
}
bug记录:当前在实现这个时,顺手写为了
w = space & 0xffff0000,h = space & 0x0000ffff
,一度未看出问题所在,space & 0xffff0000
与space >> 16
的值肯定是不一样的。😫
硬件(NEMU)同步屏幕实现:
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])
{
update_screen();
vgactl_port_base[1] = 0;
}
}
AM_GPU_FBDRAW
实现:
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);
}
}
展示你的计算机系统
cpu-tests:
VGA:
slider:
mario:
游戏如何运行
NMEU:
解析inst
→ vaddr_read()
→ paddr_read()
→ mmio_read()
→ map_read()
→ host_read()
→ return *(uint32_t *)addr
最终是map_read()
将(uint32_t *) *_ADDR
映射到*_port_base
,*
指的是各种设备(串口, 时钟, 键盘, VGA, 声卡, 磁盘, SD),write
操作同理。
AM:
io_read()
→ ioe_read()
→ lut[]
→ __am_timer_update()
(举例) → inl()
→ return *(volatile uint32_t *)addr
write
操作同理。
本地系统 → NEMU
(硬件) → AM
(软件) → 程序
到这里,如何交互也显而易见,实质就是软件和硬件向同一个地址写/读数据!typing-game
主要是通过io_read()
获取屏幕大小与系统时间,通过io_write()
来设置屏幕颜色,以及调整字母颜色。
PA2
至此结束!