下面是对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-bit25 | bit24-bit20 | bit19-bit15 | bit14 | bit13 | bit12 | bit11-bit7 | it6-bit0 |
---|---|---|---|---|---|---|---|
funct7 | rs2 | rs1 | funct3 | funct3 | funct3 | rd | opcode |
功能选择 | 寄存器编号 | 寄存器编号 | xd | xs1 | xs2 | 寄存器编号 | 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值更大,则需要更多的位宽。
下面是软件代码的仿真过程。
代码步骤:
- 写数据到accumulator模块的寄存器中,rs2最低两位为寄存器编号。
- 将accumulator模块中的值和rs1的值进行相加,rs2最低两位为寄存器编号。
- 将第二步相加的值覆盖到accumulator模块的寄存器,rs2最低两位为寄存器编号。
- 读取accumulator模块中寄存器的值,rs2最低两位为寄存器编号,并输出到总线上。
- 写数据到内存0x70001000,0x70001004,0x70001008和0x7000100C。
- 将内存0x70001000,0x70001004,0x70001008和0x7000100C的数据装载入accumulator模块的寄存器中,rs2最低两位为寄存器编号。
- 将accumulator模块中的值和rs1的值进行相加,rs2最低两位为寄存器编号。
- 将第七步相加的值覆盖到accumulator模块的寄存器,rs2最低两位为寄存器编号。
- 读取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模块的内容到这里就基本介绍完了。