--+-----------------------------------+-------------------------------------+-- --| ___ ___ | (c) 2013-2014 William R Sowerbutts |-- --| ___ ___ ___ ___( _ ) / _ \ | will@sowerbutts.com |-- --| / __|/ _ \ / __|_ / _ \| | | | | |-- --| \__ \ (_) | (__ / / (_) | |_| | | A Z80 FPGA computer, just for fun |-- --| |___/\___/ \___/___\___/ \___/ | |-- --| | http://sowerbutts.com/ |-- --+-----------------------------------+-------------------------------------+-- --| 4K paged Memory Management Unit: Translates 16-bit virtual addresses |-- --| from the CPU into 26-bit physical addresses to allow more memory to be |-- --| addressed. Also has a hack to allow unmapped physical memory to be |-- --| accessed through an IO port which synthesises memory operations. |-- --+-------------------------------------------------------------------------+-- -- -- The MMU takes a 16-bit virtual address from the CPU and divides it into a -- 4-bit frame number and a 12-bit offset. The frame number is used as an index -- into an array of sixteen registers which contain the hardware page numbers -- (the translation table). The physical address is then formed from the -- hardware page number concatenated with the 12-bit offset. My hardware page -- numbers are 14-bits long because I wanted a 64MB physical address space but -- you could use any length you wished. -- -- So if the virtual address 0xABCD is accessed, we'd divide that into frame -- number 0xA (decimal 10), offset 0xBCD. If the 10th MMU translation register -- contains 0x1234 then the translated physical address would be 0x1234BCD. -- -- My MMU is programmed using 8 I/O ports in the range 0xF8 through 0xFF. The -- chip select line is asserted for any access in that range and the MMU -- decodes the low three bits to determine which register is being accessed. -- -- The port at 0xF8 is effectively a mux which selects the function of ports -- 0xFB through 0xFF. -- -- Writing 0x00 through 0x0F to the function register at 0xF8 allows you to -- read/write one of the 16 MMU translation registers. With these selected; -- - 0xFC contains the high byte of the physical address -- - 0xFD contains the low byte of the physical address -- - 0xFB contains permission bits (read/write/execute, currently ignored) -- -- So updating a mapping generally requires just three I/O writes: One to 0xF8 -- to select which frame to modify, and one each to 0xFC and 0xFD to write out -- the new translation. The permission bits are programmable but currently -- ignored (I had planned to add some level of memory protection to UZI one day) -- -- The "17th page" is a bit of a hack bolted on. The lazy programmer in me finds -- it much more convenient to sometimes access memory without remapping a frame; -- in particular you don't have to select which frame to remap such as to avoid -- remapping the memory pointed to by PC, SP or your source/target pointer. -- -- Writing 0xFF to port 0xF8 selects the "17th page pointer". This is a 26-bit -- register but again could be wider/narrower as required. With this selected -- ports 0xFC, 0xFD, 0xFE, 0xFF are the register contents (with the high byte in -- 0xFC, low byte in 0xFF). -- -- When the CPU reads or writes the I/O port 0xFA the MMU translates the I/O -- operation into a memory operation. The physical memory address accessed is -- the address contained in the 17th page pointer register. After the memory -- operation completes the 17th page pointer register is incremented so that -- repeated accesses to 0xFA walk forward through memory. -- -- -- MMU registers: -- -- base = 0xF8 in standard socz80 system -- -- base+0 mux frame select (write 00..0F to select frame, FF to select "17th page" pointer) -- base+1 (unused) -- base+2 17th page (read/write will address pointed memory and post-increment the pointer) -- base+3 page permissions -- base+4 page address (high byte) / ptr address (high byte) -- base+5 page address (low byte) / ptr address -- base+6 ptr address -- base+7 ptr address (low byte) -- -- Basic operation of the MMU (older documentation, concise but still correct) -- For memory requests: -- The top 4 bits of CPU logical address are replaced with 14 bits taken -- from the MMU translation table entry indexed by those top four bits. -- For IO requests: -- Reads or writes to I/O port at (base+2) are converted into memory -- requests to the address pointed to by "the 17th page", a pointer which -- can be accessed by writing 0xFF to the frame select register (base+0) -- and then programming a 26-bit address into I/O ports FC FD FE FF. Each -- I/O request to port F/A results in the pointer being post-incremented -- by 1, which means that INIR our OUTIR instructions can read/write blocks -- of memory outside of the CPU logical address space through this port. -- -- Note that the MMU has to insert a forced CPU wait state to give the -- addressed memory device time to read the new address off the bus. -- library IEEE; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; entity MMU is port( clk : in std_logic; reset : in std_logic; address_in : in std_logic_vector(15 downto 0); address_out : out std_logic_vector(25 downto 0); cpu_data_in : in std_logic_vector(7 downto 0); cpu_data_out : out std_logic_vector(7 downto 0); cpu_wait : out std_logic; req_mem_in : in std_logic; req_mem_out : out std_logic; req_io_in : in std_logic; req_io_out : out std_logic; io_cs : in std_logic; req_read : in std_logic; req_write : in std_logic; access_violated : out std_logic ); end MMU; architecture behaviour of MMU is -- each MMU entry looks like this type mmu_entry_type is record frame: std_logic_vector(13 downto 0); can_read: std_logic; can_write: std_logic; end record; -- the whole MMU state looks like this type mmu_entry_array is array(natural range <>) of mmu_entry_type; -- and here's our instance signal mmu_entry : mmu_entry_array(0 to 15); -- 17th page pointer (frame FF) signal mmu_frame_ff_pointer : std_logic_vector(25 downto 0); -- IO interface signal cpu_entry_select : std_logic_vector(7 downto 0); -- break up the incoming virtual address alias frame_number : std_logic_vector( 3 downto 0) is address_in(15 downto 12); alias page_offset : std_logic_vector(11 downto 0) is address_in(11 downto 0); signal map_io_to_ff_ptr : std_logic; signal was_map_io_to_ff_ptr : std_logic := '0'; begin map_io_to_mem_proc: process(address_in, req_mem_in, req_io_in) begin if req_mem_in = '0' and req_io_in = '1' and address_in(7 downto 0) = "11111010" then map_io_to_ff_ptr <= '1'; else map_io_to_ff_ptr <= '0'; end if; end process; with map_io_to_ff_ptr select address_out <= mmu_entry(to_integer(unsigned(frame_number))).frame & page_offset when '0', mmu_frame_ff_pointer when '1'; with map_io_to_ff_ptr select req_mem_out <= req_mem_in when '0', '1' when '1'; with map_io_to_ff_ptr select req_io_out <= req_io_in when '0', '0' when '1'; with map_io_to_ff_ptr select access_violated <= req_mem_in and ((req_read and not mmu_entry(to_integer(unsigned(frame_number))).can_read) or (req_write and not mmu_entry(to_integer(unsigned(frame_number))).can_write)) when '0', '0' when '1'; -- force CPU to wait one cycle when we map IO to memory access; this -- is in order to give our synchronous memories a cycle to read the -- address, look up the data in synchronous memory, and provide a result. cpu_wait <= map_io_to_ff_ptr and (not was_map_io_to_ff_ptr); data_out: process(address_in, cpu_entry_select, mmu_entry, mmu_frame_ff_pointer) begin if cpu_entry_select = "11111111" then -- pointer (FF) case address_in(2 downto 0) is when "000" => cpu_data_out <= cpu_entry_select; when "011" => cpu_data_out <= "00000011"; when "100" => cpu_data_out <= "000000" & mmu_frame_ff_pointer(25 downto 24); when "101" => cpu_data_out <= mmu_frame_ff_pointer(23 downto 16); when "110" => cpu_data_out <= mmu_frame_ff_pointer(15 downto 8); when "111" => cpu_data_out <= mmu_frame_ff_pointer(7 downto 0); when others => cpu_data_out <= "00000000"; end case; else -- 16 frames (00 .. 0F) case address_in(2 downto 0) is when "000" => cpu_data_out <= cpu_entry_select; when "011" => cpu_data_out <= "000000" & mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).can_write & mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).can_read; when "100" => cpu_data_out <= "00" & mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(13 downto 8); when "101" => cpu_data_out <= mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(7 downto 0); when others => cpu_data_out <= "00000000"; end case; end if; end process; mmu_registers: process(clk) begin if rising_edge(clk) then if reset = '1' then mmu_entry( 0).frame <= "10000000000000"; -- first page of SRAM (monitor ROM) mmu_entry( 1).frame <= "00000000000001"; -- DRAM page 1 mmu_entry( 2).frame <= "00000000000010"; -- DRAM page 2 mmu_entry( 3).frame <= "00000000000011"; -- DRAM page 3 mmu_entry( 4).frame <= "00000000000100"; -- DRAM page 4 mmu_entry( 5).frame <= "00000000000101"; -- DRAM page 5 mmu_entry( 6).frame <= "00000000000110"; -- DRAM page 6 mmu_entry( 7).frame <= "00000000000111"; -- DRAM page 7 mmu_entry( 8).frame <= "00000000001000"; -- DRAM page 8 mmu_entry( 9).frame <= "00000000001001"; -- DRAM page 9 mmu_entry(10).frame <= "00000000001010"; -- DRAM page 10 mmu_entry(11).frame <= "00000000001011"; -- DRAM page 11 mmu_entry(12).frame <= "00000000001100"; -- DRAM page 12 mmu_entry(13).frame <= "00000000001101"; -- DRAM page 13 mmu_entry(14).frame <= "00000000001110"; -- DRAM page 14 mmu_entry(15).frame <= "10000000000001"; -- second page of SRAM mmu_entry( 0).can_read <= '1'; mmu_entry( 1).can_read <= '1'; mmu_entry( 2).can_read <= '1'; mmu_entry( 3).can_read <= '1'; mmu_entry( 4).can_read <= '1'; mmu_entry( 5).can_read <= '1'; mmu_entry( 6).can_read <= '1'; mmu_entry( 7).can_read <= '1'; mmu_entry( 8).can_read <= '1'; mmu_entry( 9).can_read <= '1'; mmu_entry(10).can_read <= '1'; mmu_entry(11).can_read <= '1'; mmu_entry(12).can_read <= '1'; mmu_entry(13).can_read <= '1'; mmu_entry(14).can_read <= '1'; mmu_entry(15).can_read <= '1'; mmu_entry( 0).can_write <= '0'; mmu_entry( 1).can_write <= '1'; mmu_entry( 2).can_write <= '1'; mmu_entry( 3).can_write <= '1'; mmu_entry( 4).can_write <= '1'; mmu_entry( 5).can_write <= '1'; mmu_entry( 6).can_write <= '1'; mmu_entry( 7).can_write <= '1'; mmu_entry( 8).can_write <= '1'; mmu_entry( 9).can_write <= '1'; mmu_entry(10).can_write <= '1'; mmu_entry(11).can_write <= '1'; mmu_entry(12).can_write <= '1'; mmu_entry(13).can_write <= '1'; mmu_entry(14).can_write <= '1'; mmu_entry(15).can_write <= '1'; mmu_frame_ff_pointer <= "10000000000000000000000000"; -- map first byte of ROM to pointer on reset was_map_io_to_ff_ptr <= '0'; else was_map_io_to_ff_ptr <= map_io_to_ff_ptr; if io_cs = '1' and req_write = '1' then case address_in(2 downto 0) is when "000" => cpu_entry_select <= cpu_data_in; when "011" => if cpu_entry_select(7 downto 4) = "0000" then mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).can_read <= cpu_data_in(0); mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).can_write <= cpu_data_in(1); end if; when "100" => if cpu_entry_select(7 downto 4) = "0000" then mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(13 downto 0) <= cpu_data_in(5 downto 0) & mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(7 downto 0); elsif cpu_entry_select = "11111111" then mmu_frame_ff_pointer <= cpu_data_in(1 downto 0) & mmu_frame_ff_pointer(23 downto 0); end if; when "101" => if cpu_entry_select(7 downto 4) = "0000" then mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(13 downto 0) <= mmu_entry(to_integer(unsigned(cpu_entry_select(3 downto 0)))).frame(13 downto 8) & cpu_data_in(7 downto 0); elsif cpu_entry_select = "11111111" then mmu_frame_ff_pointer <= mmu_frame_ff_pointer(25 downto 24) & cpu_data_in(7 downto 0) & mmu_frame_ff_pointer(15 downto 0); end if; when "110" => if cpu_entry_select = "11111111" then mmu_frame_ff_pointer <= mmu_frame_ff_pointer(25 downto 16) & cpu_data_in(7 downto 0) & mmu_frame_ff_pointer(7 downto 0); end if; when "111" => if cpu_entry_select = "11111111" then mmu_frame_ff_pointer <= mmu_frame_ff_pointer(25 downto 8) & cpu_data_in(7 downto 0); end if; when others => -- nothing end case; elsif map_io_to_ff_ptr = '0' and was_map_io_to_ff_ptr = '1' then -- post-increment our pointer (this is what makes "the 17th page" efficient!) mmu_frame_ff_pointer <= std_logic_vector(unsigned(mmu_frame_ff_pointer) + 1); end if; end if; end if; end process; end;