SPI là gì: MOSI, MISO, SCK, CS, bốn chế độ và cách dùng trên ESP32

Giải thích giao tiếp SPI: bốn dây MOSI, MISO, SCK, CS, truyền hai chiều cùng lúc và bốn chế độ clock. Kèm hai chương trình ESP32: tự gửi tự nhận để kiểm tra bus và gửi lệnh thô cho thẻ nhớ.

SPI là chuẩn giao tiếp nhanh nhất trong ba chuẩn cơ bản của vi điều khiển. Thẻ nhớ, màn hình màu, module vô tuyến và các bộ chuyển đổi tốc độ cao đều dùng SPI. Đổi lại, SPI cần nhiều dây hơn I2C và mỗi thiết bị cần thêm một dây chọn riêng. Bài viết giải thích bốn dây của SPI, cách dữ liệu đi hai chiều cùng lúc, bốn chế độ clock, rồi thực hành trên ESP32 bằng một phép tự kiểm tra và bằng cách gửi lệnh thô cho thẻ nhớ.

1. Bus SPI gồm những gì?

Sơ đồ bus SPI: ESP32 làm thiết bị chủ nối hai thiết bị tớ bằng ba dây dùng chung SCK, MOSI, MISO và hai dây chọn CS1, CS2 riêng cho từng thiết bị
Ba dây SCK, MOSI, MISO dùng chung; mỗi thiết bị tớ có một dây CS riêng.
  • SCK (Serial Clock): xung clock do thiết bị chủ phát.
  • MOSI (Master Out, Slave In): dữ liệu từ thiết bị chủ sang thiết bị tớ.
  • MISO (Master In, Slave Out): dữ liệu từ thiết bị tớ về thiết bị chủ.
  • CS (Chip Select, còn ghi là SS): dây chọn thiết bị, tích cực ở mức thấp. Mỗi thiết bị tớ có một dây CS riêng.

Trên module, các chân này có thể mang tên khác: SDI, DIN hoặc SI thay cho MOSI; SDO, DOUT hoặc SO thay cho MISO; CLK thay cho SCK. SPI không có địa chỉ và không có bit ACK như I2C: thiết bị chủ chọn thiết bị bằng dây CS, và không tự biết được thiết bị tớ có đang nghe hay không.

2. Dữ liệu đi hai chiều cùng lúc

Giản đồ tín hiệu SPI ở chế độ 0 khi truyền một byte: CS ở mức thấp, SCK phát tám nhịp, MOSI mang giá trị 0xA5 và MISO mang giá trị 0x3C, dữ liệu được đọc ở sườn lên của SCK
Trong 8 nhịp clock, thiết bị chủ gửi 0xA5 và cùng lúc nhận về 0x3C (hình vẽ minh họa).
  • Thiết bị chủ kéo CS xuống mức thấp, phát 8 nhịp clock cho mỗi byte, rồi đưa CS lên lại.
  • Ở mỗi nhịp, một bit đi ra trên MOSI và một bit đi vào trên MISO. Vì vậy mỗi byte gửi đi luôn kèm một byte nhận về, dù byte đó có ý nghĩa hay không.
  • Muốn đọc dữ liệu từ thiết bị tớ, thiết bị chủ vẫn phải gửi đi một byte nào đó (thường là 0xFF hoặc 0x00) để có clock.
  • Thông thường bit cao nhất đi trước.

3. Bốn chế độ SPI

Bốn chế độ SPI theo CPOL và CPHA: chế độ 0 và 1 có SCK nghỉ ở mức thấp, chế độ 2 và 3 có SCK nghỉ ở mức cao; chế độ 0 và 3 đọc dữ liệu ở sườn lên, chế độ 1 và 2 đọc ở sườn xuống
Bốn chế độ khác nhau ở mức nghỉ của SCK và sườn đọc dữ liệu (hình vẽ minh họa).
Chế độCPOLCPHASCK khi nghỉĐọc dữ liệu ở
000Mức thấpSườn lên
101Mức thấpSườn xuống
210Mức caoSườn xuống
311Mức caoSườn lên

Thiết bị chủ phải dùng đúng chế độ mà thiết bị tớ yêu cầu; thông tin này ghi trong tài liệu kỹ thuật của từng IC. Chế độ 0 phổ biến nhất. Dùng sai chế độ thì dữ liệu thường bị lệch một bit.

4. SPI trên ESP32

BusSCKMISOMOSICSGhi chú
VSPI (mặc định)GPIO18GPIO19GPIO23GPIO5Đối tượng SPI của Arduino
HSPIGPIO14GPIO12GPIO13GPIO15Bus thứ hai, dùng khi cần tách riêng
  • Mỗi lần truyền nên bọc trong SPI.beginTransaction(SPISettings(tốc độ, MSBFIRST, chế độ)) và SPI.endTransaction(), để mỗi thiết bị trên bus được dùng đúng tốc độ và chế độ của nó.
  • Chân CS do chương trình tự điều khiển bằng digitalWrite(). Chân CS của các thiết bị khác phải ở mức cao trong lúc đó.
  • Dây càng dài thì tốc độ dùng được càng thấp. Với dây cắm breadboard dài 10 đến 20 cm, nên bắt đầu từ 1 đến 4 MHz.

5. Chuẩn bị

6. Chương trình 1: tự gửi tự nhận

Chỉ cần một dây nối D23 (MOSI) sang D19 (MISO). Mọi bit đi ra ở MOSI quay ngay về MISO, nên dữ liệu nhận về phải giống hệt dữ liệu gửi đi. Chương trình thử cả bốn chế độ ở 1 MHz và 8 MHz, đồng thời đo thời gian truyền 64 byte.

/*
  PYWORLD.VN - SPI trên ESP32: tự gửi tự nhận bằng một dây nối MOSI sang MISO
  Nối D23 (MOSI) với D19 (MISO); chương trình gửi 64 byte ở bốn chế độ SPI và hai tốc độ rồi so với dữ liệu nhận về
  Mỗi byte gửi đi luôn kèm một byte nhận về: SPI truyền hai chiều cùng lúc
*/

#include <SPI.h>

#define SCK_PIN 18
#define MISO_PIN 19
#define MOSI_PIN 23

const uint8_t CHE_DO[4] = {SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3};
const uint32_t TOC_DO[2] = {1000000, 8000000};
const int SO_BYTE = 64;

void thuMotCauHinh(int cheDo, uint32_t tocDo) {
  uint8_t gui[SO_BYTE], nhan[SO_BYTE];
  for (int i = 0; i < SO_BYTE; i++) {
    gui[i] = (uint8_t)(i * 37 + 11);
    nhan[i] = 0;
  }

  SPI.beginTransaction(SPISettings(tocDo, MSBFIRST, CHE_DO[cheDo]));
  unsigned long batDau = micros();
  SPI.transferBytes(gui, nhan, SO_BYTE);
  unsigned long thoiGian = micros() - batDau;
  SPI.endTransaction();

  int soByteSai = 0;
  for (int i = 0; i < SO_BYTE; i++) {
    if (gui[i] != nhan[i]) {
      soByteSai++;
    }
  }
  Serial.printf("Che do %d, %4u kHz: %d byte trong %4lu us, %d byte sai\n",
                cheDo, (unsigned)(tocDo / 1000), SO_BYTE, thoiGian, soByteSai);
}

void setup() {
  Serial.begin(115200);
  SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN);

  // Một byte đơn lẻ: giá trị trả về của transfer() chính là byte đi vào chân MISO trong lúc gửi
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  uint8_t nhan = SPI.transfer(0xA5);
  SPI.endTransaction();
  Serial.printf("\nGui 0xA5, nhan 0x%02X%s\n", nhan, nhan == 0xA5 ? "" : " -> kiem tra day noi D23 voi D19");
}

void loop() {
  Serial.println();
  for (int cheDo = 0; cheDo < 4; cheDo++) {
    for (int i = 0; i < 2; i++) {
      thuMotCauHinh(cheDo, TOC_DO[i]);
    }
  }
  delay(5000);
}

Khi tự nối MOSI với MISO, chế độ nào cũng cho kết quả đúng vì gửi và nhận dùng chung một clock. Chế độ chỉ quan trọng khi nói chuyện với một thiết bị khác.

7. Sơ đồ nối module thẻ nhớ

Sơ đồ nối ESP32 DevKit V1 với module thẻ nhớ Micro SD qua SPI: MOSI nối D23, MISO nối D19, SCK nối D18, CS nối D5, GND nối GND và VCC nối VIN 5 V
MOSI nối D23, MISO nối D19, SCK nối D18, CS nối D5; module có IC ổn áp nên VCC nối VIN (5 V).

Cách cấp nguồn cho từng loại module thẻ nhớ có trong bài dùng module thẻ nhớ Micro SD với ESP32. Tháo dây nối D23 với D19 của chương trình 1 trước khi nối module.

8. Chương trình 2: gửi lệnh thô cho thẻ nhớ

Thư viện SD che hết phần giao tiếp. Để thấy SPI làm việc thật, chương trình này tự gửi hai lệnh đầu tiên mà mọi thẻ nhớ đều phải trả lời:

  • CMD0 (6 byte 40 00 00 00 00 95): đưa thẻ về trạng thái chờ và chuyển sang chế độ SPI. Thẻ trả lời một byte 0x01.
  • CMD8 (6 byte 48 00 00 01 AA 87): hỏi thẻ có làm việc ở 2,7 đến 3,6 V không. Thẻ đời mới trả lời 0x01 rồi nhắc lại 00 00 01 AA.
/*
  PYWORLD.VN - SPI trên ESP32: gửi lệnh thô cho thẻ nhớ để thấy vai trò của CS, SCK, MOSI, MISO
  Không dùng thư viện SD: chương trình tự gửi lệnh CMD0 và CMD8 rồi in các byte thẻ trả lời
  Module thẻ nhớ: CS = D5, SCK = D18, MOSI = D23, MISO = D19
*/

#include <SPI.h>

#define SCK_PIN 18
#define MISO_PIN 19
#define MOSI_PIN 23
#define CS_PIN 5

// Lúc khởi động, thẻ nhớ chỉ nhận clock chậm (tối đa 400 kHz), chế độ 0, bit cao đi trước
const SPISettings CAI_DAT_THE(400000, MSBFIRST, SPI_MODE0);

// Một lệnh của thẻ nhớ dài 6 byte: 01 + số lệnh (6 bit), 4 byte tham số, 1 byte kiểm tra.
// Sau đó thiết bị chủ tiếp tục phát clock (gửi 0xFF) để thẻ có nhịp mà trả lời.
uint8_t guiLenh(uint8_t soLenh, uint32_t thamSo, uint8_t byteKiemTra, uint8_t *traLoiThem, int soByteThem) {
  SPI.beginTransaction(CAI_DAT_THE);
  digitalWrite(CS_PIN, LOW);            // Kéo CS xuống: chọn thẻ
  SPI.transfer(0xFF);

  SPI.transfer(0x40 | soLenh);
  SPI.transfer((uint8_t)(thamSo >> 24));
  SPI.transfer((uint8_t)(thamSo >> 16));
  SPI.transfer((uint8_t)(thamSo >> 8));
  SPI.transfer((uint8_t)thamSo);
  SPI.transfer(byteKiemTra);

  // Thẻ trả lời trong vòng vài byte; byte trả lời có bit cao nhất bằng 0
  uint8_t traLoi = 0xFF;
  for (int i = 0; i < 10 && (traLoi & 0x80); i++) {
    traLoi = SPI.transfer(0xFF);
  }
  for (int i = 0; i < soByteThem; i++) {
    traLoiThem[i] = SPI.transfer(0xFF);
  }

  digitalWrite(CS_PIN, HIGH);           // Đưa CS lên: nhả thẻ
  SPI.transfer(0xFF);
  SPI.endTransaction();
  return traLoi;
}

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);
  SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN);
  delay(100);

  // Ít nhất 74 xung clock trong lúc CS ở mức cao để thẻ sẵn sàng nhận lệnh
  SPI.beginTransaction(CAI_DAT_THE);
  for (int i = 0; i < 10; i++) {
    SPI.transfer(0xFF);
  }
  SPI.endTransaction();

  // CMD0: đưa thẻ về trạng thái chờ và chuyển sang chế độ SPI
  uint8_t traLoi = guiLenh(0, 0, 0x95, NULL, 0);
  Serial.printf("\nCMD0: gui 40 00 00 00 00 95, the tra loi 0x%02X\n", traLoi);
  if (traLoi == 0xFF) {
    Serial.println("  Khong co tra loi: kiem tra the nho, nguon va bon day SPI");
    return;
  }
  if (traLoi == 0x01) {
    Serial.println("  0x01: the dang o trang thai cho, da vao che do SPI");
  }

  // CMD8: hỏi thẻ có làm việc ở 2,7 đến 3,6 V không; thẻ nhắc lại 2 byte cuối của tham số
  uint8_t them[4] = {0, 0, 0, 0};
  traLoi = guiLenh(8, 0x000001AA, 0x87, them, 4);
  Serial.printf("CMD8: gui 48 00 00 01 AA 87, the tra loi 0x%02X, 4 byte tiep theo %02X %02X %02X %02X\n",
                traLoi, them[0], them[1], them[2], them[3]);
  if (traLoi == 0x01 && them[2] == 0x01 && them[3] == 0xAA) {
    Serial.println("  The doi 2 tro len (SDHC, SDXC), nhan dien ap 2,7 den 3,6 V");
  } else if (traLoi & 0x04) {
    Serial.println("  The khong hieu lenh nay: the doi 1");
  }
}

void loop() {
}

Ba điều của SPI thể hiện rõ trong chương trình: CS phải xuống thấp trước khi gửi và lên cao sau khi xong; muốn nhận câu trả lời thì thiết bị chủ phải tiếp tục gửi 0xFF để phát clock; và tốc độ, chế độ phải theo yêu cầu của thiết bị tớ (thẻ nhớ lúc khởi động chỉ nhận tối đa 400 kHz).

9. Kết quả mong đợi

Hình minh họa Serial Monitor của hai chương trình SPI trên ESP32: kết quả tự gửi tự nhận ở các chế độ và tốc độ, câu trả lời 0x01 của thẻ nhớ cho lệnh CMD0 và CMD8
Kết quả mong đợi trên Serial Monitor của hai chương trình (hình minh họa, thời gian là số ví dụ).
  1. Chương trình 1: mở Tools > Serial Monitor ở 115200 baud. Mọi dòng báo 0 byte sai; thời gian truyền ở 8 MHz ngắn hơn nhiều so với ở 1 MHz. Rút dây nối ra thì số byte sai tăng lên.
  2. Chương trình 2: nhấn nút EN để chạy lại. Thẻ trả lời 0x01 cho CMD0 và 0x01 kèm 00 00 01 AA cho CMD8.

10. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Nhận về toàn 0xFFMISO không được nối, thiết bị không trả lời, hoặc CS chưa xuống thấpKiểm tra dây MISO và CS; kiểm tra nguồn của thiết bị
Nhận về toàn 0x00MISO chạm GND, hoặc nối nhầm MOSI với MISO của moduleMOSI của ESP32 nối chân MOSI (hoặc DI, SDI) của module; không nối chéo như UART
Dữ liệu lệch một bitSai chế độ SPIDùng đúng chế độ ghi trong tài liệu của thiết bị
Chạy ở tốc độ thấp, lỗi ở tốc độ caoDây dài, tiếp xúc breadboard kémRút ngắn dây, giảm tốc độ trong SPISettings
Hai thiết bị trên bus làm hỏng dữ liệu của nhauHai chân CS cùng ở mức thấp, hoặc CS của thiết bị kia đang thả nổiĐặt mọi chân CS ở mức cao ngay đầu setup()
CMD0 không có trả lờiChưa cắm thẻ, module thiếu nguồn, hoặc tốc độ quá caoCắm lại thẻ, cấp đúng nguồn cho module, giữ 400 kHz lúc khởi động

11. Xem thêm


Tin tức liên quan

Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32
Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32

Giải thích Bluetooth Low Energy: quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, UUID. Hướng dẫn cho ESP32 làm thiết bị BLE có đặc tính đọc, ghi, thông báo và thử bằng điện thoại.

Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32
Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32

Giải thích điều khiển hồng ngoại: sóng mang 38 kHz, mắt thu và giao thức NEC với khung 32 bit, mã lặp. Hướng dẫn tự giải mã phím điều khiển và tự phát lệnh bằng ESP32, không dùng thư viện ngoài.

RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32
RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32

Giải thích sóng RF 433 MHz và điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung và khung tin 24 bit. Hướng dẫn tự viết chương trình phát và thu trên hai mạch ESP32, không dùng thư viện ngoài.

Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32
Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32

Giải thích vòng dòng 4-20 mA: vì sao dùng dòng điện, vì sao bắt đầu từ 4 mA, cách đổi ra giá trị đo và ngưỡng báo lỗi. Hướng dẫn đọc bằng ESP32 qua điện trở shunt 120 Ω, với ADC có sẵn và với ADS1115.

CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32
CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32

Phần 2 về CAN bus với ESP32: đặt bộ lọc nhận theo mã lọc và mặt nạ, chọn tốc độ bit theo chiều dài dây, hiểu ba trạng thái lỗi của nút và viết chương trình theo dõi bộ đếm lỗi, tự hồi phục sau bus-off.

CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32
CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32

Giải thích CAN bus: hai dây CAN_H, CAN_L, mức trội và mức lặn, khung tin có ID 11 bit, phân xử ưu tiên, CRC và ACK. Kèm hai chương trình ESP32 dùng bộ điều khiển TWAI: tự kiểm tra và hai nút gửi nhận.

Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU
Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU

Hướng dẫn dùng ESP32 làm máy khách Modbus TCP đọc thanh ghi của một thiết bị trong mạng, và làm cổng chuyển đổi Modbus TCP sang Modbus RTU qua RS-485: cách đổi khung, mã thiết bị, thời gian chờ, mã lỗi 0x0B.

Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi
Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi

Giải thích Modbus TCP/IP: phần đầu MBAP, cổng 502, điểm khác với Modbus RTU. Kèm chương trình ESP32 làm máy chủ Modbus TCP qua Wi-Fi với 10 thanh ghi giữ và 8 cuộn, tự phân tích khung, không dùng thư viện.


Bình luận
Đã thêm vào giỏ hàng