
讲义主题你已经知道程序需要什么样的运行时环境现在要把这套环境提供给 NPC。AM 项目已经提供了riscv64-npc/riscv32e-npc的基本框架你只需要补上设备等细节。下面逐个文件分析这个“NPC 运行时环境”是怎么搭起来的。一、NPC 运行时环境由谁提供AM 的 riscv/npc 目录 scripts/platform/npc.mk NPC 仿真器 Vrv32AM 给 NPC 提供TRM启动、putch、halt IOE定时器、输入 CTE异常处理NPC 仿真器提供CPU 物理内存 MMIO 设备串口、定时器二、构建脚本platform/npc.mk文件abstract-machine/scripts/platform/npc.mkAM_SRCS : riscv/npc/start.S \ riscv/npc/trm.c \ riscv/npc/ioe.c \ riscv/npc/timer.c \ riscv/npc/input.c \ riscv/npc/cte.c \ riscv/npc/trap.S \ platform/dummy/vme.c \ platform/dummy/mpe.c LDFLAGS --defsym_pmem_start0x80000000 \ --defsym_entry_offset0x0 LDFLAGS --gc-sections -e _start NPC_EXEC : /home/xuhaitao/ysyx-workbench/npc/obj_dir/Vrv32 run: insert-arg $(NPC_EXEC) -b $(IMAGE).bin分析AM_SRCS说明 NPC 的 AM 运行环境由这些文件组成start.S 启动 trm.c TRM ioe.c IOE timer.c 定时器 input.c 输入 cte.c CTE trap.S 异常汇编--defsym_pmem_start0x80000000内存从0x80000000开始和 NEMU 一致。-e _start入口是_start。run: $(NPC_EXEC) -b $(IMAGE).bin最终不是启动 NEMU而是启动npc/obj_dir/Vrv32三、启动start.S文件abstract-machine/am/src/riscv/npc/start.S.section entry, ax .globl _start _start: mv s0, zero la sp, _stack_pointer call _trm_init分析和 NEMU 完全一样_start - 设置栈指针 - call _trm_initNPC 复位后从0x80000000开始执行_start。四、TRMtrm.c文件abstract-machine/am/src/riscv/npc/trm.cextern char _pmem_start; #define PMEM_SIZE (128 * 1024 * 1024) #define PMEM_END ((uintptr_t)_pmem_start PMEM_SIZE) Area heap RANGE(_heap_start, PMEM_END); static const char mainargs[MAINARGS_MAX_LEN] TOSTRING(MAINARGS_PLACEHOLDER); #define SERIAL_DATA_PORT 0xa00003f8 void putch(char ch) { outb(SERIAL_DATA_PORT, ch); } void halt(int code) { asm volatile ( mv a0, %0\n ebreak\n : : r(code) : a0 ); __builtin_unreachable(); } void _trm_init() { int ret main(mainargs); halt(ret); }分析1. 堆范围Area heap RANGE(_heap_start, PMEM_END);NPC 内存0x80000000 ~ 0x88000000共 128MB。2. 串口输出#define SERIAL_DATA_PORT 0xa00003f8 void putch(char ch) { outb(SERIAL_DATA_PORT, ch); }这和 NEMU 使用相同的地址0xa00003f8所以NPC 的串口 MMIO 地址和 NEMU 一致 上层程序不用改3. 停机asm volatile (mv a0, %0; ebreak)把退出码放到a0然后ebreak。NPC 仿真器捕获ebreak读取a0判断是否正常退出。五、IOEioe.c文件abstract-machine/am/src/riscv/npc/ioe.cstatic 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_UART_CONFIG] __am_uart_config, }; bool ioe_init() { for (int i 0; i LENGTH(lut); i) if (!lut[i]) lut[i] fail; __am_timer_init(); return true; }分析NPC 目前只支持定时器 键盘 UART没有GPU 音频 磁盘 网络所以NPC 阶段只跑字符程序不跑图形/音频六、定时器timer.c文件abstract-machine/am/src/riscv/npc/timer.c#define RTC_UPTIME_LO 0xa0000048 #define RTC_UPTIME_HI 0xa000004c #define RTC_YEAR 0xa0000100 #define RTC_MONTH 0xa0000104 #define RTC_DAY 0xa0000108 #define RTC_HOUR 0xa000010c #define RTC_MINUTE 0xa0000110 #define RTC_SECOND 0xa0000114 void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) { uint32_t hi1, lo, hi2; do { hi1 inl(RTC_UPTIME_HI); lo inl(RTC_UPTIME_LO); hi2 inl(RTC_UPTIME_HI); } while (hi1 ! hi2); uptime-us ((uint64_t)hi1 32) | lo; } void __am_timer_rtc(AM_TIMER_RTC_T *rtc) { rtc-year inl(RTC_YEAR); rtc-month inl(RTC_MONTH); rtc-day inl(RTC_DAY); rtc-hour inl(RTC_HOUR); rtc-minute inl(RTC_MINUTE); rtc-second inl(RTC_SECOND); }分析1. uptime 地址0xa0000048 低 32 位 0xa000004c 高 32 位和 NEMU 一致。2. 为什么读三次do { hi1 读高32位 lo 读低32位 hi2 读高32位 } while (hi1 ! hi2);这是为了处理“进位”如果读高32位后低32位刚好进位 那 hi1 和 hi2 会不同 重新读一遍3. RTC 日历NPC 的 RTC 地址0xa0000100 年 0xa0000104 月 0xa0000108 日 0xa000010c 时 0xa0000110 分 0xa0000114 秒这比 NEMU 更完整。七、键盘input.c文件abstract-machine/am/src/riscv/npc/input.cvoid __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) { kbd-keydown 0; kbd-keycode AM_KEY_NONE; }分析NPC 的键盘当前是空实现永远返回无按键因为NPC 当前没有实现键盘设备八、CTEcte.c文件abstract-machine/am/src/riscv/npc/cte.cContext* __am_irq_handle(Context *c) { if (user_handler) { Event ev {0}; switch (c-mcause) { case 11: ev.event EVENT_YIELD; c-mepc 4; break; default: ev.event EVENT_ERROR; break; } c user_handler(ev, c); assert(c ! NULL); } return c; } bool cte_init(Context*(*handler)(Event, Context*)) { asm volatile(csrw mtvec, %0 : : r(__am_asm_trap)); user_handler handler; return true; } Context *kcontext(Area kstack, void (*entry)(void *), void *arg) { Context *ctx (Context *)((uintptr_t)kstack.end - sizeof(Context)); memset(ctx, 0, sizeof(Context)); ctx-mepc (uintptr_t)entry; ctx-gpr[10] (uintptr_t)arg; ctx-gpr[2] (uintptr_t)kstack.end; ctx-mstatus 0x00001880; return ctx; }分析1. 只处理ecallcase 11: ev.event EVENT_YIELD; c-mepc 4;NPC 当前只处理ecall把它当成yield。2. 没有处理定时器中断NPC 的 CTE 还没有处理0x80000007 定时器中断3.kcontext创建上下文ctx-mepc entry; ctx-gpr[10] arg; ctx-gpr[2] kstack.end; ctx-mstatus 0x00001880;和 NEMU 版本相比多了ctx-gpr[2] (uintptr_t)kstack.end;设置了栈指针sp。九、异常汇编trap.S文件abstract-machine/am/src/riscv/npc/trap.S和 NEMU 的trap.S基本一样但多了li a0, (1 17) or t1, t1, a0 csrw mstatus, t1分析mstatus.MPRV 位设置MPRV让 load/store 使用mstatus里的特权模式进行地址翻译。这是为了和 Difftest 对齐。十、NPC 仿真器侧怎么配合文件npc/obj_dir/Vrv32run目标最终执行/home/xuhaitao/ysyx-workbench/npc/obj_dir/Vrv32 -b image.binNPC 仿真器会1. 读取 image.bin 2. 加载到 0x80000000 3. 复位 CPU 4. 从 0x80000000 执行 _start 5. 遇到 ebreak 停止内存访问由 DPI-C 调用pmem_read pmem_write设备地址0xa00003f8 串口 0xa0000048 定时器由 NPC 仿真侧paddr.cpp处理。十一、NPC 和 NEMU 运行时环境对比项目NEMUNPC入口_start_start内存基址0x800000000x80000000串口地址0xa00003f80xa00003f8uptime 地址0xa00000480xa0000048RTC 日历未实现已实现键盘支持空实现GPU支持未支持CTE支持 yield只支持 ecall十二、总结NPC 的运行时环境 AM riscv/npc platform/npc.mk Vrv32 仿真器它已经提供了✅ 启动 _start ✅ putch 串口 ✅ halt ebreak ✅ 定时器 uptime / RTC ✅ 异常入口 cte_init ✅ 上下文 kcontext还缺❌ 键盘 ❌ GPU ❌ 定时器中断所以 NPC 当前能跑hello cpu-tests klib-tests但还不能跑图形和交互程序。