การใช้งาน SPI และการเขียนโปรแกรม Arduino สำหรับ ESP32#


SPI Bus#

ไมโครคอนโทรลเลอร์อย่างเช่น ESP32 มีขา GPIO สำหรับการใช้งานบัส SPI (Serial Peripheral Interface) และมีวงจรภายใน SPI Controller จำนวน 4 ชุด ได้แก่ SPI0 / SPI1 เป็นสองชุดแรกแต่จะใช้ในการเขียนอ่านข้อมูลในหน่วยความจำ SPI Flash / PSRAM อีกสองชุดคือ HSPI (SPI2) และ VSPI (SPI3) ใช้ได้สำหรับวัตถุประสงค์ทั่วไป

โดยทั่วไปแล้ว ก็จะให้ ESP32 ทำหน้าที่เป็น SPI Master หรือ SPI Controller หรือตัวกำหนดจังหวะการสื่อสารข้อมูลกับอุปกรณ์อื่น ซึ่งอีกฝ่ายหนึ่งเรียกว่า SPI Slave หรือ SPI Peripheral

Standard SPI (Normal) ใช้สัญญาณ 4 เส้น (ใช้งานตามรูปแบบที่เรียกว่า 4-Wire SPI) ได้แก่

  • SCK (Serial Clock) — เป็นสัญญาณ CLK ที่ถูกสร้างโดยอุปกรณ์ที่เป็น SPI Master
  • MOSI (Master-Out, Slave-In) หรือ COPI (Controller-Out, Peripheral-In) — เป็นสัญญาณสำหรับส่งข้อมูลบิตออกจาก SPI Master ไปยัง SPI Slave
  • MISO (Master-In, Slave-Out) หรือ หรือ CIPO (Controller-In, Peripheral-Out) — เป็นสัญญาณสำหรับส่งข้อมูลบิตออกจาก SPI Slave ไปยัง SPI Master
  • SS (Slave Select) หรือ CS (Chip Select) — เป็นสัญญาณที่สร้างโดย SPI Master และทำงานแบบ Active-Low (เช่น มีสัญลักษณ์ # เขียนกำกับไว้ข้างหน้าหรือหลังชื่อสัญญาณ) เพื่อใช้ระบุว่า ต้องการสื่อสารกับ SPI Slave หรือไม่

รูป: ตัวอย่างการสื่อสารข้อมูลสำหรับชิปหน่วยความจำด้วย Standard SPI Transaction

 

วงจร SPI Controller ของ ESP32 รองรับการสื่อสารด้วยบัส SPI และมีขา I/O ที่เกี่ยวข้อง รวมทั้งหมด 6 ขา ได้แก่

  • CS#
  • SCK
  • MOSI / D0
  • MISO / D1
  • WP# / D2
  • HOLD# / D3

และมีการแบ่งโหมดของ SPI Controller ให้เลือกใช้งานได้ดังนี้

  • Standard SPI (4-Wire)
  • Dual SPI / Dual Output
  • Dual I/O (DIO)
  • Quad SPI / Quad Output
  • Quad I/O (QIO)

ในกรณีที่ใช้ชิปประเภทหน่วยความจำ เช่น QSPI NOR Flash การสื่อสารด้วยบัส SPI ในแต่ละครั้ง จะต้องมีการส่งคำสั่ง (Command หรือ Opcode Bits) ตามด้วยแอดเดรส (Address Bits) และข้อมูล (Data Bits) ตามลำดับ โหมดการทำงานของ SPI จึงมีให้เลือกใช้แตกต่างกันตามจำนวนของขาสัญญาณที่ใช้สำหรับการกำหนดค่าแอดเดรสและข้อมูล (Single vs. Multiple Address Lines & Data Lines) เช่น 2 ขา (Dual) หรือ 4 ขา (Quad)

ตาราง: โหมดการใช้งาน SPI Controller ของ ESP32 สำหรับการสื่อสารกับชิปหน่วยความจำ SPI Flash

 

ขา GPIO ที่เหมาะสมสำหรับการใช้งานบัส HSPI ของ ESP32 ได้แก่

  • HSPI-CS = GPIO 15
  • HSPI-SCK = GPIO 14
  • HSPI-MOSI = GPIO 13
  • HSPI-MISO = GPIO 12
  • HSPI-QUADWP = GPIO 2
  • HSPI-QUADHD = GPIO 4

ขา GPIO ที่เหมาะสมสำหรับการใช้งานบัส VSPI ของ ESP32 ได้แก่

  • VSPI-CS = GPIO 5
  • VSPI-SCK = GPIO 18
  • VSPI-MOSI = GPIO 23
  • VSPI-MISO = GPIO 19
  • VSPI-QUADWP = GPIO 22
  • VSPI-QUADHD = GPIO 21

บริษัท Espressif ได้จัดทำ API ซึ่งเป็นส่วนหนึ่งของ ESP-IDF เพื่อใช้ในการเขียนโปรแกรม C/C++ สำหรับ HSPI และ VSPI โดยแบ่งเป็น 2 ลักษณะการใช้งานได้แก่

หากต้องการเขียนโปรแกรมด้วย Arduino-ESP32 เพื่อใช้งาน HSPI หรือ VSPI และทำหน้าที่เป็น SPI Controller ก็มีไลบรารีที่มีชื่อว่า SPI ประกอบด้วยสองไฟล์ที่สำคัญคือ SPI.h และ SPI.cpp ในไฟล์ดังกล่าวมีการสร้างคลาส C++ ที่มีชื่อว่า SPIClass เอาไว้ใช้งาน และมีการประกาศตัวแปรภายนอกจากคลาสดังกล่าวและมีชื่อว่า SPI ไว้ให้แล้วสำหรับการใช้งาน VSPI (หรือจะสร้างตัวแปรใหม่ก็ได้) มีการกำหนดค่าเริ่มต้น (default) สำหรับการใช้งาน เช่น ความถี่ของ SPI SCLK เท่ากับ 1MHz ทำงานในโหมด 0 (SPI Mode 0) และส่งข้อมูลตามลำดับบิตแบบ MSB First

รูป: ตัวอย่างโค้ดบางส่วนจากไฟล์ SPI.h

ในไฟล์ SPI.h จะเห็นได้ว่า มีคำสั่งต่าง ๆ หรือเมธอดของคลาส SPIClass เช่น

  • void begin( int8_t sck, int8_t miso, int8_t mosi, int8_t ss ) เริ่มต้นใช้งาน SPI Controller โดยระบุขาที่จะใช้งานสำหรับ SPI Controller
  • void end() จบการใช้งาน SPI Controller และขา GPIO ที่เกี่ยวข้อง
  • void setHwCs( bool use ) ตั้งค่าการใช้งานขา Chip Select (CS) ของ SPI Controller ที่เกี่ยวข้อง ถ้ากำหนดค่าเป็น false จะต้องเลือกใช้ขา GPIO ให้เป็นขาเอาต์พุต โดยผู้ใช้เอง และกำหนดค่าลอจิกของขาดังกล่าวให้เป็น LOW ก่อนส่งข้อมูลด้วย SPI ทุกครั้ง แต่ถ้าเป็น true การทำงานของขา CS จะถูกควบคุมโดย SPI Controller โดยอัตโนมัติ
  • void setBitOrder( uint8_t bitOrder ) เลือกลำดับการส่งข้อมูล MSB First หรือ LSB First อย่างใดอย่างหนึ่ง
  • void setDataMode( uint8_t dataMode ) เลือกโหมดการทำงานของ SPI ซึ่งมีให้เลือก 4 โหมด ได้แก่ SPI_MODE0 .. SPI_MODE3
  • void setFrequency( uint32_t freq ) ตั้งค่าความถี่ของสัญญาณ SPI SCLK
  • uint8_t transfer( uint8_t data ) ส่งข้อมูลออกหนึ่งไบต์ และอ่านข้อมูลเข้ามาด้วยเช่นกัน (ได้เป็นค่ากลับคืนของฟังก์ชัน)
  • uint16_t transfer16( uint16_t data ) ส่งข้อมูลออก 2 ไบต์ และอ่านข้อมูลเข้ามาด้วยเช่นกัน (ได้เป็นค่ากลับคืนของฟังก์ชัน)
  • uint32_t transfer32( uint32_t data ) ส่งข้อมูลออก 4 ไบต์ และอ่านข้อมูลเข้ามาด้วยเช่นกัน (ได้เป็นค่ากลับคืนของฟังก์ชัน)
  • void transferBytes( const uint8_t *data, uint8_t *out, uint32_t size ) ส่งและรับข้อมูลหลายไบต์ โดยใช้ข้อมูลขาออกจากอาร์เรย์ data และเก็บข้อมูลขาเข้าที่รับได้ลงในอาร์เรย์ out ตามจำนวนที่ระบุโดย size
  • void write( uint8_t data ) ส่งข้อมูลออกหนึ่งไบต์ (Write Only ไม่สนใจข้อมูลขาเข้า)
  • void write16( uint16_t data ) ส่งข้อมูลออก 2 ไบต์ (Write Only)
  • void write32( uint32_t data ) ส่งข้อมูลออก 4 ไบต์ Write Only)
  • void writeBytes( const uint8_t *data, uint32_t size ) ส่งข้อมูลออกหลายไบต์จากข้อมูลที่อยู่ในอาร์เรย์ data ตามจำนวนที่ระบุโดย size

 


▷ ตัวอย่างโค้ดสาธิตการใช้งาน SPI Controller#

ถัดไปเป็นตัวอย่างการเขียนโค้ด Arduino Sketch เพื่อสาธิตการใช้งาน HSPI หรือ VSPI อย่างใดอย่างหนึ่ง ให้ทำหน้าที่เป็น SPI Controller (Standard SPI)

ในตัวอย่างนี้ มีการสร้างตัวแปรชื่อ spidev จากคลาส SPIclass ได้เลือกใช้ SPI Mode 0 และส่งข้อมูลแบบ MSB First และได้กำหนดความถี่สำหรับ SCK ไว้เท่ากับ 4MHz เปรียบเทียบกับ 16MHz เมื่อมีการสื่อสารด้วยบัส SPI สัญญาณ CS# จะเปลี่ยนจาก HIGH เป็น LOW โดยอัตโนมัติ

ในการทดสอบการทำงานของโค้ดตัวอย่างนี้ จะต้องเชื่อมต่อขาสัญญาณ MOSI ไปยัง MISO เพื่อรับข้อมูลที่ถูกส่งออกไปให้กลับเข้ามา (ส่งข้อมูล เช่น 4 ไบต์ต่อหนึ่งรอบ) แล้วนำค่าที่อ่านได้มาแสดงผลเป็นข้อความทาง Serial ดังนั้นจึงเป็นการทดสอบแบบ SPI Loopback

#include "SPI.h"

// Define the SPI clock frequency (Hz)
#define SPI_SPEED  (16000000)

//#define USE_HSPI
#define USE_VSPI

#ifdef USE_HSPI
// Set the HSPI pins to use for the connection
#define HSPI_MOSI  (13)
#define HSPI_MISO  (12)
#define HSPI_SCK   (14)
#define HSPI_CS    (15)
// Global variable
SPIClass spidev(HSPI); // VSPI is SPI2.
#endif

#ifdef USE_VSPI
// Set the VSPI pins to use for the connection
#define VSPI_MOSI  (23)
#define VSPI_MISO  (19)
#define VSPI_SCK   (18)
#define VSPI_CS    (5)
// Global variable
SPIClass spidev(VSPI); // VSPI is SPI3.
#endif

#define BUFSIZE  (4)
uint8_t wdata[BUFSIZE]; // Write data buffer
uint8_t rdata[BUFSIZE]; // Read data buffer

void setup() {
  Serial.begin(115200);
  Serial.println("ESP32 SPI Demo... ");
  Serial.flush();

#ifdef USE_HSPI
  // Initialize the HSPI interface.
  spidev.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
#endif

#ifdef USE_VSPI
  // Initialize the VSPI interface.
  spidev.begin(VSPI_SCK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
#endif

  // Use the hardware SPI chip select pin
  spidev.setHwCs(true);
  // Set the SPI bit order to MSB first
  spidev.setBitOrder(MSBFIRST);
  // Set the SPI data mode to mode 0
  spidev.setDataMode(SPI_MODE0);
  // Set the SPI clock frequency
  spidev.setFrequency(SPI_SPEED);
}

void loop() {  
  for ( int i=0; i < BUFSIZE; i++ ) {
    wdata[i] = (uint8_t)i;
  }
  memset( rdata, 0xFF, BUFSIZE );

  // Send / receive data bytes to the SPI bus
  spidev.transferBytes( wdata, rdata, BUFSIZE );

  // Show data bytes received from the SPI bus
  for ( int i=0; i < BUFSIZE; i++ ) {
    Serial.printf("%02X ", rdata[i] );
    if ( i % 8 == 7 ) {
      Serial.println();
    }
  }
  Serial.println();
  delay(1000);
}

 

ผลจากการทดสอบการทำงานของโค้ดตัวอย่าง โดยใช้บอร์ด Wemos Lolin32 Lite และการวัดสัญญาณ CS, SCLK และ MOSI (สัญญาณช่องที่ 1-3 ตามลำดับ) มีดังนี้

รูป: ตัวอย่างคลื่นสัญญาณที่วัดได้ด้วยออสซิลโลสโคป เมื่อใช้ความถี่ 4MHz สำหรับ SCLK

รูป: ตัวอย่างคลื่นสัญญาณที่วัดได้ด้วยออสซิลโลสโคป เมื่อใช้ความถี่สูงขึ้นเป็น 16MHz สำหรับ SCLK

จากรูปคลื่นสัญญาณจะเห็นได้ว่า การใช้ความถี่สูงขึ้นสำหรับ SCLK จะส่งผลต่อคุณภาพของสัญญาณที่ลดลง (และอาจทำให้เกิดความผิดพลาดในการส่งและรับข้อมูลได้)

 

การใช้คำสั่งหรือเมธอด transferBytes(...) ของตัวแปร spidev (คลาส SPIClass) เป็นการส่งและรับข้อมูลหลายไบต์ในคำสั่งเดียวกัน โดยมี wdata เป็นอาร์เรย์สำหรับข้อมูลไบต์ที่จะถูกส่งออกไป และมี rdata เป็นอาร์เรย์สำหรับเอาไว้เก็บข้อมูลไบต์ที่ได้รับเข้ามา ตามจำนวนไบต์หรือขนาดของอาร์เรย์ (BUFSIZE)

 // Send data bytes to the SPI bus
 spidev.transferBytes( wdata, rdata, BUFSIZE );

หรือจะลองใช้คำสั่ง SPISettings(...) เพื่อกำหนดความถี่ของบัส ลำดับการส่งข้อมูลบิต และโหมดการทำงานของ SPI ก่อนการส่งและรับข้อมูลในแต่ละครั้งด้วย (เรียกว่า SPI Transaction) โดยจะต้องใช้ร่วมกับคำสั่ง beginTransaction(...) และ endTransaction() สำหรับตัวแปร spidev

 // Start SPI transaction
 spidev.beginTransaction( SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0) );
 // Send data bytes to the SPI bus
 spidev.transferBytes( wdata, rdata, BUFSIZE );
 // End SPI transaction
 spidev.endTransaction();

 


▷ การกำหนดสถานะลอจิกของสัญญาณ Chip Select (CS)#

ตัวอย่างถัดไปสาธิตการเขียนโค้ด เพื่อส่งข้อมูลโดยใช้ VSPI และมีการใช้คำสั่ง digitalWrite() เพื่อเปลี่ยนสถานะลอจิกของขา CS จาก HIGH เป็น LOW (ไม่ได้เกิดขึ้นโดยอัตโนมัติ) ก่อนการส่ง-รับข้อมูล แล้วเปลี่ยนกลับให้เป็น HIGH หลังจากจบการทำงาน

#include "SPI.h"

// Set the VSPI pins to use for the connection
#define VSPI_MOSI  (23)
#define VSPI_MISO  (19)
#define VSPI_SCK   (18)
#define VSPI_CS    (5)
#define SPI_SPEED  (4000000)

// Create a global variable for the VSPI interface
SPIClass spidev(VSPI);  // VSPI=3

#define BUFSIZE (4)
uint8_t wdata[BUFSIZE];
uint8_t rdata[BUFSIZE];

void setup() {
  Serial.begin(115200);
  Serial.println("ESP32 VSPI Demo... ");
  Serial.flush();
  // Initialize the SPI interface.
  spidev.begin(VSPI_SCK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
  // Do not use the hardware SPI chip select pin
  spidev.setHwCs(false);
  // Set the SPI bit order to MSB first
  spidev.setBitOrder(MSBFIRST);
  // Set the SPI data mode to mode 0
  spidev.setDataMode(SPI_MODE0);
  // Set the SPI clock frequency
  spidev.setFrequency(SPI_SPEED);
  // Set the GPIO pin for user-defined SPI CS
  pinMode( VSPI_CS, OUTPUT );
  // Set the SPI CS pin to HIGH
  digitalWrite( VSPI_CS, HIGH );
}

void loop() { 
  for ( int i=0; i < BUFSIZE; i++ ) {
    wdata[i] = (uint8_t)i;
  }
  memset( rdata, 0xFF, BUFSIZE );

  // Assert the VSPI_CS line (change to LOW)
  digitalWrite( VSPI_CS, LOW );
  // Send and receive data bytes
  spidev.transferBytes( wdata, rdata, BUFSIZE );
  // Deassert the VSPI_CS line (change to HIGH)
  digitalWrite( VSPI_CS, HIGH );

  // Show the received data bytes
  for ( int i=0; i < BUFSIZE; i++ ) {
    Serial.printf("%02X ", rdata[i] );
    if ( i % 8 == 7 ) {
      Serial.println();
    }
  }
  Serial.println();
  delay(1000);
}

รูป: ตัวอย่างรูปคลื่นสัญญาณที่ได้

 


กล่าวสรุป#

บทความนี้ได้กล่าวถึง การใช้งานวงจร SPI Controller เช่น HSPI และ VSPI ของชิป ESP32 ในเบื้องต้น และตัวอย่างการเขียนโค้ด Arduino-ESP32 เพื่อใช้งานในโหมด Standard SPI

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2022-12-26 | Last Updated: 2022-12-27