| #ifndef SerialFlash_h_ | |||||
| #define SerialFlash_h_ | |||||
| #include <Arduino.h> | |||||
| #include <SPI.h> | |||||
| class SerialFlashFile; | |||||
| class SerialFlashChip | |||||
| { | |||||
| public: | |||||
| static bool begin(); | |||||
| static uint32_t capacity(); | |||||
| static uint32_t blockSize(); | |||||
| static void read(void *buf, uint32_t addr, uint32_t len); | |||||
| static bool ready(); | |||||
| static void wait(); | |||||
| static void write(const void *buf, uint32_t addr, uint32_t len); | |||||
| static void eraseAll(); | |||||
| static SerialFlashFile open(const char *filename); | |||||
| static bool create(const char *filename, uint32_t length, uint32_t align = 0); | |||||
| static bool createWritable(const char *filename, uint32_t length) { | |||||
| return create(filename, length, 256); | |||||
| } | |||||
| static bool createErasable(const char *filename, uint32_t length) { | |||||
| return create(filename, length, blockSize()); | |||||
| } | |||||
| static void opendir() { dirindex = 0; } | |||||
| static bool readdir(char *filename, uint32_t strsize, uint32_t &filesize); | |||||
| private: | |||||
| static uint16_t dirindex; // current position for readdir() | |||||
| static uint8_t fourbytemode; // 0=use 24 bit address, 1=use 32 bit address | |||||
| static uint8_t busy; // 0 = ready, 1 = suspendable busy, 2 = busy for realz | |||||
| static uint8_t blocksize; // erasable uniform block size, 1=4K, 2=8K, etc | |||||
| static uint8_t capacityId; // 3rd byte from 0x9F identification command | |||||
| }; | |||||
| extern SerialFlashChip SerialFlash; | |||||
| class SerialFlashFile | |||||
| { | |||||
| public: | |||||
| SerialFlashFile() : address(0) { | |||||
| } | |||||
| operator bool() { | |||||
| if (address > 0) return true; | |||||
| return false; | |||||
| } | |||||
| uint32_t read(uint8_t *buf, uint32_t rdlen) { | |||||
| if (offset + rdlen > length) { | |||||
| if (offset >= length) return 0; | |||||
| rdlen = length - offset; | |||||
| } | |||||
| SerialFlash.read(buf, address + offset, rdlen); | |||||
| offset += rdlen; | |||||
| return rdlen; | |||||
| } | |||||
| uint32_t write(const void *buf, uint32_t wrlen) { | |||||
| if (offset + wrlen > length) { | |||||
| if (offset >= length) return 0; | |||||
| wrlen = length - offset; | |||||
| } | |||||
| SerialFlash.write(buf, address + offset, wrlen); | |||||
| offset += wrlen; | |||||
| return wrlen; | |||||
| } | |||||
| void seek(uint32_t n) { | |||||
| offset = n; | |||||
| } | |||||
| uint32_t position() { | |||||
| return offset; | |||||
| } | |||||
| uint32_t size() { | |||||
| return length; | |||||
| } | |||||
| uint32_t available() { | |||||
| if (offset >= length) return 0; | |||||
| return length - offset; | |||||
| } | |||||
| void erase(); | |||||
| void flush() { | |||||
| } | |||||
| void close() { | |||||
| } | |||||
| uint32_t getFlashAddress() { | |||||
| return address; | |||||
| } | |||||
| protected: | |||||
| friend class SerialFlashChip; | |||||
| uint32_t address; // where this file's data begins in the Flash, or zero | |||||
| uint32_t length; // total length of the data in the Flash chip | |||||
| uint32_t offset; // current read/write offset in the file | |||||
| }; | |||||
| #endif |
| #include "SerialFlash.h" | |||||
| #include "SPIFIFO.h" | |||||
| #define CSCONFIG() pinMode(6, OUTPUT) | |||||
| #define CSASSERT() digitalWriteFast(6, LOW) | |||||
| #define CSRELEASE() digitalWriteFast(6, HIGH) | |||||
| #define SPICONFIG SPISettings(50000000, MSBFIRST, SPI_MODE0) | |||||
| uint16_t SerialFlashChip::dirindex = 0; | |||||
| uint8_t SerialFlashChip::fourbytemode = 0; | |||||
| uint8_t SerialFlashChip::busy = 0; | |||||
| uint8_t SerialFlashChip::blocksize = 1; | |||||
| uint8_t SerialFlashChip::capacityId = 0; | |||||
| void SerialFlashChip::wait(void) | |||||
| { | |||||
| uint32_t status; | |||||
| do { | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| status = SPI.transfer16(0x0500); | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| } while ((status & 1)); | |||||
| busy = 0; | |||||
| } | |||||
| void SerialFlashChip::read(void *buf, uint32_t addr, uint32_t len) | |||||
| { | |||||
| uint8_t *p = (uint8_t *)buf; | |||||
| uint8_t b; | |||||
| memset(p, 0, len); | |||||
| b = busy; | |||||
| if (b) { | |||||
| if (b == 1) { | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0x75); // Suspend program/erase | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| delayMicroseconds(20); // Tsus = 20us | |||||
| } else { | |||||
| wait(); | |||||
| } | |||||
| } | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| // TODO: FIFO optimize.... | |||||
| if (fourbytemode) { | |||||
| SPI.transfer(0x13); | |||||
| SPI.transfer16(addr >> 16); | |||||
| SPI.transfer16(addr); | |||||
| } else { | |||||
| SPI.transfer16(0x0300 | ((addr >> 16) & 255)); | |||||
| SPI.transfer16(addr); | |||||
| } | |||||
| SPI.transfer(p, len); | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| if (b == 1) { | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0x7A); // Resume program/erase | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| } | |||||
| } | |||||
| void SerialFlashChip::write(const void *buf, uint32_t addr, uint32_t len) | |||||
| { | |||||
| const uint8_t *p = (const uint8_t *)buf; | |||||
| uint32_t max, pagelen; | |||||
| do { | |||||
| if (busy) wait(); | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0x06); | |||||
| CSRELEASE(); | |||||
| //delayMicroseconds(1); | |||||
| max = 256 - (addr & 0xFF); | |||||
| pagelen = (len <= max) ? len : max; | |||||
| len -= pagelen; | |||||
| CSASSERT(); | |||||
| if (fourbytemode) { | |||||
| SPI.transfer(0x12); | |||||
| SPI.transfer16(addr >> 16); | |||||
| SPI.transfer16(addr); | |||||
| } else { | |||||
| SPI.transfer16(0x0200 | ((addr >> 16) & 255)); | |||||
| SPI.transfer16(addr); | |||||
| } | |||||
| do { | |||||
| SPI.transfer(*p++); | |||||
| } while (--pagelen > 0); | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| busy = 1; | |||||
| } while (len > 0); | |||||
| } | |||||
| void SerialFlashChip::eraseAll() | |||||
| { | |||||
| if (busy) wait(); | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0x06); | |||||
| CSRELEASE(); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0xC7); | |||||
| CSRELEASE(); | |||||
| busy = 2; | |||||
| } | |||||
| bool SerialFlashChip::ready() | |||||
| { | |||||
| uint32_t status; | |||||
| if (!busy) return true; | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| status = SPI.transfer16(0x0500); | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| if ((status & 1)) return false; | |||||
| busy = 0; | |||||
| return true; | |||||
| } | |||||
| bool SerialFlashChip::begin() | |||||
| { | |||||
| SPI.begin(); | |||||
| if (busy) wait(); | |||||
| CSCONFIG(); | |||||
| SPI.beginTransaction(SPICONFIG); | |||||
| CSASSERT(); | |||||
| SPI.transfer(0x9F); | |||||
| SPI.transfer(0); // manufacturer ID | |||||
| SPI.transfer(0); // memory type | |||||
| capacityId = SPI.transfer(0); // capacity | |||||
| CSRELEASE(); | |||||
| SPI.endTransaction(); | |||||
| //Serial.print("capacity = "); | |||||
| //Serial.println(capacityId, HEX); | |||||
| if ((capacityId & 0xF0) == 0x20) { | |||||
| fourbytemode = 1; // chip larger than 16 MByte | |||||
| } else { | |||||
| fourbytemode = 0; | |||||
| } | |||||
| // TODO: how to detect the uniform sector erase size? | |||||
| blocksize = 1; | |||||
| return true; | |||||
| } | |||||
| uint32_t SerialFlashChip::capacity() | |||||
| { | |||||
| return 16777216; // TODO: compute this from capacityId... | |||||
| } | |||||
| uint32_t SerialFlashChip::blockSize() | |||||
| { | |||||
| return 4096; // TODO: how to discover this? | |||||
| } | |||||
| // size sector | |||||
| // Part Mbit kbyte ID bytes Digikey | |||||
| // ---- ---- ----- -------- ------- | |||||
| // Winbond W25Q128FV 128 EF 40 18 W25Q128FVSIG-ND | |||||
| // Winbond W25Q256FV 256 64 EF 40 19 | |||||
| // SST SST25VF016B 16 BF 25 41 | |||||
| // Spansion S25FL127S 128 64? 01 20 18 1274-1045-ND | |||||
| // Spansion S25FL128P 128 01 20 18 | |||||
| // Spansion S25FL064A 64 01 02 16 | |||||
| // Macronix MX25L12805D 128 C2 20 18 | |||||
| // Micron M25P80 8 20 20 14 | |||||
| // Numonyx M25P128 128 20 20 18 | |||||
| // SST SST25WF512 0.5 BF 25 01 | |||||
| // SST SST25WF010 1 BF 25 02 | |||||
| // SST SST25WF020 2 BF 25 03 | |||||
| // SST SST25WF040 4 BF 25 04 | |||||
| // Spansion FL127S 128 01 20 18 ? | |||||
| // Spansion S25FL512S 512 01 02 20 ? | |||||
| // Micron N25Q512A 512 4 20 BA 20 557-1569-ND | |||||
| // Micron N25Q00AA 1024 4/64 20 BA 21 557-1571-5-ND | |||||
| // Micron MT25QL02GC 2048 4/64 20 BB 22 | |||||
| SerialFlashChip SerialFlash; |
| #include "SerialFlash.h" | |||||
| #include "util/crc16.h" | |||||
| /* On-chip SerialFlash file allocation data structures: | |||||
| uint32_t signature = 0xFA96554C; | |||||
| uint16_t maxfiles | |||||
| uint16_t stringssize // div by 4 | |||||
| uint16_t hashes[maxfiles] | |||||
| struct { | |||||
| uint32_t file_begin | |||||
| uint32_t file_length | |||||
| uint16_t string_index // div4 | |||||
| } fileinfo[maxfiles] | |||||
| char strings[stringssize] | |||||
| A 32 bit signature is stored at the beginning of the flash memory. | |||||
| If 0xFFFFFFFF is seen, the entire chip should be assumed blank. | |||||
| If any value other than 0xFA96554C is found, a different data format | |||||
| is stored. This could should refuse to access the flash. | |||||
| The next 4 bytes store number of files and size of the strings | |||||
| section, which allow the position of every other item to be found. | |||||
| The string section size is the 16 bit integer times 4, which allows | |||||
| up to 262140 bytes for string data. | |||||
| An array of 16 bit filename hashes allows for quick linear search | |||||
| for potentially matching filenames. A hash value of 0xFFFF indicates | |||||
| no file is allocated for the remainder of the array. | |||||
| Following the hashes, and array of 10 byte structs give the location | |||||
| and length of the file's actual data, and the offset of its filename | |||||
| in the strings section. | |||||
| Strings are null terminated. The remainder of the chip is file data. | |||||
| */ | |||||
| #define DEFAULT_MAXFILES 600 | |||||
| #define DEFAULT_STRINGS_SIZE 25560 | |||||
| static uint32_t check_signature(void) | |||||
| { | |||||
| uint32_t sig[2]; | |||||
| SerialFlash.read(sig, 0, 8); | |||||
| if (sig[0] == 0xFA96554C) return sig[1]; | |||||
| if (sig[0] == 0xFFFFFFFF) { | |||||
| sig[0] = 0xFA96554C; | |||||
| sig[1] = ((DEFAULT_STRINGS_SIZE/4) << 16) | DEFAULT_MAXFILES; | |||||
| SerialFlash.write(sig, 0, 8); | |||||
| while (!SerialFlash.ready()) ; // TODO: timeout | |||||
| SerialFlash.read(sig, 0, 8); | |||||
| if (sig[0] == 0xFA96554C) return sig[1]; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| static uint16_t filename_hash(const char *filename) | |||||
| { | |||||
| uint16_t crc; | |||||
| const char *p; | |||||
| crc = 0xFFFF; | |||||
| for (p=filename; *p; p++) { | |||||
| // TODO: replace with fast CRC hardware? | |||||
| crc = _crc16_update(crc, *p); | |||||
| } | |||||
| crc ^= 0xFFFF; | |||||
| if (crc == 0xFFFF) crc = 0; | |||||
| return crc; | |||||
| } | |||||
| static bool filename_compare(const char *filename, uint32_t straddr) | |||||
| { | |||||
| unsigned int i; | |||||
| const char *p; | |||||
| char buf[16]; | |||||
| p = filename; | |||||
| while (1) { | |||||
| SerialFlash.read(buf, straddr, sizeof(buf)); | |||||
| straddr += sizeof(buf); | |||||
| for (i=0; i < sizeof(buf); i++) { | |||||
| if (*p++ != buf[i]) return false; | |||||
| if (buf[i] == 0) return true; | |||||
| } | |||||
| } | |||||
| } | |||||
| SerialFlashFile SerialFlashChip::open(const char *filename) | |||||
| { | |||||
| uint32_t maxfiles, straddr; | |||||
| uint16_t hash, hashtable[8]; | |||||
| uint32_t i, n, index=0; | |||||
| uint32_t buf[3]; | |||||
| SerialFlashFile file; | |||||
| maxfiles = check_signature(); | |||||
| if (!maxfiles) return file; | |||||
| maxfiles &= 0xFFFF; | |||||
| hash = filename_hash(filename); | |||||
| while (index < maxfiles) { | |||||
| n = 8; | |||||
| if (n > maxfiles - index) n = maxfiles - index; | |||||
| SerialFlash.read(hashtable, 8 + index * 2, n * 2); | |||||
| for (i=0; i < n; i++) { | |||||
| if (hashtable[i] == hash) { | |||||
| buf[2] = 0; | |||||
| SerialFlash.read(buf, 8 + maxfiles * 2 + (index+i) * 10, 10); | |||||
| straddr = 8 + maxfiles * 12 + buf[2] * 4; | |||||
| if (filename_compare(filename, straddr)) { | |||||
| file.address = buf[0]; | |||||
| file.length = buf[1]; | |||||
| file.offset = 0; | |||||
| return file; | |||||
| } | |||||
| } else if (hashtable[i] == 0xFFFF) { | |||||
| return file; | |||||
| } | |||||
| } | |||||
| index += n; | |||||
| } | |||||
| return file; | |||||
| } | |||||
| static uint32_t find_first_unallocated_file_index(uint32_t maxfiles) | |||||
| { | |||||
| uint16_t hashtable[8]; | |||||
| uint32_t i, n, index=0; | |||||
| do { | |||||
| n = 8; | |||||
| if (index + n > maxfiles) n = maxfiles - index; | |||||
| SerialFlash.read(hashtable, 8 + index * 2, n * 2); | |||||
| for (i=0; i < n; i++) { | |||||
| if (hashtable[i] == 0xFFFF) return index + i; | |||||
| } | |||||
| index += n; | |||||
| } while (index < maxfiles); | |||||
| return 0xFFFFFFFF; | |||||
| } | |||||
| static uint32_t string_length(uint32_t addr) | |||||
| { | |||||
| char buf[16]; | |||||
| const char *p; | |||||
| uint32_t len=0; | |||||
| while (1) { | |||||
| SerialFlash.read(buf, addr, sizeof(buf)); | |||||
| for (p=buf; p < buf + sizeof(buf); p++) { | |||||
| if (*p == 0) return len; | |||||
| len++; | |||||
| } | |||||
| addr += len; | |||||
| } | |||||
| } | |||||
| // uint32_t signature = 0xFA96554C; | |||||
| // uint16_t maxfiles | |||||
| // uint16_t stringssize // div by 4 | |||||
| // uint16_t hashes[maxfiles] | |||||
| // struct { | |||||
| // uint32_t file_begin | |||||
| // uint32_t file_length | |||||
| // uint16_t string_index // div 4 | |||||
| // } fileinfo[maxfiles] | |||||
| // char strings[stringssize] | |||||
| bool SerialFlashChip::create(const char *filename, uint32_t length, uint32_t align) | |||||
| { | |||||
| uint32_t maxfiles, stringsize; | |||||
| uint32_t index, buf[3]; | |||||
| uint32_t address, straddr, len; | |||||
| SerialFlashFile file; | |||||
| // first, get the filesystem parameters | |||||
| maxfiles = check_signature(); | |||||
| if (!maxfiles) return false; | |||||
| stringsize = (maxfiles & 0xFFFF0000) >> 14; | |||||
| maxfiles &= 0xFFFF; | |||||
| // find the first unused slot for this file | |||||
| index = find_first_unallocated_file_index(maxfiles); | |||||
| if (index >= maxfiles) return false; | |||||
| // compute where to store the filename and actual data | |||||
| straddr = 8 + maxfiles * 12; | |||||
| if (index == 0) { | |||||
| address = straddr + stringsize; | |||||
| } else { | |||||
| buf[2] = 0; | |||||
| SerialFlash.read(buf, 8 + maxfiles * 2 + (index-1) * 10, 10); | |||||
| address = buf[0] + buf[1]; | |||||
| straddr += buf[2] * 4; | |||||
| straddr += string_length(straddr); | |||||
| straddr = (straddr + 3) & 0x0003FFFC; | |||||
| } | |||||
| // for files aligned to pages or sectors, adjust addr & len | |||||
| if (align > 0) { | |||||
| address += align - 1; | |||||
| address /= align; | |||||
| address *= align; | |||||
| length += align - 1; | |||||
| length /= align; | |||||
| length *= align; | |||||
| } | |||||
| // last check, if enough space exists... | |||||
| len = strlen(filename); | |||||
| // TODO: check for enough string space for filename | |||||
| if (address + length > SerialFlash.capacity()) return false; | |||||
| SerialFlash.write(filename, straddr, len+1); | |||||
| buf[0] = address; | |||||
| buf[1] = length; | |||||
| buf[2] = (straddr - (8 + maxfiles * 12)) / 4; | |||||
| SerialFlash.write(buf, 8 + maxfiles * 2 + index * 10, 10); | |||||
| buf[0] = filename_hash(filename); | |||||
| SerialFlash.write(buf, 8 + index * 2, 2); | |||||
| while (!SerialFlash.ready()) ; // TODO: timeout | |||||
| return false; | |||||
| } | |||||
| bool SerialFlashChip::readdir(char *filename, uint32_t strsize, uint32_t &filesize) | |||||
| { | |||||
| uint32_t maxfiles, index, straddr; | |||||
| uint32_t i, n; | |||||
| uint32_t buf[2]; | |||||
| char str[16], *p=filename; | |||||
| filename[0] = 0; | |||||
| maxfiles = check_signature(); | |||||
| if (!maxfiles) return false; | |||||
| maxfiles &= 0xFFFF; | |||||
| index = dirindex; | |||||
| if (index >= maxfiles) return false; | |||||
| dirindex = index + 1; | |||||
| buf[1] = 0; | |||||
| SerialFlash.read(buf, 8 + 4 + maxfiles * 2 + index * 10, 6); | |||||
| if (buf[0] == 0xFFFFFFFF) return false; | |||||
| filesize = buf[0]; | |||||
| straddr = 8 + maxfiles * 12 + buf[1] * 4; | |||||
| while (strsize) { | |||||
| n = strsize; | |||||
| if (n > sizeof(str)) n = sizeof(str); | |||||
| SerialFlash.read(str, straddr, n); | |||||
| for (i=0; i < n; i++) { | |||||
| *p++ = str[i]; | |||||
| if (str[i] == 0) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| strsize -= n; | |||||
| } | |||||
| *(p - 1) = 0; | |||||
| return true; | |||||
| } | |||||
| void SerialFlashFile::erase() | |||||
| { | |||||
| // TODO: erase all the blocks of a file | |||||
| // if it's been block aligned, of course | |||||
| } | |||||
| #include <SerialFlash.h> | |||||
| #include <SPI.h> | |||||
| SerialFlashFile file; | |||||
| void setup() { | |||||
| char filename[40]; | |||||
| uint32_t len; | |||||
| while (!Serial) ; | |||||
| delay(10); | |||||
| SPI.setSCK(14); // Audio shield has SCK on pin 14 | |||||
| SPI.setMOSI(7); // Audio shield has MOSI on pin 7 | |||||
| Serial.println("Test Hardware"); | |||||
| SerialFlash.begin(); | |||||
| #if 0 | |||||
| Serial.println("erase"); | |||||
| SerialFlash.eraseAll(); | |||||
| while (!SerialFlash.ready()) { | |||||
| } | |||||
| Serial.println("erase done"); | |||||
| #endif | |||||
| Serial.println("Directory:"); | |||||
| while (SerialFlash.readdir(filename, sizeof(filename), len)) { | |||||
| Serial.print(" file: "); | |||||
| Serial.print(filename); | |||||
| Serial.print(" bytes: "); | |||||
| Serial.print(len); | |||||
| Serial.println(); | |||||
| } | |||||
| Serial.println(); | |||||
| Serial.println("simple.txt test"); | |||||
| file = SerialFlash.open("simple.txt"); | |||||
| if (file) { | |||||
| Serial.println(" file opened"); | |||||
| Serial.print(" length = "); | |||||
| Serial.println(file.size()); | |||||
| Serial.print(" addr on chip = "); | |||||
| Serial.println(file.getFlashAddress()); | |||||
| file.close(); | |||||
| } else { | |||||
| Serial.println(" create file"); | |||||
| SerialFlash.create("simple.txt", 516); | |||||
| } | |||||
| Serial.println("soundfile.wav test"); | |||||
| file = SerialFlash.open("soundfile.wav"); | |||||
| if (file) { | |||||
| Serial.println(" file opened"); | |||||
| Serial.print(" length = "); | |||||
| Serial.println(file.size()); | |||||
| Serial.print(" addr on chip = "); | |||||
| Serial.println(file.getFlashAddress()); | |||||
| file.close(); | |||||
| } else { | |||||
| Serial.println(" create file"); | |||||
| SerialFlash.createWritable("soundfile.wav", 3081000); | |||||
| } | |||||
| Serial.println("wavetable1 test"); | |||||
| file = SerialFlash.open("wavetable1"); | |||||
| if (file) { | |||||
| Serial.println(" file opened"); | |||||
| Serial.print(" length = "); | |||||
| Serial.println(file.size()); | |||||
| Serial.print(" addr on chip = "); | |||||
| Serial.println(file.getFlashAddress()); | |||||
| file.close(); | |||||
| } else { | |||||
| Serial.println(" create file"); | |||||
| SerialFlash.create("wavetable1", 181003); | |||||
| } | |||||
| Serial.println("end"); | |||||
| } | |||||
| void loop() { | |||||
| } | |||||
| void printbuf(const void *buf, uint32_t len) | |||||
| { | |||||
| const uint8_t *p = (const uint8_t *)buf; | |||||
| do { | |||||
| Serial.print(*p++); | |||||
| Serial.print(" "); | |||||
| } while (--len > 0); | |||||
| Serial.println(); | |||||
| } | |||||
| SerialFlash KEYWORD1 | |||||
| createWritable KEYWORD2 | |||||
| erase KEYWORD2 | |||||
| eraseAll KEYWORD2 | |||||
| ready KEYWORD2 | |||||
| create KEYWORD2 | |||||
| createWritable KEYWORD2 | |||||
| getAddress KEYWORD2 |