SPI總線是我們常用的串行設備接口,一般情況下我們都會適應硬件SPI接口,但有些時候當硬件端口不足時,我們也希望可以使用軟件來模擬SPI硬件接口,特別是要求不是很高的時候。在這一篇中我們將來討論如何使用GPIO和軟件來模擬SPI通訊接口。
1、功能概述
??SPI即串行外設接口,是一種同步串行通訊接口,用于微處理器及控制器和外圍擴展芯片之間的串行連接,現已發展成為一種工業標準。
1.1、物理層
??SPI總線在物理層通常使用3條總線及1條片選線,3條總線分別為 SCK、 MOSI、 MISO,片選線為NSS,它們的作用介紹如下:
??NSS( Slave Select),從設備選擇信號線,常稱為片選信號線。在SPI協議中沒有設備地址,所以需要使用NSS信號線來尋址,當主機要選擇從設備時,把該從設備的NSS信號線設置為低電平,該從設備即被選中,即片選有效,接著主機開始與被選中的從設備進行SPI通訊。所以SPI通訊以NSS線置低電平為開始信號,以NSS線被拉高作為結束信號。
??SCK (Serial Clock),時鐘信號線,用于通訊數據同步。它由通訊主機產生,決定了通訊的速率,不同的設備支持的最高時鐘頻率不一樣,如 STM32 的 SPI 時鐘頻率最大為fpclk/2,兩個設備之間通訊時,通訊速率受限于低速設備。
??MOSI (Master Output, Slave Input),主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出,從機由這條信號線讀入主機發送的數據,即這條線上數據的方向為主機到從機。
??MISO(Master Input,, Slave Output),主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。
??對于使用SPI總線來進行通訊的設備,一臺主機可以與多臺從機進行通訊,時鐘與數據總線為公用,片選線每臺從機都是獨立的,具體連接方式如下圖所示:

??也就是說,有多個SPI從設備與SPI主機通訊時,設備的時鐘線和總線數據線 SCK、MOSI及MISO同時并聯到相同的SPI總線上,即無論有多少個從設備,都共同只使用這 3 條總線。而每個從設備都擁有獨立的一條NSS信號線,即有多少個從設備,就有多少條片選信號線。
1.2、協議層
??我們已經簡述了SPI總線的物理連接方式,接下來我們再來了解一下具體的通訊協議。在了解協議前,我們需要清楚兩個概念,即時鐘的極性和時鐘的相位。
??所謂時鐘極性,通常稱作CPOL,即是指在空閑狀態下,時鐘所處的電平狀態。如果SCLK在數據發送之前和之后的空閑狀態是高電平,那么就是CPOL=1;如果空閑狀態SCLK是低電平,那么就是 CPOL=0。
??而時鐘的相位,通常稱作CPHA,就是指數據采樣是在時鐘脈沖的第1個跳變沿還是在第2個跳變沿。如果在SCK的第1個跳變沿進行數據采樣,則CPHA=0;如果在SCK的第2個跳變沿采樣,則CPHA=1。
??在通訊協議中,根據CPOL和CPHA的取值不同存在4種不同的配置方式,不同的配置方式對應不同的通訊模式。
??(1)當CPOL=0,CPHA=0時,空閑狀態時鐘SCK的電平要保持在低電平,而數據的采樣時刻在時鐘脈沖的奇數跳變邊沿,其時序圖如下:

??(2)當CPOL=0,CPHA=1時,空閑狀態時鐘SCK的電平要保持在低電平,而數據的采樣時刻在時鐘脈沖的偶數跳變邊沿,其時序圖如下:
??(3)當CPOL=1,CPHA=0時,空閑狀態時鐘SCK的電平要保持在高電平,而數據的采樣時刻在時鐘脈沖的奇數跳變邊沿,其時序圖如下:

??(4)當CPOL=1,CPHA=1時,空閑狀態時鐘SCK的電平要保持在高電平,而數據的采樣時刻在時鐘脈沖的偶數跳變邊沿,其時序圖如下:

??根據這時鐘情況,將SPI總線的工作模式分為4種,如下圖所示:

??而只有主機與從機擁有相同的工作模式時,主從機之間才可以正常通訊。在實際應用中,“模式 0”與“模式 3”是比較常見的工作模式,但在我們的驅動設計中應該兼顧這4種模式,以具備更廣泛的適應性。
2、驅動設計與實現
??我們已經簡要的描述了SPI通訊總線的物理連接和通訊協議,接下來我們將根據其協議的特性設計并實現基于GPIO模擬的SPI總線驅動。
2.1、對象定義
??我們依然使用基于對象的思想來實現基于GPIO模擬的SPI總線驅動。既然是基于對象,那么在使用一個對象之前我們需要先獲得這個對象。所以我們必須先定義一個基于GPIO模擬的SPI總線的對象。
2.1.1、對象的抽象
??我們要得到基于GPIO模擬的SPI總線的對象,需要先分析其基本特性。一般來說,一個對象至少包含屬性與操作兩方面的特性。接下來我們就來從這兩個方面思考一下基于GPIO模擬的SPI總線的對象。
??我們首先來考慮對象的屬性,作為屬性肯定是用于標識或記錄對象特征的東西。我們在前面已經了解到SPI總線的一些特點和獨特設定,那么這些特點和設定是否能成為對象的屬性呢?
??我們考慮到作為同步總線,SPI總線的控制需要時鐘,這關系到SPI總線的通訊速率,這一通訊速率不僅配置SPI總線的工作方式也標識當前的工作狀態,所以我們將工作速率作為模擬SPI總線對象的一個屬性。在前面我們討論過SPI總線的工作模式,工作模式由CPOL和CPHA決定,所以在初始化總線時就會確定工作模式,所以我們需要記錄CPOL和CPHA,我們將CPOL和CPHA也作為對象的屬性。
??接下來我們考慮GPIO模擬SPI總線的操作問題。我們將那些對象要實現的,并且依賴于具體的平臺的行為實現定義為對象的操作。對于GPIO模擬SPI總線來說,我們需要通過總線發送數據和接收數據,而接收和發送的實現都依賴于具體的軟硬件平臺,所以我們將發送數據和接收數據定義為對象的操作。SPI總線作為同步總線需要時鐘,而時鐘操作也是依賴于具體的軟硬件平臺來實現,所以我們將始終的控制也作為對象的操作。
??根據上述我們對GPIO模擬SPI總線的分析,我們可以定義GPIO模擬SPI總線對象類型如下:
/*定義GPIO模擬SPI接口對象*/typedef struct SimuSPIObject{
uint16_t CPOL:1; uint16_t CPHA:1; uint16_t period:14; //確定速度為大于0K小于等于400K的整數,默認為100K
void (*SetSCKPin)(SimuSPIPinValueType op); //設置SCL引腳
void (*SetMOSIPin)(SimuSPIPinValueType op); //設置SDA引腳
uint8_t (*ReadMISOPin)(void); //讀取SDA引腳位
void (*Delayus)(volatile uint32_t period); //速度延時函數}SimuSPIObjectType;
2.1.2、對象初始化
??我們知道,在使用一個對象之前需要先對其進行初始化,所以這里我們來考慮GPIO模擬SPI對象的初始化函數。一般來說,初始化函數需要處理幾個方面的問題。一是檢查輸入參數是否合理;二是為對象的屬性賦初值;三是對對象做必要的初始化配置。據此我們設計GPIO模擬SPI對象的初始化函數如下:
/* GPIO模擬SPI通訊初始化 */void SimuSPIInitialization(SimuSPIObjectType *simuSPIInstance,//初始化的模擬SPI對象
uint32_t speed, //時鐘頻率
SimuSPICPOLType CPOL, //時鐘極性
SimuSPICPHAType CPHA, //時鐘頻率
SimuSPISetSCKPin setSCK, //SCK時鐘操作函數指針
SimuSPISetMOSIPin setMOSI, //MOSI操作函數指針
SimuSPIReadMISOPin getMISO, //MISO操作函數指針
SimuSPIDelayus delayus //微秒延時操作函數指針
){ if((simuSPIInstance==NULL)||(setSCK==NULL)||(setMOSI==NULL)||(getMISO==NULL)||(delayus==NULL))
{ return;
}
simuSPIInstance->SetSCKPin=setSCK;
simuSPIInstance->SetMOSIPin=setMOSI;
simuSPIInstance->ReadMISOPin=getMISO;
simuSPIInstance->Delayus=delayus;
/*初始化速度,默認100K*/
if((speed>0)&&(speed<=500))
{
simuSPIInstance->period=500/speed;
} else
{
simuSPIInstance->period=5;
}
simuSPIInstance->CPOL=CPOL;
simuSPIInstance->CPHA=CPHA;
/*拉高總線,使處于空閑狀態*/
if(simuSPIInstance->CPOL==SimuSPI_POLARITY_LOW)
{
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
} else
{
simuSPIInstance->SetSCKPin(SimuSPI_Set);
}
}
2.2、對象操作
??我們已經定義了對象類型,也實現了對象的初始化函數,但我們還沒有實現對象的具體操作,所以接下來我們就來實現對象的具體操作。
2.2.1、數據的發送
??在我們使用SPI來實現數據通訊時,免不了要發送數據,所以在我們使用GPIO模擬SPI端口時就需要解決數據發送的問題。這里我們考慮使用模擬SPI發送一個字節的問題,因為發送多個字節無非是多重復幾次。根據前面分析的在不同模式下的時序圖我們可以編寫GPIO模擬SPI發送一個字節的函數如下:
/* 通過模擬SPI發送一個字節 */static void SendByteBySimuSPI(SimuSPIObjectType *simuSPIInstance,uint8_t byte){// uint8_t length[2]={8,16};
if(simuSPIInstance->CPOL==SimuSPI_POLARITY_LOW)
{ /*拉低SCL引腳準備數據傳輸*/
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
if(simuSPIInstance->CPHA==SimuSPI_PHASE_1EDGE) //模式0
{ for(uint8_t count = 0; count < 8; count++)
{ if(byte & 0x80) //每次發送最高位
{
simuSPIInstance->SetMOSIPin(SimuSPI_Set);
} else
{
simuSPIInstance->SetMOSIPin(SimuSPI_Reset);
}
byte <<= 1; //發送一位后,左移一位
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
}
} else //模式1
{ for(uint8_t count = 0; count < 8; count++)
{ if(byte & 0x80) //每次發送最高位
{
simuSPIInstance->SetMOSIPin(SimuSPI_Set);
} else
{
simuSPIInstance->SetMOSIPin(SimuSPI_Reset);
}
byte <<= 1; //發送一位后,左移一位
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
}
}
} else
{ /*拉低SCL引腳準備數據傳輸*/
simuSPIInstance->SetSCKPin(SimuSPI_Set);
if(simuSPIInstance->CPHA==SimuSPI_PHASE_1EDGE) //模式2
{ for(uint8_t count = 0; count < 8; count++)
{ if(byte & 0x80) //每次發送最高位
{
simuSPIInstance->SetMOSIPin(SimuSPI_Set);
} else
{
simuSPIInstance->SetMOSIPin(SimuSPI_Reset);
}
byte <<= 1; //發送一位后,左移一位
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Set);
}
} else //模式3
{ for(uint8_t count = 0; count < 8; count++)
{ if(byte & 0x80) //每次發送最高位
{
simuSPIInstance->SetMOSIPin(SimuSPI_Set);
} else
{
simuSPIInstance->SetMOSIPin(SimuSPI_Reset);
}
byte <<= 1; //發送一位后,左移一位
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
simuSPIInstance->SetSCKPin(SimuSPI_Set);
}
}
}
}
2.2.2、數據的接收
??對于SPI端口來說不光需要發送數據,也需要從對方接收數據,同樣的我們再次考慮接收一個字節的情況。同樣我們根據前面對不同模式下,接收數據的時序要求可以編寫接收一個字節的函數如下:
/* 通過模擬SPI接收一個字節 */static uint8_t RecieveByteBySimuSPI(SimuSPIObjectType *simuSPIInstance){ uint8_t receive = 0;
if(simuSPIInstance->CPOL==SimuSPI_POLARITY_LOW)
{ /*拉低SCL引腳準備數據傳輸*/
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
if(simuSPIInstance->CPHA==SimuSPI_PHASE_1EDGE) //模式0
{ for(uint8_t count = 0; count < 8; count++ )
{
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
receive <<= 1;
if(simuSPIInstance->ReadMISOPin())
{
receive++;
}
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
}
} else //模式1
{
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period); for(uint8_t count = 0; count < 8; count++ )
{
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
receive <<= 1;
if(simuSPIInstance->ReadMISOPin())
{
receive++;
}
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
}
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
}
} else
{ /*拉低SCL引腳準備數據傳輸*/
simuSPIInstance->SetSCKPin(SimuSPI_Set);
if(simuSPIInstance->CPHA==SimuSPI_PHASE_1EDGE) //模式2
{ for(uint8_t count = 0; count < 8; count++ )
{
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
receive <<= 1;
if(simuSPIInstance->ReadMISOPin())
{
receive++;
}
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
}
} else //模式3
{
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period); for(uint8_t count = 0; count < 8; count++ )
{
simuSPIInstance->SetSCKPin(SimuSPI_Set);
simuSPIInstance->Delayus(simuSPIInstance->period);
receive <<= 1;
if(simuSPIInstance->ReadMISOPin())
{
receive++;
}
simuSPIInstance->SetSCKPin(SimuSPI_Reset);
simuSPIInstance->Delayus(simuSPIInstance->period);
}
simuSPIInstance->SetSCKPin(SimuSPI_Set);
}
}
return receive;
}
3、驅動的使用
??我們已經設計并實現了GPIO模擬SPI總線的驅動程序,接下來我們將基于這一驅動設計一個簡單的用例,以驗證驅動程序的正確性。
3.1、聲明并初始化對象
??我們是基于對象來實現GPIO模擬SPI驅動的,所以在開始之前我們需要聲明一個模擬SPI對象如下:
SimuSPIObjectType simuSPI;
??聲明了這一對象之后,我們還需要對這一變量進行初始化才能使用。前面我們已經實現了對象變量的初始化函數,使用這一函數就可方便的初始化對象變量,該函數有多個輸入數:
SimuSPIObjectType *simuSPIInstance,//初始化的模擬SPI對象uint32_t speed, //時鐘頻率SimuSPICPOLType CPOL, //時鐘極性SimuSPICPHAType CPHA, //時鐘相位SimuSPIDataSizeType dataSize,//數據長度SimuSPISetSCKPin setSCK, //SCK時鐘操作函數指針SimuSPISetMOSIPin setMOSI, //MOSI操作函數指針SimuSPIReadMISOPin getMISO, //MISO操作函數指針SimuSPIDelayus delayus //微秒延時操作函數指針
??在這些參數中simuSPIInstance為我們想要初始化的對象變量的指針。時鐘極性、時鐘相位以及數據長度都是枚舉量,我們根據實際的使用要求選擇輸入即可。時鐘頻率為我們希望的時鐘速度,最大500K。而余下的幾個參數則都是回調函數的指針。而這幾個函數則是我們在應用程序中需要實現的,它們的原型如下:
//設置SCL引腳typedef void (*SimuSPISetSCKPin)(SimuSPIPinValueType op);//設置SDA引腳typedef void (*SimuSPISetMOSIPin)(SimuSPIPinValueType op);//讀取SDA引腳位typedef uint8_t (*SimuSPIReadMISOPin)(void);//速度延時函數typedef void (*SimuSPIDelayus)(volatile uint32_t period);
??這些函數的實現與具體的應用平臺有關,我們在STM32F407上基于HAL庫實現的這些函數如下:
//設置SCL引腳void SPISCKOperation(SimuSPIPinValueType op){
GPIO_PinState PinState=(GPIO_PinState)op;
HAL_GPIO_WritePin(GPIOSPI, SPISCK, PinState);
}//設置SDA引腳void SPIMOSIOperation(SimuSPIPinValueType op){
GPIO_PinState PinState=(GPIO_PinState)op;
HAL_GPIO_WritePin(GPIOSPI, SPIMOSI, PinState);
}//讀取SDA引腳位uint8_t SPIMISORead(void){ if(HAL_GPIO_ReadPin(GPIOSPI, SPIMISO))
{ return 1;
}
return 0;
}
??而延時操作函數則采用我們系統中通用的Delayus。有了這些參數后我們合一調用初始化函數對對象變量進行初始化如下:
/* GPIO模擬SPI通訊初始化 */
SimuSPIInitialization(&simuSPI,//初始化的模擬SPI對象
500, //時鐘頻率
SimuSPI_POLARITY_LOW, //時鐘極性
SimuSPI_PHASE_1EDGE, //時鐘頻率
SimuSPI_DataSize_8Bit,//數據長度
SPISCKOperation, //SCK時鐘操作函數指針
SPIMOSIOperation, //MOSI操作函數指針
SPIMISORead, //MISO操作函數指針
Delayus //微秒延時操作函數指針
);
3.2、基于對象進行操作
??初始化這個對象變量后,我們就可以基于它操作這一對象了。我們基于驅動實現一個簡單的讀寫數據的操作如下:
/* 使用模擬SPI讀寫數據*/void SimuSPIDataExchange(void){ uint8_t wDatas[3]; uint8_t rDatas[3];
/* 通過模擬SPI向從站寫數據 */
WriteDataBySimuSPI(&simuSPI,wDatas,3,1000);
HAL_Delay(10);
/* 通過模擬SPI自從站讀數據 */
ReadDataBySimuSPI(&simuSPI,rDatas, 3,1000);
HAL_Delay(10);
/* 通過模擬SPI實現對從站先寫數據緊接讀數據組合操作 */
WriteReadDataBySimuSPI(&simuSPI, wDatas,3,rDatas, 3,1000);
HAL_Delay(10);
/* 通過模擬SPI實現對從站同時寫和讀數據組合操作*/
WriteWhileReadDataBySimuSPI(&simuSPI, wDatas,rDatas,3,1000);
}
??我們分別測試了讀取數據、下發數據、同時寫和讀數據以及寫完后再讀等幾種情況的測試,效果還是比較理想的。
4、應用總結
??在這一篇中,我們設計并實現了基于GPIO模擬的SPI接口驅動程序。并在此基礎上設計了一個簡單的測試應用。我們通過GPIO模擬的SPI接口向SPI接口的Flash中寫數據、讀數據、同時讀寫和先寫后讀試驗都沒有問題。
??在使用驅動程序時需要注意,由于是使用GPIO模擬的SPI端口,其速度是受到限制的,目前最快能夠支持到500K,再快就不能支持了。所以這個驅動程序只能應用于通訊速度小于500K的設備。
電子發燒友App






















評論