[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX驱动篇连载-15 PL AXI-SPI实验

文档创建者:LINUX课程
浏览次数:550
最后更新:2024-09-10
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 2_LINUX应用开发
本帖最后由 LINUX课程 于 2024-9-11 10:41 编辑

软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
登录“米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!

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 系统框图
image.jpg
3 AXI-QUAD-SPI IP概述
当axi_quad_spi ip可以配置成普通模式axi4-lite或者高性能模式axi4接,IP的框图如下:
image.jpg
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次的事务传输
image.jpg
这种模式适合引导程序使用。该模式下,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
Core Grouping
40h
SRR
Write
NA(1)
Software reset register
60h
SPICR
R/W
0x180
SPI control register
64h
SPISR
Read
0x0a5
SPI status register
68h
SPI DTR
Write
0x0
SPI data transmit register. A single register or a FIFO
6Ch
SPI DRR
Read
N/A(1)
SPI data receive register. A single register or a FIFO
70h
SPISSR
R/W
No slave is selected
0xFFFF
SPI Slave select register
74h
SPI Transmit FIFO
Occupancy Register(2)
Read
0x0
Transmit FIFO occupancy register
78h
SPI Receive FIFO
Occupancy Register(2)
Read
0x0
Receive FIFO occupancy register
Interrupt Control Grouping
1Ch
DGIER
R/W
0x0
Device global interrupt enable register
20h
IPISR
R/TOW(3)
0x0
IP interrupt status register
28h
IPIER
R/W
0x0
IP interrupt enable register
1-SPI DRR上电复位数据未知或全为零。处理步骤这个寄存器不应该考虑在上电复位的情况下使用。
2-仅当“FIFO Depth”为16或256时存在。
3-TOW = Toggle on write。将1写入寄存器内的位位置将导致寄存器中相应的位位置发生切换。
1:SRR软件复位寄存器(offset=0x40)
image.jpg
image.jpg
2:SPICR控制寄存器 (offset=0x60)
image.jpg
image.jpg
图片4.jpg
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)
image.jpg
image.jpg
image.jpg
image.jpg
4:DTR数据发送寄存器(offset=0x68)
image.jpg
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)
image.jpg
SPI DRR (Data Receive Register)用于读取从SPI总线接收到的数据。这是一个双缓冲寄存器。在每次完成传输后,接收到的数据被放入这个寄存器。SPI架构没有为从机提供任何方式来控制总线上的流量;因此,只有当SPI DRR在最后一次SPI传输之前被读取时,SPI DRR才会在每个完成的事务之后更新。
6:SPISSR 从机选择寄存器(offset=0x70)
image.jpg
image.jpg
7:TX_FIFO_OCY发送FIFO占用寄存器(offset=0x74)
如果使用了FIFO并且发送的FIFO非空,这个值有效,这个值等于FIFO实际使用的大小减去1。
image.jpg
image.jpg
8:RX_FIFO_OCY接收FIFO占用寄存器(offset=0x78)
如果使用了FIFO并且接收的FIFO非空,这个值有效,这个值等于FIFO实际使用的大小减去1。
image.jpg
image.jpg
9:DGIER全局中断寄存器(offset=0x1C)
image.jpg
10:IPISR中断状态寄存器(offset=0x20)
image.jpg
image.jpg
image.jpg
image.jpg
image.jpg
11:IPIER中断使能寄存器(offset=0x28)
image.jpg
image.jpg
image.jpg
image.jpg
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扩展接口的定义
image.jpg
下图是MZ7035FA开发板FEP和CEP-BASE卡FEP接口的定义。
image.jpg image.jpg
我们把IO分配到CEP6_P: CEP6_N  CEP5_P: CEP5_N
4.2硬件位置
将FEP-BASE-CARD插入开发板的FEP扩展接口,用短接冒,短接如图所示PIN脚,这两个脚在原理图中是CEP6P和CEP6N。MZ7035系列默认都是选3.3V的BASE CARD,选择FEP卡的时候一定不要选错。
1725945459165.jpg
5 搭建SOC系统工程
详细的搭建过程这里不再重复,对于初学读者如果还不清楚如何创建SOC工程的,请学习“01Vitis Soc开发入门”这篇文章。
5.1 SOC系统工程
image.jpg
1:中断设置
image.jpg
2:设置GP Master接口
image.jpg
3:设置复位输出
image.jpg
4:设置PL时钟
image.jpg
5:AXI-SPI IP
image.jpg
6:ILA IP
采样深度选择合适 数值,对于BRAM足够的FPGA可以设置大一些
image.jpg
image.jpg
5.2 设置AXI外设地址分配
只要添加的AXI总线外设都要正确分配地址,这一步不能遗漏
image.jpg
5.3 编译并导出平台文件
以下步骤简写,有不清楚的看Linux基础篇第四章。
1:打开soc_prj内工程。
2:生成Bit文件。
3:导出到硬件: FileàExport HardwareàInclude bitstream
4:导出完成后,对应工程路径的soc_hw路径下有硬件平台文件:system_wrapper.xsa的文件。根据硬件平台文件system_wrapper.xsa来创建需要Platform平台。
image.jpg
5:打开vitis,并添加设备树模板。
6:创建工程文件,选择device tree创建,创建完成后编译工程。
7:获得设备树以及启动文件,打开虚拟机将文件拷贝到开发包的指定位置。
5.4 设备树及驱动修改
1:设备树修改
在对应的demo中的soc_dts文件夹内有修改好的设备树,不建议新手自己修改设备树。vitis生成的设备树已经生成了部分spi的节点,我们要做的是在节点上添加我们需要的设备,先说说设备树多了哪些内容:
在自动生成的aliases字段里,已经添加好了axi的这个spi节点。
1725946031911.jpg
在pl.dtsi这个文件里,已经自动生成好了pl端spi的设备树的设备树。
image.jpg
手动添加一个spi的dev设备,添加在pl端spi中。
添加的设备如下:
  1. spidev@0 {
  2. compatible = "milianke,spidev";
  3. spi-max-frequency = <187500000>;
  4. reg = <0 0 0>;
  5. };
复制代码
可以看到这个设备的名称是我们自定义的,在之后匹配驱动时用。另外规定了最大频率,还有地址为0。
2:修改驱动
由于上一步设置了自己的驱动匹配名称,所以我们需要前往内核源码的文件夹内修改源码。我们是想使用spi的字符驱动,所以我们在spi源码内找到spi的字符驱动实现,位置如下:
1725946113311.jpg
在672行按照规则添加一条设备名称,效果如下:
image.jpg
这样我们的设备树就能和spi字符驱动达成匹配了。
3:打开内核选项
修改了设备树,修改了驱动,还要打开这个驱动的驱动开关才行。首先source好指定文件。
image.jpg
然后使用make_kernel_menuconfig.sh命令载入内核选项。根据左上角路径找到对应的位置,如下图所示:
1725946166099.jpg
选中如图所示的驱动选项。保存并退出。
4:编译系统
编译uboot,编译kernel,制作镜像并烧录系统。具体步骤参考Linux基础篇第四章。
5:拷贝程序
将对应的demo拷贝至sd卡上:
image.jpg
6 程序分析
6.1 应用程序分析
  1. #include <stdint.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <getopt.h>
  6. #include <fcntl.h>
  7. #include <sys/ioctl.h>
  8. #include <linux/types.h>
  9. #include <linux/spi/spidev.h>

  10. #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

  11. static const char *device = "/dev/spidev1.0";
  12. static uint8_t mode;
  13. static uint8_t bits = 8;
  14. static uint32_t speed = 500000;
  15. static uint16_t delay;

  16. static void transfer(int fd)
  17. {
  18.     int ret;
  19.     uint8_t tx[] = {
  20.         0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
  21.         0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
  22.         0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11,
  23.         0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
  24.         0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
  25.         0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23,
  26.         0x24, 0x25,
  27.     };
  28.     uint8_t rx[ARRAY_SIZE(tx)] = {0, };
  29.     struct spi_ioc_transfer tr = {
  30.         .tx_buf = (unsigned long)tx,
  31.         .rx_buf = (unsigned long)rx,
  32.         .len = ARRAY_SIZE(tx),
  33.         .delay_usecs = delay,
  34.         .speed_hz = speed,
  35.         .bits_per_word = bits,
  36.     };
  37.     ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
  38.     if (ret < 1)
  39.         printf("can't send spi message\n");

  40.     for (ret = 0; ret < ARRAY_SIZE(tx); ret++) {
  41.         if (!(ret % 6))
  42.             puts("");
  43.         printf("%.2X ", rx[ret]);
  44.     }
  45.     puts("");
  46. }

  47. int main(int argc, char *argv[])
  48. {
  49.     int ret = 0;
  50.     int fd;

  51.     fd = open(device, O_RDWR);
  52.     if (fd < 0)
  53.         printf("can't open device\n");
  54.     ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
  55.     if (ret == -1)
  56.         printf("can't set spi mode\n");

  57.     ret = ioctl(fd, SPI_IOC_RD_MODE, &mode);
  58.     if (ret == -1)
  59.         printf("can't get spi mode\n");

  60.     ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
  61.     if (ret == -1)
  62.         printf("can't set bits per word\n");

  63.     ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
  64.     if (ret == -1)
  65.         printf("can't get bits per word\n");

  66.     ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
  67.     if (ret == -1)
  68.         printf("can't set max speed hz\n");

  69.     ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
  70.     if (ret == -1)
  71.         printf("can't get max speed hz\n");

  72.     transfer(fd);

  73.     close(fd);
  74.     return ret;
  75. }
复制代码
先来看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分析
  1. SPI_IOC_RD_MODE                                //读 模式
  2. SPI_IOC_RD_LSB_FIRST                //读 LSB
  3. SPI_IOC_RD_BITS_PER_WORD        //读 每字多少位
  4. SPI_IOC_RD_MAX_SPEED_HZ        //读 最大速率
  5. SPI_IOC_WR_MODE                                //写 模式
  6. SPI_IOC_WR_LSB_FIRST                //写 LSB
  7. SPI_IOC_WR_BITS_PER_WORD        //写 每字多少位
  8. SPI_IOC_WR_MAX_SPEED_HZ        //写 最大速率
  9. SPI_IOC_MESSAGE(n)                        //传输n个数据包
复制代码
外设的写操作和读操作是同步完成的,如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。所以代码使用了ioctl控制,而非write/read函数。这是因为write的数据无法和read同时进行(或者说很难同时进行),所以write完再read永远读不到数据,而ioctl的数据交换是同时的,不存在如上问题。
7 演示结果
SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,参考Linux基础篇第四章)
2f5038eb9880afd532753935815b079.jpg
将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。
1725946492612.jpg
接入12V直流电源开机。
登录板卡后,cd至demo的位置:
image.jpg
运行sudo ./spiloop查看结果,密码为root:
image.jpg
可以拔掉跳线帽再试,数据就不对了,装上跳线帽数据运行程序恢复正常。
若想自己编译,可使用gcc命令:
image.jpg
可以看到编译完多了一个文件,gcc编译的默认输出名称为a.out。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则