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
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.