下面是对CharacterCount模块的具体实现进行说明。
3、CharacterCount
类CharacterCountExample,需要输入opcodes参数,里面使用new创建CharacterCountExampleModuleImp的对象。声明atlNode为TLClientNode的类,用于TileLink协议,TLClientNode应该是作为master方的,用于发起数据的请求 。
class CharacterCountExample(opcodes: OpcodeSet)(implicit p: Parameters) extends LazyRoCC(opcodes) {
override lazy val module = new CharacterCountExampleModuleImp(this)
override val atlNode = TLClientNode(Seq(TLClientPortParameters(Seq(TLClientParameters("CharacterCountRoCC")))))
}
CharacterCountExampleModuleImp类中混入LazyRoCCModuleImp、HasL1CacheParameters和HasCoreParameters。
- 声明cacheParams值,如果存在tileParams.icache(存在icache),则cacheParams为1,不然为None(val cacheParams = tileParams.icache.get这句含义我也不太确定),但可以知道的是CharacterCountExampleModuleImp类的下文没有使用到cacheParams的值,所以暂时先这样理解。如果有明确知道的请告诉我,我再修改补充,谢谢。
- 声明私有的值blockOffset,是blockOffBits的值,而追下去可以找到:
def blockOffBits = lgCacheBlockBytes
def lgCacheBlockBytes = log2Up(cacheBlockBytes)
def cacheBlockBytes = p(CacheBlockBytes)
class WithCacheBlockBytes(linesize: Int) extends Config((site, here, up) => {
case CacheBlockBytes => linesize
})
- 也就是blockOffset的值可以通过WithCacheBlockBytes()来设定的。
- 声明私有的值beatOffset,是以2为底(cacheDataBits/8)的对数。
- 声明needle为寄存器,位宽为8。
- 声明addr为寄存器,位宽为coreMaxAddrBits。
- 声明count为寄存器,位宽为xLen。
- 声明resp_rd为寄存器,位宽和io.resp.bits.rd一致,应该是5。
- 声明addr_block为addr的某些位,应该是addr[31:6]。
- 声明offset为addr的某些位,应该是addr[5:0]。
- 声明next_addr为(addr_block+1)的值左移6位。
- 声明状态机的状态,分别为s_idle、s_acq、s_gnt、s_check和s_resp。
- 声明state寄存器,用于存储状态机的状态,初始值为s_idle。
- 声明tl_out值,将与core tilelink总线连接的信号atlNode.out(0)赋给它,这里的edgesOut我理解为边缘触发的意思。
- 将tl_out.d.bits各信号(tilelink的d组信号,有多个信号并非只有一个信号)赋值给gnt,即gnt为d组信号的组合。
- 声明recv_data为寄存器,位宽为cacheDataBits。
- 声明recv_beat为寄存器,位宽是以2为底(cacheDataBeats+1)的对数,且初始值为0。
- 声明一维数组data_bytes,深度为cacheDataBits/8,i为深度减1。当i=0时,data_bytes[0]= recv_data(7,0),当i=1时,data_bytes[1]= recv_data(15,8),当i=2时,data_bytes[2]= recv_data(23,16),当i=3时,data_bytes[3]= recv_data(31,24)。
- 声明zero_match信号组,当data_bytes[0] = 0,则zero_match[0]为1,当data_bytes[1] = 0,则zero_match[1]为1,当data_bytes[2] = 0,则zero_match[2]为1,当data_bytes[3] = 0,则zero_match[3]为1。
声明needle_match信号组,当data_bytes[0] = needle,则needle_match[0]为1,当data_bytes[1] = needle,则needle_match[1]为1,当data_bytes[2] = needle,则needle_match[2]为1,当data_bytes[3] = needle,则needle_match[3]为1。 - 声明first_zero的值,对zero_match信号组进行优先级解码。即当{zero_match[0],zero_match[1],zero_match[2],zero_match[3]}={1’b1,1’bx,1’bx,1’bx}时,first_zero=0;当{zero_match[0],zero_match[1],zero_match[2],zero_match[3]}={1’b0,1’b1,1’bx,1’bx}时,first_zero=1;当{zero_match[0],zero_match[1],zero_match[2],zero_match[3]}={1’b0,1’b0,1’b1,1’bx}时,first_zero=2;当{zero_match[0],zero_match[1],zero_match[2],zero_match[3]}={1’b0,1’b0,1’b0,1’bx}时,first_zero=3。
- 声明chars_found的值,此值通过一系列操作得到。needle_match.zipWithIndex.map {},做的就是将needle_match信号组按index展开,然后进行{}里面的操作。
case (matches, i) =>
val idx = Cat(recv_beat - UInt(1), UInt(i, beatOffset))
matches && idx >= offset && UInt(i) <= first_zero
- 因为needle_match会展开为各个信号,所以这里的matches就是指对应展开的needle_match_X(X=0/1/2/3),i就是index,可以为0/1/2/3。即needle_match_0和i=0进行一次val idx = Cat(recv_beat - UInt(1), UInt(i, beatOffset))和matches && idx >= offset && UInt(i) <= first_zero操作,以此类推。idx为信号拼接,分别采用recv_beat - UInt(1)和UInt(i, beatOffset)拼成。UInt(x,y),x数值,y为位宽。后面一行是条件判断。
- PopCount()操作是将括号内的结果逐一相加。
- 那么这段操作的意思是:
- 取i=0,matches=needle_match_0,得到idx0={recv_beat - UInt(1), UInt(0)},然后判断needle_match_0 && (idx0 >= offset) && (0 <= first_zero)的结果,并存入tmp0中。
- 取i=1,matches=needle_match_1,得到idx1={recv_beat - UInt(1), UInt(1)},然后判断needle_match_1 && (idx1 >= offset) && (1 <= first_zero)的结果,并存入tmp1中。
- 取i=2,matches=needle_match_2,得到idx2={recv_beat - UInt(1), UInt(2)},然后判断needle_match_2 && (idx2 >= offset) && (2 <= first_zero)的结果,并存入tmp2中。
- 取i=3,matches=needle_match_3,得到idx3={recv_beat - UInt(1), UInt(3)},然后判断needle_match_3 && (idx3 >= offset) && (3 <= first_zero)的结果,并存入tmp3中。
- 最后利用PopCount()操作,将tmp0,tmp1,tmp2和tmp3加起来,得到一个3bits的数据,赋给chars_found[2:0]。
声明zero_found,其值为zero_match_0 || zero_match_1 || zero_match_2 || zero_match_3。
- 声明finished为单bit寄存器。
- 当state为s_idle时,io.cmd.ready输出为1。
- 当state为s_resp时,io.resp.valid输出为1。
- io.resp.bits.rd等于 resp_rd,io.resp.bits.data等于count。
- 当state为s_acq时,tl_out.a.valid输出为1。
- tl_out.a.bits是a组多个信号的捆绑,使用edgesOut.Get(这里的Get是TileLink的一种操作方式,可以简单理解为数据获取,是通过A组信号进行的,与缓存操作没有关系,具体说明大家可以查看TileLink协议)对不同信号进行赋值。edgesOut.Get在tilelink/Edges.scala中定义,代码如下。
// Accesses
def Get(fromSource: UInt, toAddress: UInt, lgSize: UInt) = {
require (manager.anySupportGet, s"TileLink: No managers visible from this edge support Gets, but one of these clients would try to request one: ${client.clients}")
val legal = manager.supportsGetFast(toAddress, lgSize)
val a = Wire(new TLBundleA(bundle))
a.opcode := TLMessages.Get
a.param := UInt(0)
a.size := lgSize
a.source := fromSource
a.address := toAddress
a.mask := mask(toAddress, lgSize)
a.data := UInt(0)
a.corrupt := Bool(false)
(legal, a)
}
-
当stata为s_gnt时,tl_out.d.ready输出为1。
-
当io.cmd.ready=1和io.cmd.vaild=1时,进行下面的非阻塞操作:
-
将io.cmd.bits.rs1赋给addr。
-
将io.cmd.bits.rs2赋给needle。
-
将io.cmd.bits.inst.rd赋给resp_rd。
-
count信号清0,finished信号清0,状态跳转至s_acq态。
-
当tl_out.a.ready=1和tl_out.a.valid=1时,状态跳转至s_gnt态。
-
当tl_out.d.ready=1和tl_out.d.valid=1时,进行下面非阻塞操作:
1)recv_beat自加1。
2)recv_data等于gnt.data。
3)状态跳转至s_check态。 -
当状态为s_check时,进行以下非阻塞操作:
1)若操作没有完成的话,count自加chars_found。
2)若zero_found为1时,finished由0跳变为1。
3)如果recv_beat等于cacheDataBeats时,addr跳变为next_addr,而state根据zero_found || finished的结果来选择,如果为1则state<= s_resp;为0则state<=s_acq。
4)如果recv_beat不等于cacheDataBeats时,state<=s_gnt。 -
当io.resp.ready=1和io.resp.valid=1时,state<=s_idle。
-
当state 不等于s_idle时,io.busy都为1。
-
io.interrupt强接0,给优化处理掉。
-
io.mem.req.valid强接0,给优化处理掉。
-
因为TileLink协议是有5组信号的,分别是a,b,c,d和e,不同组负责不同的功能,a具有类似于写的功能,d具有类似于读的功能,这里只是简化,a和d组还有更复杂的功能。这里tl_out.b.ready := Bool(true),tl_out.c.valid := Bool(false)和tl_out.e.valid := Bool(false),会将b,c和e组的信号优化掉,生成的RTL不会存在b,c和e组的信号。
class CharacterCountExampleModuleImp(outer: CharacterCountExample)(implicit p: Parameters) extends LazyRoCCModuleImp(outer)
with HasCoreParameters
with HasL1CacheParameters {
val cacheParams = tileParams.icache.get
private val blockOffset = blockOffBits
private val beatOffset = log2Up(cacheDataBits/8)
val needle = Reg(UInt(width = 8))
val addr = Reg(UInt(width = coreMaxAddrBits))
val count = Reg(UInt(width = xLen))
val resp_rd = Reg(io.resp.bits.rd)
val addr_block = addr(coreMaxAddrBits - 1, blockOffset)
val offset = addr(blockOffset - 1, 0)
val next_addr = (addr_block + UInt(1)) << UInt(blockOffset)
val s_idle :: s_acq :: s_gnt :: s_check :: s_resp :: Nil = Enum(Bits(), 5)
val state = Reg(init = s_idle)
val (tl_out, edgesOut) = outer.atlNode.out(0)
val gnt = tl_out.d.bits
val recv_data = Reg(UInt(width = cacheDataBits))
val recv_beat = Reg(UInt(width = log2Up(cacheDataBeats+1)), init = UInt(0))
val data_bytes = Vec.tabulate(cacheDataBits/8) { i => recv_data(8 * (i + 1) - 1, 8 * i) }
val zero_match = data_bytes.map(_ === UInt(0))
val needle_match = data_bytes.map(_ === needle)
val first_zero = PriorityEncoder(zero_match)
val chars_found = PopCount(needle_match.zipWithIndex.map {
case (matches, i) =>
val idx = Cat(recv_beat - UInt(1), UInt(i, beatOffset))
matches && idx >= offset && UInt(i) <= first_zero
})
val zero_found = zero_match.reduce(_ || _)
val finished = Reg(Bool())
io.cmd.ready := (state === s_idle)
io.resp.valid := (state === s_resp)
io.resp.bits.rd := resp_rd
io.resp.bits.data := count
tl_out.a.valid := (state === s_acq)
tl_out.a.bits := edgesOut.Get(
fromSource = UInt(0),
toAddress = addr_block << blockOffset,
lgSize = UInt(lgCacheBlockBytes))._2
tl_out.d.ready := (state === s_gnt)
when (io.cmd.fire()) {
addr := io.cmd.bits.rs1
needle := io.cmd.bits.rs2
resp_rd := io.cmd.bits.inst.rd
count := UInt(0)
finished := Bool(false)
state := s_acq
}
when (tl_out.a.fire()) { state := s_gnt }
when (tl_out.d.fire()) {
recv_beat := recv_beat + UInt(1)
recv_data := gnt.data
state := s_check
}
when (state === s_check) {
when (!finished) {
count := count + chars_found
}
when (zero_found) { finished := Bool(true) }
when (recv_beat === UInt(cacheDataBeats)) {
addr := next_addr
state := Mux(zero_found || finished, s_resp, s_acq)
} .otherwise {
state := s_gnt
}
}
when (io.resp.fire()) { state := s_idle }
io.busy := (state =/= s_idle)
io.interrupt := Bool(false)
io.mem.req.valid := Bool(false)
// Tie off unused channels
tl_out.b.ready := Bool(true)
tl_out.c.valid := Bool(false)
tl_out.e.valid := Bool(false)
}
charactercount模块的实际功能是从cache中查找特定字符的数量。
- 指令中rs1的32位作为查找的起始地址。
- 指令中rs2的低8位,作为特定字符的输入。
- 当从cache中返回的数据存在8’h00,且状态机处于s_check时,计算结束,等待recv_beat等于16返回结果。
- 从cache中进行查找,因为配置的cache为4096Bytes,而sets为64,所以一个set里面有64Bytes,当recv_beat计数满足cacheDataBeats(因为总线是32bits,所以一次读4Bytes,64Bytes的数据需要读16次,即recv_beat需要等于16)时,返回结果。
- 满足计算结束的条件有三点,第一点返回的64Bytes的数据中存在8’h00;第二点状态机存在于s_check;第三点recv_beat需要等于16。
- 当64Bytes的数据中第一次检测到8’h00的存在,则不再对特定字符进行计数,即使后面还有也不再计数,默认8’h00为字符串的结束标志。
注意:如果不满足5)中所述的3点,charactercount模块会一直请求数据,一直处于s_gnt状态,同时拉死CPU,CPU不能再运行后面的指令操作。
仿真过程说明。
仿真代码如下:
#include "encoding.h"
#include "L1Dcache.h"
#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;
unsigned int test_num;
{
rs1 = 0x80000000;
rs2 = 0x1a;
for(i=0;i<32;i++) U32(0x80000000+i*4) = i + 1;
ROCC_INSTRUCTION(2, rd1, rs1, rs2, 0);
for(i=0;i<32;i++) U32(0x80000000+i*4) = 0x1a3456f1 + i;
ROCC_INSTRUCTION(2, rd1, rs1, rs2, 0);
for(i=0;i<16;i++) U32(0x80000000+i*4) = 0x1a3456f1 + i;
U32(0x8000003C) = 0x001a2345;
ROCC_INSTRUCTION(2, rd1, rs1, rs2, 0);
for(i=0;i<1000;i++) U32(0x70000000+i*4) = i;
}
U32(DEBUG_SIG) = 0xFF;
while(1);
}
代码步骤:
- 配置rs1为0x80000000,rs2为0x1a,即搜索基地址为0x80000000,搜索字符串为0x1a。
- 往0x80000000-0x8000007C地址写入0x1-0x31。
- 执行custom3。
- 往0x80000000-0x8000007C地址写入0x1a3456f1-0x0x1a345700。
- 执行custom3。
- 往0x80000000-0x80000078地址写入0x1a3456f1-0x0x1a3456ff。
- 往0x8000007C写入0x001a2345。
- 执行custom3。
- 延迟一段时间,往0x70000000+i*4地址写一堆没有用的数据。
- 最后往DEBUG_SIG地址发送0xFF结束仿真。
功能说明:
- 第一次往0x80000000-0x8000007C地址写入0x1-0x31,因为0x80000000存的数据为0x00000001,所以0x01会与0x1a进行比较,但不符合,所以count没有增加,第二个byte为0x00,所以zero_found置1,不再统计0x1a的个数。
- 第二次往0x80000000-0x8000007C地址写入0x1a3456f1-0x0x1a345700,因为每个32-bit数据都包含了0x1a,这里有16个0x1a,但最终统计的个数只会是15个,因为最后一个32-bit数据为0x0x1a345700,从最低8位开始计数,最低8位为0x00,所以在这里zero_found已经会被置1,因此不会再对后面的数据进行统计。
- 第三次往0x80000000-0x80000078地址写入0x1a3456f1-0x0x1a3456ff,往0x8000007C写入0x001a2345,这里看最后一个32-bit数据,因为是0x0x001a2345,所以第三byte的0x1a也会被统计,最后一byte才是0x00,所以最终统计的数量为16个。
仿真波形:
白色箭头:为匹配到字符串0x00,所以zero_found置1。
红色箭头:为需要匹配的字符串,rs2的低8位,为0x1a。
蓝色箭头:从memory中读取的数据,将32-bit数据拆分为4byte进行匹配,基地址为0x80000000。
绿色箭头:由于cache sizes和sets的配置,所以每次memory请求数据都是16个32-bit。
下图是为了说明功能2和3的差别,白色箭头返回的计数结果为15(0x0f),而蓝色箭头返回的计数结果为16(0x10),具体的原因在上面已经说明清楚。