什么是 SPI
和上一篇文章的 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
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
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 buffer);
- // 从从设备中读取一个字节的数据
- public override byte ReadByte();
- // 全双工传输, 即主从设备同时传输
- // writeBuffer 为要写入从设备的数据
- // readBuffer 为要从从设备中读取的数据
- // 需要注意的是 writeBuffer 和 readBuffer 需要长度一致
- public override void TransferFullDuplex(ReadOnlySpan writeBuffer, Span readBuffer);
- // 向从设备中写入一段数据, 通常 Span 中的第一个数据为要写入数据的寄存器的地址
- public override void Write(ReadOnlySpan buffer);
- // 向从设备中写入一个字节的数据, 通常这个字节为寄存器的地址
- public override void WriteByte(byte value);
- }
SPI 的通信步骤
初始化 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 readBuffer = stackalloc byte[8];
- sensor.Read(readBuffer);
- // 写入
- Span writeBuffer = stackalloc byte[] {
- 0x01, 0xFF
- };
- sensor.Write(writeBuffer);
- // 全双工读取
- Span writeBuffer = stackalloc byte[8];
- Span readBuffer = stackalloc byte[8];
- writeBuffer[0] = 0x00;
- sensor.TransferFullDuplex(writeBuffer, readBuffer);
加速度传感器读取实验
本实验选用的是三轴加速度传感器 ADXL345 , 数据手册地址: https://wenku.baidu.com/view/87a1cf5c312b3169a451a47e.html .
传感器图像
硬件需求 名称 数量 ADXL345 x1 杜邦线 若干
电路
VCC - 3.3 V GND - GND CS - CS0 (Pin24) SDO - SPI0 MISO (Pin21) SDA - SPI0 MOSI (Pin19) SCL - SPI0 SCLK (Pin23)
代码 打开 Visual Studio , 新建一个 .NET Core 控制台应用程序, 项目名称为 "Adxl345". 引入 System.Device.Gpio NuGet 包.
新建类 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
- ///
- /// ADX1345 SPI 时钟频率
- ///
- public const int SpiClockFrequency = 5000000;
- ///
- /// ADX1345 SPI 传输模式
- ///
- public const SpiMode SpiMode = System.Device.Spi.SpiMode.Mode3;
- #endregion
- ///
- /// 加速度
- ///
- public Vector3 Acceleration => ReadAcceleration();
- ///
- /// 实例化一个 ADX1345
- ///
- /// SpiDevice
- public Adxl345(SpiDevice sensor)
- {
- _sensor = sensor;
- // 设置 ADXL345 测量范围
- // 数据手册 P28, 表 21
- Span dataFormat = stackalloc byte[] { ADLX_DATA_FORMAT, 0b_0000_0010 };
- // 设置 ADXL345 为测量模式
- // 数据手册 P24
- Span powerControl = stackalloc byte[] { ADLX_POWER_CTL, 0b_0000_1000 };
- _sensor.Write(dataFormat);
- _sensor.Write(powerControl);
- }
- ///
- /// 读取加速度
- ///
- /// 加速度
- private Vector3 ReadAcceleration()
- {
- int units = Resolution / _range;
- // 7 = 1 个地址 + 3 轴数据(每轴数据 2 字节)
- Span writeBuffer = stackalloc byte[7];
- Span readBuffer = stackalloc byte[7];
- writeBuffer[0] = ADLX_X0;
- _sensor.TransferFullDuplex(writeBuffer, readBuffer);
- Span 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;
- }
- ///
- /// 释放资源
- ///
- 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
- };
- UnixSpiDevice device = new UnixSpiDevice(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);
- }
- }
- }
发布, 拷贝, 更改权限, 运行
效果图
备注
下一篇文章将谈谈 PWM 的使用.
来源: https://www.2cto.com/kf/201905/810388.html