TinyFS is a minimalist filesystem designed for tiny embedded systems with severe memory and code size constraints. It was originally developed to run on a ZX81 (with only 8KB of available ROM space for filesystem code) without requiring additional processors or complex hardware.
- Simplicity: Easy to understand and implement
- Low Code Size: Fits in 8KB of ROM on constrained systems like the ZX81
- Low RAM Usage: Minimal static buffers (approximately 1.5KB total)
- Static Memory Management: No dynamic memory allocation
- Portability: Can be ported to any platform with block storage
To achieve these goals, TinyFS deliberately omits features found in more complex filesystems:
- No Timestamps: No creation, access, or modification times
- No File Attributes/Permissions: No file permissions or ACL system
- No Caching: Direct block I/O without caching layer
- No Redundant Metadata: Harder to recover from filesystem corruption
- Fixed Block Size: 512 bytes only (no configurability)
TinyFS uses a simple, fixed-size block architecture. All blocks are exactly 512 bytes (TFS_BLOCKSIZE), which matches standard sector sizes for SD/MMC cards and simplifies the implementation.
There are only three types of blocks:
- Bitmap Blocks: Track allocated/free blocks
- Directory Blocks: Store directory entries
- Data Blocks: Store file data
The block size is fixed at 512 bytes, defined as:
#define TFS_BLOCKSIZE_WIDTH 9
#define TFS_BLOCKSIZE (1 << TFS_BLOCKSIZE_WIDTH) // 512 bytesThis size is:
- Standard for SD/MMC cards (matches physical sector size)
- Small enough for embedded systems with limited RAM
- Large enough to avoid excessive overhead
Bitmap blocks track which blocks are allocated (in use) or free. Each bit in the bitmap represents one block:
- Bit = 1: Block is allocated
- Bit = 0: Block is free
Since each bitmap block is 512 bytes = 4096 bits, each bitmap block can track 4096 blocks (512 × 8).
Bitmap Block (512 bytes):
+--+--+--+--+--+--+--+--+
| Each byte = 8 bits | Each bit represents one block
| representing 8 blks | 0 = free, 1 = allocated
+--+--+--+--+--+--+--+--+
Bitmap blocks are placed at regular intervals across the storage device:
- First bitmap block: Block 0
- Next bitmap block: Block 4096
- Next bitmap block: Block 8192
- And so on...
This pattern is calculated as:
#define TFS_BITMAP_BLK_COUNT (TFS_BLOCKSIZE << 3) // 4096
#define TFS_BITMAP_BLK_MASK (TFS_BITMAP_BLK_COUNT - 1) // 0x0FFFTo find the bitmap block for any given block number:
bitmap_block = block_number & ~TFS_BITMAP_BLK_MASK; // Round down to multiple of 4096The allocation algorithm uses a "last known good" approach to minimize disk reads:
- Start from the currently loaded bitmap block
- Search for a free bit (0) in the bitmap
- If found:
- Set the bit to 1 (mark as allocated)
- Write the updated bitmap block
- Return the block number
- If not found:
- Move to the next bitmap block (wrapping around at end of disk)
- If we've checked all bitmap blocks, disk is full
- Otherwise, repeat from step 2
The loaded_bitmap_blk variable caches the current bitmap block to avoid redundant reads.
With 32-bit block numbers and 512-byte blocks:
- Maximum blocks: 2^32 = 4,294,967,296
- Maximum size: 4,294,967,296 × 512 = 2 TB (terabytes)
The root directory is always located at block 1. This is a fixed location that never changes.
Block 0: First bitmap block
Block 1: Root directory (first block)
Block 2+: Other blocks (data, directories, more bitmap blocks)
Directory blocks are organized as a doubly-linked list to support an unlimited number of entries per directory:
typedef struct {
uint32_t prev; // Previous directory block (0 if first)
uint32_t next; // Next directory block (0 if last)
uint32_t parent; // Parent directory block (0 if root)
TFS_DIR_ITEM items[]; // Array of directory items
} TFS_DIR_BLK;Layout visualization:
+----------------+
| prev (4 bytes) | -> Previous block in chain
+----------------+
| next (4 bytes) | -> Next block in chain
+----------------+
| parent (4 b) | -> Parent directory (0 for root)
+----------------+
| item[0] | \
+----------------+ |
| item[1] | |
+----------------+ |-- TFS_DIR_BLK_ITEMS entries
| ... | | (20 items per block)
+----------------+ |
| item[19] | /
+----------------+
Each directory block contains 20 directory items (TFS_DIR_BLK_ITEMS):
#define TFS_DIR_BLK_ITEMS ((TFS_BLOCKSIZE - sizeof(TFS_DIR_BLK)) / sizeof(TFS_DIR_ITEM))
// Result: (512 - 12) / 25 = 20 itemsEach item has this structure:
#define TFS_NAME_LEN 16
typedef struct {
uint32_t blk; // Block number (directory or first data block)
uint32_t size; // File size (0 for directories)
uint8_t type; // TFS_DIR_ITEM_FREE, _DIR, or _FILE
char name[TFS_NAME_LEN]; // Name (16 characters max, NOT null-terminated if full)
} TFS_DIR_ITEM;
#define TFS_DIR_ITEM_FREE 0
#define TFS_DIR_ITEM_DIR 1
#define TFS_DIR_ITEM_FILE 2Item layout (25 bytes per item):
+----------------+
| blk (4 bytes) | -> First data block (file) or subdirectory block
+----------------+
| size (4 bytes) | -> File size in bytes (0 for directories)
+----------------+
| type (1 byte) | -> 0=free, 1=dir, 2=file
+----------------+
| name (16 b) | -> Filename (may not be null-terminated)
+----------------+
- Current Directory: Tracked in
current_dir_blkvariable - Change to Root: Set
current_dir_blk = TFS_ROOT_DIR_BLK(block 1) - Change to Parent: Read current directory block, get
parentfield - Change to Subdirectory: Search for directory item with matching name, get
blkfield
When a directory needs more than 20 entries:
- Allocate a new directory block
- Link it to the previous block (update
nextandprevpointers) - Set the
parentpointer to match the parent directory - Initialize all items as
TFS_DIR_ITEM_FREE
When a directory block becomes completely empty (all items are FREE):
- If it's the only block: Keep it
- If it's in a chain: Remove it from the chain and free the block
- This prevents wasted space from deleted files
Files are stored as doubly-linked lists of data blocks:
typedef struct {
uint32_t prev; // Previous data block (0 if first)
uint32_t next; // Next data block (0 if last)
uint8_t data[]; // User data (504 bytes)
} TFS_DATA_BLK;
#define TFS_DATA_LEN (TFS_BLOCKSIZE - sizeof(TFS_DATA_BLK)) // 504 bytesLayout visualization:
+----------------+
| prev (4 bytes) | -> Previous data block
+----------------+
| next (4 bytes) | -> Next data block
+----------------+
| data (504 b) | -> Actual file data
| |
| ... |
+----------------+
With 32-bit size field:
- Maximum file size: 2^32 - 1 = 4,294,967,295 bytes (~4 GB)
- Overhead: 8 bytes per block (prev + next pointers)
- Usable data: 504 bytes per block
- Efficiency: 504/512 = 98.4%
Writing a file:
- Find or create directory item
- Allocate first data block
- Fill data blocks in sequence, allocating new blocks as needed
- Update directory item with first block number and total size
Reading a file:
- Find directory item
- Follow the chain of data blocks from first to last
- Copy data from each block
- Stop when size bytes have been read
Deleting a file:
- Find directory item
- Follow the chain of data blocks, freeing each one
- Mark directory item as FREE
- Optimize directory (remove empty blocks if possible)
TinyFS uses static memory allocation exclusively. No dynamic memory allocation (malloc/free) is used.
static uint8_t bitmap_blk[TFS_BLOCKSIZE]; // 512 bytes - Current bitmap block
static TFS_BLK_BUFFER blk_buf; // 512 bytes - General purpose block buffer
static uint32_t loaded_bitmap_blk; // 4 bytes - Currently loaded bitmap block #
static uint32_t current_dir_blk; // 4 bytes - Current directory block #
static uint32_t loaded_dir_blk; // 4 bytes - Currently loaded directory block #
static uint32_t last_bitmap_blk; // 4 bytes - Last bitmap block on disk
static uint16_t last_bitmap_len; // 2 bytes - Bits used in last bitmap blockThe blk_buf is a union that can be interpreted as different block types:
typedef union {
uint8_t raw[TFS_BLOCKSIZE]; // Raw bytes
TFS_DIR_BLK dir; // Directory block
TFS_DATA_BLK data; // Data block
} TFS_BLK_BUFFER;When TFS_EXTENDED_API is enabled, file handles are tracked:
typedef struct {
uint8_t usage_count; // Reference count (0 = unused)
uint32_t dir_blk; // Directory block containing this file's entry
TFS_DIR_ITEM *dir_item; // Pointer to directory item
uint32_t size; // Current file size
uint32_t first_blk; // First data block
uint32_t curr_blk; // Current block for seek position
uint32_t curr_pos; // Current position in file
} TFS_FILEHANDLE;
static TFS_FILEHANDLE handles[TFS_MAX_FDS]; // Default: 32 handles on LinuxMinimal configuration (without extended API):
- Bitmap buffer: 512 bytes
- Block buffer: 512 bytes
- State variables: ~20 bytes
- Total: ~1,044 bytes
With extended API (32 file descriptors):
- Minimal: 1,044 bytes
- File handles: 32 × 28 = 896 bytes
- Total: ~1,940 bytes
These are inherent to the design:
- No Timestamps: Files have no creation, modification, or access times
- No File Attributes: No permissions, owner, or access control
- No Caching: Every operation performs disk I/O
- Fixed Block Size: Always 512 bytes (cannot be configured)
- No Fragmentation Handling: Files can become fragmented over time
- Limited Error Recovery: No redundant metadata or journaling
These depend on configuration and platform:
- Maximum Filename Length: 16 characters (TFS_NAME_LEN)
- Directory Entries Per Block: 20 items
- File Descriptors: Platform-dependent (default 32 on Linux)
- Sequential Access: Excellent (minimal overhead)
- Random Access: Good with Extended API (bidirectional seek)
- Directory Listing: Linear scan of directory blocks
- Allocation: Best-fit from currently loaded bitmap
- Deletion: Requires traversing entire file to free blocks
void drive_init(void); // Initialize storage hardware
void drive_select(void); // Select/enable device
void drive_deselect(void); // Deselect/disable device
void drive_read_block(uint32_t blkno, uint8_t *data); // Read 512-byte block
void drive_write_block(uint32_t blkno, const uint8_t *data); // Write 512-byte blockTFS_DRIVE_INFO tfs_drive_info; // Must be populated in drive_init()
uint8_t tfs_last_error; // Set by drive functions on error- RAM: ~1 KB for filesystem (more with Extended API)
- ROM/Flash: ~8 KB for filesystem code
- Storage: Block device with 512-byte sectors
- Compiler: C99 or later (uses stdint.h)
| Feature | TinyFS | FAT32 | ext2 | littlefs |
|---|---|---|---|---|
| Code Size | ~8 KB | ~50+ KB | ~100+ KB | ~15 KB |
| RAM Usage | ~1 KB | ~10+ KB | ~20+ KB | ~5 KB |
| Max File Size | 4 GB | 4 GB | 2 TB | 2 GB |
| Max Volume | 2 TB | 2 TB | 32 TB | 2 GB |
| Timestamps | No | Yes | Yes | No |
| Permissions | No | Limited | Full | No |
| Wear Leveling | No | No | No | Yes |
| Power-loss Safety | No | Limited | Journal | Yes |
TinyFS trades features and robustness for minimal resource usage, making it ideal for extremely constrained systems where other filesystems won't fit.