floppy.cafe

Functions

Let's explore some code (mostly psuedocode) for a lot of different functions that you may need to implement when writing a floppy device driver.

Table of Contents

Power On Function
Motor On Function
Step Track Function
Seek Track 00 Function
Read Pulse Function
Synchronization Function
Read Data Function
Write Data Function

Power On Function

When powering on the device, you should configure the I/O direction for each pin and immediately pull them all HIGH. This will prevent accidental writes and other undefined behavior.

// The output pins
// These are the ones you control
pin_out(DRIVE_PIN, HIGH);
pin_out(MOTOR_PIN, HIGH);
pin_out(DIR_PIN, HIGH);
pin_out(STEP_PIN, HIGH);
pin_out(HEAD_SEL_PIN, HIGH);
pin_out(GATE_PIN, HIGH);
pin_out(WRITE_PIN, HIGH);

pin_mode(DRIVE_PIN, Output);
pin_mode(MOTOR_PIN, Output);
pin_mode(DIR_PIN, Output);
pin_mode(STEP_PIN, Output);
pin_mode(HEAD_SEL_PIN, Output);
pin_mode(GATE_PIN, Output);
pin_mode(WRITE_PIN, Output);

// The input pins
// These are the ones the floppy drive controls
pin_mode(INDEX_PIN, INPUT_PULLUP);
pin_mode(TRACK00_PIN, INPUT_PULLUP);
pin_mode(WRITE_PROTECT_PIN, INPUT_PULLUP);
pin_mode(READY_PIN, INPUT_PULLUP);
pin_mode(READ_PIN, INPUT_PULLUP);

Motor On Function

Spinning up the motor is pretty simple, just pull the Motor On pin to logic LOW and wait 500ms. For good measure, you may want to also wait for a logic level transition on the Index pin to indicate that one complete revolution has been achieved. This is a good indicator that everything is working successfully.

In some floppy drives, they'll have a mode called "Automatic Calibration" which just means that it will automatically seek track0 when it turns on. I don't believe this is guaranteed functionality across drives, so you may want to seek_track_00() manually if the Track00 pin is HIGH.

Step Track Function

In common 3.5" floppy media, there are 80 tracks (also called cylinders). To step along a given track, you first configure the Direction Select pin and then pulse the Step pin. 3ms LOW and then 3ms HIGH. This will move the track stepper motor by a single step along its configured path.

Now here's the fun part: over/under stepping can happen. The timing isn't consistent across devices and sometimes the motor just skips for whatever reason. If you're trying to read data, you should inspect the Sector Metadata to ascertain what track you're actually on and adjust accordingly if it's not what you think it should be.

Seek Track 00 Function

This algorithm will step outwards 100 times, looking for a LOW pulse on the Track00 pin. If it can't find any pulse, it'll step inwards a few more times for good measure.

def seek_track00:
    set_direction(HIGH)

    # Move outwards 100 steps until we sense the track00 signal
    for _ in 0 .. 100:
        if (read_track00_pin() == 0):
            return true
        step()
    
    # Well that didn't work, move inwards a bit for funzies
    set_direction(LOW)
    for _ in 0 .. 20:
        if (read_track00_pin() == 0):
            return true
        
        step()
    
    # Can't find the track
    return false

Read Pulse Function

Reading a pulse from the data line requires knowing exactly how much time has elapsed between the leading edge of the LOW signal to the trailing edge of the HIGH signal.

You can string these pulses together and the result will be an MFM encoded signal where every even bit is data and every odd bit is possibly a clock pulse (sometimes omitted).

# 2.5 microseconds (in clock cycles)
T_25 = 2.5 * F_CPU_HZ / 1000000

# 3.5 microseconds (in clock cycles)
T_35 = 3.5 * F_CPU_HZ / 1000000

def read_symbol:
  counter = 0
while data_pin is low:
  counter++
while data_pin is high:
  counter++

if counter < T_25:
  return 0
else if counter < T_35:
  return 1
else 
  return 2

Synchronization Function

The synchronization function doesn't have to be implemented in assembly, but that's what I did for performance reasons. This code assumes the following functions exist:

If all goes well, this function will return immediately after reading the last pulse which indicates you have found a barrier. Important distinction: this will return for either a sector barrier or a data barrier.

@ This method will synchronize the clock with
@ 12 bytes of 0x0 
@ 3 bytes of 0xA1 
_asm_sync:
    push {{r0,r1,lr}}
    mov r1, #0

    full_restart:
    @ Check if we're past the index
    bl fdd_read_index
    cmp r0,#0
    beq err

    restart:
    @ If we encounter something that isn't a short pulse
    @ first check if we've collected enough to indicate
    @ that we're ready for signal processing.
    cmp r1,#60
    bge process_signal
    
    @ Otherwise, do the actual restart
    mov r1,#0

    s80_loop: @ Short 80 pulses
    @ Read a symbol, if its a short, increment
    @ otherwise, reset. Compare with #80 and
    @ if matching, then break to next segment

    bl fdd_read_pulse
    cmp r0,#0
    bne restart @ restart if its not 0
    add r1,#1
    cmp r1,#80
    b s80_loop @ loop if we're less than 80 yet

    @ If we get here, we've found 80 pulses
    @ M L M L M S L M L M S L M L M
    process_signal:

    @ Reset the short pulses
    mov r1,#0

    @ When we get here, we've still got one pulse in the buffer
    @ so we can directly evaluate it.
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#0 @ S
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#0 @ S
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#2 @ L
    bne full_restart

    bl fdd_read_pulse
    cmp r0,#1 @ M
    bne full_restart

    mov r0,#1
    pop {{r0,r1,pc}}

    @ This condition signifies that we've
    @ hit an index loop and must terminate
    @ during this cycle.
    err:
    mov r0,#0
    pop {{r0,r1,pc}}

Read Data Function

Assuming you've implemented MFM encoding elsewhere, there's still a bit of work you have to do in order to read data. The most pressing concern is detecting the various error conditions. Mainly:

If you're on the wrong track, you will need to calculate the difference and step the head.

def read_sector(head, cylinder, sector):
      set_track(cylinder)
      set_side(head)
  
      latch = False
      err = 0
  
      while err < 36:
          if mfm_sync():
              latch = False
              buf = read_bytes(22)
              if buf[0] == 0xFE and buf[1] == cylinder && buf[2] == head and buf[3] == sector:
                  mfm_sync()
                  return read_bytes(512)
              elif buf[0] == 0xFE && buf[1] != cylinder:
                  # We're on the wrong track! Calculate the 
                  # difference and move to the new spot
                  fix_track(cylinder, buf[1])
          elif latch == False:
              err += 1
              latch = True
  

Write Data Function

Assuming you've implemented MFM encoding elsewhere, there's a lot of work you'll still have to do to get in position. The timing is absolutely critical here. If you look at the Sector Metadata spec, you'll notice there are 22 bytes of 0x4E after the metadta and before the userdata. This is basically a time buffer for you do all the verification in order to make sure you're at the right place. Then you can invoke mfm_sync() to wait for the next barrier that indicates you're in the right spot.

NOTE: In my code, I skip the first flux signal. If your prefix byte is 0xFB or 0xFA, the first signal will always be a short pulse and that initial pulse is a byproduct of the barrier code. So you need to effectively skip the first pulse you prepared since it's already accounted for by the barrier.

def write_sector(head, cylinder, sector, data):
      # The algorithm will work like so:
      # First, seek the sector we want and then read the first 60 bytes
      # which are the metadata. Compare with target. If approved then
      # write based on timing.
      set_side(head);
      set_track(cylinder);
      
      error = 0
      latch = False
      flux_signals = mfm_prepare_write(0xFB, data)
      
      while error < 10:
          if mfm_sync():
              latch = False
  
              # Read the first 15 bytes
              sector_buf = read_bytes(15)
              if sector_buf[0] == 0xFE and sector_buf[1] != cylinder:
                  # Wrong track.
                  fix_track(cylinder, sector_buf[1])
              elif sector_buf[0] == 0xFE and 
                   sector_buf[1] == cylinder and 
                   sector_buf[2] == head and 
                   sector_buf[3] == sector:
                  # We are in the right spot, wait for the data barrier and then begin writing
                  if mfm_sync():
                      mfm_write_bytes(flux_signal[1..len(flux_signals)])
                      return True
          
          if read_index() == 0 and latch == False:
               latch = True
               error += 1
      
      return False