本帖最后由 LINUX课程 于 2024-9-11 10:29 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 SPI总线接口简单,SPI的时钟可以到100M以上,SPI总线可以用于多种场合串行通信,比如存储器,温度传感器,压力传感器,模拟转换器,实时时钟,显示器以及任何支持串行模式的SD卡。一些ADC比如AD7606也可以用SPI接口实现通信。 实验目的: - 熟悉掌握SPI通信协议
- 熟悉SPI控制器的硬件资源
- 实现对SPI控制器的使用
2 系统框图 3 SPI总线协议介绍 技术性能: SPI接口是Motorola 首先提出的全双工三线同步串行外围接口,采用主从模式(MasterSlave)架构;支持多slave模式应用,一般仅支持单Master。时钟由Master控制,在时钟移位脉冲下,数据按位传输,高位在前,低位在后(MSBfirst);SPI接口有2根单向数据线,为全双工通信,目前应用中的数据速率可达几Mbps的水平。总线结构如下图所示。 接口定义: SPI接口共有4根信号线,分别是:设备选择线、时钟线、串行输出数据线、串行输入数据线。 (1)MOSI:主器件数据输出,从器件数据输入 (2)MISO:主器件数据输入,从器件数据输出 (3)SCLK:时钟信号,由主器件产生 (4)/SS:从器件使能信号,由主器件控制 时钟极性和时钟相位: SPI数据的传输是在串行同步时钟信号(Serial Clock,SCK)的控制下进行的。主机的时钟发生器一方面控制主机的移位寄存器,另一方面通过从机的SCK信号线来控制从机的移位寄存器,从而保证主机与从机的数据交换是同步进行的。 SPI串行同步时钟可以设置为不同的极性(Clock Polarity ,CPOL)与相位(Clock Phase ,CPHA)。 时钟的极性(CPOL)用来决定在总线空闲时,同步时钟(SCK)信号线上的电位是高电平还是低电平。当时钟极性为0时(CPOL=0),SCK信号线在空闲时为低电平;当时钟极性为1时(CPOL=1),SCK信号线在空闲时为高电平; 时钟的相位(CPHA)用来决定何时进行信号采样。 当时钟相位为1时(CPHA=1),在SCK信号线的第二个跳变沿进行采样;这里的跳变沿究竟是上升沿还是下降沿?取决于时钟的极性。当时钟极性为0时,取下降沿;当时钟极性为1时,取上升沿;如下图: CPHA=1 的SPI时序 当时钟相位为0时(CPHA=0),在SCK信号线的第一个跳变沿进行采样。跳变沿同样与时钟极性有关:当时钟极性为0时,取上升沿;当时钟极性为1时,取下降沿;如下图: CPHA=0 的SPI时序 数据传输 在一个SPI时钟周期内,会完成如下操作: 1)主机通过MOSI线发送1位数据,从机通过该线读取这1位数据; 2)从机通过MISO线发送1位数据,主机通过该线读取这1位数据。 这是通过移位寄存器来实现的。如下图所示,主机和从机各有一个移位寄存器,且二者连接成环。随着时钟脉冲,数据按照从高位到低位的方式依次移出主机寄存器和从机寄存器,并且依次移入从机寄存器和主机寄存器。当寄存器中的内容全部移出时,相当于完成了两个寄存器内容的交换。 4 ZYNQ SPI控制器介绍 4.1 SPI控制器介绍 SPI控制器通过APB总线接入到ARM,SPI控制器部分包含了数据的SPI控制器、SPI中断、发送模块、发送FIFO、接收模块、接收FIFO,并且支持SPI Master模式,以及SPI Slave模式,对于SPI的SLAVE模式还有SLAVE 同步默默。SPI的信号接口可以定义为MIO也可以定于到FPGA的EMIO,对于MIO速度可以到50M,对于EMIO速度为25M. 以下介绍SPI控制器的重要功能模块。 1:SPI的SS选通信号 当SPI作为Master模式的时候,SPI的可以通过3个SS信号实现1~3个外设或者通过3-8译码可以实现1~8个外设选通。SS选通支持手动模式和自动模式,默认是自动模式,在自动模式下每次发送数据会自动设置SS。如果手动模式则需要手动取消SS的片选。我们演示demo就是自动模式。 2:SPI控制器的FIFO SPI控制器的RxFIFO和TxFIFO深度各是128字节。 往已经满的RxFIFO中送入数据,会导致溢出标志置1,新的数据不会添加进入FIFO。可以通过软件写1清除[RX_OVERFLOW]位。 往已经满的TxFIFO写入数据,这个数据会被忽略。当TxFIFO已经写满,TX_FIFO_full标志会置1,直到数据被读出,当FIFO非满后TX_FIFO_full标志会置0。如果TxFIFO读空产生向下溢出, TX_FIFO_underflow位置1。 3:SPI控制器的时钟 SPI_REF_CLK时钟在主机模式提供控制器的工作时钟,并且是SCLK的波特率分频器的参考时钟。在主模式下, SCLK使用波特率分频器从SPI_REF_CLK分频而来。 波特率分频器支持4~256分频(4,8,16,... 256分频)。 在从机模式下使用输入的SCL时钟驱动MISO信号和对MOSI以及SS信号采样,数据最终需要和SPI_REF_CLK时钟同步到SPI的控制器。 4:控制器的中断 下图中RXFIFO和TXFIFO的最多可以保存128个数据。可以通过设置TX FIFO或者RX FIFO的Threshold寄存器设置TX FIFO的将满中断以及RX FIFO的将空中断中FIFO的数据量。 以下这种图表示了中断相关寄存器的设置关系。 4.2 SPI控制器的寄存器 XSPIPS_CR寄存器XSPIPS_CR_OFFSET (0x00U) Field Name | Bits | Type | Reset Value | Description | | | | | 31~18bit:保留(RO只读) 17bit:模式失败生成启动(Modefail_gen_en) 1:使能,当总线冲突导致模式设置失败 0:禁用 16bit:Master启动命令(MANSTRT) 1:使能 0:禁用 15bit:手动启动使能(Man_start_en) 1:使能 0:禁用 14bit:手动CS(Manual_CS) 1:手动模式 0:自动模式 13~10bit:外设选择(CS) xxx0:Slave0选通 xx01:Slave1选通 x010:Slave2选通 0111:保留 1111:没有Slave选通 9bit:外设译码选择(PERI_SEL) 1:支持3-8译码 0:支持1~3个外设 8bit:参考时钟(REF_CLK) 1:不支持 0:使用SPI REFERENCE CLOCK 7~6bit:保留(RO只读) 5~3bit:分频器(REF_CLK) 只有Master模式才支持对spi_ref_clk的分频 000:不支持 001:4分频 010:8分频 011:16分频 100:32分频 101:64分频 110:128分频 111:256分频 2bit:时钟相位(CPHA) 1:在SCLK第2个时钟沿采样 0:在SCLK第1个时钟沿采样 1bit:时钟极性(CPOL) 1:在SCLK上升沿采样 0:在SCLK下降沿采样 0bit:模式选择(MSTREN) 1:主机模式 0:从机模式 |
XSPIPS_SR寄存器XSPIPS_SR_OFFSET (0x04U) 中断状态寄存器,当相应的中断产生后,中断状态寄存器相应的位置1,写1清零 Field Name | Bits | Type | Reset Value | Description | | | | | 31~7bit:保留(RO只读) 6bit:TX FIFO溢出(IXR_TXUF) 1:溢出 0:未溢出 5bit:RX FIFO满(IXR_RXFULL) 1:满 0:未满 4bit: RX FIFO将空(IXR_RXNEMPTY) 1:FIFO数据大于等于THRESHOLD 0:FIFO数据小于THRESHOLD 3bit:TX FIFO满(IXR_TXFULL) 1:满 0:未满 2bit:TX FIFO将满(IXR_TXOW) 1:FIFO数据小于THRESHOLD 0:FIFO数据大于等于THRESHOLD 1bit:SPI 模式错误(IXR_MODF) 表示管脚 n_ss_in上的电压与SPI模式不一致。如果 n_ss_in在主机模式(多主机竞争)下为低电平,或者n_ss_in在从机模式下传输期间变为高电平,则设置 =1。这些条件将清除 spi_enable 位并禁用SPI。该位仅在系统复位时复位,并且仅在读取该寄存器时清零。 ModeFail 中断,向该位写入1清除。 1:产生模式错误 0:没有错误 0bit: 接收溢出中断(IXR_RXOVR) 1:溢出 0:未溢出 |
XSPIPS_IER寄存器XSPIPS_IER_OFFSET (0x08U) 中断使能寄存器,对应于之前的中断状态寄存器,设置1为使能相关中断 Field Name | Bits | Type | Reset Value | Description | | | | | 31~7bit:保留(RO只读) 6bit:TX FIFO溢出(IXR_TXUF) 5bit:RX FIFO满(IXR_RXFULL) 4bit: RX FIFO将空(IXR_RXNEMPTY) 3bit:TX FIFO满(IXR_TXFULL) 2bit:TX FIFO将满(IXR_TXOW) 1bit:SPI 模式错误(IXR_MODF) 0bit: 接收溢出中断(IXR_RXOVR) |
XSPIPS_IDR寄存器XSPIPS_IDR_OFFSET (0x0CU) 中断禁用寄存器, 对应于之前的中断状态寄存器,设置1为禁用相关中断 Field Name | Bits | Type | Reset Value | Description | | | | | 31~7bit:保留(RO只读) 6bit:TX FIFO溢出(IXR_TXUF) 5bit:RX FIFO满(IXR_RXFULL) 4bit: RX FIFO将空(IXR_RXNEMPTY) 3bit:TX FIFO满(IXR_TXFULL) 2bit:TX FIFO将满(IXR_TXOW) 1bit:SPI 模式错误(IXR_MODF) 0bit: 接收溢出中断(IXR_RXOVR) |
XSPIPS_IMR寄存器XSPIPS_IMR_OFFSET (0x10U) 中断掩码寄存器, 对应于之前的中断状态寄存器、中断使能寄存器、中断禁用寄存器,1代表相关中断禁用 Field Name | Bits | Type | Reset Value | Description | | | | | 31~7bit:保留(RO只读) 6bit:TX FIFO溢出(IXR_TXUF) 5bit:RX FIFO满(IXR_RXFULL) 4bit: RX FIFO将空(IXR_RXNEMPTY) 3bit:TX FIFO满(IXR_TXFULL) 2bit:TX FIFO将满(IXR_TXOW) 1bit:SPI 模式错误(IXR_MODF) 0bit: 接收溢出中断(IXR_RXOVR) |
XSPIPS_ER寄存器XSPIPS_ER_OFFSET (0x14U) SPI使能寄存器 Field Name | Bits | Type | Reset Value | Description | | | | | 31~1bit:保留(RO只读) 0bit: 1:使能SPI 2:禁止SPI |
XSPIPS_DR寄存器XSPIPS_DR_OFFSET (0x18U) SPI的延迟寄存器用于控制选通到非选通、非选通到选通、当前WORD最后bit到下个WORD第1bit、选通到第一个Bit开始的延迟参数。 Field Name | Bits | Type | Reset Value | Description | | | | | 31~24bit:选通到取消选通延迟(d_nss) Master模式,当cpha=0时,设置SPI REFERENCE CLOCK或ext_clk个周期的延迟。 23~16bit:取消选通到选通的延迟(BTWN) 从取消选通到选通之间延迟SPI REFERENCE CLOCK或ext_clk个周期 15~8bit:最后1Bit到下1bit间延迟 (AFTER) 当前WORD的最后1Bit到下个WORD的第1bit间延迟SPI REFERENCE CLOCK或ext_clk个周期 7~0bit:nss低电平到第1bit的延迟(INIT) 低电平到第1bit间延迟SPI REFERENCE CLOCK或ext_clk个周期 |
XSPIPS_TXD寄存器XSPIPS_TXD_OFFSET (0x1CU) SPI的发送数据寄存器 Field Name | Bits | Type | Reset Value | Description | | | | | 7~0bit:数据寄存器写入这个寄存器的数据会写入FIFO |
XSPIPS_RXD寄存器XSPIPS_RXD_OFFSET (0x20U) SPI的接收数据寄存器 Field Name | Bits | Type | Reset Value | Description | | | | | |
XSPIPS_SICR寄存器XSPIPS_SICR_OFFSET (0x24U) SPI的SLAVE启动对于总线时钟监测设置 Field Name | Bits | Type | Reset Value | Description | | | | | 31~8:保留(只读) 7~0bit:当外部主机输出的SCLK时钟稳定的输入Slave_Idle_coun个SPI参考时钟周期或者SPI被取消选择,则SPI SLAVE模式会检测到启动 |
XSPIPS_TXWR寄存器XSPIPS_TXWR_OFFSET (0x28U) XSPIPS_TXWR将满值 Field Name | Bits | Type | Reset Value | Description | | | | | |
RX_thres_reg0寄存器RX_thres_reg0_OFFSET (0x2CU) 设置RX FIFO的将空值 Field Name | Bits | Type | Reset Value | Description | | | | | |
Mod_id_reg0寄存器Mod_id_reg0 (0xFCU) 这个寄存器暂时不清楚作用 Field Name | Bits | Type | Reset Value | Description | | | | | 31~25:保留(只读) 24~0bit:模块ID号 |
5硬件电路分析 5.1FEP-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 5.2硬件位置 将FEP-BASE-CARD插入开发板的FEP扩展接口,用短接冒,短接如图所示PIN脚,这两个脚在原理图中是CEP6P和CEP6N。MZ7035系列默认都是选3.3V的BASE CARD,选择FEP卡的时候一定不要选错。 6 搭建SOC系统工程 详细的搭建过程这里不再重复,对于初学读者如果还不清楚如何创建SOC工程的,请学习“01Vitis Soc开发入门”这篇文章。 6.1 SOC系统工程 ZYNQ IP中设置SPI0 设置SPI的参考时钟,以及PL 50M时钟提供给ILA使用 ILA设置 6.2 编译并导出平台文件 以下步骤简写,有不清楚的看“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”。 1:打开soc_prj内工程。 2:生成Bit文件。 3:导出到硬件: FileExport HardwareInclude bitstream 4:导出完成后,对应工程路径的soc_hw路径下有硬件平台文件:system_wrapper.xsa的文件。根据硬件平台文件system_wrapper.xsa来创建需要Platform平台。 5:打开vitis,并添加设备树模板。 6:创建工程文件,选择device tree创建,创建完成后编译工程。 7:获得设备树以及启动文件,打开虚拟机将文件拷贝到开发包的指定位置。 6.3 设备树及驱动修改 1:设备树修改 vitis生成的设备树已经生成了spi的节点,我们要做的是在节点上添加我们需要的设备。
à
添加的设备如下: - device@0 {
- compatible = "milianke,spidev";
- reg = <0>;
- spi-max-frequency = <187500000>;
- #address-cells = <1>;
- #size-cells = <1>;
- };
复制代码可以看到这个设备的名称是我们自定义的,在之后匹配驱动时用。另外规定了地址为0,还有最大频率。 2:修改驱动 由于上一步设置了自己的驱动匹配名称,所以我们需要前往内核源码的文件夹内修改源码。我们是想使用spi的字符驱动,所以我们在spi源码内找到spi的字符驱动实现,位置如下: 在672行按照规则添加一条设备名称,效果如下: 这样我们的设备树就能和spi字符驱动达成匹配了。 3:打开内核选项 修改了设备树,修改了驱动,还要打开这个驱动的驱动开关才行。首先source好指定文件。 然后使用make_kernel_menuconfig.sh命令载入内核选项。根据左上角路径找到对应的位置,如下图所示: 选中如图所示的驱动选项。保存并退出。 4:编译系统 编译uboot,编译kernel,制作镜像并烧录系统。具体步骤参考“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”。 5:拷贝程序 将对应的demo拷贝至sd卡上: 7 程序分析 7.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,打印传输回来的数据。 7.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的数据交换是同时的,不存在如上问题。 8 演示结果 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 登录板卡后,cd至demo的位置: 运行sudo ./spiloop查看结果,root的密码为root: 可以拔掉跳线帽再试,数据就不对了,装上跳线帽数据恢复正常。 若想自己编译,可使用gcc命令: 可以看到编译完多了一个文件,gcc编译的默认输出名称为a.out。 |