ESP32 序列周邊介面 SPI

ESP32教學系列(八):序列周邊介面(SPI)

 

前言

這次我們要介紹的主題是SPI通訊界面。一般在使用SPI介面傳輸的感測器或裝置時,通常還會再另外搭配其他函式庫,與ESP32原生的SPI關連不大,有關實際應用的內容我們會留到下一篇再進行說明。這篇文章將介紹如何使用ESP32的SPI,並且說明如何在兩片ESP32開發板之間使用SPI通訊。

 

什麼是SPI?

序列周邊介面(Serial Peripheral Interface, SPI),是微控制器常用的一種全雙工串列同步通訊界面,適用於與周邊設備進行短距離且高速的傳輸。它採用了主從式架構,由一個主設備和一至多個周邊設備組成,最早由摩托羅拉(Motorola)公司在1980年代中期開發。

在Flash(快閃記憶體)、EEPROM、RTC(實時時鐘)、ADC(類比數位轉換器)、DAC(數位類比轉換器)、液晶顯示器,或是兩個微控制器間的通訊都可以看到SPI的應用。但由於SPI不是一套具有標準規範的協議,它只是一種業界標準(de facto standard),所以不是每個使用SPI的設備都用相同的方式實現。

💡Tips: 過去 SPI 常以 Master 與 Slave 稱呼主從端的設備,但是開源硬體聯盟(Open Source Hardware Association, OSHWA)在A Resolution to Redefine SPI Signal Names這篇文章中建議應該使用新的命名方式,以 Controller 代替 MasterPeripheral 代替Slave,SCK(Serial Clock)的稱呼維持不變。

新舊接腳名稱對照:

接腳名稱(舊)接腳名稱(新)
MISO (Master In Slave Out)CIPO (Controller In Peripheral Out)
MOSI (Master Out Slave In)COPI (Controller Out Peripheral In)
SS (Slave Select Pin)CS (Chip Select Pin)

SPI通訊採用主從式架構,由主設備發起傳輸訊號,統一控制所有裝置傳輸,並控制設備與設備之間發送和讀取資料的時機。所有設備都共享同一個由主設備提供的時脈(clock)訊號。

 

SPI 匯流排。(圖片取自維基百科

一般SPI會有4條資料線,分別是:

  • CIPO/MISO: 周邊裝置輸出資料到控制器。
  • COPI/MOSI: 控制器輸出資料到周邊裝置。
  • SCK/SCLK(Serial Clock): 串列時脈,由主設備發出。時脈速度也決定了傳輸速度。
  • CS/SS: 晶片選擇接腳,低電位驅動。當同一條SPI Bus上有多個設備時,主設備會用CS接腳選擇要進行通訊的周邊設備。

SPI模式

當設備傳送資料時,在什麼時間點讀取訊號,會直接的影響設備之間的通訊內容。通常在SPI中,會以模式(Mode)來定義。模式由兩個條件構成:時脈相位(clock phase)與時脈極性(clock polarity)。

  • 時脈相位(CPHA):選擇訊號要在SCLK的上升緣或是下降緣進行採樣。 CPHA=0: 上升緣;CPHA = 1: 下降緣。
  • 時脈極性(CPOL):當SPI在閒置狀態時,SCLK的電位狀態。 (註:閒置狀態指的是CS接腳在傳輸開始與傳輸結束時的狀態) CPOL = 0: 閒置時保持低電位;CPOL = 1: 閒置時保持高電位。

根據相位和極性的變化,SPI就會產生4種不同的傳輸模式:

 

模式CLOCK POLARITY (CPOL) CLOCK PHASE (CPHA)
MODE000
MODE101
MODE210
MODE311

ESP32的SPI介面

ESP32總共有4個SPI介面,分別是SPI0~SPI3。SPI0 與 SPI1 已經被用來連接到晶片上的Flash Memory,我們一般可以自由使用的是SPI2與SPI3這兩個SPI控制器,其中SPI2又被稱作 HSPI(High-speed Serial Peripheral Interface),SPI3被稱作VSPI(Very High-speed Serial Peripheral Interface)。這兩個 SPI Bus 各自具有獨立的訊號,代表我們可以隨意將任意一項作為主設備或周邊設備,而不影響到另外一項。在把它們當作主設備的情況下,單一個SPI Bus最多可以驅動3個周邊設備。

HSPI 和 VSPI Bus的預設接腳如下:

💡Tips: 在某些SPI周邊設備的腳位標示中,COPI / MOSI 可能被標記為 SDO (Serial Data Out), CIPO / MISO 可能被標記為 SDI (Serial Data In)。

使用ESP32上面的SPI

VSPI(預設)

在沒有特別設定的情況下,ESP32的SPI預設使用VSPI(SPI3),因此只要照著預設接腳連接即可。

我們可以執行下面的範例查看開發板的預設接腳:

/* 顯示ESP32 SPI預設接腳 */

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.print("COPI / MOSI: ");
  Serial.println(MOSI);
  Serial.print("CIPO / MISO: ");
  Serial.println(MISO);
  Serial.print("SCK: ");
  Serial.println(SCK);
  Serial.print("CS / SS: ");
  Serial.println(SS);
  delay(1000);
  Serial.println();
}

執行結果

 

HSPI

如果要使用HSPI,就要先引入<SPI.h>這個標頭檔,然後用SPIClass函式實例化物件,並指定使用HSPI。

例如:

# include <SPI.h>

SPIClass MySPI2(HSPI);  // 指定MySPI2使用HSPI

void setup() {

  MySPI2.begin();       // 初始化MySPI2,使用HSPI預設接腳 (SCLK = 14, CIPO= 12, COPI =13, CS = 15)
  
	// 以下省略...
}

void loop() {
  
}

自訂義SPI接腳

SPIClass的begin函式也可以用來指定其他GPIO接腳:

MySPI2.begin(HSPI_SCLK, HSPI_CIPO, HSPI_COPI, HSPI_CS); //SCLK, CIPO, COPI, CS Pin

例如像是:

# include <SPI.h>

// 接腳定義
# define MySPI2_SCLK 14
# define MySPI2_CIPO 12
# define MySPI2_COPI 13
# define MySPI2_CS 15

# define SPI2CLK 1000000  // 1M Hz

SPIClass MySPI2(HSPI);

void setup() {
  MySPI2.begin(MySPI2_SCLK, MySPI2_CIPO, MySPI2_COPI, MySPI2_CS);  // 在begin()自定義SPI2的接腳
}

void loop() {
  
}

💡Tips: 當SPI裝置需要高速通訊時,建議使用預設接腳。

 

SPI傳輸流程

首先,設定好SPI的時脈頻率以及傳輸模式,然後使用beginTransaction初始化SPI介面。

SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); // 時脈1MHz, MSBFIRST, MODE0

接著用CS接腳選擇要傳送的周邊設備,將電壓準位拉低,啟用設備。

digitalWrite(CS_pin, LOW);

然後呼叫transfer()方法傳輸資料。

SPI.transfer(data);

傳輸完成後,再將CS接腳的電壓準位拉回高電位。

digitalWrite(CS_pin, HIGH);

最後,呼叫endTransaction(),釋放目前使用的SPI Bus。

SPI.endTransaction();

 

SPI訊號的實際輸出

下圖是指定ESP32當作SPI Controller輸出 0b11001100 的訊號,用邏輯分析儀捕捉到的波形:

這段通訊的傳輸模式是MODE 0,所以從圖片中可以看到,在每個時脈的上升緣讀取,MOSI接腳就會接收到 0b11001100 的資料。但是在試了幾次後都沒能抓到CS接腳的變化狀態,且CS出現的幾個尖波,也讓筆者相當不解。如果有網友知道原因,還請不吝告知。

 

測試程式碼

 /* ESP32 HSPI輸出測試 */
  
 # include <SPI.h>
  
 // 接腳定義
 # define MySPI2_SCLK 14
 # define MySPI2_CIPO 12
 # define MySPI2_COPI 13
 # define MySPI2_CS 15
  
 # define SPI2CLK 1000000 // 1M Hz
  
 SPIClass MySPI2(HSPI);
  
 void setup() {
 MySPI2.begin(MySPI2_SCLK, MySPI2_CIPO, MySPI2_COPI, MySPI2_CS); // 設定SPI2的接腳
  
 pinMode(MySPI2_CS, OUTPUT);
 }
  
 void loop() {
 MySPI2.beginTransaction(SPISettings(SPI2CLK, MSBFIRST, SPI_MODE0)); // 初始化SPI bus。與Arduino SPI用法相同
 digitalWrite(MySPI2_CS, LOW); // 將晶片選擇腳(CS)拉低,準備傳輸資料
 MySPI2.transfer(byte(0b11001100)); // 傳輸 b’11001100′
 digitalWrite(MySPI2_CS, HIGH); //將CS腳拉高,表示傳輸結束
 MySPI2.endTransaction(); // 釋放SPI Bus
 delay(100);
 }

(註: 這部分參考的是arduino-esp32 core SPI example的測試程式(Link))

從上面的範例中我們可以看到,在SPI資料傳輸階段,ESP32使用的程式語法與原生的Arduino相同。在傳輸資料前,會先將CS接腳接到低電位再送出資料。傳輸完成後再將CS接腳接回高電位,表示傳輸結束。

 

連接多個SPI設備

假如ESP32同時與周邊設備1以及設備2連接,要向周邊設備2傳送資料時,也是用同樣的方式設定。只是要先將周邊設備1的CS接腳設為高電位,再去設定設備2的CS接腳。接線部分需要注意Controller用了兩根不同的CS接腳。

void loop() {

  MySPI2.beginTransaction(SPISettings(SPI2CLK, MSBFIRST, SPI_MODE0)); // 初始化SPI bus。與Arduino SPI用法相同
  
  digitalWrite(MySPI2_CS1, HIGH); // 將設備1的晶片選擇腳(CS)拉高,禁用設備1
  digitalWrite(MySPI2_CS2, LOW); // 將設備2的晶片選擇腳(CS)拉低,準備傳輸資料

  MySPI2.transfer(byte(0b11001100)); // 傳輸二進位資料
  
  // 以下省略....
}

註:若將ESP32當作主設備讀取具有SPI介面的感測器資料,通常還會另外再搭配感測器的函式庫使用,但是此部分的程式碼與原生SPI的關聯性較低,本篇先不討論。

 

進階範例: 使用SPI在ESP32之間傳訊

接下來的範例會說明如何在兩片不同的ESP32開發板上面使用SPI通訊。我們把右邊的開發板當作主設備,左邊的開發板當作周邊設備,兩邊使用HSPI的預設接腳通訊,由主設備發送二進位訊息到周邊設備。當週邊設備接收到0x01訊號時,就會點亮開發板上的LED。

 

材料清單

  • ESP32開發板 x2micro USB 傳輸線 x1麵包板 x1杜邦線(公-公) x6

連接示意圖

 

 

腳位對照表

SPI BUS主設備周邊設備
COPIGPIO 13GPIO 13
CIPOGPIO 12GPIO 12
SCKGPIO 14GPIO 14
CSGPIO 15GPIO 15

主控端

下面的程式功能是讓主控端使用HSPI控制器固定間隔大約1秒鐘的時間,傳送訊號給周邊設備。

程式碼

 /* ESP32 SPI通訊 (Controller) */
  
 #include <SPI.h>
  
 // 接腳定義
 # define HSPI_SCLK 14
 # define HSPI_CIPO 12
 # define HSPI_COPI 13
 # define HSPI_CS 15
  
 static const int SPI_CLK = 1000000; // 1 MHz
  
 SPIClass * hspi = NULL;
  
 void setup() {
 hspi = new SPIClass(HSPI);
 hspi -> begin(HSPI_SCLK, HSPI_CIPO, HSPI_COPI, HSPI_CS);
 pinMode(HSPI_CS, OUTPUT);
 }
  
 void loop() {
 hspi_send_cmd();
 delay(100);
 }
  
 void hspi_send_cmd(){
 byte data_on = 0b00000001; // 0x01
 byte data_off = 0b00000000; // 0x00
  
 // 發送0x01
 hspi -> beginTransaction(SPISettings(SPI_CLK, MSBFIRST, SPI_MODE0)); // 準備傳輸
 digitalWrite(HSPI_CS, LOW);
 hspi -> transfer(data_on);
 digitalWrite(HSPI_CS, HIGH);
 hspi -> endTransaction(); // 傳輸結束
 delay(1000);
  
 // 發送0x00
 hspi -> beginTransaction(SPISettings(SPI_CLK, MSBFIRST, SPI_MODE0));
 digitalWrite(HSPI_CS, LOW);
 hspi -> transfer(data_off);
 digitalWrite(HSPI_CS, HIGH);
 hspi -> endTransaction();
 delay(1000);
 }

周邊設備端

由於<SPI.h>函式庫沒有支援將ESP32當作周邊設備的功能,所以我們需要先到函式庫管理員中下載ESP32SPISlave這個函式庫。

將下面的程式碼複製到IDE後上傳,打開序列埠監控視窗,就可以看到目前接收到的資料。

程式碼

(註:由於此函式庫使用Slave代稱周邊設備,因此程式碼中就沿用Slave的稱呼)

 /* ESP32 SPI通訊 (Slave) */
  
 #include <ESP32SPISlave.h>
  
 # define LED 2
  
 static constexpr uint32_t BUFFER_SIZE {32};
 uint8_t spi_slave_tx_buf[BUFFER_SIZE];
 uint8_t spi_slave_rx_buf[BUFFER_SIZE];
  
 ESP32SPISlave slave;
  
 void setup() {
 Serial.begin(115200);
 delay(2000);
 pinMode(LED, OUTPUT);
  
 slave.setDataMode(SPI_MODE0);
 slave.begin(); // 使用HSPI預設接腳
  
 // 清空buffer
 memset(spi_slave_tx_buf, 0, BUFFER_SIZE);
 memset(spi_slave_rx_buf, 0, BUFFER_SIZE);
 }
  
 void loop() {
 char data; // 紀錄收到的資料
  
 // 等待直到傳輸完成
 slave.wait(spi_slave_rx_buf, spi_slave_tx_buf, BUFFER_SIZE);
  
 while (slave.available()) {
 // 顯示收到的資料
 Serial.print(Command Received: );
 Serial.println(spi_slave_rx_buf[0]);
 data = spi_slave_rx_buf[0];
 slave.pop();
 }
 if(data == 1 )
 {
 Serial.println(點亮LED);
 digitalWrite(LED, HIGH);
 }
 else if(data == 0 )
 {
 Serial.println(熄滅LED);
 digitalWrite(LED, LOW);
 }
 Serial.println();
  
 }

執行結果

 

小結

我們可以使用ESP32上面的VSPI和HSPI這兩個獨立的SPI控制器進行SPI通訊,單一個SPI控制器可以驅動3個周邊裝置,所以最多可以控制到6個周邊裝置。在連接到多個周邊設備時,需要注意每個設備都要由不同的CS接腳控制。在這篇文章中,除了介紹預設的SPI接腳,也說明了如何用軟體重新定義接腳,以及使用SPI傳訊時實際的輸出訊號。最後透過範例說明如何在兩片ESP32間用SPI通訊。下一篇,我們將會說明如何在ESP32上面驅動SPI介面的電子紙。

You may also like...