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ì?
- 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
- 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
| Chế độ | CPOL | CPHA | SCK khi nghỉ | Đọc dữ liệu ở |
|---|---|---|---|---|
| 0 | 0 | 0 | Mức thấp | Sườn lên |
| 1 | 0 | 1 | Mức thấp | Sườn xuống |
| 2 | 1 | 0 | Mức cao | Sườn xuống |
| 3 | 1 | 1 | Mức cao | Sườ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
| Bus | SCK | MISO | MOSI | CS | Ghi chú |
|---|---|---|---|---|---|
| VSPI (mặc định) | GPIO18 | GPIO19 | GPIO23 | GPIO5 | Đối tượng SPI của Arduino |
| HSPI | GPIO14 | GPIO12 | GPIO13 | GPIO15 | Bus 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ị
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340).
- Một module đọc ghi thẻ nhớ Micro SD và một thẻ Micro SD cho chương trình 2.
- Dây cắm.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
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ớ
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 byte0x01. - 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ời0x01rồi nhắc lại00 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
- 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.
- Chương trình 2: nhấn nút EN để chạy lại. Thẻ trả lời
0x01cho CMD0 và0x01kèm00 00 01 AAcho CMD8.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Nhận về toàn 0xFF | MISO không được nối, thiết bị không trả lời, hoặc CS chưa xuống thấp | Kiểm tra dây MISO và CS; kiểm tra nguồn của thiết bị |
Nhận về toàn 0x00 | MISO chạm GND, hoặc nối nhầm MOSI với MISO của module | MOSI 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 bit | Sai chế độ SPI | Dùng đúng chế độ ghi trong tài liệu của thiết bị |
| Chạy ở tốc độ thấp, lỗi ở tốc độ cao | Dây dài, tiếp xúc breadboard kém | Rú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 nhau | Hai 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ời | Chưa cắm thẻ, module thiếu nguồn, hoặc tốc độ quá cao | Cắm lại thẻ, cấp đúng nguồn cho module, giữ 400 kHz lúc khởi động |
11. Xem thêm
- Dùng module thẻ nhớ Micro SD với ESP32: đọc ghi file bằng thư viện SD.
- Giao tiếp I2C trên ESP32, chuẩn hai dây để so sánh với SPI.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm