spike中增加uart外设

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外设,那么理论上,增加其他的外设,也都不是难事了。

此条目发表在RISCV分类目录,贴了, 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。