Skip to content

ICS PA 2 IOE

Posted on:2022.01.01

TOC

Open TOC

ICS PA IOE

volatile 关键字

考虑这样一段程序:

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

使用 gcc -O2 demo.c,并 objdump -d a.out 可得:

0000000000001170 <fun>:
1170: f3 0f 1e fa endbr64
1174: c6 05 9d 2e 00 00 00 movb $0x0,0x2e9d(%rip) # 4018 <_end>
117b: 48 8d 15 96 2e 00 00 lea 0x2e96(%rip),%rdx # 4018 <_end>
1182: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
1188: 0f b6 02 movzbl (%rdx),%eax
118b: 3c ff cmp $0xff,%al
118d: 75 f9 jne 1188 <fun+0x18>
118f: c6 05 82 2e 00 00 33 movb $0x33,0x2e82(%rip) # 4018 <_end>
1196: c6 05 7b 2e 00 00 34 movb $0x34,0x2e7b(%rip) # 4018 <_end>
119d: c6 05 74 2e 00 00 86 movb $0x86,0x2e74(%rip) # 4018 <_end>
11a4: c3 ret
11a5: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
11ac: 00 00 00
11af: 90 nop

若去掉 volatile 关键字,相同的操作会产生:

0000000000001140 <fun>:
1140: f3 0f 1e fa endbr64
1144: c6 05 cd 2e 00 00 00 movb $0x0,0x2ecd(%rip) # 4018 <_end>
114b: eb fe jmp 114b <fun+0xb>
114d: 0f 1f 00 nopl (%rax)

*p 的赋值操作就都被优化掉了。

简单来说,volatile 关键字告知编译器,代理(而不是变量所在的程序)可以改变该变量的值。我们可以认为这里指针 p 所指向的对象是一个屏幕大小的数据,屏幕控制器可以改变该数据的值。

进一步的解释可见 https://en.cppreference.com/w/c/language/volatile。

设备与 CPU

访问设备 = 读出数据 + 写入数据 + 控制状态

设备向 CPU 暴露设备寄存器的接口,把设备内部的复杂行为 (甚至一些模拟电路的特性) 进行抽象,CPU 只需要使用这一接口访问设备,就可以实现期望的功能。

对设备寄存器的编址方式(I/O 编址方式)有两种:

IBM PC 兼容机对常见设备端口号的分配有专门的规定

状态机视角下的输入输出

我们需要将设备分为两部分:

状态机模型 | 状态机模型之外
S = <R, M> | D
计算机 / 程序 <----I/O 指令----> 设备 <----模拟电路----> 物理世界
|
|

S = <R, M>,上文介绍的端口 I/O 和内存映射 I/O 都是通过寄存器 R 来进行数据交互的(从端口所对应的设备寄存器或 I/O 地址空间读写数据到 CPU 的寄存器)。

通过内存 M 来进行数据交互的输入输出方式叫 DMA

映射和 I/O 方式

map.h 中为映射定义了一个结构体类型:

typedef struct {
const char *name;
// we treat ioaddr_t as paddr_t here
paddr_t low;
paddr_t high;
void *space;
io_callback_t callback;
} IOMap;

并声明了读写的统一接口 map_readmap_write

NEMU 中设备的行为是我们自定义的,与 REF 中的标准设备的行为不完全一样(例如 NEMU 中的串口总是就绪的,但 QEMU 中的串口也许并不是这样),这导致在 NEMU 中执行输入指令的结果会和 REF 有所不同,所以 map.h 还另外定义了 find_mapid_by_addr 函数,其中调用了 difftest_skip_ref

map.c 中实现了对映射的管理,包括 I/O 空间的分配及其映射,及 map_readmap_write

port-io.c 是对端口映射 I/O 的模拟。add_pio_map() 函数用于为设备的初始化注册一个端口映射 I/O 的映射关系。pio_read()pio_write() 是面向 CPU 的端口 I/O 读写接口。

mmio.c 是对内存映射 I/O 的模拟。paddr.c 中的 paddr_read()paddr_write() 会判断地址 addr 落在物理内存空间还是设备空间,若落在物理内存空间,就会通过 pmem_read()pmem_write() 来访问真正的物理内存;否则就通过 map_read()map_write() 来访问相应的设备。

设备

NEMU 实现了串口、时钟、键盘、VGA、声卡、磁盘、SD 卡七种设备。

为了开启设备模拟的功能,需要在 menuconfig 选中相关选项。

NEMU 使用 SDL 库来实现设备的模拟,nemu/src/device/device.c 含有和 SDL 库相关的代码。另外还有 init_device() 函数和 device_update() 函数。init_monitor() 调用了 init_device() 函数,cpu_exec() 在执行每条指令之后就会调用 device_update() 函数。

将输入输出抽象成 IOE

IOE 提供三个 API:

bool ioe_init();
void ioe_read(int reg, void *buf);
void ioe_write(int reg, void *buf);

在 IOE 中,我们希望采用一种架构无关的“抽象寄存器”,这个 reg 其实是一个功能编号,我们约定在不同的架构中,同一个功能编号的含义也是相同的,这样就实现了设备寄存器的抽象。

abstract-machine/am/include/amdev.h 中定义了常见设备的“抽象寄存器”编号和相应的结构。

为了方便地对这些抽象寄存器进行访问,klib 中提供了 io_read()io_write() 这两个宏,它们分别对 ioe_read()ioe_write() 这两个 API 进行了进一步的封装。

#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })
#define io_write(reg, ...) \
({ reg##_T __io_param = (reg##_T) { __VA_ARGS__ }; \
ioe_write(reg, &__io_param); })

特别地,NEMU 作为一个平台,设备的行为是与 ISA 无关的,因此我们只需要在 abstract-machine/am/src/platform/nemu/ioe/ 目录下实现一份 IOE,来供 NEMU 平台的架构共享。其中,abstract-machine/am/src/platform/nemu/ioe/ioe.c 中实现了上述的三个 IOE API。

串口

实现

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);
}
}
void init_serial() {
serial_base = new_space(8);
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("serial", CONFIG_SERIAL_PORT, serial_base, 8, serial_io_handler);
#else
add_mmio_map("serial", CONFIG_SERIAL_MMIO, serial_base, 8, serial_io_handler);
#endif
}

另外补充宏的信息:

nemu/include/generated/autoconf.h:35:#define CONFIG_SERIAL_MMIO 0xa00003f8

NEMU 的框架代码默认为 MMIO。

我们来分析一下,串口初始化的时候会根据 I/O 编址方式注册端口或者分配空间。

可以看到输出的 Log 信息:

[src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'serial' at [0xa00003f8, 0xa00003ff]

对应的回调函数则调用了 serial_putc,根据 CONFIG_TARGET_AM 调用 putch 或 putc。

CONFIG_TARGET_AM 究竟是什么?

目前均为 n

测试

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

Terminal window
make ARCH=$ISA-nemu run

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

native + klib 依然是初始化问题。另外,hello.c 调用了 putstr 和 putch:

#define putstr(s) \
({ for (const char *p = s; *p; p++) putch(*p); })

而在 native AM 下,putch 依赖于 putchar:

abstract-machine/am/src/native/trm.c
void putch(char ch) {
putchar(ch);
}

注意 AM 把 putch() 放在了 TRM 中,而不是 IOE 中。

riscv32-nemu + klib 依然会出现 __strcpy_avx2 修改了监视点中 head 指针的值,导致 -> 触发段错误。

调试如下:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/hello$ gdb ../../../nemu/build/riscv32-nemu-interpreter
(gdb) watch head
Hardware watchpoint 1: head
(gdb) run --log=/home/vgalaxy/ics2021/nemu/build/nemu-log.txt ./build/hello-riscv32-nemu.bin

为此我们再次忍气吞声的将监视点相关的代码注释掉,就可以运行了…

在 nemu AM 下,putch 依赖于 outb:

abstract-machine/am/src/platform/nemu/trm.c
void putch(char ch) {
outb(SERIAL_PORT, ch);
}

outb 是 ISA 相关的,riscv 的定义在 abstract-machine/am/src/riscv/riscv.h 中:

static inline void outb(uintptr_t addr, uint8_t data) { *(volatile uint8_t *)addr = data; }

而 SERIAL_PORT 又定义在 abstract-machine/am/src/platform/nemu/include/nemu.h 中:

#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define SERIAL_PORT (DEVICE_BASE + 0x00003f8)

联系之前的回调函数,putch -> outb -> serial_io_handler -> serial_putc -> putc,实际上最终还是调用了库函数,设备只是其中的一层抽象。

native + glibc 的环境下当然也可以运行。

mainargs

在 AM 中,main() 函数允许带有一个字符串参数,这一参数通过 mainargs 指定,并由 AM 的运行时环境负责将它传给 main() 函数,供 AM 程序使用。具体的参数传递方式和架构相关。

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/hello$ make ARCH=native run mainargs=I-love-PA
# Building hello-run [native]
# Building am-archive [native]
# Building klib-archive [native]
# Creating image [native]
+ LD -> build/hello-native
/home/vgalaxy/ics2021/am-kernels/kernels/hello/build/hello-native
Hello, AbstractMachine!
mainargs = 'I-love-PA'.
Exit (0)

对于 native 而言,参数传递是在 am-kernels/kernels/nemu/Makefile 中进行的:

build_am:
$(MAKE) -C $(NEMU_HOME) $(ISA)-am_defconfig
$(MAKE) -C $(NEMU_HOME) ARCH=$(ARCH) mainargs=$(mainargs) || \
($(MAKE) restore_config; false)

而对于 $ISA-nemu,参数传递是 nemu/src/filelist.mk 中进行的:

ifdef mainargs
ASFLAGS += -DBIN_PATH=\"$(mainargs)\"
endif

配合 abstract-machine/am/src/platform/nemu/trm.c 中的 MAINARGS:

#ifndef MAINARGS
#define MAINARGS ""
#endif
static const char mainargs[] = MAINARGS;
void _trm_init() {
int ret = main(mainargs);
halt(ret);
}

printf

有了 putch(),我们就可以在 klib 中实现 printf() 了:

#define BUF_SIZE 1024
static char buf[BUF_SIZE];
static int index;
int printf(const char *fmt, ...) {
va_list ap;
int ret;
va_start(ap, fmt);
ret = vsprintf(buf, fmt, ap);
va_end(ap);
putstr(buf);
return ret;
}

限制一次传输的字符数。

另外还可以在 klib-tests 中写个测试用例:

#include <klibtest.h>
__attribute__((noinline))
static void check(bool cond) {
if (!cond) halt(1);
}
void test_printf() {
int res = printf("%s", "Hello world!\n");
check(res == 13);
res = printf("%d + %d = %d\n", 1, 1, 2);
check(res == 10);
res = printf("%d + %d = %d\n", 2, 10, 12);
check(res == 12);
res = printf("%c%c%c%c%c\n", 'R', 'T', 'F', 'S', 'C');
check(res == 6);
}

我们可以观察 putch 的反汇编结果:

80000748 <putch>:
80000748: a00007b7 lui a5,0xa0000
8000074c: 3ea78c23 sb a0,1016(a5) # a00003f8 <_end+0x1fff63f8>
80000750: 00008067 ret

可以看到串口的地址 0xa00003f8。

时钟

nemu/src/device/timer.c 模拟了 i8253 计时器的功能:

static uint32_t *rtc_port_base = NULL;
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;
}
}
#ifndef CONFIG_TARGET_AM
static void timer_intr() {
if (nemu_state.state == NEMU_RUNNING) {
extern void dev_raise_intr();
dev_raise_intr();
}
}
#endif
void init_timer() {
rtc_port_base = (uint32_t *)new_space(8);
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("rtc", CONFIG_RTC_PORT, rtc_port_base, 8, rtc_io_handler);
#else
add_mmio_map("rtc", CONFIG_RTC_MMIO, rtc_port_base, 8, rtc_io_handler);
#endif
IFNDEF(CONFIG_TARGET_AM, add_alarm_handle(timer_intr));
}

TODO: timer_intr 有什么用?

另外补充宏的信息:

nemu/include/generated/autoconf.h:13:#define CONFIG_RTC_MMIO 0xa0000048

这段 MMIO 空间会被映射到 RTC 寄存器。abstract-machine/am/include/amdev.h 中为时钟的功能定义了两个抽象寄存器:

#define AM_DEVREG(id, reg, perm, ...) \
enum { AM_##reg = (id) }; \
typedef struct { __VA_ARGS__; } AM_##reg##_T;
AM_DEVREG( 5, TIMER_RTC, RD, int year, month, day, hour, minute, second);
AM_DEVREG( 6, TIMER_UPTIME, RD, uint64_t us);

我们手动展开 TIMER_UPTIME:

enum { AM_TIMER_UPTIME = 6 };
typedef struct { uint64_t us; } AM_TIMER_UPTIME_T;

实现

下面我们需要在 abstract-machine/am/src/platform/nemu/ioe/timer.c 中实现 AM_TIMER_UPTIME 的功能,最终实现如下:

void __am_timer_init() {
}
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
uptime->us = (uint64_t) inl(RTC_ADDR) | ((uint64_t) inl(RTC_ADDR + 4)) << 32;
}

其中 inl 定义在 abstract-machine/am/src/riscv/riscv.h

static inline uint8_t inb(uintptr_t addr) { return *(volatile uint8_t *)addr; }
static inline uint16_t inw(uintptr_t addr) { return *(volatile uint16_t *)addr; }
static inline uint32_t inl(uintptr_t addr) { return *(volatile uint32_t *)addr; }
static inline void outb(uintptr_t addr, uint8_t data) { *(volatile uint8_t *)addr = data; }
static inline void outw(uintptr_t addr, uint16_t data) { *(volatile uint16_t *)addr = data; }
static inline void outl(uintptr_t addr, uint32_t data) { *(volatile uint32_t *)addr = data; }

RTC_ADDR 定义在 abstract-machine/am/src/platform/nemu/include/nemu.h

#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define SERIAL_PORT (DEVICE_BASE + 0x00003f8)
#define RTC_ADDR (DEVICE_BASE + 0x0000048)

与宏 CONFIG_RTC_MMIO 相对应。

测试

测试用例在 am-kernel/tests/am-tests 中,我们键入:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/tests/am-tests$ make ARCH=riscv32-nemu run mainargs=t

即可看到程序每隔 1 秒往终端输出一行信息。

我们来看看测试用例是啥:

#include <amtest.h>
void rtc_test() {
AM_TIMER_RTC_T rtc;
int sec = 1;
while (1) {
while(io_read(AM_TIMER_UPTIME).us / 1000000 < sec) ;
rtc = io_read(AM_TIMER_RTC);
printf("%d-%d-%d %02d:%02d:%02d GMT (", rtc.year, rtc.month, rtc.day, rtc.hour, rtc.minute, rtc.second);
if (sec == 1) {
printf("%d second).\n", sec);
} else {
printf("%d seconds).\n", sec);
}
sec ++;
}
}

分析

关键在于 io_read(AM_TIMER_UPTIME),下面分析其执行过程:

#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })

可得函数体为:

AM_TIMER_UPTIME_T __io_param;
ioe_read(AM_TIMER_UPTIME, &__io_param);
__io_param;

宏的值也许就是结构体变量 __io_param,这样才能访问其 us 成员。

参考 abstract-machine/am/src/platform/nemu/ioe/ioe.c

typedef void (*handler_t)(void *buf);
static void *lut[128] = {
[AM_TIMER_CONFIG] = __am_timer_config,
[AM_TIMER_RTC ] = __am_timer_rtc,
[AM_TIMER_UPTIME] = __am_timer_uptime,
[AM_INPUT_CONFIG] = __am_input_config,
[AM_INPUT_KEYBRD] = __am_input_keybrd,
[AM_GPU_CONFIG ] = __am_gpu_config,
[AM_GPU_FBDRAW ] = __am_gpu_fbdraw,
[AM_GPU_STATUS ] = __am_gpu_status,
[AM_UART_CONFIG ] = __am_uart_config,
[AM_AUDIO_CONFIG] = __am_audio_config,
[AM_AUDIO_CTRL ] = __am_audio_ctrl,
[AM_AUDIO_STATUS] = __am_audio_status,
[AM_AUDIO_PLAY ] = __am_audio_play,
[AM_DISK_CONFIG ] = __am_disk_config,
[AM_DISK_STATUS ] = __am_disk_status,
[AM_DISK_BLKIO ] = __am_disk_blkio,
[AM_NET_CONFIG ] = __am_net_config,
};
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }

可得实际的函数调用为:

__am_timer_uptime(&__io_param);

这便是我们要实现的函数。

另外注意到 nemu/src/device/timer.c 的回调函数中调用了 get_time(),定义在 nemu/src/utils/timer.c

#include MUXDEF(CONFIG_TIMER_GETTIMEOFDAY, <sys/time.h>, <time.h>)
static uint64_t get_time_internal() {
#if defined(CONFIG_TARGET_AM)
uint64_t us = io_read(AM_TIMER_UPTIME).us;
#elif defined(CONFIG_TIMER_GETTIMEOFDAY)
struct timeval now;
gettimeofday(&now, NULL);
uint64_t us = now.tv_sec * 1000000 + now.tv_usec;
#else
struct timespec now;
clock_gettime(CLOCK_MONOTONIC_COARSE, &now);
uint64_t us = now.tv_sec * 1000000 + now.tv_nsec / 1000;
#endif
return us;
}
uint64_t get_time() {
if (boot_time == 0) boot_time = get_time_internal();
uint64_t now = get_time_internal();
return now - boot_time;
}

相关宏的信息为:

CONFIG_TIMER_GETTIMEOFDAY=y

注意这里时间的获取取决于 CONFIG_TARGET_AM:

总体来说:

while (1) {
while(io_read(AM_TIMER_UPTIME).us / 1000000 < sec) ;
...

注意 sec 会不断增长,printf 也需要实现更多的输出格式。

姗姗来迟的忠告:不要在 native 链接到 klib 时运行 IOE 相关的测试

native 的 IOE 是基于 SDL 库实现的,它们假设常用库函数的行为会符合 glibc 标准,但我们自己实现的 klib 通常不能满足这一要求。因此 __NATIVE_USE_KLIB__ 仅供测试 klib 实现的时候使用,我们不要求在定义 __NATIVE_USE_KLIB__ 的情况下正确运行所有程序。

所以 native + klib 就是 XXX

跑分

跑分时请关闭 NEMU 的监视点,trace(注意给之前的 iringbuf 加上预编译)以及 DiffTest,以获得较为真实的跑分。

am-kernel/benchmarks/ 目录下:

make ARCH=riscv32-nemu run

Dhrystone PASS 151 Marks
vs. 100000 Marks (i7-7700K @ 4.20GHz)

注意修改了测试程序中的 uptime_ms:

static uint32_t uptime_ms() {
printf("READ: %lu ms\n", io_read(AM_TIMER_UPTIME).us / 1000);
return io_read(AM_TIMER_UPTIME).us / 1000;
}

并在 Stop timer 处增加了:

User_Time = End_Time - Begin_Time;
printf("Begin_Time %d ms\n", (int)Begin_Time);
printf("End_Time %d ms\n", (int)End_Time);

可以观察输出:

READ: 0
READ: 0
Begin_Time 1 ms
End_Time 7044 ms
Finished in 7043 ms

很奇怪,若不修改 uptime_ms,由于 User_Time 会变得极小,跑分就会极高:

printf("Dhrystone %s %d Marks\n", pass ? "PASS" : "FAIL",
880900 / (int)User_Time * NUMBER_OF_RUNS/ 500000);

make ARCH=native run

Dhrystone PASS 58726 Marks
vs. 100000 Marks (i7-7700K @ 4.20GHz)

make ARCH=riscv32-nemu run

CoreMark PASS 427 Marks
vs. 100000 Marks (i7-7700K @ 4.20GHz)

同样需要修改 core_portme.c 中的 uptime_ms

make ARCH=native run

CoreMark PASS 42961 Marks
vs. 100000 Marks (i7-7700K @ 4.20GHz)

make ARCH=riscv32-nemu run

MicroBench PASS 409 Marks
vs. 100000 Marks (i9-9900K @ 3.60GHz)

同样需要修改 uptime_ms

make ARCH=native run

MicroBench PASS 49104 Marks
vs. 100000 Marks (i9-9900K @ 3.60GHz)
Scored time: 274.585 ms
Total time: 354.225 ms

可以通过 mainargs 指定参数,包括 test, train, refhuge

microbench 中有一个叫 bf 的测试项目,它是 Brainf**k 语言的一个解释器。

跑分好差…

先完成,后完美 - 抑制住优化代码的冲动

运行红白机模拟器

malloc,之前搭 klib 测试的框架时已经实现了:

static bool is_init_addr = false;
static void *addr;
void init_addr() {
addr = (void *)ROUNDUP(heap.start, 8);
is_init_addr = true;
}
void *malloc(size_t size) {
if (!is_init_addr) init_addr();
size = (size_t)ROUNDUP(size, 8);
char *old = addr;
addr += size;
assert((uintptr_t)heap.start <= (uintptr_t)addr && (uintptr_t)addr < (uintptr_t)heap.end);
for (uint64_t *p = (uint64_t *)old; p != (uint64_t *)addr; p ++) {
*p = 0;
}
return old;
}

正确实现时钟后,你就可以在 NEMU 上运行一个字符版本的 FCEUX 了。修改 fceux-am/src/config.h 中的代码,把 HAS_GUI 宏注释掉,FCEUX 就会通过 putch() 来输出画面:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/fceux-am$ make ARCH=riscv32-nemu run mainargs=mario

极具艺术感…

设备访问的踪迹 - dtrace

类似 mtrace,定义了宏后,只要修改 map.c 即可:

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);
IFDEF(CONFIG_DTRACE, Log("address = " FMT_PADDR " read " FMT_PADDR " at device = %s", addr, ret, map->name));
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);
IFDEF(CONFIG_DTRACE, Log("address = " FMT_PADDR " write " FMT_PADDR " at device = %s", addr, data, map->name));
}

对于 hello,设备访问信息如下:

H[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000048 at device = serial
e[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000065 at device = serial
l[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006c at device = serial
l[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006c at device = serial
o[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006f at device = serial
,[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000002c at device = serial
[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000020 at device = serial
A[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000041 at device = serial
b[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000062 at device = serial
s[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000073 at device = serial
t[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000074 at device = serial
r[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000072 at device = serial
a[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000061 at device = serial
c[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000063 at device = serial
t[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000074 at device = serial
M[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000004d at device = serial
a[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000061 at device = serial
c[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000063 at device = serial
h[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000068 at device = serial
i[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000069 at device = serial
n[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006e at device = serial
e[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000065 at device = serial
![src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000021 at device = serial
[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000000a at device = serial
m[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006d at device = serial
a[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000061 at device = serial
i[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000069 at device = serial
n[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000006e at device = serial
a[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000061 at device = serial
r[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000072 at device = serial
g[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000067 at device = serial
s[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000073 at device = serial
[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000020 at device = serial
=[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000003d at device = serial
[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000020 at device = serial
'[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000027 at device = serial
'[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x00000027 at device = serial
.[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000002e at device = serial
[src/device/io/map.c:56 map_write] address = 0xa00003f8 write 0x0000000a at device = serial
[src/cpu/cpu-exec.c:151 cpu_exec] nemu: HIT GOOD TRAP at pc = 0x800000cc
[src/cpu/cpu-exec.c:68 statistic] host time spent = 2,963 us
[src/cpu/cpu-exec.c:69 statistic] total guest instructions = 390
[src/cpu/cpu-exec.c:70 statistic] simulation frequency = 131,623 instr/s

就是不断向地址 0xa00003f8 写入字符。

而对于时钟,设备访问信息如下:

[src/device/io/map.c:46 map_read] address = 0xa0000048 read 0x00000000 at device = rtc
[src/device/io/map.c:46 map_read] address = 0xa000004c read 0x00000000 at device = rtc
[src/device/io/map.c:46 map_read] address = 0xa0000048 read 0x00000da6 at device = rtc
[src/device/io/map.c:46 map_read] address = 0xa000004c read 0x00000000 at device = rtc
...

先读取低四字节,再读取高四字节。读取的数字会飞速增长,读取的速度也很快(导致 Log 容量瞬间爆炸)。

当到达每一个整秒时,通过 serial 写入字符。

键盘

nemu/src/device/keyboard.c 模拟了 i8042 通用设备接口芯片的功能:

static uint32_t *i8042_data_port_base = NULL;
static void i8042_data_io_handler(uint32_t offset, int len, bool is_write) {
assert(!is_write);
assert(offset == 0);
i8042_data_port_base[0] = key_dequeue();
}
void init_i8042() {
i8042_data_port_base = (uint32_t *)new_space(4);
i8042_data_port_base[0] = _KEY_NONE;
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("keyboard", CONFIG_I8042_DATA_PORT, i8042_data_port_base, 4, i8042_data_io_handler);
#else
add_mmio_map("keyboard", CONFIG_I8042_DATA_MMIO, i8042_data_port_base, 4, i8042_data_io_handler);
#endif
IFNDEF(CONFIG_TARGET_AM, init_keymap());
}

另外补充宏的信息:

nemu/include/generated/autoconf.h:38:#define CONFIG_I8042_DATA_MMIO 0xa0000060

abstract-machine/am/include/amdev.h 中为键盘的功能定义了一个抽象寄存器:

当按下一个键的时候,键盘将会发送该键的通码 (make code)

当释放一个键的时候,键盘将会发送该键的断码 (break code)

实现

下面我们需要在 abstract-machine/am/src/platform/nemu/ioe/input.c 中实现 AM_INPUT_KEYBRD 的功能,最终实现如下:

#define KEYDOWN_MASK 0x8000
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
kbd->keycode = inl(KBD_ADDR);
if (kbd->keycode & KEYDOWN_MASK) {
kbd->keycode ^= KEYDOWN_MASK;
kbd->keydown = 1;
} else {
kbd->keydown = 0;
}
}

KBD_ADDR 定义在 abstract-machine/am/src/platform/nemu/include/nemu.h

#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define KBD_ADDR (DEVICE_BASE + 0x0000060)

大概思路是,由于 KBD_ADDR 中实际同时编码了通码和断码:

我们可以通过 dtrace 观察:

[src/device/io/map.c:46 map_read] address = 0xa0000060 read 0x00000038 at device = keyboard

测试程序打印的结果为 Got (kbd): Z (56) UP

[src/device/io/map.c:46 map_read] address = 0xa0000060 read 0x0000803a at device = keyboard

测试程序打印的结果为 Got (kbd): C (58) DOWN

测试

测试用例在 am-kernel/tests/am-tests 中,我们键入:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/tests/am-tests$ make ARCH=riscv32-nemu run mainargs=k

程序输出相应的按键信息,包括按键名、键盘码以及按键状态。

观察测试程序:

#define NAMEINIT(key) [ AM_KEY_##key ] = #key,
static const char *names[] = {
AM_KEYS(NAMEINIT)
};
static bool has_uart, has_kbd;
static void drain_keys() {
if (has_uart) {
while (1) {
char ch = io_read(AM_UART_RX).data;
if (ch == -1) break;
printf("Got (uart): %c (%d)\n", ch, ch & 0xff);
}
}
if (has_kbd) {
while (1) {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
if (ev.keycode == AM_KEY_NONE) break;
printf("Got (kbd): %s (%d) %s\n", names[ev.keycode], ev.keycode, ev.keydown ? "DOWN" : "UP");
}
}
}
void keyboard_test() {
printf("Try to press any key (uart or keyboard)...\n");
has_uart = io_read(AM_UART_CONFIG).present;
has_kbd = io_read(AM_INPUT_CONFIG).present;
while (1) {
drain_keys();
}
}

其中两个寄存器的回调函数定义在 abstract-machine/am/src/platform/nemu/ioe/ioe.c

static void __am_input_config(AM_INPUT_CONFIG_T *cfg) { cfg->present = true; }
static void __am_uart_config(AM_UART_CONFIG_T *cfg) { cfg->present = false; }

而宏 AM_KEYS 定义在 abstract-machine/am/include/amdev.h

多个键同时被按下

回到 nemu/src/device/keyboard.c

在非 AM 下,首先需要解读一下宏:

// Note that this is not the standard
#define _KEYS(f) \
f(ESCAPE) f(F1) f(F2) f(F3) f(F4) f(F5) f(F6) f(F7) f(F8) f(F9) f(F10) f(F11) f(F12) \
f(GRAVE) f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9) f(0) f(MINUS) f(EQUALS) f(BACKSPACE) \
f(TAB) f(Q) f(W) f(E) f(R) f(T) f(Y) f(U) f(I) f(O) f(P) f(LEFTBRACKET) f(RIGHTBRACKET) f(BACKSLASH) \
f(CAPSLOCK) f(A) f(S) f(D) f(F) f(G) f(H) f(J) f(K) f(L) f(SEMICOLON) f(APOSTROPHE) f(RETURN) \
f(LSHIFT) f(Z) f(X) f(C) f(V) f(B) f(N) f(M) f(COMMA) f(PERIOD) f(SLASH) f(RSHIFT) \
f(LCTRL) f(APPLICATION) f(LALT) f(SPACE) f(RALT) f(RCTRL) \
f(UP) f(DOWN) f(LEFT) f(RIGHT) f(INSERT) f(DELETE) f(HOME) f(END) f(PAGEUP) f(PAGEDOWN)
#define _KEY_NAME(k) _KEY_##k,
enum {
_KEY_NONE = 0,
MAP(_KEYS, _KEY_NAME)
};
#define SDL_KEYMAP(k) keymap[concat(SDL_SCANCODE_, k)] = concat(_KEY_, k);
static uint32_t keymap[256] = {};
static void init_keymap() {
MAP(_KEYS, SDL_KEYMAP)
}

最终实际得到了一个映射表,如 keymap[SDL_SCANCODE_F1] = _KEY_F1,值为枚举。

回调函数调用了 key_dequeue 函数,解析队列中第一个的按键:

#define KEY_QUEUE_LEN 1024
static int key_queue[KEY_QUEUE_LEN] = {};
static int key_f = 0, key_r = 0;
static void key_enqueue(uint32_t am_scancode) {
key_queue[key_r] = am_scancode;
key_r = (key_r + 1) % KEY_QUEUE_LEN;
Assert(key_r != key_f, "key queue overflow!");
}
static uint32_t key_dequeue() {
uint32_t key = _KEY_NONE;
if (key_f != key_r) {
key = key_queue[key_f];
key_f = (key_f + 1) % KEY_QUEUE_LEN;
}
return key;
}
void send_key(uint8_t scancode, bool is_keydown) {
if (nemu_state.state == NEMU_RUNNING && keymap[scancode] != _KEY_NONE) {
uint32_t am_scancode = keymap[scancode] | (is_keydown ? KEYDOWN_MASK : 0);
key_enqueue(am_scancode);
}
}

另外,nemu/src/device/device.c 中的 device_update 调用了 send_key,将被按下的按键的信息放到队列末尾:

case SDL_KEYDOWN:
case SDL_KEYUP: {
uint8_t k = event.key.keysym.scancode;
bool is_keydown = (event.key.type == SDL_KEYDOWN);
send_key(k, is_keydown);
break;
}

通常情况下,多个键被按下的间隔必定大于 device_update 中处理事件队列的时间间隔,所以,这些键都会被单独解析出来。

所以只要设定一个间隔值,将间隔小于该值的多个键视为同时被按下即可。

运行红白机模拟器

可以使用键盘操控字符版本的红白机模拟器了。

VGA

nemu/src/device/vga.c 模拟了 VGA 的功能:

void init_vga() {
vgactl_port_base = (uint32_t *)new_space(8);
vgactl_port_base[0] = (screen_width() << 16) | screen_height();
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("vgactl", CONFIG_VGA_CTL_PORT, vgactl_port_base, 8, NULL);
#else
add_mmio_map("vgactl", CONFIG_VGA_CTL_MMIO, vgactl_port_base, 8, NULL);
#endif
vmem = new_space(screen_size());
add_mmio_map("vmem", CONFIG_FB_ADDR, vmem, screen_size(), NULL);
IFDEF(CONFIG_VGA_SHOW_SCREEN, init_screen());
IFDEF(CONFIG_VGA_SHOW_SCREEN, memset(vmem, 0, screen_size()));
}

另外补充宏的信息:

nemu/include/generated/autoconf.h:49:#define CONFIG_VGA_CTL_MMIO 0xa0000100
nemu/include/generated/autoconf.h:27:#define CONFIG_FB_ADDR 0xa1000000

可以对应 abstract-machine/am/src/platform/nemu/include/nemu.h

#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define MMIO_BASE 0xa0000000
#define VGACTL_ADDR (DEVICE_BASE + 0x0000100)
#define FB_ADDR (MMIO_BASE + 0x1000000)

abstract-machine/am/include/amdev.h 中为 GPU 定义了五个抽象寄存器,在 NEMU 中只会用到其中的两个:

AM_DEVREG( 9, GPU_CONFIG, RD, bool present, has_accel; int width, height, vmemsz);
AM_DEVREG(10, GPU_STATUS, RD, bool ready);
AM_DEVREG(11, GPU_FBDRAW, WR, int x, y; void *pixels; int w, h; bool sync);
AM_DEVREG(12, GPU_MEMCPY, WR, uint32_t dest; void *src; int size);
AM_DEVREG(13, GPU_RENDER, WR, uint32_t root);

GPU_STATUS 也许对应 SYNC_ADDR。

GPU_MEMCPY 暂时未使用,也许在 __am_gpu_fbdraw 中 copy 时会有用。

实现

abstract-machine/am/src/platform/nemu/ioe/gpu.c

#define SYNC_ADDR (VGACTL_ADDR + 4)
void __am_gpu_init() {
}
void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
// uint16_t w = (inl(VGACTL_ADDR) & 0xff00) >> 16;
// uint16_t h = inl(VGACTL_ADDR) & 0x00ff;
uint16_t w = 400;
uint16_t h = 300;
*cfg = (AM_GPU_CONFIG_T) {
.present = true, .has_accel = false,
.width = w,
.height = h,
.vmemsz = w * h * sizeof(uint32_t)
};
}
#include <klib.h>
void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
// uint16_t W = (inl(VGACTL_ADDR) & 0xff00) >> 16;
// uint16_t H = inl(VGACTL_ADDR) & 0x00ff;
uint16_t W = 400;
uint16_t H = 300;
int x = ctl->x;
int y = ctl->y;
int w = ctl->w;
int h = ctl->h;
uint32_t * base = (uint32_t *) ctl->pixels;
int cp_bytes = sizeof(uint32_t) * (w < W - x ? w : W - x);
for (int j = 0; j < h && y + j < H; ++j) {
memcpy(&fb[(y + j) * W + x], base, cp_bytes);
base += w;
}
//for (int j = 0; j < h; ++j)
// for (int i = 0; i < w; ++i)
// fb[(y + j) * W + x + i] = base[j * w + i];
if (ctl->sync) {
outl(SYNC_ADDR, 1);
}
// putstr("__am_gpu_fbdraw\n");
}
void __am_gpu_status(AM_GPU_STATUS_T *status) {
// status->ready = true;
status->ready = (bool) inl(SYNC_ADDR);
putstr("__am_gpu_status\n");
}

屏幕的尺寸信息可以通过观察宏 CONFIG_VGA_SIZE_800x600 得到。

通过 inl 读取似乎会出错,于是采用硬编码的方式

copy 时小心越界。

另外 __am_gpu_status 似乎没有被调用过,因为没有 io_read(AM_GPU_STATUS).ready

nemu/src/device/vga.c

void vga_update_screen() {
// TODO: call `update_screen()` when the sync register is non-zero,
// then zero out the sync register
#ifndef CONFIG_TARGET_AM
// puts("vga_update_screen");
if (vgactl_port_base[1]) {
update_screen();
vgactl_port_base[1] = 0;
}
#elif
if (io_read(AM_GPU_STATUS).ready) {
update_screen();
// io_write(AM_GPU_STATUS, false);
vgactl_port_base[1] = 0;
}
#endif
}

nemu/src/device/device.c 中的 device_update 调用了 vga_update_screen。

测试

测试用例在 am-kernel/tests/am-tests 中,我们键入:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/tests/am-tests$ make ARCH=riscv32-nemu run mainargs=v

观察测试程序:

void video_test() {
unsigned long last = 0;
unsigned long fps_last = 0;
int fps = 0;
while (1) {
unsigned long upt = io_read(AM_TIMER_UPTIME).us / 1000;
if (upt - last > 1000 / FPS) {
update();
redraw();
last = upt;
fps ++;
}
if (upt - fps_last > 1000) {
// display fps every 1s
printf("%d: FPS = %d\n", upt, fps);
fps_last = upt;
fps = 0;
}
}
}

update 是更新像素信息,关键在 redraw:

void redraw() {
int w = io_read(AM_GPU_CONFIG).width / N;
int h = io_read(AM_GPU_CONFIG).height / N;
int block_size = w * h;
assert((uint32_t)block_size <= LENGTH(color_buf));
int x, y, k;
for (y = 0; y < N; y ++) {
for (x = 0; x < N; x ++) {
for (k = 0; k < block_size; k ++) {
color_buf[k] = canvas[y][x];
}
io_write(AM_GPU_FBDRAW, x * w, y * h, color_buf, w, h, false);
}
}
io_write(AM_GPU_FBDRAW, 0, 0, NULL, 0, 0, true);
}

通过写 AM_GPU_FBDRAW,来更新像素信息。

我们可以通过 dtrace 观察:

[src/device/io/map.c:56 map_write] address = 0xa10005a6 write 0x0000006c at device = vmem

大概就是不断的写 vmem,一直等到可以刷新后再显示在屏幕上。

FPS 应该是 1s 内 vga_update_screen 的调用次数。

还可以通过 ftrace 观察,最好固定缩进,便于观察:

0x8000000c: call [_trm_init@0x80000fb8]
0x80000fc8: call [main@0x80000dd8]
0x80000e18: call [main@0x80000f84]
0x80000f94: call [ioe_init@0x8000106c]
0x80001088: call [ioe_init@0x80001094]
0x800010a8: call [__am_gpu_init@0x80001218]
0x80001218: ret [__am_gpu_init]
0x800010ac: call [__am_timer_init@0x800010fc]
0x80001118: ret [__am_timer_init]
0x800010b0: call [__am_audio_init@0x8000137c]
0x8000137c: ret [__am_audio_init]
0x800010c0: ret [ioe_init]
0x80000f98: call [video_test@0x800004c0]
0x80000504: call [ioe_read@0x800010c4]
0x800010dc: call [__am_timer_uptime@0x8000111c]
0x80001158: ret [__am_timer_uptime]
0x80000518: call [__udivdi3@0x80001f14]
0x80001f18: call [__udivmoddi4@0x80001bf8]
0x80001c70: ret [__udivmoddi4]
...
0x8000054c: call [update@0x800002e4]
0x8000044c: call [update@0x800003b8]
...
0x800004bc: ret [update]
0x80000554: call [redraw@0x80000164]
0x8000019c: call [ioe_read@0x800010c4]
0x800010dc: call [__am_gpu_config@0x8000121c]
0x80001240: ret [__am_gpu_config]
0x800001b8: call [ioe_read@0x800010c4]
0x800010dc: call [__am_gpu_config@0x8000121c]
0x80001240: ret [__am_gpu_config]
0x80000254: call [ioe_write@0x800010e0]
0x800010f8: call [__am_gpu_fbdraw@0x80001244]
0x800012cc: call [__am_gpu_fbdraw@0x800012dc]
0x800012e8: call [memcpy@0x800016c8]
0x800016e8: ret [memcpy]
...

TODO

读写对应

如 AM_GPU_STATUS

如 AM_GPU_FBDRAW

另外,只有定义了 CONFIG_TARGET_AM,才能够使用 io_read 和 io_write。

违背这种对应会发生什么,我们考虑 io_write(AM_GPU_STATUS, false),通过下面的信息展开:

#define io_write(reg, ...) \
({ reg##_T __io_param = (reg##_T) { __VA_ARGS__ }; \
ioe_write(reg, &__io_param); })

即为:

AM_GPU_STATUS_T __io_param = (AM_GPU_STATUS_T) {.ready = false};
ioe_write(AM_GPU_STATUS, &__io_param);

再展开 ioe_write:

void ioe_write(int reg, void *buf) { ((handler_t)lut[reg])(buf); }

即为 __am_gpu_status(&__io_param)

void __am_gpu_status(AM_GPU_STATUS_T *status) {
// status->ready = true;
status->ready = (bool) inl(SYNC_ADDR);
putstr("__am_gpu_status\n");
}

可以发现语义恰好相反…

运行红白机模拟器

恢复 HAS_GUI 宏,就可以使用键盘操控图形版本的红白机模拟器了。

FPS 大概 20+

声卡

在 NEMU 中,我们根据 SDL 库的 API 来设计一个简单的声卡设备。

使用 SDL 库来播放音频的过程非常简单:

  1. 通过 SDL_OpenAudio() 来初始化音频子系统,需要提供频率、格式等参数,还需要注册一个用于将来填充音频数据的回调函数
  2. SDL 库会定期调用初始化时注册的回调函数,并提供一个缓冲区,请求回调函数往缓冲区中写入音频数据
  3. 回调函数返回后,SDL 库就会按照初始化时提供的参数来播放缓冲区中的音频数据

在 AM 中,abstract-machine/am/include/amdev.h 中为声卡定义了四个抽象寄存器:

AM_DEVREG(14, AUDIO_CONFIG, RD, bool present; int bufsize);
AM_DEVREG(15, AUDIO_CTRL, WR, int freq, channels, samples);
AM_DEVREG(16, AUDIO_STATUS, RD, int count);
AM_DEVREG(17, AUDIO_PLAY, WR, Area buf);

宏信息如下:

#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define MMIO_BASE 0xa0000000
#define AUDIO_ADDR (DEVICE_BASE + 0x0000200)
#define AUDIO_SBUF_ADDR (MMIO_BASE + 0x1200000)

还有一个:

nemu/include/generated/autoconf.h:39:#define CONFIG_SB_SIZE 0x10000

实现

nemu/src/device/audio.c

首先是注册相关的空间:

enum {
reg_freq,
reg_channels,
reg_samples,
reg_sbuf_size,
reg_init,
reg_count,
nr_reg // 6
};
static uint8_t *sbuf = NULL;
static uint32_t *audio_base = NULL;
void init_audio() {
uint32_t space_size = sizeof(uint32_t) * nr_reg;
audio_base = (uint32_t *)new_space(space_size);
#ifdef CONFIG_HAS_PORT_IO
add_pio_map ("audio", CONFIG_AUDIO_CTL_PORT, audio_base, space_size, audio_io_handler);
#else
add_mmio_map("audio", CONFIG_AUDIO_CTL_MMIO, audio_base, space_size, audio_io_handler);
#endif
sbuf = (uint8_t *)new_space(CONFIG_SB_SIZE);
add_mmio_map("audio-sbuf", CONFIG_SB_ADDR, sbuf, CONFIG_SB_SIZE, NULL);
}

nemu/src/device/device.c 中的 init_device 调用了 init_audio。

注意到 audio 控制相关的寄存器有一个回调函数,目的是为了初始化音频子系统

static void init_audio_sdl() {
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 = audio_play;
SDL_InitSubSystem(SDL_INIT_AUDIO);
SDL_OpenAudio(&s, NULL);
SDL_PauseAudio(0);
// puts("init_audio_sdl");
}
static void audio_io_handler(uint32_t offset, int len, bool is_write) {
if (audio_base[reg_init] == false) {
init_audio_sdl();
audio_base[reg_init] = true;
}
}

这里音频子系统的回调函数如下:

#include <stdio.h>
static void audio_play(void *userdata, uint8_t *stream, int len) {
int nread = len;
int count = audio_base[reg_count];
if (count < len) nread = count;
memcpy(stream, sbuf, nread);
if (len > nread) memset(stream + nread, 0, len - nread);
int after = count - nread;
audio_base[reg_count] = after;
for (int i = 0; i < count - nread; ++i)
sbuf[i] = sbuf[i + nread];
memset(sbuf + count - nread, 0, nread);
printf("output: %d %d\n", count, after);
}

userdata 不使用。

SDL 库参考在下面

注意当回调函数需要的数据量大于当前流缓冲区中的数据量,需要把 SDL 提供的缓冲区剩余的部分清零。

读取流缓冲区的前 nread 字节后,将流缓冲区整体移动,并清零后面多余的部分。

abstract-machine/am/src/platform/nemu/ioe/audio.c

补充定义了一些宏:

#define AUDIO_FREQ_ADDR (AUDIO_ADDR + 0x00)
#define AUDIO_CHANNELS_ADDR (AUDIO_ADDR + 0x04)
#define AUDIO_SAMPLES_ADDR (AUDIO_ADDR + 0x08)
#define AUDIO_SBUF_SIZE_ADDR (AUDIO_ADDR + 0x0c)
#define AUDIO_INIT_ADDR (AUDIO_ADDR + 0x10)
#define AUDIO_COUNT_ADDR (AUDIO_ADDR + 0x14)
#define CONFIG_SB_SIZE 0x10000

首先两个 RD 的寄存器:

void __am_audio_config(AM_AUDIO_CONFIG_T *cfg) {
cfg->present = true;
cfg->bufsize = CONFIG_SB_SIZE; // unit: bytes
}
void __am_audio_status(AM_AUDIO_STATUS_T *stat) {
stat->count = inl(AUDIO_COUNT_ADDR);
}

然后是两个 WR 的寄存器:

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);
}
void __am_audio_play(AM_AUDIO_PLAY_T *ctl) {
Area buf = ctl->buf;
int len = buf.end - buf.start;
int count = inl(AUDIO_COUNT_ADDR);
if (len + count > CONFIG_SB_SIZE) {
printf("wait\n");
return; // wait
}
uint8_t *base = (uint8_t *)AUDIO_SBUF_ADDR;
base += count;
memcpy(base, buf.start, len);
int after = count + len;
outl(AUDIO_COUNT_ADDR, after);
printf("input: %d %d\n", count, after);
}

注意若当前流缓冲区的空闲空间少于即将写入的音频数据,此次写入将会一直等待,直到有足够的空闲空间将音频数据完全写入流缓冲区才会返回。

这里的实现是直接返回了,实际上应该用一个 while 语句。

abstract-machine/am/src/native/ioe/audio.c

#include <fcntl.h>
#include <unistd.h>

测试

测试用例在 am-kernel/tests/am-tests 中,我们键入:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/tests/am-tests$ make ARCH=riscv32-nemu run mainargs=a

观察测试程序:

#include <amtest.h>
void audio_test() {
if (!io_read(AM_AUDIO_CONFIG).present) {
printf("WARNING: %s does not support audio\n", TOSTRING(__ARCH__));
return;
}
io_write(AM_AUDIO_CTRL, 8000, 1, 1024);
extern uint8_t audio_payload, audio_payload_end;
uint32_t audio_len = &audio_payload_end - &audio_payload;
int nplay = 0;
Area sbuf;
sbuf.start = &audio_payload;
while (nplay < audio_len) {
int len = (audio_len - nplay > 4096 ? 4096 : audio_len - nplay);
sbuf.end = sbuf.start + len;
io_write(AM_AUDIO_PLAY, sbuf);
sbuf.start += len;
nplay += len;
printf("Already play %d/%d bytes of data\n", nplay, audio_len);
}
// wait until the audio finishes
while (io_read(AM_AUDIO_STATUS).count > 0);
}

一次最多写 4096 字节。

软件、硬件和 AM 程序对数据的读写

读写冲突

因为两个读写相关的回调函数 __am_audio_playaudio_play 都依赖于当前流缓冲区已经使用的大小,但是回调函数的调用时机并不确定,所以 audio_base[reg_count] 的值可能并不能在硬件和软件之间实现同步。

我们在两个回调函数中添加了打印语句观察 audio_base[reg_count] 值的变化:

input: 0 4096
output: 4096 2048
input: 4096 8192

可以发现值并未实现同步。

后续:

FFmpeg

https://zhuanlan.zhihu.com/p/113248454

使用 ffmpeg 查看 mp3 文件的一些信息,比如采样率、声道数等:

vgalaxy@vgalaxy-VirtualBox:~/Downloads$ ffmpeg -i demo.mp3

最重要的信息如下:

Input #0, mp3, from 'demo.mp3':
Metadata:
title : たゆたえ、七色
album : たゆたえ、七色
TSRC : JPI102101591
comment : ExactAudioCopy v1.3
genre : anime
DISCID : 0B01D102
track : 1/2
artist : ARCANA PROJECT
catalog : 4540774241378
date : 2021
Duration: 00:04:01.48, start: 0.025056, bitrate: 325 kb/s
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 320 kb/s
Metadata:
encoder : LAME3.99r
Side data:
replaygain: track gain - -8.100000, track peak - unknown, album gain - unknown, album peak - unknown,
Stream #0:1: Video: mjpeg (Progressive), yuvj420p(pc, bt470bg/unknown/unknown), 1000x987 [SAR 1:1 DAR 1000:987], 90k tbr, 90k tbn, 90k tbc (attached pic)
Metadata:
comment : Cover (front)

可以观察到 mp3 文件采样率是 44100Hz,双声道

使用 ffmpeg 转换时要用到上面的信息。

vgalaxy@vgalaxy-VirtualBox:~/Downloads$ ffmpeg -i demo.mp3 -acodec pcm_s16le -f s16le -ac 2 -ar 44100 demo.pcm

使用的参数如下(并不确定如何选取):

指定编码器

-acodec codec (input/output)
Set the audio codec. This is an alias for "-codec:a".

指定文件格式,是大端模式还是小端模式

-f fmt (input/output)
Force input or output file format. The format is normally auto detected for input files and guessed from the file extension for output files, so this option is not needed in most cases.

指定通道数,2 代表双通道

-ac[:stream_specifier] channels (input/output,per-stream)
Set the number of audio channels. For output streams it is set by default to the number of input audio channels. For input streams this option only makes sense for audio grabbing devices and raw demuxers and is mapped to the corresponding demuxer options.

指定采样率,这里是 44100Hz

-ar[:stream_specifier] freq (input/output,per-stream)
Set the audio sampling frequency. For output streams it is set by default to the frequency of the corresponding input stream. For input streams this option only makes sense for audio grabbing devices and raw demuxers and is mapped to the corresponding demuxer options.

得到 pcm 文件后,我们可以试听一下:

vgalaxy@vgalaxy-VirtualBox:~/Downloads$ ffplay -ar 44100 -channels 2 -f s16le -i demo.pcm

感觉不错…

然后,我们修改测试程序中的参数:

io_write(AM_AUDIO_CTRL, 44100, 2, 1024);

并修改 am-kernels/tests/am-tests/src/tests/audio/audio-data.S

.section .data
.global audio_payload, audio_payload_end
.p2align 3
audio_payload:
.incbin "src/tests/audio/little-star.pcm"
//.incbin "/home/vgalaxy/Downloads/demo.pcm"
audio_payload_end:

就可以进行全损播放了…

应该还要改 format 为 AUDIO_S16SYS,与上述的文件格式对应

SDL

参考:

重点关注 SDL_AudioSpec 中的 freq / format / samples。

运行红白机模拟器

正确实现声卡后,就可以在 NEMU 上运行带音效的 FCEUX 了。

fceux-am/src/config.h 中提供了一些配置选项,其中音效有三种配置,分别是高质量 (SOUND_HQ),低质量 (SOUND_LQ) 以及无音效 (SOUND_NONE)。

NEMU 平台默认选择低质量,以节省 FCEUX 的音效解码时间。

然而似乎会卡在音频初始化的地方…

PA2 Report

冯 · 诺依曼计算机系统

make ARCH=riscv32-nemu run

这些游戏都可以抽象成一个死循环:

while (1) {
等待新的一帧 (); // AM_TIMER_UPTIME
处理用户按键 (); // AM_INPUT_KEYBRD
更新游戏逻辑 (); // TRM
绘制新的屏幕 (); // AM_GPU_FBDRAW
}

RTFSC 指南

另一个值得 RTFSC 的项目是 LiteNES,可以看成是 NEMU 和 AM 程序的融合。

参考:

在 NEMU 上运行 NEMU

am-kernels/kernels/nemu/ 目录下:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/nemu$ make ARCH=riscv32-nemu mainargs=/home/vgalaxy/ics2021/am-kernels/kernels/hello/build/hello-riscv32-nemu.bin

它会进行如下的工作:

  1. 保存 NEMU 当前的配置选项
  2. 加载一个新的配置文件,将 NEMU 编译到 AM 上,并把 mainargs 指示 bin 文件作为这个 NEMU 的镜像文件
  3. 恢复第 1 步中保存的配置选项
  4. 重新编译 NEMU,并把第 2 步中的 NEMU 作为镜像文件来运行

第 2 步把 NEMU 编译到 AM 时,配置系统会定义宏 CONFIG_TARGET_AM,此时 NEMU 的行为和之前相比有所变化:

程序观察,在 (nemu) 中键入 c 后,会有如下的显示信息:

[/home/vgalaxy/ics2021/nemu/src/memory/paddr.c:36 init_mem] physical memory area [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/monitor/monitor.c:280 load_img] img size = %ld
[/home/vgalaxy/ics2021/nemu/src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'serial' at [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'rtc' at [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'vgactl' at [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'vmem' at [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/device/io/mmio.c:18 add_mmio_map] Add mmio map 'keyboard' at [0x%08x, 0x%08x]
[/home/vgalaxy/ics2021/nemu/src/monitor/monitor.c:13 welcome] Trace: ON
[/home/vgalaxy/ics2021/nemu/src/monitor/monitor.c:14 welcome] If trace is enabled, a log file will be generated to record the trace. This may lead to a large log file. If it is not necessary, you can disable it in menuconfig
[/home/vgalaxy/ics2021/nemu/src/monitor/monitor.c:17 welcome] Build time: 19:51:09, Oct 25 2021
Welcome to riscv32-NEMU!
For help, type "help"
Hello, AbstractMachine!
mainargs = ''.
[/home/vgalaxy/ics2021/nemu/src/cpu/cpu-exec.c:151 cpu_exec] nemu: HIT GOOD TRAP at pc = 0x%08x
[/home/vgalaxy/ics2021/nemu/src/cpu/cpu-exec.c:68 statistic] host time spent = %ld us
[/home/vgalaxy/ics2021/nemu/src/cpu/cpu-exec.c:69 statistic] total guest instructions = %ld
[/home/vgalaxy/ics2021/nemu/src/cpu/cpu-exec.c:70 statistic] simulation frequency = %ld instr/s

注意这些信息是程序本身的输出,对于外层的 nemu,也会有类似的日志信息。区别在于:

由于 printf 尚未实现完全,所以有些格式化信息没有显示出来。

printf continue

实现详见代码。

功能参考:

#include <stdio.h>
int main() {
printf("*%d*\n", 12345);
printf("*%08d*\n", 12345);
printf("*%.8d*\n", 12345);
printf("*%-8d*\n", 12345);
printf("*%+d*\n", 12345);
printf("*%+d*\n", -12345);
printf("*% 8d*\n", 12345);
printf("*%8d*\n", 12345);
printf("%.0d\n", 0);
printf("%0d\n", 0);
}

TODO:

必答题

程序是个状态机

void idex() {
inst_t *cur = (inst_t *)&M[pc];
switch (cur->rtype.op) {
case 0b0000: { RTYPE(cur); R[rt] = R[rs]; pc++; break; }
case 0b0001: { RTYPE(cur); R[rt] += R[rs]; pc++; break; }
case 0b1110: { MTYPE(cur); R[RA] = M[addr]; pc++; break; }
case 0b1111: { MTYPE(cur); M[addr] = R[RA]; pc++; break; }
default: assert(0);
}
}
  1. 取指
  2. 译码
  3. 执行
  4. 更新 PC

经典再现:

typedef union inst {
struct { u8 rs : 2, rt: 2, op: 4; } rtype;
struct { u8 addr: 4, op: 4; } mtype;
} inst_t;
#define RTYPE(i) u8 rt = (i)->rtype.rt, rs = (i)->rtype.rs;
#define MTYPE(i) u8 addr = (i)->mtype.addr;

union + struct + bit fields

RTFSC

static void fetch_decode_exec_updatepc(Decode *s) {
fetch_decode(s, cpu.pc);
s->EHelper(s);
cpu.pc = s->dnpc;
}

fetch_decode 为 ISA 无关:

void fetch_decode(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
int idx = isa_fetch_decode(s);
s->dnpc = s->snpc;
s->EHelper = g_exec_table[idx];
...
}

通过 isa_fetch_decode 得到指令执行的辅助函数:

int isa_fetch_decode(Decode *s) {
s->isa.instr.val = instr_fetch(&s->snpc, 4);
int idx = table_main(s);
return idx;
}

instr_fetch 则从内存中读取指令编码,table_main 通过树状结构层层调用,对指令进行编码匹配和操作数解析,并最终返回执行辅助函数的下标:

static inline int table_main(Decode *s) {
do {
uint32_t key, mask, shift;
pattern_decode("??????? ????? ????? ??? ????? 00000 11",
(sizeof("??????? ????? ????? ??? ????? 00000 11") - 1), &key,
&mask, &shift);
if (((get_instr(s) >> shift) & mask) == key) {
{
decode_I(s, 0);
return table_load(s);
};
}
} while (0);
...
}

macro yyds

程序如何运行

我们来看 main 函数:

int main() {
ioe_init();
video_init();
panic_on(!io_read(AM_TIMER_CONFIG).present, "requires timer");
panic_on(!io_read(AM_INPUT_CONFIG).present, "requires keyboard");
printf("Type 'ESC' to exit\n");
int current = 0, rendered = 0;
while (1) {
int frames = io_read(AM_TIMER_UPTIME).us / (1000000 / FPS);
for (; current < frames; current++) {
game_logic_update(current);
}
while (1) {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
if (ev.keycode == AM_KEY_NONE) break;
if (ev.keydown && ev.keycode == AM_KEY_ESCAPE) halt(0);
if (ev.keydown && lut[ev.keycode]) {
check_hit(lut[ev.keycode]);
}
};
if (current > rendered) {
render();
rendered = current;
}
}
}

1、ioe_init 位于 abstract-machine/am/src/platform/nemu/ioe/ioe.c

bool ioe_init() {
for (int i = 0; i < LENGTH(lut); i++)
if (!lut[i]) lut[i] = fail;
__am_gpu_init();
__am_timer_init();
__am_audio_init();
return true;
}

注册回调函数,并对一些设备进行初始化,实际上对于 nemu-riscv32 而言,这三个函数都是空的。

回顾回调函数是如何被调用的:

io_read -> ioe_read -> 回调函数

2、video_init,对打字小游戏的图形界面进行初始化:

#define CHAR_W 8
#define CHAR_H 16
void video_init() {
screen_w = io_read(AM_GPU_CONFIG).width;
screen_h = io_read(AM_GPU_CONFIG).height;
extern char font[];
for (int i = 0; i < CHAR_W * CHAR_H; i++)
blank[i] = COL_PURPLE;
for (int x = 0; x < screen_w; x += CHAR_W)
for (int y = 0; y < screen_h; y += CHAR_H) {
io_write(AM_GPU_FBDRAW, x, y, blank, min(CHAR_W, screen_w - x), min(CHAR_H, screen_h - y), false);
}
for (int ch = 0; ch < 26; ch++) {
char *c = &font[CHAR_H * ch];
for (int i = 0, y = 0; y < CHAR_H; y++)
for (int x = 0; x < CHAR_W; x++, i++) {
int t = (c[y] >> (CHAR_W - x - 1)) & 1;
texture[WHITE][ch][i] = t ? COL_WHITE : COL_PURPLE;
texture[GREEN][ch][i] = t ? COL_GREEN : COL_PURPLE;
texture[RED ][ch][i] = t ? COL_RED : COL_PURPLE;
}
}
}

首先读取屏幕的长和宽,然后以一个 blank 为单位绘制整个屏幕。接着在 texture 中存储 26 个大写字母的三种可能的颜色像素:

font 为一个大小为 16×26 的 char 数组

一些定义如下:

uint32_t texture[3][26][CHAR_W * CHAR_H], blank[CHAR_W * CHAR_H];

3、核心是一个 while 循环:

首先读取当前时间对应的帧,如 1 second 对应了 30 frames,然后对每一帧进行 game_logic_update

#define FPS 30
#define CPS 5
void game_logic_update(int frame) {
if (frame % (FPS / CPS) == 0) new_char();
for (int i = 0; i < LENGTH(chars); i++) {
struct character *c = &chars[i];
if (c->ch) {
if (c->t > 0) {
if (--c->t == 0) {
c->ch = '\0';
}
} else {
c->y += c->v;
if (c->y < 0) {
c->ch = '\0';
}
if (c->y + CHAR_H >= screen_h) {
miss++;
c->v = 0;
c->y = screen_h - CHAR_H;
c->t = FPS;
}
}
}
}
}

若当前帧为 6 的倍数(1 秒生成 CPS 个字母),则生成一个新的字母:

#define NCHAR 128
struct character {
char ch;
int x, y, v, t;
} chars[NCHAR];
void new_char() {
for (int i = 0; i < LENGTH(chars); i++) {
struct character *c = &chars[i];
if (!c->ch) {
c->ch = 'A' + randint(0, 25);
c->x = randint(0, screen_w - CHAR_W);
c->y = 0;
c->v = (screen_h - CHAR_H + 1) / randint(FPS * 3 / 2, FPS * 2);
c->t = 0;
return;
}
}
}

对于 character 结构体,ch 代表字母,x 和 y 代表坐标,初始在屏幕最上面,v 是速度,t 为miss 后在屏幕上停留的时间,初始为 0(代表还没有 miss)。

NCHAR 代表同屏最多容纳 128 个字母。

回到 game_logic_update,对这 128 个字母进行逻辑判断,主要是根据 t 来更改 y 使其下落或更改 ch 令其消失。

game_logic_update 之后还有一个循环:

while (1) {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
if (ev.keycode == AM_KEY_NONE) break;
if (ev.keydown && ev.keycode == AM_KEY_ESCAPE) halt(0);
if (ev.keydown && lut[ev.keycode]) {
check_hit(lut[ev.keycode]);
}
};
if (current > rendered) {
render();
rendered = current;
}

在极短的时间内读取键盘输入(大多数情况应该都是未读取到输入),若键入 ESC,则 halt(0):

void halt(int code) {
nemu_trap(code);
// should not reach here
while (1);
}

否则根据一张索引表 lut 来判断是否 hit:

void check_hit(char ch) {
int m = -1;
for (int i = 0; i < LENGTH(chars); i++) {
struct character *c = &chars[i];
if (ch == c->ch && c->v > 0 && (m < 0 || c->y > chars[m].y)) {
m = i;
}
}
if (m == -1) {
wrong++;
} else {
hit++;
chars[m].v = -(screen_h - CHAR_H + 1) / (FPS);
}
}

与屏幕上现有的字符一一比对,若击中则字符以固定的速度反向

最后的 render 重新绘制屏幕:

void render() {
static int x[NCHAR], y[NCHAR], n = 0;
for (int i = 0; i < n; i++) {
io_write(AM_GPU_FBDRAW, x[i], y[i], blank, CHAR_W, CHAR_H, false);
}
n = 0;
for (int i = 0; i < LENGTH(chars); i++) {
struct character *c = &chars[i];
if (c->ch) {
x[n] = c->x; y[n] = c->y; n++;
int col = (c->v > 0) ? WHITE : (c->v < 0 ? GREEN : RED);
io_write(AM_GPU_FBDRAW, c->x, c->y, texture[col][c->ch - 'A'], CHAR_W, CHAR_H, false);
}
}
io_write(AM_GPU_FBDRAW, 0, 0, NULL, 0, 0, true);
for (int i = 0; i < 40; i++) putch('\b');
printf("Hit: %d; Miss: %d; Wrong: %d", hit, miss, wrong);
}

首先绘制背景,然后绘制字符。

对于在终端的输出,采用退格的方式使其表现为在一行更新结果。要注意此处 printf 的底层还是 putch 哦。

如果在内层 NEMU(即编译到 AM 的 NEMU)上运行打字游戏:

Terminal window
make ARCH=riscv32-nemu mainargs=/home/vgalaxy/ics2021/am-kernels/kernels/typing-game/build/typing-game-riscv32-nemu.bin

此时的打字游戏又是经历了怎么样的过程才能读取按键 / 刷新屏幕的呢?

观察得,无屏幕输出,有终端输出,有键盘输入,时钟正常。

我们来分析一些设备的访问情况:

  • 串口:两次回调

putch (inner) -> outb -> serial_io_handler -> serial_putc -> putch (outer) -> outb -> serial_io_handler -> serial_putc -> putc

注意区分 CONFIG_TARGET_AM 和 native AM。

  • 时钟:

inner: rtc_io_handler -> get_time -> get_time_internal -> io_read

outer: rtc_io_handler -> get_time -> get_time_internal -> library functions

inner 和 outer 之间通过抽象寄存器进行通信。

  • 键盘:

类似时钟,通过抽象寄存器进行通信。inner 仅处理 dequeue(因为回调函数调用的是 key_dequeue),outer 会处理键盘事件队列:

#else // !CONFIG_TARGET_AM
#define _KEY_NONE 0
static uint32_t key_dequeue() {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
uint32_t am_scancode = ev.keycode | (ev.keydown ? KEYDOWN_MASK : 0);
return am_scancode;
}
#endif

总结一下设备目前的实现情况:

外层 NEMU内层 NEMU
串口
时钟
键盘
VGA
声卡

编译与链接 ①

nemu/src/engine/interpreter/rtl-basic.h 中,你会看到由 static inline 开头定义的各种 RTL 指令函数。选择其中一个函数,分别尝试去掉 static,去掉 inline 或去掉两者,然后重新进行编译,你可能会看到发生错误。请分别解释为什么这些错误会发生 / 不发生?你有办法证明你的想法吗?

首先是头文件包含关系:

rtl-basic.h -> nemu/include/rtl/rtl.h -> nemu/src/isa/riscv32/include/isa-all-instr.h -> nemu/src/isa/riscv32/instr/decode.c
rtl-basic.h -> nemu/include/rtl/rtl.h -> nemu/src/engine/interpreter/hostcall.c

defined but not used,这是编译选项所致。

没有错误。我们来看一个例子:

a.h
#ifndef __A_H__
#define __A_H__
#include <stdio.h>
inline void f() {
printf("f()\n");
}
// void f();
#endif
// a.c
#include "a.h"
extern inline void f();
void g() {
f();
printf("g()\n");
}
// b.c
#include "a.h"
void h() {
f();
printf("h()\n");
}
// main.c
#include "a.h"
void g();
void h();
int main() {
f();
g();
h();
printf("main()\n");
return 0;
}
// Makefile
.PHONY: run clean
run: a.c b.c main.c
gcc -Werror -Wall a.c b.c main.c
./a.out
clean:
rm -f *.out

注意 a.c 中的 extern inline void f();,所有包含头文件 a.h 的源文件中需要有且仅有一个这样的声明。

若没有 extern inline void f();,会报错 undefined reference to f

若多于一个 extern inline void f();,会报错 multiple definition of f

若没有 extern inline void f();,并在 a.h 中加上声明 void f();,会报错 multiple definition of f

参考 https://stackoverflow.com/questions/6312597/is-inline-without-static-or-extern-ever-useful-in-c99

注意到 NEMU 中的编译选项中有 -O2,我们开启 -O2 后,没有 extern inline void f(); 也可以运行了!

换言之,只要编译器成功内联优化,就没啥问题了。

/usr/bin/ld: /home/vgalaxy/ics2021/nemu/build/obj-riscv32-nemu-interpreter/src/engine/interpreter/hostcall.o: in function `rtl_add':
/home/vgalaxy/ics2021/nemu/src/engine/interpreter/rtl-basic.h:25: multiple definition of `rtl_add'; /home/vgalaxy/ics2021/nemu/build/obj-riscv32-nemu-interpreter/src/cpu/cpu-exec.o:/home/vgalaxy/ics2021/nemu/src/engine/interpreter/rtl-basic.h:25: first defined here

重定义。

编译与链接 ②

  1. nemu/include/common.h 中添加一行 volatile static int dummy;,然后重新编译 NEMU。请问重新编译后的 NEMU 含有多少个 dummy 变量的实体?你是如何得到这个结果的?
  2. 添加上题中的代码后,再在 nemu/include/debug.h 中添加一行 volatile static int dummy;,然后重新编译 NEMU。请问此时的 NEMU 含有多少个 dummy 变量的实体?与上题中 dummy 变量实体数目进行比较,并解释本题的结果。
  3. 修改添加的代码,为两处 dummy 变量进行初始化:volatile static int dummy = 0;,然后重新编译 NEMU。你发现了什么问题?为什么之前没有出现这样的问题?

1、查看 bss 段,未初始化的全局变量:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/nemu$ objdump -D /home/vgalaxy/ics2021/nemu/build/riscv32-nemu-interpreter | less
...
Disassembly of section .bss:
...

数了一下,共有 33 个,应该是所有直接或间接包含 common.h 头文件的数量。

变量名改成 dummy_foo 方便观察

2、还是 33 个……

可以使用 size 命令观察数据段大小:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/nemu$ size /home/vgalaxy/ics2021/nemu/build/riscv32-nemu-interpreter
text data bss dec hex filename
39735 2562 134235304 134277601 800e9e1 /home/vgalaxy/ics2021/nemu/build/riscv32-nemu-interpreter

3、重定义,因为 debug.h 包含了 common.h

+ CC src/nemu-main.c
In file included from /home/vgalaxy/ics2021/nemu/include/common.h:34,
from src/nemu-main.c:1:
/home/vgalaxy/ics2021/nemu/include/debug.h:4:21: error: redefinition of 'dummy_foo'
4 | volatile static int dummy_foo = 0;
| ^~~~~~~~~
In file included from src/nemu-main.c:1:
/home/vgalaxy/ics2021/nemu/include/common.h:4:21: note: previous definition of 'dummy_foo' was here
4 | volatile static int dummy_foo = 0;
| ^~~~~~~~~
make: *** [/home/vgalaxy/ics2021/nemu/scripts/build.mk:34: /home/vgalaxy/ics2021/nemu/build/obj-riscv32-nemu-interpreter/src/nemu-main.o] Error 1

因为初始化的全局变量是强符号,未初始化的全局变量是弱符号:

TODO: 如何解释 Q2

可以参考这篇博客

了解 Makefile

请描述你在 am-kernels/kernels/hello/ 目录下敲入 make ARCH=$ISA-nemu 后,make 程序如何组织 .c 和 .h 文件,最终生成可执行文件 am-kernels/kernels/hello/build/hello-$ISA-nemu.bin

首先来看 am-kernels/kernels/hello/ 目录下的 Makefile:

NAME = hello
SRCS = hello.c
include $(AM_HOME)/Makefile

这里包含的 Makefile 在 abstract-machine 目录下。

执行来观察一下:

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/hello$ make -B ARCH=riscv32-nemu
# Building hello-image [riscv32-nemu]
+ CC hello.c
# Building am-archive [riscv32-nemu]
+ CC src/platform/nemu/trm.c
+ CC src/platform/nemu/ioe/ioe.c
+ CC src/platform/nemu/ioe/timer.c
+ CC src/platform/nemu/ioe/input.c
+ CC src/platform/nemu/ioe/gpu.c
+ CC src/platform/nemu/ioe/audio.c
+ CC src/platform/nemu/ioe/disk.c
+ CC src/platform/nemu/mpe.c
+ AS src/riscv/nemu/start.S
+ CC src/riscv/nemu/cte.c
+ AS src/riscv/nemu/trap.S
+ CC src/riscv/nemu/vme.c
+ AR -> build/am-riscv32-nemu.a
# Building klib-archive [riscv32-nemu]
+ CC src/cpp.c
+ CC src/string.c
+ CC src/stdlib.c
+ CC src/stdio.c
+ CC src/int64.c
+ AR -> build/klib-riscv32-nemu.a
+ LD -> build/hello-riscv32-nemu.elf
# Creating image [riscv32-nemu]
+ OBJCOPY -> build/hello-riscv32-nemu.bin

大概过程如下:

均使用交叉编译工具链 riscv64-linux-gnu-xxx

vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/hello/build$ file hello-riscv32-nemu.bin
hello-riscv32-nemu.bin: data
vgalaxy@vgalaxy-VirtualBox:~/ics2021/am-kernels/kernels/hello/build$ file hello-riscv32-nemu.elf
hello-riscv32-nemu.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, not stripped