Wishbone 总线协议
本章介绍 Wishbone 总线协议,这个协议是实验中很重要的一环,穿插了多个实验。
简介
本文从为什么需要总线协议讲起,再从设计者的角度,谈谈如何设计一个总线协议,最后发现Wishbone 协议就是刚刚自己设计的总线协议。
为什么需要总线协议
在我们日常使用的电脑里,有各种各样的部件,例如键盘,鼠标,显示器,无线网卡等等,它们在操作系统里都是如何识别和管理的?同学们可能听说过一些概念,包括 USB、PCIe 等等,它们的用途就是给 CPU 一个通用的接口,例如:
- USB 总线:把键盘、鼠标、移动硬盘等等插到电脑上的 USB 接口,操作系统就可以自动地检测到新的硬件,并且用同样的方法来操控这些 USB 设备
- PCIe 总线:台式机里的独立显卡,通常就是插在主板上的 PCIe 卡槽,插上去以后,操作系统就会自动识别并运行显卡驱动
由此,我们总结一下,总线的功能就是:
- 提供一个统一的硬件接口,可以接入不同的硬件外设
- 提供一个统一的软件接口,操作系统可以用同样的方式,来操作这个总线下的所有外设
上面的 USB 总线和 PCIe 总线,都是属于 CPU 片外的总线,即同学们可以在主板上看到的。今天,我们要实现的是 CPU 片内的总线,它的目的则是给 CPU 核心一个统一的接口,来访问内存或者外设。
总线协议是什么
我们编写的代码和数据都存放在内存中,那么同学们有没有想过,CPU 是如何读写内存的?我们来回想一下 C 代码中的读和写:
int *ptr;
int read_data = *ptr; // read
*ptr = write_data; // write
对应到汇编指令,如果说 ptr 保存在寄存器 a0 中,那么上面的代码对应下面的 RISC-V 汇编:
# ptr: a0
# read_data: a1
# write_data: a2
# int read_data = *ptr;
lw a1, 0(a0)
# *ptr = write_data;
sw a2, 0(a0)
我们可以想象,执行 lw a1, 0(a0) 的时候,CPU 向内存发送地址 a0,并且进行一个读操作,内存读取完以后,内存向 CPU 返回地址为 a0 的数据;执行 sw a2, 0(a0) 的时候,CPU 向内存发送地址 a0 和数据 a2,并且进行一个写操作,内存写入以后,内存向 CPU 返回写入完成的信息。
总结一下上面的操作,从 CPU 到内存需要传输的信息:
- 地址(
addr):例如 32 位处理器就是 32 位地址,或者按照内存的大小计算地址线的宽度,例如 4GB 的内存是2^32个字节,就需要 32 位的地址 - 写入的数据(
w_data) - 读还是写(
we, write enable):高表示写,低表示读
从内存到 CPU 需要传输的信息:
- 读/写操作完成
- 读取的数据(
r_data)
接下来,我们就要自己设计一个让 CPU 访问内存的总线协议。
如何设计一个总线协议
当 CPU 不访问内存的时候,可以让内存休息,减少能耗,因此我们需要设计一个控制信号,即 CPU 告诉内存,什么时候需要进行一次读或者写的操作,我们称之为 valid:高表示 CPU 请求一次读写操作,低表示不请求。
我们已经学过,内存的访问相对 CPU 来说是很慢的,那么就需要一个机制,让 CPU 去等待内存的访问过程。我们上面已经提到,当 CPU 要进行读或者写的操作的时候,会设置 valid=1,此时内存进行实际的内存操作,等待一段时间后,需要通知 CPU 操作已经完成,同时把结果返回给 CPU。于是,我们添加一个信号 ready:高表示内存完成一次读写操作,低表示还没完成或者 CPU 没有请求。当内存完成读写的时候,设置 ready=1,就标志着一次读写操作的完成。
我们总结一下,CPU 进行一次读写操作,需要经历的过程:
- CPU 设置
valid=1,内存开始读写操作 - 内存完成操作以后,设置
ready=1,表示操作已经完成 - CPU 看到内存设置
ready=1时,就知道操作已经完成,设置valid=0 - CPU 下一次进行读写操作的时候,再从第一步开始
实际上,这种操作方式也可以用于 CPU 访问外设,因此下面我们用 master 表示 CPU 端,也就是发起请求的一端;用 slave 表示设备端,包括内存、外设等,也就是处理请求的一端。
接下来,我们回到硬件,综合以上的分析,可以得到 master 端的信号,我们约定 _o 表示输出,_i 表示输入:
clock_i:时钟输入valid_o:高表示 master 想要发送请求ready_i:高表示 slave 完成处理请求addr_o:master 想要读写的地址we_o:master 想要读还是写data_o:master 想要写入的数据be_o:master 读写的字节使能,用于实现单字节写等data_i:slave 提供给 master 的读取的数据
字节使能
通常情况下,为了性能考虑,总线上一个周期会传输多个字节,例如 data_o 是 32 位宽的话,一个周期就可以写入四个字节的数据,执行一条 sw 指令。但同时又引入了新的问题,假如只想写入一个字节怎么办,例如 sb 指令?答案是添加信号 be_o,如果数据是 32 位宽的话,那么字节使能 be_o 就是 4 位,每一位表示对应的字节是否需要写入。
例如向地址 0x00 写入一个字节的话,字节使能 be_o=0001;向地址 0x02 写入两个字节的话,字节使能 be_o=1100。
这样,我们就自己研制了一个总线协议。根据我们设计的自研总线,可以绘制出下面的波形图(以 master 的信号为例):
a周期:此时valid_o=1 && ready_i=1说明有请求发生,此时we_o=1说明是一个写操作,并且写入地址是addr_o=0x01,写入的数据是data_o=0x12b周期:此时valid_o=0 && ready_i=0说明无事发生c周期:此时valid_o=1 && ready_i=0说明 master 想要从地址 0x02(addr_o=0x02)读取数据(we_o=0),但是 slave 没有完成(ready_i=0)d周期:此时valid_o=1 && ready_i=1说明有请求发生,master 从地址 0x02(addr_o=0x02)读取数据(we_o=0),读取的数据为 0x34(data_i=0x34)e周期:此时valid_o=0 && ready_i=0说明无事发生f周期:此时valid_o=1 && ready_i=1说明有请求发生,master 向地址 0x03(addr_o=0x03)写入数据(we_o=1),写入的数据为 0x56(data_i=0x56)g周期:此时valid_o=1 && ready_i=1说明有请求发生,master 从地址 0x01(addr_o=0x01)读取数据(we_o=0),读取的数据为 0x12(data_i=0x12)h周期:此时valid_o=1 && ready_i=1说明有请求发生,master 向地址 0x02(addr_o=0x02)写入数据(we_o=1),写入的数据为 0x9a(data_i=0x9a)
从上面的波形中,可以有几点观察:
- master 想要发起请求的时候,就设置
valid_o=1;当 slave 可以完成请求的时候,就设置ready_i=1;在valid_o=1 && ready_i=1时请求完成,可以进行下一个请求 - 如果 master 发起请求,同时 slave 不能接收请求,即
valid_o=1 && ready_i=0,此时 master 要保持addr_owe_odata_o和be_o不变,直到请求结束 - 当 master 不发起请求的时候,即
valid_o=0,此时总线上的信号都视为无效数据,不应该进行处理;对于读操作,只有在valid_o=1 && ready_i=1时data_i上的数据是有效的 - 可以连续多个周期发生请求,即
valid_o=1 && ready_i=1连续多个周期等于一,此时是理想情况,可以达到总线最高的传输速度
Wishbone 总线协议
接下来我们来看一个实践中很常用的总线协议:Wishbone,其实它和我们在上面自研的总线十分类似,让我们来看看它的信号,以 master 端为例:
CLK_I: 时钟输入,即自研总线中的clock_iSTB_O:高表示 master 要发送请求,即自研总线中的valid_oACK_I:高表示 slave 完成请求,即自研总线中的ready_iADR_O:master 想要读写的地址,即自研总线中的addr_oWE_O:master 想要读还是写,即自研总线中的we_oDAT_O:master 想要写入的数据,即自研总线中的data_oSEL_O:master 读写的字节使能,即自研总线中的be_oDAT_I:master 从 slave 读取的数据,即自研总线中的data_iCYC_O:总线的使能信号,无对应的自研总线信号
可以看到,除了最后一个 CYC_O,其他的信号其实就是我们刚刚设计的自研总线,其定义和用途完全一致。CYC_O 的可以认为是 master 想要占用 slave 的总线接口,在常见的使用场景下,直接认为 CYC_O=STB_O。它的用途是:
- 占用 slave 的总线接口,不允许其他 master 访问
- 简化 interconnect 的实现
把上面自研总线的波形图改成 Wishbone,就可以得到:
在课程中,建议 Wishbone 协议每次请求结束时,master 都要拉低 CYC_O 和 STB_O,因此就不能像上面的 f-g-h 那样连续三个周期发生请求。这样做的好处是:
- slave 实现简单,例如状态机中拉高
ACK后回到 IDLE 状态即可,一些简单的 slave 也会默认 master 会在每个请求结束后拉低CYC_O和STB_O; - 防止一个 master 占用总线太长时间,使得其他 master“饿死”;
- 从波形图上看,每个请求都明确地区分开来,方便人来阅读。
最后得到如下的波形:
Wishbone 总线规范
一个规范的 Wishbone Master,需要保证:
- 不能打断正在进行的请求:如果上一个周期
CYC_O=1 && STB_O=1 && ACK_I=0,那么这个周期必须要维持CYC_O=1 && STB_O=1; - 不能修改正在进行的请求:如果上一个周期
CYC_O=1 && STB_O=1 && ACK_I=0,那么这个周期的ADR_O, WE_O, DAT_O, SEL_O应该和上一个周期相同; - 仅在
CYC_O=1 && STB_O=1 && ACK_I=1时,Wishbone 总线上 Slave 提供的DAT_I信号是有效的,其他时候的取值,不应该影响 Wishbone Master 的行为。
一个规范的 Wishbone Slave,需要保证:
- 仅在
CYC_I=1 && STB_I=1时,Wishbone 总线上 Master 提供的ADR_I, WE_I, DAT_I, SEL_I信号是有效的,其他时候的取值,不应该影响 Wishbone Slave 的行为。
我在网上查找的 Wishbone 协议还有其他的信号?
在实验中,我们采用的是 Wishbone 的基础版本 Wishbone Classic Standard,并且去掉了其他的可选信号。其他的版本,主要是为了优化性能,对教学来说是不必要的。