张高兴的 .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 |

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 是开启的)
sudo nano /boot/config.txtdtoverlay=spi1-3cs 并保存
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 是一个抽象类,通过单例模式创建具体的对象。具体实现是通过两个内部类 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 的通信步骤
初始化 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 |
| 杜邦线 | 若干 |
电路

- 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
代码
打开 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
/// <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);
}
}
}
发布、拷贝、更改权限、运行
效果图

供参考
- Serial Peripheral Interface - Wikipedia:https://en.wikipedia.org/wiki/Serial_Peripheral_Interface
- SPI source code:https://github.com/dotnet/iot/tree/master/src/System.Device.Gpio/System/Device/Spi
- SPI - 百度百科:https://baike.baidu.com/item/SPI/4429726
张高兴的 .NET Core IoT 入门指南:(四)使用 SPI 进行通信的更多相关文章
- 张高兴的 .NET Core IoT 入门指南:(二)GPIO 的使用
什么是 GPIO GPIO 是 General Purpose Input Output 的缩写,即"通用输入输出". Raspberry Pi 有两行 GPIO 引脚, Rasp ...
- 张高兴的 .NET Core IoT 入门指南:(一)环境配置、Blink、部署
如何在 Raspberry Pi 的 Raspbian 上构建使用 GPIO 引脚的 IoT 程序?你可能会回答使用 C++ 或 Python 去访问 Raspberry Pi 的引脚.现在,C# 程 ...
- 张高兴的 .NET Core IoT 入门指南:(三)使用 I2C 进行通信
什么是 I2C 总线 I2C 总线(Inter-Integrated Circuit Bus)是设备与设备间通信方式的一种.它是一种串行通信总线,由飞利浦公司在1980年代为了让主板.嵌入式系统或手机 ...
- 张高兴的 .NET Core IoT 入门指南:(五)PWM 信号输出
什么是 PWM 在解释 PWM 之前首先来了解一下电路中信号的概念,其中包括模拟信号和数字信号.模拟信号是一种连续的信号,与连续函数类似,在图形上表现为一条不间断的连续曲线.数字信号为只能取有限个数值 ...
- 张高兴的 .NET Core IoT 入门指南:(五)串口通信入门
在开始之前,首先要说明的是串口通信所用到的 SerialPort 类并不包含在 System.Device.Gpio NuGet 包中,而是在 System.IO.Ports NuGet 包中.之所以 ...
- 张高兴的 .NET IoT 入门指南:(七)制作一个气象站
距离上一篇<张高兴的 .NET Core IoT 入门指南>系列博客的发布已经过去 2 年的时间了,2 年的时间 .NET 版本发生了巨大的变化,.NET Core 也已不复存在,因此本系 ...
- 张高兴的 .NET IoT 入门指南:(八)基于 GPS 的 NTP 时间同步服务器
时间究竟是什么?这既可以是一个哲学问题,也可以是一个物理问题.古人对太阳进行观测,利用太阳的投影发明了日晷,定义了最初的时间.随着科技的发展,天文观测的精度也越来越准确,人们发现地球的自转并不是完全一 ...
- OpenCV入门指南----人脸检测
本篇介绍图像处理与模式识别中最热门的一个领域——人脸检测(人脸识别).人脸检测可以说是学术界的宠儿,在不少EI,SCI高级别论文都能看到它的身影.甚至很多高校学生的毕业设计都会涉及到人脸检测.当然人脸 ...
- Web API 入门指南 - 闲话安全
Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患.相关的防御技巧以及Web AP ...
随机推荐
- 【转】 Pro Android学习笔记(三二):Menu(3):Context菜单
目录(?)[-] 什么是Context menu 注册View带有Context menu 填Context菜单内容 Context菜单点击触发 什么是Context menu 在桌面电脑,我们都很熟 ...
- 九 fork/join CompletableFuture
1: Fork/join fork/join: fork是分叉的意思, join是合并的意思. Fork/Join框架:是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务 ...
- uva 512
1. 问题 不知道怎么存储操作 看代码注释,else if等 2. 代码 #include <iostream> #include <stdio.h> #include < ...
- HTTP ERROR
HTTP 400 – 请求无效HTTP 401.1 – 未授权:登录失败HTTP 401.2 – 未授权:服务器配置问题导致登录失败HTTP 401.3 – ACL 禁止访问资源HTTP 401.4 ...
- php学习笔记-可变变量
看一个例子. <?php $a = 'hello'; $hello = 'hi'; echo $$a; ?> 如果一个变量名前面有两个美元符号,那么这个变量就叫做可变变量.就拿上面这个举例 ...
- macos上改变输入法顺序
设置界面上是不能拖放顺序的,唯一解决办法是: 一.先选择所有文档使用相同输入源 二.选择用美国英语 三.再选择允许多个输入源,再打开原来的中文输入法 顺序就调过来了!尼玛,这就是苹果的人性化?懒得吐嘈 ...
- 10. CTF综合靶机渗透(三)
靶机说明 斗牛犬工业公司最近将其网站污损,并由恶意德国牧羊犬黑客团队拥有.这是否意味着有更多的漏洞可以利用?你为什么不知道?:) 这是标准的Boot-to-Root.你唯一的目标是进入根目录并看到祝贺 ...
- Django 的认证系统
Django自带的用户认证 auth 模块 from django.contrib import autu django.contrib.auth 中提供了许多方法, 这里主要介绍其中三个: auth ...
- 【mysql-索引+存储过程+函数+触发器-更新。。。】
BaseOn ===>MySQL5.6 一:索引 1:创建索引: create index nameIndex on seckill(name) ; 2:查看索引: show index fro ...
- 安装mysql8.0.11及修改root密码、连接navicat for mysql的思路详解
1.1. 下载: 官网下载zip包,我下载的是64位的: 下载地址:https://dev.mysql.com/downloads/mysql/ 下载zip的包: 下载后解压:(解压在哪个盘都可以的) ...