什么是 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

张高兴的 .NET Core IoT 入门指南:(四)使用 SPI 进行通信的更多相关文章

  1. 张高兴的 .NET Core IoT 入门指南:(二)GPIO 的使用

    什么是 GPIO GPIO 是 General Purpose Input Output 的缩写,即"通用输入输出". Raspberry Pi 有两行 GPIO 引脚, Rasp ...

  2. 张高兴的 .NET Core IoT 入门指南:(一)环境配置、Blink、部署

    如何在 Raspberry Pi 的 Raspbian 上构建使用 GPIO 引脚的 IoT 程序?你可能会回答使用 C++ 或 Python 去访问 Raspberry Pi 的引脚.现在,C# 程 ...

  3. 张高兴的 .NET Core IoT 入门指南:(三)使用 I2C 进行通信

    什么是 I2C 总线 I2C 总线(Inter-Integrated Circuit Bus)是设备与设备间通信方式的一种.它是一种串行通信总线,由飞利浦公司在1980年代为了让主板.嵌入式系统或手机 ...

  4. 张高兴的 .NET Core IoT 入门指南:(五)PWM 信号输出

    什么是 PWM 在解释 PWM 之前首先来了解一下电路中信号的概念,其中包括模拟信号和数字信号.模拟信号是一种连续的信号,与连续函数类似,在图形上表现为一条不间断的连续曲线.数字信号为只能取有限个数值 ...

  5. 张高兴的 .NET Core IoT 入门指南:(五)串口通信入门

    在开始之前,首先要说明的是串口通信所用到的 SerialPort 类并不包含在 System.Device.Gpio NuGet 包中,而是在 System.IO.Ports NuGet 包中.之所以 ...

  6. 张高兴的 .NET IoT 入门指南:(七)制作一个气象站

    距离上一篇<张高兴的 .NET Core IoT 入门指南>系列博客的发布已经过去 2 年的时间了,2 年的时间 .NET 版本发生了巨大的变化,.NET Core 也已不复存在,因此本系 ...

  7. 张高兴的 .NET IoT 入门指南:(八)基于 GPS 的 NTP 时间同步服务器

    时间究竟是什么?这既可以是一个哲学问题,也可以是一个物理问题.古人对太阳进行观测,利用太阳的投影发明了日晷,定义了最初的时间.随着科技的发展,天文观测的精度也越来越准确,人们发现地球的自转并不是完全一 ...

  8. OpenCV入门指南----人脸检测

    本篇介绍图像处理与模式识别中最热门的一个领域——人脸检测(人脸识别).人脸检测可以说是学术界的宠儿,在不少EI,SCI高级别论文都能看到它的身影.甚至很多高校学生的毕业设计都会涉及到人脸检测.当然人脸 ...

  9. Web API 入门指南 - 闲话安全

    Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患.相关的防御技巧以及Web AP ...

随机推荐

  1. js函数篇

    1.闭包函数,作用:不污染全局变量,  定义:与外界隔离的独立作用域被称为闭包,使用函数实现该功能称为函数闭包: 写法: (function(){ function sayHello(){ conso ...

  2. vue.js 使用高德地图

    1.获取key值 注册成为高德开发者需要分三步: 第一步,注册高德开发者: 第二步,去控制台创建应用: 第三步,获取Key 2.修改配置文件  webpack.base.conf.js externa ...

  3. 使用Timer为界面执行异步任务

    -------------------siwuxie095                         工程名:TestSwingTimer 包名:com.siwuxie095.timer 类名: ...

  4. uva 10934 Dropping water balloons

    你有k个一模一样的水球,在一个n层楼的建筑物上进行测试,你想知道水球最低从几层楼往下丢可以让水球破掉.由于你很懒,所以你想要丢最少次水球来测出水球刚好破掉的最低楼层.(在最糟情况下,水球在顶楼也不会破 ...

  5. 10、scala模式匹配

    一.模式匹配1 1.介绍 模式匹配是Scala中非常有特色,非常强大的一种功能.模式匹配,其实类似于Java中的swich case语法,即对一个值进行条件判断,然后针对不同的条件, 进行不同的处理. ...

  6. 31.TCP/IP 三次握手与四次挥手

    TCP/IP三次握手 TCP建立连接为什么是三次握手,而不是两次或四次? TCP,名为传输控制协议,是一种可靠的传输层协议,IP协议号为6. 顺便说一句,原则上任何数据传输都无法确保绝对可靠,三次握手 ...

  7. 网页编程技术与实例 PDF扫描版

    本书主要包括:Web的概念,使用网页编辑工具制作网页,HTML语言的基本结构,JavaScrip和VBScript脚本语言的编程方法,ASP的概念,ASP对象的属性.方法和事件,SQL语言,数据库建议 ...

  8. 匿名委托(方法) 以 ThreadStart 为例

    Hello Tec.   匿名委托(方法) 以 ThreadStart 为例 REF:http://baike.baidu.com/view/2761370.htm?fr=aladdin   不使用匿 ...

  9. SuperSocket框架中BinaryRequestInfo协议的使用

    一.开发环境 1.Windows 10 企业版 64位 2.Microsoft Visual Studio 2017 企业版 二.项目开始 1.新建控制台程序,项目名称“BinarySuperSock ...

  10. OpenStack虚机状态变化图解

    对官网上内容的一个翻译,方便自己以后查找资料用 The following diagrams and tables show the required virtual machine (VM) sta ...