和上一篇文章的 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 从设备的示意图。css
图1:SPI 设备html
SPI 还有一个重要的概念就是时钟的极性(CPOL,Clock Polarity)和相位(CPHA,Clock Phase),对其这里不过多解释,咱们只须要知道极性和相位的组合构成了 SPI 的传输模式(SPI Mode)。在数据手册中,只要是 SPI 通讯协议的,必定会给出传输模式,咱们根据数据手册进行设置便可。SPI 的传输模式是有固定编号的,下表给出了各个模式,经常使用的模式有 Mode0 和 Mode3。git
SPI Mode | CPOL | CPHA |
---|---|---|
Mode0 | 0 | 0 |
Mode1 | 0 | 1 |
Mode2 | 1 | 0 |
Mode3 | 1 | 1 |
图2:该时序图显示了时钟的极性和相位github
SPI 相比较 I2C 最大的优势就是传输速率高,而且数据在同一时间内能够双向传输,这都得益于它的两根输入和输出数据线。固然缺点也很明显,比 I2C 多了两根线,这就要多占用两个 IO 接口。并且 SPI 采用 CS 线去选择设备,不像 I2C 有寻址机制,若是你有不少个 SPI 设备须要链接的话 IO 接口的占用数量是至关高的。docker
在 Raspberry Pi 的引脚中,引出了两组 SPI 接口。但有意思的是,在 Raspbian 中 SPI-1 是被禁用的,你须要修改一些参数去启用 SPI-1。SPI 接口的引脚编号以下图所示。c#
提示编辑器
如何在 Raspbian 上开启 SPI-1?(在 Win10 IoT 上 SPI-1 是开启的)ide
sudo nano /boot/config.txt
dtoverlay=spi1-3cs
并保存
Raspberry Pi B+/2B/3B/3B+/Zero 引脚图svg
SPI 操做的相关类位于 System.Device.Spi 命名空间下。函数
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; } }
SpiDevice
是一个抽象类,经过单例模式建立具体的对象。具体实现是经过两个内部类 UnixSpiDevice
和 Windows10SpiDevice
,分别表明 Unix 和 Windows10 下的 SPI 控制器。
public abstract partial class SpiDevice : IDisposable { // 建立 SpiDevice 对象 public static SpiDevice Create(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); }
初始化 SPI 链接设置 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 };
读取和写入
读取和写入与 I2C 相似,这里再也不过多赘述,详见上一篇博客,这里只提供一个代码示例。惟一要说明的就是使用全双工通讯 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 |
杜邦线 | 若干 |
示例地址:https://github.com/ZhangGaoxing/dotnet-core-iot-demo/tree/master/src/Adxl345
docker build -t adxl-sample -f Dockerfile . docker run --rm -it --device /dev/spidev0.0 adxl-sample
新建类 Adxl345,替换以下代码:
public class Adxl345 : IDisposable { #region 寄存器地址 private const byte ADLX_POWER_CTL = 0x2D; // 电源控制地址 private const byte ADLX_DATA_FORMAT = 0x31; // 范围地址 private const byte ADLX_X0 = 0x32; // X轴数据地址 private const byte ADLX_Y0 = 0x34; // Y轴数据地址 private const byte ADLX_Z0 = 0x36; // Z轴数据地址 #endregion private SpiDevice _sensor = null; private readonly int _range = 16; // 测量范围(-8,8) private const int Resolution = 1024; // 分辨率 #region SpiSetting /// <summary> /// ADX1345 SPI 时钟频率 /// </summary> public const int SpiClockFrequency = 5000000; /// <summary> /// ADX1345 SPI 传输模式 /// </summary> public const SpiMode SpiMode = System.Device.Spi.SpiMode.Mode3; #endregion /// <summary> /// 加速度 /// </summary> public Vector3 Acceleration => ReadAcceleration(); /// <summary> /// 实例化一个 ADX1345 /// </summary> /// <param name="sensor">SpiDevice</param> public Adxl345(SpiDevice sensor) { _sensor = sensor; // 设置 ADXL345 测量范围 // 数据手册 P28,表 21 Span<byte> dataFormat = stackalloc byte[] { ADLX_DATA_FORMAT, 0b_0000_0010 }; // 设置 ADXL345 为测量模式 // 数据手册 P24 Span<byte> powerControl = stackalloc byte[] { ADLX_POWER_CTL, 0b_0000_1000 }; _sensor.Write(dataFormat); _sensor.Write(powerControl); } /// <summary> /// 读取加速度 /// </summary> /// <returns>加速度</returns> private Vector3 ReadAcceleration() { int units = Resolution / _range; // 7 = 1个地址 + 3轴数据(每轴数据2字节) Span<byte> writeBuffer = stackalloc byte[7]; Span<byte> readBuffer = stackalloc byte[7]; writeBuffer[0] = ADLX_X0; _sensor.TransferFullDuplex(writeBuffer, readBuffer); Span<byte> readData = readBuffer.Slice(1); // 切割空白数据 // 将小端数据转换成正常的数据 short AccelerationX = BinaryPrimitives.ReadInt16LittleEndian(readData.Slice(0, 2)); short AccelerationY = BinaryPrimitives.ReadInt16LittleEndian(readData.Slice(2, 2)); short AccelerationZ = BinaryPrimitives.ReadInt16LittleEndian(readData.Slice(4, 2)); Vector3 accel = new Vector3 { X = (float)AccelerationX / units, Y = (float)AccelerationY / units, Z = (float)AccelerationZ / units }; return accel; } /// <summary> /// 释放资源 /// </summary> public void Dispose() { _sensor?.Dispose(); _sensor = null; } }
在 Program.cs 中,将主函数代码替换以下:
static void Main(string[] args) { SpiConnectionSettings settings = new SpiConnectionSettings(busId: 0, chipSelectLine: 0) { ClockFrequency = Adxl345.SpiClockFrequency, Mode = Adxl345.SpiMode }; SpiDevice device = SpiDevice.Create(settings); using (Adxl345 sensor = new Adxl345(device)) { while (true) { Vector3 data = sensor.Acceleration; Console.WriteLine($"X: {data.X.ToString("0.00")} g"); Console.WriteLine($"Y: {data.Y.ToString("0.00")} g"); Console.WriteLine($"Z: {data.Z.ToString("0.00")} g"); Console.WriteLine(); Thread.Sleep(500); } } }
发布、拷贝、更改权限、运行