Giao thức truyền thông trên STM32 (Phần 2): SPI – bốn chế độ CPOL/CPHA và đọc flash W25Qxx
SPI trên STM32 từ nguyên lý thanh ghi dịch, bốn chế độ CPOL/CPHA, bảng tốc độ SCK theo prescaler đến đọc ID, đọc, xoá và ghi flash W25Qxx bằng HAL và DMA. Phần 2 của series giao thức truyền thông trên STM32.

Ghi chú: sơ đồ trong bài do Capitlab vẽ để minh hoạ. Số liệu trong các bảng là giá trị tính theo công thức, cột "Đo thực tế" sẽ được cập nhật khi đo trên phần cứng. Mã nguồn soạn theo tài liệu HAL và datasheet, chưa chạy thử trên phần cứng, nên hãy kiểm tra lại trước khi dùng cho sản phẩm.
SPI (Serial Peripheral Interface) xuất hiện ở flash, thẻ nhớ, màn hình, ADC, cảm biến IMU. Nó nhanh hơn UART và I²C nhiều, nhưng tốn dây hơn, và không có sẵn cơ chế báo nhận (ACK) hay phát hiện lỗi. Slave không trả lời thì master vẫn đọc về một chuỗi byte, chỉ là byte sai.
Khi SPI "không chạy", nguyên nhân hay gặp nhất là chọn sai chế độ CPOL/CPHA, tiếp theo là nhả chân CS quá sớm. Bài này giải thích cả hai, rồi dùng flash Winbond W25Qxx làm ví dụ đọc ID, đọc, xoá và ghi.
1. Nguyên lý
SPI có một master và một hoặc nhiều slave, dùng bốn tín hiệu:
| Tín hiệu | Hướng | Ý nghĩa |
|---|---|---|
| SCK | Master → Slave | Xung clock do master phát |
| MOSI | Master → Slave | Dữ liệu từ master (Master Out, Slave In) |
| MISO | Slave → Master | Dữ liệu từ slave (Master In, Slave Out) |
| CS (hay NSS) | Master → Slave | Chọn slave, tích cực mức thấp |
Có thể hình dung master và slave nối thành một vòng thanh ghi dịch. Mỗi xung SCK, master đẩy một bit ra MOSI và cùng lúc nhận một bit từ MISO. Gửi và nhận vì thế luôn đi cùng nhau. Muốn đọc một byte từ slave, master phải gửi một byte bất kỳ (thường 0xFF hoặc 0x00) để tạo ra 8 xung clock.
Bốn chế độ SPI
Hai tham số quyết định cách hai bên hiểu nhau:
- CPOL (clock polarity) là mức của SCK khi rảnh: 0 là thấp, 1 là cao.
- CPHA (clock phase) là cạnh dùng để lấy mẫu: 0 là cạnh đầu tiên của xung, 1 là cạnh thứ hai.
Hình 1. Bốn chế độ SPI khi gửi chuỗi bit 1, 0, 1, 1. Đường đứt đỏ là thời điểm slave lấy mẫu. Dữ liệu luôn ổn định tại các đường đó.
| Chế độ | CPOL | CPHA | SCK khi rảnh | Lấy mẫu | Dữ liệu đổi ở |
|---|---|---|---|---|---|
| 0 | 0 | 0 | Thấp | Cạnh lên | Cạnh xuống |
| 1 | 0 | 1 | Thấp | Cạnh xuống | Cạnh lên |
| 2 | 1 | 0 | Cao | Cạnh xuống | Cạnh lên |
| 3 | 1 | 1 | Cao | Cạnh lên | Cạnh xuống |
Chế độ cần dùng luôn có trong datasheet của thiết bị. W25Qxx và nhiều cảm biến IMU chạy được cả chế độ 0 lẫn 3, nhưng có thiết bị chỉ nhận chế độ 1 hoặc 2. Cùng lúc đó, kiểm tra luôn thứ tự bit (đa số là MSB first) và tốc độ clock tối đa.
2. Tốc độ SCK trên STM32
SCK bằng clock bus của khối SPI chia cho prescaler (2, 4, 8, ... 256), nên chỉ có vài giá trị rời rạc để chọn. Bảng dưới tính cho SPI1 trên bus 84 MHz. Đây là số tính, chưa phải số đo:
| Prescaler | SCK | Thời gian 1 byte | Đọc 4 KB (chỉ tính clock) | Thông lượng lý thuyết |
|---|---|---|---|---|
| 2 | 42 MHz | 0,19 µs | 0,78 ms | 5,25 MB/s |
| 4 | 21 MHz | 0,38 µs | 1,56 ms | 2,63 MB/s |
| 8 | 10,5 MHz | 0,76 µs | 3,12 ms | 1,31 MB/s |
| 16 | 5,25 MHz | 1,52 µs | 6,24 ms | 656 KB/s |
| 32 | 2,625 MHz | 3,05 µs | 12,5 ms | 328 KB/s |
| 64 | 1,3125 MHz | 6,10 µs | 25,0 ms | 164 KB/s |
| 128 | 656 kHz | 12,2 µs | 49,9 ms | 82 KB/s |
| 256 | 328 kHz | 24,4 µs | 99,9 ms | 41 KB/s |
Thông lượng thực luôn thấp hơn bảng: còn thời gian gửi lệnh và địa chỉ, khoảng trống giữa các byte khi chạy polling, và thời gian CPU xử lý. Mục 8 sẽ đo con số thật.
Tốc độ tối đa là giới hạn của cả mạch, không riêng của chip. Trên breadboard hoặc với dây dài, SPI có thể lỗi từ vài MHz. Nên bắt đầu chậm rồi tăng dần. SPI vốn dành cho kết nối trong cùng một board hoặc vài chục centimet. Đi xa hơn thì phải giảm tốc, hoặc dùng giao thức vi sai như RS-485, CAN.
3. Nối dây
Hình 2. Một master, hai slave. Ba dây SCK, MOSI, MISO nối chung, mỗi slave có chân CS riêng.
Ba dây SCK, MOSI, MISO dùng chung, mỗi slave có một chân CS riêng. Chân CS cần ở mức cao (không chọn) ngay từ lúc khởi động, nên đặt mức khởi tạo High trong CubeMX.
MISO của slave không được chọn phải ở trạng thái trở kháng cao (Hi-Z). Thiết bị nào không làm vậy sẽ giành bus và phá dữ liệu của slave khác.
Với dây dài hơn vài centimet, đặt một điện trở nối tiếp nhỏ (khoảng 22 đến 100 Ω) sát chân phát để giảm dội tín hiệu. Các bo phải chung GND và có mức logic tương thích.
4. Cấu hình trong STM32CubeIDE
| Mục | Giá trị | Ghi chú |
|---|---|---|
| Mode | Full-Duplex Master | |
| Hardware NSS Signal | Disable | Tự điều khiển CS bằng GPIO |
| Data Size | 8 Bits | |
| First Bit | MSB First | Theo datasheet |
| Prescaler | Tùy tốc độ cần | Xem bảng ở mục 2 |
| CPOL / CPHA | Theo datasheet | W25Qxx: chế độ 0 là Low / 1 Edge |
| Chân CS | GPIO Output, mức ban đầu High | Đặt tên nhãn FLASH_CS |
| DMA | SPI1_TX và SPI1_RX, Normal |
Chỉ khi dùng DMA |
Trong CubeMX, chế độ 0 là "Clock Polarity: Low" và "Clock Phase: 1 Edge". Chế độ 3 là "High" và "2 Edge".
5. Mã nguồn
5.1. Đọc JEDEC ID của W25Qxx
Lệnh 0x9F trả về 3 byte: mã nhà sản xuất, loại bộ nhớ, dung lượng. Winbond có mã 0xEF. W25Q64 sẽ trả EF 40 17, W25Q128 trả EF 40 18.
Hình 3. Một giao dịch đọc ID. CS xuống thấp suốt giao dịch, master gửi 1 byte lệnh rồi phát 24 xung clock để nhận 3 byte.
#define CS_LOW() HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET)
HAL_StatusTypeDef flash_read_id(uint8_t id[3])
{
uint8_t cmd = 0x9F;
CS_LOW();
HAL_StatusTypeDef st = HAL_SPI_Transmit(&hspi1, &cmd, 1, 10);
if (st == HAL_OK)
st = HAL_SPI_Receive(&hspi1, id, 3, 10);
CS_HIGH();
return st;
}
Nếu đọc ra 00 00 00 hoặc FF FF FF, xem bảng lỗi ở mục 6 trước khi nghi chip hỏng.
5.2. Đọc, xoá và ghi flash
Các lệnh cơ bản của W25Qxx theo datasheet:
| Lệnh | Mã | Ghi chú |
|---|---|---|
| Write Enable | 0x06 |
Phải gửi trước mỗi lệnh ghi hoặc xóa |
| Read Status Register-1 | 0x05 |
Bit 0 là BUSY |
| Read Data | 0x03 |
Theo sau là địa chỉ 24 bit, rồi đọc dữ liệu liên tục |
| Page Program | 0x02 |
Ghi tối đa 256 byte, không vượt ranh giới trang |
| Sector Erase (4 KB) | 0x20 |
Xóa trước khi ghi, ô nhớ xóa xong có giá trị 0xFF |
static void flash_wait_ready(void)
{
uint8_t cmd = 0x05, st;
do {
CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10);
HAL_SPI_Receive(&hspi1, &st, 1, 10);
CS_HIGH();
} while (st & 0x01); // BUSY
}
static void flash_write_enable(void)
{
uint8_t cmd = 0x06;
CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); CS_HIGH();
}
void flash_read(uint32_t addr, uint8_t *buf, uint16_t n)
{
uint8_t h[4] = { 0x03, addr >> 16, addr >> 8, addr };
CS_LOW();
HAL_SPI_Transmit(&hspi1, h, 4, 10);
HAL_SPI_Receive(&hspi1, buf, n, 100);
CS_HIGH();
}
void flash_sector_erase(uint32_t addr)
{
uint8_t h[4] = { 0x20, addr >> 16, addr >> 8, addr };
flash_write_enable();
CS_LOW(); HAL_SPI_Transmit(&hspi1, h, 4, 10); CS_HIGH();
flash_wait_ready(); // xóa sector mất từ vài chục đến vài trăm ms
}
void flash_page_write(uint32_t addr, const uint8_t *data, uint16_t n) // n <= 256, trong một trang
{
uint8_t h[4] = { 0x02, addr >> 16, addr >> 8, addr };
flash_write_enable();
CS_LOW();
HAL_SPI_Transmit(&hspi1, h, 4, 10);
HAL_SPI_Transmit(&hspi1, (uint8_t *)data, n, 100);
CS_HIGH();
flash_wait_ready();
}
Cờ Write Enable tự xoá sau mỗi lệnh ghi hoặc xoá, nên lần nào cũng phải gửi 0x06 trước. Flash chỉ chuyển được bit từ 1 về 0, muốn đưa về 1 phải xoá cả sector, vì vậy chỉ ghi vào vùng đã xoá. Thời gian xoá và ghi cụ thể lấy trong datasheet của chip bạn dùng.
5.3. Truyền nhận khối lớn bằng DMA
static volatile uint8_t spi_done;
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi == &hspi1) spi_done = 1;
}
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{
if (hspi == &hspi1) spi_done = 2; // báo lỗi để vòng chờ thoát ra
}
int spi_xfer_dma(uint8_t *tx, uint8_t *rx, uint16_t n)
{
spi_done = 0;
CS_LOW();
if (HAL_SPI_TransmitReceive_DMA(&hspi1, tx, rx, n) != HAL_OK) { CS_HIGH(); return -1; }
while (!spi_done) { } // thực tế dùng RTOS (semaphore) hoặc cờ không chặn
CS_HIGH();
return (spi_done == 1) ? 0 : -1;
}
DMA tốn một khoảng khởi động, nên với vài byte thì polling còn nhanh hơn. Nó có lợi khi khối dữ liệu từ vài chục byte trở lên, hoặc khi CPU cần làm việc khác trong lúc truyền.
Trên các dòng có cache dữ liệu (STM32F7, H7), bộ đệm DMA phải được dọn cache: SCB_CleanDCache_by_Addr trước khi gửi và SCB_InvalidateDCache_by_Addr sau khi nhận, hoặc đặt bộ đệm trong vùng không cache. F1 và F4 không có cache nên không cần.
6. Lỗi thường gặp
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Đọc ra toàn 0x00 | MISO kéo xuống GND, CS chưa kéo xuống, slave chưa cấp nguồn | Đo CS và MISO bằng logic analyzer, kiểm tra nguồn |
| Đọc ra toàn 0xFF | MISO để hở hoặc kéo lên, sai chế độ CPOL/CPHA, sai chân | Kiểm tra dây MISO, thử chế độ 0 và 3 |
| Dữ liệu lệch một bit | Sai CPHA, hoặc sai thứ tự bit MSB/LSB | Đối chiếu lại bảng chế độ và datasheet |
| Chạy ở tốc độ thấp, lỗi ở tốc độ cao | Dây dài, nhiễu, vượt tốc độ tối đa của slave | Tăng prescaler, thêm điện trở nối tiếp, rút ngắn dây |
| Byte cuối bị mất | Nhả CS khi byte cuối chưa ra hết | Chờ cờ BUSY của SPI hoặc callback hoàn tất rồi mới CS_HIGH |
| Ghi flash không thay đổi dữ liệu | Quên Write Enable hoặc chưa xóa sector | Gửi 0x06 trước ghi, xóa sector trước khi ghi |
| Hai slave nhiễu nhau | MISO của slave không chọn không về Hi-Z | Kiểm tra datasheet, thêm buffer ba trạng thái |
7. Khi STM32 làm SPI slave
STM32 cũng chạy được ở chế độ Full-Duplex Slave, khi đó SCK và CS do master bên kia cấp. Ở chế độ này nên bật NSS phần cứng để chip tự biết lúc nào được chọn, và nhận bằng DMA cho kịp nhịp master.
Slave không điều khiển được tốc độ, nên byte đầu tiên master gửi thường đến trước khi firmware kịp chuẩn bị câu trả lời. Giao thức giữa hai bên vì thế cần một byte đệm ở đầu, hoặc một chân báo "sẵn sàng" riêng. Phần này sẽ có bài đo riêng.
8. Kế hoạch đo thực tế
Cột "Đo thực tế" để trống cho đến khi chạy trên phần cứng.
| Thử nghiệm | Cách đo | Giá trị tính toán | Đo thực tế |
|---|---|---|---|
| Tần số SCK với prescaler 8 | Logic analyzer hoặc oscilloscope | 10,5 MHz (bus 84 MHz) | (điền sau) |
| Bốn chế độ SPI | Đọc JEDEC ID ở cả 4 chế độ, ghi nhận chế độ nào đọc đúng | Dự kiến đúng ở chế độ 0 và 3 | (điền sau) |
| Thời gian đọc 4 KB, polling | Bật GPIO quanh hàm flash_read, đo bằng logic analyzer |
3,12 ms ở 10,5 MHz (chỉ tính clock) | (điền sau) |
| Thời gian đọc 4 KB, DMA | Như trên, so sánh với polling | Gần bằng polling, CPU rảnh | (điền sau) |
| Khoảng trống giữa các byte khi polling | Logic analyzer, đo khoảng trống giữa hai byte liên tiếp | Không có nếu dùng một lệnh nhiều byte | (điền sau) |
| Tốc độ cao nhất đọc đúng | Tăng dần SCK, kiểm tra dữ liệu đọc về | Phụ thuộc dây và chip | (điền sau) |
| Ảnh hưởng của chiều dài dây | Oscilloscope quan sát sườn và dội ở SCK | Phụ thuộc dây | (điền sau) |
9. Hỏi đáp
SPI nhanh hơn I²C bao nhiêu? Chỉ tính clock, SPI ở 10 MHz nhanh gấp khoảng 25 lần I²C chế độ Fast (400 kHz). Con số thực còn tuỳ firmware.
SPI có cần điện trở kéo lên không? Thường thì không, vì các đường SPI là push-pull. Riêng chân CS nên có điện trở kéo lên để slave không bị chọn nhầm lúc MCU đang reset.
Có nên dùng NSS phần cứng khi làm master? Hầu hết trường hợp, điều khiển CS bằng GPIO đơn giản và linh hoạt hơn, nhất là khi có nhiều slave hoặc khi một giao dịch phải giữ CS qua nhiều lệnh HAL.
Dùng HAL_SPI_TransmitReceive hay Transmit rồi Receive? Cả hai đều được. Receive ở chế độ Full-Duplex Master vẫn tự phát byte giả. Khi chip yêu cầu byte giả cụ thể (ví dụ 0xFF), dùng TransmitReceive với bộ đệm gửi tự chuẩn bị.
Kết luận
Đọc datasheet để đặt đúng CPOL/CPHA, chỉ nhả CS khi byte cuối đã ra hết, và bắt đầu ở tốc độ thấp. Ba việc đó giải quyết phần lớn sự cố SPI.
Phần 3 chuyển sang I²C: ít dây hơn, có địa chỉ và ACK, nhưng thêm chuyện điện trở kéo lên và bus bị treo.
Để kiểm tra nhanh dữ liệu đọc từ flash, có thể dùng Hex Converter và máy tính CRC của Capitlab.
Tài liệu tham khảo
- Datasheet Winbond W25Q64/W25Q128 (bảng lệnh, JEDEC ID, thời gian xóa và ghi)
- Reference manual của dòng STM32 bạn dùng (mục SPI) và tài liệu STM32 HAL (nhóm hàm
HAL_SPI_*)