这两天做到PA2.3了,这里的代码涉及到AM和NEMU比较乱,想着写个随笔缕一下思路。
讲义中首先讲到
框架代码为映射定义了一个结构体类型IOMap(在nemu/include/device/map.h中定义), 包括名字, 映射的起始地址和结束地址, 映射的目标空间, 以及一个回调函数. 然后在nemu/src/device/io/map.c实现了映射的管理, 包括I/O空间的分配及其映射, 还有映射的访问接口.
所以我们可以认为每个IO都占用内存空间的一部分,结构体中内容都在注释中注明
typedef struct { const char *name; // we treat ioaddr_t as paddr_t here paddr_t low; //start address paddr_t high; // finish address void *space; // reflect target space io_callback_t callback; // call function } IOMap;
用我自己的话概括讲义(可能理解的不太对),简而言之就是说:
讲义中提到两种映射关系,内存映射和端口映射。
- 内存映射是单独开辟一段内存空间,然后当CPU访问这段物理内存时候,被重定向到需要访问的IO设备,所以访问IO设备本质上就是一次普通的访存。
- 端口映射是采用专门的IO指令对设备进行访问,把设备地址称作端口号。专门的IO指令中会给出端口号,类似于CPU指令中的Rd。
了解了这个对应关系,那么地址映射本质上就是类似与一个数学函数罢了,每个单独的地址对应每个设备地址。拿时钟举例:
- 我们首先要初始化,也就是分配IO空间(需要把时钟的寄存器存到内存中总得开辟空间吧)初始化代码中分配了IO_SPACE_MAX的内存空间,然后将其指针保存在io_space和p_space变量中。所有后续的设备都将存到这个区间内。
void init_map() { io_space = malloc(IO_SPACE_MAX); assert(io_space); p_space = io_space; }
- 分配RTC寄存器空间,别忘记每个设备都需要有个寄存器来存储设备状态,后续有设备指令读取的时候本质上就是读取这个设备寄存器的。
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)); }
这里就根据端口映射还是内存映射进行不同的映射关系的添加了。
先看内存映射的相关代码,代码判断添加映射的地址是否正确,是否超出正常的内存区域,同时是否覆盖了其他的设备地址,最后记录映射关系map[nr_map]。这个map本质上就是一个存储设备映射关系的数组。
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 ++;
端口映射也是同理,这里不再赘述。这里还有一个值得注意的点就是回调函数,add_mmio_map的最后一个参数就是回调函数,其中rtc_io_handler具体代码如下,函数实现了对设备寄存器(这里是时钟寄存器)的存操作,把当前时间存到时钟寄存器中,具体为什么用两个32位而不用一个64位看讲义就清楚啦。
//0号寄存器存储时间低32位,1号寄存器存储高32位时间 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; } }
当后续需要访问接口的时候采用po_read / mmio_read即可访问IO空间,本质上就是一次普通的内存读取。这里的invoke_callback来判断当前访问设备的对应状态数据是否为空,不为空的话就把数据赋值到寄存器中。然后进行一个内存读取就OK了。
static void invoke_callback(io_callback_t c, paddr_t offset, int len, bool is_write) { if (c != NULL) { c(offset, len, is_write); } } 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; } word_t mmio_read(paddr_t addr, int len) { return map_read(addr, len, fetch_mmio_map(addr)); }
到这里NEMU相关代码结束,剩下的就是AM框架的代码结构了。讲义中提示可以看看$ISA.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; }
这里白话一点:in就是读数据 out就是写数据。通过in out来把设备地址的寄存器读写。例如AM需要读取当前时钟,那么我们就需要把之前NEMU实现的时钟寄存器数据读出来,注意这里都跟NEMU一致,采用32位实现
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) { uint32_t low_time = inl(RTC_ADDR); //设备寄存器为64位 将64位功能拆分成两个32位设备寄存器 uint32_t high_time = inl(RTC_ADDR+4); uptime->us = (uint64_t)low_time + (((uint64_t) high_time) << 32); }
这两天又看了下代码,感觉之前写的还是不太清楚。所以想着在补充下。
static uint64_t uptime() { return io_read(AM_TIMER_UPTIME).us; }
可以看到上面代码是函数请求时间,用一次io_read 接口,从而得到时间。但是在AM中,这个接口对应这
__am_timer_uptim
这个函数,这个函数上面已经有提到,就是对内存RTC_ADDR进行读取。而这个内存地址存储的就是当前时间。存时间的操作在NEMU中已经实现了。
所以总流程就是:io_read->ioe_read->__am_time_update->inl(RTC_ADDR)->paddr_read->mmio_read->map_read。接下来的内容都在map_read函数中,详情可以看此函数。map_read时就开始调用回调函数rtc_io_handler了,在回调函数中会获取时间然后存到rtc_port_base中,这个rtc_port_base是可以被访问的空间,也就是map->space,然后用host_read读取map->space的内容得到时间。
AM通过端口读取时间,而NEMU将时间写到对应的地址供AM读取。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界