Rocket-chip-RoCC(3)

下面是对accumulator模块的具体实现进行说明。

1、Accumulator
类AccumulatorExample,需要输入opcodes和n参数,里面使用new创建AccumulatorExampleModuleImp的对象。

class AccumulatorExample(opcodes: OpcodeSet, val n: Int = 4)(implicit p: Parameters) extends LazyRoCC(opcodes) {
  override lazy val module = new AccumulatorExampleModuleImp(this)
}
class AccumulatorExampleModuleImp(outer: AccumulatorExample)(implicit p: Parameters) extends LazyRoCCModuleImp(outer)
    with HasCoreParameters {
  val regfile = Mem(outer.n, UInt(width = xLen))
  val busy = Reg(init = Vec.fill(outer.n){Bool(false)})

  val cmd = Queue(io.cmd)
  val funct = cmd.bits.inst.funct
  val addr = cmd.bits.rs2(log2Up(outer.n)-1,0)
  val doWrite = funct === UInt(0)
  val doRead = funct === UInt(1)
  val doLoad = funct === UInt(2)
  val doAccum = funct === UInt(3)
  val memRespTag = io.mem.resp.bits.tag(log2Up(outer.n)-1,0)

  // datapath
  val addend = cmd.bits.rs1
  val accum = regfile(addr)
  val wdata = Mux(doWrite, addend, accum + addend)

  when (cmd.fire() && (doWrite || doAccum)) {
    regfile(addr) := wdata
  }

  when (io.mem.resp.valid) {
    regfile(memRespTag) := io.mem.resp.bits.data
    busy(memRespTag) := Bool(false)
  }

  // control
  when (io.mem.req.fire()) {
    busy(addr) := Bool(true)
  }

  val doResp = cmd.bits.inst.xd
  val stallReg = busy(addr)
  val stallLoad = doLoad && !io.mem.req.ready
  val stallResp = doResp && !io.resp.ready

  cmd.ready := !stallReg && !stallLoad && !stallResp
    // command resolved if no stalls AND not issuing a load that will need a request

  // PROC RESPONSE INTERFACE
  io.resp.valid := cmd.valid && doResp && !stallReg && !stallLoad
    // valid response if valid command, need a response, and no stalls
  io.resp.bits.rd := cmd.bits.inst.rd
    // Must respond with the appropriate tag or undefined behavior
  io.resp.bits.data := accum
    // Semantics is to always send out prior accumulator register value

  io.busy := cmd.valid || busy.reduce(_||_)
    // Be busy when have pending memory requests or committed possibility of pending requests
  io.interrupt := Bool(false)
    // Set this true to trigger an interrupt on the processor (please refer to supervisor documentation)

  // MEMORY REQUEST INTERFACE
  io.mem.req.valid := cmd.valid && doLoad && !stallReg && !stallResp
  io.mem.req.bits.addr := addend
  io.mem.req.bits.tag := addr
  io.mem.req.bits.cmd := M_XRD // perform a load (M_XWR for stores)
  io.mem.req.bits.size := log2Ceil(8).U
  io.mem.req.bits.signed := Bool(false)
  io.mem.req.bits.data := Bits(0) // we're not performing any stores...
  io.mem.req.bits.phys := Bool(false)
}
  • AccumulatorExampleModuleImp类中混入LazyRoCCModuleImp和HasCoreParameters。

  • 声明regfile值为可读写存储器,深度为n,数据宽度为xLen,即有reg [xLen:0] regfile [0:n-1]。

  • 声明busy值为寄存器,初始值为0,根据n的数值声明busy个数,使用矢量vec声明n个busy信号。

  • 接着声明cmd值,它是Queue输出的值,而Queue输入的值io.cmd,这部分值是由core那边传过来的,然后输入Queue,Queue类似于FIFO的作用,先入先出,将io.cmd的信号打入后,另一边作为cmd信号打出。

  • 出来的cmd信号,将cmd.bits.inst.funct赋给funct。
    出来的cmd信号,将cmd.bits.rs2(log2Up(outer.n)-1,0)赋给addr。log2Up(outer.n)就是2^x=n,因为例化AccumulatorExample时,输入的n为4,所以x=2,所以log2Up(outer.n)-1=2-1=1,所以取cmd.bits.rs2(log2Up(outer.n)-1,0)
    => cmd.bits.rs2(1,0) ,也就是取rs2(这里需要注意的是,这个cmd.bits.rs2是一个xLen宽度的数据,我的32bits的数据,而不是cmd.bits.inst.rs2,这个cmd.bits.inst.rs2是rs2通用寄存器的编号,是5bits的)的最低两位。

  • 如果funct == 0,则doWrite值置1,如果funct == 1,那么doRead置1,如果funct ==
    2,那么doLoad置1,如果funct == 3,那么doAccum置1。

  • 声明memRespTag值,是io.mem.resp.bits.tag(log2Up(outer.n)-1,0),取io.mem.resp.bits.tag最低两位。

  • 将cmd.bits.rs1这个32位的数据赋给addend。

  • 将regfile(addr)的值赋给accum,addr由刚刚得出。
    通过doWrite进行选择,将addend和addend+accum做一个mux选择,输入给wdata。doWrite为1时,wdata为addend;doWrite为0时,wdata为addend+accum。

  • 当cmd.fire() && (doWrite ||
    doAccum)为真时,将wdata的值赋给regfile(addr)。fire()为ready==1’b1 && valid ==
    1’b1,也就是tilelink总线请求有效的时候。这里会生成时序逻辑,会使用always块。

  • 当io.mem.resp.valid为1,也就是Accumulator模块计算完成的时候,那么需要更新regfile的值,地址是memRespTag(1:0),值为io.mem.resp.bits.data。同时也需要更新busy的值,利用memRespTag的编号查找具体哪个busy值,将它更新为1’b0。

  • 当io.mem.req.fire()为1,也就是从memory中读取数据成功后,根据addr的编号,拉高相应编号busy的值,为1’b1。

  • memory接口是接到dcache中的,数据会从dcache或者外部sram进来,最后进到Accumulator模块。

  • 将cmd.bits.inst.xd的值赋给doResp,标志此次计算是需要将计算结果进行返回的。

  • 如果存在busy信号为1的情况,则置stallReg为1,这里因为有多个busy信号(由addr决定编号),所以最后的stallReg应该是一个mux出来的信号,推迟下次指令操作的数据输入。

  • 如果存在io.mem.req.ready为0(memory没有空闲)的情况,则置stallLoad为1,推迟accumulator模块转载数据的操作。
    当doResp为1(需要返回数据操作),而io.resp.ready为1(core没有准备好,在忙)时,则置stallResp为1,推迟返回计算结果的操作。

  • 如果!stallReg && !stallLoad &&
    !stallResp的情况存在,也就是没有busy,没有在装载数据,也没有在应答返回数据时,accumulator模块为空闲状态,可以接受新的指令操作输入,所以将cmd.ready置1。

  • 有效响应io.resp.valid,注释已经说明得很清楚,所以这里不解释。

  • 响应数据的寄存器编号由输入的cmd.bits.inst.rd决定。 响应的数据为accum的值。

  • accumulator模块的总busy信号(输出给core的),由cmd.valid ||
    busy.reduce(||)决定。当cmd.valid有效时,io.busy拉高;当val busy = Reg(init =
    Vec.fill(outer.n){Bool(false)})中,有一个busy为1时,那么io.busy都要拉高。reduce(||)操作是将组内的信号一一或后得到最终值。将io.interrupt强接为0,生成的RTL会对这个端口进行优化处理,也就是去掉。

  • 最后一部分是memory请求数据的接口,接口协议应该是tilelink的。

  • 说明一下,log2Ceil(8).U为无符号数的3,log2Ceil(8) -> 2^n=8,所以n=3,所以log2Ceil(8)
    =3。还有M_XRD是一个常量值,具体是多少可以grep一下这个关键字,它在其他模块定义的。其他的连接情况很明显,我就不再做说明了。

accumulator的scala代码说明完了,但可能还是比较糊涂,所以我们再扣一下,看一下这个accumulator模块到底是实现什么功能的。

下面是custom0的定制格式。

bit31-bit25bit24-bit20bit19-bit15bit14bit13bit12bit11-bit7it6-bit0
funct7rs2rs1funct3funct3funct3rdopcode
功能选择寄存器编号寄存器编号xdxs1xs2寄存器编号b0001011

xs1和xs2保留,xd为是否将计算结果返回到rd寄存器中。

功能说明:

funct7说明
7’d0运行doWrite操作,将rs1的32位数据存到accumulator模块memory中,memory addr为指令的bits[21:20]。也就是rs2的低两位。
7’d1运行doRead操作,保留。功能没有实现,应该是将accumulator模块memory(addr)的值覆盖到通用寄存器中。
7’d2运行doLoad操作,将rs1作为外部memory的地址,转载该地址的数据,并将32位数据存入accumulator模块的memory中,存入的地址为指令的bits[21:20]。也就是rs2的低两位。
7’d3运行doAccum操作,将rs1的32位数据和accumulator模块的memory(addr)值进行相加,并将结果覆盖到memory(addr)中。Addr为指令的bits[21:20]。也就是rs2的低两位。
7’d4-127保留扩展,可以自行添加。

*注意,addr的位宽是可变的,根据AccumulatorExample的n参数而定,因为我采用了默认值4,所以addr只有两位,所以只用到rs2的最低两位,如果n值更大,则需要更多的位宽。

下面是软件代码的仿真过程。
代码步骤:

  1. 写数据到accumulator模块的寄存器中,rs2最低两位为寄存器编号。
  2. 将accumulator模块中的值和rs1的值进行相加,rs2最低两位为寄存器编号。
  3. 将第二步相加的值覆盖到accumulator模块的寄存器,rs2最低两位为寄存器编号。
  4. 读取accumulator模块中寄存器的值,rs2最低两位为寄存器编号,并输出到总线上。
  5. 写数据到内存0x70001000,0x70001004,0x70001008和0x7000100C。
  6. 将内存0x70001000,0x70001004,0x70001008和0x7000100C的数据装载入accumulator模块的寄存器中,rs2最低两位为寄存器编号。
  7. 将accumulator模块中的值和rs1的值进行相加,rs2最低两位为寄存器编号。
  8. 将第七步相加的值覆盖到accumulator模块的寄存器,rs2最低两位为寄存器编号。
  9. 读取accumulator模块中寄存器的值,rs2最低两位为寄存器编号,并输出到总线上。

rocc-software-master参考路径:
https://blog.csdn.net/a_weiming/article/details/113359605

#include "encoding.h"
//xcustom.h头文件的内容看rocc-software-master代码说明。
#include "xcustom.h"

#define U32 *(volatile unsigned int *)
#define DEBUG_SIG   0x70000000
#define DEBUG_VAL   0x70000004

//--------------------------------------------------------------------------
// handle_trap function

void handle_trap()
{
    asm volatile ("nop");
    while(1);
}

//--------------------------------------------------------------------------
// Main

void main()
{
    unsigned int rs1,rs2,rd1;
    unsigned int i;

    //doWrite
    for(i=0;i<4;i++)
    {   
        rs1 = 0x0bcdef12 + 3*i;
        rs2 = i;
        ROCC_INSTRUCTION(0, rd1, rs1, rs2, 0); 
    }

    //doAccum
    for(i=0;i<4;i++)
    {   
        rs1 = 0x98765432 + 4*i;
        rs2 = i;
        ROCC_INSTRUCTION(0, rd1, rs1, rs2, 3); 
        //读取模块寄存器的数据,rs2最低两位为寄存器的编号。
        ROCC_INSTRUCTION(0, rd1, rs1, rs2, 1); 
        U32(DEBUG_VAL + 4*i + 0x100) = rd1;
    }

    //doLoad
    rs1 = 0x70001000;
    for(i=0;i<4;i++)
    {   
        U32(rs1 + 4*i*100) = 0x12345678 + 2*i;
    }   
    for(i=0;i<4;i++)
    {   
        rs2 = i;
        ROCC_INSTRUCTION(0, rd1, (rs1+4*i*100), rs2, 2); 
    }

    //doAccum
    for(i=0;i<4;i++)
    {   
        rs1 = 0x77888877 + 5*i;
        rs2 = i;
        ROCC_INSTRUCTION(0, rd1, rs1, rs2, 3); 
        //读取模块寄存器的数据,rs2最低两位为寄存器的编号。
        ROCC_INSTRUCTION(0, rd1, rs1, rs2, 1); 
        U32(DEBUG_VAL + 4*i + 0x200) = rd1;
    }

    //用于结束仿真。
    U32(DEBUG_SIG) = 0xFF;
    while(1);
}

在跑仿真过程中,我发现了accumulator模块的一个bug。如下图。绿色箭头的地方,是我执行custom0 doLoad的操作,实际上我想load的数据是0x12345678,0x1234567a,0x1234567c和0x1234567e,但现在load到寄存器的是随机值,与我预期的不一致,所以我debug了一下代码。在这里插入图片描述
当放大doLoad段波形时,如下图,io_mem_resp_valid,拉高了两拍,而实际有效的数据为第一拍的数据,accumulator模块中的寄存器变化是以io_mem_resp_valid拉高为准的,所以看到数据先是0x1234567a,然后是0xeec810dd,最后存入寄存器的反而是第二拍的数据0xeec810dd,所以这里存在bug。
在这里插入图片描述
我修改了一下RTL,修复了这个bug,下图为修改的RTL代码。我先以修改的RTL代码进行accumulator模块功能的说明,后面会附带scala代码的修改。白色箭头为原代码,蓝色箭头是我修改后的代码。我修改代码的思路是当memory返回的数据(memory tag会记录当前使用寄存器的编号)和当前使用的寄存器编号相同时,才会将返回的数据存入寄存器中。
在这里插入图片描述
看一下正常运行的波形,如下图。每次只需custom0指令,io_busy都会拉高。

  • 红色箭头:doWrite相关的操作,可以看到regfile是一个个变的。
  • 黄色箭头:doAccum相关的操作,将rs1和相应寄存器的值相加,并覆盖到寄存器中。
  • 蓝色箭头:doRead的操作,将rs2最低两位对应的寄存器数据读到rd中,并返回。上面的蓝色箭头(mmio),是将返回的数据输出到总线中,利用观察。
  • 白色圈圈:memory操作,预先将数据存入0x70001000,0x70001004,0x70001008和0x7000100C中。
  • 绿色箭头:doLoad相关的操作,将数据从0x70001000,0x70001004,0x70001008和0x7000100C装载到regfile中,绿色箭头io_busy只有3个长波形,因为第二和第三次操作的两个busy信号连在一起了。
  • 白色箭头:为rs1等于0x77888877,利用此值和装载的memory数据进行相加,并覆盖到寄存器中。
    在这里插入图片描述
    注意:由上仿真波形图可知,busy信号可以为多拍信号,当busy信号拉高时,rocket-chip core的运行是暂停的,也就是定制加速器的这些模块是可以拉住CPU的,只有定制加速器模块任务完成才会放开CPU,这里需要考虑CPU性能和效率的问题,具体内容就不在这里展开讨论。

最后贴一下修改的scala代码,我修改后重新生成了RTL代码,但我没有使用这份代码再跑仿真了,如果遇到问题可以私信我,谢谢。

  //x_modify
  val addr_latch = Reg(UInt(width = 8), init = UInt(0))
  when (cmd.fire()) {
    addr_latch := addr
  }

  //TODO: have a memory bug!
  //when (io.mem.resp.valid) {
  //x_modify
  when (io.mem.resp.valid & (busy(memRespTag) === UInt(1)) & (addr_latch === io.mem.resp.bits.tag(log2Up(outer.n)-1,0))) {
    regfile(memRespTag) := io.mem.resp.bits.data
    busy(memRespTag) := Bool(false)
  }

关于accumulator模块的内容到这里就基本介绍完了。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值