基于AXI协议的自定义IP核
- 在[FPGA5]打包IP核_PWM发生器,我们已经学习创建打包了一个PWM波生成器的IP核。在ZYNQ系统中设计IP核,最常用的就是使用AXI 总线将 PS 同 PL 部分的 IP 核连接起来。本次将尝试编写打包一个基于AXI总线协议的PWM波发生器IP核。
Vivado 工程建立
使用向导创建AXI模板
- 点击
Tools -- Create and Package IP...
,点击Next - 选择
Create a new AXI4 peripheral
,创建一个新的AXI4设备 - 填写名称与描述,并选择自定义IP的存储位置,这里将
Name
设为axi_pwm,Display name
设为axi_pwm_v1.0,Description
设为AXI_PWM_Out - 设置需要使用的接口类型,有Full、Lite、Stream三种模式可选择,由于PWM波发生器仅需要配置频率,占空比等相关的配置寄存器,故选用AXI-Lite协议。寄存器数量根据实际需要,设定为4,位宽按照协议规定,可以在 32 位和 64 位之间进行选择,这里选用32位。
- 点击
Finish
完成 IP 的创建,这时能够在IP Catalog
界面中看到刚刚创建的IP核,此时IP核只有读写寄存器的功能,故我们需编写代码,实现所需功能。
修改IP核模板
- 选择新生成的IP核,右键
Edit in IP Packager
,可填入工程名与工程位置 - 点击
Add Sources
,添加 PWM 功能的核心代码 - 在
Add or Create Design Sources
界面,点击+号创建或添加文件至工程,并勾选Copy Sources into IP Directory
选项。新建活添加的文件主要用于编写实现目标功能的代码,即PWM波的发生。 - 在自动生成的AXI模板程序中例化我们添加的pwm功能文件,并添加额外所需的输入输出接口,并使用寄存器 slv_reg0-4,对PWM的参数进行具体的控制操作。具体修改添加操作见后文。
- 若添加了额外的输入输出接口,则还需在顶层元件例化中添加对应端口
重新打包IP核
- 双击
Sources -- Design Sources -- IP-XACT -- component.xml
文件,打开IP核打包配置界面 - 在
File Groups
选项中点击Merge changers from File Groups Wizard
- 在
Customization Parameters
选项中点击Merge changes form Customization Parameters Wizard
- 当然,如果在此处想要添加配置端口,预定义常量参数,参数类型等,均可根据实际IP核的功能需求进行修改,具体配置方法详见[FPGA5]打包IP核_PWM发生器
- 最后,点击
Re-Package IP
,完成 IP核的修改工作。
AXI代码模板分析
AXI定义与申明
- 该段代码定义了使用的参数,是我们在生成模板的时候预先设置的参数,数据位宽32位,地址位4位。由于位宽为4*8byte,每一个寄存器为4Bit,4个寄存器为16Bit,故地址线为2^4=16。
module myip_v1_0 #
(
// Users to add parameters here
// User parameters ends
// Do not modify the parameters beyond this line
// Parameters of Axi Slave Bus Interface S00_AXI
parameter integer C_S00_AXI_DATA_WIDTH = 32,
parameter integer C_S00_AXI_ADDR_WIDTH = 4
)
- 该段代码的注释是让用户添加所需要的端口,在该项目中,需要添加一个PWM波的输出接口,故声明一个位宽为1的pwn_out输出接口。
// Users to add ports here
output wire pwn_out,
// User ports ends
// Do not modify the ports beyond this line
- 接下来是对于AXI系统输入输出接口的定义,具体的接口定义与功能,可参考代码注解或者查看[FPGA7]AXI通信协议
- 这段代码是对于用户会使用到的逻辑信号与寄存器的申明,包括使用向导时定义的四个寄存器slv_reg0-4,以及寄存器可读使能,可写使能,输出寄存器等
//-- Signals for user logic register space example
//------------------------------------------------
//-- Number of Slave Registers 4
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
wire slv_reg_rden;
wire slv_reg_wren;
reg [C_S_AXI_DATA_WIDTH-1:0] reg_data_out;
integer byte_index;
reg aw_en;
AXI握手逻辑
- 以写地址握手逻辑为例,进行握手代码的分析,其余握手操作均同理
- 上述代码块通过控制 awready 信号完成了一次地址通道传输。逻辑检测 awvalid,wvalid信号的电平,当主机在写地址以及写数据通道上就绪时,从机置高 awready 信号,完成一次地址传输。与上awready 是因为传输只需要在一次时钟上升沿 valid,ready 信号同时置高即可,在下一个周期,从机需要负责将 awready 信号置低,主机一般也会将 awvalid 信号置低,不过这和从机就不相干了。
- 在置高 awready 信号的条件中与上了 aw_en 变量,这是为了使从机有控制地址通道传输的能力。在整个写传输周期中,即地址-数据-写回复信号传输持续期间,如果主机又发起了一次新的写传输请求,置高 awvalid,wvalid信号,那么此时 aw_en 信号为低,从机将不会响应直至从机完成写回复,结束本次传输后,才回去响应下一次传输。
- 在从机置起 awready 信号完成地址通道传输的同时,从机也会从写地址上锁存当前的写地址。锁存的写地址会用于其后一个周期时刻来判断待写入的寄存器。
always @( posedge S_AXI_ACLK )//当时钟上升沿
begin
if ( S_AXI_ARESETN == 1'b0 )//判断是否复位
begin
axi_awready <= 1'b0; //如果复位则置0
aw_en <= 1'b1;
end
else
begin
if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
begin//判断是否收到AWVALID信号,WVALID信号和写地址使能信号
// slave is ready to accept write address when
// there is a valid write address and write data
// on the write address and data bus. This design
// expects no outstanding transactions.
axi_awready <= 1'b1;
aw_en <= 1'b0;
end
else if (S_AXI_BREADY && axi_bvalid)//或者获得了写应答通道的使能
begin
aw_en <= 1'b1;
axi_awready <= 1'b0;
end
else
begin
axi_awready <= 1'b0;//否则,不做好可写地址准备
end
end
end
AXI写入
- 以写数据为例,分析写入的核心代码
- 首先判断读取写数据的使能条件。写地址传输的后一个时刻为写数据传输,写地址的完成的信号即作为写数据的使能信号。
- 当写使能有效后,逻辑首先根据写地址判断主机所要操作的寄存器。这里对写地址进行切片:
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]
- 在完成寄存器的选择后,将总线上已经就绪的写数据写入寄存器。如果不使用 STRB 信号对某些字节进行屏蔽,那么这样就完事了:
slv_reg0 <= S_AXI_WDATA;
- STRB 信号的作用是屏蔽一些字节的写入,比如某个寄存器只有 8 bit,或者 16 bit,但寄存器的写入必须以 32 bit 为单位。完整写入 32 bit 势必会影响临近的其他寄存器,此时可以使用 STRB 信号,指定要写入的字节位置为 1,屏蔽字节位置写 0 。代码中使用 for 循环,以字节为单位,判断 STRB 信号为 1,则进行写入,否则保持原寄存器值不变。
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
slv_reg0 <= 0;
slv_reg1 <= 0;
slv_reg2 <= 0;
slv_reg3 <= 0;
end
else begin
if (slv_reg_wren)
begin
//对于地址位数进行判断,并分Bit进行for循环写入数据
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 0
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
2'h1:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 1
slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
default : begin //如果没有写入,则对数据进行保持
slv_reg0 <= slv_reg0;
slv_reg1 <= slv_reg1;
end
endcase
end
end
end
AXI写响应
- 完成一次写操作的最后一步,是由从机在写回复通道上对此次传输进行响应。
- 在完成传输,即写数据/地址通道完成握手后的下一周期,从机置起 bvalid 信号,并在 bresp 信号上给出 'OK' 信号。在响应通道完成握手后,置低 bvalid 完成响应操作。
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_bvalid <= 0;
axi_bresp <= 2'b0;
end
else
begin
if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
begin
// indicates a valid write response is available
axi_bvalid <= 1'b1;
axi_bresp <= 2'b0; // 'OKAY' response
end // work error responses in future
else
begin
if (S_AXI_BREADY && axi_bvalid)
//check if bready is asserted while bvalid is high)
//(there is a possibility that bready is always asserted high)
begin
axi_bvalid <= 1'b0;
end
end
end
end
AXI读出
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin //根据地址的不同,将对应数据写入缓存寄存器内
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0 : reg_data_out <= slv_reg0;
2'h1 : reg_data_out <= slv_reg1;
2'h2 : reg_data_out <= slv_reg2;
2'h3 : reg_data_out <= slv_reg3;
default : reg_data_out <= 0;
endcase
end
用户功能代码
- 在用户功能代码部分,关于pwm波发生的相关代码,可直接参照[FPGA5]打包IP核_PWM发生器中关于PWM波的发生代码部分,只需要将该部分代码在AXI代码模板中进行例化,并在顶层文件中添加额外的输入输出接口即可,具体添加代码如下:
// Add user logic here
axi_pwm
#(
.PWM_BIT_WIDTH(C_S_AXI_DATA_WIDTH)//预定义参数,位宽
)
(
.clk(S_AXI_ACLK), //输入时钟
.rst_n(S_AXI_ARESETN), //复位信号
.period(slv_reg0),//周期=period*时钟频率
.void_value(slv_reg1), //占空比=void_value/period
.pwm_out(pwm_out) //输出
);
// User logic ends