本文介绍的是rocket-chip l2_frontend_bus的作用。
在介绍l2_frontend_bus前,需要有的基础知识:AHB-Lite、AHB和AXI4协议的总线基础。
具体的协议文档可以上ARM官网下载。在l2_frontend_bus功能说明中,不会对AXI4、AHB和TileLink协议进行讲解,默认大家都已经知道这些协议的核心内容。
rocket-chip在总线上可以生成三种协议,分别是AHB、AXI4和TileLink。
默认采用的是AXI4,AHB和TileLink的底层scala代码是有的,但需要自行修改连接关系,从而实现AHB和TileLink总线协议。
为了减少麻烦和避免一些不稳定的因素,所以我会直接采用默认的AXI4作为例子进行说明。
前提条件:
- memory和mmio通过地址来区分端口,我分配0x8000_0000-0x9000_0000为memory,0x6000_0000-0x7000_0000为而mmio,这些地址可以在生成rtl的时候修改。
- l2_frontend_bus没有地址。
- memory和mmio端口连接的是rocket-chip和memory、rocket-chip和外设、rocket-chip和registers,这里rocket-chip永远是master,而memory、外设和registers都系slave。
- l2_frontend_bus连接的是其他核和rocket-chip,dma和rocket-chip,这里rocket-chip永远是slave,其他核和dma是master。
- 在没有dcache flush和dcache invalid的功能前,l2_frontend_bus是保持系统数据一致性的总线接口。
关于3)、4)和5)点可能不太容易懂,在这里详细说明。
早期的rocket-chip是没有指令实现dcache flush和dcache invalid的功能,那时l2_frontend_bus是保持系统数据一致性的唯一方法。
系统数据一致性的问题涉及系统架构的内容。我在这里举两个个简单的例子做说明。
例子1前提条件:
- 假设系统中存在rocket-chip、dma,uart和memory。
- 假设存在指令实现dcache flush和dcache invalid的功能。
在上述两个条件下,想利用uart进行数据发送和接收,那么就要按以下步骤完成操作:
发送:
- rocket-chip先将串口发送的数据从memory中读入,在cpu中进行处理,此时数据已经在dcache中。
- 使用dcache flush指令,将dcache中处理过的串口数据刷回至memory中。
- 配置dma(搬运数据的地址和数据的长度)和uart寄存器(发送数据的长度和格式等)。
- 启动dma将memory中处理过的串口数据搬到uart模块中,并将数据通过uart模块发送出去。
- 完成发送后,uart产生中断信号告知rocket-chip,然后再继续后续的程序。
接收:
- 配置dma(搬运数据的地址和数据的长度)和uart寄存器(接收数据的长度和格式等)。
- 启动uart和dma,uart将收到的串口数据通过dma搬到memory中。
- 因为rocket-chip之前可能读过相关memory的值(存串口接收数据的相关地址),且没有flush掉,如果读过,那么cpu会认为在dcache中的数据是最新的,即使uart收到新的数据,也不会从memory中读取新的数据,还是会一直沿用dcache中已存的数据,因此数据的一致性就存在问题。
- 使用dcache invalid指令,将dcache中已存的旧数据无效掉。
- 根据dma产生的中断判断新的数据是否已经完成接收。
- 完成接收后,rocket-chip利用总线向memory读取最新的串口接收数据,如果没有dcache invalid的操作,rocket-chip是不会发起此次读操作的。
上述就是存在dcache flush和dcache invalid指令时,外设发送和接收数据保持数据一致性的大致过程。
例子2前提条件:
- 设系统中存在rocket-chip、dma,uart和memory。
- 假设没有指令实现dcache flush和dcache invalid的功能,只有l2_frontend_bus接口。
在上述两个条件下,想利用uart进行数据发送和接收,那么就要按以下步骤完成操作:
发送:
- rocket-chip先将串口发送的数据从memory中读入,在cpu中进行处理,此时数据已经在dcache中。
- 配置dma(搬运数据的地址和数据的长度)和uart寄存器(发送数据的长度和格式等)。
- 启动dma,dma从l2_frontend_bus接口中发起数据读请求,数据请求在rocket-chip内部完成仲裁后,如果请求的数据存在于dcache中,那么会将数据刷回memory,然后再将处理过的数据从memory中再次读取并交给l2_frontend_bus,最后再移交给dma;如果请求的数据不存在与dcache中,那么会从memory中直接读取数据,并交给l2_frontend_bus接口,最后移交给dma。
- dma将处理过的串口数据搬到uart模块中,并将数据通过uart模块发送出去。
- 完成发送后,uart产生中断信号告知rocket-chip,然后再继续后续的程序。
接收:
- 配置dma(搬运数据的地址和数据的长度)和uart寄存器(接收数据的长度和格式等)。
- 启动uart和dma,uart将收到的串口数据通过dma搬到memory中。
- dma从l2_frontend_bus接口中发起数据写请求,数据写请求在rocket-chip内部完成仲裁后,如果有旧的数据存在于dcache中,那么会将旧的数据刷回memory,然后再将l2_frontend_bus新的数据写入到memory中;如果没有旧的数据存在与dcache中,那么会将l2_frontend_bus新的数据写入到memory中。
- 根据dma产生的中断判断新的数据是否已经完成写入。
- 完成接收后,rocket-chip利用总线向memory读取最新的串口接收数据。
上述就是利用l2_frontend_bus接口,外设发送和接收数据保持数据一致性的大致过程。
因此dcache flush指令、dcache invalid指令和l2_frontend_bus是保持系统数据一致性的方法。
测试代码如下:
#include "encoding.h"
#include "L1Dcache.h"
#define U32 *(volatile unsigned int *)
#define DEBUG_SIG 0x70000000
#define DEBUG_VAL 0x70000004
//--------------------------------------------------------------------------
// handle_trap function
void handle_trap()
{
asm volatile ("nop");
while(1);
}
//--------------------------------------------------------------------------
// Main
void main()
{
unsigned int i;
for(i=0;i<10;i++)
{
U32(0x60001000+4*i) = i+0x1234;
U32(0x80001000+4*i) = i+0x10086;
}
for(i=0;i<10;i++)
{
U32(0x80002000+4*i) = U32(0x80001000+4*i);
U32(0x60002000+4*i) = U32(0x60001000+4*i);
}
for(i=0;i<10;i++)
{
U32(0x80002000+4*i) = U32(0x60001000+4*i);
U32(0x60002000+4*i) = U32(0x80001000+4*i);
}
while(1) {asm volatile ("wfi");}
}
测试代码功能:
- 往0x60001000-0x60001040地址灌初始数据。
- 往0x80001000-0x80001040地址灌初始数据。
- 读0x80001000-0x80001040地址的值写入到0x80002000-0x80002040地址中,memory->memory。
- 读0x60001000-0x60001040地址的值写入到0x60002000-0x60002040地址中,mmio->mmio。
- 读0x60001000-0x60001040地址的值写入到0x80002000-0x80002040地址中,mmio->memory。
- 读0x80001000-0x80001040地址的值写入到0x60002000-0x60002040地址中,memory->mmio。
- 进行死循环,testbench中进行l2_frontend_bus->mmio和l2_frontend_bus->memory的操作。
下面进一步讨论rocket-chip三种接口的具体作用。
- 为了方便分析,所以增加了具体的对应编号。
- 源端是指数据的来源,可读可写;目标端是数据的去向,也是可读可写,但属性和源端相反,例如从0x8000_0000读数据,然后写入0x8000_1000中,那么源端就是memory,属性为read,目标端就是memory,属性为write,其他端口类似。
- memory port是以cache line的长度进行数据读写的;mmio是以单word的长度进行数据读写的。
- C、F和I是不能实现的,这里只是将它列出来,因为l2_frontend_bus永远不会成为目标端,所以在当目标端为l2_frontend_bus时,全部功能都是×的,这里上面已经说明,l2_frontend_bus另一端是作为master的,rocket-chip是作为slave的。
- 带有(*)的地方,非常特别,虽然写的方式是burst,但改变的数据却是某个具体地址的word,下面会用波形图进行说明。
- l2_frontend_bus能操作rocket-chip所有带物理地址的registers和memory,也就间接可以控制memory port和mmio port,同时PLIC的registers也能控制。
- E、G和H会在后面重点分析。
rocket-chip总线接口说明如下表:
编号 | 源端 | Read Burst | Read Single | Write Burst | Write Single | 目标端 | Read Burst | Read Single | Write Burst | Write Single |
---|---|---|---|---|---|---|---|---|---|---|
A | memory | √ | × | √ | × | memory | √ | × | √ | × |
B | memory | √ | × | √ | × | mmio | × | √ | × | √ |
C | memory | √ | × | √ | × | l2_frontend_bus | × | × | × | × |
D | mmio | × | √ | × | √ | mmio | × | √ | × | √ |
E | mmio | × | √ | × | √ | memory | √ | × | √(*) | × |
F | mmio | × | √ | × | √ | l2_frontend_bus | × | × | × | × |
G | l2_frontend_bus | √ | √ | √ | √ | mmio | √ | √ | √ | √ |
H | l2_frontend_bus | √ | √ | √ | √ | memory | × | √ | × | √ |
I | l2_frontend_bus | × | × | × | × | l2_frontend_bus | × | × | × | × |
下面对上表作一个简单分析。
- 由ABDE可知,如果memroy和mmio是由rocket-chip自由去控制的(非l2_frontend_bus控制),那么memory的读写都是按burst来进行的,mmio的读写都是按single来进行的。
- E中memory的写是按burst进行,但修改的数据则按具体地址进行修改。如下图。由于mmio的单次操作和memory的单次操作间隔太长了,所以不好截图,只能截取上面的这图。上图中,黄色是mmio的单次操作,是ar操作,读取0x60001004的值,为0x1235。红色为memory的单次操作,是aw操作,写到0x80002004的位置,红色箭头是为了显示数据更改的过程。从上图可以看到,mmio都是以单word长度进行读写的,而memory是以cache line长度进行读写的,所以memory中写的数据是一连串的,它们的地址分别是0x80002000,0x80002004,0x80002008……。因为此次单次操作是读0x60001004的值,写到0x80002004中,所以只有0x80002004地址的数据被修改了,因此写的一连串数据中只有第二个被修改,第一个是之前写0x80002000时被修改的,第三个以后的还没有被修改,所以保留之前的数值,可以看到第三个数据原值为0x10088。
- 通过l2_frontend_bus控制mmio接口时,mmio是支持burst和single操作的,和rocket-chip控制时不一样。波形如下图。
黄色箭头:l2_frontend_bus single write -> mmio。
橙色箭头:l2_frontend_bus burst write -> mmio。
蓝色箭头:l2_frontend_bus single read -> mmio。
红色箭头:l2_frontend_bus burst read -> mmio。
白色箭头:burst和single操作的切换信号。
进行的是先写后读的操作,写和读的地址是一一对应的,所以数据的一致性能证明读写操作是否正确。写的是一个随机数,比对读出后的结果是正确的。但需要注意的是,虽然执行的是burst操作,但并不是完全连续的burst操作,是分开了几拍进行的,由l2_frontend_bus_axi4_0_w_ready和l2_frontedn_bus_axi4_0_r_valid可以看到,这是由TileLink2AXI4转换代码决定的。 - 通过l2_frontend_bus控制memory接口时,memory只支持single操作的,和rocket-chip控制时不一样。波形如下图。这个结果和我的预期也不同,我预期的是同时支持burst和single操作。
红色箭头:l2_frontend_bus single write -> memory。
黄色箭头:l2_frontend_bus burst write -> memory。
蓝色箭头:l2_frontend_bus single read -> memory。
紫色箭头:l2_frontend_bus burst read -> memory。
和第3点一样,通过数据的一致性来证明读写操作是否正确。同理,通过l2_frontend_bus_axi4_0_w_ready和l2_frontedn_bus_axi4_0_r_valid可以看到,memory port那端每次都是进行single操作的,即使l2_frontend_bus这端采用的是burst操作。
关于rocket-chip三种接口的一些结论:
- rocket-chip使用memory端口是burst操作的,以cache line长度为单位,建议连接memory(rom/sram)。
- rocket-chip使用mmio端口是single操作的,以word为单位,建议连接外设/寄存器。
- l2_frontend_bus能操作rocket-chip所有带物理地址的空间,包括全部registers和memory,也就间接可以控制memory port和mmio port,还有cpu内部的registers,建议连接保持数据一致性的模块/debug模块(因为可以访问全物理地址,所以debug功能是很好的选择)。
- l2_frontend_bus的作用是保持系统的数据一致性,功能类似于dcache flush指令和dcache invalid指令。使用情况需要根据系统的总架构而定。
- l2_frontend_bus的效率性能需要额外做实验来确定,这是另一个需要研究的方向。
- l2_frontend_bus的word/half word/byte的性能分析需要额外做实验来确定,这是另一个需要研究的方向。
最后附上部分testbench的代码,需要说明的是,此部分代码只为了证明l2_frontend_bus的功能,是不全面的AXI4协议,并不能作为完整的AXI4接口代码来使用。同时因为没有进行过完全充分的验证,所以不排除有bug的存在。
/**************************/
/* Front Port */
/**************************/
reg mult;
reg aw_valid;
reg [31:0] aw_addr;
reg w_valid;
reg [63:0] w_data;
reg w_last;
reg ar_valid;
reg [31:0] ar_addr;
reg l2bus_start = 1'b0;
reg [10:0] cnt;
initial #(7000) l2bus_start = 1'b1;
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
cnt <= 11'd0;
end else if(cnt == 11'h6FF) begin
cnt <= 11'd0;
$finish(2);
end else if (l2bus_start &&
(l2_frontend_bus_axi4_0_aw_ready == 1'b1 ||
l2_frontend_bus_axi4_0_w_ready == 1'b1)
) begin
cnt <= cnt + 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
mult <= 1'b0;
end else if(cnt[7] == 1'b0 && cnt[10] != 1'b1) begin
mult <= 1'b0;
end else if(cnt[7] == 1'b1 && cnt[10] != 1'b1) begin
mult <= 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
aw_valid <= 1'b0;
end else if(aw_valid == 1'b1) begin
aw_valid <= 1'b0;
end else if(cnt[4:0] == 5'b10000 &&
cnt[8] == 1'b0 &&
cnt[10] != 1'b1 &&
mult == 1'b0 &&
l2_frontend_bus_axi4_0_aw_ready == 1'b1) begin
aw_valid <= 1'b1;
end else if(cnt[4:0] == 5'b01111 &&
cnt[8] == 1'b0 &&
cnt[10] != 1'b1 &&
mult == 1'b1 &&
l2_frontend_bus_axi4_0_aw_ready == 1'b1) begin
aw_valid <= 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
w_valid <= 1'b0;
end else if(w_valid == 1'b1 && mult == 1'b1 && cnt[3:0] == 4'b1111 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_valid <= 1'b0;
end else if(w_valid == 1'b1 && mult == 1'b0) begin
w_valid <= 1'b0;
end else if(cnt[4:0] == 5'b10000 &&
cnt[8] == 1'b0 &&
cnt[10] != 1'b1 &&
mult == 1'b0 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_valid <= 1'b1;
end else if(cnt[4:0] == 5'b01111 &&
cnt[8] == 1'b0 &&
cnt[10] != 1'b1 &&
mult == 1'b1 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_valid <= 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
aw_addr <= 32'h60000000;
end else if(cnt[9:0] == 10'h200) begin
aw_addr <= 32'h80000000;
end else if(aw_valid && mult == 1'b0) begin
aw_addr <= aw_addr + 3'h4;
end else if(aw_valid && mult == 1'b1) begin
aw_addr <= aw_addr + 8'h40;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
w_data <= 64'h0;
end else if(cnt[8:7] == 2'b00 &&
cnt[4:0] == 5'b10000 &&
cnt[10] != 1'b1 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_data <= {2{$random()}};
end else if(cnt[8:7] == 2'b01 &&
cnt[4] == 1'b1 &&
cnt[10] != 1'b1 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_data <= {2{$random()}};
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
w_last <= 1'b0;
end else if(w_last == 1'b1 && mult == 1'b1 && cnt[3:0] == 4'b1111 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_last <= 1'b0;
end else if(w_last == 1'b1 && mult == 1'b0) begin
w_last <= 1'b0;
end else if(cnt[3:0] == 4'b1110 &&
cnt[8:7] == 2'b01 &&
cnt[10] != 1'b1 &&
w_valid == 1'b1 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_last <= 1'b1;
end else if(cnt[4:0] == 5'b10000 &&
cnt[8:7] == 2'b00 &&
cnt[10] != 1'b1 &&
l2_frontend_bus_axi4_0_w_ready == 1'b1) begin
w_last <= 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
ar_valid <= 1'b0;
end else if(ar_valid == 1'b1) begin
ar_valid <= 1'b0;
end else if(cnt[4:0] == 5'b10000 &&
cnt[8] == 1'b1 &&
mult == 1'b0 &&
l2_frontend_bus_axi4_0_ar_ready == 1'b1) begin
ar_valid <= 1'b1;
end else if(cnt[5:0] == 6'b100000 &&
cnt[8] == 1'b1 &&
mult == 1'b1 &&
l2_frontend_bus_axi4_0_ar_ready == 1'b1) begin
ar_valid <= 1'b1;
end
end
always @ (posedge clock or negedge reset) begin
if(reset == 1'b1) begin
ar_addr <= 32'h60000000;
end else if(cnt[9:0] == 10'h200) begin
ar_addr <= 32'h80000000;
end else if(ar_valid && mult == 1'b0) begin
ar_addr <= ar_addr + 3'h4;
end else if(ar_valid && mult == 1'b1) begin
ar_addr <= ar_addr + 8'h40;
end
end
assign l2_frontend_bus_axi4_0_aw_valid = aw_valid;
assign l2_frontend_bus_axi4_0_aw_bits_id = mult ? 8'd0 : 8'd0;
assign l2_frontend_bus_axi4_0_aw_bits_addr = aw_addr;
assign l2_frontend_bus_axi4_0_aw_bits_len = mult ? 8'hF : 8'd0;
assign l2_frontend_bus_axi4_0_aw_bits_size = mult ? 3'd2 : 3'd2;
assign l2_frontend_bus_axi4_0_aw_bits_burst = mult ? 2'd1 : 2'd1;
assign l2_frontend_bus_axi4_0_aw_bits_lock = mult ? 1'd0 : 1'd0;
assign l2_frontend_bus_axi4_0_aw_bits_cache = mult ? 4'd0 : 4'd0;
assign l2_frontend_bus_axi4_0_aw_bits_prot = mult ? 3'd1 : 3'd1;
assign l2_frontend_bus_axi4_0_aw_bits_qos = mult ? 4'd0 : 4'd0;
assign l2_frontend_bus_axi4_0_w_valid = w_valid;
assign l2_frontend_bus_axi4_0_w_bits_data = w_data;
assign l2_frontend_bus_axi4_0_w_bits_strb = mult ? 8'hFF: 8'hFF;
assign l2_frontend_bus_axi4_0_w_bits_last = w_last;
assign l2_frontend_bus_axi4_0_ar_valid = ar_valid;
assign l2_frontend_bus_axi4_0_ar_bits_id = mult ? 8'd0 : 4'd2;
assign l2_frontend_bus_axi4_0_ar_bits_addr = ar_addr;
assign l2_frontend_bus_axi4_0_ar_bits_len = mult ? 8'hF : 8'd0;
assign l2_frontend_bus_axi4_0_ar_bits_size = mult ? 3'd2 : 3'd2;
assign l2_frontend_bus_axi4_0_ar_bits_burst = mult ? 2'd1 : 2'd1;
assign l2_frontend_bus_axi4_0_ar_bits_lock = mult ? 1'd0 : 1'd0;
assign l2_frontend_bus_axi4_0_ar_bits_cache = mult ? 4'd0 : 4'd0;
assign l2_frontend_bus_axi4_0_ar_bits_prot = mult ? 3'd1 : 3'd1;
assign l2_frontend_bus_axi4_0_ar_bits_qos = mult ? 4'd0 : 4'd0;
assign l2_frontend_bus_axi4_0_r_ready = 1'b1;
assign l2_frontend_bus_axi4_0_b_ready = 1'b1;