張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊

張高興發表於2019-05-29

什麼是 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 從裝置的示意圖。

張高興的 .NET Core IoT 入門指南:(四)使用 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

張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊

該時序圖顯示了時鐘的極性和相位。圖源: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 是開啟的)

1. 使用編輯器開啟 /boot/config.txt ,如:sudo nano /boot/config.txt
2. 新增 dtoverlay=spi1-3cs 並儲存
3. 重啟

張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊

Raspberry Pi B+/2B/3B/3B+/Zero 引腳圖

相關類

SPI 操作的相關類位於 System.Device.SpiSystem.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

UnixSpiDeviceWindows10SpiDevice 類位於 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 的通訊步驟

  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

感測器影像

張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊

硬體需求

名稱 數量
ADXL345 x1
杜邦線 若干

電路

張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊

  • VCC - 3.3 V
  • GND - GND
  • CS - CS0 (Pin24)
  • SDO - SPI0 MISO (Pin21)
  • SDA - SPI0 MOSI (Pin19)
  • SCL - SPI0 SCLK (Pin23)

程式碼

  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
        };
        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);
            }
        }
    }
  5. 釋出、拷貝、更改許可權、執行

效果圖

張高興的 .NET Core IoT 入門指南:(四)使用 SPI 進行通訊


  備註

下一篇文章將談談 PWM 的使用。

相關文章