本帖最后由 LINUX课程 于 2024-9-11 10:41 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 本文通过axi_quad_spi IP实现,本文不再详细介绍SPI协议本身,如果读者对SPI协议还有不清楚的,可以阅读前面的文章“14 SPI环路测试实验”。 本文实验目的: - 通过阅读pg153-axi-quad-spi.pdf熟悉axi-quad-spi控制器的硬件资源
- 通过VIVADO搭建axi-quad-spi的SOC工程
- 编写axi-quad-spi测试程序,实现类似PS-SPI环路测试程序
2 系统框图 3 AXI-QUAD-SPI IP概述 当axi_quad_spi ip可以配置成普通模式axi4-lite或者高性能模式axi4接,IP的框图如下: 3.1 特性 -配置成axi4-lite接口时,向下兼容IP老版本的1.00版本 -当配置成axi4-full接口时,支持高性能burst模式 -支持的SPI模式包括:标准模式、双SPI模块、四SPI模式 -可编程是SPI时钟和极性 -可配置的FIFO深度(在双/四/标准SPI模式下为16或256)和在XIP模式下固定的FIFO深度为16 -dual和quad模式下可配置的从存储器厂家有:Mixed, Micron, Winbond和Spansion (Beta版) 3.2 AXI4接口 1:Enhanced Mode 这种模式下,AXI4-full只允许访问数据收发寄存器,但是寄存器配置依然采用AXI4-Lite。 2:XIP Mode模式 当axi_quad_spi ip配置成XIP模式,通过axis4-lite配置寄存器和状态寄存器,而AXI4仅用于读取数据。在XIP模式下,IP支持以下两种模式: -High Performance Mode:在这种模式下,数据ready信号总是为高,IP支持超过64次的事务。 -Normal Mode:这种模式下,最多支持64次的事务传输 这种模式适合引导程序使用。该模式下,IPCORE将SPI视为只读存储器。在读取SPI闪存时使用的配置模式提供了三个读取命令。通过为axi4 - lite和axis4接口分配相同的频率来验证IP功能。内置在IP中的三个主要的读命令是快速读(0x0Bh)、DIOFR (0xBBh)和QIOFR (0xEBh)。 3.3 寄存器概述 Axi-quad-spi IP 支持多种工作模式,用户应当根据实际需要选择正确的模式。我们这仅仅介绍最常用的普通模式。非XIP Mode模式下的寄存器介绍如下。 Address Space Offset | Register Name | Access Type | Default Value (hex) | Description | | | SRR | Write | NA(1) | Software reset register | | | | | | | | | | | | | | | SPI data transmit register. A single register or a FIFO | | | | | SPI data receive register. A single register or a FIFO | | | | No slave is selected 0xFFFF | SPI Slave select register | | SPI Transmit FIFO Occupancy Register(2) | | | Transmit FIFO occupancy register | | SPI Receive FIFO Occupancy Register(2) | | | Receive FIFO occupancy register | Interrupt Control Grouping | | DGIER | R/W | 0x0 | Device global interrupt enable register | | | | | IP interrupt status register | | | | | IP interrupt enable register |
1-SPI DRR上电复位数据未知或全为零。处理步骤这个寄存器不应该考虑在上电复位的情况下使用。 2-仅当“FIFO Depth”为16或256时存在。 3-TOW = Toggle on write。将1写入寄存器内的位位置将导致寄存器中相应的位位置发生切换。 1:SRR软件复位寄存器(offset=0x40) 2:SPICR控制寄存器 (offset=0x60) 1-只有在标准SPI模式下才允许设置该位(LSB优先)。Dual/quad SPI模式仅支持MSB优先模式。在双/四元SPI模式下,如果设置了该位,则在SPISR中设置相应的错误位,并产生中断。 2-在dual和quad SPI模式下,CPHA-CPOL的值允许为00或11。设置其他配置会导致与内存通信时出现故障。如果设置了其他值,则在SPISR中设置相应的错误位,如果相应的位在SPI IPIER寄存器中使能,则会产生中断。 3-只有在标准SPI模式下才支持slave模式。在dual或quad SPI模式下,只支持核心的主模式。如果设置了其他值,则在SPISR中设置相应的错误位,如果相应的位在SPI IPIER寄存器中使能,则会产生中断。 4-在标准SPI模式下允许环回。 3:SPI状态寄存器SPISR(offset=0x64) 4:DTR数据发送寄存器(offset=0x68) 4.1:数据发送寄存器 SPI总线上需要传输的数据写入SPI数据传输寄存器(SPI DTR)。在主模式下SPE位被设为1,或者在从模式下spisel被设为active后,数据从SPI DTR转移到移位寄存器。 SPI DTR在写入之前应该处于复位状态,这样DTR FIFO写指针就指向第0个位置。 4.2:Dual/Quad模式 这种模式下第一个写操作必须总是来自AXI事务的SPI命令,接着是地址(24位或32位),然后写入要传输的数据。当读取内存的状态寄存器时,命令和地址(地址可选)发出后寄存器需要写入虚拟数据,来实现数据的读取。 比如dual mode read命令中,虚拟字节不是在数据线上传输的,而是被内部逻辑用来跟踪要从SPI从内存中读取的数据字节数。 5:DRR数据接收寄存器(offset=0x6C) SPI DRR (Data Receive Register)用于读取从SPI总线接收到的数据。这是一个双缓冲寄存器。在每次完成传输后,接收到的数据被放入这个寄存器。SPI架构没有为从机提供任何方式来控制总线上的流量;因此,只有当SPI DRR在最后一次SPI传输之前被读取时,SPI DRR才会在每个完成的事务之后更新。 6:SPISSR 从机选择寄存器(offset=0x70) 7:TX_FIFO_OCY发送FIFO占用寄存器(offset=0x74) 如果使用了FIFO并且发送的FIFO非空,这个值有效,这个值等于FIFO实际使用的大小减去1。 8:RX_FIFO_OCY接收FIFO占用寄存器(offset=0x78) 如果使用了FIFO并且接收的FIFO非空,这个值有效,这个值等于FIFO实际使用的大小减去1。 9:DGIER全局中断寄存器(offset=0x1C) 10:IPISR中断状态寄存器(offset=0x20) 11:IPIER中断使能寄存器(offset=0x28) 4 硬件电路分析 4.1 FEP-BASE功能拓展卡 注意:MZ7035使用的FEP扩展IO默认是3.3V,所以默认选择3.3V 的BASE卡完成本实验。 为了完成SPI的环路测试,需要使用FEP-BASE-CARD,以下截图为FEP-BASE-CARD的相关pin脚原理图。这部分都是FPGA的pin脚,所以我们这里使用EMIO。 下图是FEP-BASE-CARD上的CEP扩展接口的定义 下图是MZ7035FA开发板FEP和CEP-BASE卡FEP接口的定义。 我们把IO分配到CEP6_P: CEP6_N CEP5_P: CEP5_N 4.2硬件位置 将FEP-BASE-CARD插入开发板的FEP扩展接口,用短接冒,短接如图所示PIN脚,这两个脚在原理图中是CEP6P和CEP6N。MZ7035系列默认都是选3.3V的BASE CARD,选择FEP卡的时候一定不要选错。 5 搭建SOC系统工程 详细的搭建过程这里不再重复,对于初学读者如果还不清楚如何创建SOC工程的,请学习“01Vitis Soc开发入门”这篇文章。 5.1 SOC系统工程 1:中断设置 2:设置GP Master接口 3:设置复位输出 4:设置PL时钟 5:AXI-SPI IP 6:ILA IP 采样深度选择合适 数值,对于BRAM足够的FPGA可以设置大一些 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,并添加设备树模板。 6:创建工程文件,选择device tree创建,创建完成后编译工程。 7:获得设备树以及启动文件,打开虚拟机将文件拷贝到开发包的指定位置。 5.4 设备树及驱动修改
1:设备树修改 在对应的demo中的soc_dts文件夹内有修改好的设备树,不建议新手自己修改设备树。vitis生成的设备树已经生成了部分spi的节点,我们要做的是在节点上添加我们需要的设备,先说说设备树多了哪些内容: 在自动生成的aliases字段里,已经添加好了axi的这个spi节点。 在pl.dtsi这个文件里,已经自动生成好了pl端spi的设备树的设备树。 手动添加一个spi的dev设备,添加在pl端spi中。 添加的设备如下: - spidev@0 {
- compatible = "milianke,spidev";
- spi-max-frequency = <187500000>;
- reg = <0 0 0>;
- };
复制代码可以看到这个设备的名称是我们自定义的,在之后匹配驱动时用。另外规定了最大频率,还有地址为0。 2:修改驱动 由于上一步设置了自己的驱动匹配名称,所以我们需要前往内核源码的文件夹内修改源码。我们是想使用spi的字符驱动,所以我们在spi源码内找到spi的字符驱动实现,位置如下: 在672行按照规则添加一条设备名称,效果如下: 这样我们的设备树就能和spi字符驱动达成匹配了。 3:打开内核选项 修改了设备树,修改了驱动,还要打开这个驱动的驱动开关才行。首先source好指定文件。 然后使用make_kernel_menuconfig.sh命令载入内核选项。根据左上角路径找到对应的位置,如下图所示: 选中如图所示的驱动选项。保存并退出。 4:编译系统 编译uboot,编译kernel,制作镜像并烧录系统。具体步骤参考Linux基础篇第四章。 5:拷贝程序 将对应的demo拷贝至sd卡上: 6 程序分析 6.1 应用程序分析 - #include <stdint.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <getopt.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <linux/types.h>
- #include <linux/spi/spidev.h>
-
- #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
-
- static const char *device = "/dev/spidev1.0";
- static uint8_t mode;
- static uint8_t bits = 8;
- static uint32_t speed = 500000;
- static uint16_t delay;
-
- static void transfer(int fd)
- {
- int ret;
- uint8_t tx[] = {
- 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
- 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
- 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11,
- 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
- 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
- 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23,
- 0x24, 0x25,
- };
- uint8_t rx[ARRAY_SIZE(tx)] = {0, };
- struct spi_ioc_transfer tr = {
- .tx_buf = (unsigned long)tx,
- .rx_buf = (unsigned long)rx,
- .len = ARRAY_SIZE(tx),
- .delay_usecs = delay,
- .speed_hz = speed,
- .bits_per_word = bits,
- };
- ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
- if (ret < 1)
- printf("can't send spi message\n");
-
- for (ret = 0; ret < ARRAY_SIZE(tx); ret++) {
- if (!(ret % 6))
- puts("");
- printf("%.2X ", rx[ret]);
- }
- puts("");
- }
-
- int main(int argc, char *argv[])
- {
- int ret = 0;
- int fd;
-
- fd = open(device, O_RDWR);
- if (fd < 0)
- printf("can't open device\n");
- ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
- if (ret == -1)
- printf("can't set spi mode\n");
-
- ret = ioctl(fd, SPI_IOC_RD_MODE, &mode);
- if (ret == -1)
- printf("can't get spi mode\n");
- ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
- if (ret == -1)
- printf("can't set bits per word\n");
-
- ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
- if (ret == -1)
- printf("can't get bits per word\n");
-
- ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
- if (ret == -1)
- printf("can't set max speed hz\n");
-
- ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
- if (ret == -1)
- printf("can't get max speed hz\n");
-
- transfer(fd);
-
- close(fd);
- return ret;
- }
复制代码先来看main里的代码,主要设置了spi的参数: 行57,打开spi字符驱动,具体字符驱动名称使用ls /dev | grep spi可查看。 行60~66,设置spi的写模式和读模式。 行68~74,设置spi的读写多少bit每字节。 行76~82,设置spi的读写速率。 行84,调用函数传输数据。 接下来转到transfer函数中: 行22,为一个发送缓存区,里面已经设置好了需要发送的数据。 行31,接收缓存区,初始化为0。 行32~39,为spi传输结构体,里面包括了发送缓存区地址,接收缓存区地址,传输长度,延时,速率,每字节比特数。 行40,进行一次传输,括号内1的意思为传输一个数据包。 行44~49,打印传输回来的数据。 6.2 驱动IOCTL分析 - SPI_IOC_RD_MODE //读 模式
- SPI_IOC_RD_LSB_FIRST //读 LSB
- SPI_IOC_RD_BITS_PER_WORD //读 每字多少位
- SPI_IOC_RD_MAX_SPEED_HZ //读 最大速率
- SPI_IOC_WR_MODE //写 模式
- SPI_IOC_WR_LSB_FIRST //写 LSB
- SPI_IOC_WR_BITS_PER_WORD //写 每字多少位
- SPI_IOC_WR_MAX_SPEED_HZ //写 最大速率
- SPI_IOC_MESSAGE(n) //传输n个数据包
复制代码外设的写操作和读操作是同步完成的,如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。所以代码使用了ioctl控制,而非write/read函数。这是因为write的数据无法和read同时进行(或者说很难同时进行),所以write完再read永远读不到数据,而ioctl的数据交换是同时的,不存在如上问题。 7 演示结果 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,参考Linux基础篇第四章) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 接入12V直流电源开机。 登录板卡后,cd至demo的位置: 运行sudo ./spiloop查看结果,密码为root: 可以拔掉跳线帽再试,数据就不对了,装上跳线帽数据运行程序恢复正常。 若想自己编译,可使用gcc命令: 可以看到编译完多了一个文件,gcc编译的默认输出名称为a.out。 |