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=0x12
b
周期:此时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_o
we_o
data_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_i
STB_O
:高表示 master 要发送请求,即自研总线中的valid_o
ACK_I
:高表示 slave 完成请求,即自研总线中的ready_i
ADR_O
:master 想要读写的地址,即自研总线中的addr_o
WE_O
:master 想要读还是写,即自研总线中的we_o
DAT_O
:master 想要写入的数据,即自研总线中的data_o
SEL_O
:master 读写的字节使能,即自研总线中的be_o
DAT_I
:master 从 slave 读取的数据,即自研总线中的data_i
CYC_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,并且去掉了其他的可选信号。其他的版本,主要是为了优化性能,对教学来说是不必要的。