17 AXI总线
在实际的应用项目中,Xilinx的FPGA使用量比较大,Altera的好像越用越少了,这个可能跟很多因素相关,但是开发软件可能是一个重要的因素,Xilinx的Vivado开发工具,使用的门槛要低很多。为了让我们的RISC_V核在Xilinx的FPGA上跑起来,特意买了一块Xilinx的FPGA开发板来做适配。
FPGA开发越来越系统化了,Altera有QSys,Xilinx则以AXI总线为基础来支持系统设计,它的Vivado设计软件有图形化设计工具,提供了很多常用的IP核,减少了很多设计工作量,并通过代码自动生成减少了不少人为的错误。好用的设计软件,直接降低了使用的门槛,让设计人员更加关注项目相关的逻辑设计,而不是花精力跟一些格式符合性之类的八股文纠缠。本节介绍Vivado下的RISC-V核改造以及如何使用在一个项目中,我们先介绍AXI-Lite总线协议,然后为RISCV核提供AXI-Lite总线支持。
17.1 AXI-Lite
AXI总线是ARM的AMBA总线协议的一部分,完整的协议规范请到ARM公司网站协议规范下载,这里只做一般性描述。
AXI-Lite是AXI总线的一个简化版本,它在一次读写过程中只读写一个字,比较适合于设备的状态读写,并不适合于大规模的高速数据连续传输。AXI-Lite总线能够独立传输5种信息,各种信息在传输时没有相互之间的依赖关系,实现的时候可以独立实现,当然要完成一次读写操作,还是需要其中几种信息协同工作。AXI-Lite总线上的节点分为Master和Slave两种类型。
这五种信息分别是:写地址,写数据(包括字节是能),写回应,读地址,读数据,每种信息有独立的总线信号来表达。分别是:
- 写地址:awvalid, awready, awaddr, awprot,Master为源端,Slave为目的端
- 写数据:wvalid, wready, wdata, wstrb,Master为源端,Slave为目的端
- 写回应:bvalid, bready, bresp,Master为目的端,Slave为源端
- 读地址:arvalid, arready, araddr, arprot,Master为源端,Slave为目的端
- 读数据:rvalid, rready, rdata, rresp,Master为目的端,Slave为源端
其中地址和数据可以是32位或64位的。每种信息都有valid信号和ready信号,这是用来在传输过程中实现握手的。AXI-Lite传输信息时,是通过源端和目的端之间的信号握手实现的,传输信息的过程是源端给出valid信号,以及要传输的信息,目的端给出ready信号,两个信号有效的先后不做要求,在valid和ready信号同时有效时,就表示一次信息传输完成。为了避免死锁,AXI规定同一组内的valid信号不能依赖于ready信号来设置有效,ready信号则可以依赖valid信号生成,所有的源端和目的端实现时都遵循这个规定,就不会出现相互等待的死锁情况了。当然,源端收到ready信号之后,表示传输完成,即可以撤销valid信号。
AXI-Lite还规定,源端一旦设置了valid有效,在传输完成之前不得撤销valid信号,所传输的信息也需要维持有效。
使用这五种信息可以实现由Master发起对Slave的读或者写操作:
1、读操作:Master传输读地址到Slave,Slave收到读地址后,可以根据读地址的内容准备数据,在数据准备好时发起传输读数据到Master,这样就通过读地址和读数据两种信息传递完成一次读操作。注意,AXI-Lite协议中,Master传输读地址给Slave之后,必须等待Slave回传读数据信息,然后才能启动下一次读操作。
2 、写操作:Master传输写地址和写数据两种信息到Slave, 两种信息的传输顺序无关,可以一先一后,也可以同时,Slave收到两种信息后,回传写回应信息到Master,即可完成一次写操作。写操作过程中,Master传输写地址和写数据信息之后,必须等待写回应信息,才能开始下一次写操作。
注意,读写操作之间可以交叉进行,中间没有互相等待的关系。
17.2 RISC-V核实现AXI-Lite Master
17.2.1 RISC-V内部的修改
前面实现RISC-V时,外部的总线是比较简单的一种局部总线,写的时候不需要写回应,读的时候也假设读信号有效的下一拍就能够得到返回值,这样的假设并不能与AXI-Lite兼容,因此,要想让RISC-V核支持AXI-Lite总线,得为读写操作增加回应信号。我们修改RISC-V的端口如下:
module riscv_core_v5(
input wClk,
input nwReset,
output wWrite,
output [31:0] bWriteAddr,
output [31:0] bWriteData,
output [3:0] bWriteMask,
input wWriteReady,
output reg wRead,
output reg [31:0] bReadAddr,
input [31:0] bReadData,
input wReadReady,
output reg [4:0] regno,
output reg [3:0] regena,
output reg [31:0] regwrdata,
output reg regwren,
input [31:0] regrddata,
output reg [4:0] regno2,
output reg [3:0] regena2,
output reg [31:0] regwrdata2,
output reg regwren2,
input [31:0] regrddata2
);
其中增加的wWriteReady和wReadReady两个信号就是写操作和读操作的回应信息。RISC-V本身作为Master发起读写操作,等wWriteReady和wReadyReady信号时读写完成。
我们对RISC-V内部的状态机修改如下:
其中红色的状态转移就是等待相应的ready信号,读寄存器,等待读结果和等待读结果2三个状态下等到读ready时才转移到下一个状态,写RAM和 写RAM2则等待写ready信号有效才转移到下一个状态。注意等待过程中valid信号和信息需要维持,因此内部还增加了一些寄存器来存储相关的信息。下面是状态转移相关的代码,其他代码就不给出了。眼尖的应该发现这个版本还增加了watchdog支持,这是通过CSR实现的看门狗,RISC-V启动时将狗粮寄存器设置到半秒,然后软件保证半秒内通喂狗(狗粮寄存器:编号12’hb20,32位,该寄存器每个时钟周期减一,减到零就将pc和state设置到复位值,重新启动运行,按说应该生成nwReset复位信号就更好了,软件必须在减到零之前写该寄存器合适的值)。
//DEFINE_FUNC(riscv_core_gen_state, "state, instr, nwReset") {
always @(posedge wClk)
if (~nwReset
`ifdef WATCHDOG
|| (watchdog == 0)
`endif
) begin
state <= `RISCVSTATE_READ_INST;
end else begin
case (state)
`RISCVSTATE_READ_INST: state <= `RISCVSTATE_READ_REGS;
`RISCVSTATE_READ_REGS:
if (wReadReady) begin
state <= `RISCVSTATE_EXEC_INST;
end
`RISCVSTATE_EXEC_INST: begin
if (opcode == 5'h00) begin
state <= `RISCVSTATE_WAIT_LD;
end else if (opcode == 5'h08) begin
state <= `RISCVSTATE_WAIT_ST;
end else if (opcode == 5'h0c && instr[25] && func3[2] && (rs2 != 0)) begin
state <= `RISCVSTATE_WAIT_DIV;
divclk <= 31;
end else if (opcode == 5'h0c && instr[25] && (func3[2]==0) ) begin
state <= `RISCVSTATE_WAIT_MUL;
divclk <= 3;
end else
state <= `RISCVSTATE_READ_REGS;
end
`RISCVSTATE_WAIT_LD: begin
if (wReadReady) begin
if (func3 == 1 && ldaddr[1:0] == 3) begin /* lh */
state <= `RISCVSTATE_WAIT_LD2;
end
else if (func3 == 2 && ldaddr[1:0] != 0) begin /* lw */
state <= `RISCVSTATE_WAIT_LD2;
end
else if (func3 == 5 && ldaddr[1:0] == 3) begin /* lhu */
state <= `RISCVSTATE_WAIT_LD2;
end
else begin
state <= `RISCVSTATE_READ_REGS;
end
end
end
`RISCVSTATE_WAIT_LD2:
if (wReadReady) begin
state <= `RISCVSTATE_READ_REGS;
end
`RISCVSTATE_WAIT_ST: if (wWriteReady) begin
state <= `RISCVSTATE_READ_REGS;
if (opcode == 5'h08) begin
if (func3 == 1 && (lastaddr & 3) == 3) begin /* sh */
state <= `RISCVSTATE_WAIT_ST2;
end
else if (func3 == 2 && (lastaddr & 3) != 0) begin
state <= `RISCVSTATE_WAIT_ST2;
end
end
end
`RISCVSTATE_WAIT_ST2:
if (wWriteReady )
state <= `RISCVSTATE_READ_REGS;
`RISCVSTATE_WAIT_MUL: begin
`ifdef USEMUL32
if (muldone)
state <= `RISCVSTATE_READ_REGS;
`else
if (divclk == 0)
state <= `RISCVSTATE_READ_REGS;
else
divclk <= divclk - 1;
`endif
end
`RISCVSTATE_WAIT_DIV: begin
`ifdef USEDIV32
if (divdone)
state <= `RISCVSTATE_READ_REGS;
`else
if (divclk == 0)
state <= `RISCVSTATE_READ_REGS;
else
divclk <= divclk - 1;
`endif
end
endcase
end
17.2.2 AXI-Lite Master的实现
下面我们在修改后的局部总线基础上增加AXI-Lite Master信号支持。我们的办法是在核的外面包一个模块,将内存也实例化在里边,AXI-Lite总线外部可能通过互联方式扩展,经过外部握手以及转发,Latency是比较大的,会影响取指令或内存读写指令的读写效率,特别是我们没有Cache支持的情况下,这个问题尤其严重,因此我们把内存实例化在里边, CPU与内存之间还是直接用局部总线相连接。这样做还有个好处,将来我们要增加Cache支持的时候,直接将内存读写改为Cache读写即可。这里我们直接上代码好了:
`timescale 1 ns / 1 ps
module riscv_core_with_axi_master (
input wire m00_axi_aclk,
input wire m00_axi_aresetn,
output wire [31 : 0] m00_axi_awaddr,
output wire [2 : 0] m00_axi_awprot,
output wire m00_axi_awvalid,
input wire m00_axi_awready,
output wire [31 : 0] m00_axi_wdata,
output wire [3 : 0] m00_axi_wstrb,
output wire m00_axi_wvalid,
input wire m00_axi_wready,
input wire [1 : 0] m00_axi_bresp,
input wire m00_axi_bvalid,
output wire m00_axi_bready,
output wire [31 : 0] m00_axi_araddr,
output wire [2 : 0] m00_axi_arprot,
output wire m00_axi_arvalid,
input wire m00_axi_arready,
input wire [31 : 0] m00_axi_rdata,
input wire [1 : 0] m00_axi_rresp,
input wire m00_axi_rvalid,
output wire m00_axi_rready
);
reg axi_awvalid; assign m00_axi_awvalid = axi_awvalid;
reg [31:0] axi_awaddr; assign m00_axi_awaddr = axi_awaddr;
assign m00_axi_awprot = 3'b000;
reg axi_wvalid; assign m00_axi_wvalid = axi_wvalid;
reg [31:0] axi_wdata; assign m00_axi_wdata = axi_wdata;
reg [3:0] axi_wstrb; assign m00_axi_wstrb = axi_wstrb;
assign m00_axi_bready = 1'b1;
reg axi_arvalid; assign m00_axi_arvalid = axi_arvalid;
reg [31:0] axi_araddr; assign m00_axi_araddr = axi_araddr;
assign m00_axi_arprot = 3'b001;
assign m00_axi_rready = 1'b1;
wire wWrite, wRead, wReadReady, wWriteReady;
wire [31:0] bWriteAddr, bWriteData, bReadAddr, bReadData, bReadDataRam, bReadDataKey;
wire [3:0] bWriteMask;
wire [4:0] regno;
wire [3:0] regena;
wire [31:0] regwrdata;
wire regwren;
wire [31:0] regrddata;
wire [4:0] regno2;
wire [3:0] regena2;
wire [31:0] regwrdata2;
wire regwren2;
wire [31:0] regrddata2;
reg [31:0] lastreadaddr;
reg lastread;
always @(posedge m00_axi_aclk)
if (~m00_axi_aresetn) begin
lastreadaddr <= 0;
lastread <= 0;
end else begin
lastreadaddr <= bReadAddr;
lastread <= wRead;
end
wire isramaddr = (lastreadaddr & 32'hfff0_0000) == 32'h0000_0000; /* 1MB ram addr */
assign bReadData = isramaddr ? bReadDataRam : m00_axi_rdata;
assign wReadReady = isramaddr ? lastread : m00_axi_rvalid;
wire isramwriteaddr = (bWriteAddr & 32'hfff0_0000) == 32'h0000_0000; /* 1MB ram addr */
wire isramreadaddr = (bReadAddr & 32'hfff0_0000) == 32'h0000_0000; /* 1MB ram addr */
wire [29:0] ramaddr;
assign ramaddr = wWrite?bWriteAddr[31:2]:bReadAddr[31:2];
reg [4:0] lastregno;
reg [4:0] lastregno2;
always @(posedge m00_axi_aclk) begin
lastregno <= regno;
lastregno2 <= regno2;
end
regfile regs(regno, regena, m00_axi_aclk, regwrdata, regwren, regrddata);
regfile regs2(regno2, regena2, m00_axi_aclk, regwrdata2, regwren2, regrddata2);
`define ALTERA
`ifdef ALTERA
ram4kB ram(.clock(m00_axi_aclk), .address(ramaddr), .byteena(~bWriteMask), .data(bWriteData), .wren(isramwriteaddr ? wWrite : 1'b0), .q(bReadDataRam));
`else
ram4KB ram(.clka(m00_axi_aclk), .ena(1'b1), .addra(ramaddr), .wea((isramwriteaddr && wWrite)?(~bWriteMask):4'b0), .dina(bWriteData) , .douta(bReadDataRam));
`endif
riscv_core_v5 core(
m00_axi_aclk,
m00_axi_aresetn,
wWrite,
bWriteAddr,
bWriteData,
bWriteMask,
wWriteReady,
wRead,
bReadAddr,
bReadData,
wReadReady,
regno,
regena,
regwrdata,
regwren,
(lastregno == 0) ? 0 : regrddata,
regno2,
regena2,
regwrdata2,
regwren2,
(lastregno2 == 0) ? 0 : regrddata2
);
//Write Address
wire writeaxi = (wWrite && ~isramwriteaddr);
reg [31:0] awaddr;
reg awvalid;
always @(posedge m00_axi_aclk)
if (~m00_axi_aresetn) begin
awvalid <= 1'b0;
end else if (writeaxi) begin
awaddr <= bWriteAddr;
awvalid <= 1'b1;
end else if (m00_axi_awready) begin
awvalid <= 1'b0;
end
always @(wWrite or awvalid or bWriteAddr or awaddr)
begin
axi_awvalid = writeaxi ? 1'b1 : awvalid;
axi_awaddr = wWrite ? bWriteAddr : awaddr;
end
/* Write Data */
reg [31:0] waddr;
reg [31:0] wdata;
reg [3:0] wstrb;
reg wvalid;
reg write_local;
always @(posedge m00_axi_aclk)
begin
if (~m00_axi_aresetn) begin
wvalid <= 1'b0;
end else if (writeaxi) begin
waddr <= bWriteAddr;
wdata <= bWriteData;
wstrb <= ~bWriteMask;
wvalid <= 1'b1;
end if (m00_axi_wready) begin
wvalid <= 1'b0;
end
if (~m00_axi_aresetn) begin
write_local <= 1'b0;
end else if (wWrite) begin
if (isramwriteaddr) begin
write_local <= 1;
end else begin
write_local <= 0;
end
end
end
reg writeready;
assign wWriteReady = writeready;
always @(posedge m00_axi_aclk)
if (~m00_axi_aresetn)
writeready <= 1'b0;
else if (~writeready)
writeready <= m00_axi_bvalid || write_local || isramwriteaddr;
always @(wWrite or wvalid or bWriteData or wdata or bWriteMask or wstrb)
begin
axi_wvalid = writeaxi ? 1'b1 : wvalid;
axi_wdata = writeaxi ? bWriteData : wdata;
axi_wstrb = writeaxi ? ~bWriteMask : wstrb;
end
wire readaxi = wRead && ~isramreadaddr;
//Read Address
reg [31:0] araddr;
reg arvalid;
always @(posedge m00_axi_aclk)
if (~m00_axi_aresetn) begin
arvalid <= 1'b0;
end else if (readaxi) begin
araddr <= bReadAddr;
arvalid <= 1'b1;
end else if (m00_axi_arready) begin
arvalid <= 1'b0;
end
always @(wRead or arvalid or bReadAddr or araddr)
begin
axi_arvalid = readaxi ? 1'b1 : arvalid;
axi_araddr = wRead ? bReadAddr : araddr;
end
endmodule
我们为内存地址预留了最低的1MB的地址空间,其他的地址都转发到外部的AXI-Lite接口上去。
注意,AXI-Lite信号相关的端口的顺序和名称不要修改,这样xilinx的Vivado软件能够直接识别出这是一个AXI-Lite Master的接口,这样就可以直接在它的Block Design图形设计软件中使用这个模块作为一个部件来使用。
17.3 AXI-Lite Slave的实现
我们把开发板上的led灯和按键的访问封装成一个AXI-Lite Slave模块,这样也可以在Vivado中直接识别出来。这部分代码比较简单,直接看代码好了:
`timescale 1 ns / 1 ps
module led_key
(
input wire s00_axi_aclk,
input wire s00_axi_aresetn,
input wire [3 : 0] s00_axi_awaddr,
input wire [2 : 0] s00_axi_awprot,
input wire s00_axi_awvalid,
output wire s00_axi_awready,
input wire [31 : 0] s00_axi_wdata,
input wire [3 : 0] s00_axi_wstrb,
input wire s00_axi_wvalid,
output wire s00_axi_wready,
output wire [1 : 0] s00_axi_bresp,
output wire s00_axi_bvalid,
input wire s00_axi_bready,
input wire [3 : 0] s00_axi_araddr,
input wire [2 : 0] s00_axi_arprot,
input wire s00_axi_arvalid,
output wire s00_axi_arready,
output wire [31 : 0] s00_axi_rdata,
output wire [1 : 0] s00_axi_rresp,
output wire s00_axi_rvalid,
input wire s00_axi_rready,
input wire [2:0] key,
output wire [3:0] led
);
reg [31:0] count;
reg [31:0] cpucount;
reg [3:0] led_r;
assign led = led_r;
always @(posedge s00_axi_aclk)
if (~s00_axi_aresetn) begin
count <= 0;
led_r <= 4'b1111;
end else begin
led_r[0] <= cpucount[17];
led_r[1] <= cpucount[19];
led_r[2] <= cpucount[21];
if (count >= 25000000) begin
count <= 0;
led_r[3] <= ~led_r[3];
end else begin
count <= count + 1;
end
end
reg [3:0] axi_awaddr_r;
reg axi_awvalid_r;
wire axi_awvalid = s00_axi_awvalid || axi_awvalid_r;
wire [3:0] axi_awaddr = s00_axi_awvalid ? s00_axi_awaddr : axi_awaddr_r;
reg [31:0] axi_wdata_r;
reg [3:0] axi_wstrb_r;
reg axi_wvalid_r;
wire axi_wvalid = s00_axi_wvalid || axi_wvalid_r;
wire [31:0] axi_wdata = s00_axi_wvalid ? s00_axi_wdata : axi_wdata_r;
wire [3:0] axi_wstrb = s00_axi_wvalid ? s00_axi_wstrb : axi_wstrb_r;
assign s00_axi_awready = 1;
assign s00_axi_wready = 1;
always @(posedge s00_axi_aclk)
if (~s00_axi_aresetn) begin
end else begin
if (s00_axi_awvalid) begin
axi_awaddr_r <= s00_axi_awaddr;
end
if (s00_axi_wvalid) begin
axi_wdata_r <= s00_axi_wdata;
axi_wstrb_r <= s00_axi_wstrb;
end
end
always @(posedge s00_axi_aclk)
if (~s00_axi_aresetn) begin
axi_wvalid_r <= 0;
axi_awvalid_r <= 0;
end else begin
if (axi_awvalid && axi_wvalid) begin
axi_wvalid_r <= 0;
axi_awvalid_r <= 0;
end else if (axi_awvalid) begin
axi_awvalid_r <= 1;
end else if (axi_wvalid) begin
axi_wvalid_r <= 1;
end
end
reg [1 : 0] axi_bresp;
reg axi_bvalid;
reg wen;
assign s00_axi_bresp = axi_bresp;
assign s00_axi_bvalid = axi_bvalid;
always @(posedge s00_axi_aclk)
if (~s00_axi_aresetn) begin
axi_bresp <= 0;
axi_bvalid <= 0;
end else if (axi_awvalid && axi_wvalid) begin
axi_bresp <= 0;
axi_bvalid <= 1;
end else begin
axi_bresp <= 0;
axi_bvalid <= 0;
end
always @( posedge s00_axi_aclk )
begin
if (~s00_axi_aresetn) begin
cpucount <= 32'hffffffff;
wen <= 1;
end else if (axi_awvalid && axi_wvalid && wen) begin
wen <= 0;
if (axi_awaddr == 4) begin
cpucount <= {axi_wstrb[3] ? axi_wdata[31:24] : cpucount[31:24],
axi_wstrb[2] ? axi_wdata[23:16] : cpucount[23:16],
axi_wstrb[1] ? axi_wdata[18:8] : cpucount[15:8],
axi_wstrb[0] ? axi_wdata[7:0] : cpucount[7:0]
};
end
end else begin
wen <= 1;
end
end
reg axi_arready;
reg [3:0] axi_raddr_r;
reg axi_raddr_valid_r;
reg [31 : 0] axi_rdata;
reg [1 : 0] axi_rresp;
reg axi_rvalid;
wire axi_raddr_valid = s00_axi_arvalid || axi_raddr_valid_r;
wire [3:0] axi_raddr = s00_axi_arvalid ? s00_axi_araddr : axi_raddr_r;
assign s00_axi_arready = axi_arready;
assign s00_axi_rvalid = axi_rvalid;
assign s00_axi_rdata = axi_rdata;
assign s00_axi_rresp = axi_rresp;
always @( posedge s00_axi_aclk )
begin
if ( ~s00_axi_aresetn) begin
axi_arready <= 1'b0;
end if (s00_axi_arvalid) begin
axi_arready <= 1'b1;
axi_raddr_r <= s00_axi_araddr;
end else begin
axi_arready <= 1'b0;
end
end
always @( posedge s00_axi_aclk )
begin
if (~s00_axi_aresetn) begin
axi_raddr_valid_r <= 0;
end else begin
if (s00_axi_rready && axi_raddr_valid) begin
axi_raddr_valid_r <= 0;
end else if (s00_axi_arvalid) begin
axi_raddr_valid_r <= 1;
end
end
end
always @( posedge s00_axi_aclk )
begin
if (~s00_axi_aresetn) begin
axi_rdata <= 0;
axi_rvalid <= 0;
end else if (axi_raddr_valid && ~axi_rvalid) begin
if (axi_raddr == 0)
axi_rdata <= {28'h0, s00_axi_aresetn, key};
axi_rvalid <= 1;
end else if (s00_axi_rready && axi_rvalid) begin
axi_rvalid <= 0;
end
end
endmodule
注意,我们只取了AXI地址中的低4位作为偏移地址使用,设备的高地址作为设备地址由外部来进行控制。写偏移地址0其实是将一个值写到cpucount寄存器中,led[3]实际是对时钟进行计数,然后每半秒翻转一次,这样如果FPGA正常运行起来,就可以看到led[3]以一秒为周期闪烁,检测闪烁频率可以确认时钟信号是否正确。led[2:0]分别接到cpucount寄存器的第17,19和21位,这样软件在运行过程中,每个周期将一个32位变量增1,然后写到cpucount中,观察led[2:0]的闪烁情况可以确认CPU是否正常运行起来。
17.4 通过Vivado将部件连接在一起
这一段我们来描述Vivado FPGA开发软件如何进行模块集成。我们选用的时ALINX的A7020开发板,上面有一个Xilinx ZYNQ7000系列的FPGA z77020,我们在其开发板的GPIO扩展J10的5号和7号脚上连接串口TTL信号。
需要VIVADO软件的可以到xilinx的官网上下载,它有免费的版本,WIN10下面可以用它的2021版本,WIN7下好像只能用2018版本,不过对我们的开发已经足够了。
具体的VIVADO中的操作过程如下:
1.生成工程,取名比如test_riscv
选择RTL_Project:
2.增加设计文件riscv,led-key
选择器件类型Zynq-7000,器件代号:xc7z020clg400-2,这是ALINX7020开发板上的型号。
检查一下是否正确:
然后就可以生成工程目录。
3.增加顶层block生成端口
input wClk, nwReset
input uart_rx
output uart_tx
input [2:0] key
output [3:0] led
点击Create Block Design生成一个设计界面: 设计界面在Diagram窗口中。在Diagram窗口中点鼠标右键,选择Create Port…
这是时钟信号wClk,类型为Clock,频率输入50(MHz),这是开发板上的时钟输入
复位信号nwReset,类型Reset,Active Low,后面将它配置到开发板上的按键PL_KEY3
led[3:0]是数据输出信号,注意选择output, Data,Create Vector,并给出宽度
其他的信号类似。
最后生成了测试项目的所有外部端口:
4.在Sources窗口中的design_1项目上点鼠标右键,选择Create HDL Wrapper…,生成顶层HDL包装。然后在Project Summary窗口中将它设置为工程的顶层模型
5.增加ram4KB IP,
在IP Catalog中选择Block Memory Generator
选择Customize IP
module name : ram4KB,接口类型Native, 存储器类型Single Port RAM
Byte Write Enable(on), byte size=8
PortA Options: Width=32, Depth=1024, Primitive Output Register(off)
Other Options: Load Init File(on), coe file,选择RISC-V编译后生成的coe文件(我们提供了cod2mif转换程序,可以将工具链中objcopy生成的FPGA cod文件生成mif和coe文件供Quartus II和Vivado使用)。
Synthesis Option: Global(on)
Generate
此时,就生成了riscv_core_with_axi模块中需要的ram4KB模块,注意如果使用Vivado 2018,在修改coe文件后,需要删除ram4KB组件然后再重新生成。
6.增加module riscv,led-key到block design面板中
在diagram窗口中点鼠标右键选择弹出菜单中的Add Module…
选择加入riscv_core_with_axi_master模块
此时2018版本会出个错误:
好像是说它自己取的名字自己不认识了,应该是个软件bug。此时只好把命令拷贝到下面的TCL命令框修改一下敲回车重新运行。
这样就生成了一个模块实例riscv_core_with_axi_0,其中的AXI-Lite Master接口信号已经被识别出来作为一个整体,名字是m00_axi:
同样的操作加入led_key模块。
7.增加UART IP
为了测试我们实现的AXI-Lite的正确性,我们直接选择Xilinx的uart ip加入到设计中:
这样我们就加入了需要的模块:
8.自动连接各个IP
点击上方的Run Connection Automation,让设计工具把它们连接起来,设计工具自己增加了两个模块来实现一个Master带两个Slave:
再操作鼠标将uart_tx, uart_rx, led, key, nwReset连接到相应的模块上,得到设计的顶层模块(为了看着舒服调整了一下模块的位置):
9.修改Adress Map,在Address Editor中修改下面接口的地址:
uart: f0001000, 4KB
led_key: f0000000, 4KB
其他设备可以使用00100000开始的任何地址(内存部分预留了1MB地址空间)
这部分设置要跟软件配合一起,这个配置下写led读key应该用0xf0000000开始的地址,读写串口则用0xf0001000开始的地址。
10.设置端口管脚
此时可以综合和布线实现,
成功后点击Open Implemented Design,就可以配置管脚了:
全部设置为LVCMOS33
wClk: U18, 50MHz
nwReset: R17, 低有效
uart_rx: Y17
uart_tx: R14
key[0]: N15
key[1]: N16
key[2]: T17
led[0]: M14
led[1]: M15
led[2]: K16
led[3]: J16
11.生成烧写文件
然后可以生成FPGA的烧写文件:
并通过Hardware Manager将Bitstream文件烧写到硬件上,可以看到串口中的提示符,并可以输入命令进行操作了。
这段描述看着比较麻烦,但是初学者可以一步步照着操作,能够很快上手,不需要反复尝试摸索了。这个比当年的ISE要简单多了,某些方面比Altera 的Quatus II要方便,比如那个block design的工具。
17.5 AXI-Lite HUB
Altera的Quatus II下面没有自动连接的开发工具,也没有提供相关的IP,为了在Altera的FPGA开发板上跑带AXI的RISCV,我们设计了AXI-Lite的一拖二模块,可以很简单地扩展到一拖N:
module axi1to2
#(
parameter integer M00_ADDR_MASK = 32'hfffff000,
parameter integer M00_ADDR_START = 32'h00000000,
parameter integer M01_ADDR_MASK = 32'hfffff000,
parameter integer M01_ADDR_START = 32'h00001000
)
(
input wire axi_aclk,
input wire axi_aresetn,
input wire [31 : 0] s00_axi_awaddr,
input wire [2 : 0] s00_axi_awprot,
input wire s00_axi_awvalid,
output reg s00_axi_awready,
input wire [31 : 0] s00_axi_wdata,
input wire [3 : 0] s00_axi_wstrb,
input wire s00_axi_wvalid,
output reg s00_axi_wready,
output reg [1 : 0] s00_axi_bresp,
output reg s00_axi_bvalid,
input wire s00_axi_bready,
input wire [31 : 0] s00_axi_araddr,
input wire [2 : 0] s00_axi_arprot,
input wire s00_axi_arvalid,
output reg s00_axi_arready,
output reg [31 : 0] s00_axi_rdata,
output reg [1 : 0] s00_axi_rresp,
output reg s00_axi_rvalid,
input wire s00_axi_rready,
output wire [31 : 0] m00_axi_awaddr,
output wire [2 : 0] m00_axi_awprot,
output wire m00_axi_awvalid,
input wire m00_axi_awready,
output wire [31 : 0] m00_axi_wdata,
output wire [3 : 0] m00_axi_wstrb,
output wire m00_axi_wvalid,
input wire m00_axi_wready,
input wire [1 : 0] m00_axi_bresp,
input wire m00_axi_bvalid,
output wire m00_axi_bready,
output wire [31 : 0] m00_axi_araddr,
output wire [2 : 0] m00_axi_arprot,
output wire m00_axi_arvalid,
input wire m00_axi_arready,
input wire [31 : 0] m00_axi_rdata,
input wire [1 : 0] m00_axi_rresp,
input wire m00_axi_rvalid,
output wire m00_axi_rready,
output wire [31 : 0] m01_axi_awaddr,
output wire [2 : 0] m01_axi_awprot,
output wire m01_axi_awvalid,
input wire m01_axi_awready,
output wire [31 : 0] m01_axi_wdata,
output wire [3 : 0] m01_axi_wstrb,
output wire m01_axi_wvalid,
input wire m01_axi_wready,
input wire [1 : 0] m01_axi_bresp,
input wire m01_axi_bvalid,
output wire m01_axi_bready,
output wire [31 : 0] m01_axi_araddr,
output wire [2 : 0] m01_axi_arprot,
output wire m01_axi_arvalid,
input wire m01_axi_arready,
input wire [31 : 0] m01_axi_rdata,
input wire [1 : 0] m01_axi_rresp,
input wire m01_axi_rvalid,
output wire m01_axi_rready
);
reg [31:0] axi_araddr;
reg axi_arvalid;
always @(posedge axi_aclk)
if (~axi_aresetn) begin
axi_araddr <= 0;
axi_arvalid <= 0;
end else if (s00_axi_arvalid) begin
axi_araddr <= s00_axi_araddr;
axi_arvalid <= 1'b1;
end else if (s00_axi_rready) begin
axi_arvalid <= 1'b0;
end
wire is_M00_w = ((s00_axi_awaddr & M00_ADDR_MASK) == M00_ADDR_START);
wire is_M01_w = ((s00_axi_awaddr & M01_ADDR_MASK) == M01_ADDR_START);
wire is_M00_r = ((s00_axi_araddr & M00_ADDR_MASK) == M00_ADDR_START);
wire is_M01_r = ((s00_axi_araddr & M01_ADDR_MASK) == M01_ADDR_START);
wire is_M00_r_r = axi_arvalid && ((axi_araddr & M00_ADDR_MASK) == M00_ADDR_START);
wire is_M01_r_r = axi_arvalid && ((axi_araddr & M01_ADDR_MASK) == M01_ADDR_START);
assign m00_axi_awaddr = s00_axi_awaddr;
assign m00_axi_awprot = s00_axi_awprot;
assign m00_axi_awvalid = s00_axi_awvalid && is_M00_w;
assign m00_axi_wdata = s00_axi_wdata;
assign m00_axi_wstrb = s00_axi_wstrb;
assign m00_axi_wvalid = s00_axi_wvalid && is_M00_w;
assign m00_axi_bready = s00_axi_bready;
assign m00_axi_araddr = s00_axi_araddr;
assign m00_axi_arprot = s00_axi_arprot;
assign m00_axi_arvalid = s00_axi_arvalid && is_M00_r;
assign m00_axi_rready = s00_axi_rready && is_M00_r;
assign m01_axi_awaddr = s00_axi_awaddr;
assign m01_axi_awprot = s00_axi_awprot;
assign m01_axi_awvalid = s00_axi_awvalid && is_M01_w;
assign m01_axi_wdata = s00_axi_wdata;
assign m01_axi_wstrb = s00_axi_wstrb;
assign m01_axi_wvalid = s00_axi_wvalid && is_M01_w;
assign m01_axi_bready = s00_axi_bready;
assign m01_axi_araddr = s00_axi_araddr;
assign m01_axi_arprot = s00_axi_arprot;
assign m01_axi_arvalid = s00_axi_arvalid && is_M01_r;
assign m01_axi_rready = s00_axi_rready && is_M01_r;
always @(*)
if (is_M00_w)
s00_axi_awready = m00_axi_awready;
else if (is_M01_w)
s00_axi_awready = m01_axi_awready;
else
s00_axi_awready = 1;
always @(*)
if (is_M00_w)
s00_axi_wready = m00_axi_wready;
else if (is_M01_w)
s00_axi_wready = m01_axi_wready;
else
s00_axi_wready = 1;
always @(*)
if (is_M00_w)
s00_axi_bresp = m00_axi_bresp;
else if (is_M01_w)
s00_axi_bresp = m01_axi_bresp;
else
s00_axi_bresp = 0;
always @(*)
if (is_M00_w)
s00_axi_bvalid = m00_axi_bvalid;
else if (is_M01_w)
s00_axi_bvalid = m01_axi_bvalid;
else
s00_axi_bvalid = 1;
always @(*)
if (is_M00_r_r)
s00_axi_arready = m00_axi_arready;
else if (is_M01_r_r)
s00_axi_arready = m01_axi_arready;
else
s00_axi_arready = 1;
always @(*)
if (is_M00_r_r)
s00_axi_rdata = m00_axi_rdata;
else if (is_M01_r_r)
s00_axi_rdata = m01_axi_rdata;
else
s00_axi_rdata = 32'hefbeadde;
always @(*)
if (is_M00_r_r)
s00_axi_rresp = m00_axi_rresp;
else if (is_M01_r_r)
s00_axi_rresp = m01_axi_rresp;
else
s00_axi_rresp = 0;
always @(*)
if (is_M00_r_r)
s00_axi_rvalid = m00_axi_rvalid;
else if (is_M01_r_r)
s00_axi_rvalid = m01_axi_rvalid;
else
s00_axi_rvalid = 1;
endmodule
我们通过模块参数来指定每个端口的地址范围,实现中对不在地址范围内的地址访问时,都默认ready信号有效,读数据则有效并返回0xefbeadde,在串口中显示deadbeef,这样处理避免访问非法地址时导致CPU陷入等待ready或者状态。
我们还设计了一个与Altera和Xilinx无关的uart收发模块,同样带了AXI-Lite Slave接口,这里就不贴出代码了,感兴趣的到HDL4SE 的git去下载好了。
【请参考】
01.HDL4SE:软件工程师学习Verilog语言(十六)
02.HDL4SE:软件工程师学习Verilog语言(十五)
03.HDL4SE:软件工程师学习Verilog语言(十四)
04.HDL4SE:软件工程师学习Verilog语言(十三)
05.HDL4SE:软件工程师学习Verilog语言(十二)
06.HDL4SE:软件工程师学习Verilog语言(十一)
07.HDL4SE:软件工程师学习Verilog语言(十)
08.HDL4SE:软件工程师学习Verilog语言(九)
09.HDL4SE:软件工程师学习Verilog语言(八)
10.HDL4SE:软件工程师学习Verilog语言(七)
11.HDL4SE:软件工程师学习Verilog语言(六)
12.HDL4SE:软件工程师学习Verilog语言(五)
13.HDL4SE:软件工程师学习Verilog语言(四)
14.HDL4SE:软件工程师学习Verilog语言(三)
15.HDL4SE:软件工程师学习Verilog语言(二)
16.HDL4SE:软件工程师学习Verilog语言(一)
17.LCOM:轻量级组件对象模型
18.LCOM:带数据的接口
19.工具下载:在64位windows下的bison 3.7和flex 2.6.4
20.git: verilog-parser开源项目
21.git: HDL4SE项目
22.git: LCOM项目
23.git: GLFW项目
24.git: SystemC项目