张高兴的 .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 从设备的示意图。
图源: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 是开启的)
sudo nano /boot/config.txt
dtoverlay=spi1-3cs
并保存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<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 ,数据手册地址: 。
传感器图像
硬件需求
名称 | 数量 |
---|---|
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 /// <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 }; 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 的使用。