PA2.3-设备的输入和输出

📚 使用须知

  • 本博客内容仅供学习参考
  • 建议理解思路后独立实现
  • 欢迎交流讨论

task : 设备的输入和输出

输入输出-串口

理解volatile关键字

也许你从来都没听说过C语言中有volatile这个关键字, 但它从C语言诞生开始就一直存在. volatile关键字的作用十分特别, 它的作用是避免编译器对相应代码进行优化. 你应该动手体会一下volatile的作用, 在GNU/Linux下编写以下代码:

void fun() {
  extern unsigned char _end;  // _end是什么?
  volatile unsigned char *p = &_end;
  *p = 0;
  while(*p != 0xff);
  *p = 0x33;
  *p = 0x34;
  *p = 0x86;
}

然后使用-O2编译代码. 尝试去掉代码中的volatile关键字, 重新使用-O2编译, 并对比去掉volatile前后反汇编结果的不同.

你或许会感到疑惑, 代码优化不是一件好事情吗? 为什么会有volatile这种奇葩的存在? 思考一下, 如果代码中p指向的地址最终被映射到一个设备寄存器, 去掉volatile可能会带来什么问题?

_end是一个标记,用于表示程序的结束位置。在C语言中,它通常是一个指向程序结束位置的符号。这个符号通常由链接器(linker)提供,并在程序的链接过程中确定。

在大多数情况下,_end符号用于确定程序的堆(heap)结束位置。堆是一块动态分配的内存区域,用于存储动态分配的内存对象。程序在运行时会在堆中动态分配内存,而_end符号可以帮助程序确定何时达到了堆的末尾。

如果代码中p指向的地址最终被映射到一个设备寄存器, 去掉volatile可能会带来什么问题?
p就会永远只指向0x0

image

image

运行Hello World

task : 运行Hello World

不需要实现额外的代码, 因为NEMU的框架代码已经支持MMIO了.

am-kernels/kernels/hello/目录下键入

make ARCH=$ISA-nemu run

如果你的实现正确, 你将会看到程序往终端输出一些信息(请注意不要让输出淹没在调试信息中).

需要注意的是, 这个hello程序和我们在程序设计课上写的第一个hello程序所处的抽象层次是不一样的: 这个hello程序可以说是直接运行在裸机上, 可以在AM的抽象之上直接输出到设备(串口); 而我们在程序设计课上写的hello程序位于操作系统之上, 不能直接操作设备, 只能通过操作系统提供的服务进行输出, 输出的数据要经过很多层抽象才能到达设备层. 我们会在PA3中进一步体会操作系统的作用.

//nemu/src/device/serial.c
static void serial_putc(char ch) {
  MUXDEF(CONFIG_TARGET_AM, putch(ch), putc(ch, stderr));
}
static void serial_io_handler(uint32_t offset, int len, bool is_write) {
  assert(len == 1);
  switch (offset) {
    /* We bind the serial port with the host stderr in NEMU. */
    case CH_OFFSET:
      if (is_write) serial_putc(serial_base[0]);
      else panic("do not support read");
      break;
    default: panic("do not support offset = %d", offset);
  }
}

这里有个回调函数serial_io_handler,他的作用是通过变量is_write来判断是否要将一个字符输出到主机的标准错误中

我们到nemu/src/device/io目录下发现有三个c文件:

  • 实现端口I/Oport-io.c,有函数pio_read,pio_write

  • 实现内存映射I/Ommio.c,有函数mmio_read,mmio_write

  • 上述read,write函数都是调用mmp.c中的map_readmap_write实现的
    其中都会调用 invoke_callback(map->callback, offset, len, true/false);,即调用回调函数

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); // prepare data to read
word_t ret = host_read(map->space + offset, len);
return ret;
}
 
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);
}

也就是说,每次我们使用pio_read,pio_writemmio_read,mmio_write在端口/内存访问是串口的情况下,都会调用函数serial_io_handler

通过传入参数is_write,来判断这时读串口,写串口。如果是写,那么就输出对应字符到主机的标准错误输出

同时在nemu/src/memory/paddr.c

word_t paddr_read(paddr_t addr, int len) {
  if (likely(in_pmem(addr))) return pmem_read(addr, len);
  IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
  IFDEF(CONFIG_MTRACE, mtraceRead_display(addr, len));
  out_of_bound(addr);
  return 0;
}
 
void paddr_write(paddr_t addr, int len, word_t data) {
  if (likely(in_pmem(addr))) { pmem_write(addr, len, data); return; }
  IFDEF(CONFIG_DEVICE, mmio_write(addr, len, data); return);
  IFDEF(CONFIG_MTRACE, mtraceWrite_display(addr, len, data));
  out_of_bound(addr);
}

可以看到会调用mmio_read,mmio_write
nemu/src/memory/vaddr.c中的vaddr_ifetchvaddr_readvaddr_writepaddr_readpaddr_write进行了进一步的封装

然后再我们的指令中nemu/src/isa/riscv32/inst.c有宏:

#define Mr vaddr_read
#define Mw vaddr_write

噢!原来我们在进行指令解析的时候,如果发现指令访存,那么我们就调用Mr,Mw进行操作。
如果访问的内存是我们进行端口/内存映射的内存,那么在调用Mr,Mw时就会触发回调函数serial_io_handler的执行,输出字符

实现printf

运行alu-tests

理解mainargvs

输入输出-时钟

实现IOE

看看NEMU跑多快

运行演示程序

观看"Bad Apple!!"PV

运行红白机模拟器

输入输出-dtrace

输入输出-键盘

运行红白机模拟器(2)

实现IOE(2)

输入输出-VGA

实现IOE(3)(4)

运行演示程序(2)

运行红白机模拟器(3)

输入输出-冯诺依曼计算机系统

展示你的计算机系统

游戏是如何运行的

为NPC添加串口和时钟

posted @ 2026-01-06 20:38  mo686  阅读(4)  评论(0)    收藏  举报