和上一篇文章的 I2C 总线一样,SPI(Serial Peripheral Interface,串行外设接口)也是设备与设备间通信方式的一种。SPI 是一种全双工(数据可以两个方向同时传输)的串行通信总线,由摩托罗拉于上个世纪 80 年代开发[1],用于短距离设备之间的通信。SPI 包含 4 根信号线,一根时钟线 SCK(Serial Clock,串行时钟),两根数据线 MOSI(Master Output Slave Input,主机输出从机输入)和 MISO(Master Input Slave Output,主机输入从机输出),以及一根片选信号 CS(Chip Select,或者叫 SS,Slave Select)。所谓的时钟线就是一种周期,两台设备数据传输不能各发各的,这样就没有意义,因此需要一种周期去对通信进行约束;数据线就是按照 MOSI 和 MISO 的中文翻译理解即可;片选信号用于主设备选择 SPI 上的从设备,I2C 是靠地址选择设备,而 SPI 靠的是片选信号,一般来说要选择哪个从设备只要将相应的 CS 线设置为低电平即可,特殊情况需要看数据手册。下图展示了一个 SPI 主设备和三个 SPI 从设备的示意图。
图源:Wikipedia
SPI 还有一个重要的概念就是时钟的极性(CPOL,Clock Polarity)和相位(CPHA,Clock Phase),对其这里不过多解释,我们只需要知道极性和相位的组合构成了 SPI 的传输模式(SPI Mode)。在数据手册中,只要是 SPI 通信协议的,一定会给出传输模式,我们根据数据手册进行设置即可。SPI 的传输模式是有固定编号的,下表给出了各个模式,常用的模式有 Mode0 和 Mode3。
SPI Mode | CPOL | CPHA |
---|---|---|
Mode0 | 0 | 0 |
Mode1 | 0 | 1 |
Mode2 | 1 | 0 |
Mode3 | 1 | 1 |
该时序图显示了时钟的极性和相位。图源:Wikipedia
SPI 相比较 I2C 最大的优点就是传输速率高,并且数据在同一时间内可以双向传输,这都得益于它的两根输入和输出数据线。当然缺点也很明显,比 I2C 多了两根线,这就要多占用两个 IO 接口。而且 SPI 采用 CS 线去选择设备,不像 I2C 有寻址机制,如果你有很多个 SPI 设备需要连接的话 IO 接口的占用数量是相当高的。
在 Raspberry Pi 的引脚中,引出了两组 SPI 接口。但有意思的是,在 Raspbian 中 SPI-1 是被禁用的,你需要修改一些参数去启用 SPI-1。SPI 接口的引脚编号如下图所示。
提示
如何在 Raspbian 上开启 SPI-1?(在 Win10 IoT 上 SPI-1 是开启的)
1. 使用编辑器打开 /boot/config.txt ,如:sudo nano /boot/config.txt
2. 添加 dtoverlay=spi1-3cs
并保存
3. 重启
Raspberry Pi B+/2B/3B/3B+/Zero 引脚图
SPI 操作的相关类位于 System.Device.Spi 和 System.Device.Spi.Drivers 命名空间下。
SpiConnectionSettings
类位于 System.Device.Spi 命名空间下,表示 SPI 设备的连接设置。
public sealed class SpiConnectionSettings
{
// 构造函数
// busId 是 SPI 的内部 ID
// chipSelectLine 是 CS Pin 的编号(在 Raspberry Pi 上,SPI-0 对应 0 和 1,SPI-1 对应 2)
public SpiConnectionSettings(int busId, int chipSelectLine);
// 属性
// SPI 传输模式
public SpiMode Mode { get; set; }
// SPI 时钟频率
public int ClockFrequency { get; set; }
// CS 线激活状态(即高电平选中设备还是低电平选中设备)
public PinValue ChipSelectLineActiveState { get; set; }
}
UnixSpiDevice
和 Windows10SpiDevice
类位于 System.Device.Spi.Drivers 命名空间下。两个类均派生自抽象类 SpiDevice,分别代表 Unix 和 Windows10 下的 SPI 控制器,使用时按照所处的平台有选择的进行实例化。这里以 UnixSpiDevice
类为例说明。
public class UnixSpiDevice : SpiDevice
{
// 构造函数
// 需要传入一个 SpiConnectionSettings 对象
public UnixSpiDevice(SpiConnectionSettings settings);
// 方法
// 从从设备中读取一段数据,数据长度由 Span 的长度决定
public override void Read(Span<byte> buffer);
// 从从设备中读取一个字节的数据
public override byte ReadByte();
// 全双工传输,即主从设备同时传输
// writeBuffer 为要写入从设备的数据
// readBuffer 为要从从设备中读取的数据
// 需要注意的是 writeBuffer 和 readBuffer 需要长度一致
public override void TransferFullDuplex(ReadOnlySpan<byte> writeBuffer, Span<byte> readBuffer);
// 向从设备中写入一段数据,通常 Span 中的第一个数据为要写入数据的寄存器的地址
public override void Write(ReadOnlySpan<byte> buffer);
// 向从设备中写入一个字节的数据,通常这个字节为寄存器的地址
public override void WriteByte(byte value);
}
SpiConnectionSettings
一般情况下,我们只需要配置 SPI 的 ID,CS 的编号,时钟频率和 SPI 传输模式。其中像时钟频率、传输模式等设置都来自于设备的数据手册。比如要使用 Raspberry Pi 的 SPI-0 去操作一个时钟频率为 5 MHz,SPI 传输模式为 Mode3 的设备,代码如下:
SpiConnectionSettings settings = new SpiConnectionSettings(busId: 0, chipSelectLine: 0) { ClockFrequency = 5000000, Mode = SpiMode.Mode3 };TransferFullDuplex()
时,要求写入的数据和读取的数据长度要一致,并且能否使用也需要看设备是否支持。比如从地址为 0x00 的寄存器中向后连续读取 8 个字节的数据,并且向地址为 0x01 的寄存器写入一个字节的数据,代码如下:
// 读取 sensor.WriteByte(0x00); Span<byte> readBuffer = stackalloc byte[8]; sensor.Read(readBuffer); // 写入 Span<byte> writeBuffer = stackalloc byte[] { 0x01, 0xFF }; sensor.Write(writeBuffer); // 全双工读取 Span<byte> writeBuffer = stackalloc byte[8]; Span<byte> readBuffer = stackalloc byte[8]; writeBuffer[0] = 0x00; sensor.TransferFullDuplex(writeBuffer, readBuffer);本实验选用的是三轴加速度传感器 ADXL345 ,数据手册地址:http://wenku.baidu.com/view/87a1cf5c312b3169a451a47e.html 。
名称 | 数量 |
---|---|
ADXL345 | x1 |
杜邦线 | 若干 |