diff --git a/inc/core/MicroBitConfig.h b/inc/core/MicroBitConfig.h index f8505f9..b036757 100644 --- a/inc/core/MicroBitConfig.h +++ b/inc/core/MicroBitConfig.h @@ -72,6 +72,11 @@ DEALINGS IN THE SOFTWARE. #define MICROBIT_HEAP_END (CORTEX_M0_STACK_BASE - MICROBIT_STACK_SIZE) #endif +// Defines the size of a physical FLASH page in RAM. +#ifndef PAGE_SIZE +#define PAGE_SIZE 1024 +#endif + // Enables or disables the MicroBitHeapllocator. Note that if disabled, no reuse of the SRAM normally // reserved for SoftDevice is possible, and out of memory condition will no longer be trapped... // i.e. panic() will no longer be triggered on memory full conditions. @@ -318,6 +323,16 @@ DEALINGS IN THE SOFTWARE. #define MICROBIT_DEFAULT_SERIAL_MODE SYNC_SLEEP #endif +// +// File System configuration defaults +// +#ifndef MBFS_BLOCK_SIZE +#define MBFS_BLOCK_SIZE 256 +#endif + +#ifndef MBFS_CACHE_SIZE +#define MBFS_CACHE_SIZE 0 +#endif // // I/O Options diff --git a/inc/drivers/MicroBitFileSystem.h b/inc/drivers/MicroBitFileSystem.h new file mode 100644 index 0000000..2d08b73 --- /dev/null +++ b/inc/drivers/MicroBitFileSystem.h @@ -0,0 +1,481 @@ +#ifndef MICROBIT_FILE_SYSTEM_H +#define MICROBIT_FILE_SYSTEM_H + +#include "MicroBitConfig.h" +#include "MicroBitFlash.h" + + +// Configuration options. +#define MBFS_FILENAME_LENGTH 16 +#define MBFS_MAGIC "MICROBIT_FS_1_0" + +// open() flags. +#define MB_READ 0x01 +#define MB_WRITE 0x02 +#define MB_CREAT 0x04 +#define MB_APPEND 0x08 + +// seek() flags. +#define MB_SEEK_SET 0x01 +#define MB_SEEK_END 0x02 +#define MB_SEEK_CUR 0x04 + +// Status flags +#define MBFS_STATUS_INITIALISED 0x01 + +// FileTable codes +#define MBFS_UNUSED 0xFFFF +#define MBFS_EOF 0xEFFF +#define MBFS_DELETED 0x0000 + +// DirectorEntry flags +#define MBFS_DIRECTORY_ENTRY_FREE 0x8000 +#define MBFS_DIRECTORY_ENTRY_VALID 0x4000 +#define MBFS_DIRECTORY_ENTRY_DIRECTORY 0x2000 +#define MBFS_DIRECTORY_ENTRY_NEW 0xffff +#define MBFS_DIRECTORY_ENTRY_DELETED 0x0000 + +// Enumeration of BLOCK YPES +#define MBFS_BLOCK_TYPE_FILE 1 +#define MBFS_BLOCK_TYPE_DIRECTORY 2 +#define MBFS_BLOCK_TYPE_FILETABLE 3 + + + +// +// Every file in the file system has a file descriptor. +// These are held in directory entries. +// +struct DirectoryEntry +{ + char file_name[MBFS_FILENAME_LENGTH]; // Name of the file + uint16_t first_block; // Logical block address of the start of the file. + uint16_t flags; // Status of the file. + uint32_t length; // Length of the file in bytes. +}; + +struct Directory +{ + DirectoryEntry entry[0]; +}; + +// +// A FileDescriptor holds contextual information needed for each open file. +// +struct FileDescriptor +{ + // read/write/creat flags. + uint16_t flags; + + // FileDescriptor id + uint16_t id; + + // current file position, in bytes. + uint32_t seek; + + // the current file size. n.b. this may be different to that stored in the DirectoryEntry. + uint32_t length; + + // the directory entry of this file. + DirectoryEntry *dirent; + + // the directory entry of our parent directory. + DirectoryEntry *directory; + + // We maintain a chain of open file descriptors. Reference to the next FileDescriptor in the chain. + FileDescriptor *next; + + // Optional writeback cache, to minimise FLASH write operations at the expense of RAM. + uint16_t cacheLength; + uint8_t cache[MBFS_CACHE_SIZE]; +}; + +/** + * @brief Class definition for the MicroBit File system + * + * Microbit file system class. Presents a POSIX-like interface consisting of: + * - open() + * - close() + * - read() + * - write() + * - seek() + * - remove() + * + * Only a single instance shoud exist at any given time. + */ +class MicroBitFileSystem +{ + private: + + // Status flags + uint32_t status; + + // The instance of MicroBitFlash - the interface used for all flash writes/erasures + MicroBitFlash flash; + + // Total Number of logical pages available for file data (including the file table) + int fileSystemSize; + + // Memory address of the start of the file system. + uint16_t* fileSystemTable = NULL; + + // Size of the file table + uint16_t fileSystemTableSize; + + // Cache of the last block allocated. + uint16_t lastBlockAllocated = 0; + + // Reference to the root directory of the file system. + DirectoryEntry *rootDirectory = NULL; + + // Chain of open files. + FileDescriptor *openFiles = NULL; + /** + * Initialize the flash storage system + * + * The file system is located dynamically, based on where the program code + * and code data finishes. This avoids having to allocate a fixed flash + * region for builds even without MicroBitFileSystem. + * + * This method checks if the file system already exists, and loads it it. + * If not, it will determines the optimal size of the file system, if necessary, and format the space + * + * @return MICROBIT_OK on success, or an error code. + */ + int init(uint32_t flashStart, int flashPages); + + /** + * Attempts to detect and load an exisitng file file system. + * + * @return MICROBIT_OK on success, or MICROBIT_NO_DATA if the file system could not be found. + */ + int load(); + + /** + * Allocate a free logical block. + * This is chosen at random from the blocks available, to even out the wear on the physical device. + * @return NULL on error, page address on success + */ + uint16_t getFreeBlock(); + + /** + * Allocates a free physical block. + * This is chosen at random from the blocks available, to even out the wear on the physical device. + * @return NULL on error, page address on success + */ + uint32_t* getFreePage(); + + /** + * Retrieve the DirectoryEntry assoiated with the given file's DIRECTORY (not the file itself). + * + * @param filename A fully qualified filename, from the root. + * + * @return A pointer to the DirectoryEntry for the given file's directory, or NULL if no entry is found. + */ + DirectoryEntry* getDirectoryOf(char const * filename); + + /** + * Retrieve the DirectoryEntry for the given filename. + * + * @param filename A fully or partially qualified filename. + * @param directory The directory to search. If ommitted, the root directory will be used. + * + * @return A pointer to the DirectoryEntry for the given file, or NULL if no entry is found. + */ + DirectoryEntry* getDirectoryEntry(char const * filename, const DirectoryEntry *directory = NULL); + + /** + * Create a new DirectoryEntry with the given filename and flags. + * + * @param filename A fully or partially qualified filename. + * @param directory The directory in which to create the entry + * @param isDirectory true if the entry being created is itself a directory + * + * @return A pointer to the new DirectoryEntry for the given file, or NULL if it was not possible to allocated resources. + */ + DirectoryEntry* createFile(char const * filename, DirectoryEntry *directory, bool isDirectory); + + /** + * Allocate a free DiretoryEntry in the given directory, extending and refreshing the directory block if necessary. + * + * @param directory The directory to add a DirectoryEntry to + * @return A pointer to the new DirectoryEntry for the given file, or NULL if it was not possible to allocated resources. + */ + DirectoryEntry* createDirectoryEntry(DirectoryEntry *directory); + + /** + * Refresh the physical page associated with the given block. + * Any logical blocks marked for deletion on that page are recycled. + * + * @param block the block to recycle. + * @param isDirectory set to tru of the block contains a directory to enable deep DirectoryEntry recycling. + * @return MICROBIT_OK on success. + */ + int recycleBlock(uint16_t block, int type = MBFS_BLOCK_TYPE_FILE); + + /** + * Refresh the physical pages associated with the file table. + * Any logical blocks marked for deletion on those pages are recycled back to UNUSED. + ** + * @return MICROBIT_OK on success. + */ + int recycleFileTable(); + + /** + * Retrieve a memory pointer for the start of the physical memory page containing the given block. + * + * @param block A valid block in a file. + * + * @return A pointer to the physical page in FLASH memory holding the given block. + */ + uint32_t *getPage(uint16_t block); + + /** + * Retrieve a memory pointer for the start of the given block. + * + * @param block A valid block in a file. + * + * @return A pointer to the FLASH memory associated with the given block. + */ + uint32_t *getBlock(uint16_t block); + + /** + * Retrieve the next block in a chain. + * + * @param block A valid block in a file. + * + * @return The block number of the next block in the file. + */ + uint16_t getNextFileBlock(uint16_t block); + + /** + * Determine the logical block that contains the given address. + * + * @param address A valid memory location within the file system space. + * + * @return The block number containing the given address. + */ + uint16_t getBlockNumber(void *address); + + /** + * Determine the number of logical blocks required to hold the file table. + * + * @return The number of logical blocks required to hold the file table. + */ + uint16_t calculateFileTableSize(); + + /* + * Update a file table entry to a given value. + * + * @param block The block to update. + * @param value The value to store in the file table. + * @return MICROBIT_OK on success. + */ + int fileTableWrite(uint16_t block, uint16_t value); + + /** + * Searches the list of open files for one with the given identifier. + * + * @param fd A previsouly opened file identifier, as returned by open(). + * @param remove Remove the file descriptor from the list if true. + * @return A FileDescriptor matching the given ID, or NULL if the file is not open. + */ + FileDescriptor* getFileDescriptor(int fd, bool remove = false); + + /** + * Initialises a new file system + * + * @return MICROBIT_OK on success, or an error code.. + */ + int format(); + + /** + * Flush a given file's cache back to FLASH memory. + * + * @param file File descriptor to flush. + * @return The number of bytes written. + * + */ + int writeBack(FileDescriptor *file); + + /** + * Write a given buffer to the file provided. + * + * @param file FileDescriptor of the file to write + * @param buffer The start of the buffer to write + * @param length The number of bytes to write + * @return The number of bytes written. + */ + int writeBuffer(FileDescriptor *file, uint8_t* buffer, int length); + + public: + + static MicroBitFileSystem *defaultFileSystem; + + /** + * Constructor. Creates an instance of a MicroBitFileSystem. + */ + MicroBitFileSystem(uint32_t flashStart = 0, int flashPages = 0); + + /** + * Open a new file, and obtain a new file handle (int) to + * read/write/seek the file. The flags are: + * - MB_READ : read from the file. + * - MB_WRITE : write to the file. + * - MB_CREAT : create a new file, if it doesn't already exist. + * + * If a file is opened that doesn't exist, and MB_CREAT isn't passed, + * an error is returned, otherwise the file is created. + * + * @todo The same file can only be opened by a single handle at once. + * @todo Add MB_APPEND flag. + * + * @param filename name of the file to open, must be null terminated. + * @param flags + * @return return the file handle,MICROBIT_NOT_SUPPORTED if the file system has + * not been initialised MICROBIT_INVALID_PARAMETER if the filename is + * too large, MICROBIT_NO_RESOURCES if the file system is full. + * + * @code + * MicroBitFileSystem f(); + * int fd = f.open("test.txt", MB_WRITE|MB_CREAT); + * if(fd<0) + * print("file open error"); + * @endcode + */ + int open(char const * filename, uint32_t flags); + + /** + * Close the specified file handle. + * File handle resources are then made available for future open() calls. + * + * close() must be called at some point to ensure the filesize in the + * FT is synced with the cached value in the FD. + * + * @warning if close() is not called, the FT may not be correct, + * leading to data loss. + * + * @param fd file descriptor - obtained with open(). + * @return non-zero on success, MICROBIT_NOT_SUPPORTED if the file system has not + * been initialised, MICROBIT_INVALID_PARAMETER if the given file handle + * is invalid. + * + * @code + * MicroBitFileSystem f(); + * int fd = f.open("test.txt", MB_READ); + * if(!f.close(fd)) + * print("error closing file."); + * @endcode + */ + int close(int fd); + + /** + * Move the current position of a file handle, to be used for + * subsequent read/write calls. + * + * The offset modifier can be: + * - MB_SEEK_SET set the absolute seek position. + * - MB_SEEK_CUR set the seek position based on the current offset. + * - MB_SEEK_END set the seek position from the end of the file. + * E.g. to seek to 2nd-to-last byte, use offset=-1. + * + * @param fd file handle, obtained with open() + * @param offset new offset, can be positive/negative. + * @param flags + * @return new offset position on success, MICROBIT_NOT_SUPPORTED if the file system + * is not intiialised, MICROBIT_INVALID_PARAMETER if the flag given is invalid + * or the file handle is invalid. + * + * @code + * MicroBitFileSystem f; + * int fd = f.open("test.txt", MB_READ); + * f.seek(fd, -100, MB_SEEK_END); //100 bytes before end of file. + * @endcode + */ + int seek(int fd, int offset, uint8_t flags); + + /** + * Write data to the file. + * + * Write from buffer, length bytes to the current seek position. + * On each invocation to write, the seek position of the file handle + * is incremented atomically, by the number of bytes returned. + * + * The cached filesize in the FD is updated on this call. Also, the + * FT file size is updated only if a new page(s) has been written too, + * to reduce the number of FT writes. + * + * @param fd File handle + * @param buffer the buffer from which to write data + * @param length number of bytes to write + * @return number of bytes written on success, MICROBIT_NO_RESOURCES if data did + * not get written to flash or the file system has not been initialised, + * or this file was not opened with the MB_WRITE flag set, MICROBIT_INVALID_PARAMETER + * if the given file handle is invalid. + * + * @code + * MicroBitFileSystem f(); + * int fd = f.open("test.txt", MB_WRITE); + * if(f.write(fd, "hello!", 7) != 7) + * print("error writing"); + * @endcode + */ + int write(int fd, uint8_t* buffer, int length); + + /** + * Read data from the file. + * + * Read len bytes from the current seek position in the file, into the + * buffer. On each invocation to read, the seek position of the file + * handle is incremented atomically, by the number of bytes returned. + * + * @param fd File handle, obtained with open() + * @param buffer to store data + * @param len number of bytes to read + * @return number of bytes read on success, MICROBIT_NOT_SUPPORTED if the file + * system is not initialised, or this file was not opened with the + * MB_READ flag set, MICROBIT_INVALID_PARAMETER if the given file handle + * is invalid. + * + * @code + * MicroBitFileSystem f; + * int fd = f.open("read.txt", MB_READ); + * if(f.read(fd, buffer, 100) != 100) + * print("read error"); + * @endcode + */ + int read(int fd, uint8_t* buffer, int len); + + /** + * Remove a file from the system, and free allocated assets + * (including assigned blocks which are returned for use by other files). + * + * @todo the file must not already have an open file handle. + * + * @param filename name of the file to remove. + * @return MICROBIT_OK on success, MICROBIT_INVALID_PARAMETER if the given filename + * does not exist, MICROBIT_CANCELLED if something went wrong + * + * @code + * MicroBitFileSystem f; + * if(!f.remove("file.txt")) + * print("file could not be removed") + * @endcode + */ + int remove(char const * filename); + + /** + * Creates a new directory with the given name and location + * + * @param name The fully qualified name of the new directory. + * @return MICROBIT_OK on success, MICROBIT_INVALID_PARAMETER if the path is invalid, or MICROBT_NO_RESOURCES if the FileSystem is full. + */ + int createDirectory(char const *name); + + void debugFAT(); + void debugRootDirectory(); + int debugDirectory(char *name); + void debugDirectory(DirectoryEntry *directory); +}; + +#endif diff --git a/inc/drivers/MicroBitFlash.h b/inc/drivers/MicroBitFlash.h new file mode 100644 index 0000000..6cfe24a --- /dev/null +++ b/inc/drivers/MicroBitFlash.h @@ -0,0 +1,138 @@ +#ifndef MICROBIT_FLASH_H_ +#define MICROBIT_FLASH_H_ + +#include + +#define DEFAULT_SCRATCH_ADDR 0x3B000 +#define PAGE_SIZE 1024 + +typedef enum flash_mode_t +{ + WR_WRITE, + WR_MEMSET +} flash_mode; + +class MicroBitFlash +{ + private: + + + /** + * Write to flash memory, assuming that a write is valid + * (using need_erase). + * + * @param page_address address of memory to write to. + * Must be word aligned. + * @param buffer address to write from, must be word-aligned. + * @param len number of uint32_t words to write. + */ + void flash_burn(uint32_t* page_address, uint32_t* buffer, int len); + + /** + * Write to address in flash, implementing either flash_write (copy + * data from buffer), or flash_memset (set all bytes in flash to + * provided constant. + * + * Function ensures that data is written correctly: + * - erase page if necessary (using need_erase) + * - preserve non-target flash by copying to scratch page. + * + * flash_write_mem assumes that the provided scratch page is already clean, + * i.e. all bytes = 0xFF. The page is erased after use, all bytes reset to + * 0xFF. + * + * @param address in flash to write to + * @param from_buffer address to write data from + * @param write_byte if writing the same byte to add addressed bytes + * @param len number of bytes to write to/copy to + * @param m mode to use this function, memset/memcpy. + * @param scratch_addr scratch page address (must be page-aligned) + * @return non-zero on success, zero on error. + */ + int flash_write_mem(uint8_t* address, uint8_t* from_buffer, + uint8_t write_byte, int len, flash_mode m, + uint8_t* scratch_addr); + + /** + * Check if an erase is required to write to a region in flash memory. + * This is determined if, for any byte: + * ~O & N != 0x00 + * Where O=Original byte, N = New byte. + * + * @param source to write from + * @param flash_address to write to + * @param len number of uint8_t to check. + * @return non-zero if erase required, zero otherwise. + */ + int need_erase(uint8_t* source, uint8_t* flash_addr, int len); + + public: + /** + * Default constructor. + */ + MicroBitFlash(); + + /** + * Writes the given number of bytes to the address in flash specified. + * Neither address nor buffer need be word-aligned. + * @param address location in flash to write to. + * @param buffer location in memory to write from. + * @length number of bytes to burn + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on sucess, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * uint32_t word = 0x01; + * flash.flash_write((uint8_t*)0x38000, &word, sizeof(word)) + * @endcode + */ + int flash_write(uint8_t* address, uint8_t* buffer, int length, + uint8_t* scratch_addr); + + /** + * Set bytes in [address, address+length] to byte. + * Neither address nor buffer need be word-aligned. + * @param address to write to + * @param byte byte to burn to flash + * @param length number of bytes to write to. + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on success, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * flash.flash_memset((uint8_t*)0x38000, 0x66, 12); //12 bytes to 0x66. + * @endcode + */ + int flash_memset(uint8_t* address, uint8_t byte, int length, + uint8_t* scratch_addr); + + /** + * Erase bytes in memory, from set address. + * @param address to erase bytes from (needn't be word-aligned. + * @param length number of bytes to erase (set to 0xFF). + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on success, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * flash.flash_erase((uint8_t*)0x38000, 10240); //erase a flash page. + * @endcode + */ + int flash_erase_mem(uint8_t* address, int length, + uint8_t* scratch_addr); + + /** + * Erase an entire page. + * @param page_address address of first word of page. + */ + void erase_page(uint32_t* page_address); +}; + +#endif diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 863224d..9a9b4fc 100755 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -41,6 +41,8 @@ set(YOTTA_AUTO_MICROBIT-DAL_CPP_FILES "drivers/MicroBitStorage.cpp" "drivers/MicroBitThermometer.cpp" "drivers/TimedInterruptIn.cpp" + "drivers/MicroBitFlash.cpp" + "drivers/MicroBitFileSystem.cpp" "bluetooth/MicroBitAccelerometerService.cpp" "bluetooth/MicroBitBLEManager.cpp" diff --git a/source/drivers/MicroBitFileSystem.cpp b/source/drivers/MicroBitFileSystem.cpp new file mode 100644 index 0000000..bca05c3 --- /dev/null +++ b/source/drivers/MicroBitFileSystem.cpp @@ -0,0 +1,1404 @@ +//#ifdef SIMULATOR +#include "stdafx.h" +#include +#include "malloc.h" + +#include "MicroBitConfig.h" +#include "MicroBitFileSystem.h" +#include "MicroBitFlash.h" +#include "MicroBitStorage.h" +#include "MicroBitCompat.h" +#include "ErrorNo.h" + + +// Symbols provided by the linker script. +extern uint32_t __data_end__; +extern uint32_t __data_start__; +extern uint32_t __etext; + +#ifdef SIMULATOR +static uint8_t fs_mem_buf[2*MBFS_PAGES*PAGE_SIZE] = { }; +static uint8_t *fs_mem; +static uint32_t defaultScratchPage[PAGE_SIZE]; +#endif + +MicroBitFileSystem* MicroBitFileSystem::defaultFileSystem = NULL; + +/** +* Allocate a free logical block. +* This is chosen at random from the blocks available, to even out the wear on the physical device. +* @return a valid, unused block address on success, or zero if no space is available. +*/ +uint16_t MicroBitFileSystem::getFreeBlock() +{ + // Walk the File Table and allocate the first free block - starting immediately after the last block allocated, + // and wrapping around the filesystem space if we reach the end. + uint16_t block; + uint16_t deletedBlock = 0; + + for (block = (lastBlockAllocated + 1) % fileSystemSize; block != lastBlockAllocated; block++) + { + if (fileSystemTable[block] == MBFS_UNUSED) + { + lastBlockAllocated = block; + return block; + } + + if (fileSystemTable[block] == MBFS_DELETED) + deletedBlock = block; + } + + // if no UNUSED blocks are available, try to recycle one marked as DELETED. + block = deletedBlock; + + // If no blocks are available - either UNUSED or marked as DELETED, then we're out of space and there's nothing we can do. + if (block) + { + // recycle the FileTable, such that we can mark all previously deleted blocks as re-usable. + // Better to do this in bulk, rather than on a block by block basis to improve efficiency. + recycleFileTable(); + + // Record the block we just allocated, so we can round-robin around blocks for load balancing. + lastBlockAllocated = block; + } + + return block; +} + +/** +* Allocates a free physical block. +* This is chosen at random from the blocks available, to even out the wear on the physical device. +* @return NULL on error, page address on success +*/ +uint32_t* MicroBitFileSystem::getFreePage() +{ + // Walk the file table, starting at the last allocated block, looking for an unused page. + int blocksPerPage = (PAGE_SIZE / MBFS_BLOCK_SIZE); + + // get a handle on the next physical page. + uint16_t currentPage = getBlockNumber(getPage(lastBlockAllocated)); + uint16_t page = (currentPage + blocksPerPage) % fileSystemSize; + uint16_t recyclablePage = 0; + // Walk aaround the file table, looking for a free page. + while (page != currentPage) + { + bool empty = true; + bool deleted = false; + uint16_t next; + + for (int i = 0; i < blocksPerPage; i++) + { + next = getNextFileBlock(page + i); + + if (next == MBFS_DELETED) + deleted = true; + + else if (next != MBFS_UNUSED) + { + empty = false; + break; + } + } + + // See if we found one... + if (empty) + { + lastBlockAllocated = page; + return getBlock(page); + } + + // make note of the first unused but un-erased page we find (if any). + if (deleted && !recyclablePage) + recyclablePage = page; + + page = (page + blocksPerPage) % fileSystemSize; + } + + // No empty pages are available, but we may be able to recycle one. + if (recyclablePage) + { + uint32_t *address = getBlock(recyclablePage); + flash.erase_page(address); + return address; + } + + // Nothing available at all. + return defaultScratchPage; +} + + +/** + * Constructor. Creates an instance of a MicroBitFileSystem. + */ +MicroBitFileSystem::MicroBitFileSystem(uint32_t flashStart, int flashPages) +{ +#ifdef SIMULATOR + fs_mem = (uint8_t *)(((uint32_t)fs_mem_buf & ~(PAGE_SIZE - 1)) + PAGE_SIZE); + flashStart = (uint32_t) fs_mem; + flashPages = MBFS_PAGES; + + // make it look like a freshly erased set of pages. + for (int i = 0; i < (MBFS_PAGES * PAGE_SIZE); i++) + fs_mem[i] = 0xff; + + for (int i = 0; i < PAGE_SIZE; i++) + defaultScratchPage[i] = 0xff; + +#endif + // Attempt tp load an existing filesystem, if it exisits + init(flashStart, flashPages); + + // If this is the first FileSystem created, so it as the default. + if(MicroBitFileSystem::defaultFileSystem == NULL) + MicroBitFileSystem::defaultFileSystem = this; +} + +/** + * Initialize the flash storage system + * + * The file system is located dynamically, based on where the program code + * and code data finishes. This avoids having to allocate a fixed flash + * region for builds even without MicroBitFileSystem. + * + * This method checks if the file system already exists, and loads it it. + * If not, it will determines the optimal size of the file system, if necessary, and format the space + * + * @return MICROBIT_OK on success, or an error code. + */ +int MicroBitFileSystem::init(uint32_t flashStart, int flashPages) +{ + // Protect against accidental re-initialisation + if (status & MBFS_STATUS_INITIALISED) + return MICROBIT_NOT_SUPPORTED; + + // Validate parameters + if (flashPages < 0) + return MICROBIT_INVALID_PARAMETER; + + // If we have a zero length, then dynamically determine our geometry. + if (flashStart == 0) + { + // Flash start is on the first page after the programmed ROM contents. + // This is: __etext (program code) + static data. + // Size of static data is calculated from __data_end__ and __data_start__ + // (See the linker script) + +#ifdef SIMULATOR + flashStart = (uint32_t) fs_mem; +#else + flashStart = (uint32_t)&__etext + ((uint32_t)&__data_end__ - (uint32_t)&__data_start__); + flashStart = ((uint32_t)flashStart & ~0x3FF) + PAGE_SIZE; +#endif + } + + if (flashPages == 0) + { +#ifdef SIMULATOR + flashPages = MBFS_PAGES; +#else + flashPages = (MBFS_LAST_PAGE_ADDR - flashStart) / PAGE_SIZE + 1; +#endif + } + + // The FileTable alays resides at the start of the file system. + fileSystemTable = (uint16_t *)flashStart; + + // First, try to load an existing file system at this location. + if (load() != MICROBIT_OK) + { + // No file system was found, so format a fresh one. + // Bring up a freshly formatted file system here. + fileSystemSize = flashPages * (PAGE_SIZE / MBFS_BLOCK_SIZE); + fileSystemTableSize = calculateFileTableSize(); + + format(); + } + + // indicate that we have a valid FileSystem + status = MBFS_STATUS_INITIALISED; + return MICROBIT_OK; +} + +/** +* Attempts to detect and load an exisitng file file system. +* +* @return MICROBIT_OK on success, or MICROBIT_NO_DATA if the file system could not be found. +*/ +int MicroBitFileSystem::load() +{ + uint16_t rootOffset = fileSystemTable[0]; + + // A valid MBFS has the first 'N' blocks set to the value 'N' followed by a valid root directory block with magic signature. + for (int i = 0; i < rootOffset; i++) + { + if (fileSystemTable[i] >= MBFS_EOF || fileSystemTable[i] != rootOffset) + return MICROBIT_NO_DATA; + } + + // Check for a valid signature at the start of the root directory + DirectoryEntry *root = (DirectoryEntry *) getBlock(rootOffset); + if (strcmp(root->file_name, MBFS_MAGIC) != 0) + return MICROBIT_NO_DATA; + + // We have a valid File System. Load the data... + fileSystemSize = root->length; + fileSystemTableSize = calculateFileTableSize(); + + return MICROBIT_OK; +} + + +/** +* Initialises a new file system. Assumes all pages are already erased. +* +* @return MICROBIT_OK on success, or an error code.. +*/ +int MicroBitFileSystem::format() +{ + uint16_t value = fileSystemTableSize; + + // Mark the FileTable blocks themselves as used. + for (uint16_t block = 0; block < fileSystemTableSize; block++) + flash.flash_write(&fileSystemTable[block], &value, 2); + + // Create a root directory + value = MBFS_EOF; + flash.flash_write(&fileSystemTable[fileSystemTableSize], &value, 2); + + // Store a MAGIC value in the first root directory entry. + // This will let us identify a valid File System later. + DirectoryEntry magic; + + strcpy(magic.file_name, MBFS_MAGIC); + magic.first_block = fileSystemTableSize; + magic.flags = MBFS_DIRECTORY_ENTRY_VALID; + magic.length = fileSystemSize; + + // Cache the root directory entry for later use. + rootDirectory = (DirectoryEntry *)getBlock(fileSystemTableSize); + flash.flash_write(rootDirectory, &magic, sizeof(DirectoryEntry)); + + return MICROBIT_OK; +} + +/** + * Retrieve the DirectoryEntry for the given filename. + * + * @param filename A fully or partially qualified filename. + * @param directory The directory to search. If ommitted, the root directory will be used. + * + * @return A pointer to the DirectoryEntry for the given file, or NULL if no entry is found. + */ +DirectoryEntry* MicroBitFileSystem::getDirectoryEntry(char const * filename, const DirectoryEntry *directory) +{ + Directory *dir; + char const *file; + uint16_t block; + DirectoryEntry *dirent; + + // Determine the filename from the (potentially) fully qualified filename. + file = filename + strlen(filename); + while (file >= filename && *file != '/') + file--; + file++; + + // Obtain a handle on the directory to search. + if (directory == NULL) + directory = rootDirectory; + + block = directory->first_block; + dir = (Directory *) getBlock(block); + dirent = &dir->entry[0]; + + // Iterate through the directory entries until we find our file, or run out of space. + while (1) + { + if ((uint32_t)(dirent + 1) > (uint32_t)dir + MBFS_BLOCK_SIZE) + { + block = getNextFileBlock(block); + if (block == MBFS_EOF) + return NULL; + + dir = (Directory *)getBlock(block); + dirent = &dir->entry[0]; + } + + // Check for a valid match + if (dirent->flags & MBFS_DIRECTORY_ENTRY_VALID && strcmp(dirent->file_name, file) == 0) + return dirent; + + // Move onto the next entry. + dirent++; + } + + return NULL; +} + +/** +* Determine the number of logical blocks required to hold the file table. +* +* @param blocks The number of blocks in the file system +* @ param blockSize The size of a logical block in the file system +* +* @return The number of logical blocks required to hold the file table. +*/ +uint16_t MicroBitFileSystem::calculateFileTableSize() +{ + uint16_t size = (fileSystemSize * 2) / MBFS_BLOCK_SIZE; + if ((fileSystemSize * 2) % MBFS_BLOCK_SIZE) + size++; + + return size; +} + +/** +* Retrieve a memory pointer for the start of the physical memory page containing the given block. +* +* @param block A valid block in a file. +* +* @return A pointer to the physical page in FLASH memory holding the given block. +*/ +uint32_t *MicroBitFileSystem::getPage(uint16_t block) +{ + uint32_t address = (uint32_t) getBlock(block); + return (uint32_t *) (address - address % PAGE_SIZE); +} + +/** +* Retrieve a memory pointer for the start of the given block. +* +* @param block A valid block in a file. +* +* @return A pointer to the FLASH memory associated with the given block. +*/ +uint32_t *MicroBitFileSystem::getBlock(uint16_t block) +{ + return (uint32_t *)((uint32_t)fileSystemTable + block * MBFS_BLOCK_SIZE); +} + +/** +* Retrieve the next block in a chain. +* +* @param block A valid block in a file. +* +* @return The block number of the next block in the file. +*/ +uint16_t MicroBitFileSystem::getNextFileBlock(uint16_t block) +{ + return fileSystemTable[block]; +} + +/** +* Determine the logical block that contains the given address. +* +* @param address A valid memory location within the file system space. +* +* @return The block number containing the given address. +*/ +uint16_t MicroBitFileSystem::getBlockNumber(void *address) +{ + return (((uint32_t) address - (uint32_t) fileSystemTable) / MBFS_BLOCK_SIZE); +} + +/* +* Update a file table entry to a given value. +* +* @param block The block to update. +* @param value The value to store in the file table. +* @return MICROBIT_OK on success. +*/ +int MicroBitFileSystem::fileTableWrite(uint16_t block, uint16_t value) +{ + flash.flash_write(&fileSystemTable[block], &value, 2); + return MICROBIT_OK; +} + + + +/** +* Retrieve the DirectoryEntry for the given filename. +* +* @param filename A fully qualified filename, from the root. Should be end with a "/" if no filename is provided. +* +* @return A pointer to the DirectoryEntry for the given file, or NULL if no entry is found. +*/ +DirectoryEntry* MicroBitFileSystem::getDirectoryOf(char const * filename) +{ + DirectoryEntry* directory; + + // If not path is provided, return the root diretory. + if (filename == NULL || filename[0] == 0) + return rootDirectory; + + char s[MBFS_FILENAME_LENGTH + 1]; + + uint8_t i = 0; + + directory = rootDirectory; + + while (*filename != '\0') { + if (*filename == '/') { + s[i] = '\0'; + + // Ensure each level of the filename is valid + if (i == 0 || i > MBFS_FILENAME_LENGTH + 1) + return NULL; + + // Extract the relevant entry from the directory. + directory = getDirectoryEntry(s, directory); + + // If file / directory does not exist, then there's nothing more we can do. + if (!directory) + return NULL; + + i = 0; + } + else + s[i++] = *filename; + + filename++; + } + + return directory; +} + +/** +* Refresh the physical page associated with the given block. +* Any logical blocks marked for deletion on that page are recycled. +* +* @param block the block to recycle. +* @param isDirectory true if the entry being created is itself a directory. +* +* @return MICROBIT_OK on success. +*/ +int MicroBitFileSystem::recycleBlock(uint16_t block, int type) +{ + uint32_t *page = getPage(block); + uint32_t* scratch = getFreePage(); + uint8_t *write = (uint8_t *)scratch; + uint16_t b = getBlockNumber(page); + + for (int i = 0; i < PAGE_SIZE / MBFS_BLOCK_SIZE; i++) + { + // If we have an unused or deleted block, there's nothing to do - allow the block to be recycled. + if (fileSystemTable[b] == MBFS_DELETED || fileSystemTable[b] == MBFS_UNUSED) + {} + + // If we have been asked to recycle a valid directory block, recycle individual entries where possible. + else if (b == block && type == MBFS_BLOCK_TYPE_DIRECTORY) + { + DirectoryEntry *direntIn = (DirectoryEntry *)getBlock(b); + DirectoryEntry *direntOut = (DirectoryEntry *)write; + + for (int entry = 0; entry < MBFS_BLOCK_SIZE / sizeof(DirectoryEntry); entry++) + { + if (direntIn->flags & MBFS_DIRECTORY_ENTRY_VALID) + flash.flash_write((uint32_t *)direntOut, (uint32_t *)direntIn, sizeof(DirectoryEntry)); + + direntIn++; + direntOut++; + } + } + + // All blocks before the root directory are the FileTable. + // Recycle any entries marked as DELETED to UNUSED. + else if (getBlock(b) < (uint32_t *)rootDirectory) + { + uint16_t *tableIn = (uint16_t *)getBlock(b); + uint16_t *tableOut = (uint16_t *)write; + + for (int entry = 0; entry < MBFS_BLOCK_SIZE / 2; entry++) + { + if (*tableIn != MBFS_DELETED) + flash.flash_write(tableOut, tableIn, 2); + + tableIn++; + tableOut++; + } + } + + // Copy all other VALID blocks directly into the scratch page. + else + flash.flash_write(write, getBlock(b), MBFS_BLOCK_SIZE); + + // move on to next block. + write += MBFS_BLOCK_SIZE; + b++; + } + + // Now refresh the page originally holding the block. + flash.erase_page(page); + flash.flash_write(page, scratch, PAGE_SIZE); + + // Recycle the scratch page for later use. + flash.erase_page(scratch); + + return MICROBIT_OK; +} + +/** +* Refresh the physical pages associated with the file table. +* Any logical blocks marked for deletion on those pages are recycled back to UNUSED. +** +* @return MICROBIT_OK on success. +*/ +int MicroBitFileSystem::recycleFileTable() +{ + bool pageRecycled = false; + + for (uint16_t block = 0; block < fileSystemSize; block++) + { + // if we just crossed a page boundary, reset pageRecycled. + if (block % (PAGE_SIZE / MBFS_BLOCK_SIZE) == 0) + pageRecycled = false; + + if (fileSystemTable[block] == MBFS_DELETED && !pageRecycled) + { + recycleBlock(block); + pageRecycled = true; + } + } + + // now, recycle the FileSystemTable itself, upcycling entries marked as DELETED to UNUSED as we go. + for (uint16_t block = 0; getPage(block) < (uint32_t *)rootDirectory; block += PAGE_SIZE / MBFS_BLOCK_SIZE) + recycleBlock(block); + + return MICROBIT_OK; +} + + +/** +* Allocate a free DiretoryEntry in the given directory, extending and refreshing the directory block if necessary. +* +* @param directory The directory to add a DirectoryEntry to +* @return A pointer to the new DirectoryEntry for the given file, or NULL if it was not possible to allocated resources. +*/ +DirectoryEntry* MicroBitFileSystem::createDirectoryEntry(DirectoryEntry *directory) +{ + Directory *dir; + uint16_t block; + DirectoryEntry *dirent; + DirectoryEntry *empty = NULL; + DirectoryEntry *invalid = NULL; + + // Try to find an unused entry in the directory. + block = directory->first_block; + dir = (Directory *)getBlock(block); + dirent = &dir->entry[0]; + + // Iterate through the directory entries until we find and unused entry, or run out of space. + while (1) + { + // Scan through each of the blocks in the directory + if ((uint32_t)(dirent+1) > (uint32_t)dir + MBFS_BLOCK_SIZE) + { + block = getNextFileBlock(block); + if (block == MBFS_EOF) + break; + + dir = (Directory *)getBlock(block); + dirent = &dir->entry[0]; + } + + // If we find an empty slot, use that. + if (dirent->flags & MBFS_DIRECTORY_ENTRY_FREE) + { + empty = dirent; + break; + } + + // Record the first invalid block we find (used, but then deleted). + if ((dirent->flags & MBFS_DIRECTORY_ENTRY_VALID) == 0 && invalid == NULL) + invalid = dirent; + + // Move onto the next entry. + dirent++; + } + + + // Now choose the best available slot, giving preference to entries that would void a FLASH page erase opreation. + dirent = NULL; + + // Ideally, choose an unused entry within an existing block. + if (empty) + { + dirent = empty; + } + + // if not possible, try to re-use a second-hand block that has been freed. This will result in an erase operation of the block, + // but will not consume any more resources. + else if (invalid) + { + dirent = invalid; + uint16_t b = getBlockNumber(dirent); + recycleBlock(b, MBFS_BLOCK_TYPE_DIRECTORY); + } + + // If nothing is available, extend the directory with a new block. + else + { + // Allocate a new logical block + uint16_t newBlock = getFreeBlock(); + if (newBlock == 0) + return NULL; + + // Append this to the directory + uint16_t lastBlock = directory->first_block; + while (getNextFileBlock(lastBlock) != MBFS_EOF) + lastBlock = getNextFileBlock(lastBlock); + + // Append the block. + fileTableWrite(lastBlock, newBlock); + fileTableWrite(newBlock, MBFS_EOF); + + dirent = (DirectoryEntry *)getBlock(newBlock); + } + + return dirent; +} + +/** +* Create a new DirectoryEntry with the given filename and flags. +* +* @param filename A fully or partially qualified filename. +* @param directory The directory in which to create the entry +* @param isDirectory true if the entry being created is itself a directory +* +* @return A pointer to the new DirectoryEntry for the given file, or NULL if it was not possible to allocated resources. +*/ +DirectoryEntry* MicroBitFileSystem::createFile(char const * filename, DirectoryEntry *directory, bool isDirectory) +{ + char const *file; + DirectoryEntry *dirent; + + // Determine the filename from the (potentially) fully qualified filename. + file = filename + strlen(filename); + while (file >= filename && *file != '/') + file--; + file++; + + // Allocate a directory entry for our new file. + dirent = createDirectoryEntry(directory); + if (dirent == NULL) + return NULL; + + // Create a new block to represent the file. + uint16_t newBlock = getFreeBlock(); + if (newBlock == 0) + return NULL; + + // Populate our assigned Directory Entry. + DirectoryEntry d; + strcpy(d.file_name, file); + d.first_block = newBlock; + + if (isDirectory) + { + // Mark as a directory, and set a zero length (special case for directories, to minimize unecessary FLASH erases). + d.flags = MBFS_DIRECTORY_ENTRY_VALID | MBFS_DIRECTORY_ENTRY_DIRECTORY; + d.length = 0; + } + else + { + // We leave the file size as unwritten for regular files - pending a possible forthcoming write/close operation. + d.flags = MBFS_DIRECTORY_ENTRY_NEW; + d.length = 0xffffffff; + } + + // Push the new data back to FLASH memory + flash.flash_write(dirent, &d, sizeof(DirectoryEntry)); + fileTableWrite(d.first_block, MBFS_EOF); + return dirent; +} + +/** +* Searches the list of open files for one with the given identifier. +* +* @param fd A previsouly opened file identifier, as returned by open(). +* @param remove Remove the file descriptor from the list if true. +* @return A FileDescriptor matching the given ID, or NULL if the file is not open. +*/ +FileDescriptor* MicroBitFileSystem::getFileDescriptor(int fd, bool remove) +{ + FileDescriptor *file = openFiles; + FileDescriptor *prev = NULL; + + while (file) + { + if (file->id == fd) + { + if (remove) + { + if (prev) + prev->next = file->next; + else + openFiles = file->next; + } + return file; + } + + prev = file; + file = file->next; + } + + return NULL; +} + +/** + * Creates a new directory with the given name and location + * + * @param name The fully qualified name of the new directory. + * @return MICROBIT_OK on success, MICROBIT_INVALID_PARAMETER if the path is invalid, or MICROBT_NO_RESOURCES if the FileSystem is full. + */ +int MicroBitFileSystem::createDirectory(char const *name) +{ + DirectoryEntry* directory; // Directory holding this file. + DirectoryEntry* dirent; // Entry in the direcoty of this file. + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Reject invalid filenames. + if (name == NULL || strlen(name) == 0) + return MICROBIT_INVALID_PARAMETER; + + // Determine the directory for this file. + directory = getDirectoryOf(name); + + if (directory == NULL) + return MICROBIT_INVALID_PARAMETER; + + // Find the DirectoryEntry associated with the given name (if it exists). + // We don't permit files or directories with the same name. + dirent = getDirectoryEntry(name, directory); + + if (dirent) + return MICROBIT_INVALID_PARAMETER; + + dirent = createFile(name, directory, true); + if (dirent == NULL) + return MICROBIT_NO_RESOURCES; + + return MICROBIT_OK; +} + + +int MicroBitFileSystem::open(char const * filename, uint32_t flags) +{ + FileDescriptor *file; // File Descriptor of this file. + DirectoryEntry* directory; // Directory holding this file. + DirectoryEntry* dirent; // Entry in the direcoty of this file. + int id; // FileDescriptor id to be return to the caller. + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Reject invalid filenames. + if (filename == NULL || strlen(filename) == 0) + return MICROBIT_INVALID_PARAMETER; + + // Determine the directory for this file. + directory = getDirectoryOf(filename); + + if (directory == NULL) + return MICROBIT_INVALID_PARAMETER; + + // Find the DirectoryEntry assoviate with the given file (if it exists). + dirent = getDirectoryEntry(filename, directory); + + // Only permit files to be opened once... + // also, detemrine a valid ID for this open file as we go. + file = openFiles; + id = 0; + + while (file && dirent) + { + if (file->dirent == dirent) + return MICROBIT_NOT_SUPPORTED; + + if (file->id == id) + { + id++; + file = openFiles; + continue; + } + + file = file->next; + } + + if (dirent == NULL) + { + // If the file doesn't exist, and we haven't been asked to create it, then there's nothing we can do. + if (!(flags & MB_CREAT)) + return MICROBIT_INVALID_PARAMETER; + + dirent = createFile(filename, directory, false); + if (dirent == NULL) + return MICROBIT_NO_RESOURCES; + } + + // Try to add a new FileDescriptor into this directory. + file = new FileDescriptor; + if (file == NULL) + return MICROBIT_NO_RESOURCES; + + // Populate the FileDescriptor + file->flags = (flags & ~(MB_CREAT)); + file->id = id; + file->seek = (flags & MB_APPEND) ? dirent->length : 0; + file->length = dirent->flags == MBFS_DIRECTORY_ENTRY_NEW ? 0 : dirent->length; + file->dirent = dirent; + file->directory = directory; + file->cacheLength = 0; + + // Add the file descriptor to the chain of open files. + file->next = openFiles; + openFiles = file; + + // Return the FileDescriptor id to the user + return file->id; +} + + +/** + * Close the specified file handle. + * File handle resources are then made available for future open() calls. + * + * close() must be called at some point to ensure the filesize in the + * FT is synced with the cached value in the FD. + * + * @warning if close() is not called, the FT may not be correct, + * leading to data loss. + * + * @param fd file descriptor - obtained with open(). + * @return MICROBIT_OK on success, MICROBIT_NOT_SUPPORTED if the file system has not + * been initialised, MICROBIT_INVALID_PARAMETER if the given file handle + * is invalid. + * + * @code + * MicroBitFileSystem f(); + * int fd = f.open("test.txt", MB_READ); + * if(!f.close(fd)) + * print("error closing file."); + * @endcode + */ +int MicroBitFileSystem::close(int fd) +{ + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + FileDescriptor *file = getFileDescriptor(fd, true); + + // Ensure the file is open. + if(file == NULL) + return NULL; + + // Flush any data in the writeback cache. + writeBack(file); + + // If the file has changed size, create an updated directory entry for the file, reflecting it's new length. + if (file->dirent->length != file->length) + { + DirectoryEntry d = *file->dirent; + d.length = file->length; + + // Do some optimising to reduce FLASH churn if this is the first write to a file. No need then to create a new dirent... + if (file->dirent->flags & MBFS_DIRECTORY_ENTRY_NEW) + { + d.flags = MBFS_DIRECTORY_ENTRY_VALID; + flash.flash_write(file->dirent, &d, sizeof(DirectoryEntry)); + } + + // Otherwise, replace the dirent with a freshly allocated one, and mark the other as INVALID. + else + { + DirectoryEntry *newDirent; + uint16_t value = MBFS_DELETED; + + // invalidate the old directory entry and create a new one with the updated data. + flash.flash_write(&file->dirent->flags, &value, 2); + newDirent = createDirectoryEntry(file->directory); + flash.flash_write(newDirent, &d, sizeof(DirectoryEntry)); + } + } + + delete file; + return MICROBIT_OK; +} + +/** + * Move the current position of a file handle, to be used for + * subsequent read/write calls. + * + * The offset modifier can be: + * - MB_SEEK_SET set the absolute seek position. + * - MB_SEEK_CUR set the seek position based on the current offset. + * - MB_SEEK_END set the seek position from the end of the file. + * E.g. to seek to 2nd-to-last byte, use offset=-1. + * + * @param fd file handle, obtained with open() + * @param offset new offset, can be positive/negative. + * @param flags + * @return new offset position on success, MICROBIT_NOT_SUPPORTED if the file system + * is not intiialised, MICROBIT_INVALID_PARAMETER if the flag given is invalid + * or the file handle is invalid. + * + * @code + * MicroBitFileSystem f; + * int fd = f.open("test.txt", MB_READ); + * f.seek(fd, -100, MB_SEEK_END); //100 bytes before end of file. + * @endcode + */ +int MicroBitFileSystem::seek(int fd, int offset, uint8_t flags) +{ + FileDescriptor *file; + uint32_t position; + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Ensure the file is open. + file = getFileDescriptor(fd); + + if (file == NULL) + return MICROBIT_INVALID_PARAMETER; + + // Flush any data in the writeback cache. + writeBack(file); + + position = file->seek; + + if(flags == MB_SEEK_SET) + position = offset; + + if(flags == MB_SEEK_END) + position = file->length + offset; + + if (flags == MB_SEEK_CUR) + position = file->seek + offset; + + if (position < 0 || position > file->length) + return MICROBIT_INVALID_PARAMETER; + + file->seek = position; + + return position; +} + +/** + * Read data from the file. + * + * Read len bytes from the current seek position in the file, into the + * buffer. On each invocation to read, the seek position of the file + * handle is incremented atomically, by the number of bytes returned. + * + * @param fd File handle, obtained with open() + * @param buffer to store data + * @param len number of bytes to read + * @return number of bytes read on success, MICROBIT_NOT_SUPPORTED if the file + * system is not initialised, or this file was not opened with the + * MB_READ flag set, MICROBIT_INVALID_PARAMETER if the given file handle + * is invalid. + * + * @code + * MicroBitFileSystem f; + * int fd = f.open("read.txt", MB_READ); + * if(f.read(fd, buffer, 100) != 100) + * print("read error"); + * @endcode + */ +int MicroBitFileSystem::read(int fd, uint8_t* buffer, int size) +{ + FileDescriptor *file; + uint16_t block; + uint8_t *readPointer; + uint8_t *writePointer; + + uint32_t offset; + uint32_t position = 0; + int bytesCopied = 0; + int segmentLength; + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Ensure the file is open. + file = getFileDescriptor(fd); + + if (file == NULL || buffer == NULL || size == 0) + return MICROBIT_INVALID_PARAMETER; + + // Flush any data in the writeback cache before we change the seek pointer. + writeBack(file); + + // Validate the read length. + size = min(size, file->length - file->seek); + + // Find the read position. + block = file->dirent->first_block; + + // Walk the file table until we reach the start block + while (file->seek - position > MBFS_BLOCK_SIZE) + { + block = getNextFileBlock(block); + position += MBFS_BLOCK_SIZE; + } + + // Once we have the correct start block, handle the byte offset. + offset = file->seek - position; + + // Now, start copying bytes into the requested buffer. + writePointer = buffer; + while (bytesCopied < size) + { + // First, determine if we need to write a partial block. + readPointer = (uint8_t *)getBlock(block) + offset; + segmentLength = min(size - bytesCopied, MBFS_BLOCK_SIZE - offset); + + if(segmentLength > 0) + memcpy(writePointer, readPointer, segmentLength); + + bytesCopied += segmentLength; + writePointer += segmentLength; + offset += segmentLength; + + if (offset == MBFS_BLOCK_SIZE) + { + block = getNextFileBlock(block); + offset = 0; + } + } + + file->seek += bytesCopied; + + return bytesCopied; +} + +/** +* Flush a given file's cache back to FLASH memory. +* +* @param file File descriptor to flush. +* @return The number of bytes written. +* +*/ +int MicroBitFileSystem::writeBack(FileDescriptor *file) +{ + if (file->cacheLength) + { + int r = writeBuffer(file, file->cache, file->cacheLength); + file->cacheLength = 0; + return r; + } + + return 0; +} + +/** +* Write a given buffer to the file provided. +* +* @param file FileDescriptor of the file to write +* @param buffer The start of the buffer to write +* @param length The number of bytes to write +*/ +int MicroBitFileSystem::writeBuffer(FileDescriptor *file, uint8_t *buffer, int size) +{ + uint16_t block, newBlock; + uint8_t *readPointer; + uint8_t *writePointer; + + uint32_t offset; + uint32_t position = 0; + int bytesCopied = 0; + int segmentLength; + + // Find the read position. + block = file->dirent->first_block; + + // Walk the file table until we reach the start block + while (file->seek - position > MBFS_BLOCK_SIZE) + { + block = getNextFileBlock(block); + position += MBFS_BLOCK_SIZE; + } + + // Once we have the correct start block, handle the byte offset. + offset = file->seek - position; + writePointer = (uint8_t *)getBlock(block) + offset; + + // Now, start copying bytes from the requested buffer. + readPointer = buffer; + while (bytesCopied < size) + { + // First, determine if we need to write a partial block. + segmentLength = min(size - bytesCopied, MBFS_BLOCK_SIZE - offset); + + if (segmentLength != 0) + flash.flash_write(writePointer, readPointer, segmentLength); + + offset += segmentLength; + bytesCopied += segmentLength; + readPointer += segmentLength; + + if (offset == MBFS_BLOCK_SIZE && bytesCopied < size) + { + newBlock = getFreeBlock(); + if (newBlock == 0) + break; + + fileTableWrite(newBlock, MBFS_EOF); + fileTableWrite(block, newBlock); + + block = newBlock; + + writePointer = (uint8_t *)getBlock(block); + offset = 0; + } + } + + // update the filelength metadata and seek position such that multiple writes are sequential. + file->length = max(file->length, file->seek + bytesCopied); + file->seek += bytesCopied; + + return bytesCopied; +} + + +/** + * Write data to the file. + * + * Write from buffer, len bytes to the current seek position. + * On each invocation to write, the seek position of the file handle + * is incremented atomically, by the number of bytes returned. + * + * The cached filesize in the FD is updated on this call. Also, the + * FT file size is updated only if a new page(s) has been written too, + * to reduce the number of FT writes. + * + * @param fd File handle + * @param buffer the buffer from which to write data + * @param len number of bytes to write + * @return number of bytes written on success, MICROBIT_NO_RESOURCES if data did + * not get written to flash or the file system has not been initialised, + * or this file was not opened with the MB_WRITE flag set, MICROBIT_INVALID_PARAMETER + * if the given file handle is invalid. + * + * @code + * MicroBitFileSystem f(); + * int fd = f.open("test.txt", MB_WRITE); + * if(f.write(fd, "hello!", 7) != 7) + * print("error writing"); + * @endcode + */ +int MicroBitFileSystem::write(int fd, uint8_t* buffer, int size) +{ + FileDescriptor *file; + int bytesCopied = 0; + int segmentSize; + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Ensure the file is open. + file = getFileDescriptor(fd); + + if (file == NULL || buffer == NULL || size == 0) + return MICROBIT_INVALID_PARAMETER; + + // Determine how to handle the write. If the buffer size is less than our cache size, + // write the data via the cache. Otherwise, a direct write through is likely more efficient. + // This may take a few iterations if the cache is already quite full. + if (size < MBFS_CACHE_SIZE) + { + while (bytesCopied < size) + { + segmentSize = min(size, MBFS_CACHE_SIZE - file->cacheLength); + memcpy(&file->cache[file->cacheLength], buffer, segmentSize); + + file->cacheLength += segmentSize; + bytesCopied += segmentSize; + + if (file->cacheLength == MBFS_CACHE_SIZE) + writeBack(file); + + + } + + return bytesCopied; + } + + // If we have a relatively large block, then write it directly ( + writeBack(file); + + return writeBuffer(file, buffer, size); +} + +/** + * Remove a file from the system, and free allocated assets + * (including assigned blocks which are returned for use by other files). + * + * @param filename null-terminated name of the file to remove. + * @return MICROBIT_OK on success, MICROBIT_INVALID_PARAMETER if the given filename + * does not exist, MICROBIT_CANCELLED if something went wrong. + * + * @code + * MicroBitFileSystem f; + * if(!f.remove("file.txt")) + * print("file could not be removed") + * @endcode + */ +int MicroBitFileSystem::remove(char const * filename) +{ + int fd = open(filename, MB_READ); + uint16_t block, nextBlock; + uint16_t value; + + // If the file can't be opened, then it is impossible to delete. Pass through any error codes. + if (fd < 0) + return fd; + + FileDescriptor *file = getFileDescriptor(fd, true); + + // To erase a file, all we need to do is mark its directory entry and data blocks as INVALID. + // First mark the file table + block = file->dirent->first_block; + while (block != MBFS_EOF) + { + nextBlock = fileSystemTable[block]; + fileTableWrite(block, MBFS_DELETED); + block = nextBlock; + } + + // Mark the directory entry of this file as invalid. + value = MBFS_DIRECTORY_ENTRY_DELETED; + flash.flash_write(&file->dirent->flags, &value, 2); + + // release file metadata + delete file; + + return MICROBIT_OK; +} + +void MicroBitFileSystem::debugFAT() +{ + int index = 0; + printf("------ FAT ------\n"); + for (int j = 0; j < fileSystemSize / 16; j++) + { + for (int i = 0; i < 16; i++) + { + if (fileSystemTable[index] == MBFS_UNUSED) + printf(" ---- "); + + else if (fileSystemTable[index] == MBFS_EOF) + printf(" _EOF "); + + else if (fileSystemTable[index] == MBFS_DELETED) + printf(" _XX_ "); + else + printf(" %.4X ", fileSystemTable[index]); + + index++; + } + printf("\n"); + } + + printf("\n\n"); +} + +void MicroBitFileSystem::debugRootDirectory() +{ + debugDirectory(rootDirectory); +} + +int MicroBitFileSystem::debugDirectory(char *name) +{ + DirectoryEntry* directory; // Directory holding this file. + DirectoryEntry* dirent; // Entry in the direcoty of this file. + + // Protect against accidental re-initialisation + if ((status & MBFS_STATUS_INITIALISED) == 0) + return MICROBIT_NOT_SUPPORTED; + + // Reject invalid filenames. + if (name == NULL || strlen(name) == 0) + return MICROBIT_INVALID_PARAMETER; + + // Determine the directory for this file. + directory = getDirectoryOf(name); + + if (directory == NULL) + return MICROBIT_INVALID_PARAMETER; + + // Find the DirectoryEntry associated with the given name (if it exists). + // We don't permit files or directories with the same name. + dirent = getDirectoryEntry(name, directory); + + if (dirent == NULL) + return MICROBIT_INVALID_PARAMETER; + + debugDirectory(dirent); + + return MICROBIT_OK; +} + +void MicroBitFileSystem::debugDirectory(DirectoryEntry *directory) +{ + int index = 0; + int block = directory->first_block; + Directory *dir; + + char *STATUS, *NAME, *TYPE; + + printf("--- DIRECTORY: %s ---\n", directory->file_name); + + while (block != MBFS_EOF) + { + dir = (Directory *)getBlock(block); + + for (int i = 0; i < MBFS_BLOCK_SIZE / sizeof(DirectoryEntry); i++) { + + STATUS = " "; + NAME = ""; + TYPE = ""; + + if (dir->entry[i].flags == MBFS_DIRECTORY_ENTRY_NEW) + { + STATUS = ""; + } + + else if (dir->entry[i].flags == MBFS_DIRECTORY_ENTRY_DELETED) + { + STATUS = ""; + NAME = dir->entry[i].file_name; + } + else if (dir->entry[i].flags & MBFS_DIRECTORY_ENTRY_VALID) + { + STATUS = ""; + NAME = dir->entry[i].file_name; + } + + if (dir->entry[i].flags & MBFS_DIRECTORY_ENTRY_DIRECTORY) + TYPE = " "; + + if ((int) dir->entry[i].length == -1) + { + STATUS = " "; + NAME = ""; + TYPE = ""; + } + + printf(" %s [type: %s] [status: %s] [length: %d] [start: %.4X]\n", NAME, TYPE, STATUS, dir->entry[i].length, dir->entry[i].first_block); + } + + block = getNextFileBlock(block); + } + + printf("\n\n"); + +} \ No newline at end of file diff --git a/source/drivers/MicroBitFlash.cpp b/source/drivers/MicroBitFlash.cpp new file mode 100644 index 0000000..95be700 --- /dev/null +++ b/source/drivers/MicroBitFlash.cpp @@ -0,0 +1,277 @@ +#include "MicroBitFlash.h" +#include "mbed.h" // NVIC + +#define MIN(a,b) ((a)<(b)?(a):(b)) + +#define WORD_ADDR(x) (((uint32_t)x) & 0xFFFFFFFC) + +/** + * Default Constructor + */ +MicroBitFlash::MicroBitFlash() +{ +} + +/** + * Check if an erase is required to write to a region in flash memory. + * This is determined if, for any byte: + * ~O & N != 0x00 + * Where O=Original byte, N = New byte. + * + * @param source to write from + * @param flash_address to write to + * @param len number of uint8_t to check. + * @return non-zero if erase required, zero otherwise. + */ +int MicroBitFlash::need_erase(uint8_t* source, uint8_t* flash_addr, int len) +{ + // Erase is necessary if for any byte: + // O & ~N != 0 + // Where O = original, and N = new byte. + + for(;len>0;len--) + { + if((~*(flash_addr++) & *(source++)) != 0x00) return 1; + } + return 0; +} + +/** + * Erase an entire page + * @param page_address address of first word of page + */ +void MicroBitFlash::erase_page(uint32_t* pg_addr) +{ + + // Turn on flash erase enable and wait until the NVMC is ready: + NRF_NVMC->CONFIG = (NVMC_CONFIG_WEN_Een); + while (NRF_NVMC->READY == NVMC_READY_READY_Busy) { } + + // Erase page: + NRF_NVMC->ERASEPAGE = (uint32_t)pg_addr; + while (NRF_NVMC->READY == NVMC_READY_READY_Busy) { } + + // Turn off flash erase enable and wait until the NVMC is ready: + NRF_NVMC->CONFIG = (NVMC_CONFIG_WEN_Ren << NVMC_CONFIG_WEN_Pos); + while (NRF_NVMC->READY == NVMC_READY_READY_Busy) { } +} + +/** + * Write to flash memory, assuming that a write is valid + * (using need_erase). + * + * @param page_address address of memory to write to. + * Must be word aligned. + * @param buffer address to write from, must be word-aligned. + * @param len number of uint32_t words to write. + */ +void MicroBitFlash::flash_burn(uint32_t* addr, uint32_t* buffer, int size) +{ + + // Turn on flash write enable and wait until the NVMC is ready: + NRF_NVMC->CONFIG = (NVMC_CONFIG_WEN_Wen << NVMC_CONFIG_WEN_Pos); + while (NRF_NVMC->READY == NVMC_READY_READY_Busy) {}; + + for(int i=0;iREADY == NVMC_READY_READY_Busy) {}; + } + + // Turn off flash write enable and wait until the NVMC is ready: + NRF_NVMC->CONFIG = (NVMC_CONFIG_WEN_Ren << NVMC_CONFIG_WEN_Pos); + while (NRF_NVMC->READY == NVMC_READY_READY_Busy) {}; +} + +/** + * Write to address in flash, implementing either flash_write (copy + * data from buffer), or flash_memset (set all bytes in flash to + * provided constant. + * + * Function ensures that data is written correctly: + * - erase page if necessary (using need_erase) + * - preserve non-target flash by copying to scratch page. + * + * flash_write_mem assumes that the provided scratch page is already clean, + * i.e. all bytes = 0xFF. The page is erased after use, all bytes reset to + * 0xFF. + * + * @param address in flash to write to + * @param from_buffer address to write data from + * @param write_byte if writing the same byte to add addressed bytes + * @param len number of bytes to write to/copy to + * @param m mode to use this function, memset/memcpy.$a + * @param scratch_addr scratch page address (must be page-aligned) + * @return non-zero on success, zero on error. + */ +int MicroBitFlash::flash_write_mem(uint8_t* address, uint8_t* from_buffer, + uint8_t write_byte, int length, flash_mode m, uint8_t* scratch_addr) +{ + + // Check that scratch_addr is aligned on a page boundary. + if((uint32_t)scratch_addr & 0x3FF) + { + return 0; + } + + // page number. + int page = (uint32_t)address / PAGE_SIZE; + + //page address. + uint32_t* pgAddr = (uint32_t*)(page * PAGE_SIZE); + + // offset to write from within page. + int offset = (uint32_t)address % PAGE_SIZE; + + // uBit.serial.printf("flash_write to 0x%x, from 0x%x, length: 0x%x\n", + // address, from_buffer, length); + // uBit.serial.printf(" - offset = %d, pgAddr = 0x%x, page = %d\n", + // offset, pgAddr, page); + + uint8_t* writeFrom = (uint8_t*)pgAddr; + int start = WORD_ADDR(offset); + int end = WORD_ADDR((offset+length+4)); + int erase = need_erase(from_buffer, address, length); + + // Preserve the data by writing to the scratch page. + if(erase) + { + this->flash_burn((uint32_t*)scratch_addr, pgAddr, PAGE_SIZE/4); + this->erase_page(pgAddr); + writeFrom = (uint8_t*)scratch_addr; + start = 0; + end = PAGE_SIZE; + } + + uint32_t writeWord = 0; + + for(int i=start;i= offset && i < (offset + length)) + { + if(m == WR_WRITE) + { + // Write from buffer. + writeWord |= (from_buffer[i-offset] << ((byteOffset)*8)); + } + else if(m == WR_MEMSET) + { + // Write constant. + writeWord |= ((uint32_t)write_byte << (byteOffset*8)); + } + } + else + { + writeWord |= (writeFrom[i] << ((byteOffset)*8)); + } + + if( ((i+1)%4) == 0) + { + this->flash_burn(pgAddr + (i/4), &writeWord, 1); + writeWord = 0; + } + } + + // If the scratch page was used, reset it to 0xFF. + if(erase) + { + this->erase_page((uint32_t*)scratch_addr); + } + + return 1; +} + +/** + * Writes the given number of bytes to the address in flash specified. + * Neither address nor buffer need be word-aligned. + * @param address location in flash to write to. + * @param buffer location in memory to write from. + * @length number of bytes to burn + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on sucess, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * uint32_t word = 0x01; + * flash.flash_write((uint8_t*)0x38000, &word, sizeof(word)) + * @endcode + */ +int MicroBitFlash::flash_write(uint8_t* address, uint8_t* from_buffer, + int length, uint8_t* scratch_addr) +{ + if(scratch_addr == NULL) + { + return this->flash_write_mem(address, from_buffer, 0, length, + WR_WRITE,(uint8_t*)DEFAULT_SCRATCH_ADDR); + } + else + { + return this->flash_write_mem(address, from_buffer, 0, length, + WR_WRITE, scratch_addr); + } +} + +/** + * Set bytes in [address, address+length] to byte. + * Neither address nor buffer need be word-aligned. + * @param address to write to + * @param byte byte to burn to flash + * @param length number of bytes to write to. + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on success, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * flash.flash_memset((uint8_t*)0x38000, 0x66, 12); //12 bytes to 0x66. + * @endcode + */ +int MicroBitFlash::flash_memset(uint8_t* address, uint8_t write_byte, + int length, uint8_t* scratch_addr) +{ + if(scratch_addr == NULL) + { + return this->flash_write_mem(address, NULL, write_byte, length, + WR_MEMSET,(uint8_t*)DEFAULT_SCRATCH_ADDR); + } + else + { + return this->flash_write_mem(address, NULL, write_byte, length, + WR_MEMSET, scratch_addr); + } +} + +/** + * Erase bytes in memory, from set address. + * @param address to erase bytes from (needn't be word-aligned. + * @param length number of bytes to erase (set to 0xFF). + * @param scratch_addr if specified, scratch page to use. Use default + * otherwise. + * @return non-zero on success, zero on error. + * + * Example: + * @code + * MicroBitFlash flash(); + * flash.flash_erase((uint8_t*)0x38000, 10240); //erase a flash page. + * @endcode + */ +int MicroBitFlash::flash_erase_mem(uint8_t* address, int length, + uint8_t* scratch_addr) +{ + if(scratch_addr == NULL) + { + return this->flash_write_mem(address, NULL, 0xFF, length, + WR_MEMSET,(uint8_t*)DEFAULT_SCRATCH_ADDR); + } + else + { + return this->flash_write_mem(address, NULL, 0xff, length, + WR_MEMSET, scratch_addr); + } +} +