资讯动态

【一生一芯 / PA】从 hello 到串口:NEMU 的输入输出(IOE)是怎么跑通的

发布时间:2026/10/3 7:22:39 来源:尧图企业网站定制
环境riscv32 NEMU AM代码位置~/ysyx-workbench/nemu和~/ysyx-workbench/abstract-machine一、为什么 CPU 能“打印”到屏幕我们先想一个问题cpu-tests 只能默默计算为什么 hello 能把字符串显示到终端答案CPU 本身不会“打印”它只会读写内存地址。所谓打印本质是CPU 向一个特殊地址写一个字节 ↓ NEMU 发现这个地址不是普通内存而是 MMIO 设备地址 ↓ NEMU 调用串口设备回调 ↓ 把字符输出到宿主机终端这就是本文要讲的核心MMIOMemory-Mapped I/O。二、NEMU 的物理地址布局在你的 AM 代码里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 SERIAL_PORT (DEVICE_BASE 0x00003f8) #define KBD_ADDR (DEVICE_BASE 0x0000060) #define RTC_ADDR (DEVICE_BASE 0x0000048) #define VGACTL_ADDR (DEVICE_BASE 0x0000100) #define AUDIO_ADDR (DEVICE_BASE 0x0000200) #define DISK_ADDR (DEVICE_BASE 0x0000300) #define FB_ADDR (MMIO_BASE 0x1000000) #define AUDIO_SBUF_ADDR (MMIO_BASE 0x1200000)重点看#define SERIAL_PORT (DEVICE_BASE 0x00003f8)也就是0xa0000000 0x00003f8 0xa00003f8这个地址就是NEMU 模拟的串口数据寄存器。三、AM 侧putch()只做了一次outb文件abstract-machine/am/src/platform/nemu/trm.c你的代码void putch(char ch) { outb(SERIAL_PORT, ch); } void halt(int code) { nemu_trap(code); while (1); } void _trm_init() { int ret main(mainargs); halt(ret); }关键只有一句outb(SERIAL_PORT, ch);outb会展开成一条向指定地址写 1 字节的指令。对于 RISC-V 来说最终就是向0xa00003f8写入字符ch。四、NEMU 侧串口设备是怎么接到 MMIO 上的1. 串口设备实现文件nemu/src/device/serial.c核心代码static uint8_t *serial_base NULL; 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) { 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 }你的当前配置走的是 MMIOadd_mmio_map(serial, CONFIG_SERIAL_MMIO, serial_base, 8, serial_io_handler);其中CONFIG_SERIAL_MMIO 0xa00003f8所以写入地址 0xa00003f8 ↓ serial_io_handler 被调用 ↓ serial_putc(serial_base[0]) ↓ putc(ch, stderr) ↓ 终端显示字符2. MMIO 映射注册文件nemu/src/device/io/mmio.c关键函数void add_mmio_map(const char *name, paddr_t addr, void *space, uint32_t len, io_callback_t callback) { assert(nr_map NR_MAP); paddr_t left addr, right addr len - 1; if (in_pmem(left) || in_pmem(right)) { report_mmio_overlap(name, left, right, pmem, PMEM_LEFT, PMEM_RIGHT); } for (int i 0; i nr_map; i) { if (left maps[i].high right maps[i].low) { report_mmio_overlap(name, left, right, maps[i].name, maps[i].low, maps[i].high); } } maps[nr_map] (IOMap) { .name name, .low addr, .high addr len - 1, .space space, .callback callback }; Log(Add mmio map %s at [ FMT_PADDR , FMT_PADDR ], maps[nr_map].name, maps[nr_map].low, maps[nr_map].high); nr_map; }它做的事检查地址是否落在普通物理内存里检查是否和其他设备地址重叠把地址区间、回调函数记录下来。你启动 NEMU 时会看到类似Add mmio map serial at [0xa00003f8, 0xa00003ff]3. MMIO 读写分派文件nemu/src/device/io/mmio.cword_t mmio_read(paddr_t addr, int len) { return map_read(addr, len, fetch_mmio_map(addr)); } void mmio_write(paddr_t addr, int len, word_t data) { map_write(addr, len, data, fetch_mmio_map(addr)); }当 CPU 读写某个地址时NEMU 会判断普通内存 - 直接读写 pmem MMIO 地址 - 调用对应设备的 callback4. 设备回调的真正执行文件nemu/src/device/io/map.cword_t map_read(paddr_t addr, int len, IOMap *map) { check_bound(map, addr); paddr_t offset addr - map-low; invoke_callback(map-callback, offset, len, false); word_t ret host_read(map-space offset, len); return ret; } void map_write(paddr_t addr, int len, word_t data, IOMap *map) { check_bound(map, addr); paddr_t offset addr - map-low; host_write(map-space offset, len, data); invoke_callback(map-callback, offset, len, true); }串口写入时map_write - host_write 先把数据写进 serial_base - invoke_callback - serial_io_handler - serial_putc五、完整的“打印一个字符”调用链假设hello.c执行printf(Hello, AbstractMachine!\n);完整链路是1. AM 程序 hello.c printf(...) ↓ 2. klib 的 printf 最终调用 putch(ch) ↓ 3. AM TRM 的 putch() outb(SERIAL_PORT, ch) ↓ 4. RISC-V 生成一条写内存指令 向 0xa00003f8 写 ch ↓ 5. NEMU paddr_write() 发现 0xa00003f8 是 MMIO ↓ 6. mmio_write() 找到 serial 设备 ↓ 7. serial_io_handler() serial_putc(ch) ↓ 8. putc(ch, stderr) 输出到宿主机终端六、不止串口AM 的 IOE 抽象文件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, };上层程序只需要ioe_read(reg, buf); ioe_write(reg, buf);就能访问任意设备。七、具体设备举例1. 定时器文件abstract-machine/am/src/platform/nemu/ioe/timer.cvoid __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) { uptime-us inl(RTC_ADDR 4); uptime-us 32; uptime-us inl(RTC_ADDR); }它通过读RTC_ADDR 0xa0000048来读取模拟器运行时间。2. 键盘文件abstract-machine/am/src/platform/nemu/ioe/input.cvoid __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; }它通过读KBD_ADDR 0xa0000060来读取按键状态。3. 显卡文件abstract-machine/am/src/platform/nemu/ioe/gpu.cvoid __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) { uint32_t *pixels ctl-pixels; uint32_t *fb (uint32_t *)(uintptr_t)FB_ADDR; for (int i 0; i h; i) { uint32_t *dst fb (y i) * screen_w x; uint32_t *src pixels i * w; for (int j 0; j w; j) { dst[j] src[j]; } } if (ctl-sync) { outl(SYNC_ADDR, 1); } }帧缓冲基地址是FB_ADDR 0xa1000000写这个地址就是直接写“屏幕显存”。八、动手实验观察串口输出1. 编译并运行 helloexport NEMU_HOME~/ysyx-workbench/nemu export AM_HOME~/ysyx-workbench/abstract-machine cd ~/ysyx-workbench/am-kernels/kernels/hello make ARCHriscv32-nemu clean make ARCHriscv32-nemu run你会看到Hello, AbstractMachine! mainargs . HIT GOOD TRAP at pc 0x800000b0这就是串口 MMIO 在工作。2. 观察 NEMU 注册的设备地址启动时会打印Add mmio map serial at [0xa00003f8, 0xa00003ff] Add mmio map rtc at [0xa0000048, 0xa000004f] Add mmio map vgactl at [0xa0000100, 0xa0000107] Add mmio map vmem at [0xa1000000, 0xa10752ff] Add mmio map keyboard at [0xa0000060, 0xa0000063] Add mmio map audio at [0xa0000200, 0xa0000217]这说明 NEMU 已经把各种设备挂到了 MMIO 地址上。九、串口地址为什么是0xa00003f8NEMU 配置里nemu/include/generated/autoconf.h有#define CONFIG_SERIAL_MMIO 0xa00003f8来源MMIO_BASE 0xa0000000 串口偏移 0x3f8所以0xa0000000 0x3f8 0xa00003f8而0x3f8正是传统 PC 上 COM1 串口寄存器的地址。NEMU 借用了这个经典设计。十、总结这篇文章讲清楚了一条线printf - putch - outb(SERIAL_PORT, ch) - CPU 写 0xa00003f8 - NEMU 识别 MMIO - serial_io_handler - putc(ch, stderr) - 屏幕显示核心结论CPU 不做“输入输出”。 CPU 只读写地址。 设备通过 MMIO 把自己伪装成一段地址。 谁写这段地址谁就在操作设备。这就是 NEMU / AM 中输入输出的本质。需要的话下一篇我还可以继续写AM 的键盘输入 AM 的 RTC 定时器 AM 的 VGA 绘图 NEMU 的 device_update 和 SDL 事件16:14不够仔细完整16:16Worked for 47秒【一生一芯 / PA】输入输出一个字符从 AM 到 NEMU 再到屏幕的完整旅程超详细版代码环境NEMU~/ysyx-workbench/nemuAM~/ysyx-workbench/abstract-machine内核~/ysyx-workbench/am-kernelsISAriscv320. 本文要解决的问题我们已经能跑cpu-tests但那些程序只是“默默计算”。hello不一样它能把字符串显示出来。本文要回答一个非常本质的问题CPU 明明只会执行指令、读写内存 它到底是怎么把字符“打印”到屏幕上的答案就是MMIOMemory-Mapped I/O内存映射输入输出一、程序 计算 输入输出一个完整的程序通常包含两部分1. 计算做算术、访问内存、控制流程 2. 输入输出和外界交互CPU 本身并没有“屏幕”“键盘”“串口”这些外设。CPU 能做的只有读取某个地址 写入某个地址于是计算机设计者约定把一部分地址不再当作普通内存 而是把这些地址“映射”到外部设备这种技术就是 MMIO。二、NEMU 的地址空间内存和设备怎么分区NEMU 的物理地址空间可以简单分成两类地址范围含义0x80000000 ~ 0x87ffffff普通物理内存pmem0xa0000000 开始的一段地址MMIO 设备地址在你的 AM 代码中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 SERIAL_PORT (DEVICE_BASE 0x00003f8) #define KBD_ADDR (DEVICE_BASE 0x0000060) #define RTC_ADDR (DEVICE_BASE 0x0000048) #define VGACTL_ADDR (DEVICE_BASE 0x0000100) #define AUDIO_ADDR (DEVICE_BASE 0x0000200) #define DISK_ADDR (DEVICE_BASE 0x0000300) #define FB_ADDR (MMIO_BASE 0x1000000) #define AUDIO_SBUF_ADDR (MMIO_BASE 0x1200000)所以我们得到设备地址表设备地址串口 serial0xa00003f8键盘 keyboard0xa0000060RTC 定时器0xa0000048VGA 控制寄存器0xa0000100音频 audio0xa0000200磁盘 disk0xa0000300帧缓冲 FB0xa1000000其中串口SERIAL_PORT DEVICE_BASE 0x3f8 0xa0000000 0x3f8 0xa00003f8NEMU 配置里也有nemu/include/generated/autoconf.h#define CONFIG_SERIAL_MMIO 0xa00003f8三、AM 怎么读写这些地址1. RISC-V 下的inb/outb文件abstract-machine/am/src/riscv/riscv.hstatic 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; }注意outb 不是一个“系统调用” 它只是一次普通的内存写入 只是这个地址恰好是 MMIO 设备地址四、AM TRMputch()是 I/O 的最底层文件abstract-machine/am/src/platform/nemu/trm.cvoid putch(char ch) { outb(SERIAL_PORT, ch); } void halt(int code) { nemu_trap(code); while (1); } void _trm_init() { int ret main(mainargs); halt(ret); }整个 AM 的“输出一个字符”动作就是outb(SERIAL_PORT, ch);等价于向 0xa00003f8 写入一个字节 ch五、hello程序如何输出字符串文件am-kernels/kernels/hello/hello.c#include am.h #include klib-macros.h int main(const char *args) { const char *fmt Hello, AbstractMachine!\n mainargs %.\n; for (const char *p fmt; *p; p) { (*p %) ? putstr(args) : putch(*p); } return 0; }它逐字符调用putch(*p)而putch最终就是outb(SERIAL_PORT, ch)补充putstr宏文件abstract-machine/klib/include/klib-macros.h#define putstr(s) \ ({ for (const char *p s; *p; p) putch(*p); })所以putstr(args)也会逐字符调用putch(*p)六、printf也是一样的最终落到putch如果你的程序调用printf(hello);那么 klib 的printf实现如下文件abstract-machine/klib/src/stdio.cint printf(const char *fmt, ...) { char buf[MAX_STRING_LEN]; va_list args; va_start(args, fmt); int len process_format_string(buf, fmt, args); for (int i 0; i len; i) { putch(buf[i]); } return len; }关键在这里for (int i 0; i len; i) { putch(buf[i]); }也就是说printf - 格式化字符串 - 逐字符 putch - outb(SERIAL_PORT, ch)所以printf的高层抽象最后也是落到 TRM 的putch。七、NEMU 内存读写入口如何分流内存和 MMIOAM 写0xa00003f8后RISC-V CPU 执行一条访存指令。NEMU 解释器最终会调用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); }); 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); out_of_bound(addr); }关键判断in_pmem(addr)如果地址是0x80000000 ~ 0x87ffffff - 普通内存 其他地址 - 可能交给 MMIO于是写 0xa00003f8 ↓ 不是 pmem ↓ 调用 mmio_write(0xa00003f8, 1, ch)八、NEMU 的 MMIO 管理1. IOMap设备映射表NEMU 用IOMap描述一个设备nemu/include/device/map.h核心字段typedef struct { const char *name; paddr_t low; paddr_t high; void *space; io_callback_t callback; } IOMap;含义字段含义name设备名字例如seriallowMMIO 起始地址highMMIO 结束地址space设备自己的存储空间callback读写时被调用的函数2. 注册设备add_mmio_map()文件nemu/src/device/io/mmio.cvoid add_mmio_map(const char *name, paddr_t addr, void *space, uint32_t len, io_callback_t callback) { assert(nr_map NR_MAP); paddr_t left addr, right addr len - 1; if (in_pmem(left) || in_pmem(right)) { report_mmio_overlap(name, left, right, pmem, PMEM_LEFT, PMEM_RIGHT); } for (int i 0; i nr_map; i) { if (left maps[i].high right maps[i].low) { report_mmio_overlap(name, left, right, maps[i].name, maps[i].low, maps[i].high); } } maps[nr_map] (IOMap) { .name name, .low addr, .high addr len - 1, .space space, .callback callback }; Log(Add mmio map %s at [ FMT_PADDR , FMT_PADDR ], maps[nr_map].name, maps[nr_map].low, maps[nr_map].high); nr_map; }它做了三件事禁止设备地址和物理内存重叠禁止设备地址和设备地址重叠把设备加入全局maps表。3. 找设备fetch_mmio_map()static IOMap* fetch_mmio_map(paddr_t addr) { int mapid find_mapid_by_addr(maps, nr_map, addr); return (mapid -1 ? NULL : maps[mapid]); }当 CPU 访问0xa00003f8时NEMU 在表里找到serial。4. 真正执行读写map_read/map_write文件nemu/src/device/io/map.cword_t map_read(paddr_t addr, int len, IOMap *map) { check_bound(map, addr); paddr_t offset addr - map-low; invoke_callback(map-callback, offset, len, false); word_t ret host_read(map-space offset, len); return ret; } void map_write(paddr_t addr, int len, word_t data, IOMap *map) { check_bound(map, addr); paddr_t offset addr - map-low; host_write(map-space offset, len, data); invoke_callback(map-callback, offset, len, true); }串口写一个字节的流程map_write - host_write把字符写到设备的 space - invoke_callback调用 serial_io_handler九、串口设备详细实现文件nemu/src/device/serial.c完整核心代码#define CH_OFFSET 0 static uint8_t *serial_base NULL; 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) { 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 }逐行解释serial_base new_space(8);给串口分配 8 字节的模拟寄存器空间。add_mmio_map(serial, CONFIG_SERIAL_MMIO, serial_base, 8, serial_io_handler);把地址 0xa00003f8 开始长度 8 字节注册成串口设备。当写0xa00003f8时serial_io_handler(offset 0, len 1, is_write true)然后serial_putc(serial_base[0])在 NEMU 模式下putc(ch, stderr)最终把字符写到宿主机终端。十、设备初始化顺序文件nemu/src/device/device.cvoid init_device() { IFDEF(CONFIG_TARGET_AM, ioe_init()); init_map(); IFDEF(CONFIG_HAS_SERIAL, init_serial()); IFDEF(CONFIG_HAS_TIMER, init_timer()); IFDEF(CONFIG_HAS_VGA, init_vga()); IFDEF(CONFIG_HAS_KEYBOARD, init_i8042()); IFDEF(CONFIG_HAS_AUDIO, init_audio()); IFDEF(CONFIG_HAS_DISK, init_disk()); IFDEF(CONFIG_HAS_SDCARD, init_sdcard()); IFNDEF(CONFIG_TARGET_AM, init_alarm()); }顺序1. ioe_init AM 设备层初始化 2. init_map MMIO 映射表初始化 3. serial 4. timer 5. vga 6. keyboard 7. audio 8. disk 9. sdcard所以启动时你会看到Add mmio map serial at [0xa00003f8, 0xa00003ff] Add mmio map rtc at [0xa0000048, 0xa000004f] Add mmio map vgactl at [0xa0000100, 0xa0000107] Add mmio map vmem at [0xa1000000, 0xa10752ff] Add mmio map keyboard at [0xa0000060, 0xa0000063] Add mmio map audio at [0xa0000200, 0xa0000217]十一、其他设备怎么访问 MMIO1. RTC 定时器文件abstract-machine/am/src/platform/nemu/ioe/timer.cvoid __am_timer_init() { 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); }读取RTC_ADDR 0xa0000048 RTC_ADDR 4 0xa000004c得到 64 位微秒时间。2. 键盘文件abstract-machine/am/src/platform/nemu/ioe/input.c#define KEYDOWN_MASK 0x8000 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; }读取KBD_ADDR 0xa0000060得到按键码和按下/抬起状态。3. VGA文件abstract-machine/am/src/platform/nemu/ioe/gpu.cvoid __am_gpu_init() { uint32_t screen_wh inl(VGACTL_ADDR); screen_w screen_wh 16; screen_h screen_wh 0xffff; } void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) { uint32_t *pixels ctl-pixels; uint32_t *fb (uint32_t *)(uintptr_t)FB_ADDR; for (int i 0; i h; i) { uint32_t *dst fb (y i) * screen_w x; uint32_t *src pixels i * w; for (int j 0; j w; j) { dst[j] src[j]; } } if (ctl-sync) { outl(SYNC_ADDR, 1); } }屏幕宽高从VGACTL_ADDR 0xa0000100读取。像素写入FB_ADDR 0xa1000000十二、AM 的 IOE 抽象层AM 不希望上层程序直接写outb(SERIAL_PORT, ch);而是提供统一接口ioe_read(reg, buf); ioe_write(reg, buf);文件abstract-machine/am/src/platform/nemu/ioe/ioe.ctypedef 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, }; 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; } void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); } void ioe_write(int reg, void *buf) { ((handler_t)lut[reg])(buf); }十三、AM 的设备寄存器定义文件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( 1, UART_CONFIG, RD, bool present); AM_DEVREG( 2, UART_TX, WR, char data); AM_DEVREG( 3, UART_RX, RD, char data); AM_DEVREG( 4, TIMER_CONFIG, RD, bool present, has_rtc); AM_DEVREG( 5, TIMER_RTC, RD, int year, month, day, hour, minute, second); AM_DEVREG( 6, TIMER_UPTIME, RD, uint64_t us); AM_DEVREG( 7, INPUT_CONFIG, RD, bool present); AM_DEVREG( 8, INPUT_KEYBRD, RD, bool keydown; int keycode); 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); ...上层程序使用io_read/io_write的宏io_read(AM_TIMER_UPTIME) io_write(AM_GPU_FBDRAW, x, y, pixels, w, h, sync)这些宏在abstract-machine/klib/include/klib-macros.h定义#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); })十四、一个字符的完整调用链现在我们完整串一遍AM 程序 printf(Hello) ↓ klib printf 逐字符调用 putch ↓ AM TRM putch outb(SERIAL_PORT, ch) ↓ riscv.h 的 outb *(volatile uint8_t *)0xa00003f8 ch ↓ RISC-V 生成一条存字节指令 sb ch, 0(addr) ↓ NEMU isa_exec_once 执行 sb 指令 ↓ vaddr_write / paddr_write 发现地址不是 pmem ↓ mmio_write(0xa00003f8, 1, ch) ↓ fetch_mmio_map 找到 serial 设备 ↓ map_write 写 serial_base调用 callback ↓ serial_io_handler serial_putc(ch) ↓ putc(ch, stderr) ↓ 字符显示在宿主机终端十五、动手实验实验 1跑 hello看串口输出export NEMU_HOME~/ysyx-workbench/nemu export AM_HOME~/ysyx-workbench/abstract-machine cd ~/ysyx-workbench/am-kernels/kernels/hello make ARCHriscv32-nemu clean make ARCHriscv32-nemu run期望输出Hello, AbstractMachine! mainargs . HIT GOOD TRAP at pc 0x800000b0实验 2观察 NEMU 注册的设备启动时会看到Add mmio map serial at [0xa00003f8, 0xa00003ff] Add mmio map rtc at [0xa0000048, 0xa000004f] Add mmio map vgactl at [0xa0000100, 0xa0000107] Add mmio map vmem at [0xa1000000, 0xa10752ff] Add mmio map keyboard at [0xa0000060, 0xa0000063] Add mmio map audio at [0xa0000200, 0xa0000217]实验 3用 SDB 观察串口写入启动 NEMU 交互模式cd ~/ysyx-workbench/nemu ./build/riscv32-nemu-interpreter \ ~/ysyx-workbench/am-kernels/kernels/hello/build/hello-riscv32-nemu.bin设置监视点观察写到串口的字符w *0xa00003f8 c当0xa00003f8的内容发生变化时程序会停下。十六、为什么串口偏移是0x3f8传统 PC 的 COM1 串口基地址就是0x3f8。NEMU 直接借用了这个经典设计0xa0000000 0x3f8 0xa00003f8所以MMIO_BASE 是 NEMU 设备区的基地址 0x3f8 是串口寄存器偏移十七、总结这篇博客的核心只有一句话CPU 不做输入输出CPU 只读写地址。 设备把自己映射成一段地址。 读写这段地址就是在操作设备。从代码层面看putch - outb - 写 MMIO - NEMU paddr_write - mmio_write - device callback - 宿主机输出掌握这条链路后你就理解了 NEMU 和 AM 中的“输入输出”到底是怎么一回事。16:16

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑