一、原理圖分析
由原理圖可知w25Q128 CS片選引腳為PB14、MISO是PB4、MOSI是PB5.
二、程序編寫
1、spi初始化以及讀寫函數
#include "spi.h"
void Spi_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
//使能端口 B 的硬件時鍾
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
//使能SPI的硬件時鍾
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
//PB3-PB5引腳連接到SPI1的硬件
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;//PB3 PB4 PB5
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;//復用模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽輸出
GPIO_InitStruct.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1);
//配置PB14為輸出模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;//PB14
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;//輸出模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽輸出
GPIO_InitStruct.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOB,&GPIO_InitStruct);
//PB14初始電平狀態?
SPI_CS = 1;//片選引腳 低電平有效選擇,高電平無效選擇
//配置SPI相關參數
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//雙線全雙工通信
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;//默認是主機角色,主動控制從機
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;//默認是8位數據傳輸,主要根據從機的設備進行配置 【看從機的數據手冊的時序圖】
//模式3
SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;//SPI總線空閑的時候,時鍾線為高電平 CPOL=1,【看從機的數據手冊的時序圖】
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;//CPHA = 1,,就是主機會對MOSI引腳進行電平采樣在時鍾的第二個條邊沿【看從機的數據手冊的時序圖】
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;//片選引腳有軟件代碼控制【看從機的數據手冊的時序圖】
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;//SPI的硬件時鍾=84MHz/4=21MHz {看從機的數據手冊的芯片描述,一般在開頭介紹}
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;//高位先出【看從機的數據手冊的時序圖】
//SPI_InitStruct.SPI_CRCPolynomial = 7;//主要是用在兩個M4芯片進行通信,最后添加CRC檢驗碼
SPI_Init(SPI1, &SPI_InitStruct);
//使能SPI1硬件
SPI_Cmd(SPI1, ENABLE);
}
/*
* 功能:SPI 讀寫一個字節函數 ---》數據交換
* 參數:發送一個字節數據
* 返回值:返回讀取的數據
*/
uint16_t spi_read_writeByte(uint8_t TXdata)
{
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); //等待上一次的數據發完
SPI_I2S_SendData(SPI1,TXdata);//發送數據
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); //等上次數據接收完
return SPI_I2S_ReceiveData(SPI1);//接收數據
}
2、讀寫廠商ID和設備ID(09H)--模式3
由上圖可知廠商ID是0xEF,設備ID是0x17.
該指令與Release from Power-Down/Device ID指令相似。該指令以/CS拉低開始,然后通過DI傳輸指令代碼90H和24位的地址(全為00000H)。這之后WINBOND的ID(EFH)和芯片ID將在時鍾的下降沿以高位在前的方式傳出。關於W25Q128BV的芯片和制造商ID,在圖29中列出。如果24位地址傳輸的是00001H,那么芯片ID將首先被傳出,然后緊接着的是制造商ID。這兩個是連續讀出來的。該指令以/CS拉高結束。
- CS拉低表示開始進行數據傳輸。
- 第一個字節發送指令0x90,代表開始讀取ID.
- 第二個字節、第三個字節為dummy(任意值)、第四個字節為0x00
- 第五、六個字節隨便發兩個字節數據,分別返回制造商ID和設備ID.
- CS拉高表示結束。
代碼:
uint16_t w25qxx_read_id(void)
{
uint16_t id = 0;
//片選有效
SPI_CS = 0;
//發送0x90,讀取廠商ID和設備ID
spi_read_writeByte(0x90);
//發送24位地址(3個字節) 前面兩個字節可以任意,第三個字節必須是0x00
spi_read_writeByte(0x00);
spi_read_writeByte(0x00);
spi_read_writeByte(0x00);//一定是0x00
//隨便發2個字節的數據
id |= spi_read_writeByte(0xFF)<<8; //id:0xEF17 廠商ID:0xEF
id |= spi_read_writeByte(0xFF); //設備ID:0x17
//片選無效
SPI_CS = 1;
return id;
}
3、 讀數據(03H) ---模式3
讀數據指令允許從存儲器讀一一個字 節和連續多個字節。該指令是以/CS拉低開始,然后通DI在時鍾的上升沿來傳輸指令代碼(03H)和24位地址。當芯片接受完地址位后,相應地址處的值將會,在時鍾的下降沿,以高位在前低位在后的方式,在DO.上傳輸。如果連續的讀多個字節的話,地址是自動加1的。這意味着可以一次讀出整個芯片。該指令也是以/CS拉高來結束的。如果當BUSY=1時執行該指令,該指令將被忽略,並且對正在執行的其他指令不會有任何影響。讀數據指令的時鍾可以從D.C到最大的fR.
讀數據流程:
- CS拉低開始
- 第一個字節發送指令0x03,代表開始讀取數據。
- 發送一個24bit要讀取的地址(三個字節)。
- 數據讀取。
- CS拉高結束。
代碼編寫:
/*
* 功能:w25q128 讀取一個字節函數 ---》數據交換
* 參數:addr ----->打算從 addr 這個地址開始讀取數據
pbuf ----->你要讀取的數據所在的緩沖區
lenth ----->你要讀取的字節數
* 返回值:返回讀取的數據
*/
void w25qxx_read_data(uint32_t addr,uint8_t *pbuf,uint32_t lenth)
{
uint8_t *p = pbuf;
//片選有效
SPI_CS = 0;
//發送0x03,讀取讀取數據
spi_read_writeByte(Read_Data);
//接下來發一個你要讀取的24位地址 0xyy123456
spi_read_writeByte((addr>>16)&0xFF);//0x12 發送[23:16]
spi_read_writeByte((addr>>8)&0xFF);//0x34 發送[15:8]
spi_read_writeByte((addr>>0)&0xFF);//0x56 發送[7:0]
while(lenth--)
{
*p++ = spi_read_writeByte(0xFF);//隨意加的,你可以改成其它試試
}
//片選無效
SPI_CS = 1;
}
4、擦除扇區(20H)
扇區擦除可以擦除4K-byte存儲空間(全為0XFF)。進行扇區擦寫指令之前,必須進行寫使能指令。該指令是以/CS拉低開始的,然后在DI.上傳輸指令代碼20H和24位地址。時序圖如圖21。當最后字節的第8位進入芯片后,/CS必須拉高。如果/CS沒有拉高,那么扇區擦寫指令將不被執行。/CS拉高后,扇區擦寫指令的內建時間為tSE。在扇區擦寫指令執行期間,讀狀態寄存器指令仍然可以識別,以此來進行檢查BUSY位。當扇區擦寫指令執行期間,BUSY 位為了1。當執行完后,BUSY 為0,表明可以接受新的指令了。扇區擦寫指令完成后WEL位自動清零。如果該指令要操作的任何--頁已經被保護起來,那么該指令也將不執行。
扇區擦除流程:
- CS拉低開始。
- 發送指令0x20,代表擦除扇區開始。
- 發送一個要擦除的24bit地址
- CS拉高結束。
/*
* 功能:w25q128 寫入一頁(256Byte)函數 ---》數據交換
* 參數:addr ----->打算從 addr 這個地址開始擦除
* 返回值:無
*/
void w25qxx_EraseSector(uint32_t addr)
{
//片選有效
SPI_CS = 0;
//發送0x20,扇區擦除
spi_read_writeByte(Sector_Erase);
//接下來發一個你要讀取的24位地址 0xyy123456
spi_read_writeByte((addr>>16)&0xFF);//0x12 發送[23:16]
spi_read_writeByte((addr>>8)&0xFF);//0x34 發送[15:8]
spi_read_writeByte((addr>>0)&0xFF);//0x56 發送[7:0]
//片選無效
SPI_CS = 1;
}
5、讀狀態寄存器1指令(05H)和讀狀態寄存器2指令(35H)
讀狀態寄存器指令允許讀8位狀態寄存器位。這條指令是以/CS拉低開始,然后通過DI在時鍾的上升沿傳輸指令代碼05H(讀寄存器1指令)或者是35H(讀寄存器2指令),然后狀態寄存器的相應位通過DO在時鍾的下降沿從高位到低位依次傳出。最后以/CS拉高結束。讀狀態寄存指令可以任何時間使用,在擦寫,寫狀態寄存器指令周期中依然可以。這樣就可以隨時檢查BUSY位,檢查相應的指令周期有沒有結束,芯片是不是可以接受新的指令。狀態寄存器可以連續的讀出來,如圖7。.
讀狀態寄存器流程:
- CS拉低。
- 發送指令0x05,表示讀取狀態寄存器1.
- 接收數據。
- CS拉高。
代碼編寫:
*
* 功能:w25q128 讀取狀態寄存器1
* 參數:無
* 返回值:狀態寄存的值
*/
uint8_t w25qxx_read_SR1(void)
{
uint16_t status = 0;
//片選有效
SPI_CS = 0;
//發送0x05,讀取狀態寄存器1的值 發送0x35,讀取狀態寄存器2的值
spi_read_writeByte(Read_SR1);
//接收數據 就是狀態寄存器1+狀態寄存器2
status = spi_read_writeByte(0xFF);
//片選無效
SPI_CS = 1;
return status;
}
6、寫使能指令(06H)
寫使能指可以設置狀態寄存器中的WEL位置1。在頁寫,QUAD頁寫,扇區擦除,塊擦除,片擦除,寫狀態寄存器,擦寫安全寄存器指令之前,必須先將WEL位置1。寫使能指令是以/CS拉低開始的,將06H通過DI在時鍾的上升沿鎖存,然后/CS拉高來結束指令。
寫使能流程:
- CS拉低
- 發送寫使能指令0x06.
- CS拉高。
代碼編寫:
void w25qxx_wirte_enable(void)
{
//片選有效
SPI_CS = 0;
//發送0x06,寫使能
spi_read_writeByte(Write_Enable);
//片選無效
SPI_CS = 1;
}
7、判斷擦除是否完成
判斷狀態寄存器1的S0為是否為0,值為0則擦除完成。
void w25qxx_wait_busy(void)
{
while((w25qxx_read_SR1()&(0x01<<0)));//當busy為0,即擦除完畢 當busy為1,即擦除還在繼續
}
8、頁寫指令(02H)
頁編程指令允許1到256字節寫入存儲器的某- -頁,這一頁必須是被擦除過的(也就是只能寫.0,不能寫1,擦除時是全寫為1)。在頁編程指令之前,必須先寫入寫使能指令。頁編程指令是以/CS拉低開始,然后在DI上傳輸指令代碼02H,再接着傳輸24位的地址,接着是至少-一個字節的數據。/CS管腳必須一直保持低。頁編程指令的時序圖如圖19。如果一-次寫-整頁數據(256 字節),最后的地址字節應該全為0。如果最后8字節地址不為0,但是要寫入的數據長度超過頁剩下的長度,那么芯片會回到當前頁的開始地址寫。寫入少於256字節的的數據,對頁內的其他數據沒有任何影響。對於這種情況的惟一要求是,時鍾數不能超過剩下頁的長度。如果一-次寫入多於是256字節的數據,那么在頁內會回頭寫,先前寫的數據可能已經被覆蓋。作為擦寫指令,當最后字節的第8位進入芯片后,/CS必須拉高。如果/CS沒有拉高, .那么頁寫指令將不被執行。/CS拉高后,頁編程指令的內建時間為tpp。在頁寫指令執行期間,讀狀態寄存器指令仍然可以識別,以此來進行檢查BUSY位。當頁寫指令執行期間,BUSY 位為了1。當執行完后,BUSY 為0,表明可以接受新的指令了。頁寫指令完成后WEL位自動清零。如果該指令要操作的頁已經被保護起來,那么該指令也將不執行。
頁寫流程:
- 寫使能。
- 擦除扇區(擦除也是個寫操作,寫0)
- 判斷扇區是否擦除完畢。
- 擦除完畢后寫使能。
- CS拉低,片選有效。
- 發送頁寫指令0x02,代表頁寫開始
- 發送一個要寫入的24bit地址。
- 開始寫數據,寫入一頁數據(354byte)
- CS拉高,片選無效。
/*
* 功能:w25q128 寫入一頁(256Byte)函數 ---》數據交換
* 參數:addr ----->打算從 addr 這個地址開始寫入數據
pbuf ----->你要寫入的數據所在的緩沖區
lenth ----->你要寫入的字節數
* 返回值:返回讀取的數據
*/
void w25qxx_write_page(uint32_t addr,uint8_t *pbuf,uint32_t lenth)
{
uint8_t *p = pbuf;
//擦除之前必須進行寫使能
w25qxx_wirte_enable();
//擦除扇區
//w25qxx_EraseSector(0x000000);
w25qxx_EraseSector(addr/4096*4096);
//判忙
w25qxx_wait_busy();
//寫入必須進行寫使能
w25qxx_wirte_enable();
//開始寫入數據
//片選有效
SPI_CS = 0;
//發送0x02,寫入數據
spi_read_writeByte(Page_Program);
//接下來發一個你要寫入的24位地址 0xyy123456
spi_read_writeByte((addr>>16)&0xFF);//0x12 發送[23:16]
spi_read_writeByte((addr>>8)&0xFF);//0x34 發送[15:8]
spi_read_writeByte((addr>>0)&0xFF);//0x56 發送[7:0]
while(lenth--)
{
spi_read_writeByte(*p++);
}
//片選無效
SPI_CS = 1;
}
三、主函數測試
uint8_t i;
uint8_t wbuf[8]={'h','e','l','l','o','b','b','a'};
uint8_t rbuf[8]={0};
uint16_t id;
id = w25qxx_read_id();
printf("id=0x%X\r\n",id);
w25qxx_write_page(10086,wbuf,8);
delay_ms(50);
w25qxx_read_data(10086,rbuf,8);
printf("addr10086 read 8bit data:");
for(i=0;i<8;i++)
{
printf("%c ",rbuf[i]);
}
printf("\r\n");