加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長(zhǎng)期合作伙伴
立即加入
  • 正文
    • 1 硬件介紹
    • 2 軟件開發(fā)
    • 3 運(yùn)行測(cè)試
    • 4 讀寫速度測(cè)試
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

Arduino應(yīng)用開發(fā)——spi flash(以esp32和w25qxx為例)

10/25 14:22
5007
閱讀需 34 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

flash是我們?cè)谧?a class="article-link" target="_blank" href="/baike/502952.html">嵌入式開發(fā)時(shí)一定會(huì)用到的,因?yàn)?a class="article-link" target="_blank" href="/e/1604036.html">MCU本身就要使用flash來(lái)存儲(chǔ)代碼,flash的好處是掉電不會(huì)丟數(shù)據(jù),只是一般MCU本身flash的容量都不大,如果我們需要存儲(chǔ)大量的數(shù)據(jù),就需要外接flash。

flash常用spi接口的,與傳感器,電源IC這些芯片不同,不同型號(hào)和廠商的flash芯片在通訊協(xié)議和內(nèi)部寄存器這些方面很統(tǒng)一,這對(duì)我們開發(fā)而言有著很大的好處,這意味著我們的驅(qū)動(dòng)代碼可以兼容各種各樣的flash,比如W25Q64,W25Q128,GD25Q64等等都是通用的,可以在不改代碼的基礎(chǔ)上直接替換。

不過(guò)讓我覺(jué)得奇怪的是,Arduino好像并沒(méi)有單純使用外置flash的庫(kù),大部分都是在存放代碼的flash里面分區(qū),做文件系統(tǒng)或者其他用途,實(shí)在沒(méi)找到合適的庫(kù),于是我就自己寫了一個(gè)。

1 硬件介紹

1.1 模塊簡(jiǎn)介

本文基于ESP32-S2測(cè)試了W25Q128和GD32Q64兩種FLASH。

注:ESP32和ESP32-S2讀寫flash是完全一樣的,只有SPI的接口引腳號(hào)有區(qū)別。而ESP8266的硬件SPI庫(kù)則有略微區(qū)別,需要稍做修改。

硬件配置如下:

模塊 型號(hào) 說(shuō)明
ESP32-S2 ESP32-S2-WROVER 這是樂(lè)鑫的一款模組,內(nèi)部主要是用樂(lè)鑫的ESP32-S2再加上一個(gè)4M FLASH和2M PSRAM組成,開發(fā)板用的是樂(lè)鑫的ESP32-S2-SAOLA
W25Q128 W25Q128 W25QXX是很常用的型號(hào), 這里不具體介紹了
GD25Q64 GD25Q64 跟W25Q64是兼容的,可以直接替代

因?yàn)槠鶈?wèn)題,本文主要講解軟件部分開發(fā),關(guān)于硬件就不在這里具體介紹了,不懂的同學(xué)可以網(wǎng)上自行查閱資料,也可以翻一下我之前發(fā)布的博客。

1.2 硬件連接

ESP32-S2 W25Q128GD25Q64 說(shuō)明
VCC VCC 電源正,3.3V
GND GND 電源負(fù)
GPIO12SPI_CLK CLK SPI時(shí)鐘線
GPIO11SPI_MOSI DI SPI數(shù)據(jù)線,esp32s2輸出,flash輸入
GPIO13SPI_MISO DO SPI數(shù)據(jù)線,esp32s2輸入,flash輸出
GPIO10SPI_CS CS SPI片選
GPIO9 HOLD 保持引腳,可以用GPIO控制也可以直接硬件拉高
當(dāng)HOLD引腳拉低時(shí),DO將處于高阻抗,DI和CLK針上的信號(hào)將被忽略
當(dāng)HOLD引腳拉高時(shí),設(shè)備允許操作
GPIO14 WP 寫入保護(hù)引腳,低電有效,可以用GPIO控制也可以直接硬件拉高

2 軟件開發(fā)

2.1 寄存器介紹

以W25QXX為例,列舉部分常用的寄存器。

指令名稱 數(shù)值
制造商設(shè)備ID 90h
JEDEC ID 9Fh
寫狀態(tài)寄存器 01h
讀狀態(tài)寄存器1 05h
讀狀態(tài)寄存器2 35h
讀數(shù)據(jù) 03h
寫使能 06h
寫失能 04h
扇區(qū)擦除(4KB) 20h
全片擦除 C7h
頁(yè)編程 02h

2.2 編程講解

我們要使用SPI和flash通訊,通過(guò)讀寫flash內(nèi)部的寄存器達(dá)到數(shù)據(jù)存儲(chǔ)和讀取的目的,因此,我們第一步是要搞定SPI的驅(qū)動(dòng)代碼。

可以使用硬件SPI也可以用軟件模擬,硬件SPI跟你選用的MCU有直接關(guān)聯(lián),比如ESP32和ESP8266,硬件SPI這部分代碼一般都是有現(xiàn)成的庫(kù),這個(gè)在各自的開發(fā)板庫(kù)就有,但需要注意的是不用的MCU用的庫(kù)不同,因此SPI相關(guān)的API可能會(huì)有差異。

所以,同樣是使用Arduino的SPI,代碼卻不一定一樣,不過(guò)一般都是大同小異的,相互之間是可以參考的。我下面會(huì)以ESP32-S2為例編寫驅(qū)動(dòng)代碼。

Arduino官方SPI可以參考:

http://arduino.cc/en/Tutorial/BarometricPressureSensor
http://arduino.cc/en/Tutorial/SPIDigitalPot

FLASH驅(qū)動(dòng)示例代碼:

我這里以ESP32-S2為例測(cè)試了硬件SPI和軟件SPI,可以通過(guò)宏定義HARDWARE_SPI和SOFTWARE_SPI切換,另外測(cè)試的時(shí)候可以打開uart debug的宏,方便在遇到問(wèn)題時(shí)排查,實(shí)際使用時(shí)建議關(guān)閉,因?yàn)樵谧x寫大量數(shù)據(jù)的時(shí)候串口會(huì)頻繁輸出數(shù)據(jù),影響讀寫速度。

#include <SPI.h>

// #define NORFLASH_DEBUG_ENABLE  // uart debug
#define HARDWARE_SPI           // use hardware spi
// #define SOFTWARE_SPI           // use software spi

#define NORFLASH_CS_PIN         10
#define NORFLASH_CLK_PIN        12        
#define NORFLASH_MOSI_PIN       11       
#define NORFLASH_MISO_PIN       13        
// #define NORFLASH_HOLD_PIN       9   // hold pin no connect 3.3V 
// #define NORFLASH_WP_PIN         14  // hold pin no connect 3.3V  
#define NORFLASH_HOLD_PIN       -1     // hold pin connect 3.3V
#define NORFLASH_WP_PIN         -1     // wp pin connect 3.3V

#define ManufactDeviceID_CMD	0x90   
#define READ_JEDEC_ID_CMD       0x9F
#define WRITE_STATUS            0x01    
#define READ_STATU_REGISTER_1   0x05    
#define READ_STATU_REGISTER_2   0x35
#define READ_DATA_CMD	        0x03
#define WRITE_ENABLE_CMD	    0x06
#define WRITE_DISABLE_CMD	    0x04
#define SECTOR_ERASE_CMD	    0x20
#define CHIP_ERASE_CMD	        0xC7
#define PAGE_PROGRAM_CMD        0x02

#define ONE_PAGE_SIZE           256     
#define SPI_FREQUENCY           40 * 1000000

#define FLASH_TEST_ENABLE

/* Norflash spi init */
void norflash_spi_init()
{
    // gpio init
    pinMode(NORFLASH_HOLD_PIN, OUTPUT); 
    pinMode(NORFLASH_WP_PIN, OUTPUT);  
    digitalWrite(NORFLASH_HOLD_PIN, HIGH); 
    digitalWrite(NORFLASH_WP_PIN, HIGH); 

    pinMode(NORFLASH_CS_PIN, OUTPUT);  
    digitalWrite(NORFLASH_CS_PIN, HIGH);

#ifdef HARDWARE_SPI
    SPI.begin(NORFLASH_CLK_PIN, NORFLASH_MISO_PIN, NORFLASH_MOSI_PIN, NORFLASH_CS_PIN);
    SPI.setDataMode(SPI_MODE0);
    SPI.setBitOrder(MSBFIRST);
    SPI.setFrequency(SPI_FREQUENCY);
#else
    pinMode(NORFLASH_CLK_PIN, OUTPUT);  
    pinMode(NORFLASH_MOSI_PIN, OUTPUT); 
    pinMode(NORFLASH_MISO_PIN, INPUT); 
    digitalWrite(NORFLASH_CLK_PIN, LOW);
    delay(1);
#endif

    // check write enable status
    uint8_t data = 0;
    write_enable();                
    data = read_status();
#ifdef NORFLASH_DEBUG_ENABLE
    Serial.printf("norflash write enable status:");
    Serial.println(data, BIN);
#endif

    // read device id
    uint16_t device_id = 0;
    device_id = read_norflash_id();
#ifdef NORFLASH_DEBUG_ENABLE
    Serial.printf("norflash device id: 0x%04X", device_id);
#endif
}

/* Norflash write one byte */
void write_byte(uint8_t data)
{
#ifdef HARDWARE_SPI
    // SPI.transfer(data);
    SPI.write(data);
#else if SOFTWARE_SPI
    for(uint8_t i = 0; i < 8; i++)
    {     
        uint8_t status;
        status = data & (0x80 >> i);
        digitalWrite(NORFLASH_MOSI_PIN, status);
        digitalWrite(NORFLASH_CLK_PIN, LOW);
        digitalWrite(NORFLASH_CLK_PIN, HIGH);   
    }
#endif
}

/* Norflash read one byte */
uint8_t read_byte(uint8_t tx_data)
{   
#ifdef HARDWARE_SPI
    uint8_t data = 0;
    data = SPI.transfer(tx_data);
    return data;
#else if SOFTWARE_SPI
    uint8_t i = 0, data = 0;
    for(i = 0; i < 8; i++)
    {        
        digitalWrite(NORFLASH_CLK_PIN, HIGH);
        digitalWrite(NORFLASH_CLK_PIN, LOW);
        if(digitalRead(NORFLASH_MISO_PIN)) 
        {
            data |= 0x80 >> i;
        }
    }
    return data;
#endif
}

/* Norflash write enable */
void write_enable()
{
    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(WRITE_ENABLE_CMD);
    digitalWrite(NORFLASH_CS_PIN, HIGH);
}

/* Norflash write disable */
void write_disable()
{
    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(WRITE_DISABLE_CMD);
    digitalWrite(NORFLASH_CS_PIN, HIGH);
}

/* Read norflash status */
uint8_t read_status()
{
    uint8_t status = 0; 
    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(READ_STATU_REGISTER_1);      
    status = read_byte(0);                      
    digitalWrite(NORFLASH_CS_PIN, HIGH);  
    return status;
}

/* check norflash busy status */
void check_busy(char *str)
{
    while(read_status() & 0x01)
    {
    #ifdef NORFLASH_DEBUG_ENABLE
        Serial.printf("status = 0, flash is busy of %sn", str);   
    #endif 
    }
}

/* Write less than oneblock(256 byte) data */
void write_one_block_data(uint32_t addr, uint8_t * pbuf, uint16_t len)
{
    uint16_t i; 
    check_busy("write_one_block_data");
    write_enable();                               
    digitalWrite(NORFLASH_CS_PIN, LOW);           
    write_byte(PAGE_PROGRAM_CMD);                 
    write_byte((uint8_t)(addr >> 16));             
    write_byte((uint8_t)(addr >> 8));   
    write_byte((uint8_t)addr);      
    for(i = 0; i < len; i++)                       
    {
        write_byte(*pbuf++);   
    }
    digitalWrite(NORFLASH_CS_PIN, HIGH);                              
    check_busy("write_one_block_data");
} 

/* Write less than one sector(4096 byte) length data  */
void write_one_sector_data(uint32_t addr, uint8_t * pbuf, uint16_t len)
{ 
    uint16_t free_space, head, page, remain;  
    free_space = ONE_PAGE_SIZE - addr % ONE_PAGE_SIZE;  
    if(len <= free_space) 
    {
        head = len;
        page = 0;                      
        remain = 0; 
    }
    if(len > free_space) 
    {
        head = free_space;
        page = (len - free_space) / ONE_PAGE_SIZE;     
        remain = (len - free_space) % ONE_PAGE_SIZE;
    }
    
    if(head != 0)
    {
    #ifdef NORFLASH_DEBUG_ENABLE
        Serial.print("head:");
        Serial.println(head);
    #endif
        write_one_block_data(addr, pbuf, head);
        pbuf = pbuf + head;
        addr = addr + head;
    }
    if(page != 0) 
    {
    #ifdef NORFLASH_DEBUG_ENABLE
        Serial.print("page:");
        Serial.println(page);
    #endif
        for(uint16_t i = 0; i < page; i++) 
        {
            write_one_block_data(addr, pbuf, ONE_PAGE_SIZE);
            pbuf = pbuf + ONE_PAGE_SIZE;
            addr = addr + ONE_PAGE_SIZE;
        }
    }
    if(remain != 0) 
    {
    #ifdef NORFLASH_DEBUG_ENABLE
        Serial.print("remain:");
        Serial.println(remain);
    #endif
        write_one_block_data(addr, pbuf, remain);
    }  
}

/* Write arbitrary length data */
void write_arbitrary_data(uint32_t addr, uint8_t* pbuf, uint32_t len)   
{ 
	uint32_t secpos;
	uint16_t secoff;
	uint16_t secremain;	   
 	uint16_t i;    
    uint8_t *write_buf = pbuf;	 
    uint8_t save_buffer[4096];  // save sector original data and add new data  
 	secpos = addr / 4096;       // sector number  
	secoff = addr % 4096;       // sector offset
	secremain = 4096 - secoff;  // sector remaining space   
    
 	if(len <= secremain)
    {
        secremain = len;        // sector remaining space less than 4096
    }
	while(1) 
	{	
		read_data(secpos * 4096, save_buffer, 4096); // read sector data
		for(i = 0; i < secremain; i++)
		{// check data, if all data is 0xFF no need erase sector
			if(save_buffer[secoff + i] != 0XFF)
            {// need erase sector
                break;	  
            }
		}
		if(i < secremain)
		{// erase sector and write data
			sector_erase(secpos);
			for(i = 0; i < secremain; i++)	       
			{
				save_buffer[i + secoff] = write_buf[i];	 // add new data 
			}
			write_one_sector_data(secpos * 4096, save_buffer, 4096); // write sector
		}
        else 
        {// no need erase sector
            write_one_sector_data(addr, write_buf, secremain);	
        }			   
		if(len == secremain)
        {// write done
            break; 
        }
		else
		{// continue write
			secpos ++;  // sector number + 1
			secoff = 0; // sector offset = 0 	 

		   	write_buf += secremain;  // write buff offset
			addr += secremain;       // addr offset	   
		   	len -= secremain;		 // remaining data len
			if(len > 4096)
            {// remaining data more than one sector(4096 byte)
                secremain = 4096;	 
            }
			else 
            {// remaining data less than one sector(4096 byte)
                secremain = len;			
            }
		}	 
	}	 
}

/* Read arbitrary length data */
void read_data(uint32_t addr24, uint8_t *pbuf, uint32_t len)
{
    check_busy("read_data");
    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(READ_DATA_CMD);
    write_byte((uint8_t)(addr24 >> 16));   
    write_byte((uint8_t)(addr24 >> 8));   
    write_byte((uint8_t)addr24);  
    for(uint32_t i = 0; i < len; i++)
    {
        *pbuf = read_byte(0xFF);
        pbuf ++;
    }
    digitalWrite(NORFLASH_CS_PIN, HIGH); 
}

/* Erase sector */
void sector_erase(uint32_t addr24)     
{
    addr24 *= 4096;  
    check_busy("sector_erase");
    write_enable();                     
    
    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(SECTOR_ERASE_CMD);          
    write_byte((uint8_t)(addr24 >> 16));    
    write_byte((uint8_t)(addr24 >> 8));   
    write_byte((uint8_t)addr24);  
      
    digitalWrite(NORFLASH_CS_PIN, HIGH);
    check_busy("sector_erase");
} 

/* Read norflash id */
uint16_t read_norflash_id()
{
    uint8_t data = 0;
    uint16_t device_id = 0;

    digitalWrite(NORFLASH_CS_PIN, LOW);
    write_byte(ManufactDeviceID_CMD);
    write_byte(0x00);
    write_byte(0x00);
    write_byte(0x00);
    data = read_byte(0);
    device_id |= data;  // low byte
    data = read_byte(0);
    device_id |= (data << 8);  // high byte
    digitalWrite(NORFLASH_CS_PIN, HIGH);
    
    return device_id;
}

void setup() {
    Serial.begin(115200);

    norflash_spi_init(); 

#ifdef FLASH_TEST_ENABLE
    /* readwrite test */
    int g = 0;
    uint8_t str[1280];
    memset(str, 0, sizeof(str));
    unsigned int  j = 0;       
    for(int k=0; k < 5; k++)
    {
        for(int i = 0; i < 256; i++)
        {
            str[j] = i;
            j++;
        }
    }
    Serial.println("");
    Serial.println("-----write data-------");
    sector_erase(0x00);
    write_one_sector_data(0x10, str, 256);
    memset(str, 0, sizeof(str));
    read_data(0x00, str, 512);
    Serial.println("str:");
    for(int k = 0; k < 512; k++)
    {
        if(g == 16)
        {
            Serial.println("|");
            if(k % 256 == 0) Serial.println("---------------");
            {
                g = 1;
            }
        }   
        else 
        {
            g++;
        }
        Serial.printf("%02X ", str[k]);
    }
#endif
}

void loop() {
}

3 運(yùn)行測(cè)試

運(yùn)行結(jié)果如下:

設(shè)備啟動(dòng)之后先讀取了flash的制造商設(shè)備ID信息,這個(gè)數(shù)值僅供參考,因?yàn)檫@個(gè)是跟具體的flash芯片有關(guān)的。不過(guò),只要這個(gè)數(shù)值符合芯片datasheet所描述的值就能說(shuō)明spi的通訊是正常的,同理,如果讀出來(lái)的值是00或者FF,不符合datasheet的值,那就是異常的。

讀取了設(shè)備ID之后在0x10的位置開始寫入了256個(gè)字節(jié)的數(shù)據(jù),然后再?gòu)?x00的位置開始讀取512個(gè)字節(jié)。

從串口打印的信息可以看出,flash相應(yīng)的位置都已經(jīng)寫入了對(duì)應(yīng)的數(shù)據(jù),沒(méi)有操作的地址都是FF,與代碼一致。

在這里插入圖片描述

4 讀寫速度測(cè)試

測(cè)試代碼示例如下:

我這里是外擴(kuò)了2M PSRAM的,因此可以直接分配1MB的動(dòng)態(tài)內(nèi)存用來(lái)測(cè)試,測(cè)試數(shù)據(jù)量越大應(yīng)該是越準(zhǔn)確的,但是如果是普通的ESP8266或者ESP32,是沒(méi)有這么多RAM的,要測(cè)試的話只能改小一點(diǎn)。

    uint32_t test_size = 1024 * 1024;
    long lTime;
    uint8_t *buf = (uint8_t*)ps_malloc(test_size);
    memset(buf, 0, test_size);
    randomSeed(analogRead(0));  // randomize using noise from analog pin 0
    for(uint32_t i = 0; i < test_size; i++)
    {
        buf[i] = random(0, 255);  // get a random number from 0 to 255
    }
    Serial.printf("norflash speed test start n");

    lTime = micros();
    write_arbitrary_data(0x00, buf, test_size);
    lTime = micros() - lTime;
    Serial.printf("write %d byte data end, spi frequency: %ld, time: %f sn", test_size, SPI_FREQUENCY, (lTime / 1000000.0));

    memset(buf, 0, test_size);
    lTime = micros();
    read_data(0x00, buf, test_size);
    lTime = micros() - lTime;
    Serial.printf("read %d byte data end, spi frequency: %ld, time: %f sn", test_size, SPI_FREQUENCY, (lTime / 1000000.0));
    if(buf)
    {
        free(buf);
    }

速度測(cè)試結(jié)果如下:

我這里只測(cè)試了GD25Q64,沒(méi)有測(cè)試W25Q128,讀寫速度應(yīng)該是差不多的。

SPI 40MHz讀寫1MB數(shù)據(jù)測(cè)試:
在這里插入圖片描述
SPI 20MHz讀寫1MB數(shù)據(jù)測(cè)試:
在這里插入圖片描述

結(jié)束語(yǔ)

好了,關(guān)于spi flash的編程就介紹到這里。本文只列舉了ESP8266和ESP32-S2的情況,ESP32或者其他MCU基本也是一樣的,大家舉一反三即可。如果這篇文章對(duì)你有幫助,可以點(diǎn)贊收藏,如果還有什么問(wèn)題,歡迎在評(píng)論區(qū)留言或者私信給我。

想了解更多Arduino的內(nèi)容,可以關(guān)注一下博主,后續(xù)我還會(huì)繼續(xù)分享更多的經(jīng)驗(yàn)給大家。

相關(guān)推薦

電子產(chǎn)業(yè)圖譜