之前做的所有的工作都是要么仅仅使用PS部分,有么仅仅使用PL部分,接下去开始,则需要两个部分同时编程,同时运行。之前所有的项目中间,都是构造一个系统,该系统中由各个功能不同的IP核组成,数据在各个IP和之间通过总线相连接。这一部分计划实现一个自己实现的IP核,并通过AXI4-Lite总线完成几个寄存器内的数据进行共享。
正巧,在基于ZYNQ-7000开发板的调试系列(5)提到过基于TTC计时器的基础使用用途,即通过PL部分的PWM来完成一个呼吸灯的效果,这一部分主要就是实现呼吸灯的效果。
这里没有需要注意的地方,仅仅需要将DDR的型号调整至于芯片型号一致即可。
首先是菜单栏中的Tools -> Create and Package New IP,或者是直接按照上面的指示,按在打开Tools栏后直接按k即可。
打开的界面如下:
之后选择创建一个新的外设IP核。
修改IP核的名称等基本信息,这里随便取了一个名称ip1_pwm。
设置外设与PS部分的数据接口,以及接口的基本属性。
这里无需改动,但是需要注意这里的Interface Type选项,里面包括了Lite、Full以及Stream选项,代表了不同的AXI总线,之后将对这部分进行展开,使用不同的总线传输需要的数据。具体的功能如图所示:
接口协议 | 特性 | 应用场合 |
---|---|---|
AXI4-Lite | 地址、单数据传输 | 低速外设或控制 |
AXI4 | 地址、突发数据传输 | 地址的批量传输 |
AXI4-Stream | 仅传输数据,突发传输 | 数据流和媒体流传输 |
在IP Catalog中找到刚刚创建的IP核,并对它进行编辑,之后会进入一个新的Vivado项目中,即IP核的项目。
在Design Source里新建一个verilog文件,完成自己的module的编写。
首先先考虑需要的IP核需要有哪些功能:
需要的接口:
代码如下:
`timescale 1ns / 1ps
module led #(
parameter N = 32
)
(
input wire clk,
input wire rst,
input wire [N-1:0] period,
input wire [N-1:0] duty,
output wire ledOut
);
reg [N-1:0] regPeriod;
reg [N-1:0] regDuty;
reg [N-1:0] regCnt;
reg regLedOut;
assign ledOut = regLedOut;
always@(posedge clk, posedge rst) begin
if(rst) begin
regPeriod <= {N{1'b0}};
regDuty <= {N{1'b0}};
end
else begin
regPeriod <= period;
regDuty <= duty;
end
end
always@(posedge clk, posedge rst) begin
if(rst) begin
regCnt <= {N{1'b0}};
end
else begin
regCnt <= regCnt + regPeriod;
end
end
always@(posedge clk, posedge rst) begin
if(rst) begin
regLedOut <= 1'b1;
end
else begin
if(regCnt >= regDuty)
regLedOut <= 1'b1;
else
regLedOut <= 1'b0;
end
end
endmodule
这一部分参考了我的ZYNQ开发板的教程里的一段代码。
这里可以看到,IP核中间存在两层封装。
第一层是ip1_pwm_v1_0.v,这一层是未来我们需要直接调用这个IP核用到的,这里面的接口都是直接与其他IP核相连接的。这个函数本质上就是把外部的接口经过封装后,调用了ip1_pwm_v1_0_S00_AXI函数。其中无论是时钟还是复位引脚都已经有相关定义。数据部分也通过AXI4-Lite总线部分取得,这里也不需要自己去定义。这里仅需要定义一个输出引脚,用以控制一个LED灯。在这里写入这一行代码即可。
然后需要将定义的led_out接口连接到ip1_pwm_v1_0_S00_AXI上去,所以在调用ip1_pwm_v1_0_S00_AXI的部分,将out_led接口传入该函数
第二层是ip1_pwm_v1_0_S00_AXI函数,这一部分代表着函数的核心功能,函数的大部分都是在实现AXI4-Lite总线的通信部分。
首先需要注意在上一级中,通过out参数将一个out_led的接口接到了该函数内,故而需要在次函数内对out进行定义。
然后将我们的功能核心代码写入到该函数内。
直接调用此函数即可,其中,对于复位引脚,这里的定义如下:
// Global Reset Signal. This Signal is Active LOW
input wire S_AXI_ARESETN,
当激活时为低电平,这与我们的定义正好完全相反,故需对这一引脚取反使用。
针对period和duty直接使用AXI4-Lite总线传输数据的寄存器即可。该寄存器最大位数为32位。
至此,我们针对IP核的修改已经完成了,之后需要让它自动生成重新封装即可。
插入刚刚自己定义的IP核,并自动连接,将自己定义的out引脚引到外部。最后完整的系统框图如下图:
待完成后,打包成wrapper HDL即可。
这里我遇到一个特别有意思的bug,这里的ZYNQ_PROCESSING_SYSTEM的DDR选项会莫名其妙的被换为默认值,导致在后面调试的过程中无法正常工作,我也不知道这种现象产生的原因,但是这里一定需要重新检查一遍该部分。
这个引脚约束文件比较简单:
set_property IOSTANDARD LVCMOS33 [get_ports out_led_0]
set_property PACKAGE_PIN B8 [get_ports out_led_0]
之后生成Bitstream文件后,导出硬件平台信息后。
这一部分有两个文件比较关键xparameters.h 以及之前自定义的ip1_pwm.h文件。
这个程序的本质就是在恰当的时间去改变寄存器的值,而这两个文件就是起到这个功能的。
我们需要用的函数仅有一个:
IP1_PWM_mWriteReg(
BaseAddress,
RegOffset,
Data
); //往寄存器内写入值
具体的实现代码如下:
#include "xparameters.h"
#include "ax_pwm.h"
#include "sleep.h"
#define FREQ 200
int main(){
u32 period;
u32 duty;
int i;
period = (2 << 25)/ (390625) * FREQ;
AX_PWM_mWriteReg(XPAR_AX_PWM_0_S00_AXI_BASEADDR, AX_PWM_S00_AXI_SLV_REG0_OFFSET, period);
while(1){
for(duty=0x0; duty<0xFFEFFFFF; duty=duty+0xFFFFF){
AX_PWM_mWriteReg(XPAR_AX_PWM_0_S00_AXI_BASEADDR, AX_PWM_S00_AXI_SLV_REG1_OFFSET, duty);
usleep(100);
}
i++;
for(duty=0xFFFFFFFF; duty>0x100000; duty=duty-0xFFFFF){
AX_PWM_mWriteReg(XPAR_AX_PWM_0_S00_AXI_BASEADDR, AX_PWM_S00_AXI_SLV_REG1_OFFSET, duty);
usleep(100);
}
}
return 0;
}
这里可能还需要解释一下 那个 2 25 396025 \frac{2^{25}}{396025} 396025225的部分,这里实际上就是 2 32 50 × 1 0 6 \frac{2^{32}}{50\times10^6} 50×106232,这样就比较好理解了。
以上,该部分完成。
这里实际上一直有一个bug,以上的步骤应该是没有问题的,但是我这里碰到了在一些项目内可以使用,在一些项目内不可使用的情况。而且问题可以肯定是出现在ZYNQ芯片上电启动这一部分的,因为SDK中main函数可以正常运行,但是无法按照计划点亮LED灯。
这里也可以肯定,XDC文件与PL创建的IP核没有问题,在IP核最开始创建的部分,我选择当程序复位时,LED会被点亮,则在调试时,LED灯会被点亮不到1s,然后就不再有任何的反应。当DDR被设置错误时,如果不直接报错,该程序也是同样的现象。
该现象针对某一项目一直存在,但是与成功的项目进行对比,在修改的文件中,两项目完全一致。这一问题现在仅能被记录,未被解决。
以上就是PL与PS数据共享第一部分。