The primary change in Chisel v3.6.0 is the transition from the Scala FIRRTL
Compiler to the new MLIR FIRRTL Compiler. This will have a minimal impact on
typical Chisel user APIs but a large impact on custom compiler flows. For
more information, please see the ROADMAP.
总线是什么?总线通常用于连接 CPU 和外设,为了更好的兼容性和可复用性,会想到能否设计一个统一的协议,其中 CPU 实现的是发起请求的一方(又称为 master),外设实现的是接收请求的一方(又称为 slave),那么如果要添加外设、或者替换 CPU 实现,都会变得比较简单,减少了许多适配的工作量。
那么,我们来思考一下,一个总线协议需要包括哪些内容?对于 CPU 来说,程序会读写内存,读写内存就需要以下几个信号传输到内存:
上面的 Wishbone Classic Standard 协议非常简单,但是会遇到一个问题:假设实现的是一个 SRAM 控制器,它的读操作有一个周期的延迟,也就是说,在这个周期给出地址,需要在下一个周期才可以得到结果。在 Wishbone Classic Standard 中,就会出现下面的波形:
a 周期:master 给出读地址 0x01,此时 SRAM 控制器开始读取,但是此时数据还没有读取回来,所以 ACK_I=0
b 周期:此时 SRAM 完成了读取,把读取的数据 0x12 放在 DAT_I 并设置 ACK_I=1
c 周期:master 给出下一个读地址 0x02,SRAM 要重新开始读取
d 周期:此时 SRAM 完成了第二次读取,把读取的数据 0x34 放在 DAT_I 并设置 ACK_I=1
从波形来看,功能没有问题,但是每两个周期才能进行一次读操作,发挥不了最高的性能。那么怎么解决这个问题呢?我们在 a 周期给出第一个地址,在 b 周期得到第一个数据,那么如果能在 b 周期的时候给出第二个地址,就可以在 c 周期得到第二个数据。这样,就可以实现流水线式的每个周期进行一次读操作。但是,Wishbone Classic Standard 要求 b 周期时第一次请求还没有结束,因此我们需要修改协议,来实现流水线式的请求。
实现思路也很简单:既然 Wishbone Classic Standard 认为 b 周期时,第一次请求还没有结束,那就让第一次请求提前在 a 周期完成,只不过它的数据要等到 b 周期才能给出。实际上,这个时候的一次读操作,可以认为分成了两部分:首先是 master 向 slave 发送读请求,这个请求在 a 周期完成;然后是 slave 向 master 发送读的结果,这个结果在 b 周期完成。为了实现这个功能,我们进行如下修改:
The timing parameter tWC is controlling the deassertion of smc_we_n_0. You can
use it to vary the hold time of smc_cs_n_0[3:0], smc_add_0[31:0] and
smc_data_out_0[31:0]. This differs from the read case where the timing
parameter tCEOE controls the delay in the assertion of smc_oe_n_0.
Additionally, smc_we_n_0 is always asserted one cycle after smc_cs_n_0[3:0] to
ensure the address bus is valid.
Valid, Invalid: When valid, the cache line is present in the cache. When invalid, the cache line is not present in the cache.
Unique, Shared: When unique, the cache line exists only in one cache. When shared, the cache line might exist in more than one cache, but this is not guaranteed.
Clean, Dirty: When clean, the cache does not have responsibility for updating main memory. When dirty, the cache line has been modified with respect to main memory, and this cache must ensure that main memory is eventually updated.
既然 snoop 是从 Interconnect 发给 Master,在已有的 AR R AW W B channel 里没办法做这个事情,不然会打破已有的逻辑。那不得不添加一对 channel,比如我规定一个 AC channel 发送 snoop 请求,规定一个 C channel 让 master 发送响应,这样就可以了。这就相当于 TileLink 里面的 B channel(Probe 请求)和 C channel(ProbeAck 响应)。实际 ACE 和刚才设计的实际有一些区别,把 C channel 拆成了两个:CR 用于返回所有响应,CD 用于返回那些需要数据的响应。这就像 AW 和 W 的关系,一个传地址,一个传数据;类似地,CR 传状态,CD 传数据。
traitCanHaveTraceIO{this:HasTiles=>valmodule:CanHaveTraceIOModuleImp// Bind all the trace nodes to a BB; we'll use this to generate the IO in the impvaltraceNexus=BundleBridgeNexusNode[Vec[TracedInstruction]]()valtileTraceNodes=tiles.flatMap{caseext_tile:WithExtendedTraceport=>Nonecasetile=>Some(tile)}.map{_.traceNode}tileTraceNodes.foreach{traceNexus:=_}}
traitCanHaveTraceIOModuleImp{this:LazyModuleImpLike=>valouter:CanHaveTraceIOwithHasTilesimplicitvalp:ParametersvaltraceIO=p(TracePortKey)map(traceParams=>{valextTraceSeqVec=(outer.traceNexus.in.map(_._1)).map(ExtendedTracedInstruction.fromVec(_))valtio=IO(Output(TraceOutputTop(extTraceSeqVec)))valtileInsts=((outer.traceNexus.in).map{case(tileTrace,_)=>DeclockedTracedInstruction.fromVec(tileTrace)}// Since clock & reset are not included with the traced instruction, plumb that out manually(tio.traceszip(outer.tile_prci_domainsziptileInsts)).foreach{case(port,(prci,insts))=>port.clock:=prci.module.clockport.reset:=prci.module.reset.asBoolport.insns:=insts}tio})}
/** Node for the core to drive legacy "raw" instruction trace. */valtraceSourceNode=BundleBridgeSource(()=>Vec(traceRetireWidth,newTracedInstruction()))/** Node for external consumers to source a legacy instruction trace from the core. */valtraceNode:BundleBridgeOutwardNode[Vec[TracedInstruction]]=traceNexus:=traceSourceNode
作为一个防御机制,首先要确定攻击方的能力。一个常见的威胁模型是认为,攻击者具有物理的控制,可以任意操控内存中的数据,但是无法读取或者修改 CPU 内部的数据。也就是说,只有 CPU 芯片内的数据是可信的,离开了芯片都是攻击者掌控的范围。一个简单的想法是让内存中保存的数据是加密的,那么怎样攻击者可以如何攻击加密的数据?下面是几个典型的攻击方法:
Spoofing attack:把内存数据改成任意攻击者控制的数据;这种攻击可以通过签名来解决
Splicing or relocation attack:把某一段内存数据挪到另一部分,这样数据的签名依然是正确的;所以计算签名时需要把地址考虑进来,这样地址变了,验证签名就会失败
另一种设计是 Parallelizable Authentication Tree(PAT),它采用 MAC 而不是 Hash,每个结点保存了一个随机的 nonce 和计算出来的 MAC 值,最底层的 MAC 输入是实际的数据,其他层的 MAC 输入是子结点的 nonce,最后在片内保存最后一次 MAC 使用的 nonce 值。这样的好处是更新的时候,每一层都可以并行算,因为 MAC 的输入是 nonce 值,不涉及到子结点的 MAC 计算结果。缺点是要保存更多数据,即 MAC 和 nonce。
具体来说,为了避免重放攻击,每次更新数据的时候,就让 counter 加一,这和原来采用一个足够长(比如 64-bit)的随机 nonce 是类似的。重新加密是很耗费时间的,因此为了把重新加密的范围局限到一个小的局部,又设计了一个两级的 counter:7-bit 的 local counter,每次更新数据加一;64-bit 的 global counter,当某一个 local counter 溢出的时候加一。这时候实际传入 MAC 计算的 counter 则是 global counter 拼接上 local counter。这样相当于是做了一个 counter 的共同前缀,在内存访问比较均匀的时候,比如每个 local counter 轮流加一,那么每次 local counter 溢出只需要重新加密一个小范围的内存,减少了开销。
Acquire:M->S 在 A channel 上发送 Acquire,S->M 在 D channel 上发送 Grant,然后 M->S 在 E channel 上发送 GrantAck;功能是获取一个 copy,可以看到这个和 Get 是类似的,都是在 A channel 上发送请求,在 D channel 上接受响应,只不过额外需要在 E channel 上发送 GrantAck。
Release:M->S 在 C channel 上发送 Release/ReleaseData,S->M 在 D channel 上发送 ReleaseAck;功能是删除自己的 copy,一般是缓存行要被换出的时候,如果要写回 Dirty 数据,就用 ReleaseData,否则用 Release
Probe:S->M 在 B channel 上发送 Probe,M->S 在 C channel 上发送 ProbeAck;功能是要求 M 删除自己的 copy,通常是有某一个缓存发送了 Acquire,导致其他缓存需要降低权限
可以看到,A C E 三个 channel 是 M->S,B D 两个 channel 是 S->M。
假如一个缓存(Master A)要写入一块只读数据,或者读取一块 miss 的缓存行,如果是广播式的缓存一致性协议,那么需要经历如下的过程:
Master A -> Slave: Acquire
Slave -> Master B: Probe
Master B -> Slave: ProbeAck
Slave -> Master A: Grant
Master A -> Slave: GrantAck
首先 Master A 发出 Acquire 请求,然后 Slave 向其他 Master 广播 Probe,等到其他 Master 返回 ProbeAck 后,再向 Master A 返回 Grant,最后 Master A 发送 GrantAck 给 Slave。这样 Master A 就获得了这个缓存行的一份拷贝,并且让 Master B 的缓存行失效或者状态变成只读。
此外,标准还说可以在 B 和 C channel 上进行 TL-UH 的操作。标准这么设计的意图是可以让 Slave 转发操作到拥有缓存数据的 Master 上。比如 Master A 在 A channel 上发送 Put 请求,那么 Slave 向 Master B 的 B channel 上发送 Put 请求,Master B 在 C channel 上发送 AccessAck 响应,Slave 再把响应转回 Master A 的 D channel。这就像是一个片上的网络,Slave 负责在 Master 之间路由请求。
首先来看 B channel 上的 Probe 逻辑。它记录了一个 todo bitmask,表示哪些 Master 需要发送 Probe,这里采用了 Probe Filter 来减少发送 Probe 的次数,因为只需要向拥有这个缓存行的 Master 发送 Probe:
valprobe_todo=RegInit(0.U(max(1,caches.size).W))valprobe_line=Reg(UInt())valprobe_perms=Reg(UInt(2.W))valprobe_next=probe_todo&~(leftOR(probe_todo)<<1)valprobe_busy=probe_todo.orR()valprobe_target=if(caches.size==0)0.UelseMux1H(probe_next,cache_targets)// Probe whatever the FSM wants to do nextin.b.valid:=probe_busyif(caches.size!=0){in.b.bits:=edgeIn.Probe(probe_line<<lineShift,probe_target,lineShift.U,probe_perms)._2}when(in.b.fire()){probe_todo:=probe_todo&~probe_next}
在 B channel 发送 Probe 的同时,也要处理 C channel 上的 ProbeAck 和 ProbeAckData:
// Incoming C can be:// ProbeAck => decrement tracker, drop // ProbeAckData => decrement tracker, send out A as PutFull(DROP)// ReleaseData => send out A as PutFull(TRANSFORM)// Release => send out D as ReleaseAck
valid must never depend on ready. If a sender wishes to send a beat, it must assert valid independently of whether the receiver signals that it is ready.
As a consequence, there must be no combinational path from ready to valid or any of the control and data signals.
A low priority valid may not combinationally depend on a high priority valid. In other words, the decision to send a request may not be based on receiving a response in the same cycle.
A high priority ready may not combinationally depend on a low priority ready. In other words, acceptance of a response may not be made contingent upon a request being accepted the same cycle.
这两条的意思是,同一个周期内,我设置发送的请求的 valid,不能依赖于同一个周期内接受到的响应的 valid,比如 A 的 valid 不能组合依赖于 D 的 valid。另一方面,我设置的响应的 ready 不能依赖于同一个周期内的请求,比如 D 的 ready 不能组和依赖于 A 的 ready。
那么,有这么几种用法是可以的:
It is acceptable for a receiver to drive ready in response to valid or any of the control and data signals. For example, an arbiter may lower ready if a valid request is made for an address which is busy. However, whenever possible, it is recommended that ready be driven independently so as to reduce the handshaking circuit depth. 接收方可以让 ready 依赖于 valid 或者其他的控制和数据信号,不过这样会让组合逻辑比较长。
A channel may change valid and all control and data signals based on the value of ready in the prior cycle. For example, after a request has been accepted (ready HIGH), a new request may be presented. Only a same-cycle dependency of valid on ready is forbidden. 可以让当前周期的 valid 依赖于上一个周期的 ready 信号,只是不能有同周期的 valid 对 ready 的依赖。
A device may legally drive valid for a response based on valid of a request in the same cycle. For example, a combinational ROM which answers immediately. In this case, presumably ready for the request will likewise be driven by ready for the response. The converse relationship is forbidden. 设备可以让响应的 valid 依赖请求的 valid,比如一个组合的 ROM,它的 D channel 的 valid 可以组合依赖于 A channel 的 valid,同时 A channel 的 ready 组合依赖于 D channel 的 ready。这样就简化了设备的设计,并且可以无延迟地进行访问。
Note that a sender may raise valid and then lower it on the following
cycle, even if the message was not accepted on the previous cycle. For example,
the sender might have some other higher priority task to perform on the
following cycle, instead of trying to send the rejected message again.
Furthermore, the sender may change the contents of the control and data signals
when a message was not accepted.
TileLink 的 burst 请求是通过比 bus 更宽的 size 的多个 beat 组成的。一旦第一个 beat fire 了,后续只能发送同一个 burst 的数据,不可以交错。
Agents must eventually present all beats of a received message
Unless they have a higher priority message in flight or unanswered
Agents must eventually accept a presented beat
Agents must eventually answer a received request message
大概意思是,beat 不能无限推迟,无论是发送方还是接受方。对于每个请求,它的响应不能无限推迟。
TileLink 定义了各个 channel 的优先级,从低到高是 A<B<C<D<E。对于同一个 channel,A C E 上是 master/sender 优先级更高,B D 上是 slave/receiver 优先级更高。
TileLink 的设计里保证了,每个请求的响应都比请求优先级更高。比如 A channel 的请求(Get/Put/AcquireBlock)的响应在 D channel(AccessAckData/AccessAck/Grant),B channel 的请求(Probe)的响应在 C channel(ProbeAck),C channel 的请求(Release)的响应在 D channel(ReleaseAck),D channel 的请求(Grant)的响应在 E channel(GrantAck)。
接下来看看沁恒提供的代码是如何配置的。在 EVT/EXAM/SRC/Startup/startup_ch32v30x_D8C.S 可以看到初始化的汇编代码。比较有意思的是,这个核心扩展了 mtvec,支持 ARM 的 vector table 模式,即放一个指针数组,而不是指令:
.section.vector,"ax",@progbits.align1_vector_base:.optionnorvc;.word_start.word0.wordNMI_Handler/* NMI */.wordHardFault_Handler/* Hard Fault */
这些名字如此熟悉,只能说这是 ARVM 了(ARM + RV)。后面的部分比较常规,把 data 段复制到 sram,然后清空 bss:
handle_reset:.optionpush.optionnorelaxlagp,__global_pointer$.optionpop1:lasp,_eusrstack2:/* Load data section from flash to RAM */laa0,_data_lmalaa1,_data_vmalaa2,_edatabgeua1,a2,2f1:lwt0,(a0)swt0,(a1)addia0,a0,4addia1,a1,4bltua1,a2,1b2:/* Clear bss section */laa0,_sbsslaa1,_ebssbgeua0,a1,2f1:swzero,(a0)addia0,a0,4bltua0,a1,1b2:
最后是进行一些 csr 的配置,然后进入 C 代码:
lit0,0x1fcsrw0xbc0,t0/* Enable nested and hardware stack */lit0,0x1fcsrw0x804,t0/* Enable floating point and interrupt */lit0,0x6088csrsmstatus,t0lat0,_vector_baseorit0,t0,3csrwmtvec,t0luia0,0x1fffflia1,0x300sha1,0x1b0(a0)1:luis2,0x40022lwa0,0xc(s2)andia0,a0,1bneza0,1bjalSystemInitlat0,maincsrwmepc,t0mret
set(CMAKE_SYSTEM_NAMEGeneric)set(CMAKE_C_COMPILERriscv64-unknown-elf-gcc)set(CMAKE_CXX_COMPILERriscv64-unknown-elf-g++)# Make CMake happy about those compilersset(CMAKE_TRY_COMPILE_TARGET_TYPE"STATIC_LIBRARY")
-defer
only read the abstract syntax tree and defer actual compilation
to a later 'hierarchy' command. Useful in cases where the default
parameters of modules yield invalid or not synthesizable code.