之前, 一直覺得SPI和w25q128都是很複雜的操作.
看過野火的示例代碼, .....譁, c代碼+註釋幾百行, h文件也過百, 涉及函數記不清有多少, 反正很高大上.
原子哥的, 翻查參考數次, 寄存器版本的很精簡, 但新手想理解其中的分扇區和分頁邏輯還有點喫力.
之前在LTDC的屏顯上, 用來存取字庫, 蒙查查地東拼西湊, 反正能正常工作, 沒出問題, 嘻~
這兩天反覆參查, 更深入學習後, 精簡了代碼, 主要是write分頁部分, 使其脈絡更清晰.
函數也只留三個全局函數, 使用時, 修改頭文件就可以使用.
三個主函數: Init, Read, Write
五個基本功能函數: sendByte, writeSector, writeEnable, earseSector, waitReady
/*==================================================================================================================
* 文件名稱 W25Qxx.c
* 功能描述
* 創建信息 L 2019.5.11
*
=================================================================================================================*/
#include "w25qx.h"
#include "led.h"
u16 W25QxxType = 0 ;
void vW25qx_ReadID(void);
// 5個基本功能函數
u8 cW25qx_SendByte(u8 d); // 5_1
void vW25qx_writeEnable(void) ; // 5_2
void vW25qx_waitReady(void) ; // 5_3
void vW25qx_eraseSector(u32 addr); // 5_4
void vW25qx_writeSector(char* p,u32 addr,u16 num); // 5_5
/*****************************************************************************
* @Fun W25Qxx_Init
* @brief 字模存儲設備
* @arg
*
* @return
*
*/
void vW25qx_Init()
{
// NSS W25Qx設備片選線
vSys_SetGPIO (W25Qx_NSS_GPIOx , W25Qx_NSS_PIN , 1,0,2,1,0);
W25QX_NSS_HIGH ; // 片選拉高
// SPI_GPIO
vSys_SetGPIO (W25Qx_CLK_GPIOx , W25Qx_CLK_PIN ,2,0,2,1,W25Qx_SPIx_AFx ); // SPI_CLK
vSys_SetGPIO (W25Qx_MISO_GPIOx ,W25Qx_MISO_PIN ,2,0,2,1,W25Qx_SPIx_AFx ); // SPI_MISO
vSys_SetGPIO (W25Qx_MOSI_GPIOx ,W25Qx_MOSI_PIN, 2,0,2,1,W25Qx_SPIx_AFx ); // SPI_MOSI
// SPI_通信配置
W25Qx_RCC_EN ; // 使能SPI
RCC->APB2RSTR |= 0x1<<20; // 復位SPI5
RCC->APB2RSTR &=~(0x1<<20); // 停止復位SPI5
W25Qx_SPIx -> CR1 = 0x1<<0; // CPHA:時鐘相位,0x1=在第2個時鐘邊沿進行數據採樣
W25Qx_SPIx -> CR1 |= 0x1<<1; // CPOL:時鐘極性,0x1=空閒狀態時,SCK保持高電平
W25Qx_SPIx -> CR1 |= 0x1<<2; // 主從模式: 1 = 主配置
W25Qx_SPIx -> CR1 |= 0x0<<3; // 波特率控制[5:3]: 0 = fPCLK /2
W25Qx_SPIx -> CR1 |= 0x0<<7; // 幀格式: 0 = 先發送MSB
W25Qx_SPIx -> CR1 |= 0x1<<9; // 軟件從器件管理 : 1 = 使能軟件從器件管理(軟件NSS)
W25Qx_SPIx -> CR1 |= 0x1<<8; // 內部從器件選擇,根據9位設置(失能內部NSS)
W25Qx_SPIx -> CR1 |= 0x0<<11; // 數據幀格式, 0 = 8位
W25Qx_SPIx -> CR1 |= 0x1<<6; // SPI使能 1 = 使能外設
print("Flash存儲 配置完成");
vW25qx_ReadID(); // 讀取芯片型號,以判斷通訊是否正常
}
/*****************************************************************************
* @Fun W25Qxx_ReadID
* @brief 讀取芯片型號,用於判斷通訊狀況
* @arg
*
* @return 芯片型號值
*
*/
void vW25qx_ReadID()
{
// 1: 讀取芯片型號, 判斷聯接狀況
W25QX_NSS_LOW;
cW25qx_SendByte(0x90); // 發送讀取ID命令,命令分兩分,第一字節是命令,第四字節是0
cW25qx_SendByte(0x00);
cW25qx_SendByte(0x00);
cW25qx_SendByte(0x00); // 第四字節必節須是 0h
W25QxxType = cW25qx_SendByte(0x00)<<8; // u16 W25QxxType 在本文件定義,全局
W25QxxType |= cW25qx_SendByte(0x00);
W25QX_NSS_HIGH;
switch (W25QxxType)
{
case W25Q16:
sprintf(Data.w25qx_Type ,"%s","W25Q16");
break;
case W25Q32:
sprintf(Data.w25qx_Type ,"%s","W25Q32");
break;
case W25Q64:
sprintf(Data.w25qx_Type ,"%s","W25Q64");
break;
case W25Q128:
sprintf(Data.w25qx_Type ,"%s","W25Q128");
break;
case W25Q256:
sprintf(Data.w25qx_Type ,"%s","W25Q256"); // 注意:W25Q256的地址是4字節
break;
default:
sprintf(Data.w25qx_Type ,"%s","Flash設備失敗 !!!");
Flag.w25qx_Fail =1; // 設備失敗
break;
}
// 2:讀取存儲數據, 增加啓動次數記錄
if(Flag.w25qx_Fail !=1 )
{
u32 numAddr=0x00000000; // 數據地址, 0x0000:標誌0xEE, 0x0001:標誌0X00, 0x0002:數據高位, 0x0003:數據低位
char d[4]; //
u16 f =0; // 標誌
u16 num =0; // 啓動次數
vW25qx_Read( d, numAddr, 4); // 讀取4個字節數據
f = (d[0]<<8) | d[1]; // 標誌
num = (d[2]<<8) | d[3]; // 啓動次數
//printf("f= 0X%X, num= %d\n", f, num);
if(f!=0xEE00) {
num=1;
d[2]=0;
d[3]=1;
}
else{
num++;
d[2]=(u8)(num>>8);
d[3]=(u8)num;
}
d[0]=0xEE;
d[1]=0x00;
vW25qx_Write(d, numAddr ,4);
Data.w25qx_Num =num;
}
if( Flag.w25qx_Fail ==1) vLed_RedOn ();
char c[45]=" ";
sprintf (c," 存儲設備:%s ,第%d次使用!", Data.w25qx_Type, Data.w25qx_Num);
print(c);
}
/******************************************************************************
* @Function W25Qxx_Read 全局 4_3
* @Description 讀取數據
*
* @Input u8 *p 讀出的數值存放位置
* u32 addr 讀取地址
* u16 num 連續讀取的字節數
*
* @Return
*
**/
void vW25qx_Read(char *p,u32 addr, u16 num)
{
W25QX_NSS_LOW ;
cW25qx_SendByte ( 0x03); // 發送讀取命令 03h
cW25qx_SendByte ((u8)(addr>>16));
cW25qx_SendByte ((u8)(addr>>8));
cW25qx_SendByte ((u8)addr);
for(int i=0;i<num;i++)
{
p[i]=cW25qx_SendByte(0);
}
W25QX_NSS_HIGH ;
}
/******************************************************************************
* @Function W25Qxx_Write 全局 4_4
* @Description 讀取數據
*
* @Input u8 *p 要寫入的數據存儲區
* u32 addr 寫入地址 (W25Q128 只用3字節, W25Q256用4字節)
* u16 num 連續寫入的字節數 (
*
* @Return
*
**/
char W25Qx_buffer[4096]; // 開闢一段內存空間
void vW25qx_Write(char* p, u32 addr, u16 num)
{
u32 secPos = addr/4096; // 扇區地址,第幾個扇區
u16 secOff = addr%4096; // 開始地始偏移字節數: 數據在扇區的第幾字節存放
u16 secRemain = 4096-secOff; // 扇區剩餘空間字節數 ,用於判斷夠不夠存放餘下的數據
char *buf = W25Qx_buffer; // 原子哥代碼,爲什麼不直接使用所聲明的數組. (回看前面的疑問, 接觸C有15年, 原來沒下過工夫)
if(num<=secRemain) secRemain=num;
while(1)
{
vW25qx_Read (buf, secPos*4096, 4096); // 讀取扇區內容到緩存
vW25qx_eraseSector(secPos ); // 擦扇區
for(u16 i=0;i<secRemain ;i++) // 原始數據寫入緩存
buf[secOff +i]=p[i];
vW25qx_writeSector(buf, secPos*4096, 4096); // 緩存數據寫入設備
if(secRemain == num) // 已全部寫入
break;
else // 未寫完
{
p=p+secRemain ; // 原始數據指針偏移
secPos ++; // 新扇區
secOff =0; // 新偏移位,扇區內數據起始地址
num=num-secRemain ; // 剩餘未寫字節數
secRemain = (num>4096)?4096:num; // 計算新扇區寫入字節數
}
}
}
// 內部功能函數
// *******************************************************************************************************************************
// 5_1 發送1字節,返回1字節
// SPI通信,只一個動作:向DR寫入從設命令值,同步讀出數據!寫讀組合,按從設時序圖來. 作爲主設,因爲收發同步,連接收發送中斷也不用開,未驗證其它中斷對其工作的影響.
u8 cW25qx_SendByte(u8 d)
{
while((W25Qx_SPIx ->SR&(0x1<<1)) == 0); // 等待發送區爲空
W25Qx_SPIx ->DR =d;
while((W25Qx_SPIx->SR&(0x1<<0)) == 0); // 等待接收完數據
return W25Qx_SPIx->DR ;
}
// 5_2 寫使能
void vW25qx_writeEnable()
{
W25QX_NSS_LOW ;
cW25qx_SendByte (0x6); // 命令: Write Enable : 06h
W25QX_NSS_HIGH ;
}
// 5_3 等待空閒
void vW25qx_waitReady()
{
u8 t=1;
W25QX_NSS_LOW ;
cW25qx_SendByte (0x5); // 命令: Read Status Register : 05h
while((t&0x1)==1)
t=cW25qx_SendByte(0x00); // 只要發送讀狀態寄存器指令,芯片就會持續向主機發送最新的狀態寄存器內容 ,直到收到通信的停止信號。
W25QX_NSS_HIGH ;
}
// 5_4 擦除一個扇區, 每扇區>150ms
void vW25qx_eraseSector(u32 addr)
{
addr=addr*4096; // 從第幾扇區開始
vW25qx_writeEnable();
vW25qx_waitReady();
// 命令
W25QX_NSS_LOW ;
cW25qx_SendByte (0x20); // 命令: Sector Erase(4K) : 20h
cW25qx_SendByte ((u8)(addr>>16));
cW25qx_SendByte ((u8)(addr>>8));
cW25qx_SendByte ((u8)addr);
W25QX_NSS_HIGH ;
vW25qx_waitReady();
}
// 5_5 寫扇區. 要分頁寫入
void vW25qx_writeSector(char *p,u32 addr,u16 num)
{
u16 pageRemain = 256; // W25Qxx每個頁命令最大寫入字節數:256字節;
// 扇區:4096bytes, 緩存頁:256bytes, 寫扇區要分16次頁命令寫入
for(char i=0;i<16;i++)
{
vW25qx_writeEnable (); // 寫使能
vW25qx_waitReady (); // 等待空閒
W25QX_NSS_LOW ; // 低電平,開始
cW25qx_SendByte(0x02); // 命令: page program : 02h , 每個寫頁命令最大緩存256字節
cW25qx_SendByte((u8)(addr>>16)); // 地址
cW25qx_SendByte((u8)(addr>> 8));
cW25qx_SendByte ((u8)addr);
for(u16 i=0;i<pageRemain; i++) // 發送寫入的數據
cW25qx_SendByte( p[i] ); // 高電平, 結束
W25QX_NSS_HIGH ;
vW25qx_waitReady (); // 等待空閒
p = p + pageRemain; // 緩存指針增加一頁字節數
addr = addr + pageRemain ; // 寫地址增加一頁字節數
}
}