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
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.
- A 2us pulse represents the binary
0b10
- A 3us pulse represents the binary
0b100
- A 4us pulse represents the binary
0b1000
# 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:
- fdd_read_index - A function to sense the INDEX pin
- fdd_read_pulse - A function to parse the next pulse from the DATA pin
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:
- The track being wrong
- Completing a full revolution without finding your sector
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