簡介:
本文主要講解使用2440裸機的GPIO模擬SPI來控制flash進行數據的存儲和讀取。
所用開發板:JZ2440 V3
FLASH:W25Q16DV
聲明:
本文主要還是學習韋東山老師視頻后所做的學習總結。同時由於我在上面一篇文章中已經介紹了:2440裸機GPIO模擬SPI驅動OLED,而本文同樣使用GPIO模擬SPI,所以可能與上篇文章有相似的部分。但為了讓沒有看過上篇文章的人看懂本篇文章。所以我還是會用上篇文章中的知識。這樣可能會顯得有些啰嗦,請大家原諒,謝謝。
flash介紹:
介紹知識點之前我還是想說,既然這篇文章中同樣是使用GPIO模擬SPI,那么這篇文章的重點就落在了看懂flash的芯片手冊上,因為我們不用去關心開發板上SPI控制器是如何實現的,我們只要用GPIO去模擬SPI就可以了。因此我們首先先了解我們所使用的flash芯片。(由於芯片的功能很多,而我們要做的是學習SPI,所以我們只是實現了他們最簡單的功能,因此我只是將我用到的內容翻譯了下來,這里我簡稱該芯片為25Q)。
25Q(16M-bit)由8192個可編程頁組成,每個可編程頁為256字節。因此一次可以編寫256字節。同時16頁(4KB),128頁(32KB)或者256頁(64KB)可以分為一組進行擦除。25Q支持標准的SPI接口,同時其時鍾頻率可以高達104MHz。
而25Q的引腳圖為:
而25Q對應的引腳描述為:
引腳號 |
引腳名 |
I/O |
功能 |
1 |
/CS |
I |
片選輸入 |
2 |
DO(IO1) |
I/O |
數據輸出 |
3 |
/WP(IO2) |
I/O |
寫保護輸入 |
4 |
GND |
|
地 |
5 |
DI(IO0) |
I/O |
數據輸入 |
6 |
CLK |
I |
時鍾輸入 |
7 |
/HOLD(IO3) |
I/O |
HOLD輸入 |
8 |
VCC |
|
電源正 |
寫保護引腳:低電平有效,硬件防止狀態寄存器被更改。
HOLD引腳:當該引腳被選中,設備被暫停。當多設備共享SPI信號時,該引腳是有用的。
而我們看老師模塊上flash的電路圖:
從上圖可以看出寫保護引腳和HOLD引腳都接到電源正極。所以我們的模塊並沒有使用到這兩個引腳的功能。
下面我們繼續介紹對flash的操作,我們知道要寫flash就要先擦除flash原有的扇區,而擦除扇區之前是要去除對於扇區的保護的。而具體的去保護操作為:
1. 寫使能
2. 寫狀態寄存器
我們先介紹寫使能,flash上電自動進入寫失能狀態,即狀態寄存器寫使能鎖存(WEL)位為0。而在進行頁編程,扇區擦除,塊擦除或者寫狀態寄存器指令前要發送寫使能指令。而當完成編程,擦除或者寫指令操作后寫使能鎖存自動清零,即進入寫失能狀態。
我們上面說了去保護要操作狀態寄存器,我們現在看看狀態寄存器中都有那些位:
上面就是兩個狀態寄存器了,而是否可以對其進行操作要設置寄存器中的兩位:狀態寄存器保護位SRP1,SRP0。而具體的設置看下圖:
而具體的關於狀態寄存器的操作或者說關於flash的操作就要通過指令進行操作了。
代碼分析:
上面的介紹我們對SPI_FLASH的操作步驟有了基本的了解,我們下面通過分析代碼來看具體如何對這個SPI_FLASH進行操作。
首先我們還是先來對2440中的GPIO進行初始化:
static void spi_gpio_init(void) { /* GPG2 FLASH_CS0 output * GPG4 OLED_D/C output * GPG5 SPI_MISO input * GPG6 SPI_MOSI output * GPG7 SPI_CLK output */ GPGCON &= ~((0x3<<(2*2)) | (0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2))); GPGCON |= ((0x1<<(2*2)) | (0x1<<(4*2)) | (0x1<<(6*2)) | (0x1<<(7*2))); GPGDAT |= (0x1<<2); }初始化完端口我們就可以按着25Q的芯片手冊來對flash進行操作了,我們從上面的知識知道,flash在擦除或者寫寄存器之前都要對flash進行寫使能操作。下面我們結合25Q的數據手冊先完成寫使能。
寫使能(06h):
寫使能指令設置狀態寄存器中的寫使能鎖存位(WEL)為1 。在編程,擦除或者寫寄存器之前都要寫使能。當片選/CS為低電平時將指令碼06在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,數據傳輸完成然后再拉高/CS,這時就將寫使能指令寫入芯片。
寫使能對應的時序圖為:
而相對於寫使能的就是寫失能了,我們看寫失能做了什么。
寫失能(04h):
寫失能指令重設狀態寄存器中的寫使能鎖存位(WEL)為0。當片選/CS為低電平時將指令碼04在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,數據傳輸完成然后再拉高/CS,這時就將寫失能指令寫入芯片。注意當上電或完成編程擦除以及寫寄存器時,WEL位會自動清零。
由於上面兩個指令除了指令碼不一樣之外其他的設置都一樣,所以我們將這兩個指令放到了一個函數中。而這個函數的實現為:
static void spi_flash_write_enable(int enable) { if(enable){ spi_set_CS(0); flash_write_cmd(0x06); spi_set_CS(1); }else{ spi_set_CS(0); flash_write_cmd(0x04); spi_set_CS(1); } }
又由於在flash中寫指令的格式都是一樣的,即在時鍾上升沿以最高有效位方式傳輸8位命令或者數據,所以我們將他提出用一個函數表示,所以寫命令函數為:
static void flash_write_cmd(unsigned char cmd) { spi_send_byte(cmd); }
同時由於我們在上一篇文章中已經寫了一個類似的函數:spi_send_byte,所以在這里調用他,而spi_send_byte函數為:
void spi_send_byte(unsigned char val) { int i; for(i=0;i<8;i++){ spi_set_clk(0); spi_set_DO(val & 0x80); spi_set_clk(1); val <<= 1; } }
這個函數與時序圖的對照就更貼近一些。
我們在前面介紹了,要去保護,第一是寫使能,而第二件事就是寫狀態寄存器。因此下面我們介紹操作狀態寄存器,而操作狀態寄存器之前我們先要將狀態寄存器中的值讀出來,然后再按位操作設置我們要設置的位,最后將這設置后的寄存器值寫入狀態寄存器。這樣才能算是對狀態寄存器的寫操作。我們先來看一下25Q中讀狀態寄存器的操作。
讀狀態寄存器1(05h)和讀狀態寄存器2(35h):
通過讀狀態寄存器指令可以讀取8位的狀態寄存器的值。當片選/CS為低電平時將讀狀態寄存器1的指令碼05或者將讀狀態寄存器2的指令碼35在時鍾上升沿時以最高有效位方式傳入數據輸入引腳。然后狀態寄存器的值將同樣按照最高有效位的方式在數據輸出引腳等待在每一個時鍾下降沿的時候被讀取。
讀狀態寄存器的指令可以在任何時候都可以用到,甚至是在編程,擦除或者寫寄存器的過程中。它允許BUSY態可以被檢查到來顯示是否完成上面的操作,並顯示設備是否可以接收新的指令。狀態寄存器可以連續的讀取,直到當/CS設置為高時停止。
而我們對應這個時序圖將函數寫出,所以讀寄存器操作為:
static unsigned char spi_flash_read_status_Reg1(void) { unsigned char val = 0; spi_set_CS(0); flash_write_cmd(0x05); val = flash_read_byte(); spi_set_CS(1); return val; } static unsigned char spi_flash_read_status_Reg2(void) { unsigned char val = 0; spi_set_CS(0); flash_write_cmd(0x35); val = flash_read_byte(); spi_set_CS(1); return val; }
同時又由於在flash中讀數據和讀寄存器的操作是相同的,即在時鍾上升沿以最高有效位方式讀取8位數據(這里需要說明的是:上面在芯片手冊上說的是在時鍾的下降沿讀取數據,但是我們看時序圖中紅框,發現當指令傳輸完成時,在數據輸出引腳上就有了數據(這里的數據就是狀態寄存器的值),而在時鍾的上升沿時正好可以讀取數據,因此我們將程序改為了上升沿,並實驗可以使用)。因此我們看數據接收函數:
static unsigned char flash_read_byte(void) { int i; unsigned char val = 0; for(i=0;i<8;i++){ val <<= 1; spi_set_clk(0); val |= spi_get_DI(); spi_set_clk(1); } return val; }上面函數需要說明第一點是 val <<= 1;的位置。我剛開始的時候將個語句放到了for語句的末尾,結果發現我的實驗結果是錯的,而當我將這個語句放到for語句的開頭時,發現結果正確了。然后仔細分析程序我才知道, 左移語句只要執行7次就可以了。而當放到for語句末尾時就會執行8次。得出的結果是正確答案的兩倍。
好了,我們下一步就要對狀態寄存器進行操作來去保護了。我們先去狀態寄存器的保護,通過芯片手冊知道,要去狀態寄存器的保護需要操作狀態寄存器的SRP1,SRP0位,使這兩位都為清零。如下圖:
而具體的去狀態寄存器保護的 程序為:
static void spi_flash_clear_protect_for_status_Reg(void) { unsigned char reg1,reg2; reg1 = spi_flash_read_status_Reg1(); reg2 = spi_flash_read_status_Reg2(); reg1 &= ~(1<<7); /* 清零SRP0 */ reg2 &= ~(1<<0); /* 清零SRP1 */ spi_flash_write_status_Reg(reg1,reg2); spi_flash_wait_when_busy(); }
而上面函數中寫狀態寄存器函數spi_flash_write_status_Reg(reg1,reg2)的原函數為:
static void spi_flash_write_status_Reg(unsigned char reg1,unsigned char reg2) { spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x01); flash_write_cmd(reg1); flash_write_cmd(reg2); spi_set_CS(1); spi_flash_wait_when_busy(); }
而關於寫狀態寄存器,25Q芯片手冊這樣描述:
寫狀態寄存器(01h):
寫狀態寄存器允許向狀態寄存器中寫入值。但只有非易失的狀態寄存器位可以被寫入值,如:SRP0,SEC,TB,BP2,BP1,BP0(狀態寄存器1的bit7~2)以及CMP,LB3, LB2, LB1,QE,SRP1(狀態寄存器2的bit14~8)。而剩余的位是不可被寫入的。LB[3:1]是非易失的一次編程,一旦他們被設為1,他們將不會被清零。
而在寫狀態寄存器之前,寫使能(06h)指令要先執行來使設備接收寫寫狀態寄存器指令。一旦寫使能,當片選/CS為低電平時將指令碼01在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,然后將寄存器的值寫入,最后拉高/CS,這時就將寫狀態寄存器指令寫入芯片。
為了完成寫狀態寄存器指令,在寫完8位或16位指令后一定要拉高/CS。如果沒有拉高,那么這個指令將不被執行。如果/CS在第8個時鍾后被拉高,CMP位和QE位將自動清零。
而他對應的時序圖為:
狀態寄存器去保護完成后我們就要去內存的保護,而在本程序中,我們要去除的是整個芯片的保護,對應的芯片手冊的設置為:
我們的設置如上圖紅色方框中所示,設置去除整個內存的保護,也就是設置CMP為0,BP2~0 都為0。而對應的函數為:
static void spi_flash_clear_protect_for_data(void) { unsigned char reg1,reg2; reg1 = spi_flash_read_status_Reg1(); reg2 = spi_flash_read_status_Reg2(); reg1 &= ~(7<<2); /* BP2~0都為0 */ reg2 &= ~(1<<6); /* cmp為0 */ spi_flash_write_status_Reg(reg1,reg2); spi_flash_wait_when_busy(); }
在上面幾個函數中都調用了一個函數spi_flash_wait_when_busy,我們現在介紹這個函數,我們看他的函數:
static void spi_flash_wait_when_busy(void) { while(spi_flash_read_status_Reg1() & 1); }
從上面程序可以看出,其實這個函數就是讀取狀態寄存器1中的第0位BUSY位,來確定flash是否處在忙的狀態。這里我們又要介紹一下BUSY狀態了。我們看芯片手冊:
BUSY狀態:
BUSY位在狀態寄存器中是只讀位,當設備執行編程,擦除,寫寄存器操作時,該位被設置為1 。在這時,設備將忽略除讀狀態寄存器和擦除/編程懸掛指令外所有的指令。而當編程,擦除,寫寄存器完成后該位將清零,表示該設備准備就緒,可以操作后面的指令。
而我們上面的程序就是不斷的讀取BUSY位是否為1,如果為1則一直在這里等待。我們去完保護現在就可以對flash進行擦除了,而芯片手冊中對flash的擦除有好幾種,我們這里選擇擦除面積最小的扇區擦除進行介紹,我們先看芯片手冊的描述。
扇區擦除(20h):
扇區擦除指令用於擦除指定扇區中的所有內存。而在扇區擦除之前,寫使能(06h)指令要先執行來使設備接收扇區擦除指令。一旦寫使能,當片選/CS為低電平時將指令碼20在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,然后再以最高有效位方式傳入24位的地址(A23-A0)。完成地址的傳輸后,拉高/CS,這時就完成了扇區擦除。
當最后一個字節的第8位傳輸完成時,/CS一定要拉高。如果沒有拉高,該指令將不會被執行。如果要擦除的地址被塊保護位保護,那么該擦除指令將不會完成。
而他對應的程序為:
/* erase 4K */ void spi_flash_erase_sector(unsigned int addr) { spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x20); flash_write_addr(addr); spi_set_CS(1); spi_flash_wait_when_busy(); }
從上面時序圖上看,我們傳輸完擦除指令后接着就要傳輸24位地址值,所以我們將他們封裝了一個函數:
static void flash_write_addr(unsigned int addr) { spi_send_byte((addr>>16)&0xff); spi_send_byte((addr>>8)&0xff); spi_send_byte(addr&0xff); }
從上面看我們先傳輸的是最高有效位。
擦除完我們就可以在剛擦除的區域進行寫操作了,具體的操作我們看一下芯片手冊。
頁編程(02h):
頁編程指令允許1~256個字節的數據寫入到先前擦除的內存地址中。而在頁編程之前,寫使能(06h)指令要先執行來使設備接收頁編程指令。一旦寫使能,當片選/CS為低電平時將指令碼02在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,然后再以最高有效位方式傳入24位的地址(A23-A0)。完成地址的傳輸后,再將要寫入內存的數據傳入數據輸入引腳,最后拉高/CS,這時就完成了頁編程。
如果要完成256字節的頁編程,最后的地址字節(最低有效地址位)應該設置為0。如果該位不是0,時鍾數將會超出剩余的頁長度,從而導致地址將回到頁開始位置。
像寫和擦除指令一樣,當最后一個字節的第8位傳輸完成時,/CS一定要拉高。如果沒有拉高,該指令將不會被執行。如果要寫入的地址被塊保護位保護,那么該頁編程指令將不會完成。
而與時序圖對應的函數為:
void spi_flash_program(unsigned int addr,unsigned char *buf,int len) { int i; spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x02); flash_write_addr(addr); for(i=0;i<len;i++){ flash_write_cmd(buf[i]); } spi_set_CS(1); spi_flash_wait_when_busy(); }
我們向內存中寫完東西都是要讀的,因此我們要完成讀函數。
讀數據(03h):
讀數據指令允許從內存中讀取一個或者更多的字節數據。當片選/CS為低電平時將指令碼03在時鍾上升沿時以最高有效位方式傳入數據輸入引腳,然后再以最高有效位方式傳入24位的地址(A23-A0)。完成地址的傳輸后,指定地址中的值將以最高有效位方式移到數據輸出端口,然后在每個時鍾的下降沿將數據讀出。而在完成一字節數據傳輸后,指定地址的值將自動增加來傳輸下一字節的數據,直到傳輸完。這就意味着只要時鍾不停整個內存的數據可以在一次指令中全部讀完。而當/CS拉高時,該指令結束。
而相應的程序為:
void spi_flash_read(unsigned int addr,unsigned char *buf,int len) { int i; spi_set_CS(0); flash_write_cmd(0x03); flash_write_addr(addr); for(i=0;i<len;i++){ buf[i] = flash_read_byte(); } spi_set_CS(1); }講到這里我們就講完了這個SPI_FLASH的操作過程。
我已將代碼傳到:2440裸板GPIO模擬SPI控制FLASH,如果需要全部的文檔可以到這里下載。
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。