SPI (Serial Peripheral Interface) là chuẩn giao tiếp tốc độ cao, song công (full-duplex) rất phổ biến khi làm việc với SPI trong STM32 và các module như màn hình, thẻ nhớ SD, cảm biến hay Flash ngoài. Bài viết này trình bày nguyên lý 4 dây MOSI/MISO/SCK/CS và hai cách triển khai trên STM32F1: SPI Software (giả lập bằng GPIO) và SPI Hardware (dùng ngoại vi SPI tích hợp).
SPI trong STM32: SPI mềm (Software) là gì?
Trên mỗi dòng vi điều khiển khác nhau, module SPI được tích hợp và điều khiển bởi các thanh ghi, phần cứng, IO khác nhau – đó gọi là SPI cứng (hardware SPI). Bản chất chuẩn truyền thông SPI giống nhau trên mọi chip, nhưng lại được cài đặt và sử dụng không giống nhau. Điều này gây khó khăn khi bạn tìm hiểu một dòng vi điều khiển mới: phải nhớ các chân MISO, SS, MOSI, SCK, nhớ các thanh ghi, các chế độ hoạt động và cách cài đặt trên từng dòng chip.
Để khắc phục nhược điểm trên, có một cách lập trình giả lập SPI cứng gọi là “SPI mềm”. Thực chất SPI mềm là cách “mô phỏng” – tạo ra một giao thức truyền thông giống SPI nhưng chỉ sử dụng các cổng vào ra (GPIO) của vi điều khiển. Như vậy, chỉ với việc điều khiển GPIO, ta hoàn toàn có thể thực hiện giao thức SPI trên bất kỳ vi điều khiển nào mà không cần nhớ thanh ghi hay các chân phần cứng.
Cấu hình GPIO cho SPI mềm
SPI dùng 4 chân để truyền nhận, gồm MISO, MOSI, CS và SCK:

- MISO (Master In Slave Out): chân nhận của Master, nối với chân truyền của Slave. Cấu hình Input ở Master và Output ở Slave.
- MOSI (Master Out Slave In): ngược lại với MISO – Output ở Master và Input ở Slave.
- SCK (Clock): chân truyền xung đồng bộ từ Master cho Slave. Output ở Master, Input ở Slave.
- CS (Chip Select): chân chọn Slave của Master. Output ở Master, Input ở Slave. Có thể có nhiều CS để một Master điều khiển nhiều Slave.
Đầu tiên, ta định nghĩa 4 chân sử dụng cho SPI:
#define SPI_MISO_Pin GPIO_Pin_1
#define SPI_MOSI_Pin GPIO_Pin_2
#define SPI_CS_Pin GPIO_Pin_3
#define SPI_GPIO GPIOA
#define SPI_RCC RCC_APB2Periph_GPIOA
Tạo hàm khởi tạo các giá trị ban đầu cho các chân khi chưa có sự kiện truyền nhận:
void SPI_Init(void){
GPIO_WriteBit(SPI_PORT, SPI_SCK, 0);
GPIO_WriteBit(SPI_PORT, SPI_CS, 1);
GPIO_WriteBit(SPI_PORT, SPI_MISO, 0);
GPIO_WriteBit(SPI_PORT, SPI_MOSI, 0);
}
- SCK = 0: clock ở mức thấp (tùy cấu hình CPHA/CPOL mà giá trị này có thể khác). Ở đây ta mô phỏng đơn giản: clock mức 1 sẽ lấy mẫu dữ liệu, mức 0 là chế độ nghỉ.
- CS = 1: khi Master chưa truyền nhận, CS ở mức 1 cho tất cả các Slave.
- Các chân MISO, MOSI được kéo về 0 ở chế độ nghỉ.
SPI Software code cho Master
Clock được tạo bằng hàm delay kết hợp set/reset tín hiệu trên đường SCK. Mỗi lần gọi Clock() sẽ tạo ra một xung vuông, dữ liệu được truyền nhận đồng bộ theo clock này:
void Clock(void)
{
GPIO_WriteBit(SPI_PORT, SPI_SCK, 1);
delay(1000);
GPIO_WriteBit(SPI_PORT, SPI_SCK, 0);
delay(1000);
}

Clock() tạo ra một xung vuông trên đường SCK để đồng bộ truyền nhậnHàm truyền ở Master gửi lần lượt 8 bit của byte dữ liệu ra chân MOSI, mỗi bit đồng bộ với một xung Clock():
void SPI_Master_Transmit(uint8_t u8Data){
uint8_t u8Mask = 0x80;
uint8_t tempData;
GPIO_WriteBit(SPI_GPIO, SPI_CS_Pin, Bit_RESET); /* Kéo CS xuống chọn Slave */
delay(10);
for(int i = 0; i < 8; i++){
tempData = u8Data & u8Mask;
if(tempData){
GPIO_WriteBit(SPI_GPIO, SPI_MOSI_Pin, Bit_SET);
delay(10);
} else {
GPIO_WriteBit(SPI_GPIO, SPI_MOSI_Pin, Bit_RESET);
delay(10);
}
u8Data <<= 1;
Clock(); /* Tạo xung clock cho mỗi bit */
}
GPIO_WriteBit(SPI_GPIO, SPI_CS_Pin, Bit_SET); /* Nhả CS */
delay(10);
}
Mô phỏng quá trình truyền một chuỗi dữ liệu DataTrans: các byte sẽ được truyền lần lượt bằng SPI_Master_Transmit:
while(1)
{
for(int i = 0; i < 7; i++){
SPI_Master_Transmit(DataTrans[i]);
delay(5000000);
}
}
SPI Software code cho Slave
Phía Slave dựa vào tín hiệu CS và SCK để xác định lệnh truyền nhận từ Master, cũng như thời điểm đọc dữ liệu trên MISO:
uint8_t SPI_SlaveReceive(void)
{
uint8_t i = 0;
uint8_t dataReceive = 0x00;
bool st = 0;
while (digitalRead(SS)); /* Đợi CS được kéo xuống */
while (!digitalRead(SCK));
for(i = 0; i < 8; i++)
{
if(digitalRead(SCK)){
while (digitalRead(SCK)) st = digitalRead(MISO);
dataReceive = dataReceive << 1;
dataReceive = dataReceive | st;
}
while (!digitalRead(SCK));
}
return dataReceive;
}
/* Trong main loop */
void loop(void){
if(!(digitalRead(SS))){
dat = SPI_SlaveReceive();
Serial.println(dat, DEC);
}
}
SPI Hardware trên STM32
STM32F1 có 2 khối SPI: SPI1 nằm trên bus APB2 và SPI2 nằm trên bus APB1. Các khối này được xây dựng sẵn kết nối, driver và các hàm riêng trong bộ thư viện chuẩn (SPL), giúp truyền nhận nhanh và ít tốn CPU.
Cấu hình GPIO cho SPI
STM32 quy định sẵn các chân dành cho chức năng SPI. Với SPI1, các chân mặc định nằm trên Port A (PA4–PA7):

Ta định nghĩa các chân rồi cấu hình chúng ở chế độ Alternate Function Push-Pull (GPIO_Mode_AF_PP):
#define SPI1_NSS GPIO_Pin_4
#define SPI1_SCK GPIO_Pin_5
#define SPI1_MISO GPIO_Pin_6
#define SPI1_MOSI GPIO_Pin_7
#define SPI1_GPIO GPIOA
void GPIO_Config(void){
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = SPI1_NSS | SPI1_SCK | SPI1_MISO | SPI1_MOSI;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(SPI1_GPIO, &GPIO_InitStructure);
}
Cấu hình SPI
Tương tự các ngoại vi khác, các tham số SPI được khai báo trong struct SPI_InitTypeDef:
| Tham số | Ý nghĩa |
|---|---|
SPI_Mode | Chế độ hoạt động: Master hay Slave. |
SPI_Direction | Kiểu truyền: 1 chiều, 2 chiều, full-duplex… |
SPI_BaudRatePrescaler | Hệ số chia clock cấp cho module SPI. |
SPI_CPOL | Cực tính của SCK khi nghỉ: SPI_CPOL_Low (mức 0) hoặc SPI_CPOL_High (mức 1). |
SPI_CPHA | Pha lấy mẫu: SPI_CPHA_1Edge (cạnh xung đầu tiên) hoặc SPI_CPHA_2Edge (cạnh thứ hai). |
SPI_DataSize | Số bit truyền: 8 hoặc 16 bit. |
SPI_FirstBit | Chiều truyền bit: MSB hay LSB trước. |
SPI_CRCPolynomial | Số bit CheckSum (CRC) cho SPI. |
SPI_NSS | Chân SS điều khiển bằng phần cứng hay phần mềm. |
Hàm SPI_Config() cấu hình các tham số cho SPI1:
void SPI_Config(void){
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
Các hàm truyền nhận và cờ trạng thái
SPI_I2S_SendData(SPIx, Data)– truyền 8 hoặc 16 bit dữ liệu (tùy DataSize).SPI_I2S_ReceiveData(SPIx)– trả về 8 hoặc 16 bit data đọc được trên SPIx.SPI_I2S_GetFlagStatus(SPIx, FLAG)– trả về giá trị một cờ trạng thái trong thanh ghi SPI.
Các cờ (SPI_I2S_FLAG) thường dùng:
| Cờ | Ý nghĩa |
|---|---|
SPI_I2S_FLAG_TXE | Cờ báo truyền – set lên 1 khi đã truyền xong data trong buffer. |
SPI_I2S_FLAG_RXNE | Cờ báo nhận – set lên 1 khi đã nhận xong data. |
SPI_I2S_FLAG_BSY | Cờ báo bận – set lên 1 khi SPI đang bận truyền nhận. |
Vì cấu hình NSS bằng phần mềm (SPI_NSS_Soft), nên khi truyền ta phải chủ động kéo SS xuống Low bằng phần mềm:
void SPI_Send1Byte(uint8_t data){
GPIO_WriteBit(SPI1_GPIO, SPI1_NSS, Bit_RESET);
SPI_I2S_SendData(SPI1, data);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == 0);
GPIO_WriteBit(SPI1_GPIO, SPI1_NSS, Bit_SET);
}
uint8_t SPI_Receive1Byte(void){
uint8_t temp;
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == 1);
temp = (uint8_t)SPI_I2S_ReceiveData(SPI1);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == 0);
return temp;
}
So sánh SPI Software và SPI Hardware
| Tiêu chí | SPI Software | SPI Hardware |
|---|---|---|
| Cách tạo tín hiệu | Điều khiển GPIO thủ công (bit-banging) | Ngoại vi SPI tự động |
| Chân sử dụng | Bất kỳ chân GPIO nào | Cố định theo datasheet (PA4–PA7…) |
| Tốc độ | Chậm, phụ thuộc hàm delay | Nhanh (chia clock qua BaudRatePrescaler) |
| Tải CPU | Cao (CPU tạo từng xung) | Thấp (phần cứng xử lý) |
| CPOL/CPHA, CRC | Phải tự xử lý | Có sẵn trong SPI_InitTypeDef |
Kết luận
Qua bài viết, bạn đã nắm được nguyên lý SPI trong STM32: giao tiếp 4 dây MOSI/MISO/SCK/CS đồng bộ theo clock của Master. Với SPI Software, ta tự tạo tín hiệu bằng GPIO và các hàm Clock/Transmit/Receive, giúp chạy trên bất kỳ chip nào và hiểu rõ bản chất giao thức. Với SPI Hardware, ta tận dụng khối SPI1/SPI2 cùng SPI_InitTypeDef (CPOL, CPHA, DataSize…) và các cờ TXE/RXNE/BSY để truyền nhận tốc độ cao, nhẹ CPU.
Trong thực tế automotive embedded, SPI thường dùng cho màn hình TFT, thẻ nhớ SD, ADC/DAC ngoài hay Flash. Hiểu cả hai cách triển khai giúp bạn linh hoạt: dùng SPI Hardware khi cần hiệu năng, hoặc SPI Software khi hết chân cứng hay cần giao tiếp trên chân bất kỳ.


