CP/M is a disk operating system. It relies on one or more “disks” for permanent storage, which persists while the computer is off. At the time, 8” floppy drives had just started to become available, at “affordable” prices (under a thousand US dollars).

The actual media, i.e. the floppies inserted into these drives, were very crude and easily damaged. Not only that, but an 8” single-sided single-density (SSSD) disk could hold only a small amount of data: 77 tracks with 26 sectors each, containing 128 bytes of “payload”. That’s a grand total of 250.25 raw kilobytes, leaving a mere 241 KB for actual file storage.

Floppy technology evolved quickly, from single- to double-sided, from single- to double-density, and then from 8” to 5.25” to 3.5”, gradually increasing storage size to 1.44 MB:

How archaic it all looks, compared to a fingernail-sized µSD card with 10’s of gigabytes!

But floppies were the state of the art at the time when CP/M was developed, which is why it uses 128-byte sector buffers and was distributed on 8” SSSD floppy disks.

Virtual floppies

With today’s flash memory, magnetic floppy disk media have become totally outdated. Not only is flash memory much more compact and durable, it’s also incomparably faster. An 8” floppy disk rotates at 300 RPM, i.e. 5 revolutions per second. In the worst case, access to a single 128-byte sector will take 200 ms. In addition, track seek times were in the tens of milliseconds. Yes, floppy disks were really s-l-o-w … !

I’m not very interested in the mechanical limitations of the 1970’s, nor in the clunky, noisy, and unreliable properties of floppy disks. Re-implementing those is more expensive and cumbersome than switching to more modern options. Retro, sure … but up to a point!

There are three simple ways to get permanent storage on a modern µC:

For a first design, I’m going to use internal flash. That way, the application won’t require the Black F407 board, specifically. The DIYmore board will also work, for example.

Internal flash memory

The F407 has 512..1024 KB of internal flash memory. A small portion is needed to store the emulator firmware. This is currently about 20 KB, so a generous allocation of 64 KB for the code will be fine, leaving ample room for additional features later.

That leaves 448 KB of flash memory to store a virtual floppy disk image. This might seem plenty, but it’s only just enough to implement one virtual 8” SSSD. The reason is that flash memory has one limitation: it can only be erased in “pages”. The F407’s page sizes are:

As already mentioned, the lower 64K need to be reserved for the emulator code.

If we take the 512K version of the F407 as baseline, that leaves 1x 64K and 3x 128K memory “segments” which can be independently erased. This is not very convenient for emulating a floppy drive which must be able to erase individual 128-byte sectors!

Another issue is that flash memory supports only a limited number of erase / write cycles. In the F407 µC, this is in the order of 10,000 cycles per page. After that, some weak spots in flash memory might cause writes to fail. The same applies to SPI flash and SD cards.

Wear leveling

There is a way to overcome both limitations, based on a technique called wear leveling.

The idea is very simple: write new data sequentially to a freshly erased section of flash memory, and keep track of these changes. When a sector is read, we then return either the original one, or the latest change if there is one. The physical layout of sectors in memory is no longer the same as the logical layout presented to the application.

At some point, we will run out of free space in flash memory, and need to reclaim unused sectors. I’m going to use a simple-but-effective algorithm for this:

Not quite by coincidence, this approach uses all the available segments and creates a virtual disk of 256 KB, i.e. large enough to contain one virtual 8” SSSD disk image:

There will be a brief, but noticable (≈ 1 second), hiccup when this algorithm decides to clean up and prepare the 64K segment for new sector changes. During this time, sectors will get copied from one 128K segment to another, with the latest changes applied.

This is essentially a form of garbage collection with “partial from-/to-space copying”.

Since the 64K segment can store over 500 sectors of 128-bytes, the effect is that erase cycles have also been reduced by a factor of 500. So with this approach, flash memory will only reach its 10K cycle limit after 5 million sector writes. That’s good enough for me.

Note that cleanup is independent of power cycling: the changes in the 64K segment also persist, and we can simply continue adding changed sectors to it as long as there is room.

Implementation

Code for the above algorithm can be found here. The API boils down to:

class FlashWear {
public:
    bool valid       ();
    int  init        (bool erase =false);
    void readSector  (int pos, void* buf);
    void writeSector (int pos, void const* buf);
};

For an F407VE with 512K flash, there are 2048 sectors available for reading and writing. For an F407VG with 1024K flash, this increases to 6144 sectors. Only a single spare 128K segment is required for cleanup, regardless of how many segments are in flash.

That’s enough for 1 to 3 virtual 8” SSSD disk images. All on-chip, i.e. inside the F407.