FPGABee v3.0

Disk Geometry

The main inspiration for build FPGABee version 2 came from the realization that I didn't need to use the FAT file system on the SD card for disk images. The complexity with FAT comes from the fact that files can become fragmented making it difficult to calculate the location of a virtual disk sector in hardware.

By dropping support for FAT and using a custom file system that guarantees that virtual disk images are never fragmented (ie: always contiguous) implementing the disk controller entirely in hardware becomes alot easier.

But, it still needs a way to map the sector/head/track that the Microbee is asking for onto an offset into the virtual disk image. In this post I talk about FPGABee's DiskGeometry component that is responsible for these calculations.

Supported Disk Formats

The first task in building the DiskGeometry component was settling on a set of raw virtual disk image formats that will be supported. A raw disk image is one that is an exact sector by sector copy of the original disk, with no additional meta data. The list of support formats boils down to this:

  • DS40 - 40 tracks, 2 heads, 10 sectors per track
  • SS80 - 80 tracks, 1 head, 10 sectors per track
  • DS80 - 80 tracks, 2 heads, 10 sectors per track (data track 2)
  • DS82 - 80 tracks, 2 heads, 10 sectors per track
  • DS84 - 80 tracks, 2 heads, 10 sectors per track
  • DS8B - 80 tracks, 2 heads, 10 sectors per track (data track 2, data sector 21)
  • HD0 - 306 tracks, 4 heads, 17 sectors per track
  • HD1 - 80 tracks, 4 heads, 63 sectors per track

The data track and data sector variables control how sector numbers are mapped into the image.

Note that these disk formats are the exact same formats as used by ubee512. Unfortunately the DSK format isn't supported since it's not a raw disk image. Most DSKs can be converted to one of the above formats though.

Note too, that all of the above disk formats have a sector size of 512 bytes - which very conveniently maps exactly onto the size of a SD card block.

Sector/Head/Track to Block Number Calculation

Next was to work out the equations for converting a sector, head and track number to a block offset into the virtual disk image. The source code for ubee512 helped out with this, and for formats FPGABee supports boils down to the following pseudo code:

if TrackNumber >= DataTrack
    PhysicalSector = SectorNumber - DataSector
else
	PhyiscalSector = SectorNumber

BlockNumber = TrackNumber * HeadCount * SectorsPerTrack  +  HeadNumber * SectorsPerTrack  + PhysicalSector

Multiplication in Hardware

Looking at the above pseudo code, the bit that stands out as being tricky to implementing hardware are the two multiplications - especially since they're not powers of two (ie: 17 and 63 for sectors per track).

Although the Xilinx synthesis tools can infer multipliers from VHDL, I decided to use a CoreGen generated multiplier for this (mainly because I don't have alot of experience with multipliers and I just wanted to get it up and running), which generated the following entity:

ENTITY DiskControllerMultiplier IS
  PORT (
    clk : IN STD_LOGIC;
    a : IN STD_LOGIC_VECTOR(8 DOWNTO 0);
    b : IN STD_LOGIC_VECTOR(5 DOWNTO 0);
    p : OUT STD_LOGIC_VECTOR(14 DOWNTO 0)
  );
END DiskControllerMultiplier;

The size of the operands were selected to be just big enough for the values I need to multiply and I used the CoreGen suggested optimum number of pipeline stages of three. Three pipeline stages means that after supplying the two operands to inputs a and b, the product is available at p three clock cycles later.

Note: by adjusting the number of pipeline stages you can tweak logic size vs speed of calculation vs maximum clock speed of the entire design. This is not critical decision for FPGABee's humble disk controller however.

The other thing to note about the multiplier is that after the first two operands are supplied in one clock cycle, another two operands can be supplied on the next clock cycle and the multiplier can effectively do the two multiplications simulataneously. ie: it takes 3 clock cycles to do one multiplication, but 4 clock cycles to do two multiplications.

Bringing it all together

So bringing everything from above together we have, firstly a way to generate the geometry metrics of a particular disk type:

case disk_type is

	when DISK_DS40 =>
		disk_tracks <= "000101000";		-- 40
		disk_heads <= "010";				-- 2
		disk_sectors <= "001010";			-- 10

	when DISK_SS80 =>
		disk_tracks <= "001010000";		-- 80
		disk_heads <= "001";				-- 1
		disk_sectors <= "001010";			-- 10

	-- etc...

Some simple arithmetic to calculate the phyiscal sector offset:

-- Calculate the actual sector offset within the track
process (masked_track, disk_data_track, masked_sector, disk_data_sector)
begin
	if unsigned(masked_track) >= unsigned(disk_data_track) then
		sector_offset <= std_logic_vector(unsigned(masked_sector) - unsigned(disk_data_sector));
	else
		sector_offset <= std_logic_vector(unsigned(masked_sector) - 1);
	end if;
end process;

A simple state machine to multiply the TrackNumber by SectorsPerTrack and HeadNumber by SectorsPerTrack:

case state is

	when STATE_READY =>

		if invoke='1' then
			mult_a <= masked_track;
			state <= MUL_PIPE_1;
		end if;

	when MUL_PIPE_1 =>
		state <= MUL_PIPE_2;
		mult_a <= "000000" & head;

	when MUL_PIPE_2 =>
		state <= MUL_PIPE_3;

	when MUL_PIPE_3 =>
		state <= MUL_PIPE_4;

	when MUL_PIPE_4 =>
		track_times_sector_per_track <= mult_result;
		state <= MUL_PIPE_5;

	when MUL_PIPE_5 =>
		head_offset <= mult_result(7 downto 0);
		state <= STATE_READY;

end case;

Some bit shifting to do the multiplication by head count (which is always a power of two multiplication)

case disk_heads is

	when "001" =>
		-- *1
		track_offset <= "00" & track_times_sector_per_track;

	when "010" =>
		-- *2
		track_offset <= "0" & track_times_sector_per_track & "0";

	when "100" =>
		-- *4
		track_offset <= track_times_sector_per_track & "00";

	when others =>
		track_offset <= (others => '0');

end case;

And finally add it all up to give the final block offset within the disk image:

block_offset <= std_logic_vector(unsigned(track_offset) + unsigned(head_offset) + unsigned(sector_offset));

Finished Component and Testing

The finished component has the interface shown below and was tested in the simulator by comparing some pre-canned inputs/outputs.

entity DiskGeometry is
	port
	(
		reset : in std_logic;
		clock : in std_logic;
		invoke : in std_logic;							-- Assert to start calculation
		ready : out std_logic;							-- Asserts when calculation finished
		error : out std_logic;							-- Indicates track/head/sector out of range

		disk_type : in std_logic_vector(3 downto 0);	-- DISK_xxx constant
		track : in std_logic_vector(15 downto 0);		-- 0 - 511  supported
		head : in std_logic_vector(2 downto 0);			-- 0 - 3    supported
		sector : in std_logic_vector(7 downto 0);		-- 0 - 63   supported

		cluster : out std_logic_vector(16 downto 0)		-- Calculated cluster offset
	);
end DiskGeometry;

The next step will be to start on the disk controller itself.