16. SPI通信

在Linux内核文档的Documentation/SPI目录下有关于SPI驱动非常详细的说明。

16.1. SPI通讯协议简介

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、 LCD 等设备与 MCU 间, 要求通讯速率较高的场合学习本章时,可与 I2C 章节对比阅读,体会两种通讯总线的差异。 下面我们分别对 SPI 协议的物理层及协议层进行讲解。

SPI通讯设备之间的常用连接方式见下图。

未找到图片

SPI通讯使用3条总线及片选线,3条总线分别为SCK、MOSI、MISO,片选线为SS* ,它们的作用介绍如下:

  • 片选线(Slave Select):从设备选择信号线,常称为片选信号线, 也称为NSS、CS,以下用NSS表示。SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。

  • SCK(Serial Clock):时钟信号线,用于通讯数据同步,它由通讯主机产生。

  • MOSI(Master Output,Slave Input):主设备输出/从设备输入引脚。数据方向为主机到从机。

  • MISO(Master Input,Slave Output):主设备输入/从设备输出引脚。数据方向为从机到主机。

SPI分成了四种模式,见下表, 主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。

SPI模式

CPOL

CPHA

空闲时SCK时钟

采样时刻

0

0

0

低电平

奇数边沿

1

0

1

低电平

偶数边沿

2

1

0

高电平

奇数边沿

3

1

1

高电平

偶数边沿

16.2. 使能SPI设备

IMX6ULL的spi接口称为“ECSPI”,它在普通spi接口新加入来一些特性。 把它当作普通spi接口使用即可。 IMX6ULL有四个ECSPI接口,本实验主要使用它的第三个。

/boot/uEnv.txt 文件中打开ECSPI_3相关设备树,并需要关闭串口2的设备树插件, 两者存在io复用冲突,不能同时使用,如下图:

未找到图片

16.2.1. 检查SPI设备

使能exspi_3设备树插件之后重新开发板,通过SPI设备文件来判断spi驱动是否加载成功。 SPI_3对应的设备文件是spidev2.0,如下所示

1
2
3
root@npi:/home/zhan# ls -l  /dev/spidev2.0
crw-rw---- 1 root spi 153, 0 Feb 14  2019 /dev/spidev2.0
root@npi:/home/zhan#

16.2.2. SPI引脚关系

其中ECSPI_3的引脚关系如下表所示

SPI引脚

原理图网络标号

引脚引出位置(排针)

SPI3_MOSI

UART2_CTS

CN4排针 IO1.22 脚

SPI3_MISO

UART2_RTS

CN4排针 IO1.23 脚

SPI3_CLOCK

UART2_RXD

CN4排针 IO1.21 脚

SPI3_CS

UART2_TXD

CN4排针 IO1.20 脚

16.3. SPI相关数据结构与ioctl函数

编写应用程序需要使用到spi_ioc_transfer结构体,如下所示

linux/spi/spidev.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct spi_ioc_transfer {
   __u64             tx_buf;     //发送数据缓存
   __u64             rx_buf;     //接收数据缓存

   __u32             len;        //数据长度
   __u32             speed_hz;   //通讯速率

   __u16             delay_usecs;    //两个spi_ioc_transfer之间的延时,微秒
   __u8              bits_per_word;  //数据长度
   __u8              cs_change;      //取消选中片选
   __u8              tx_nbits;       //单次数据宽度(多数据线模式)
   __u8              rx_nbits;       //单次数据宽度(多数据线模式)
   __u16             pad;

};

在编写应用程序时还需要使用ioctl函数设置spi相关配置,其函数原型如下

1
2
3
 #include <sys/ioctl.h>

 int ioctl(int fd, unsigned long request, ...);

其中对于终端request的值常用的有以下几种

SPI_IOC_RD_MODE

设置读取SPI模式

SPI_IOC_WR_MODE

设置写入SPI模式

SPI_IOC_RD_LSB_FIRST

设置SPI读取数据模式(LSB先行返回1)

SPI_IOC_WR_LSB_FIRST

设置SPI写入数据模式。(0:MSB,非0:LSB)

SPI_IOC_RD_BITS_PER_WORD

设置SPI读取设备的字长

SPI_IOC_WR_BITS_PER_WORD

设置SPI写入设备的字长

SPI_IOC_RD_MAX_SPEED_HZ

设置读取SPI设备的最大通信频率。

SPI_IOC_WR_MAX_SPEED_HZ

设置写入SPI设备的最大通信速率

SPI_IOC_MESSAGE(N)

一次进行双向/多次读写操作

提示

SPI的读写和写入可以设置为不同的参数。

16.4. 编写测试程序

根据ioctl相关参数即可编写spi相关测试程序,此处为了简单仅做回环测试实验, 只需要将SPI3的 MIOS与MOSI引脚(pro开发板上的IO1.22和IO1.23)使用跳线帽短接即可。

ebf_6ull_quick_start_code/Source/spi/spi_test/spi_test.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include <linux/spi/spidev.h>

#define spi3_oled_DEV_path        "/dev/spidev2.0"

/*SPI 接收 、发送 缓冲区*/
static unsigned char tx_buffer[100] = "hello the world !";
static unsigned char rx_buffer[100];

static int fd;                          // SPI 控制引脚的设备文件描述符

static uint32_t mode = SPI_MODE_2;      //用于保存 SPI 工作模式
static uint8_t bits = 8;                // 接收、发送数据位数
static uint32_t speed = 500000;         // 发送速度
static uint16_t delay;                  //保存延时时间

//spi初始化
static int spi_init(void);
//spi发送数据
static int transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len);

int main(int argc,char * argv[])
{
   int ret;

   ret = spi_init();
   if( -1 == ret  )
   {
      printf("spi_init error\n");
      exit(-1);
   }

   ret = transfer(fd, tx_buffer, rx_buffer, sizeof(tx_buffer));
   if (-1 == ret  )
   {
      printf("transfer error...\n");
   }

   /*打印 tx_buffer 和 rx_buffer*/
   printf("tx_buffer: \n %s\n ", tx_buffer);
   printf("rx_buffer: \n %s\n ", rx_buffer);

   return 0;
}

int transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
   int ret;

   struct spi_ioc_transfer tr = {
      .tx_buf = (unsigned long )tx,
      .rx_buf = (unsigned long )rx,
      .len = len,
      .delay_usecs = delay,
      .speed_hz = speed,
      .bits_per_word =bits,
   };

   ret = ioctl(fd,SPI_IOC_MESSAGE(1),&tr);
   if( ret == -1 )
   {
      return -1;
   }

   return 0;
}

int spi_init(void)
{
   int ret;
   fd = open(spi3_oled_DEV_path,O_RDWR);
   if(fd < 0)
   {
      perror("/dev/spidev2.0");
      return -1;
   }

   //设置spi工作模式
   ret = ioctl(fd,SPI_IOC_RD_MODE,&mode);
   if( ret == -1)
   {
      printf("SPI_IOC_RD_MODE error......\n ");
      goto fd_close;
   }
   ret = ioctl(fd,SPI_IOC_WR_MODE,&mode);
   if( ret == -1)
   {
      printf("SPI_IOC_WR_MODE error......\n ");
      goto fd_close;
   }

   //设置SPI通信的字长
   ret = ioctl(fd,SPI_IOC_RD_BITS_PER_WORD,&bits);
   if( ret == -1)
   {
      printf("SPI_IOC_RD_BITS_PER_WORD error......\n ");
      goto fd_close;
   }
   ret = ioctl(fd,SPI_IOC_WR_BITS_PER_WORD,&bits);
   if( ret == -1)
   {
      printf("SPI_IOC_WR_BITS_PER_WORD error......\n ");
      goto fd_close;
   }

   //设置SPI最高工作频率
   ret = ioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ,&speed);
   if( ret == -1)
   {
      printf("SPI_IOC_WR_MAX_SPEED_HZ error......\n ");
      goto fd_close;
   }
   ret = ioctl(fd,SPI_IOC_RD_MAX_SPEED_HZ,&speed);
   if( ret == -1)
   {
      printf("SPI_IOC_RD_MAX_SPEED_HZ error......\n ");
      goto fd_close;
   }


   printf("spi mode: 0x%x\n", mode);
   printf("bits per word: %d\n", bits);
   printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);

   return 0;

fd_close:

   close(fd);
   return -1;
}

16.5. 编译运行

我们把代码下载到开发板命名为spi.c

1
2
3
 sudo gcc spi.c -o spi_test
 sudo chmod 777 spi_test
 ./spi_test

效果如下图所示

未找到图片spi_show

16.6. spi oled实验

在代码仓库中提供了野火spi oled显示相关代码,鉴于文章长度,有兴趣的读者可查看相关源码。

spi oled相关资料

代码地址

如果要修改I2C的引脚可参考 spi设备树插件修改 小节

16.7. 参考资料

本文档主要提供有一定经验的使用者快速入门使用,详细资料可阅读以下文档:

《SPI通信》