SPI:Serial Peripheral interface,串行外圍設備接口。
SPI接口一般使用4條線通信:
MISO主設備數據輸入,從設備數據輸出。
MOSI主設備數據輸出,從設備數據輸入。
SCLK時鐘信號,由主設備產生。
從圖中可以看出,主機和從機都有一個串行移位寄存器,主機通過向它的SPI串行寄存器寫入一個字節來發起一次傳輸。寄存器通過MOSI信號線將字節傳送給從機,從機也將自己的移位寄存器中的內容通過MISO信號線返回給主機。這樣,兩個移位寄存器中的內容就被交換。外設的寫操作和讀操作是同步完成的。如果只進行寫操作,主機只需忽略接收到的字節;反之,若主機要讀取從機的一個字節,就必須發送一個空字節來引發從機的傳輸,這個空字節通常稱為dummy_byte。
SPI總線四種工作方式是為了和不同外設進行數據交換,其輸出串行同步時鐘極性和相位可以進行配置。
時鐘極性CPOL=0,串行同步時鐘的空閑狀態為低電平;如果CPOL=1,串行同步時鐘的空閑狀態為高電平。
時鐘相位CPHA=0,在串行同步時鐘的第一個跳變沿(上升或下降)數據被采樣;如果CPHA=1,在串行同步時鐘的第二個跳變沿(上升或下降)數據被采樣。
SPI主從設備的時鐘相位和極性應該一致。
?
STM32F1的SPI時鐘最多可以到18Mhz。可用DMA。
SPI配置示例
void SPI2_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;SPI_InitTypeDef SPI_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_SetBits(GPIOB, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;SPI_InitStructure.SPI_Mode = SPI_Mode_Master;SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;SPI_InitStructure.SPI_CRCPolynomial = 7;SPI_Init(SPI2, &SPI_InitStructure);SPI_Cmd (SPI2, ENABLE);SPI2_ReadWriteByte(0xff);
}
SPI_Direction:選擇半雙工、全雙工。
SPI_Mode:選擇主機模式、從機模式。
SPI_DataSize:選擇8位還是16位幀格式傳輸。
SPI_CPOL:設置時鐘極性為高電平還是低電平。
SPI_CPHA:設置時鐘相位,選擇在時鐘的第幾個跳變沿數據被采樣。
SPI_NSS:設置NSS信號由硬件(NSS管腳)還是軟件控制。
SPI_BaudRatePrescaler:設置SPI波特率預分頻值。示例選擇256即本次SPI傳輸速度=36M/256=140.625KHz。
SPI_FirstBit:設置數據傳輸順序是MSB位在前還是LSB位在前。大部分應用是MSB在前。
SPI_CRCPolynomial:設置CRC校驗多項式,提高通信可靠性,大于1即可。
?
標準庫里SPI發送一個字節函數為
SPI_I2S_SendData(SPI2, data);
接受一個字節函數為
data =?SPI_I2S_ReceiveData(SPI2);
因此需要我們再封裝幾個函數,SPI寫任意字節,讀任意字節,讀寫任意字節。
?
SPI最常見的應用場景是讀寫FLASH。
正點原子戰艦板子上的FLASH為W25Q128。該款FLASH的容量為128Mb也就是16M字節。將16M的容量分為256個塊(Block),每個塊大小為64K字節,每個塊又分為16個扇區(Sector),每個扇區4K字節。W25Q128最小擦除單位為一個扇區,也就是每次必須擦除4K字節。這樣就需要開辟一個至少4K的緩存區,即單片機需要有4K以上的SRAM才能很好操作。W25Q128最大SPI時鐘能達80Mhz。
?
FLASH拿到手通常先測試能否到它的ID
從W25QXX datasheet ?里看到,主機給它發0x90,0x00,0x00,0x00,即可讀取16位ID
u16 W25QXX_ReadID(void)
{u16 Temp = 0;W25QXX_CS = 0;SPI2_ReadWriteByte(0x90);SPI2_ReadWriteByte(0x00);SPI2_ReadWriteByte(0x00);SPI2_ReadWriteByte(0x00);Temp |= SPI2_ReadWriteByte(0xFF) << 8;Temp |= SPI2_ReadWriteByte(0xFF);W25QXX_CS = 1;return Temp;
}
這里要注意讀ID時MOSI發送的是0xFF,這個就是DUMMY_BYTE,大多DUMMY_BYTE是0xFF,少部分設備會有特殊要求。
FLASH讀取數據,先發0x03和讀取數據的地址起點,然后即可讀取。
代碼如下
void W25QXX_Read(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{ u16 i; W25QXX_CS = 0;SPI2_ReadWriteByte(0x03);SPI2_ReadWriteByte((u8) ((ReadAddr) >> 16));SPI2_ReadWriteByte((u8) ((ReadAddr) >> 8)); SPI2_ReadWriteByte((u8) ReadAddr);for(i = 0; i < NumByteToRead; i++){ pBuffer[i] = SPI2_ReadWriteByte(0XFF);}W25QXX_CS = 1;
}
?
W25Q128寫使能,要給flash寫數據,必須先寫使能。
從W25QXX datasheet ?里看到,只要發0x06即可寫使能。
void W25QXX_Write_Enable(void)
{W25QXX_CS=0;SPI2_ReadWriteByte(0x06);W25QXX_CS=1;
}
?
寫使能后還不能立即往FLASH里面寫數據,需要將寫入數據的FLASH地址擦除。
void W25QXX_Erase_Sector(u32 Dst_Addr)
{ Dst_Addr *= 4096;W25QXX_Write_Enable();W25QXX_Wait_Busy();W25QXX_CS = 0;SPI2_ReadWriteByte(0x20);SPI2_ReadWriteByte((u8) ((Dst_Addr) >> 16)); SPI2_ReadWriteByte((u8) ((Dst_Addr) >> 8)); SPI2_ReadWriteByte((u8) Dst_Addr);W25QXX_CS = 1;W25QXX_Wait_Busy();
}
?
FLASH寫數據需要考慮跨扇區寫入的問題,因為FLASH只提供了往一個頁page寫數據的命令0x02。
void W25QXX_Write_Page(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{u16 i;W25QXX_Write_Enable();W25QXX_CS = 0;SPI2_ReadWriteByte(0x02);SPI2_ReadWriteByte((u8) ((WriteAddr) >> 16));SPI2_ReadWriteByte((u8) ((WriteAddr) >> 8));SPI2_ReadWriteByte((u8) WriteAddr);for(i = 0; i < NumByteToWrite; i++){SPI2_ReadWriteByte(pBuffer[i]);}W25QXX_CS = 1;W25QXX_Wait_Busy();
} void W25QXX_Write_NoCheck(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{ u16 pageremain;pageremain = 256 - WriteAddr % 256;if(NumByteToWrite <= pageremain){pageremain = NumByteToWrite;}while(1){W25QXX_Write_Page(pBuffer , WriteAddr, pageremain);if(NumByteToWrite == pageremain){ break;}else{pBuffer += pageremain;WriteAddr += pageremain; NumByteToWrite -= pageremain;if(NumByteToWrite > 256){pageremain = 256;}else{pageremain = NumByteToWrite;}}};
} u8 W25QXX_BUFFER[4096];void W25QXX_Write(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{u32 secpos;u16 secoff;u16 secremain;u16 i;u8 * W25QXX_BUF;W25QXX_BUF = W25QXX_BUFFER; secpos = WriteAddr / 4096; secoff = WriteAddr % 4096;secremain = 4096 - secoff; if(NumByteToWrite <= secremain){secremain = NumByteToWrite;}while(1) { W25QXX_Read(W25QXX_BUF, secpos * 4096, 4096);for(i = 0; i < secremain; i++){if(W25QXX_BUF[secoff + i] != 0XFF){break;}}if(i < secremain){W25QXX_Erase_Sector(secpos);for(i = 0; i < secremain; i++){W25QXX_BUF[i + secoff] = pBuffer[i]; }W25QXX_Write_NoCheck(W25QXX_BUF, secpos * 4096, 4096);}else{W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain);}if(NumByteToWrite == secremain){break;}else{secpos++;secoff = 0;pBuffer += secremain;WriteAddr += secremain;NumByteToWrite -= secremain;if(NumByteToWrite > 4096){secremain = 4096;}else{secremain = NumByteToWrite;}} }
}
?