上一篇博文中注释了通用寄存器reg.v模块,现在来介绍总线模块rib.v:
另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。
(这个RIB总线协议为工程原作者自定义的总线协议)
目录
0 RISC-V SoC注解系列文章目录
1. rib.v功能
设想一下一个没有总线的SoC,处理器核与外设之间的连接是怎样的。可能会如下图所示:
可见,处理器核core直接与每个外设进行交互。假设一个外设有一条地址总线和一条数据总线,总共有N个外设,那么处理器核就有N条地址总线和N条数据总线,而且每增加一个外设就要修改(改动还不小)core的代码。有了总线之后,处理器核只需要一条地址总线和一条数据总线,大大简化了处理器核与外设之间的连接。
目前已经有不少成熟、标准的总线,比如AMBA、wishbone、AXI等。设计CPU时大可以直接使用其中某一种,以节省开发时间。但是为了追求简单,tinyriscv并没有使用这些总线,而是自主设计了一种名为RIB(RISC-V Internal Bus)的总线。RIB总线支持多主多从连接,但是同一时刻只支持一主一从通信。RIB总线上的各个主设备之间采用固定优先级仲裁机制。
2. rib总线接口注解
input wire clk,
input wire rst,
// master 0 interface
input wire[`MemAddrBus] m0_addr_i, // 主设备0读、写地址
input wire[`MemBus] m0_data_i, // 主设备0写数据
output reg[`MemBus] m0_data_o, // 主设备0读取到的数据
input wire m0_req_i, // 主设备0访问请求标志
input wire m0_we_i, // 主设备0写标志
// master 1 interface
input wire[`MemAddrBus] m1_addr_i, // 主设备1读、写地址
input wire[`MemBus] m1_data_i, // 主设备1写数据
output reg[`MemBus] m1_data_o, // 主设备1读取到的数据
input wire m1_req_i, // 主设备1访问请求标志
input wire m1_we_i, // 主设备1写标志
...
// slave 0 interface
output reg[`MemAddrBus] s0_addr_o, // 从设备0读、写地址
output reg[`MemBus] s0_data_o, // 从设备0写数据
input wire[`MemBus] s0_data_i, // 从设备0读取到的数据
output reg s0_we_o, // 从设备0写标志
// slave 1 interface
output reg[`MemAddrBus] s1_addr_o, // 从设备1读、写地址
output reg[`MemBus] s1_data_o, // 从设备1写数据
input wire[`MemBus] s1_data_i, // 从设备1读取到的数据
output reg s1_we_o, // 从设备1写标志
...
output reg hold_flag_o // 暂停流水线标志
主设备的优先级仲裁和主设备选择访问从设备各对应一个always(*)组合逻辑模块:
3. 总线工作机制
3.1. 总线仲裁机制
首先,由各主机向总线发送访问请求req:
// 主设备请求信号
//m0_req_i:来自执行模块ex.v的访问请求信号
//m1_req_i:pc_reg发来的读指令地址请求,一直为1,保持请求状态
//m2_req_i:jtag的操作请求
//m3_req_i:串口下载模块的请求信号
assign req = {m3_req_i, m2_req_i, m1_req_i, m0_req_i};
然后由总线的仲裁机制对主机的访问请求进行仲裁,如下代码所示:
// 仲裁逻辑
// 固定优先级仲裁机制
// 优先级由高到低:主设备3,主设备0,主设备2,主设备1
always @ (*) begin
if (req[3]) begin //m3_req_i:串口下载模块的请求信号
grant = grant3;
hold_flag_o = `HoldEnable; //只需要暂停pc寄存器
end else if (req[0]) begin //m0_req_i:来自执行模块ex.v的访问请求信号
grant = grant0;
hold_flag_o = `HoldEnable;
end else if (req[2]) begin //m2_req_i:jtag的操作请求
grant = grant2;
hold_flag_o = `HoldEnable;
end else begin //m1_req_i:pc_reg发来的读指令地址请求,一直为1,保持请求状态
grant = grant1;
hold_flag_o = `HoldDisable;
end
end
主设备的优先级仲裁always(*)组合逻辑模块:
通过if_else根据优先级的顺序选择主设备进行相应的访问操作,对于主设备的仲裁,主设备优先级顺序为:uart串口下载、ex.v执行模块、jtag模块、pc_reg取指模块。
对这个优先级顺序的理解:
- uart程序下载,既然要更新程序,程序执行到哪一步都无所谓,不需要考虑其他模块请求,直接下载,重新运行新的程序;
- ex.v执行模块(内存读写请求),除非重新下载新的程序代码,在程序不变的情况下,需要保证当前指令完整运行,才能保障后续操作不会出错;
- jtag模块,上一步指令运行完毕,jtag调试模块即可修改调试 参数,控制程序执行,包括取值,因为调试过程中,设置断点可能就会暂停取值操作了;
- pc_reg取指模块是以上所有主设备模块的基础第一步,为以上“主设备”服务。
(前三个“主设备uart、ex.v、jtag”访问总线,需要暂停流水线)
3.2. 主设备选择访问从设备
主设备选择访问从设备always(*)组合逻辑模块:
grant0: begin //主设备0
case (m0_addr_i[31:28])
slave_0: begin
s0_we_o = m0_we_i; //主设备0的写使能信号——>从设备
s0_addr_o = {{4'h0}, {m0_addr_i[27:0]}};//从设备0的地址
s0_data_o = m0_data_i; //主设备0数据写入——>从设备0
m0_data_o = s0_data_i; //从设备0数据写入——>主设备0
end
slave_1: begin
s1_we_o = m0_we_i;
s1_addr_o = {{4'h0}, {m0_addr_i[27:0]}};
s1_data_o = m0_data_i;
m0_data_o = s1_data_i;
end
...
- 通过case语句选择相应需要操作的从设备,然后将主设备的写使能信号m0_we_i传递给s0_we_o要写的从设备;
- 将从设备的低位地址也传送出来以便对从设备的相应寄存器进行操作;s0_addr_o = {{4'h0}, {m0_addr_i[27:0]}};
- 将主设备要写的数据传送至从设备s0_data_o = m0_data_i;
- 将从设备传出的数据传送至主设备m0_data_o = s0_data_i。
4. 对RIB总线的理解
- RIB总线支持多主多从连接,但是同一时刻只支持一主一从通信。RIB总线上的各个主设备之间采用固定优先级仲裁机制。
- RIB总线本质上是一个多路选择器,从多个主设备中选择其中一个来访问对应的从设备。
- RIB总线地址的最高4位决定要访问的是哪一个从设备,因此最多支持16个从设备。
以一个主从和RIB之间的接口为例: