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.