1.接口定義:
SPI 總線是由前摩托羅拉公司命名的一種工作與全雙工模式的同步數據通信標准。SPI 是單主設備(single-master)通信協議,這意味着總線中的只有一支中心設備能發起通信。
當SPI以主從方式工作的,通常有一個主器件和一個或多個從器件,當具有多個設備的時候通過片選信號選中/失能設備,通常 SPI 使用3條通訊總線和1條片選線,定義如下:
MOSI – 主器件數據輸出,從器件數據輸入
MISO – 主器件數據輸入,從器件數據輸出
SCLK – 時鍾信號,由主器件產生
nSS – 從器件片選/使能信號,由主器件控制,低電平有效
-- 在點對點的通信中,SPI接口不需要進行尋址操作,且為全雙工通信,顯得簡單高效。
-- 在多個從器件的系統中,每個從器件需要獨立的使能信號,硬件上比 I2C 系統要稍微復雜一些。
注意:SPI也有3線模式。即將 MOSI,MISO合並為 DATA 線,但此時 SPI 就不再能實現高效的全雙工通信了。
2.工作模式
SPI 根據時鍾極性和時鍾相位的不同可以有4種工作模式,通常在編寫 SPI 設備驅動程序時需要特別注意從設備的時鍾相位與時鍾極性,如果與主設備(SOC)不一致時,一般都不能進行正常的通信。
-- 時鍾極性(CPOL)指通訊設備處於空閑狀態( SPI 開始通訊前、nSS 線無效)時 SCK 的狀態。
CPOL = 0:SCK在空閑時為低電平
CPOL = 1:SCK在空閑時為高電平
-- 時鍾相位(CPHA)指數據的采樣時刻位於 SCK 的偶數邊沿采樣還是奇數邊沿采樣。
CPHA = 0:在SCK的奇數邊沿采樣
CPHA = 1:在SCK的偶數邊沿采樣
-- 通過時鍾極性和時鍾相位的不同組合 SPI 總共可以設置為4種工作模式
MODE0 : CPOL = 0 CPHA = 0 SCK空閑為低,SCK的奇數次邊沿采樣
MODE1 : CPOL = 0 CPHA = 1 SCK空閑為低,SCK的偶數次邊沿采樣
MODE2 : CPOL = 1 CPHA = 0 SCK空閑為高,SCK的奇數次邊沿采樣
MODE3 : CPOL = 1 CPHA = 1 SCK空閑為高,SCK的偶數次邊沿采樣
3.SPI時序分析實例
SPI 通訊時 nSS 、SCK 、MOSI 信號均由主機產生,MISO 信號由從機產生。在 nSS 為低電平(片選選中)的前提下,MOSI 和 MISO 信號才有效。在每個時鍾周期 MOSI 和 MISO 傳輸一位數據。跟I2C通訊類似,SPI通訊也需要通訊的起始/結束信號,有效數據 和 同步時鍾。
起始/結束信號:
nSS 信號由高電平變為低電平即為 SPI 通訊的起始信號,反過來, nSS 信號由低電平變為高電平即為SPI通訊的結束信號。
當從機檢測到自身的 nSS 引腳被拉低時就知道自己被主機選中,准備和主機進行通訊。
數據同步:
SCK 用於數據同步,MOSI 和 MISO 線上的數據在每個 SCK 時鍾周期傳輸一位數據,數據的輸入/輸出可以同時進行(雙線全雙工)。
3.1 ADE7953 SPI 時序分析
現在通過分析一款之前筆者使用過的芯片 ADE7953,來對 SPI 進行跟深入的理解。首先我們查看其用戶手冊,找到它的 SPI 時序圖。
上圖即是ADE7953的時序圖和時序圖中對應的參數表,通常我們分析時序圖都是結合參數表一起來分析的。其中作為編程者的角度我們需要理解的東西並不多,下面我們結合部分參數對這個時序圖進行簡單的分析:
1. 首先主設備拉低CS片選選中該設備。
2. 延時至少t_CS之后,主設備拉低時鍾線,等待最多t_SF后,時鍾線變為低電平。
3. 在時鍾線被拉低后,再經過t_DAV的時間,該設備會在MISO線上准備好數據供主設備讀取主設備應在數據保持的時間內讀取出來(這里的數據保持時間比較長)
4. 時鍾線低電平維持至少t_SL后,開始進入上升沿,等待最多t_SR時間,時鍾線變為高
5. 主設備需要時鍾線為高之前至少 t_DSU時間內,在MOSI上建立需要數據,因為從設備會在時鍾線為高之后的t_DHD時間內,從MOSI線上讀取數據同時主機需要在這個時間內一直維持數據有效。
6. 時鍾線高電平維持至少t_SH后,拉低時鍾線,一個周期結束,開始下一個周期的數據傳輸
通過以上的分析可以了解到:
從設備會在時鍾線拉低之后延時 t_DAV 之后,輸出數據,直到時鍾線為低,數據仍然保持.
從設備會在上升沿結束后的 t_DHD 時間內讀取主設備寫入到 MOSI 上的數據.
同時也可以看出,時鍾線空閑時為高電平(CPOL = 1),數據采樣發生在SCLK的第2個邊沿,即偶數邊沿(CPHA = 1)所以如果使用硬件SPI需要配置主設備SPI為模式3.
SCLK 周期也反映了 SPI 的通信速率,這里看到最小值是 200ns,也就意味着 SPI 最大的通信速率可以達到 5MHZ,同時每個時鍾周期交換一位數據,可以大致算出每秒最大能傳輸的數據量。
4. IO模擬SPI讀寫實現
至此就可以大致歸納出模擬該設備SPI通信的編程思路:
1.拉低片選信號
2.延時 至少t_cs 拉低時鍾線
3.延時 至少t_sl 后,主設備立即將需要發送的電平信號發送待 MOSI 上
4.再拉高時鍾線,主設備讀 MISO 的電平信號,時鍾高維持 t_sh 時間后周期結束
5.垃低時鍾線,重復步驟3、4開始讀取下一位的數據
7.最后一位數據讀取完成,拉高片選
注意:需要保證交換一位數據的周期時間大於 200ns
通過以上分析,我們就能明確編程思路了,這里我為了節省空間,是將 SPI 的讀寫函數寫到了一起,當然也可以分開寫。
1 uint8_t SPI_ExchangeByte(uint8_t data) 2 { 3 uint8_t ret; 4 uint8_t cnt; 5 for(cnt = 0; cnt < 8; cnt++) 6 { 7 SCLK_CTRL(0); /*拉低時鍾線*/ 8 delay_ns(200); /*t_sl 至少80ns,這里給200ns*/ 9 if(data&0x80) MOSI_CTRL(1); /*主設備發送電平信號到 MOSI*/ 10 else MOSI_CTRL(0); 11 12 SCLK_CTRL(1); /*拉高時鍾線,從設備會在之后的 5ns 內讀走 MOSI 上信號*/ 13 if(MISO_READ()) res|=0x01; /*主設備讀取 MISO 上的信號*/ 14 else res&=0xfe; 15 data<<=1; 16 ret<<=1; 17 delay_ns(200); /*t_sh 至少80ns,這里給200ns*/ 18 } 19 }
ADE7953的SPI只支持模式3,如果有其他模式的器件,我們也可以舉一反三:
如果有設備CPOL = 0,CPHA = 1:只需要將 SCLK_CTRL(0),和 SCLK_CTRL(1) 交換一下位置即可
類似還有CPOL = 1,CPHA = 0:此時數據采樣發生在奇數邊沿,即拉低SCLK時,
主設備需要在此之前發送電平信號到 MOSI 以供從設備采樣,也需要在此之后采樣從設備發送到 MISO 上的信號,我們只需要將上面的代碼,交換主設備讀和主設備寫兩部分的位置即可。
注意:理解時鍾相位只需要明白一點,無論我們的編程對象是主機還是從機只需要記住在采樣邊沿發生之前,應該准備好我們當前設備需要發送的數據在采樣邊沿發生之后,再去讀取其他設備發送過來的數據。
現在我們可以將上面驅動完善一下,實現多字節寄存器數據的讀寫,單字節讀寫類似:
1 void ADE7953_Read_Byte(uint16_t reg, uint8_t *pBuffer, uint16_t nLen) 2 { 3 uint16_t cnt; 4 uint8_t buf[3]; 5 6 buf[0] = (uint8_t)(reg >> 8); /*ADE7953 的寄存器是16位的,這里需要拆成兩個字節發送*/ 7 buf[1] = (uint8_t)reg; 8 buf[2] = 0x80; /*ADE7953 讀數時還需要發送數據讀取標識字節*/ 9 10 CS_CTRL(0); /*片選使能*/ 11 SPI_ExchangeByte(buf[0]); /*發送寄存器地址和讀取標識字節*/ 12 SPI_ExchangeByte(buf[1]); 13 SPI_ExchangeByte(buf[2]); 14 for(cnt = 0; cnt < nLen; cnt++) 15 { 16 /*循環讀取數據,讀數時可以發送空字節(NOP = 0)*/ 17 pBuffer[cnt] = SPI_ExchangeByte(NOP); 18 } 19 CS_CTRL(1); /*片選失能,數據讀取完成*/ 20 } 21 22 void ADE7953_Write_NByte(uint16_t reg, uint8_t *pBuffer, uint16_t nLen) 23 { 24 uint16_t cnt; 25 uint8_t buf[3]; 26 27 buf[0] = (uint8_t)(reg >> 8); /*ADE7953 的寄存器是16位的,這里需要拆成兩個字節發送*/ 28 buf[1] = (uint8_t) reg; 29 buf[2] = 0x00; /*需要發送寫入標識字節*/ 30 31 CS_CTRL(0); /*片選使能*/ 32 SPI_ExchangeByte(buf[0]); /*發送寄存器地址和寫入標識字節*/ 33 SPI_ExchangeByte(buf[1]); 34 SPI_ExchangeByte(buf[2]); 35 for(cnt = 0; cnt < nLen; cnt++) 36 { 37 /*將 pBuffer中的數據循環寫入*/ 38 SPI_ExchangeByte(pBuffer[cnt]); 39 } 40 CS_CTRL(1); /*片選失能,數據寫入完成*/ 41 }
5. STM8硬件SPI及SPI參數配置
5.1 stm8l151c8t6 SPI 初始化函數及參數配置
1 void STM8L15x_SPI2_Init() 2 { 3 CLK_PeripheralClockConfig(CLK_Peripheral_SPI2,ENABLE); 4 SPI_DeInit(SPI2); 5 SPI_Init(SPI2,SPI_FirstBit_MSB, /*高位在前*/ 6 SPI_BaudRatePrescaler_4, /*SPI時鍾預分頻4*/ 7 SPI_Mode_Master, /*主機模式*/ 8 SPI_CPOL_High, /*SCLK空閑為高*/ 9 SPI_CPHA_2Edge, /*SCLK的偶數次邊沿采樣*/ 10 SPI_Direction_2Lines_FullDuplex, /*2數據線,全雙工*/ 11 SPI_NSS_Soft, /*NSS信號由硬件管理,不需要編程操作*/ 12 7); /*CRC 值計算的多項式*/ 13 SPI_Cmd(SPI2,ENABLE); 14 /*SPI相關IO口配置*/ 15 GPIO_Init(PORT_SPI, PIN_MISO, GPIO_Mode_In_PU_No_IT); // MISO 16 GPIO_Init(PORT_SPI, PIN_SCLK, GPIO_Mode_Out_PP_High_Slow); // SCLK 17 GPIO_Init(PORT_SPI, PIN_MOSI, GPIO_Mode_Out_PP_High_Slow); // MOSI 18 }
5.2 spi 單字節讀寫函數
1 uint8_t SPI2_ExchangeByte(uint8_t data) 2 { 3 SPI_SendData(SPI2, data); 4 while (RESET == SPI_GetFlagStatus(SPI2, SPI_FLAG_TXE)); // 等待數據傳輸完成 5 while (RESET == SPI_GetFlagStatus(SPI2, SPI_FLAG_RXNE)); // 等待數據接收完成 6 return (SPI_ReceiveData(SPI2)); 7 }
注意:如果使用 stm8 的內部高速時鍾 HSI,需要設置足夠大的分頻系數,比如 ADE7953 SPI 速率最大為 5MHZ, 如果這里分頻系數為 SPI_BaudRatePrescaler_2,則 stm8 的 spi 時鍾為 16M/2=8MHZ,超過了 ADE7953 支持的最大速率,很有可能導致通信失敗。所以這里需要注意計算 spi 的通信速率。
6. STM32硬件SPI及SPI參數配置
6.1. stm32f407 spi 配置(hal庫版本)
1 SPI_HandleTypeDef hspi1; 2 void STM32F4x_SPI_Init(void) 3 { 4 hspi1.Instance = SPI1; 5 hspi1.Init.Mode = SPI_MODE_MASTER; /*SPI模式:主機模式*/ 6 hspi1.Init.Direction = SPI_DIRECTION_2LINES; /*雙線雙向全雙工*/ 7 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; /*SPI發送接收幀:8位*/ 8 hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; /*時鍾線空閑時電平:高*/ 9 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; /*數據采樣邊沿:偶數次邊沿*/ 10 hspi1.Init.NSS = SPI_NSS_SOFT; /*NSS信號控制:軟件*/ 11 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; /*數據傳輸首位:MSB位*/ 12 hspi1.Init.TIMode = SPI_TIMODE_DISABLE; /*SPI TIM MODE:失能*/ 13 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /*預分頻:256*/ 14 hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /*CRC校驗計算:禁止*/ 15 hspi1.Init.CRCPolynomial = 7; /*CRC多項式:7*/ 16 HAL_SPI_Init(&hspi1); 17 } 18 19 void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) 20 { 21 GPIO_InitTypeDef GPIO_InitStruct = {0}; 22 if(spiHandle->Instance==SPI1) 23 { 24 __HAL_RCC_SPI1_CLK_ENABLE(); 25 __HAL_RCC_GPIOA_CLK_ENABLE(); 26 GPIO_InitStruct.Pin = SPI1_SCK_Pin|SPI1_MISO_Pin|SPI1_MOSI_Pin; 27 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; 28 GPIO_InitStruct.Pull = GPIO_NOPULL; 29 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; 30 GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; 31 HAL_GPIO_Init(SPI1_Port, &GPIO_InitStruct); 32 } 33 } 34 35 uint8_t SPI1_Exchange_Byte(uint8_t data) 36 { 37 uint8_t ret; 38 if(HAL_SPI_TransmitReceive(&hspi1, &data, &ret, 1, 500) != HAL_OK) 39 { 40 Error_Handler(); 41 } 42 return ret; 43 }
注意: 這里除了需要注意 SPI的速率之外還需要注意SPI的引腳復用,關於STM32F4的引腳復用可以查看官方的 Datasheet 第三章 Pinouts and pin description.
6.2. stm32f407 spi 配置(std庫版本)
1 void STM32F4x_SPI_Init(void) 2 { 3 GPIO_InitTypeDef GPIO_InitStructure; 4 SPI_InitTypeDef SPI_InitStructure; 5 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //使能 GPIOB 時鍾 6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 使能 SPI1 時鍾 7 8 /*GPIOFB3,4,5 初始化設置: 復用功能輸出*/ 9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5; 10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //復用功能 11 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽輸出 12 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100MHz 13 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 14 GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化 15 16 /*配置引腳復用映射*/ 17 GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3 復用為 SPI1 18 GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB4 復用為 SPI1 19 GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB5 復用為 SPI1 20 21 /*這里只針對 SPI 口初始化*/ 22 RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE); //復位 SPI1 23 RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE); //停止復位 SPI1 24 25 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; /*雙線雙向全雙工*/ 26 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; /*SPI模式:主機模式*/ 27 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; /*SPI發送接收幀:8位*/ 28 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; /*時鍾線空閑時電平:高*/ 29 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; /*數據采樣邊沿:偶數次邊沿*/ 30 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; /*NSS信號控制:軟件*/ 31 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; /*預分頻:256*/ 32 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; /*數據傳輸首位:MSB位*/ 33 SPI_InitStructure.SPI_CRCPolynomial = 7; /*CRC多項式:7*/ 34 SPI_Init(SPI1, &SPI_InitStructure); 35 SPI_Cmd(SPI1, ENABLE); /*使能 SPI1*/ 36 } 37 38 uint8_t SPI_Exchange_Byte(uint8_t data) 39 { 40 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){} //等待發送區空 41 SPI_I2S_SendData(SPI1, data); //通過外設 SPIx 發送一個 byte 數據 42 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){} //等待接收完 43 return SPI_I2S_ReceiveData(SPI1); //返回通過 SPIx 最近接收的數據 44 }