不同的计算机内部需要通信,例如CPU和内存控制器通过一套协议通信,而内存控制器和内存颗粒通过另一套协议通信。
CPU <---> Memory Controller <-----> Memory Device
而在CPU内部,IFU和IDU需要通过信号通信,IDU和EXU通过信号通信。
软件模块中也有类似的需求,例如Difftest中NEMU需要和Spike通信,NPC需要和NEMU进行通信。
而广义的总线就叫通信系统-> TCP/IP 以太网,网线,RTL信号,系统调用等等
而我们用到的是狭义的总线-硬件模块的通信协议。
最简单的总线
IFU -> (inst) -> IDU
主动发起通信的模块叫master(主设备),响应通信的模块叫做slave(从设备)。
其背后的通信协议有:
- master(IFU)往slave(IDU)发送消息(也就是当前的inst),
- 协议规定,只要master发送,slave就立即收到
- 上述的发送行为每个周期都会发生
- 即每个周期master都往slave发送有效的指令
- 单周期处理器就是上述的协议。
较为真实的处理器总线:
IFU并非每个周期都能取到指令
- IDU需要等待IFU完成取值后,才能进行译码。
->inst -> IFU ->valid -> IDU
需要添加valid信号,用来指示何时发送有效的指令。通信协议如下:
- master(IFU)需要往slave(IDU)发送消息(当前指令inst)
- 双方约定,只要master发送,slave立即收到
总结来说只有valid有效的时候IDU才开始工作,但是inst在无时无刻都在传递。
更真实的处理器:
如果IDU并非每周期都能译码指令,
- IFU需要㩐带IDU完成当前的译码工作后才能发送下一条指令
inst -> IFU valid -> IDU <-ready
需要添加ready信号,通信协议如下:
- master(IFU)往slave(IDU)发送消息(即当前指令inst)
- 双方约定 ,若master发送,仅在ready有效的时候才认为slave收到。
- 上述发送行为仅在valid有效的时候才发生。
- 上述发送行为仅在valid有效发生
这就是异步总线,需要注意的点有: - 通信发生的时刻无法提前与之,在 valid & ready时才发生,称之为握手。
- valid & ready时,master需要暂存消息,避免丢失。
异步总线的RTL实现,也就是接口信号,Chisel提供了Decoupled模板,通过元编程实现异步总线接口。
Decoupled模板自带valid和ready
class Message extends Bundle { val inst = Output(UInt(32.W)) //定义类Message中有啥,有指令inst32位 } class IFU extends Module { val io = IO(new Bundle { val out = Decoupled(new Message) }) //把Message塞到Decoupled里面,IFU是发送方 // ... } class IDU extends Module { val io = IO(new Bundle { val in = Filpped(Decoupled(new Message)) }) //IDU是接受方,Chisel里接受方只需要外套一个Filpped // ... }
用这种高级语言的思想,我们想加一个信号的话只需要:
class Message extends Bundle { val inst = Output(UInt(32.W)) + val pc = Output(UInt(32.W)) //只需要加一行,其余的指令都不用改 }
异步总线的RTL实现-模块逻辑
master和slave需要根据握手信号的情况来实现约定的总线协议,其中master的状态转移图:
# master的状态转移图 +-+ valid = 0 | v valid = 1 1. idle ----------------> 2. wait_ready <-+ ^ | | | ready = 0 +--------------------------+ +----+ ready = 1
master处于空闲,valid就一直等于0,则master下一个状态也是idle。
如果master有消息要发,valid就设置为1,进入下一个状态(wait_ready)。也就是等slave的ready信号就绪
如果valid和ready的信号状态同时为1,就算成功握手,那么master就从wait_ready状态进入到idle状态了。
上面是IDU和IFU的总线协议,那么不同微结构的处理器,只是模块间的通信协议不同。例如:
- 单周期:每周期上游发送消息均有效,下游均就绪收到新消息。
- 多周期:模块空闲的时候消息无效,模块忙碌时候不接收新消息,IFU收到WBU的完成信号后在取下一条指令。
- 流水线:IFU一直取值,各模块每周期都尝试往下游发送消息
- 乱需执行:下游模块有一个队列,上游只需要把消息发送到队列,即可继续处理新消息
+-----+ inst ---> +-----+ ... ---> +-----+ ... ---> +-----+ | IFU | valid ---> | IDU | valid ---> | EXU | valid ---> | WBU | +-----+ <--- ready +-----+ <--- ready +-----+ <--- ready +-----+
分布式控制和集中式控制
+--------------+ +-------------> | Controller | <--------------+ | +--------------+ | | ^ ^ | v v v v +-----+ inst +-----+ ... +-----+ ... +-----+ | IFU | ------> | IDU | ------> | EXU | ------> | WBU | +-----+ +-----+ +-----+ +-----+
其中集中式控制:控制器需要收集所有模块状态,并决定如何控制各模块的工作。
- 可扩展性比较低,随着模块数量增加,控制器越来越难设计
分布式控制:各模块的行为仅仅取决于自身状态和下游模块状态,也就是: - 各模块可以独立工作,直到下游无法接收消息
- 容易插入新模块,只需修改上下游模块的接口实现。
所以乱序执行天生就是分布式控制的。
最简单的系统总线就是连接处理器和存储器以及设备之间的总线,其中读是最基本的需求。
而npc中提供的接口pm_read在真实的处理器中是不可能实现的。
对于可读可写的系统总线来说:
+-----+ raddr[log2(N)-1:0] ---> +-----+ | | <--- rdata[31:0] | | | | waddr[log2(N)-1:0] ---> | | | CPU | wdata[31:0] ---> | MEM | | | wen ---> | | | | wmask[3:0] ---> | | +-----+ +-----+
我们需要添加新信号:
- 写地址 waddr,写数据wdata
- 并非每个周期都要写,因此需要写使能wen
允许只写入部分的字节,所以需要写掩码wmask,例如lb lh lw
若同时读写同一地址,读出结果可能会undefine(需要RTFM)
而常用的存储器延迟更大, 所以我们有了新的需求:
- slave需要识别master何时发送有效请求
- master也需要识别slave何时可以接收请求
这就需要了握手信号。
- 握手 = 双方对请求的发送和接收达成共识。 而且不会遗漏或重复
异步的系统总线:
+-----+ raddr[log2(N)-1:0] ---> +-----+ | | rvalid ---> | | | | <--- rready | | | | <--- rdata[31:0] | | | CPU | waddr[log2(N)-1:0] ---> | MEM | | | wdata[31:0] ---> | | | | wen ---> | | | | wmask[3:0] ---> | | +-----+ +-----+
上图就是CPU和Memory之间的交互,其中CPU可读的地址有raddr位宽个,rvalid为1时代表读有效 ,等待rready,实现读请求raddr的握手。
但是此刻又有新问题了
- 例如slave读出rdata的时刻无法提前确定
- 例如DRAM会定时对存储单元的电容进行充电刷新,此时需要等待
- master也不一定总接收slave读出的数据。(例如上一次读出的数据还没用完,取决于状态机的状态)
而握手的意义就是解耦
通信的一方无法得知另一方处于什么状态,因此也无法的值另一方的处理延迟方法。
但是只要有了握手信号,双方均无需关心上述细节,只要等待握手即可,也就是只要模块遵循同一套通信协议,即可替换/接入,各模块皆可顺利工作。
同时我们也需要有错误处理:
+-----+ araddr[log2(N)-1:0] ---> +-----+ | | arvalid ---> | | | | <--- arready | | | | <--- rdata[31:0] | | | | <--- rresp[1:0] | | | | <--- rvalid | | | | rready ---> | | | CPU | waddr[log2(N)-1:0] ---> | MEM | | | wdata[31:0] ---> | | | | wmask[3:0] ---> | | | | wvalid ---> | | | | <--- wready | | | | <--- bresp[1:0] | | | | <--- bvalid | | +-----+ bready ---> +-----+
读写请求可能会出错,例如超过存储区域的边界,通过resp和bresp(b表示backward)向master回复读写操作是否成功)
优先判断resp是否符合预期值,如果符合的话rdata才有效。
若失败,CPU可抛出异常,通过软件处理。
得到手册的AXI-Lite总线规范
1、将写地址和写数据分开,写地址通过单独握手
2、分组,并将wmask改名为wstrb
araddr ---> araddr ---> araddr ---> -+ arvalid ---> arvalid ---> arvalid ---> AR <--- arready <--- arready <--- arready -+ <--- rdata <--- rdata <--- rresp <--- rresp <--- rdata -+ <--- rvalid <--- rvalid <--- rresp | rready ---> 1 rready ---> 2 <--- rvalid R waddr ---> ===> awaddr ---> ===> rready ---> -+ wdata ---> awvalid ---> * wmask ---> <--- awready * awaddr ---> -+ wvalid ---> wdata ---> awvalid ---> AW <--- wready wmask ---> <--- awready -+ <--- bresp wvalid ---> <--- bvalid <--- wready wdata ---> -+ bready ---> <--- bresp wstrb ---> | <--- bvalid wvalid ---> W bready ---> <--- wready -+ <--- bresp -+ <--- bvalid B bready ---> -+
对于读地址,分为AR的三个通道。也就是araddr arvalid 和arready
读数据的话分为R 也就是rdata rresp rvalid rready
写地址 awaddr awvalid awready
写数据 wdata wstrb wvalid wready
如果想使用总线,那么我们需要把NPC升级为多周期的处理器。
如果想要获得更高的主频,还需要在多模块之间添加暂存信号。
避免两种情况,
例如系统死锁(OS!!),master和slave都在等待对方先将握手信号置为1。
- master:我等slave将ready置为1后,再将valid置为1
- slave:我等master将valid置为1后,在将ready置为1
相互等待直接G!
活锁(OS!!!)
局部看没卡死,全局看没进展
master和slave都在试探性的握手,但试探失败后都取消握手。
而B就是back bresp 写回复,看写没写成功,bvalid同理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界