To download the Operating System tOSh, you can do it from the Resources section of this same site.

Parts

This series of articles will have the following parts:

fuck wINdOzE gO tOSh!fuck wINdOzE gO tOSh!

Programming a Driver

Have you ever wondered what the fuck a driver is?

It’s a pain in the ass, that’s what it is. ;-( – But it’s a pain in the ass that we have to program, otherwise nothing fucking works. 3;-D !

A shitload of years went by and life put a stop to the development of tOSh, the revolutionary Operating System that’s going to change the life of I don’t know who, but it sure as hell changed mine.

We cleared the ground so we could finally start doing something useful –after years!– with our operating system. And one of the useful things a computer actually does is, indeed, store files.

In tOSh, for now, we don’t even have a hard drive. We can’t even save a file on the same floppy disk we boot from because, first of all, we’re not controlling the floppy drive, let alone do we have a filesystem to save anything!

At some point, I thought that controlling the floppy on x86 was going to be easier than putting together a hard-drive driver.

Since I decided to go with the floppy because I LIKE FLOPPY DISKS, I don’t really know yet whether it would’ve been better to deal with HDDs or the floppy drive, but getting a working floppy-drive driver up and running was surprisingly harder than I expected.

While developing tOSh, I generally use qemu and bochs to test that things are working, including the floppy drive. But when I test some version of the operating system on real hardware, like my [1997 Machine], or some of the machines at the hackerspace where I’m a member, or friends who have really old hardware, things get even more complicated.

Join me on this adventure to program, as generically as possible, a driver for floppy disk controllers, FDC (Floppy Disk Controller), and a filesystem for writing data non-stop, 70s style.

Floppy Drive Driver

Let’s start with some #dEfInEz:

#define FDC_DOR   0x3F2   /* Digital Output Register */
#define FDC_MSR   0x3F4   /* Main Status Register */
#define FDC_FIFO  0x3F5   /* Data FIFO */
#define FDC_CCR   0x3F7   /* Configuration Control Register */
  • FDC_DOR turns the motor on and off, selects the active drive, and enables IRQ/DMA.
  • FDC_MSR is read-only and tells you what state the controller is in.
  • FDC_FIFO is the 1-byte buffer through which commands go in and results come out. The entire FDC protocol goes through this single port, one byte at a time.
  • FDC_CCR defines the "speed" of the transfer.

Protocol

The protocol has three parts or phases.

  • command phase, you send bytes.
  • execution phase, the controller does extremely important stuff.
  • result phase, the controller sends you back the status of the previous operation.

The MSR tells you which part you’re in by looking at the RQM (Request For Master) and DIO (Data Input/Output) bits:

static bool fdc_wait_write_ready(void)
{
    uint32_t start = pit_get_ticks();

    while ((pit_get_ticks() - start) < 100) {

        uint8_t msr = inb(FDC_MSR);

        if ((msr & 0xC0) == 0x80)
            return true;

        inb(0x80); /* I/O Delay for ISA hardware */
    }

    terminal_writestring("FLOPPY: FDC write timeout\n");
    return false;
}

(msr & 0xC0) == 0x80 means RQM=1, DIO=0. The controller is waiting for a byte. To read a result, the condition changes to (msr & 0xD0) == 0xD0, which adds CB=1 (controller busy) to RQM=1, DIO=1: the FDC has a byte ready and is processing a command (hence the busy).

I do all of this with polling with a timeout. If the FDC doesn’t respond, the Kernel will hang forever. Each fdc_write / floppy_read_byte has 100 ticks of leeway (at 100Hz, one second), and if it can’t do it, it reports it and aborts.

The inb(0x80) is needed –on real hardware– to perform an ISA I/O delay. The bus needs X amount of time between polls.

*Coughs*
Let’s talk about emulators.

Have you noticed that sometimes some people –fukken beautiful people in this fucking world– are obsessed with emulating entire architectures with clock-level accuracy? Well, qemu and bochs aren’t those.

Right now we’re talking about x86, but on other architectures or consoles, like Super Nintendo or I don’t know, Playstation, there are several ways of approaching emulation. Maybe with the SNES, since it’s a ROM, it’s not so obvious, but with CDROM, where there’s a motor involved (like with a floppy), things change.

If I program a controller for some piece of shit that’s emulated with qemu, I’m missing all the physical peculiarities that the motor and the RPMs of the CD or the magnetic platter of the floppy disk have. In this case, we’re talking about a floppy drive, where there’s a motor (!) spinning and taking a few milliseconds to reach a stable rotational speed.

When I was programming this driver and the Operating System in general, it worked in qemu, and when I tried to run it on real hardware, it fucking didn’t work at all.

That’s why it’s incredibly important to follow the datasheets for whatever the hell you’re programming, and if you’re lucky, to dig up information that other people have researched before, like Josh Cole from floppy.cafe, who collected practically 90% of everything I needed to develop the driver in a single place. What a fucking champ.

In the age of Artificial Intelligence, knowing where to find the documentation it was trained on may be even better than blindly asking it for an implementation.

I guarantee it.

Reset and Initialization

The FDC startup sequence in floppy_init() follows the order from the datasheet:

1. Unmask IRQ6 on the PIC
2. Configure data rate (CCR = 0x00)
3. DOR = enable + irq, motor off
4. Reset: DOR = 0x00
5. I/O delay (4x writes to 0x80)
6. DOR = enable + irq (end of reset)
7. Wait for IRQ6 (the FDC reset generates an IRQ)
8. 4x SENSE INTERRUPT
9. SPECIFY
10. CONFIGURE
11. Motor on -> RECALIBRATE -> motor off
12. Test read (sector 0, head 0, sector 1)

The only relevant thing here is why we do 4 Sense Interrupt after the reset. This is because I left the controller in "drive polling" mode: when the FDC performs a reset with that mode active, it generates a pending status for each of the 4 drives it can have (A, B, C, or D floppies), even if you only have one connected. If you don’t drain those 4 results with floppy_sense_interrupt(), the controller is left with crap in the queue and you have to exhaust the data in it.

I spent quite some time trying to understand why the drive kept hanging on me, and one of the reasons was this.

Specify

static bool floppy_specify(void)
{
    if (!fdc_write(FDC_CMD_SPECIFY))
        return false;

    if (!fdc_write(0x8f))   /* SRT=8, HUT=15 */
        return false;

    if (!fdc_write(0x1e))   /* HLT=15, NDMA=0 */
        return false;

    return true;
}

SPECIFY has no result phase and doesn’t generate an IRQ, so once the 3 bytes have been sent, that’s it. The values:

  • Byte 1 (0x8f): high nibble = SRT (Step Rate Time, how long each head step takes, in inverse units – higher values = faster steps), low nibble = HUT (Head Unload Time, how long it waits before "releasing" the head after an operation).

  • Byte 2 (0x1e): high bits = HLT (Head Load Time, how long it takes to settle the head before reading/writing), low bit = NDMA. NDMA=0 is the important one: it tells the FDC that we’re going to use DMA for data transfers, not PIO.

I use DMA transfers because (I think this is commented in floppy.h) because bochs doesn’t support PIO transfers, only DMA.

If you forget this bit, the FDC expects you to read each byte manually from the FIFO, and the whole DMA pipeline we build further down becomes completely useless.

Since the reset clears this configuration, we have to send it again on every floppy_init().

Configure

static bool floppy_configure(void)
{
    if (!fdc_write(0x13))   /* CONFIGURE */
        return false;
    if (!fdc_write(0x00))   /* reserved byte */
        return false;
    if (!fdc_write(0x57))   /* EIS=1, EFIFO=0, POLL=1, FIFOTHR=7 */
        return false;
    if (!fdc_write(0x00))   /* PRETRK */
        return false;

    return true;
}

0x57 enables three things: EIS (Enable Implied Seek, the FDC performs the seek automatically before a read/write without you explicitly asking for it), EFIFO=0 (which, counterintuitively, enables the controller’s internal 16-byte FIFO instead of disabling it – the bit’s name is the opposite of what you’d expect) and FIFOTHR=7, the 8-byte threshold that triggers a DMA/memory access. This gives the system some leeway so it doesn’t lose bytes if there’s a bit of bus latency.

CONFIGURE, like SPECIFY, also doesn’t generate an IRQ or have a result phase.

Motor and timing

static void floppy_motor_on(void)
{
    outb(FDC_DOR, FDC_DOR_ENABLE | FDC_DOR_IRQ | FDC_DOR_MOTOR_A | FDC_DRIVE_A);
    pit_wait_ms(500);
}

500ms is more than what osdev documents (it suggests 300ms for 3.5" and even says 50ms is enough). In my real setup, 50ms wasn’t enough and I was getting intermittent seek failures. I preferred to play it safe with half a second instead of chasing the theoretical minimum – the cost of waiting a little longer is insignificant compared to having a failed read in the middle of boot.

For this I need a timer that doesn’t depend on counting CPU cycles by hand, so enter pit.c.

El PIT (Programmable Interval Timer)

void pit_init(uint32_t frequency)
{
    pit_hz = frequency;
    uint32_t divisor = PIT_FREQUENCY / frequency;

    outb(PIT_COMMAND, 0x36);   /* Ch0, lo/hi, mode 3, binario */
    outb(PIT_CH0, divisor & 0xFF);
    outb(PIT_CH0, (divisor >> 8) & 0xFF);
}

The PIT runs internally at 1193182 Hz (PIT_FREQUENCY). With pit_init(100), I configure a divider that triggers an IRQ0 every 10ms, meaning 100 ticks per second. pit_ticks is a global counter incremented from pit_irq_handler(), and everything else in the system (pit_wait_ms, the FDC timeouts, floppy_wait_irq) relies on that counter instead of blindly busy-waiting.

static bool floppy_wait_irq(void)
{
    uint32_t start = pit_get_ticks();

    while (!floppy_irq) {
        if ((pit_get_ticks() - start) >= 200) {
            terminal_writestring("FLOPPY: IRQ timeout\n");
            return false;
        }
        __asm__ volatile ("hlt");
    }
    return true;
}

200 ticks at 100Hz means a 2-second timeout for the FDC’s IRQ6 to arrive – deliberately generous, because I don’t want a real floppy (which can take some time to spin up and seek) to trigger a false timeout.

Notice the hlt in the loop: instead of spin-waiting and burning CPU, the processor stops until the next interrupt (either from the PIT or the FDC), which is exactly what a real kernel should do when waiting for I/O.

Sense Interrupt, Recalibrate y Seek

RECALIBRATE and SEEK generate an IRQ but don’t have their own result phase. The status has to be requested separately with SENSE INTERRUPT:

static bool floppy_sense_interrupt(uint8_t *st0, uint8_t *cyl)
{
    inb(0x80);
    inb(0x80);

    if (!fdc_write(FDC_CMD_SENSE_INT))
        return false;
    if (!floppy_read_byte(st0))
        return false;
    if (!floppy_read_byte(cyl))
        return false;

    return true;
}

RECALIBRATE moves the head to track 0 (useful for "resynchronizing" the known physical position), while SEEK moves it to a specific cylinder. In both cases, after the IRQ, I do SENSE INTERRUPT and validate two things: the Seek End bit (st0 & 0x20) and that the reported cylinder (cyl) matches the expected one (0 for recalibrate, the requested cylinder for seek). If anything goes wrong, I spit it out to the console.

Sector Read and Write

The read/write command sends 9 bytes in total:

if (!fdc_write(FDC_CMD_READ_DATA | 0xC0)) { [...] }  /* MT=1, MFM=1 */
if (!fdc_write((head << 2) | FDC_DRIVE_A)) { [...] } /* HD + DR */
if (!fdc_write(cylinder)) { [...] }                  /* C */
if (!fdc_write(head)) { [...] }                      /* H */
if (!fdc_write(sector)) { [...] }                    /* R */
if (!fdc_write(2)) { [...] }                         /* N = 512 bytes */
if (!fdc_write(18)) { [...] }                        /* EOT: último sector de la pista */
if (!fdc_write(0x1B)) { [...] }                      /* GPL */
if (!fdc_write(0xFF)) { [...] }                      /* DTL, ignorado cuando N != 0 */

0xC0 on the command enables MT (Multi-Track, the controller can automatically continue onto the next head if it crosses the EOT) and MFM (the standard modulation for floppies and magnetic storage from double density upwards). N=2 tells the FDC "each sector is 512 bytes" using the chip’s standard size table (0=128B, 1=256B, 2=512B…). EOT=18 is the number of sectors per track in the 1.44MB geometry.

After the IRQ (which arrives when the DMA transfer is complete, not before), comes the result phase of 7 bytes (ST0 through ST2 plus C/H/R/N returned), of which I only check the first three:

if (st[0] & 0xC0) { [...] }   /* error phase */
if (st[1] != 0)   { [...] }   /* ST1: errores de datos, CRC, etc. */
if (st[2] != 0)   { [...] }   /* ST2: errores específicos de la pista/sector */

Any bit set in ST1/ST2 is treated as a total operation failure – I don’t try to distinguish "it’s a recoverable CRC error" from "the sector doesn’t exist"; I simply retry the whole thing.

floppy_write_sector_impl is almost a mirror image of the read, changing the command (FDC_CMD_WRITE_DATA | 0xC0, MFM with no need for MT… although in the code I left the same 0xC0) and the DMA direction.

CHS and LBA Conversion

So I don’t have to think about cylinder/head/sector anywhere else in the kernel, I expose floppy_read_lba / floppy_write_lba:

uint32_t cylinder = lba / (2 * 18);
uint32_t tmp = lba % (2 * 18);
uint32_t head = tmp / 18;
uint32_t sector = (tmp % 18) + 1;

This assumes the fixed geometry of a 1.44MB floppy: 2 heads, 18 sectors per track, 80 cylinders (2×18×80 = 2880 total sectors, hence the if (lba >= 2880) return false;). There’s no geometry detection from the media descriptor and no support for other formats – for tOSh, a single type of floppy is more than enough, and hardcoding the geometry saves an entire layer of abstraction that I don’t need.

Retry logic

bool floppy_read_sector(uint8_t cylinder, uint8_t head, uint8_t sector, uint8_t *buffer)
{
    for (int attempt = 0; attempt < 3; attempt++) {
        if (floppy_read_sector_impl(cylinder, head, sector, buffer))
            return true;

        terminal_writestring("FLOPPY: retrying read...\n");
        floppy_motor_on();
        floppy_recalibrate();
        floppy_motor_off();
    }
    return false;
}

Real floppies are prone to transient errors: dust, worn-out media, a head that didn’t settle properly on the first attempt, putting A FRIDGE MAGNET AS A PAPERWEIGHT ON TOP OF A STACK OF FLOPPIES, etc…

Instead of failing immediately on the first error, I retry 3 times, and between each attempt I do a full RECALIBRATE to force the head to resynchronize its physical position – if the error was caused precisely by the head being out of position, the recalibrate fixes it before the next attempt.

DMA, 8237 Chip

The FDC doesn’t deliver the read data byte by byte through the FIFO when NDMA=0 (see Specify above) – it uses the 8237 DMA chip to write directly to memory while the CPU does something else (or, in our case, waits in a hlt).

static void dma_transfer(uint8_t channel, uint32_t address, uint16_t count, uint8_t mode)
{
    uint8_t page = (address >> 16) & 0xff;
    uint16_t offset = address & 0xffff;

    outb(DMA_MASK, 0x04 | channel);   /* deshabilita el canal */
    outb(DMA_CLEAR_FF, 0);            /* resetea el flip-flop de byte lo/hi */

    outb(addr_port, offset & 0xff);
    outb(addr_port, (offset >> 8) & 0xff);
    outb(page_port, page);

    outb(DMA_CLEAR_FF, 0);

    count--;                          /* el 8237 cuenta N-1 */
    outb(count_port, count & 0xff);
    outb(count_port, (count >> 8) & 0xff);

    outb(DMA_MODE, mode | channel);
    outb(DMA_MASK, channel);          /* habilita el canal */
}

A few points are worth highlighting here:

  • The flip-flop is an internal bit in the 8237 that indicates whether the next byte written to an address/count port is the low byte or the high byte. Since this state is shared between channels, you have to explicitly reset it (DMA_CLEAR_FF) before writing both the address and the count – otherwise, you can end up writing the high byte where the low byte was supposed to go and build an address that is basically anything.

  • The page register (DMA_PAGE_CH2 = 0x81) is a remnant of the fact that the original 8237 only handles 16-bit addresses for the offset. To reach 24-bit memory addresses (as a real PC needs), the high byte of the address is written separately, into the page register for the corresponding channel. This is also why there’s a restriction that the buffer cannot cross a 64KiB boundary: the 8237 increments the 16-bit offset for every byte transferred, but never touches the page register during the transfer. If the buffer starts near the end of a 64KB page and crosses it, the DMA doesn’t advance to the next page – it wraps the offset around and starts overwriting the beginning of the same 64KB page. That’s why I set up the floppy buffer like this:

static uint8_t floppy_dma_buffer[512]
    __attribute__((aligned(512), section(".dma")));

Aligning it to 512 bytes (the size of a sector) guarantees that you’ll never have a 512-byte buffer that starts, for example, at address 0xFFF00 and ends up crossing into 0x10000 in the next 64KB block. The linker’s .dma section (see Part III) puts this buffer in its own 4KB-page-aligned region, away from any other data, so in practice, boundary-crossing never happens, which means the driver doesn’t need to perform any runtime checks.

  • The mode (0x46 for reading, 0x4A for writing) follows the 8237’s naming convention from the peripheral’s perspective, not the memory’s: dma_read() means "the FDC reads from the disk and writes to memory" (which effectively fills your buffer in floppy_read_sector_impl), while dma_write() means "memory is read and written to the FDC" (to send data to the disk). It’s an easy name to get confused if you think in terms of the CPU instead of the peripheral – here dma_read/dma_write are named from the perspective of "what does the FDC do with the data?", which is consistent with how Intel documents the chip.

  • count-- because the 8237 expects the count as N-1 (you transfer count bytes, but the internal register counts down from count-1 to 0).

So, roughly, floppy_read_sector with dma_read() sets up channel 2 pointing at the buffer for 512 bytes, the READ DATA command is sent to the FDC, the transfer happens in the background while the CPU waits with hlt, and when it finishes, the FDC generates IRQ6, which is handled by floppy_wait_irq.

There’s a lot going on here, and maybe some things are easier to understand by reading the source code.

As I say at the beginning of this whole series of articles, if you want, you can download the source from the Resources page.

The Filesystem

Probando el Filesystem en mi máquina del 97'Testing the Filesystem on my ‘97 Machine

Let’s Get It Workin'

For tOSh there are no blocks, no FAT, no directory tree, no journaling, no long names, and no paths.

tOSh doesn’t give a shit about any of that, not even about the consistency of your data. tOSh is an Operating System ONLY FOR THE CRAZY AND THE BRAVE.

Having said that, and with that clarification out of the way, I’ll start telling you how I built it.

There’s a fixed-size directory with up to 256 files, each with a name, a starting sector, and a size. Period.

The reasoning behind this is the same as with the fixed floppy geometry: any feature from a real filesystem (FAT, ext2, whatever) adds a layer of indirection – blocks, linked lists of clusters, free-space bitmaps – that solves problems tOSh doesn’t have yet, such as real fragmentation on a disk that gets filled and emptied over and over. For what I need (persisting a handful of small files on a 1.44MB floppy during kernel development), a flat directory is enough.

Disk Layout

#define FS_SUPERBLOCK_SECTOR   256

#define FS_DIRECTORY_START     257
#define FS_DIRECTORY_SECTORS   32

#define FS_DATA_START          320
+--------------------------+
| 0    BOOT                |
+--------------------------+
| 1-8  STAGE1              |
+--------------------------+
| 9-136 KERNEL             | <- 64 KiB
+--------------------------+
| 137-255 RESERVED         |
+--------------------------+
| 256     SUPERBLOCK       |
| 257-288 DIRECTORY        |
| 289-319 RESERVED         |
+--------------------------+
| 320-2879 FILE DATA       | <- ~1.28 MB
+--------------------------+

The filesystem starts at sector 256, well after the kernel (which ends at 136). The gap I leave between 137 and 255 (119 sectors, ~59KB) is room for the kernel to grow while I keep developing it without colliding with the filesystem – remember that the Makefile/build.sh from Part III checks that the kernel doesn’t exceed the 128 sectors assigned to it, so this reserved range is the actual buffer before I have to move the whole layout.

The gap between 289 and 319 (31 sectors) is the same thing, but for the directory: FS_DIRECTORY_SECTORS is fixed at 32, but the actual size occupied by struct fs_file[FS_MAX_FILES] may end up using less. This margin saves me from having to recalculate FS_DATA_START every time I change FS_MAX_FILES or the size of fs_file.

The Superblock

struct fs_superblock {
    uint32_t magic;
    uint16_t version;
    uint16_t sector_size;
    uint32_t total_sectors;
    uint32_t data_start;
} __attribute__((packed));

FS_MAGIC is 0x48534f54, which read as little-endian ASCII bytes gives TOSH. fs_is_initialized() reads sector 256 and compares that magic – if it doesn’t match, it assumes the floppy was never formatted with this filesystem and that’s it; it doesn’t try to "recover" or interpret anything else from that sector. __attribute__((packed)) is necessary so the struct occupies exactly the bytes I declare, without any padding the compiler would add for alignment.

The Directory

struct fs_file {
    char     name[FS_FILENAME_MAX];
    uint32_t start_sector;
    uint32_t size;
    uint8_t  used;
} __attribute__((packed));
static struct fs_file directory[FS_MAX_FILES];

The entire directory is an array of 256 entries that lives in RAM while the kernel is running and is persisted in its entirety to disk with fs_save_directory() every time something changes (create, write, delete). There is no separate "free entries" bitmap – an entry is free if used == 0, and finding a file (fs_find) is simply a matter of walking the array and comparing names.

This means that every operation that modifies the filesystem rewrites all 32 sectors of the directory, even if only one entry changed. It’s clearly non-optimal in terms of I/O, but against a floppy that is already painfully slow, the difference between writing 1 sector or 32 doesn’t change the user experience, and I save myself from having to track which specific directory sector corresponds to which entry.

Not Managing Free Space

static uint32_t fs_next_free_sector(void)
{
    uint32_t sector = FS_DATA_START;

    for (int i = 0; i < FS_MAX_FILES; i++) {
        if (!directory[i].used)
            continue;

        uint32_t sectors = (directory[i].size + 511) / 512;
        uint32_t end = directory[i].start_sector + sectors;

        if (end > sector)
            sector = end;
    }

    return sector;
}

Space allocation is about as primitive as it gets: I go through all used files, find the highest start_sector + sectors_occupied, and the next file starts there. There is no free-sector bitmap, no free-list, no compression.

The direct consequence of this –and a conscious decision, IT’S NOT A BUG, IT’S A FEATURE– is that fs_delete() doesn’t free or recycle the space.

Deleting a file only clears its directory entry (used = 0); the sectors it occupied stay there, ignored forever by fs_next_free_sector(), because that function doesn’t look at entries with used == 0, it only looks at the furthest point occupied by currently live files… but that furthest point may have been left behind by a file that no longer exists. In other words: creating and deleting files leaves holes that will never be used again until you reformat the entire disk with fs_format(). This used to be called disk fragmentation and it’s what the "Windows Defragmenter" was supposed to fix. MS-DOS and Windows 9x also suffered from it a bit, for other reasons.

For the current state of tOSh, this is acceptable: the filesystem is not intended (yet, who knows) for intensive, prolonged write/delete usage, but rather to persist a handful of configuration files or system binaries. The day this becomes a real problem (kkjjj I mean never, long live tOSh), the simplest solution without redesigning everything would be to add a free-sector bitmap, or migrate to a block-based scheme with a free-list – but that would completely change the filesystem.

Operations

fs_create, fs_write, fs_read and fs_delete are pretty straightforward given the design I explained above:

  • fs_create looks for a free entry, assigns it the next sector via fs_next_free_sector(), stores the name, and persists the directory. The size starts at 0 – there is no data yet, only the position reservation.
  • fs_write splits the data into 512-byte sectors (padding the last one with zeros if it’s not an exact multiple) and writes them sequentially starting from start_sector. It updates size and persists the entire directory again.
  • fs_read does the opposite: it calculates how many sectors the file occupies from size, reads them all, and copies only the valid bytes (not the zero padding) into the caller’s buffer.
  • fs_delete clears the entry without touching the data sectors (see just above).

None of these four functions support files growing beyond the free space they had when they were created – there is no concept of "extending" a file beyond whatever space was available when it was first written contiguously, because fs_write always writes starting from start_sector, assuming that the space has already been reserved by fs_create + the creation order of the other files.

Known Limitations

So I have it documented and it doesn’t get lost over time when I come back to implementing things in tOSh three years from now:

  • Fixed maximum filesystem size of FS_TOTAL_SECTORS = 2880 (the full 1.44MB of the floppy, without distinguishing that part of it is already used by the boot/kernel – making sure you don’t overwrite that is the responsibility of setting FS_DATA_START correctly, not something the filesystem checks at runtime). Still, maybe if I come back to touch or rewrite the filesystem, it would also be a good idea to implement support for hard drives and take that into account to make it a bit more serious. We’ll see.
  • Non-hierarchical filenames: FS_FILENAME_MAX = 32 bytes, with no path separator, everything lives in the same hardcoded namespace.
  • No file fragmentation: each file occupies a contiguous range of sectors. This simplifies the code enormously (there’s no need to track chains of blocks), but it means that large files in a filesystem with lots of holes from previous deletes will eventually not fit, even if there is plenty of "total space" spread across gaps. Eventually, after the filesystem has created and deleted enough files, it’s going to crash :)
  • No journaling or any kind of recovery mechanism for a power failure in the middle of an fs_save_directory(). If the directory write gets interrupted halfway through, you’re left with a partial version and you can even lose the entire filesystem because the superblock can get corrupted too.

Next time, we’ll run tOSh on several machines, emulated and real, borrowed and/or donated for that purpose by dear nerd friends and fellow geeks whom I respect and love very much.

References