/* Theory of operation: An appfs filesystem is meant to store executable applications (=ESP32 programs) alongside other data that is mmap()-able as a contiguous file. Appfs does that by making sure the files rigidly adhere to the 64K-page-structure (called a 'sector' in this description) as predicated by the ESP32s MMU. This way, every file can be mmap()'ed into a contiguous region or ran as an ESP32 application. (For the future, maybe: Smaller files can be stored in parts of a 64K page, as long as all are contiguous and none cross any 64K boundaries. What about fragmentation tho?) Because of these reasons, only a few operations are available: - Creating a file. This needs the filesize to be known beforehand; a file cannot change size afterwards. - Modifying a file. This follows the same rules as spi_flash_* because it maps directly to the underlying flash. - Deleting a file - Mmap()ping a file This makes the interface to appfs more akin to the partition interface than to a real filesystem. At the moment, appfs is not yet tested with encrypted flash; compatibility is unknown. Filesystem meta-info is stored using the first sector: there are 2 32K half-sectors there with management info. Each has a serial and a checksum. The sector with the highest serial and a matching checksum is taken as current; the data will ping-pong between the sectors. (And yes, this means the pages in these sectors will be rewritten every time a file is added/removed. Appfs is built with the assumption that it's a mostly store-only filesystem and apps will only change every now and then. The flash chips connected to the ESP32 chips usually can do up to 100.000 erases, so for most purposes the lifetime of the flash with appfs on it exceeds the lifetime of the product.) Appfs assumes a partition of 16MiB or less, allowing for 256 128-byte sector descriptors to be stored in the management half-sectors. The first descriptor is a header used for filesystem meta-info. Metainfo is stored per sector; each sector descriptor contains a zero-terminated filename (no directories are supported, but '/' is an usable character), the size of the file and a pointer to the next entry for the file if needed. The filename is only set for the first sector; it is all zeroes (actually: ignored) for other entries. Integrity of the meta-info is guaranteed: the file system will never be in a state where sectors are lost or anything. Integrity of data is *NOT* guaranteed: on power loss, data may be half-written, contain sectors with only 0xff, and so on. It's up to the user to take care of this. However, files that are not written to do not run the risk of getting corrupted. With regards to this code: it is assumed that an ESP32 will only have one appfs on flash, so everything is implemented as a singleton. */ #include #include #include #include #include #include #include "esp_spi_flash.h" #include "esp_partition.h" #include "esp_log.h" #include "esp_err.h" #include "appfs.h" #include "rom/cache.h" #include "sdkconfig.h" #if !CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED #error "Appfs will not work with SPI flash dangerous regions checking. Please use 'make menuconfig' to enable writing to dangerous regions." #endif static const char *TAG = "appfs"; #define APPFS_SECTOR_SZ SPI_FLASH_MMU_PAGE_SIZE #define APPFS_META_SZ (APPFS_SECTOR_SZ/2) #define APPFS_META_CNT 2 #define APPFS_META_DESC_SZ 128 #define APPFS_PAGES 255 #define APPFS_MAGIC "AppFsDsc" #define APPFS_USE_FREE 0xff //No file allocated here #define APPFS_ILLEGAL 0x55 //Sector cannot be used (usually because it's outside the partition) #define APPFS_USE_DATA 0 //Sector is in use for data typedef struct __attribute__ ((__packed__)) { uint8_t magic[8]; //must be AppFsDsc uint32_t serial; uint32_t crc32; uint8_t reserved[128-16]; } AppfsHeader; typedef struct __attribute__ ((__packed__)) { char name[112]; //Only set for 1st sector of file. Rest has name set to 0xFF 0xFF ... uint32_t size; //in bytes uint8_t next; //next page containing the next 64K of the file; 0 if no next page (Because allocation always starts at 0 and pages can't refer to a lower page, 0 can never occur normally) uint8_t used; //one of APPFS_USE_* uint8_t reserved[10]; } AppfsPageInfo; typedef struct __attribute__ ((__packed__)) { AppfsHeader hdr; AppfsPageInfo page[APPFS_PAGES]; } AppfsMeta; static int appfsActiveMeta=0; //number of currently active metadata half-sector (0 or 1) static const AppfsMeta *appfsMeta=NULL; //mmap'ed flash #ifndef BOOTLOADER_BUILD static const esp_partition_t *appfsPart=NULL; static spi_flash_mmap_handle_t appfsMetaMmapHandle; #else static uint32_t appfsPartOffset=0; #endif static int page_in_part(int page) { #ifndef BOOTLOADER_BUILD return ((page+1)*APPFS_SECTOR_SZ < appfsPart->size); #else return 1; #endif } //Find active meta half-sector. Updates appfsActiveMeta to the most current one and returns ESP_OK success. //Returns ESP_ERR_NOT_FOUND when no active metasector is found. static esp_err_t findActiveMeta() { int validSec=0; //bitmap of valid sectors uint32_t serial[APPFS_META_CNT]={0}; AppfsHeader hdr; for (int sec=0; secserial[best]) best=sec; } } ESP_LOGI(TAG, "Meta page 0: %svalid (serial %d)", (validSec&1)?"":"in", serial[0]); ESP_LOGI(TAG, "Meta page 1: %svalid (serial %d)", (validSec&2)?"":"in", serial[1]); //'best' here is either still -1 (no valid sector found) or the sector with the highest valid serial. if (best==-1) { ESP_LOGI(TAG, "No valid page found."); //Eek! Nothing found! return ESP_ERR_NOT_FOUND; } else { ESP_LOGI(TAG, "Using page %d as current.", best); } appfsActiveMeta=best; return ESP_OK; } #ifdef BOOTLOADER_BUILD IRAM_ATTR #endif static int appfsGetFirstPageFor(const char *filename) { for (int j=0; j=APPFS_PAGES) return false; if (appfsMeta[appfsActiveMeta].page[(int)fd].used!=APPFS_USE_DATA) return false; if (appfsMeta[appfsActiveMeta].page[(int)fd].name[0]==0xff) return false; return true; } int appfsExists(const char *filename) { return (appfsGetFirstPageFor(filename)==-1)?0:1; } appfs_handle_t appfsOpen(const char *filename) { return appfsGetFirstPageFor(filename); } void appfsClose(appfs_handle_t handle) { //Not needed in this implementation. Added for possible later use (concurrency?) } void appfsEntryInfo(appfs_handle_t fd, const char **name, int *size) { if (name) *name=appfsMeta[appfsActiveMeta].page[fd].name; if (size) *size=appfsMeta[appfsActiveMeta].page[fd].size; } appfs_handle_t appfsNextEntry(appfs_handle_t fd) { if (fd==APPFS_INVALID_FD) { fd=0; } else { fd++; } if (fd>=APPFS_PAGES || fd<0) return APPFS_INVALID_FD; while (appfsMeta[appfsActiveMeta].page[fd].used!=APPFS_USE_DATA || appfsMeta[appfsActiveMeta].page[fd].name[0]==0xff) { fd++; if (fd>=APPFS_PAGES) return APPFS_INVALID_FD; } return fd; } size_t appfsGetFreeMem() { size_t ret=0; for (int i=0; ipage_count) return ESP_ERR_NO_MEM; } } DPORT_REG_CLR_BIT( DPORT_PRO_CACHE_CTRL1_REG, (DPORT_PRO_CACHE_MASK_IRAM0) | (DPORT_PRO_CACHE_MASK_IRAM1 & 0) | (DPORT_PRO_CACHE_MASK_IROM0 & 0) | DPORT_PRO_CACHE_MASK_DROM0 | DPORT_PRO_CACHE_MASK_DRAM1 ); DPORT_REG_CLR_BIT( DPORT_APP_CACHE_CTRL1_REG, (DPORT_APP_CACHE_MASK_IRAM0) | (DPORT_APP_CACHE_MASK_IRAM1 & 0) | (DPORT_APP_CACHE_MASK_IROM0 & 0) | DPORT_APP_CACHE_MASK_DROM0 | DPORT_APP_CACHE_MASK_DRAM1 ); Cache_Read_Enable( 0 ); return ESP_OK; } IRAM_ATTR void* appfsBlMmap(int fd) { if (appfsMeta) appfs_bootloader_munmap(appfsMeta); appfsMeta=NULL; //Bootloader_mmap only allows mapping of one consecutive memory range. We need more than that, so we essentially //replicate the function here. Cache_Read_Disable(0); Cache_Flush(0); int page=fd; for (int i=0; i<50; i++) { // ESP_LOGI(TAG, "Mapping flash addr %X to mem addr %X for page %d", appfsPartOffset+((pages[i]+1)*APPFS_SECTOR_SZ), MMU_BLOCK0_VADDR+(i*APPFS_SECTOR_SZ), pages[i]); int e = cache_flash_mmu_set(0, 0, MMU_BLOCK0_VADDR+(i*APPFS_SECTOR_SZ), appfsPartOffset+((page+1)*APPFS_SECTOR_SZ), 64, 1); if (e != 0) { ESP_LOGE(TAG, "cache_flash_mmu_set failed: %d", e); Cache_Read_Enable(0); return NULL; } page=next_page_for[page]; if (page==0) break; } Cache_Read_Enable(0); return (void *)(MMU_BLOCK0_VADDR); } IRAM_ATTR void appfsBlMunmap() { /* Full MMU reset */ Cache_Read_Disable(0); Cache_Flush(0); mmu_init(0); if (keep_meta_mapped) { appfsMeta=appfs_bootloader_mmap(appfsPartOffset, APPFS_SECTOR_SZ); } } IRAM_ATTR esp_err_t appfs_bootloader_read(int fd, size_t src_addr, void *dest, size_t size) { int page=fd; int pos=0; int have_read=0; uint8_t *destp=(uint8_t*)dest; int offset_in_page=src_addr&(APPFS_SECTOR_SZ-1); for (int i=0; i<255; i++) { if (pos+APPFS_SECTOR_SZ-1>=src_addr) { size_t rsize=APPFS_SECTOR_SZ-offset_in_page; if (rsize>size) rsize=size; esp_err_t r=appfs_bootloader_flash_read(appfsPartOffset+((page+1)*APPFS_SECTOR_SZ)+offset_in_page, destp+have_read, rsize, true); if (r!=ESP_OK) return r; offset_in_page=0; have_read+=rsize; if (have_read>=size) { return ESP_OK; } } page=next_page_for[page]; if (page==0) break; pos+=APPFS_SECTOR_SZ; } return ESP_OK; } #else //so if !BOOTLOADER_BUILD //Modifies the header in hdr to the correct crc and writes it to meta info no metano. //Assumes the serial etc is in order already, and the header section for metano has been erased. static esp_err_t writeHdr(AppfsHeader *hdr, int metaNo) { hdr->crc32=0; uint32_t crc=0; crc=crc32_le(crc, (const uint8_t *)hdr, APPFS_META_DESC_SZ); for (int j=0; jcrc32=crc; return esp_partition_write(appfsPart, metaNo*APPFS_META_SZ, hdr, sizeof(AppfsHeader)); } //Kill all existing filesystem metadata and re-initialize the fs. static esp_err_t initializeFs() { esp_err_t r; //Kill management sector r=esp_partition_erase_range(appfsPart, 0, APPFS_SECTOR_SZ); if (r!=ESP_OK) return r; //All the data pages are now set to 'free'. Add a header that makes the entire mess valid. AppfsHeader hdr; memset(&hdr, 0xff, sizeof(hdr)); memcpy(hdr.magic, APPFS_MAGIC, 8); hdr.serial=0; //Mark pages outside of partition as invalid. int lastPage=(appfsPart->size/APPFS_SECTOR_SZ); for (int j=lastPage; j0) { //Eek! Can't allocate enough space! ESP_LOGD(TAG, "Not enough free space!"); return ESP_ERR_NO_MEM; } //Re-write a new meta page but with file allocated int newMeta=(appfsActiveMeta+1)%APPFS_META_CNT; ESP_LOGD(TAG, "Re-writing meta data to meta page %d...", newMeta); r=esp_partition_erase_range(appfsPart, newMeta*APPFS_META_SZ, APPFS_META_SZ); if (r!=ESP_OK) return r; //Prepare header AppfsHeader hdr; memcpy(&hdr, &appfsMeta[appfsActiveMeta].hdr, sizeof(hdr)); hdr.serial++; hdr.crc32=0; for (int j=0; jaddress/SPI_FLASH_MMU_PAGE_SIZE)+1; while (offset >= APPFS_SECTOR_SZ) { page=appfsMeta[appfsActiveMeta].page[page].next; offset-=APPFS_SECTOR_SZ; ESP_LOGD(TAG, "Skipping a page (to page %d), remaining offset 0x%X", page, offset); } int *pages=alloca(sizeof(int)*((len/APPFS_SECTOR_SZ)+1)); int nopages=0; size_t mappedlen=0; while(len>mappedlen) { pages[nopages++]=page+dataStartPage; ESP_LOGD(TAG, "Mapping page %d (part offset %d).", page, dataStartPage); page=appfsMeta[appfsActiveMeta].page[page].next; mappedlen+=APPFS_SECTOR_SZ; } r=spi_flash_mmap_pages(pages, nopages, memory, out_ptr, out_handle); if (r!=ESP_OK) { ESP_LOGD(TAG, "Can't map file: pi_flash_mmap_pages returned %d\n", r); return r; } *out_ptr=((uint8_t*)*out_ptr)+offset; return ESP_OK; } void appfsMunmap(spi_flash_mmap_handle_t handle) { spi_flash_munmap(handle); } //Just mmaps and memcpys the data. Maybe not the fastest ever, but hey, if you want that you should mmap //and read from the flash cache memory area yourself. esp_err_t appfsRead(appfs_handle_t fd, size_t start, void *buf, size_t len) { const void *flash; spi_flash_mmap_handle_t handle; esp_err_t r=appfsMmap(fd, start, len, &flash, SPI_FLASH_MMAP_DATA, &handle); if (r!=ESP_OK) return r; memcpy(buf, flash, len); spi_flash_munmap(handle); return ESP_OK; } esp_err_t appfsErase(appfs_handle_t fd, size_t start, size_t len) { esp_err_t r; int page=(int)fd; if (!appfsFdValid(page)) return ESP_ERR_NOT_FOUND; //Bail out if trying to erase past the file end. //Allow erases past the end of the file but still within the page reserved for the file. int roundedSize=(appfsMeta[appfsActiveMeta].page[page].size+(APPFS_SECTOR_SZ-1))&(~(APPFS_SECTOR_SZ-1)); if (roundedSize < (start+len)) { return ESP_ERR_INVALID_SIZE; } //Find initial page while (start >= APPFS_SECTOR_SZ) { page=appfsMeta[appfsActiveMeta].page[page].next; start-=APPFS_SECTOR_SZ; } //Page now is the initial page. Start is the offset into the page we need to start at. while (len>0) { size_t size=len; //Make sure we do not go over a page boundary if ((size+start)>APPFS_SECTOR_SZ) size=APPFS_SECTOR_SZ-start; ESP_LOGD(TAG, "Erasing page %d offset 0x%X size 0x%X", page, start, size); r=esp_partition_erase_range(appfsPart, (page+1)*APPFS_SECTOR_SZ+start, size); if (r!=ESP_OK) return r; page=appfsMeta[appfsActiveMeta].page[page].next; len-=size; start=0; //offset is not needed anymore } return ESP_OK; } esp_err_t appfsWrite(appfs_handle_t fd, size_t start, uint8_t *buf, size_t len) { esp_err_t r; int page=(int)fd; if (!appfsFdValid(page)) return ESP_ERR_NOT_FOUND; if (appfsMeta[appfsActiveMeta].page[page].size < (start+len)) { return ESP_ERR_INVALID_SIZE; } while (start > APPFS_SECTOR_SZ) { page=appfsMeta[appfsActiveMeta].page[page].next; start-=APPFS_SECTOR_SZ; } while (len>0) { size_t size=len; if (size+start>APPFS_SECTOR_SZ) size=APPFS_SECTOR_SZ-start; ESP_LOGD(TAG, "Writing to page %d offset %d size %d", page, start, size); r=esp_partition_write(appfsPart, (page+1)*APPFS_SECTOR_SZ+start, buf, size); if (r!=ESP_OK) return r; page=appfsMeta[appfsActiveMeta].page[page].next; len-=size; buf+=size; start=0; } return ESP_OK; } void appfsDump() { printf("AppFsDump: ..=free XX=illegal no=next page\n"); for (int i=0; i<16; i++) printf("%02X-", i); printf("\n"); for (int i=0; iaddress; int page=(phys_offs/APPFS_SECTOR_SZ)-1; if (page<0 || page>=APPFS_PAGES) { return ESP_ERR_NOT_FOUND; } //Find first sector for this page. int tries=APPFS_PAGES; //make sure this loop always exits while (appfsMeta[appfsActiveMeta].page[page].name[0]==0xff) { int i; for (i=0; i=APPFS_PAGES) return ESP_ERR_NOT_FOUND; tries--; } //Okay, found! *ret_app=page; return ESP_OK; } #endif