esp32|Arduino应用开发——spi flash(以esp32和w25qxx为例)

Arduino应用开发——spi flash
目录

  • Arduino应用开发——spi flash
    • 前言
    • 1 硬件介绍
      • 1.1 模块简介
      • 1.2 硬件连接
    • 2 软件开发
      • 2.1 寄存器介绍
      • 2.2 编程讲解
    • 3 运行测试
    • 4 读写速度测试
    • 结束语

前言 flash是我们在做嵌入式开发时一定会用到的,因为MCU本身就要使用flash来存储代码,flash的好处是掉电不会丢数据,只是一般MCU本身flash的容量都不大,如果我们需要存储大量的数据,就需要外接flash。
flash常用spi接口的,与传感器,电源IC这些芯片不同,不同型号和厂商的flash芯片在通讯协议和内部寄存器这些方面很统一,这对我们开发而言有着很大的好处,这意味着我们的驱动代码可以兼容各种各样的flash,比如W25Q64,W25Q128,GD25Q64等等都是通用的,可以在不改代码的基础上直接替换。
不过让我觉得奇怪的是,Arduino好像并没有单纯使用外置flash的库,大部分都是在存放代码的flash里面分区,做文件系统或者其他用途,实在没找到合适的库,于是我就自己写了一个。
1 硬件介绍 1.1 模块简介
本文基于ESP32-S2测试了W25Q128和GD32Q64两种FLASH。
注:ESP32和ESP32-S2读写flash是完全一样的,只有SPI的接口引脚号有区别。而ESP8266的硬件SPI库则有略微区别,需要稍做修改。
硬件配置如下:
模块 型号 说明
ESP32-S2 ESP32-S2-WROVER 这是乐鑫的一款模组,内部主要是用乐鑫的ESP32-S2再加上一个4M FLASH和2M PSRAM组成,开发板用的是乐鑫的ESP32-S2-SAOLA
W25Q128 W25Q128 W25QXX是很常用的型号, 这里不具体介绍了
GD25Q64 GD25Q64 跟W25Q64是兼容的,可以直接替代
因为篇幅问题,本文主要讲解软件部分开发,关于硬件就不在这里具体介绍了,不懂的同学可以网上自行查阅资料,也可以翻一下我之前发布的博客。
1.2 硬件连接
ESP32-S2 W25Q128\GD25Q64 说明
VCC VCC 电源正,3.3V
GND GND 电源负
GPIO12\SPI_CLK CLK SPI时钟线
GPIO11\SPI_MOSI DI SPI数据线,esp32s2输出,flash输入
GPIO13\SPI_MISO DO SPI数据线,esp32s2输入,flash输出
GPIO10\SPI_CS CS SPI片选
GPIO9 HOLD 保持引脚,可以用GPIO控制也可以直接硬件拉高
当HOLD引脚拉低时,DO将处于高阻抗,DI和CLK针上的信号将被忽略
当HOLD引脚拉高时,设备允许操作
GPIO14 WP 写入保护引脚,低电有效,可以用GPIO控制也可以直接硬件拉高
2 软件开发 2.1 寄存器介绍
【esp32|Arduino应用开发——spi flash(以esp32和w25qxx为例)】以W25QXX为例,列举部分常用的寄存器。
指令名称 数值
制造商设备ID 90h
JEDEC ID 9Fh
写状态寄存器 01h
读状态寄存器1 05h
读状态寄存器2 35h
读数据 03h
写使能 06h
写失能 04h
扇区擦除(4KB) 20h
全片擦除 C7h
页编程 02h
2.2 编程讲解
我们要使用SPI和flash通讯,通过读写flash内部的寄存器达到数据存储和读取的目的,因此,我们第一步是要搞定SPI的驱动代码。
可以使用硬件SPI也可以用软件模拟,硬件SPI跟你选用的MCU有直接关联,比如ESP32和ESP8266,硬件SPI这部分代码一般都是有现成的库,这个在各自的开发板库就有,但需要注意的是不用的MCU用的库不同,因此SPI相关的API可能会有差异。
所以,同样是使用Arduino的SPI,代码却不一定一样,不过一般都是大同小异的,相互之间是可以参考的。我下面会以ESP32-S2为例编写驱动代码。
Arduino官方SPI可以参考:
http://arduino.cc/en/Tutorial/BarometricPressureSensor
http://arduino.cc/en/Tutorial/SPIDigitalPot
FLASH驱动示例代码:
我这里以ESP32-S2为例测试了硬件SPI和软件SPI,可以通过宏定义HARDWARE_SPI和SOFTWARE_SPI切换,另外测试的时候可以打开uart debug的宏,方便在遇到问题时排查,实际使用时建议关闭,因为在读写大量数据的时候串口会频繁输出数据,影响读写速度。
#include // #define NORFLASH_DEBUG_ENABLE// uart debug #define HARDWARE_SPI// use hardware spi // #define SOFTWARE_SPI// use software spi#define NORFLASH_CS_PIN10 #define NORFLASH_CLK_PIN12 #define NORFLASH_MOSI_PIN11 #define NORFLASH_MISO_PIN13 // #define NORFLASH_HOLD_PIN9// hold pin no connect 3.3V // #define NORFLASH_WP_PIN14// 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_CMD0x9F #define WRITE_STATUS0x01 #define READ_STATU_REGISTER_10x05 #define READ_STATU_REGISTER_20x35 #define READ_DATA_CMD0x03 #define WRITE_ENABLE_CMD0x06 #define WRITE_DISABLE_CMD0x04 #define SECTOR_ERASE_CMD0x20 #define CHIP_ERASE_CMD0xC7 #define PAGE_PROGRAM_CMD0x02#define ONE_PAGE_SIZE256 #define SPI_FREQUENCY40 * 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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 %s\n", 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 spaceif(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 = 0write_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 = https://www.it610.com/article/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 intj = 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 运行测试 运行结果如下:
设备启动之后先读取了flash的制造商设备ID信息,这个数值仅供参考,因为这个是跟具体的flash芯片有关的。不过,只要这个数值符合芯片datasheet所描述的值就能说明spi的通讯是正常的,同理,如果读出来的值是00或者FF,不符合datasheet的值,那就是异常的。
读取了设备ID之后在0x10的位置开始写入了256个字节的数据,然后再从0x00的位置开始读取512个字节。
从串口打印的信息可以看出,flash相应的位置都已经写入了对应的数据,没有操作的地址都是FF,与代码一致。
esp32|Arduino应用开发——spi flash(以esp32和w25qxx为例)
文章图片

4 读写速度测试 测试代码示例如下:
我这里是外扩了2M PSRAM的,因此可以直接分配1MB的动态内存用来测试,测试数据量越大应该是越准确的,但是如果是普通的ESP8266或者ESP32,是没有这么多RAM的,要测试的话只能改小一点。
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 s\n", 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 s\n", test_size, SPI_FREQUENCY, (lTime / 1000000.0)); if(buf) { free(buf); }

速度测试结果如下:
我这里只测试了GD25Q64,没有测试W25Q128,读写速度应该是差不多的。
SPI 40MHz读写1MB数据测试:
esp32|Arduino应用开发——spi flash(以esp32和w25qxx为例)
文章图片

SPI 20MHz读写1MB数据测试:
esp32|Arduino应用开发——spi flash(以esp32和w25qxx为例)
文章图片

结束语 好了,关于spi flash的编程就介绍到这里。本文只列举了ESP8266和ESP32-S2的情况,ESP32或者其他MCU基本也是一样的,大家举一反三即可。如果这篇文章对你有帮助,可以点赞收藏,如果还有什么问题,欢迎在评论区留言或者私信给我。

    推荐阅读