赞
踩
ZYNQ芯片操作DDR3不需要例化MIG IP(ZYNQ可能根本就没有MIG IP?Xilinx官方作了一些规定,说使用ZYNQ芯片操作DDR3必须通过AXI_HP进行操作,但此说法出自哪里仍然需要考证),只需要直接往AXI_HP总线读写数据即可。所以要想使用ZYNQ芯片操作DDR3,重点转化为如何操作AXI总线。
AXI端口存在于PS端(存在于PS端的意思是端口的命名使用PS端的作用命名),分为以下三种:
AXI端口使用AXI4总线协议。AXI4协议是ARM公司提出的一种高性能,高带宽,低延迟的片内总线。它主要描述了主设备到从设备之间的数据传输方式。为了适用不同的数据传输情况,提高性能,AXI4协议有以下三种分类:
AXI端口和AXI4协议的关系是什么?其实从名称上就可见一斑。AXI的GP、HP、ACP都是ZYNQ芯片上独有的片上传输端口,使用了AXI4协议进行通信;而AXI4协议不单单可以存在于ZYNQ芯片上,FPGA主模块和从模块之间也可以使用AXI4协议进行数据交互,例如MIG IP核于用户端之间就可以选择AXI4协议进行数据交互。
GP接口共有4个,其中两个PS作主机,两个PS作从机;HP接口共有4个,全部都是PS作从机;ACP接口有一个,PS作从机。主机和从机指的是控制信号由主机给出,从机进行接收,并不是指数据传输的方向。在主机和从机之间,数据是可以进行双向传输的。
一般而言,使用PL直接操作DDR3是十分复杂的,所以我们可以让PL直接读写位于PS端的DDR3 Controller,DDR3 Controller就会自动帮助开发者完成读写操作。ARM核同样可以通过Cache操作DDR3 Controller读写数据。如果要采用这种方式完成PS和PL的数据交互,可以让PL写入DDR3,PS读出DDR3;也可以让PS写入DDR3,PL读出DDR3,此时DDR3就起到“数据中介”的作用。
PL端通过ACP接口可以将数据写入Cache,那么就可以通过Cache操作DDR3 Controller进而操作DDR3芯片。PL端既可以通过HP接口操作DDR3,也可以通过ACP接口操作DDR3,哪个更好呢?显然HP更合理,因为使用ACP接口让数据的传输多了一个Cache传输,有“舍近求远”的嫌疑。
当PS端作为主机使用GP接口传输数据时,PL为从机,此时可以将PL看作PS的外设。与微控制器系统不同的是,以PL作为外设可以让外设成为可编程器件,大大扩展了设计的灵活性和性能上限。
当PS将PL端看作外设使用时,PL端必须定义一系列的寄存器并划定地址供PS使用。PL端仅定义了这些寄存器的地址,但是实际上这些寄存器是PS的寄存器 (这些寄存器到底在PS还是PL?有待考证)。
上图中0x4300_0000可以是基地址,也成为起始地址;0x4300_FFFF是结束地址。每个寄存器都是32位的,这里的寄存器地址相当于寄存器首字节的起始地址(内部依然按字节编址)。起始地址和结束地址的值是可以通过Vivado软件在PL端配置的。PS通过基地址+偏移的形式就可以访问所有的寄存器。基地址相当于PL端外设的ID号,在PL端定义的不同基地址对应不同的可编程外设模块。
进行数据交互时,PS将数据写入寄存器,PL通过GP总线读出寄存器的值,就完成了PS到PL的数据传输;也可以让PL通过GP写入寄存器,PS将数据读出,就完成了PL到PS的数据传输。
从这里就可以看到HP接口和GP接口的一个区别了:PL通过HP接口写入的数据存储到了DDR芯片中,而PL通过GP接口传输的数据存储在了PS端的寄存器中。
在实际操作中,在PS端常用以下两个函数:
xil_out32(addr, data)
xil_in32(addr)
这两个函数将PS和PL的数据交互封装起来了,隐藏了很多内部的时序逻辑。实际上PS和PL进行数据交互时使用握手机制(handshake)表示数据何时有效,何时准备好传输。实际的传输时序是相当比较复杂的。这一部分的实际操作相关的细节之后再进行补充,这里可以仅作简单了解。
上图给出了各个接口的带宽理论上限。这些数据是如何计算出来的?以第一行GP接口为例,数据位宽为32bit,接口时钟(IF Clock)频率典型值为150MHz,所以单个GP的接口的峰值数据带宽为32×150=4800 Mb/s,也就是32×150/8=600MB/s。在这里需要注意Mb和MB是两个不同的概念。读写带宽重叠的带宽为1200MB/s,M_GP_AXI一共有两条,所以该通道的数据带宽峰值为2400MB/s。除了DDR和OCM的计算不能按照这种方法外,其他的总线数据带宽计算都可以参照这种方式。
在AXI协议中,读写数据通道是分离的,其中读通道包含两个部分,写通道包含三个部分:
AR
R
AW
W
B
可以看到,读通道不存在一个单独的“读响应通道”,但是并不表示读通道没有读响应机制。读通道的读响应信号包含在了读数据通道中。
握手信号是AXI总线数据传输中的一个重点,甚至是AXI总线的核心。发送端发出一个请求,如果接收端准备好,就向发送端返回一个“准备好了”信号;如果接收端没有准备好,发送端的请求就一直保持,直到收到一个准备好信号才开始下一步动作。从波形上理解,可以认为只有当请求信号和准备好信号同时有效(同时为高电平)时数据才正确传输过去。
一般而言,我们将请求信号称为VALID
,也可以称为命令有效;将准备好信号成为READY
。
主机如何写数据到从机?用写数据通道的部分信号为例说明:
WVALID
:从机通过这个信号知道主机发起了请求。当从机在时钟的上升沿检测到VALID
信号拉高,就说明接到了主机的请求;
WREADY
:主机通过这个信号知道从机已准备好接收。当主机在时钟上升沿检测到READY
信号拉高,就说明从机准备好了;
WDATA
:在写数据过程中,有效数据通常和VALID
信号同时发出。
VALID
和READY
信号可以同时到达,也可以先后到达。只有这两个信号同时拉高后,握手机制才正式成立,读时序也就完成了。握手机制不单单存在于读写时序中,也存在于响应时序中,换言之,AXI总线的五个通道都存在握手机制,只要主机和从机之间进行了数据传输,无论这个数据的形式是怎样的(地址、数据或响应),都必然采用握手机制保证数据传输的稳定。
AXI总线是ARM公司设计的,想必设计之初也不单单为FPGA内部数据通信考量,而是更多地为CPU、MCU等设备内部的数据交互提供了便利,所以在FPGA中使用AXI总线并不需要过分关注AXI总线多如牛毛的端口,而是更应该将精力放在时序的理解上。
AXI_GP接口使用AXI_Lite协议进行低速的数据传输,AXI_Lite实际上就是轻量化的AXI_Full信号,这个“听起来”很唬人,实际上就是砍掉了一部分在FPGA设计中不常用的信号,剩下的都是在FPGA应用中的核心。AXI_Lite协议中不同通道的信号线如下图所示:
可以看到,这些读写通道的组成都是有规律的。除了握手机制中提到的请求VALID
,READY
和数据DATA/ADDR
外,还有以下几种信号:
STRB
:这个信号指有效字节的位置。由于AXI_Lite协议中数据线只能是32bit,所以STRB
的位宽为4bit,分别表明哪些字节的传输是有效的,1代表有效,0代表无效,低位对应低字节,高位对应高字节;PORT
:保护类型信号,表示传输数据的安全等级和优先级,无论传输的是指令还是数据。不常用,通常默认为3'b000
。BRESP/RRESP
:写响应通道的响应信号和读响应信号,当从机返回的值为2'b00
时表示传输成功,当返回的值为2'b01
是表示EXOKAY(暂时不知道是什么,AXI_Lite不支持),返回值为2'b10
时表示从机发生了错误,当返回值为2'b11
是表示DECERR(暂时不知道是什么,但是也不常用)。写过程使用写通道,通道中三个子通道的时序(先后顺序示意图)如下图所示:
上图的时序并不是完全固定的,写地址和写数据的先后顺序可以调整,主机可以先发送写地址,再发送写数据;也可以先发送写数据,再发送写地址,当然也可以同时发送。这就对应了三种主机发送模式,对应从机也有三种接收模式。最常用的模式如下所示:
读过程和写过程同理,但是上面的时序图是相对固定的。我们最常用的模式是:
虽然Xilinx官方为AXI总线提供了很多基于IP核的设计,应用这些设计可以大大简化设计流程,但是这种基于IP核的设计是以消耗了FPGA设计的灵活性为代价的,所以还是有必要自己手撕AXI代码。
AXI实现写数据的步骤:
AXI实现读数据的步骤:
VALID
和READY
信号都有效时接收数据;在写代码之前可以参考Xilinx官方的代码,用下面的方法获得官方给出的AXI4接口代码,在工具栏Tools下的Create and Package New IP,选择创建一个AXI4接口。选择端口信息后创建IP核并选择编辑IP,就可以获得Xilinx官方的AXI接口代码。
AxSIZE
和AxLEN
的赋值下面的函数描述了AxSIZE
和参数C_M_AXI_DATA_WIDTH/8-1
之间的转换关系。C_M_AXI_DATA_WIDTH
是传输的数据的位宽,单位为bit。在效果上等同于运用了一个巧妙的方法计算了一个数的以2为底的对数。
/* calculate the binary bit width of number */
function integer clogb2(input integer number);
begin
for (clogb2 = 0; number > 0; clogb2 = clogb2 + 1)
number = number >> 1;
end
endfunction
实际使用时用以下的形式调用:
assign M_AXI_AWSIZE = clogb2((C_MAX_DATA_WIDTH/8)-1);
在ARM官方的AXI手册中,对AxLEN的定义如下:
所以需要注意,在根据参数建模时,需要将C_M_AXI_BURST_LEN
减一之后赋值给AxLEN
,否则会出现读时序的错误:
assign M_AXI_AWLEN = C_M_AXI_BURST_LEN - 1;
/* Some code... */
assign M_AXI_ARLEN = C_M_AXI_BURST_LEN - 1;
为了和Xilinx官方提供的AXI接口IP互联时省去很多操作,可以直接选择官方生成的代码中的端口命名方式。下面是笔者自己写的一个AXI主机接口,对AXI从机接口进行突发写和突发读操作,突发长度都是16。程序中的状态机在这样的一个简单的例子中没有实际应用意义,但是在数据量大时采用三段式状态机可以帮助优化代码编写的思路。
/* * File Created: Wednesday, 17th April 2024 23:52:15 * * Last Modified: Thursday, 18th April 2024 16:59:09 * * Function: Creat a AXI Bus Master module without IP */ module user_AXI_FULL_M # ( parameter C_M_TARGET_SLAVE_BASE_ADDR = 32'h40000000, parameter integer C_M_AXI_BURST_LEN = 16, // Burst length must greater than 2 in this module parameter integer C_M_AXI_ID_WIDTH = 1, parameter integer C_M_AXI_ADDR_WIDTH = 32, parameter integer C_M_AXI_DATA_WIDTH = 32, parameter integer C_M_AXI_AWUSER_WIDTH = 0, parameter integer C_M_AXI_ARUSER_WIDTH = 0, parameter integer C_M_AXI_WUSER_WIDTH = 0, parameter integer C_M_AXI_RUSER_WIDTH = 0, parameter integer C_M_AXI_BUSER_WIDTH = 0 ) ( input wire INIT_AXI_TXN , output wire TXN_DONE , output reg ERROR , input wire M_AXI_ACLK , input wire M_AXI_ARESETN , output wire [C_M_AXI_ID_WIDTH-1 : 0] M_AXI_AWID , output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_AWADDR , output wire [7 : 0] M_AXI_AWLEN , output wire [2 : 0] M_AXI_AWSIZE , output wire [1 : 0] M_AXI_AWBURST , output wire M_AXI_AWLOCK , output wire [3 : 0] M_AXI_AWCACHE , output wire [2 : 0] M_AXI_AWPROT , output wire [3 : 0] M_AXI_AWQOS , output wire [C_M_AXI_AWUSER_WIDTH-1 : 0] M_AXI_AWUSER , output wire M_AXI_AWVALID , input wire M_AXI_AWREADY , output wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_WDATA , output wire [C_M_AXI_DATA_WIDTH/8-1 : 0] M_AXI_WSTRB , output wire M_AXI_WLAST , output wire [C_M_AXI_WUSER_WIDTH-1 : 0] M_AXI_WUSER , output wire M_AXI_WVALID , input wire M_AXI_WREADY , input wire [C_M_AXI_ID_WIDTH-1 : 0] M_AXI_BID , input wire [1 : 0] M_AXI_BRESP , input wire [C_M_AXI_BUSER_WIDTH-1 : 0] M_AXI_BUSER , input wire M_AXI_BVALID , output wire M_AXI_BREADY , output wire [C_M_AXI_ID_WIDTH-1 : 0] M_AXI_ARID , output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_ARADDR , output wire [7 : 0] M_AXI_ARLEN , output wire [2 : 0] M_AXI_ARSIZE , output wire [1 : 0] M_AXI_ARBURST , output wire M_AXI_ARLOCK , output wire [3 : 0] M_AXI_ARCACHE , output wire [2 : 0] M_AXI_ARPROT , output wire [3 : 0] M_AXI_ARQOS , output wire [C_M_AXI_ARUSER_WIDTH-1 : 0] M_AXI_ARUSER , output wire M_AXI_ARVALID , input wire M_AXI_ARREADY , input wire [C_M_AXI_ID_WIDTH-1 : 0] M_AXI_RID , input wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_RDATA , input wire [1 : 0] M_AXI_RRESP , input wire M_AXI_RLAST , input wire [C_M_AXI_RUSER_WIDTH-1 : 0] M_AXI_RUSER , input wire M_AXI_RVALID , output wire M_AXI_RREADY ); /* calculate the binary bit width of number */ function integer clogb2(input integer number); begin for (clogb2 = 0; number > 0; clogb2 = clogb2 + 1) number = number >> 1; end endfunction /* --------------------Parameter define--------------------- */ parameter P_ST_IDLE = 'd0, P_ST_WRITE_START = 'd1, P_ST_WRITE_TRANS = 'd2, P_ST_WRITE_END = 'd3, P_ST_READ_START = 'd4, P_ST_READ_TRANS = 'd5, P_ST_READ_END = 'd6; /* --------------------State machine------------------------ */ reg [7:0] r_st_current_write; reg [7:0] r_st_next_write; reg [7:0] r_st_current_read; reg [7:0] r_st_next_read; /* --------------------Reg define--------------------------- */ reg [C_M_AXI_ADDR_WIDTH - 1 : 0] r_m_axi_awaddr; reg r_m_axi_awvalid; reg [C_M_AXI_DATA_WIDTH - 1 : 0] r_m_axi_wdata; reg r_m_axi_wvalid; reg r_m_axi_wlast; reg [C_M_AXI_ADDR_WIDTH - 1 : 0] r_m_axi_araddr; reg r_m_axi_arvalid; reg r_m_axi_rready; reg r_write_start; // write exec signal reg r_read_start; // read exec signal reg [7:0] r_burst_cnt; // counter for burst write reg [C_M_AXI_DATA_WIDTH - 1 : 0] r_axi_read_data; // read data from slave interface /* --------------------Net define--------------------------- */ /* --------------------Combinational Logic------------------ */ assign M_AXI_AWID = 'd0; assign M_AXI_AWLEN = C_M_AXI_BURST_LEN - 1; assign M_AXI_AWSIZE = clogb2((C_M_AXI_DATA_WIDTH/8)-1); assign M_AXI_AWBURST = 2'b01; // burst type, select INCR here assign M_AXI_AWLOCK = 1'd0; assign M_AXI_AWCACHE = 4'b0010; // normal non-cacheable non-bufferable assign M_AXI_AWPROT = 'd0; assign M_AXI_AWQOS = 'd0; assign M_AXI_AWUSER = 'd0; assign M_AXI_AWADDR = r_m_axi_awaddr + C_M_TARGET_SLAVE_BASE_ADDR; assign M_AXI_AWVALID = r_m_axi_awvalid; assign M_AXI_WSTRB = {(C_M_AXI_DATA_WIDTH / 8){1'b1}}; assign M_AXI_WUSER = 'd0; assign M_AXI_WDATA = r_m_axi_wdata; assign M_AXI_WLAST = r_m_axi_wlast; assign M_AXI_WVALID = r_m_axi_wvalid; assign M_AXI_BREADY = 1'b1; // master ready for accept response any time assign M_AXI_ARID = 'd0; assign M_AXI_ARADDR = r_m_axi_araddr + C_M_TARGET_SLAVE_BASE_ADDR; assign M_AXI_ARLEN = C_M_AXI_BURST_LEN - 1; assign M_AXI_ARSIZE = clogb2((C_M_AXI_DATA_WIDTH/8)-1); assign M_AXI_ARBURST = 2'b01; assign M_AXI_ARLOCK = 1'b0; assign M_AXI_ARCACHE = 4'b0010; assign M_AXI_ARPROT = 'd0; assign M_AXI_ARQOS = 'd0; assign M_AXI_ARUSER = 'd0; assign M_AXI_ARVALID = r_m_axi_arvalid; assign M_AXI_RREADY = r_m_axi_rready; /* --------------------Sequencial Logic--------------------- */ /* WRITE logic---------------------------------------------- */ /* address write valid */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN || (M_AXI_AWVALID && M_AXI_AWREADY)) begin r_m_axi_awvalid <= 'd0; end else if (r_write_start) begin r_m_axi_awvalid <= 'd1; end else begin r_m_axi_awvalid <= r_m_axi_awvalid; end end /* send address for write */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN) begin r_m_axi_awaddr <= 'd0; end else if (r_write_start) begin r_m_axi_awaddr <= 'd0; // set the write address to 0 end else begin r_m_axi_awaddr <= r_m_axi_awaddr; end end /* write data valid */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN || M_AXI_WLAST) begin r_m_axi_wvalid <= 1'd0; end else if (M_AXI_AWVALID && M_AXI_AWREADY) begin // write data when sending write address is successful r_m_axi_wvalid <= 1'b1; end else begin r_m_axi_wvalid <= r_m_axi_wvalid; end end /* write data */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN || M_AXI_WLAST) begin r_m_axi_wdata <= 'd1; // write data begin with 1, followed by 2, 3, 4... end else if (M_AXI_WVALID && M_AXI_WREADY) begin // write data is successful r_m_axi_wdata <= r_m_axi_wdata + 'd1; end else begin r_m_axi_wdata <= r_m_axi_wdata; end end /* write last data signal */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (r_burst_cnt == C_M_AXI_BURST_LEN - 2) begin // Burst length must greater than 2 r_m_axi_wlast <= 1'b1; end else begin r_m_axi_wlast <= 1'b0; end end /* burst length counter */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN) begin r_burst_cnt <= 'd0; end else if (r_burst_cnt == C_M_AXI_BURST_LEN - 1) begin r_burst_cnt <= 'd0; end else if (M_AXI_WVALID && M_AXI_WREADY) begin r_burst_cnt <= r_burst_cnt + 'd1; end else begin r_burst_cnt <= r_burst_cnt; end end /* READ logic---------------------------------------------- */ /* address read valid */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN || (M_AXI_ARVALID && M_AXI_ARREADY)) begin r_m_axi_arvalid <= 1'b0; end else if (r_read_start) begin r_m_axi_arvalid <= 1'b1; end end /* send address for read */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN) begin r_m_axi_araddr <= 'd0; end else if (r_read_start) begin r_m_axi_araddr <= 'd0; end else begin r_m_axi_araddr <= r_m_axi_araddr; end end /* read ready */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN || M_AXI_RLAST) begin r_m_axi_rready <= 1'b0; end else if (M_AXI_ARVALID && M_AXI_ARREADY) begin // read data when sending read address is successful r_m_axi_rready <= 1'b1; end end always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (M_AXI_RVALID && M_AXI_RREADY) begin r_axi_read_data <= M_AXI_RDATA; end else begin r_axi_read_data <= r_axi_read_data; end end /* State machine------------------------------------------- */ /* WRITE machine */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN) r_st_current_write <= P_ST_IDLE; else r_st_current_write <= r_st_next_write; end always @(*) begin case (r_st_current_write) P_ST_IDLE : r_st_next_write = P_ST_WRITE_START; P_ST_WRITE_START : r_st_next_write = r_write_start ? P_ST_WRITE_TRANS : P_ST_WRITE_START; P_ST_WRITE_TRANS : r_st_next_write = M_AXI_WLAST ? P_ST_WRITE_END : P_ST_WRITE_TRANS; P_ST_WRITE_END : r_st_next_write = (r_st_current_read == P_ST_READ_END) ? P_ST_IDLE : P_ST_WRITE_END; default: r_st_next_write = P_ST_IDLE; endcase end always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (r_st_current_write == P_ST_WRITE_START) r_write_start <= 1'b1; else r_write_start <= 1'b0; end /* READ machine */ always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (!M_AXI_ARESETN) r_st_current_read <= P_ST_IDLE; else r_st_current_read <= r_st_next_read; end always @(*) begin case (r_st_current_read) P_ST_IDLE : r_st_next_read = (r_st_current_write == P_ST_WRITE_END) ? P_ST_READ_START : P_ST_IDLE; P_ST_READ_START : r_st_next_read = r_read_start ? P_ST_READ_TRANS : P_ST_READ_START; P_ST_READ_TRANS : r_st_next_read = M_AXI_RLAST ? P_ST_READ_END : P_ST_READ_TRANS; P_ST_READ_END : r_st_next_read = P_ST_IDLE; default: r_st_next_read = P_ST_IDLE; endcase end always @(posedge M_AXI_ACLK or negedge M_AXI_ARESETN) begin if (r_st_current_read == P_ST_READ_START) r_read_start <= 1'b1; else r_read_start <= 1'b0; end endmodule
首先创建一个Vivado工程,添加编写好的RTL代码后创建一个Block Design,在空白处右键点击Add Module…
出现下面的窗口,Vivado会自动识别工程中可以添加的RTL模块,点击添加即可。
或者直接在Sources菜单中右键写好的RTL模块,选择Add Module to Block Design,效果是一样的。
创建一个拥有从机AXI_Full接口的IP核,引出端口,连接对应信号,生成底层后就可以添加仿真文件进行仿真了(在这里博主写的模块进行连线验证后会出现一个警告,提示两个模块的AXI端口并不能完全连接,但是这个警告暂时不影响仿真效果)。
module axi_full_test_tb(); reg txn; reg clk; reg rstn; initial begin clk = 0; rstn = 0; txn = 0; #100 rstn = 1; #100 txn = 1; end always #10 clk = ~clk; user_axi_full_test instent( .INIT_AXI_TXN_0(txn), .M_AXI_ACLK_0(clk), .M_AXI_ARESETN_0(rstn) ); endmodule
仿真波形如下所示,可以看到读写突发长度都是16,写入读出的数据都是正常的。
持续不定期更新完善中……
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、FPGA方面的学习笔记。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。