什麼是 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 ,數據手冊地址:http://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 /// <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 的使用。