# SPI总线协议

  - [概要](#概要)
  - [什么是SPI](#什么是spi)
    - [通信原理](#通信原理)
      - [物理接线](#物理接线)
      - [起始、停止信号](#起始停止信号)
      - [数据有效性](#数据有效性)
      - [操作SPI设备通用方法集合](#操作spi设备通用方法集合)
    - [SPI优点](#spi优点)
    - [SPI缺点](#spi缺点)
  - [SPI  引脚定义](#spi--引脚定义)
  - [machine.SPI API详解](#machinespi-api详解)
    - [硬件SPI构造`machine.spi`](#硬件spi构造machinespi)
      - [宏定义](#宏定义)
      - [类](#类)
      - [函数](#函数)
        - [SPI.init初始化](#spiinit初始化)
        - [SPI.write写数据](#spiwrite写数据)
        - [SPI.read读数据](#spiread读数据)
        - [释放资源SPI.deinit](#释放资源spideinit)
    - [软件SPI构造`machine.SoftSPI`](#软件spi构造machinesoftspi)
      - [宏定义](#宏定义-1)
      - [类](#类-1)
      - [函数](#函数-1)
  - [示例](#示例)

## 概要

​		本节详细讲解SPI总线协议。

## 什么是SPI

- SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司推出的一 种同步串行接口技术,是一种高速的,全双工,同步的通信总线。

- SPI用于在主设备和从设备之间进行通信,常用于与闪存、实时时钟、传感器以及模数转换器等进行通信。

- SPI协议主要用于短距离的通信系统中,特别是嵌入式系统,很多芯片的外围设备,比如LED显示驱动器、I/O接口芯片、UART收发器等都广泛的采用SPI总线协议。

### 通信原理

​		SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多
​		个从设备。在英文中,通常把主设备称作为 Master, 从设备称作为 Slave.

#### 物理接线

​		SPI理论上需要4根线才能进行双向数据传输,3根线可以进行单向传输:
​		SPI理论上的4根接线分别是以下四种:

| 功能编号        | 缩写含义                  | 中文含义                                  |
| :-------------- | :------------------------ | ----------------------------------------- |
| SDO 或者叫 MOSI | Master Output Slave Input | 主设备数据输出,从设备数据输入            |
| SDI 或者叫 MISO | Master Input Slave Output | 主设备数据输入,从设备数据输出            |
| SCLK            | serial clock              | 时钟信号, 由主设备产生                   |
| CS 或者叫SS     | chip select               | 片选信号, 从设备使能信号,由主设备控制。 |

​		一个主设备和两个从设备的连接示意图如图1所示,Device A和Device B共享主设备的SCLK、MISO和MOSI三根引脚,Device A的片选CS0连接主设备的CS0,Device B的片选CS1连接主设备的CS1。

![img](Image/zh-cn_image_0000001054142582.png)
<div align=center>
图 1 SPI主从设备连接示意图
</div>

- SPI通信通常由主设备发起,通过以下步骤完成一次通信:
1. 通过CS选中要通信的从设备,在任意时刻,一个主设备上最多只能有一个从设备被选中。
2. 通过SCLK给选中的从设备提供时钟信号。
3. 基于SCLK时钟信号,主设备数据通过MOSI发送给从设备,同时通过MISO接收从设备发送的数据,完成通信。

#### 起始、停止信号

![img](Image/spi-wave-cs.png)

​		如上图,红色编号1和6即为起始和停止信号的发生区域。

​		**CS片选信号(图中的NSS)电平由高变低,则产生起始信号;**

​		**CS片选信号电平由低变高,则产生停止信号。**

​		从机检测到自己的CS片选信号线电平被置低,则开始与主机进行通讯;

​		反之,检测到NSS电平被拉高,则停止通讯。

#### 数据有效性

​		MOSI和MISO线在SCK的每个时钟周期传输一位数据,开发者可以自行设置MSB或LSB先行,不过需要保证两个通讯设备都使用同样的协定。

​		从以下的时序图可以看出,在SCK时钟周期的上升沿和下降沿时进行触发和采样。

![img](Image/spi-wave-cs.png)

​		这里的触发和采样其实是两个特殊的时间节点,分别对应了SCK时钟周期的上升沿和下降沿。

​		SPI有四种通讯模式,在SCK上升沿触发,下降沿采样只是其中一种模式。四种模式的主要区别便是总线空闲时SCK的状态及数据采样时刻。这涉及到“时钟极性CPOL”和“时钟相位CPHA”,由CPOL和CPHA的组合而产生了四种的通讯模式。

- CPOL:即在没有数据传输时,时钟的空闲状态的电平。上面的两幅图示中,无数据传输时的时钟空闲状态为低电平。
- CPHA:即数据的采样时刻,可以是SCK的上升沿,也可以是SCK的下降沿。

  ​	将CPOL和CPHA的两种状态分别用0,1表示,因此由这两种方式排列组合,便可以产生四种模式的SPI:

| SPI模式 | CPOL | 空闲时SCK时钟 | CPHA | 采样时刻  |
| :------ | :--- | :------------ | :--- | :-------- |
| 0       | 0    | 低电平        | 0    | SCK下降沿 |
| 1       | 0    | 低电平        | 1    | SCK上升沿 |
| 2       | 1    | 高电平        | 0    | SCK下降沿 |
| 3       | 1    | 高电平        | 1    | SCK上升沿 |

​		配合下图理解:

![img](Image/spi-modes.png)

​		**很重要的一点是,主机和从机需要工作在相同的模式下才能正常通讯**

### 操作SPI设备通用方法集合

​		SPI接口定义了操作SPI设备的通用方法集合,包括:

- SPI设备句柄获取和释放。
- SPI读写: 从SPI设备读取或写入指定长度数据。
- SPI自定义传输:通过消息传输结构体执行任意读写组合过程。
- SPI设备配置:获取和设置SPI设备属性。

### SPI优点

- 支持全双工通信,发送数据和接收数据可以同时进行。
- 通信简单
- 数据传输速率快

### SPI缺点

- 接线繁杂,需要至少四根接线
- 在多个从机的情况下,每个从机都需要接入一根CS片选信号线,这是十分的浪费芯片的IO资源

## SPI  引脚定义

&emsp;有10个引脚可以用作SPI

| SPI组号 | SCK           | MOSI         | MSIO  |
| ------- | ------------- | ------------ | ----- |
| SPI 1   | pin 0         | pin 2        | pin 1 |
| SPI 0   | pin 6、pin 10 | pin 8、pin 9 | pin 7 |

&emsp;&emsp;除此两组SPI硬件资源外,其余的GPIO理论上也可以配置成SPI总线的输入输出管脚,只要满足该管脚既能够作为输入也能够作为输出。因此,

在MicroPython中,拥有两种模式的SPI总线:

- 硬件SPI:
- 软件SPI (GPIO模拟):

## machine.SPI API详解

### 硬件SPI构造`machine.spi`

&emsp;&emsp;硬件SPI:由SPI 0或SPI1组成

&emsp;&emsp;使用`from machine import SPI`导入`machine`模块的类`SPI`

&emsp;&emsp;再使用`TAB` 按键来查看`SPI`中所包含的内容:

```python
>>>from machine import SPI
>>>SPI.
read            readinto        write           LSB
MSB             deinit          init            write_readinto
```

#### 宏定义

&emsp;&emsp;下面的宏定义用于配置SPI发送字节数据的顺序。

| 宏定义  | 含义                                               |
| ------- | -------------------------------------------------- |
| Pin.MSB | 从一个字节中的最高位依次到最低位开始发送该字节数据 |
| Pin.LSB | 从一个字节中的最低位依次到最高位开始发送该字节数据 |

&emsp;&emsp;以下是SPI1的构造:

```python
>>> from machine import SPI
>>> spi=SPI(1,sck=Pin(0))
>>> spi
SPI(id=1, baudrate=500000, polarity=0, phase=0, bits=8, endia=1, sck=0, mosi=-1, miso=-1)
```

&emsp;&emsp;以下是SPI0的构造:

```python
>>> from machine import SPI
>>> spi0=SPI(0,sck=Pin(6))
>>> spi0
SPI(id=0, baudrate=500000, polarity=0, phase=0, bits=8, endia=1, sck=6, mosi=-1, miso=-1)
```

#### 类

&emsp;&emsp;`class machine.SPI(id,baudrate, polarity, phase, bits, endia, sck, mosi, miso)`

- `id`:取决于特定端口及其硬件。 值0、1等常用于选择硬件SPI0或SPI1。
- `baudrate`:SCK时钟频率 范围 `0 < baudrate ≤ 0x0FFFFFFF (十进制:0 < baudrate ≤ 2147483647)`
- `polarity`:极性
  - `0` 时钟空闲时候的电平是低电平,所以当SCLK有效的时候,就是高电平
  - `1` 时钟空闲时候的电平是高电平,所以当SCLK有效的时候,就是低电平
- `phase`:相位
  - `0` 在下降沿采样数据
  - `1` 在上升沿采样数据
- `bits`:传输数据位数
- `endia`:字节数据传输的方式,可为 `SPI.MSB`即0 或 `SPI.LSB`即1
- `sck` 时钟信号引脚
- `mosi` 主设备输出,从设备输入引脚
- `miso` 主设备输入,从设备输出引脚

&emsp;&emsp;示例:

```python
from machine import SPI, Pin
spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))
#构造SPI0,SCK时钟频率为40000000,SCK空闲状态为高电平,SCK下降沿采样
#传输数据位8位,字节数据传输的方式为SPI.MSB从高到低
#sck时钟信号引脚是pin6,mosi主设备输出引脚是pin8,msio主设备输入引脚是pin7
```

#### 函数

##### 初始化

&emsp;&emsp;`SPI.init(baudrate, polarity, phase, sck, mosi, miso)`

&emsp;&emsp;函数说明:初始化SPI总线
&emsp;&emsp;参数含义同上文类构造一致

&emsp;&emsp;示例:

```python
>>>from machine import SPI, Pin
>>>spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#构造硬件SPI0
>>>spi0.init(baudrate=100000, polarity=1, phase=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#初始化SPI总线
>>>spi0
SPI(id=0, baudrate=100000, polarity=1, phase=0, bits=8, endia=0, sck=6, mosi=8, miso=7)
```

##### 写数据

&emsp;&emsp;`SPI.write(buf)`

&emsp;&emsp;函数说明:将 buf 中的所有数据写入到总线。
&emsp;&emsp;示例:

```python
>>>from machine import SPI, Pin
>>>spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#构造硬件SPI0
>>>write_buf = bytearray([1,2,3,4,5,6,7,8])
>>>sw=spi0.write(write_buf)#将write中的所有数据写入到总线
```

##### 读数据

&emsp;&emsp;`SPI.read(len, data=0x00)`

&emsp;&emsp;函数说明:读取len个数据的同时写入len个data数据,以数组的形式返回读取到的数据。

&emsp;&emsp;`len`: 需要读取的字节长度
&emsp;&emsp;`data`: 写入的单字节数据

&emsp;&emsp;示例:

```python
>>>from machine import SPI, Pin
>>>spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#构造硬件SPI0
>>print(spi0.read(2, 0x00)) #读取2个字节,写入数据0x00
b'\x00\x00'
```

&emsp;&emsp;`SPI.readinto(buf, data=0x00)`

&emsp;&emsp;函数说明:读取buf.len个数据并存入buf中,同时写入buf.len个data数据,函数返回None。

&emsp;&emsp;`buf`: 数据缓冲区
&emsp;&emsp;`data`: 写入的单字节数据

&emsp;&emsp;`SPI.write_readinto(write_buf, read_buf`)

&emsp;&emsp;函数说明:写入write_buf并读取到 read_buf,写入并读取的长度为buf长度,要求两个缓冲区长度相同。

&emsp;&emsp;`write_buf`: 写数据缓冲区
&emsp;&emsp;`read_buf`: 读数据缓冲区

&emsp;&emsp;示例:

```python
>>>from machine import SPI, Pin
>>>spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#构造硬件SPI0
>>>write_buf = bytearray([1, 2, 3, 4, 5, 6, 7, 8])
>>>read_buf = bytearray(8)
>>>spi.write_readinto (write_buf, read_buf)#写入write_buf并读取8个字节
>>>print(read_buf)
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00')
```

##### 释放资源

&emsp;&emsp;`SPI.deinit()`

&emsp;&emsp;函数说明:关闭SPI。
&emsp;&emsp;示例:

```python
>>>from machine import SPI, Pin
>>>spi0 = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))#构造硬件SPI0
>>>spi0.deinit()#关闭SPI0
>>>spi0
SPI(id=0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0, sck=6, mosi=8, miso=7)
```

### 软件SPI构造`machine.SoftSPI`

&emsp;&emsp;硬件SPI构造的引脚功能必须全为SPI0或全为SPI1的,软件SPI构造更为灵活,可以随机选择SPI0或SPI1的sck,miso,msio引脚。

&emsp;&emsp;使用`from machine import SoftSPI`导入`machine`模块的类`SoftSPI`

&emsp;&emsp;再使用`TAB` 按键来查看`SoftSPI`中所包含的内容:

```python
>>>from machine import SoftSPI
>>>SoftSPI.
read            readinto        write           LSB
MSB             deinit          init            write_readinto
```

#### 宏定义

&emsp;&emsp;软件SPI构造宏定义和硬件构造相同

#### 类

&emsp;&emsp;`class machine.SoftSPI(id,baudrate, polarity, phase, bits, endia, sck, mosi, miso)`

- `baudrate`:SCK时钟频率 范围 `0 < baudrate ≤ 0x0FFFFFFF (十进制:0 < baudrate ≤ 2147483647)`
- `polarity`:极性
  - `0` 时钟空闲时候的电平是低电平,所以当SCLK有效的时候,就是高电平
  - `1` 时钟空闲时候的电平是高电平,所以当SCLK有效的时候,就是低电平
- `phase`:相位
  - `0` 在下降沿采样数据
  - `1` 在上升沿采样数据
- `bits`:传输数据位数
- `firstbi`:字节数据传输的方式,可为 `SPI.MSB`即0 或 `SPI.LSB`即1
- `sck` 时钟信号引脚
- `mosi` 主设备输出,从设备输入引脚
- `miso` 主设备输入,从设备输出引脚

&emsp;&emsp;示例:

```python
>>>from machine import SoftSPI, Pin
>>>softspi = SoftSPI(baudrate=400000, polarity=0, phase=0, bits=8, firstbit=0, sck=Pin(6, dir=Pin.OUT), mosi=Pin(8, dir=Pin.OUT), miso=Pin(7, dir=Pin.OUT))
#构造SPI,SCK时钟频率为400000,SCK空闲状态为低电平,SCK下降沿采样
#传输数据位8位,字节数据传输的方式为SPI.MSB从高到低
#sck时钟信号引脚是pin6、,mosi主设备输出引脚是pin8,msio主设备输入引脚是pin7,引脚都是输出模式
```

#### 函数

&emsp;&emsp;软件构造的函数与硬件构造的函数相同

## 示例

&emsp;&emsp;分别构建硬件SPI和软件SPI

```python
from machine import SPI, Pin, SoftSPI
# 硬件 SPI
spi = SPI(0, baudrate=2000000, polarity=0, phase=0, bits=8, endia=0, sck=Pin(6), mosi=Pin(8), miso=Pin(7))
# 软件 SPI
softspi = SoftSPI(baudrate=400000, polarity=0, phase=0, bits=8, firstbit=0, sck=Pin(6, dir=Pin.OUT), mosi=Pin(8, dir=Pin.OUT), miso=Pin(7, dir=Pin.OUT))
```

&emsp;&emsp;第一行导入`machine`模块的硬件类`Pin`,硬件SPI类`SPI`和软SPI`SoftSPI`

&emsp;&emsp;第三行构造硬件SPI

- 0代表构建的是SPI0
- baudrate=2000000表示SCK时钟频率为2000000
- polarity=0表示SCK空闲状态为低电平
- phase=0表示SCK下降沿采样
- bits=8传输数据位8位
- endia=0表示字节数据传输的方式为SPI.MSB从高到低
- sck=Pin(6)表示sck时钟信号引脚是pin6
- mosi=Pin(8)表示mosi主设备输出引脚是pin8
- miso=Pin(7)msio主设备输入引脚是pin7

&emsp;&emsp;第五行构造软件SPI

- baudrate=2000000表示SCK时钟频率为2000000
- polarity=0表示SCK空闲状态为低电平
- phase=0表示SCK下降沿采样
- bits=8传输数据位8位
- firstbit=0表示字节数据传输的方式为SPI.MSB从高到低
- sck=Pin(6, dir=Pin.OUT)表示sck时钟信号引脚是pin6,引脚为输出模式
- mosi=Pin(8, dir=Pin.OUT)表示主设备输出引脚是pin6,引脚为输出模式
- miso=Pin(7, dir=Pin.OUT))表示主设备输入引脚是pin6,引脚为输出模式