zoukankan      html  css  js  c++  java
  • 张高兴的 .NET Core IoT 入门指南:(四)使用 SPI 进行通信

    什么是 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 从设备的示意图。

    图1:SPI 设备

    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

    图2:该时序图显示了时钟的极性和相位

    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 命名空间下。

    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; }
    }
    

    SpiDevice

    SpiDevice 是一个抽象类,通过单例模式创建具体的对象。具体实现是通过两个内部类 UnixSpiDeviceWindows10SpiDevice ,分别代表 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 的通信步骤

    1. 初始化 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
      };
      
    2. 读取和写入

      读取和写入与 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
    杜邦线 若干

    电路

    • VCC - 3.3 V
    • GND - GND
    • CS - CS0 - GPIO 8 (Pin 24)
    • SDO - SPI0 MISO - GPIO 9 (Pin 21)
    • SDA - SPI0 MOSI - GPIO 10 (Pin 19)
    • SCL - SPI0 SCLK - GPIO 11 (Pin 23)

    使用 Docker 运行示例

    示例地址: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
    

    代码

    1. 打开 Visual Studio ,新建一个 .NET Core 控制台应用程序,项目名称为“Adxl345”。

    2. 引入 System.Device.Gpio NuGet 包。

    3. 新建类 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;
          }
      }
      
    4. 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);
              }
          }
      }
      
    5. 发布、拷贝、更改权限、运行

    效果图

    供参考

    1. Serial Peripheral Interface - Wikipedia:https://en.wikipedia.org/wiki/Serial_Peripheral_Interface
    2. SPI source code:https://github.com/dotnet/iot/tree/master/src/System.Device.Gpio/System/Device/Spi
    3. SPI - 百度百科:https://baike.baidu.com/item/SPI/4429726
  • 相关阅读:
    有问题的Py代码
    Python撑爆内存的代码
    python socket 绑定端口收发信息
    python socket UDP通信
    B类IP地址
    python in的用法
    Python continue的用法
    python27接受用户输入的数据
    基于jQuery实现左右图片轮播(原理通用)
    Jquery实现的简单轮播效果-代码
  • 原文地址:https://www.cnblogs.com/zhanggaoxing/p/10943822.html
Copyright © 2011-2022 走看看