这两天做到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读取。