PA2.3-设备的输入和输出
📚 使用须知
- 本博客内容仅供学习参考
- 建议理解思路后独立实现
- 欢迎交流讨论
输入输出-串口
理解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了


运行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/O的port-io.c,有函数pio_read,pio_write -
实现
内存映射I/O的mmio.c,有函数mmio_read,mmio_write -
上述
read,write函数都是调用mmp.c中的map_read和map_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_write,mmio_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_ifetch,vaddr_read,vaddr_write对paddr_read, paddr_write进行了进一步的封装
然后再我们的指令中nemu/src/isa/riscv32/inst.c有宏:
#define Mr vaddr_read
#define Mw vaddr_write
噢!原来我们在进行指令解析的时候,如果发现指令访存,那么我们就调用Mr,Mw进行操作。
如果访问的内存是我们进行端口/内存映射的内存,那么在调用Mr,Mw时就会触发回调函数serial_io_handler的执行,输出字符

浙公网安备 33010602011771号