MFM Write Sector
Writing Sectors to a Floppy Disk
This will go into the details of how to write a sector to a floppy disk using only a AVR microcontroller without any additional hardware, with just bit-banging the outputs and counting cycles.
Floppy Timing
When you write a sector on a floppy (or hard drive) you need to know the details of the format. Because when you write a sector to the floppy you write a complete record with header, data and trailer and you always need to write it more or less to the same position as the previously written data record.
The address records are left untouched, they only serve as positioning and addressing marks. Only when you format a floppy you write the address records with the appropriate address information. The data records written during format normally only contain dummy data. There are exceptions of course.
The data record consists of sync bytes, sync marks, data mark, data, CRC and some write splice bytes. This record needs to be written right after the gap which follows the address record. The gap has the function of compensating the jitter or rotational speed variations that can occur between different drives and within the same drive. The write splice bytes will blend into the gap which follows the data record. These gaps have been written during formatting.
Finding the Data Record Position
As with decoding MFM, encoding is just writing a series of write pulses
at a given interval. But first we need to find the correct position. For
this we have to go to the readsection routine. In fact in the real
source code you will find the following code at the beginning of the
readsection
routine.
sts TCCR1B, zero ; Stop Timer
sts TCCR1A, zero ; No Compare Mode, Normal Counter Mode
sbi TIFR1, TOV1 ; Clear timer overflow bit
;
; We use counter 1 in free running mode and with the clock divided
; by 8. Each byte in MFM requires 16usec so we need to need to wait
; about 430usec before we start to write, so we need to setup the
; initial timer with a value that sets the overflow bit after
;
ldi temp, high(-F_CPU/8*16*28/1000000)
sts TCNT1H, temp
ldi temp, low(-F_CPU/8*16*28/1000000)
sts TCNT1L, temp
This prepares timer 1 with a value that will allow us later to detect the right moment to start writing. If it is an address record this will have the address mark, track number, side number, sector number, sector length, two CRC bytes followed by a 22 byte gap of 0x4E. This is a total of 28 bytes. Later in the readsection routine when we have synchronized to the MFM bit-stream we will start the timer right after we have detected the sync marks.
clr miss ; Reset miss counter
ldi temp, (1<<CS11)
sts TCCR1B, temp ; Start Counter, prescaler 8
ldi count, 8 ; Initialise bit counter
;
; Here we are in phase with a "data" pulse
;
l0010:
The timer will then resume from the counter value we have set before and overflow, i.e. reach 0xFFFF, at the very moment a sector data record should be written. The timer is always started regardless whether the sync byte are from an address or a data record. In case we read a data record the timer is not used, even it is initialised and started, that is the calling program will of course just ignore the timer. Only when the calling program wants to write a record, we just have to call the readsection routine until we have read the address record for the sector we want to write and then wait for the overflow flag of timer1 to start writing the sector.
Writing the sector
Before we can write a sector we need to find the corresponding address record. Similar to when we wanted to read a specificy sector we need to read records from the floppy until we have found the address record of the sector we want to write. Again we just call readsection to read 8 bytes, check if the record is an address record and then check the CRC and the sector. As there is a gap of 22 bytes after the address record we have enough time, approx 350µs, to do all the checks and prepare for writing.
Encoding MFM
When we have found the address record of our sector we just need to wait for timer 1 to overflow and then start to write the data record encoded as MFM bit stream. Here is one possible way to creating the correct write pulses using a cycle accurate program. This example just uses bit-banging the WP and WD pins of the floppy interface. Of course there are more elegant ways to create the WD pulses.
write_waitgap:
write_waitgap010:
sbis TIFR1, TOV1
rjmp write_waitgap010
cbi PORTC, WG
;
; 96 short intervals ......SSSSSSSSSSSSSS
;
ldi count, 12*8
wp0010:
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 19
dec count
brne wp0010
nop
;
; followed by three Sync Marks MLMLMSLMLMSLMLM
;
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 22 ; S
;
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 22 ; S
;
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38 ; M
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 54 ; L
cbi PORTC, WD
w6cycles
sbi PORTC, WD
wcycles 38-6 ; M-shortened to compensate initialisation
;
; We use the following defintions
;
; T-bit Previous Clock Type '1'=data, '0'=clock
; Char The current byte
; Count Number of bits left in char
; Y Pointer to the buffer
; X Number of bytes to transmit (must be positive number)
;
; Initiate the state
;
set ; Sync Mark ends with a data pulse
clr count ; We have no bits in char
ldi xl, low(1+512+2+2)
ldi xh, high(1+512+2+2)
ldi yl, low(sectorbuffer)
ldi yh, high(sectorbuffer)
; We are now here and need to create the pulse to finishe the
; last M interval of the third SYNC MARK
; ------------->
; Data 1 0 1 0 0 0 0 1| 1
; Clock 0 0 0 1 1 1 0 |0
; MFM 100010010001001|01
;
;
; Logic to generate MFM is very simple. We need only to remember
; one state, that is if the previous pulse was a clock or a data
; pulse.
;
; get_next_bit()
; if (previous_pulse == clock_pulse)
; {
; if (next_data_bit == '0')
; {
; prev next
; ---- ----
; Data: 0| 0|
; Clock: 1 | 1 |
; MFM: 0100 0100
;
; interval=2usec
; previous_pulse=clock_pulse
; }
; else
; {
; prev next
; ---- ----
; Data: 0| 1|
; Clock: 1 | 0 |
; MFM: 0100 0001
;
; interval=3usec
; previous_pulse=data_pulse
; }
; }
; else
; {
; if (next_data_bit == '0')
; {
; get_next_bit()
; if (next_data_bit == '0')
; {
; prev next
; ---- ---- ----
; Data: 1| 0| 0|
; Clock: 0 | 0 | 1 |
; MFM: 0001 0000 0100
;
; interval=3usec
; prevous_pulse=clock_pulse
; }
; else
; {
;
; prev next
;
; ---- ---- ----
; Data: 1| 0| 1|
; Clock: 0 | 0 | 0 |
; MFM: 0001 0000 0001
;
; interval=4usec
; prevous_pulse=data_pulse
; }
; else
; {
; prev next
; ---- ----
; Data: 1| 1|
; Clock: 0 | 0 |
; MFM: 0001 0001
;
; interval=2usec
; previous_pulse=data_pulse
; }
; }
;
; We first generate a pulse using first a cbi some nop and a sbi
; In case we have a clock of 16MHz and we want to create pulses
; of 500ns this looks like the following.
;
w0000:
cbi PORTC, WD
rjmp PC+1 ; rjmp PC+1 uses one instruction word
rjmp PC+1 ; and 2 cycles, its 2 nops in one instruction
rjmp PC+1
sbi PORTC, WD
;
; Each pulse is followed by at least 1500nsec silence. This corresponds
; to 24 cycles. However the pulse generation already took 2 cycles so
; we have 22 cycles left.
; cycles
w0010:
dec count ; 1 More bits to go
brmi w0020 ; 2 Need a byte
nop ; Compensate branch not taken
rjmp PC+1 ; Instead of getting a new byte
rjmp PC+1 ; we need to waste time
rjmp w0030 ;
;
; Getting a new byte
; - if we reached the end of the buffer then we are done
; - else set the number of bits we need to process to 7
; because we are going to immediately process a bit
; - get the new byte
;
w0020:
sbiw X, 1 ; 2 More bytes to go
brmi w0090 ; 1 No we are done
ldi count, 7 ; 1 We will have 7 bits left
ld char, Y+ ; 2 Get next char from buffer
w0030:
lsl char ; 1 Get next bit to the Carry bit
; ==
; 10 cycles
; We now have 4 possibilities
; 25-02-2019 start to share code so the branch to w0090 reaches
; to the end of the block so we can immediately proceed with the
; GAP
;
; T=C=0
; T=0, C=1
; T=1, C=0
; T=C=1
;
; Some info about T-bit processing:
; If interval is an even number of usec then T does not change if it is
; odd it changes, in other words it changes whenever the interval is 3usec.
;
brts w0050 ; decision branch 1 or 2 cycle
brcc w0040 ; decision branch 1 or 2 cycle
;
;
; TC=0, C=1
;
; 2 cycles from two branches
; not taken
; and second branch taken
;
; Required Interval 3usec 48 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 2 cycles
; Cycles left 26 cycles
;
set ; 1
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
; ==
; 17 cycles and fall through
;
; T=C=0
;
;
w0040:
; 3 cycles from first branch
; not taken and second taken
;
; Required Interval 2usec 32 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 3 cycles
; Cycles left 9 cycles
;
;
nop ; 1
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp w0000 ; 2
; ==
; 9 cycles
;
w0050:
brcs w0080 ;
;
; T=1, C=0
;
; 3 cycles from first branch taken
; and second branch not taken
;
; We need another bit
;
dec count ; 1 More bits to go
brmi w0060 ; 2 Need a byte
nop ; compensate branch not taken
rjmp PC+1 ; Instead of getting a new byte
rjmp PC+1 ; we need to waste time
rjmp w0070
w0060:
sbiw X, 1 ; 2 More bytes to go
brmi w0100 ; 1 No we are done
ldi count, 7 ; 1 We will have 7 bits left
ld char, Y+ ; 2 Get next char from buffer
w0070:
lsl char ; 1
; ==
; 10 cycles
;
brcc w0075 ;
;
; Next bit is '1'
;
; Required Interval 4usec 64 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 3 cycles
; Check for new character -10 cycles
; Decision Branch - 1 cycle
; Cycles left 30 cycles
;
;
;
;
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp w0080 ; 2
; ==
; 22 and fall through
;
; Next bit is '0'
;
; Required Interval 3usec 48 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 3 cycles
; Check for new character -10 cycles
; Decision Branch - 2 cycle
; Cycles left 13 cycles
;
w0075:
clt ; 1
rjmp PC+1 ; 2
rjmp PC+1 ; 2
; ==
; 5 and fall through
;
w0080:
;
; T=C=1
;
; 4 cycles from both branch taken
;
; Required Interval 2usec 32 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 4 cycles
; Cycles left 8 cycles
;
; So far we have spent 14 cycles (10 cycles from the buffer check and 2 x 2
; cycles from branches taken). Previous pulse was a data pulse and next
; bit is '1' so the interval is usec, so we need to waste 8 cycles
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp PC+1 ; 2
rjmp w0000 ; 2
; ==
; 8
;---------------------------------------------------------
;
; All data bits written, add gap
;
; a) when we arrive from label w0020, if the gap byte
; would be part of the buffer the lsl at w0030 would
; set C=0
;
;
w0090:
brtc w0091
;
; T=1, C=0 this case requires another bit and we know
; this bit is the second bit of the gap byte 0x4E, i.e.
; this bit is '1'
;
; Required Interval 4usec 64 cycles
; Pulse creation -10 cycles
; Check for last character - 7 cycles
; Branch not taken - 1 cycles
; Cycles left 46 cycles
rjmp w0105 ; 2
w0091:
;
; T=0, C=0
;
;
; Required Interval 2usec 32 cycles
; Pulse creation -10 cycles
; Check for last character - 7 cycles
; Branch taken - 2 cycles
; Cycles left 13 cycles
;
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
nop
cbi PORTC, WD
rjmp PC+1
rjmp PC+1
rjmp PC+1
sbi PORTC, WD
;
; Then we need to process the next bit
; T=0, C=1
;
; Required Interval 3usec 48 cycles
; Pulse creation -10 cycles
; Cycles left 38 cycles
;
rjmp w0110 ; 2
;
; b) when we arrive from label w0060, if the gap byte
; would be part of the buffer the lsl at w0070 would
; set C=0, which is the same as to proceed to w0075
w0100:
;
; Required Interval 3usec 48 cycles
; Pulse creation -10 cycles
; Check for new character -10 cycles
; Decision Branches - 3 cycles
; Check for last character - 7 cycles
; Cycles left 18 cycles
;
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
cbi PORTC, WD
rjmp PC+1
rjmp PC+1
rjmp PC+1
sbi PORTC, WD
;
; Then we need to process the next bit
; T=0 (as would be the case at w0075), C=1
;
; Required Interval 3usec 48 cycles
; Pulse creation -10 cycles
; Cycles left 38 cycles
;
rjmp w0110 ; 2
w0105:
rjmp PC+1
rjmp PC+1
rjmp PC+1
rjmp PC+1
w0110:
wcycles 36
cbi PORTC, WD
rjmp PC+1
rjmp PC+1
rjmp PC+1
sbi PORTC, WD
nop
nop
nop
nop
sbi PORTC, WG
ret
Floppy Formats
The MFM read and write routines assume that we use the standard MS-DOS floppy
format. This has been derived from the IBM System/34 floppy format. That is
the reason why we use exactly three sync marks. Also this format assumes that
the gap between the address record and the data record consists of exactly
22 bytes of 0x4E
. Most FDC assume this as well.
Knowing this you could actually define your own floppy format. You could reduce the number of gap bytes, change the way you calculate the CRC and also define a different number of sync marks. Also it is possible to use your own coding for e.g. address or data records.
Another field you often found on older floppy formats was the index mark. In order to read or write sectors this is not needed. In fact they are not required at all.