本文介紹如何使用STM32標准外設庫的GPIO端口模擬SPI,本例程使用PA5、PA6和PA7模擬一路SPI。SPI有4種工作模式,模擬SPI使用模式0,即空閑時SCK為低電平,在奇數邊沿采樣。
本文適合對單片機及C語言有一定基礎的開發人員閱讀,MCU使用STM32F103VE系列。
1. 簡介
SPI 協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在要求通訊速率較高的場合。SPI用於多設備之間通訊,分為主機Master和從機Slave,主機只有一個,從機可以有多個,通過片選信號對從機進行選擇,一次只能選擇一個從機。通訊只能由主機發起,支持的操作分為讀取和寫入,即主機讀取從機的數據,以及向從機寫入數據。
SPI一般有4根線,分別是片選線SS、時鍾線SCK、主設備輸出\從設備輸入MOSI、主設備輸入\從設備輸出MISO,其中除MISO對於主機為輸入引腳外,其他引腳對於主機均為輸出引腳。因為有獨立的輸入和輸出引腳,因此SPI支持全雙工工作模式,即可以同時接收和發送。
2. 總線傳輸信號
- 空閑狀態:片選信號SS低電平有效,那么空閑狀態片選信號SS為高。
- 開始信號及結束信號:開始信號需要將片選信號SS拉低,結束信號需要將片選信號SS拉高。
- 通訊模式:SPI有4種通訊模式,分別為0、1、2、3,根據時鍾極性和時鍾相位確定,時鍾極性分別為空閑低電平和空閑高電平,時鍾相位分別為SCK奇數邊沿采樣和偶數邊沿采樣。常用的模式為模式0和模式3。
SPI模式 | 時鍾極性(空閑時SCK時鍾) | 時鍾相位(采樣時刻) |
0 | 低電平 | 奇數邊沿 |
1 | 低電平 | 偶數邊沿 |
2 | 高電平 | 奇數邊沿 |
3 | 高電平 | 偶數邊沿 |
3. 時序說明
以模式0舉例說明:
- 空閑狀態:片選信號SS為高,SCK輸出低電平。
- 開始信號:片選信號SS變低,SCK輸出低電平。
- 結束信號:片選信號SS變高,SCK輸出低電平。
- 讀取:SCK由低變高之后,讀取MISO引腳信號。
- 寫入:SCK輸出低電平,MOSI引腳輸出相應的電平,然后SCK輸出高電平。
- 一個時鍾周期同時讀取和寫入:SCK輸出低電平,主設備控制MOSI輸出相應電平,從設備控制MISO輸出相應電平,然后SCK輸出高電平,從設備讀取MOSI引腳電平,主設備讀取MISO引腳電平。即無論主設備還是從設備,均在SCK為低電平時輸出信號,在SCK為高電平時讀取信號。
4. 初始化
初始化跟普通GPIO類似,SCK和MOSI設置為推挽輸出,而MISO設置為浮空輸入。
GPIO初始化完成之后,SCK置為低電平,進入空閑狀態。
5. 模擬信號
由於SPI支持一個周期內同時讀取和寫入,因此讀取和寫入操作可以用一個函數實現,而單獨的讀取函數和寫入函數可以通過調用該讀寫函數實現。
完整代碼(僅自己編寫的部分)
1 #define SPI_SCK_1 GPIO_SetBits(GPIOA, GPIO_Pin_5) /* SCK = 1 */ 2 #define SPI_SCK_0 GPIO_ResetBits(GPIOA, GPIO_Pin_5) /* SCK = 0 */ 3 4 #define SPI_MOSI_1 GPIO_SetBits(GPIOA, GPIO_Pin_7) /* MOSI = 1 */ 5 #define SPI_MOSI_0 GPIO_ResetBits(GPIOA, GPIO_Pin_7) /* MOSI = 0 */ 6 7 #define SPI_READ_MISO GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) /* 讀MISO口線狀態 */ 8 9 #define Dummy_Byte 0xFF //讀取時MISO發送的數據,可以為任意數據 10 11 12 //初始化SPI 13 void SPI_IoInit(void) 14 { 15 GPIO_InitTypeDef GPIO_InitStructure; 16 17 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE); 18 19 //CS引腳初始化 20 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 21 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽輸出 22 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 23 GPIO_Init(GPIOC, &GPIO_InitStructure); 24 25 //SCK和MOSI引腳初始化 26 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7; 27 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽輸出 28 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 29 GPIO_Init(GPIOA, &GPIO_InitStructure); 30 31 //MISO引腳初始化 32 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; 33 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ; //浮空輸入 34 GPIO_Init(GPIOA, &GPIO_InitStructure); 35 36 SPI_CS_1; 37 SPI_SCK_1; 38 } 39 40 //SPI可以同時讀取和寫入數據,因此一個函數即可滿足要求 41 uint8_t SPI_ReadWriteByte(uint8_t txData) 42 { 43 uint8_t i; 44 uint8_t rxData = 0; 45 46 for(i = 0; i < 8; i++) 47 { 48 SPI_SCK_0; 49 delay_us(1); 50 //數據發送 51 if(txData & 0x80){ 52 SPI_MOSI_1; 53 }else{ 54 SPI_MOSI_0; 55 } 56 txData <<= 1; 57 delay_us(1); 58 59 SPI_SCK_1; 60 delay_us(1); 61 //數據接收 62 rxData <<= 1; 63 if(SPI_READ_MISO){ 64 rxData |= 0x01; 65 } 66 delay_us(1); 67 } 68 SPI_SCK_0; 69 70 return rxData; 71 } 72 73 uint8_t SPI_ReadByte(void) 74 { 75 return SPI_ReadWriteByte(Dummy_Byte); 76 } 77 78 void SPI_WriteByte(uint8_t txData) 79 { 80 (void)SPI_ReadWriteByte(txData); 81 }