软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述
XILINX ZYNQ以及ZYNQ MPSOC相比其他厂家的异构ARM+FPGA,独领风骚。其中关键非常关键的一点使用了AXI总线进行高速互联。而且这个AXI总线是开放给我们用户使用的。在前面我们使用的AXI-GPIO、AXI-IIC、AXI-UART等IP方案中都使用到了AXI总线对FPGA部分的IP互联到AXI总线,因为ARM的CPU也是互联到AXI总线,这样FPGA和ARM就可以交互数据了。
常用的AXI总线包括AXI4-Lite,AXI4-FULL和AXI4-Stream三种总线协议,需要注意的是PS与PL之间的接口(AXI-GP接口,AXI-HP接口以及AXI-ACP接口)只支持AXI-Lite和AXI-FULL后面默认AXI就是AXI-FULL)协议这两种总线协议。也就是说PL这边的AXI-Stream的接口是不能直接与PS对接的,需要经过AXI4或者AXI4-Lite的转换。比如后面将用到的VDMA IP ,它就实现了在PL内部AXI4到AXI-Stream的转换,VDMA利用的接口就是AXI-HP接口。
本文首先介绍AXI总线协议、AXI-Stream协议、AXI-lite协议,然后本文以AXI-Lite作为demo。我们在后续的文章中,会单独开辟一个章节,详细介绍AXI4-FULL 、AXI-LITE 、AXI-Stream。
本文实验目的:
学习AXI总线协议包括AXI-FULL、AXI-Lite、AXI-Stream 掌握基于VIVADO工具产生AXI协议模板 掌握通过VIVADO工具产生AXI-lite-Slave代码,并且会修改寄存器 理解AXI-lite-Slave中自定义寄存器的地址分配 掌握通过VIVADO封装AXI-LITE-SLAVE 图形化IP 掌握通过VITIS-SDK访问AXI-LITE-SLAVE的寄存器
2 系统框图
3 AXI总线协议介绍
3.1 AXI总线概述
在ZYNQ Ultrascale+中有支持三种AXI总线,拥有三种AXI接口,当然用的都是AXI协议。其中三种AXI总线分别为:
AXI4:(For high-performance memory-mapped requirements.)主要面向高性能地址映射通信的需求,是面向地址映射的接口,允许最大256轮的数据突发传输;
AXI4-Lite:(For simple, low-throughput memory-mapped communication )是一个轻量级的地址映射单次传输接口,占用很少的逻辑单元。
AXI4-Stream:(For high-speed streaming data.)面向高速流数据传输;去掉了地址项,允许无限制的数据突发传输规模。
AXI4总线和AXI4-Lite总线具有相同的组成部分:
(1)读地址通道,包含ARVALID, ARADDR, ARREADY信号;
(2)读数据通道,包含RVALID, RDATA, RREADY, RRESP信号;
(3)写地址通道,包含AWVALID,AWADDR, AWREADY信号;
(4)写数据通道,包含WVALID, WDATA,WSTRB, WREADY信号;
(5)写应答通道,包含BVALID, BRESP, BREADY信号;
(6)系统通道,包含:ACLK,ARESETN信号。
AXI4总线和AXI4-Lite总线的信号也有他的命名特点:
读地址信号都是以AR开头(A:address;R:read)
写地址信号都是以AW开头(A:address;W:write)
读数据信号都是以R开头(R:read)
写数据信号都是以W开头(W:write)
应答型号都是以B开头(B:back(answer back))
了解到总线的组成部分以及命名特点,那么在后续的实验中您将逐渐看到他们的身影。每个信号的作用暂停不表,放在后面一一介绍。
AXI4-Stream总线的组成有:
(1)ACLK信号:总线时钟,上升沿有效;
(2)ARESETN信号:总线复位,低电平有效
(3)TREADY信号:从机告诉主机做好传输准备;
(4)TDATA信号:数据,可选宽度32,64,128,256bit
(5)TSTRB信号:每一bit对应TDATA的一个有效字节,宽度为TDATA/8
(6)TLAST信号:主机告诉从机该次传输为突发传输的结尾;
(7)TVALID信号:主机告诉从机数据本次传输有效;
(8)TUSER信号 :用户定义信号,宽度为128bit。
对于AXI4-Stream总线命名而言,除了总线时钟和总线复位,其他的信号线都是以T字母开头,后面跟上一个有意义的单词,看清这一点后,能帮助读者记忆每个信号线的意义。如TVALID = T+单词Valid(有效),那么读者就应该立刻反应该信号的作用。每个信号的具体作用,在后面分析源码时再做分析
3.2 AXI接口介绍
ZYNQ MPSOC的AXI总线接口可以分为Master接口和Slave接口:
AXI-Master接口包括了 ,HPM0 FPD、HPM1 FPD 、HPM0 LPD
ZYNQ MPSOC的AXI-SLAVE非常丰富,包括了:HPC0 FPD、HPC1 FPD、HP0 FPD、HP1 FPD、HP2 FPD、HP3 FPD、AXI LPD、AXI ACP 、AXI ACE
我们可以双击查看ZYNQ Ultrascale+的IP核的内部配置,图中红色线框内为PS和PL互联的AXI总线接口部分。
3.3 AXI协议概述
协议和总线关系密切,协议要在总线的结构上制定。虽然说AXI4,AXI4-Lite,AXI4-Stream都是AXI4协议,但是各自细节上还是不同的。
总的来说,AXI总线协议的两端可以分为分为主(master)、从(slave)两端,他们之间一般需要通过一个AXI Interconnect相连接,作用是提供将一个或多个AXI主设备连接到一个或多个AXI从设备的一种交换机制。当我们添加了zynq以及带AXI的IP后再进行自动连线时,vivado会自动帮我们添加上这个IP,大家应该是不陌生了。
AXI Interconnect的主要作用是,当存在多个主机以及从机器时,AXI Interconnect负责将它们联系并管理起来。由于AXI支持乱序发送,乱序发送需要主机的ID信号支撑,而不同的主机发送的ID可能相同,而AXI Interconnect解决了这一问题,他会对不同主机的ID信号进行处理让ID变得唯一。
AXI协议将读地址通道,读数据通道,写地址通道,写数据通道,写响应通道分开,各自通道都有自己的握手协议。每个通道互不干扰却又彼此依赖。这也是AXI高效的原因之一。
3.4 AXI协议之握手协议
AXI4所采用的是一种READY,VALID握手通信机制,简单来说主从双方进行数据通信前,有一个握手的过程。传输源产生VLAID信号来指明何时数据或控制信息有效。而目地源产生READY信号来指明已经准备好接受数据或控制信息。传输发生在VALID和READY信号同时为高的时候。VALID和READY信号的出现有三种关系。
(1)VALID先变高READY后变高。时序图如下:
在箭头处信息传输发生。
(2)READY先变高VALID后变高。时序图如下:
同样在箭头处信息传输发生。
(3)VALID和READY信号同时变高。时序图如下:
在这种情况下,信息传输立马发生,如图箭头处指明信息传输发生。
需要强调的是,AXI的五个通道,每个通道都有握手机制,接下来我们就来分析一下AXI-Lite的源码来更深入的了解AXI机制。
3.5 突发式读写
1:突发式读的时序图如下:
当地址出现在地址总线后,传输的数据将出现在读数据通道上。设备保持VALID为低直到读数据有效。为了表明一次突发式读写的完成,设备用RLAST信号来表示最后一个被传输的数据。
2:突发式写时序图如下:
这一过程的开始时,主机发送地址和控制信息到写地址通道中,然后主机发送每一个写数据到写数据通道中。当主机发送最后一个数据时,WLAST信号就变为高。当设备接收完所有数据之后他将一个写响应发送回主机来表明写事务完成。
4 自定义AXI4-Lite IP CORE
4.1 AXI4-Lite源码查看
建立一个自定义AXI-Lite的IP,查看AXI-Lite的源码。
1:打开VIVADO软件,新建一个工程。
2:单击Tools à Create and Package NEW IP 。
3:单击Next,选择Create a new AXI4 peripheral,单击Next。
4:输入要创建的IP名字,此处命名为myip(名字尽量和最终自己的IP名字一致,后面还可以修改),选择保存路径,单击Next。
5: Name à S00_AXI ;
Interface Type(接口类型) à Lite ;
Data Width(Bits)(数据位宽) à 32 位;
Number of Registers(寄存器数量) à 4 ;单击 next 。
6:选择Edit IP,点击Finish按钮。将打开一个编辑IP的工程。以下图片中,用户也可以选择Verify Peripheral IP using AXI4 VIP,如果选择这个这个选型可以对IP做一个软件仿真。我们的视频教程中有这个步骤。
7:此后,Vivado会新建一个工程,专门编辑该IP,通过该工程,就可以看到Vivado生成的AXI-Lite的操作源码。
不过这个源码看起来不舒服,一般我们很少对每个源码用版本命名,所以本人喜欢修改下名字。把源码修改后如下,当然源码的接口名字也要改下:
然后重新添加进来,初学不会的读者可以打开我们源码对比下
4.2 AXI-Lite 源码分析
当打开顶层文件,即myip。首先看到的是一堆AXI端口信号,这些信号是否似曾相识?
// Ports of Axi Slave Bus Interface S00_AXI
input wire s00_axi_aclk,
input wire s00_axi_aresetn,
input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr,
input wire [2 : 0] s00_axi_awprot,
input wire s00_axi_awvalid,
output wire s00_axi_awready,
input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata,
input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 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 [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr,
input wire [2 : 0] s00_axi_arprot,
input wire s00_axi_arvalid,
output wire s00_axi_arready,
output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata,
output wire [1 : 0] s00_axi_rresp,
output wire s00_axi_rvalid,
input wire s00_axi_rready
没错笔者曾在《AXI总线概述》这节中提到了他们,这次通过源码分析再次隆重介绍它们。
读
通
道
地址通道
数据通道
读地址有效。此信号表明该信道此时能有效读出地址和控制信息
RVALID
读数据有效。此信号表明该信道此时能有效读出数据
读地址准备好了。该信号指示从器件准备好接受一个地址和相关联的控制信号
保护类型。这个信号表示该事务的特权和安全级别,并确定是否该事务是一个数据存取或指令的访问
写地址有效。这个信号表示该主信令有效的写地址和控制信息。
写有效。这个信号表示有效的写数据和选通信号都可用。
BVALID
写响应有效。此信号表明写命令的有效写入响应。
写地址准备好了。该信号指示从器件准备好接受一个地址和相关联的控制信号
写选通。这个信号表明该字节通道持有效数据。每一bit对应WDATA一个字节
写通道保护类型。这个信号表示该事务的特权和安全级别,并确定是否该事务是一个数据存取或指令的访问
Vivado为我们生成的AXI-Lite的操作源码,是一个模板,我们只需要读懂它,然后稍加修改,就可以在实际工程使用。
我们先来看一段源码中与WDATA相关的代码:
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
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
2'h2:
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 2
slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
2'h3:
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 3
slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
default : begin
slv_reg0 <= slv_reg0;
slv_reg1 <= slv_reg1;
slv_reg2 <= slv_reg2;
slv_reg3 <= slv_reg3;
end
endcase
end
end
end
这段程序的作用:
当PS向AXI4-Lite总线写数据时,将从总线上传输过来的数据存储到寄存器slv_reg。而slv_reg寄存器有0~3共4个。至于赋值给哪一个由
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]决定,根据宏定义其实就是由axi_awaddr[3:2] (写地址中不仅包含地址,而且包含了控制位,这里的[3:2]就是控制位)决定赋值给哪个slv_reg。
PS调用写函数时,如果不做地址偏移的话,axi_awaddr[3:2]的值默认是为0的,举个例子,如果我们自定义的IP的地址被映射为0x43C00000,那么我们Xil_Out32(0x43C00000,Value)写的就是slv_reg0的值。如果地址偏移4位,如Xil_Out32(0x43C00000 + 4,Value) 写的就是slv_reg1的值,依次类推。
本例分析时只关注slv_reg0(其他几个寄存器结构相同,分析方法相同):
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
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
其中,C_S_AXI_DATA_WIDTH的宏定义的值为32,也就是数据位宽;S_AXI_WSTRB是写选通信号,S_AXI_WDATA就是写数据信号。
存在于for循环中的最关键的一句:
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
当byte_index = 0的时候这句话就等价于:
slv_reg0[7:0] <= S_AXI_WDATA[7:0];
当byte_index = 1的时候这句话就等价于:
slv_reg0[15:8] <= S_AXI_WDATA[15:8];
当byte_index = 2的时候这句话就等价于:
slv_reg0[23:16] <= S_AXI_WDATA[23:16];
当byte_index = 3的时候这句话就等价于:
slv_reg0[31:24] <= S_AXI_WDATA[31:24];
也就是说,只有当写选通信号为1时,它所对应S_AXI_WDATA的字节才会被读取。
读懂了这段话之后,我们就知道了,如果我们想得到PS写到总线上的数据,我们只需要读取slv_reg0的值即可。
那么如果,我们想写数据到总线让PS读取该数据,我们该怎么做呢?我们继续来看有关RADTA读数据代码:
// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rdata <= 0;
end
else
begin
// When there is a valid read address (S_AXI_ARVALID) with
// acceptance of read address by the slave (axi_arready),
// output the read dada
if (slv_reg_rden)
begin
axi_rdata <= reg_data_out; // register read data
end
end
end
观察可知,当PS读取数据时,程序会把reg_data_out复制给axi_rdata(RADTA读数据)。我们继续追踪reg_data_out:
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
和前面分析的一样此时通过判断axi_awaddr[3:2]的值来判断将那个值给reg_data_out上,同样当PS调用读取函数时,这里axi_awaddr[3:2]默认是0,所以我们只需要把slv_reg0替换成我们自己数据,就可以让PS通过总线读到我们提供的数据。
这里可能有的读者会问了,slv_reg0不是总线写过来的数据吗?因为笔者说过这个程序是Vivado为我们提供的例子,它这么做无非是想验证写出去的值和读进入的值相等。但是他怎么写确实会对初看代码的人造成困扰。
最后笔者提出一个问题,为什么写通道要比读通道多了一列应答通道,这是为什么呢?
首先,你要知道这个应答信号是干什么用的?
写应答,主要是回复主机你这个写过程是没有问题的,那读为什么不需要这个过程呢?
这时因为主机在读取数据时,从机可以直接通过读数据通道给主机反馈信息,因此就没有必要再来开辟一个单独的应答通道了。
小结:
如果我们想读AXI4_Lite总线上的数据时,只需关注slv_reg的数据,我们可自行添加一段代码,如:
reg [11:0]rlcd_rgb;
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
rlcd_rgb <= 12'd0;
end
else
begin
rlcd_rgb <= slv_reg0[11:0];
end
end
assign lcd_rgb = rlcd_rgb;
如果我们想对AXI4_Lite信号写数据时,我们只需修改对reg_data_out的赋值,如:
//写总线测试修改!!!!!!!!!
wire[31:0]wlcd_xy;// = {10'd0,lcd_xy};
assign wlcd_xy = {10'd0,lcd_xy};
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 <= wlcd_xy;//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
4.3 打包成图形IP
可以根据自己源码的修改指定版本号
一般我们设置兼容VIVADO支持的芯片的所有系列
文件类型
用户自定义参数
可以观察下自动识别的信号,以及信号类型
地址设置,一般用不到
这个页面可以定制GUI目前这个IP用默认的就可以
单击Re-Package IP 重新打包IP
5 搭建SOC系统工程
详细的搭建过程这里不再重复,对于初学读者如果还不清楚如何创建SOC工程的,请学习“01Vitis Soc开发入门”这篇文章。
5.1 SOC系统工程
1:中断设置
本实验可以不设置中断
2:设置GP Master接口
3:设置复位输出
4:设置PL时钟
5:添加自定义IP
设置自定义IP路径,并且添加IP
6:添加ILA
右击需要添加ILA在下逻辑分析仪的总线,选择Debug
根据提示选择自动连线
5.2设置AXI外设地址分配
只要添加的AXI总线外设都要正确分配地址,这一步不能遗漏
5.3 编译并导出平台文件
以下步骤简写,有不清楚的看Linux基础篇第四章。
1:打开soc_prj内工程。
2:生成Bit文件。
3:导出到硬件: File à Export Hardware à Include bitstream
4:导出完成后,对应工程路径的soc_hw路径下有硬件平台文件:system_wrapper.xsa的文件。根据硬件平台文件system_wrapper.xsa来创建需要Platform平台。
5:打开vitis,并添加设备树模板,不清楚请参考3-4-1Linux入门第四课。
6:创建工程文件,选择device tree创建,创建完成后编译工程。
7:获得设备树以及启动文件,打开虚拟机将文件拷贝到开发包的指定位置。
5.4 设备树修改及驱动编译
1:设备树修改
本章vitis会生成pl端的设备树,需要手动添加。另外soc_dts内有写好的设备树,新手请不要自行修改,直接使用教程提供的设备树。设备树的修改如下:
这次直接使用vitis生成的设备树来写驱动,所以不对生成的设备树进行修改。
2:编译系统
拷贝上一步写好的设备树到指定路径,拷贝其他文件后,编译uboot,编译kernel,制作镜像并烧录系统。具体步骤参考Linux基础篇第四课。
3:编译驱动
将对应的demo拷贝至/home/uisrc下,同时确保/home/uisrc下有软件开发包uisrc-lab-xlnx。
进入demo的路径,使用make编译驱动。
其中.ko结尾的文件为我们编译出来的驱动。
4:拷贝程序
将对应的文件拷贝至sd卡上:
6 程序分析
这个驱动需要根据设备树来编写驱动,所以采用了一个比较熟悉的开发模式,即基于设备树的平台驱动开发。
#include <linux/ide.h>
#include <linux/module.h>
#include <linux/of_platform.h>
struct AxiLite
{
int addr_width;
int data_width;
struct device_node *dev_node;
int reg_num;
unsigned int *reg_value;
};
struct AxiLite *AxiLite_data;
static unsigned int *AxiData_0 = NULL;
static unsigned int *AxiData_1 = NULL;
static unsigned int *AxiData_2 = NULL;
static unsigned int *AxiData_3 = NULL;
static struct of_device_id AxiLite_of_match[] = {
{.compatible = "xlnx,myip-1.0"},
{},
};
int of_AxiLite_data(struct AxiLite *pdata, struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
int ret = 0;
pdata->reg_num = of_property_count_elems_of_size(np, "reg", sizeof(int));
if (pdata->reg_num < 0)
{
dev_err(&pdev->dev, "get reg_num failed\n");
return ret;
}
pdata->reg_value = (unsigned int *)kmalloc(sizeof(unsigned int) * pdata->reg_num, GFP_KERNEL);
if (!pdata->reg_value)
{
kfree(pdata->reg_value);
dev_err(&pdev->dev, "kmalloc failed\n");
return -1;
}
ret = of_property_read_u32_array(np, "reg", pdata->reg_value, pdata->reg_num);
if (ret != 0)
{
kfree(pdata->reg_value);
dev_err(&pdev->dev, "get reg failed\n");
return ret;
}
ret = of_property_read_u32(np, "xlnx,s00-axi-addr-width", &pdata->addr_width);
if (ret < 0)
{
dev_err(&pdev->dev, "get addr_width failed\n");
return ret;
}
ret = of_property_read_u32(np, "xlnx,s00-axi-data-width", &pdata->data_width);
if (ret < 0)
{
dev_err(&pdev->dev, "get data_width failed\n");
return ret;
}
return 0;
}
static int AxiLite_probe(struct platform_device *pdev)
{
int ret = 0;
int i = 0;
struct device *dev = &pdev->dev;
struct AxiLite *pdata = dev_get_platdata(dev);
if (!pdata)
{
pdata = devm_kzalloc(dev, sizeof(struct AxiLite), GFP_KERNEL);
if (!pdata)
return -ENOMEM;
platform_set_drvdata(pdev, pdata);
}
ret = of_AxiLite_data(pdata, pdev);
AxiLite_data = pdata;
// 打印属性值
printk("addr_width = %d\r\n", AxiLite_data->addr_width);
printk("data_width = %d\r\n", AxiLite_data->data_width);
printk("reg_num = %d\r\n", AxiLite_data->reg_num);
for (i = 0; i < AxiLite_data->reg_num; i += 2)
{
printk("reg = %#X \r\n", AxiLite_data->reg_value[i]);
}
printk("for is done!\r\n");
AxiData_0 = ioremap(AxiLite_data->reg_value[0] + AxiLite_data->addr_width * 0, AxiLite_data->addr_width);
AxiData_1 = ioremap(AxiLite_data->reg_value[0] + AxiLite_data->addr_width * 1, AxiLite_data->addr_width);
AxiData_2 = ioremap(AxiLite_data->reg_value[0] + AxiLite_data->addr_width * 2, AxiLite_data->addr_width);
AxiData_3 = ioremap(AxiLite_data->reg_value[0] + AxiLite_data->addr_width * 3, AxiLite_data->addr_width);
printk("ioremap is done!\r\n");
writel(0xA, AxiData_0);
writel(0xB, AxiData_1);
writel(0xC, AxiData_2);
writel(0xD, AxiData_3);
printk(KERN_CRIT "AxiData_0:%#X\n", readl(AxiData_0));
printk(KERN_CRIT "AxiData_1:%#X\n", readl(AxiData_1));
printk(KERN_CRIT "AxiData_2:%#X\n", readl(AxiData_2));
printk(KERN_CRIT "AxiData_3:%#X\n", readl(AxiData_3));
return ret;
}
static int AxiLite_remove(struct platform_device *pdev)
{
kfree(AxiLite_data->reg_value);
iounmap(AxiData_0);
iounmap(AxiData_1);
iounmap(AxiData_2);
iounmap(AxiData_3);
return 0;
}
static struct platform_driver AxiLite_device_driver = {
.driver = {
.name = "AxiLite",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(AxiLite_of_match),
},
.probe = AxiLite_probe,
.remove = AxiLite_remove,
};
static int __init AxiLite_init(void)
{
return platform_driver_register(&AxiLite_device_driver);
}
static void __exit AxiLite_exit(void)
{
platform_driver_unregister(&AxiLite_device_driver);
}
late_initcall(AxiLite_init);
module_exit(AxiLite_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uisrc"); 复制代码
行5~13,用以存储从设备树获取的各项数据,reg_num用来计算reg中参数的数量,*reg_value用来存储reg内的数据。
行15~18,分别用来映射4个axi寄存器。
行21,将vitis生成设备树的名称填入驱动的匹配数组里。
行25~67,从设备树读取所需节点的信息。
行69~112,驱动的初始化函数。
行96~99,分别映射四个寄存器。
行101~104,分别往四个寄存器内写入数据。
行106~109,分别读取四个寄存器内的数据并打印,用于和输入进行对比。
行114~122,注销函数用于注销之前申请的一些资源。
7 演示结果
SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,参考Linux基础篇第四章)
将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。
接入12V直流电源开机。
找到刚才放驱动的目录:
使用sudo insmod axilite.ko命令装载驱动,密码为root:
驱动会先向寄存器分别写入数据,然后读取寄存器内的数据,可以看到输入寄存器的数据读出正常。