spike中增加uart外设
0赞spike是官方发布的riscv的指令模拟器。但是在spike中,支持的外设比较少,有如下一些:
- irom:存放复位之后的boot代码
- clinc:实现中断
- ram:模拟DRAM
如果我们想要做一个soc级的riscv指令模拟器,那么我们就需要往spike中,增加我们自己的外设。
下面介绍下,如何往spike中,增加uart外设。如果能成功增加uart外设,那么增加其他外设,也同样是可以的。
一、实现uart外设
在spike中外设,均要从类 abstract_device_t 中集成。该类,定义在 riscv/devices.h 中。
class abstract_device_t { public: virtual bool load(reg_t addr, size_t len, uint8_t* bytes) = 0; virtual bool store(reg_t addr, size_t len, const uint8_t* bytes) = 0; virtual ~abstract_device_t() {} } |
这个类中,主要实现了2个函数:
- load函数,负责实现core读取该外设时的行为
- store函数,负责实现core写入该外设时的行为
两个函数,如果返回1,就表示访问成功。如果返回0,就表示访问不成功。
实现uart_t类,代码如下:
#define UART_SIZE 0x1000 class uart_t : public abstract_device_t { public: uart_t() { std::cout << "create uart component" << std::endl; } ~uart_t() {} bool load(reg_t addr, size_t len, uint8_t* bytes); bool store(reg_t addr, size_t len, const uint8_t* bytes); size_t size() {return UART_SIZE;} private: uint32_t control_register=8; uint32_t reserve_register=7; uint8_t data_register=5; std::string s_data; } |
uart外设的大小是 0x1000。有如下寄存器:
- control_register: 控制寄存器
- data_register: 数据寄存器
- reserve_register:除了 控制/数据 寄存器之外的所有寄存器
s_data,用于保存写入到uart数据寄存器的信息。因为实现了行缓冲,当往数据寄存器写入回车时,uart外设就会将s_data中的数据全部打印出来。
load,store的函数,实现如下:
#define DATA_REG_OFFSET 0x00 #define CON_REG_OFFSET 0x04 bool uart_t::load(reg_t addr, size_t len, uint8_t *bytes) { if(addr == CON_REG_OFFSET) memcpy(bytes, (uint8_t*)&control_register, len); else memcpy(bytes, (uint8_t*)&reserve_register, len); return true; } bool uart_t::store(reg_t addr, size_t len, const uint8_t *bytes) { if(addr == DATA_REG_OFFSET) { memcpy((uint8_t*)&data_register, bytes, 1); if( data_register == '\n') { std::cout << s_data << std::endl; s_data.clear(); } else { s_data += data_register; } } else if(addr == CON_REG_OFFSET) { memcpy((uint8_t*)&control_register, bytes, 4); } return true; } |
在riscv-isa-sim跟目录,创建device目录,将实现的uart.h和uart.c 文件,放到这个目录下。
另外实现soc_components.h文件,用来include 我们自己实现的外设。
#ifndef __SOC_COMPONENTS__ #define __SOC_COMPONENTS__ #include "uart.h" #endif |
在实现 soc_devices.h 文件,内容如下,用来往spike中,加入自己的外设。
#define UART_BASE 0x40000000 uart_t *uart; uart = new uart_t(); bus.add_device(reg_t(UART_BASE), uart); |
二、修改configure流程
因为有自己增加目录device,并且放置了我们自己实现的文件。需要修改configure流程,实现能够将增加的文件,编译到spike中。
首先修改根目录下的 configure.ac文件,在 MCPPBS_SUBPOJECTS选项中,增加device,表示编译的时候,要编译这个目录下的文件。
将riscv目录下的riscv.ac文件,拷贝到device目录下,并且重命名为device.ac。
创建 device.mk.in 文件,填入如下内容:
device_subproject_deps = \ riscv
device_install_prog_srcs =
device_hdrs = \ uart.h \ soc_devices.h \ soc_components.h
device_srcs = \ uart.cc |
修改riscv目录下的riscv.mk.in文件,在riscv_subproject_deps中,加入device,表示riscv这个目录编译时候,会依赖device这个目录下的源文件。
在加入如下内容,将uart.cc加入到 riscv_srcs 变量中。我这里,定义了 soc_devices_srcs 中间变量,方便将来增加其他外设。
上述修改之后,使用 autoconf configure.ac > configure, 更新configure文件。这样,之后编译的时候,才会将我们增加的代码,给编译进去。
三、修改sim_t
spike的仿真控制,是由sim_t这个类控制的。这个类,实现在 riscv/sim.cc中。
在sim_t的构造函数的最后,增加一行代码:
sim_t::sim_t(const char* isa, size_t nprocs, bool halted, reg_t start_pc, std::vector<std::pair<reg_t, mem_t*>> mems, const std::vector<std::string>& args, std::vector<int> const hartids, const debug_module_config_t &dm_config) : htif_t(args), mems(mems), procs(std::max(nprocs, size_t(1))), start_pc(start_pc), current_step(0), current_proc(0), debug(false), histogram_enabled(false), dtb_enabled(true), remote_bitbang(NULL), debug_module(this, dm_config) { signal(SIGINT, &handle_signal);
for (auto& x : mems) bus.add_device(x.first, x.second);
debug_module.add_device(&bus);
debug_mmu = new mmu_t(this, NULL);
if (hartids.size() == 0) { for (size_t i = 0; i < procs.size(); i++) { procs[i] = new processor_t(isa, this, i, halted); } } else { if (hartids.size() != procs.size()) { std::cerr << "Number of specified hartids doesn't match number of processors" << strerror(errno) << std::endl; exit(1); } for (size_t i = 0; i < procs.size(); i++) { procs[i] = new processor_t(isa, this, hartids[i], halted); } }
clint.reset(new clint_t(procs)); bus.add_device(CLINT_BASE, clint.get());
// add our soc device #include "soc_devices.h" } |
增加的 #include "soc_devices.h" 这行代码,就将我们之前实现的,往spike中增加外设的代码,给包含在这,从而实现增加外设。
四、spike访问外设原理
sim_t类中,有bus_t bus成员变量。
class bus_t : public abstract_device_t { public: bool load(reg_t addr, size_t len, uint8_t* bytes); bool store(reg_t addr, size_t len, const uint8_t* bytes); void add_device(reg_t addr, abstract_device_t* dev); std::pair<reg_t, abstract_device_t*> find_device(reg_t addr); private: std::map<reg_t, abstract_device_t*> devices; } |
实现的外设,均需要挂载到这个bus上面,这样sim_t,最终,可以通过访问该bus的load或者store函数,来实现core的load和store。
bus_t中,有devices这个关联数组,key是外设的首地址,键值是 abstract_device_t类指令。所以之前说,外设,需要从abstract_device_t中继承。
bus_t中的add_device,就往bus上,增加一个外设。所以之前soc_devices.h程序中,就通过调用bus.add_device函数,将我们实现的uart外设,挂载到bus上。从而让之后,core可以访问。
在sim_t中,实现了mmio_load和mmio_store函数。
bool sim_t::mmio_load(reg_t addr, size_t len, uint8_t* bytes) { if (addr + len < addr) return false; return bus.load(addr, len, bytes); } bool sim_t::mmio_store(reg_t addr, size_t len, const uint8_t* bytes) { if (addr + len < addr) return false; return bus.store(addr, len, bytes); |
当core执行一条load指令时,最终,会调用sim_t的mmio_load函数,而mmio_load函数,调用bus_t的load函数,bus_t根据传入的地址,确定是哪一个外设访问,在调用该外设的load函数,从而实现外设的load访问。同理,可以推出core执行一条store指令,是如何工作的。
五、总结
通过以上的流程,就往spike中增加了uart外设。当我们能够成功增加uart外设,那么理论上,增加其他的外设,也都不是难事了。
更多内容,访问我的个人网站:www.lujun.org.cn