【Arty-A7填坑笔记】03:为Microblaze定制AXI总线接口的PWM外设
0.前言
0.1.自定义IP
上回我们使用了Microblaze软核使用AXI-GPIO驱动LED以及RTL计数器分频驱动LED两种方法完成了点灯。这次我们还是尝试一个玩单片机的保留项目——呼吸灯。
不过呢,暂且不用Xilinx提供的定时器IP,我们使用Vivado的自定义IP功能,设计一个PWM模块给Microblaze调用,在C代码设计的程序中循环改变PWM的脉宽,实现呼吸灯效果。
顺便,学习自定义IP就可以方便地将FPGA的高速、并行处理能力与软核方便编写繁杂代码的优点相结合,快速做出更多好玩的东西啦。
0.2.资源链接
本次自定义IP的配置主要参考了正点原子编写的《达芬奇之Microblaze开发指南》的相关章节,不过略有改动,且由直接建立IP工程改为了在主工程界面中建立IP,大家可以对比参考。该教程可以在这里找到:
http://www.alientek.com/
Xilinx文档UG1119
“Creating and PackagingCustom IP”:
https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_2/ug1118-vivado-creating-packaging-custom-ip.pdf
Xilinx文档XAPP1168
“Packaging Custom AXI IP for Vivado IP Integrator”:
https://www.xilinx.com/support/documentation/application_notes/xapp1168-axi-ip-integrator.pdf
1.设计过程
1.1.硬件及逻辑设计
本次FPGA部分的工程结构和上次的大致相同,可直接将上个工程另存为到这次的工程目录下,修改使用:
在新的工程中,打开Block Design,在顶部导航栏点击Tools -> Create and Package New IP…:
选择建立一个带有AXI4接口的外设,方便Microblaze操作:
对IP进行命名及标注。第一次新建IP时,默认会在工程目录上一层目录建立ip_repo
文件夹,即IP仓库,之后建立的IP文件都会放在这里。当然也可选择别的路径,同时也可在设置中添加用户IP的查找路径。
在AXI4接口配置的界面,对于此次应用可以保持默认。本次建立的是一个带有AXI4-Lite接口的从机设备,有四个AXI4-Lite可访问的寄存器:
保持在第一项:将IP添加到仓库,一会儿我们再进行IP的修改:
打开IP Catalog,已经可以看到我们的IP了:
在IP上右键,点选Edit in IP Packager,准备修改:
弹出Edit in IP Packager窗口,确认无误可点击OK。之后会自动打开一个临时Vivado工程,用于IP的修改与配置:
工程中已经为我们生成了两个文件,分别是myip_pwm_v1_0.v
和myip_pwm_v1_0_S00_AXI_inst.v
,前者是IP核的顶层文件,后者是实现AXI4-Lite接口逻辑的模块,前者例化了后者,而我们编写的功能模块,则需要被接口模块例化。
接口模块提供了一套读取及修改之前接口配置页面选择的四个寄存器的功能,将这四个寄存器按照设计的功能与我们自己的模块连接即可。那么就可以尽情发挥了。右键Design Source添加或创建源文件:
新建一个Verilog文件,这里我命名为custom_pwm.v
:
存放路径选择在IP目录下的hdl
文件夹里就好(别处也可以):
接着再创建一个Verilog文件custom_pwm_tb.v
用于我们设计的PWM模块的仿真:
custom_pwm.v
文件中添加了如下代码。输入接口中,psc
用于设置预分频系数,实际上是一个计数器的溢出值,用于将输入时钟分频再驱动PWM的主计数器,以实现更大范围可调的PWM频率;arr
用于设计PWM的主计数器溢出值,与psc
一同决定PWM频率;cmp
用于设定比较值,其将与PWM的主计数器值进行比较,以完成PWM输出,实际上这个输入决定了PWM的占空比。
`timescale 1ns / 1psPWM
// 注释略
module custom_pwm (
input clk,
input rst_n,
input w_en,
input [15:0] psc,
input [15:0] arr,
input [15:0] cmp,
output pwm
);
// Regist inputs
reg [15:0] psc_reg; // Prescale register
reg [15:0] arr_reg; // Accumulate register
reg [15:0] cmp_reg; // Compare register
always @(posedge clk) begin
if (!rst_n) begin
psc_reg <= 16'd0;
arr_reg <= 16'd0;
cmp_reg <= 16'd0;
end else begin
if (w_en) begin
psc_reg <= psc;
arr_reg <= arr;
cmp_reg <= ccr;
end else begin
psc_reg <= psc_reg;
arr_reg <= arr_reg;
cmp_reg <= cmp_reg;
end
end
end
// Prescale
reg [15:0] psc_cnt;
wire psc_out;
always @(posedge clk) begin
if (!rst_n) begin
psc_cnt <= 16'd0;
end else begin
if (psc_cnt < psc_reg - 1) begin
psc_cnt <= psc_cnt + 1'b1;
end else begin
psc_cnt <= 16'd0;
end
end
end
assign psc_out = (psc_cnt == psc_reg - 1) ? 1'b1 : 1'b0;
// Count and auto-reload
reg [15:0] main_cnt;
always @(posedge clk) begin
if (!rst_n) begin
main_cnt <= 16'd0;
end else begin
if (psc_out) begin
if (main_cnt < arr_reg - 1) begin
main_cnt <= main_cnt + 1'b1;
end else begin
main_cnt <= 16'd0;
end
end else begin
main_cnt <= main_cnt;
end
end
end
// Compare and output
assign pwm = (main_cnt >= cmp_reg) ? 1'b0 : 1'b1;
endmodule
custom_pwm.v
文件中添加了如下代码,用于简略测试custom_ip
模块,确定它可以实现预期的功能。
`timescale 1ns / 1psPWM
// 注释略
module custom_pwm_tb ();
reg clk;
reg rst_n;
reg w_en;
reg [15:0] psc;
reg [15:0] arr;
reg [15:0] cmp;
wire pwm;
custom_pwm
u_custom_pwm (
.clk (clk ),
.rst_n (rst_n ),
.w_en (w_en ),
.psc (psc ),
.arr (arr ),
.cmp (cmp ),
.pwm (pwm )
);
always #10 clk <= ~clk;
initial begin
clk <= 1'b0;
rst_n <= 1'b0;
w_en <= 1'b0;
psc <= 15'd4;
arr <= 15'd8;
cmp <= 15'd1;
#20
rst_n <= 1'b1;
#30
w_en <= 1'b1;
#40
w_en <= 1'b0;
#2000
cmp <= 15'd7;
#2100
w_en <= 1'b1;
#2200
w_en <= 1'b0;
end
endmodule
分别右键我们自己建立的两个Verilog文件,点击Set as Top暂且设置为顶层文件,因为我们接下俩仅针对自己设计的模块部分进行仿真:
在左侧导航栏点击Run Simulation -> Run Behavioral Simulation打开仿真页面,使用对应操作按键即可进行仿真:
右键可关闭仿真界面,准备将PWM模块接入AXI4-Lite接口:
不要忘了将myip_pwm_v1_0.v
文件重新设为顶层文件:
先打开接口模块文件myip_pwm_v1_0_S00_AXI_inst.v
,在18行即注释提示的位置加入将要对外界引出的信号pwm
:
同样,在400行的注释提示处例化我们的模块,将AXI4-Lite中提供的时钟、复位及寄存器接入,四个寄存器正好用上,其中slv_reg0
的最低位用于使能PWM模块(其实这四个寄存器都是32位的,这里只使用了低16位):
接下来打开顶层文件myip_pwm_v1_0.v
,同在18行即注释提示的位置加入将要对外界引出的信号pwm
:
在46行处,代码例化了接口模块,这里一样要将PWM信号引出:
全部保存后,模块例化关系应该是下面这样:
在左侧导航栏点击Run Synthesis进行综合,验证一下有没有错误,完成后如下,不用进行Implementation,关闭即可:
转到IP配置界面,也可通过双击\IP-XACT\component.xml
打开:
在File Groups点击Merge changes from File Groups Wizard,这样会重新生成IP的相关文件,注意其中还包含软件驱动代码。这些都位于IP的目录下:
GUI界面,可以预览IP在Block Design中的外形,以及调整引脚的排列顺序:
此页面的配置基本保持默认即可,详细的功能可参考资源链接处的文档。全部打勾完成后,点击Re-Packa IP完成设计,弹出提示确认后将关闭这个临时工程:
刷新一下IP目录以完成修改:
这里也是完成一些更新:
现在可以在Block Design 中添加自定义的PWM模块了:
双击加入,点击自动连接:
完成连接并引出接口,可见myip_pwm_0
模块已经通过AXT总线连接到了总线互联器上,就和其他几个AXI外设一样:
之后就是综合,约束管脚,生成比特流的套路了。我在综合是遇到了这样一个报错,提示工程路径下的某文件不存在。推测原因是一层层文件夹加起来的路径实在太长导致的,不得已将工程移到了磁盘根目录综合,这次便没有报错了(真要命)。
管脚约束如下,添加pwm
的约束即可,我把它连到了另一颗RGBLED的一个颜色上:
生成比特流后,即可准备导出硬件进行软件设计了。
1.2.软件设计
由于工程是从上次的另存为来的,所以SDK文件也一同保留了过来,偷个懒可以直接将之前的内容都删除:
导出硬件至此,开启SDK,可以看到我们的硬件平台信息,同时可见自定义的AXI外设被分配的地址:
和上篇一样建立Hello world 工程来修改使用,与上个工程相比,添加myip_pwm.h
,这是打包IP时自动生成的驱动代码,且在通过硬件平台文件建立工程后会自动包含:
在xparameters.h
文件中可以看到相关AXI总线地址的定义:
在myip_pwm.h
文件中可以看到四个寄存器的偏移地址定义:
helloworld.h
中的完整代码如下:
#include
#include "platform.h"
#include "xil_printf.h"
#include "sleep.h"
#include "xparameters.h"
#include "xgpio.h"
#include "myip_pwm.h"
/* RGB LED 0 ----------------------------------------------------------------*/
#define RGB0_ID XPAR_GPIO_0_DEVICE_ID
#define RGB0_CH 1
XGpio Rgb0Gpio;
/* Custom PWM ---------------------------------------------------------------*/
#define PWM_IP_BASE XPAR_MYIP_PWM_0_S00_AXI_BASEADDR
#define PWM_IP_RCFG MYIP_PWM_S00_AXI_SLV_REG0_OFFSET
#define PWM_IP_RPSC MYIP_PWM_S00_AXI_SLV_REG1_OFFSET
#define PWM_IP_RARR MYIP_PWM_S00_AXI_SLV_REG2_OFFSET
#define PWM_IP_RCCR MYIP_PWM_S00_AXI_SLV_REG3_OFFSET
int main() {
uint16_t time_ms;
uint8_t pwm_inc_dir;
uint16_t pwm_duty;
init_platform();
print("Custom IP Test: \n\r");
// RGB0 initialize
XGpio_Initialize(&Rgb0Gpio, RGB0_ID);
XGpio_SetDataDirection(&Rgb0Gpio, RGB0_CH, 0); // Set as output
// Custom PWM initialize
MYIP_PWM_mWriteReg(PWM_IP_BASE, PWM_IP_RCFG, 0x0001); // Enable
MYIP_PWM_mWriteReg(PWM_IP_BASE, PWM_IP_RPSC, 100); // Set prescale to 100
MYIP_PWM_mWriteReg(PWM_IP_BASE, PWM_IP_RARR, 1000); // Set period to 1000
MYIP_PWM_mWriteReg(PWM_IP_BASE, PWM_IP_RCCR, 500); // Set duty to 50%
while (1) {
// RGB0 is shifted every 500ms
// PWM duty is updated every 10ms
// (Approximately)
for (time_ms = 0; time_ms < 1500; time_ms += 10) {
// Shift RGB0
XGpio_DiscreteWrite(&Rgb0Gpio, RGB0_CH, 0x01 << (time_ms/500));
// Increase or decrease PWM duty
if (pwm_inc_dir == 0) {
pwm_duty = pwm_duty + 10;
} else {
pwm_duty = pwm_duty - 10;
}
if (pwm_duty == 1000) {
pwm_inc_dir = 1;
} else if (pwm_duty == 0) {
pwm_inc_dir = 0;
}
// Write duty value to custom PMW IP
MYIP_PWM_mWriteReg(PWM_IP_BASE, PWM_IP_RCCR, pwm_duty);
// Delay 10ms
usleep(10*1000);
}
}
cleanup_platform();
return 0;
}
编译无误后运行,在配置中可能要重新指定一下硬件比特流的路径,如下所示:
下载,固化等和上期相同。最终的效果如下,除上期的两款流水灯之外,LD2中的绿灯呼吸闪烁:
2.总结
自定义IP的完成,FPGA的RTL逻辑部分和软核完美结合了。那么在下一期中,就该解锁板上的DDR3内存,配合DMA,为软核与RTL逻辑带来更大的数据处理空间。不过,这同时也会引出QSPI接口的使用与Bootloader的制作以及更多的坑点,那么下期再见吧。