diff --git a/README.MD b/README.MD index 6c22d27..a9df3a3 100644 --- a/README.MD +++ b/README.MD @@ -4,7 +4,7 @@ Trashernet is a very trashy Ethernet stack for FPGAs written in VHDL aiming to cover all OSI layers: -* Layer 1, Physical: `trashernet_phy`, (`trashernet_rmii`) +* Layer 1, Physical: `trashernet_phy`, (`trashernet_rmii`, `trashernet_mii`) * Layer 2, Data link: `trashernet_mac`, `trashernet_eth`, `trashernet_arp` * Layer 3, Network: `trashernet_ipv4`, `trashernet_ipv4prot`, `trashernet_icmp` * Layer 4, Transport: `trashernet_udp`, `trashernet_udpprot` @@ -17,7 +17,7 @@ When writing it, the following were the main design philosophies: **IMPORTANT:** This code (and HW suggestions) violate the Ethernet standard in many ways. Do not use in proper (especially commercial) products. -Note: The `trashernet_rmii` component uses a standard RMII Ethernet PHY instead of the hardware suggestions below. If you are looking for the true Trashernet experience, choose the `trashernet_phy` instead. +Note: The `trashernet_rmii` and `trashernet_mii` components use a standard (R)MII Ethernet PHY instead of the hardware suggestions below. If you are looking for the true Trashernet experience, choose the `trashernet_phy` instead. ## Hardware diff --git a/bench/bench_trashernet_mii.vhd b/bench/bench_trashernet_mii.vhd new file mode 100644 index 0000000..02092ec --- /dev/null +++ b/bench/bench_trashernet_mii.vhd @@ -0,0 +1,239 @@ +-- -------------------------------------------------------------------------- -- +-- TRASHERNET - A Trashy Ethernet Stack for FPGAs -- +-- -------------------------------------------------------------------------- -- +-- bench_trashernet_mac.vhd : Stimulus-only test bench for the MAC+PHY parts +-- Tests TX path through the RX path. Not great, but whatever. +-- -------------------------------------------------------------------------- -- +-- Author : Markus Koch +-- Contributors : None +-- License : Mozilla Public License (MPL) Version 2 +-- -------------------------------------------------------------------------- -- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.bench_pkg.all; + +library trashernet; +use trashernet.trashernet_pkg.all; + +library vunit_lib; +context vunit_lib.vunit_context; + +entity bench_trashernet_mii is + generic( + runner_cfg : string + ); +end entity bench_trashernet_mii; + +architecture bench of bench_trashernet_mii is + signal clk : std_logic; + signal rst : std_logic; + signal phy_out : phy_out_t; + signal phy_in : phy_in_t; + + signal rmii_tx_start : std_logic := '0'; + signal trashernet_tx_start : std_logic := '0'; + + signal mii_int_n : std_logic; + signal mii_rst_n : std_logic; + signal mii_rxd : std_logic_vector(3 downto 0); + signal mii_rx_dv : std_logic; + signal mii_rx_clk : std_logic; + signal mii_rx_err : std_logic; + signal mii_rx_col : std_logic; + signal mii_rx_crs : std_logic; + signal mii_txd : std_logic_vector(3 downto 0); + signal mii_tx_en : std_logic; + signal mii_tx_clk : std_logic; + +begin + trashernet_mii_inst : entity trashernet.trashernet_mii + port map( + clk => clk, + rst => rst, + phy_out => phy_out, + phy_in => phy_in, + mii_int_n => mii_int_n, + mii_rst_n => mii_rst_n, + mii_rxd => mii_rxd, + mii_rx_dv => mii_rx_dv, + mii_rx_clk => mii_rx_clk, + mii_rx_err => mii_rx_err, + mii_rx_col => mii_rx_col, + mii_rx_crs => mii_rx_crs, + mii_txd => mii_txd, + mii_tx_en => mii_tx_en, + mii_tx_clk => mii_tx_clk + ); + + clockDriver : process + constant period : time := 10 ns; + begin + clk <= '0'; + wait for period / 2; + clk <= '1'; + wait for period / 2; + end process clockDriver; + + test : process is + begin + rst <= '1'; + test_runner_setup(runner, runner_cfg); + + wait for 10 ns; + rst <= '0'; + wait for 10 ns; + + while test_suite loop + if run("rx") then + rmii_tx_start <= not rmii_tx_start; + wait until phy_out.rx_active = '1' for 1 ms; + assert phy_out.rx_active'event report "Timeout for reception to start"; + wait until phy_out.rx_active = '0' for 1 ms; + assert phy_out.rx_active'event report "Timeout for reception to end"; + wait for 1 us; + + elsif run("tx") then + trashernet_tx_start <= not trashernet_tx_start; + wait until phy_out.tx_active = '1' for 1 ms; + assert phy_out.tx_active'event report "Timeout for transmission to start"; + wait until phy_out.tx_active = '0' for 1 ms; + assert phy_out.tx_active'event report "Timeout for transmission to stop"; + + wait until phy_out.tx_active = '1' for 1 ms; + assert phy_out.tx_active'event report "Timeout for transmission to start"; + wait until phy_out.tx_active = '0' for 1 ms; + assert phy_out.tx_active'event report "Timeout for transmission to stop"; + + end if; + end loop; + + wait for 1 ns; + + test_runner_cleanup(runner); + end process test; + + phy_rx : process(clk) is + begin + if rising_edge(clk) then + if phy_out.rx_data_valid = '1' then + report "RX data: " & to_hstring(phy_out.rx_data); + end if; + end if; + end process phy_rx; + + rmiirxclk : process + constant period : time := 40 ns; + begin + mii_rx_clk <= '0'; + wait for period / 2; + mii_rx_clk <= '1'; + wait for period / 2; + end process rmiirxclk; + + rmii_tx_gen : process is + procedure send_frame(data : byte_vector) is + variable sr : byte; + begin + wait until rising_edge(mii_rx_clk); + mii_rxd <= "0101"; + wait for 2.5 ns; + mii_rx_dv <= '1'; + + for i in 0 to 5 loop + wait until mii_rx_clk; + end loop; + mii_rxd <= "1101"; + wait until mii_rx_clk; + + for i in data'range loop + sr := data(i); + for j in 0 to 1 loop + mii_rxd <= sr(mii_rxd'range); + wait until rising_edge(mii_rx_clk); + sr := "XXXX" & sr(sr'high downto mii_rxd'length); + end loop; + end loop; + + mii_rx_dv <= '0'; + + wait for 1 us; -- IPG + end procedure send_frame; + + begin + mii_rx_crs <= '1'; + mii_rx_dv <= '0'; + mii_rxd <= (others => '0'); + mii_rx_err <= '0'; + + wait on rmii_tx_start; + send_frame(byte_vector'(x"c6", x"b8", x"c1", x"db", x"b1", x"1d", x"00", x"ff", x"ff", x"11", x"22", x"33", x"08", x"00", x"45", x"00", x"00", x"29", x"00", x"00", x"00", x"00", x"40", x"11", x"f5", x"70", x"c0", x"a8", x"02", x"02", x"c0", x"a8", x"02", x"01", x"ab", x"cd", x"00", x"ff", x"00", x"15", x"00", x"00", x"48", x"65", x"6c", x"6c", x"6f", x"20", x"57", x"6f", x"72", x"6c", x"64", x"21", x"0a", x"00", x"00", x"00", x"00", x"00", x"64", x"90", x"a9", x"ea")); + wait; + end process rmii_tx_gen; + + rmiitxclk : process + constant period : time := 40 ns; + begin + mii_tx_clk <= '1'; + wait for period / 2; + mii_tx_clk <= '0'; + wait for period / 2; + end process rmiitxclk; + + trashernet_tx_gen : process is + procedure send_frame(data : byte_vector) is + begin + for i in data'range loop + phy_in.tx_data_en <= '1'; + phy_in.tx_data <= data(i); + wait until rising_edge(clk); + while not phy_out.tx_data_ack loop + wait until rising_edge(clk); + end loop; + end loop; + phy_in.tx_data_en <= '0'; + wait until rising_edge(clk); + end procedure send_frame; + begin + phy_in.tx_data_en <= '0'; + wait on trashernet_tx_start; + wait until rising_edge(clk); + send_frame(byte_vector'(x"c6", x"b8", x"c1", x"db", x"b1", x"1d", x"00", x"ff", x"ff", x"11", x"22", x"33", x"08", x"00", x"45", x"00", x"00", x"29", x"00", x"00", x"00", x"00", x"40", x"11", x"f5", x"70", x"c0", x"a8", x"02", x"02", x"c0", x"a8", x"02", x"01", x"ab", x"cd", x"00", x"ff", x"00", x"15", x"00", x"00", x"48", x"65", x"6c", x"6c", x"6f", x"20", x"57", x"6f", x"72", x"6c", x"64", x"21", x"0a", x"00", x"00", x"00", x"00", x"00", x"64", x"90", x"a9", x"ea")); + -- Currently, there's a bug in the CDC preventing single-cycle deassertions from deasserting the transmit enable + while phy_out.tx_active loop + wait until rising_edge(clk); + end loop; + send_frame(byte_vector'(x"c6", x"b8", x"c1", x"db", x"b1", x"1d", x"00", x"ff", x"ff", x"11", x"22", x"33", x"08", x"00", x"45", x"00", x"00", x"29", x"00", x"00", x"00", x"00", x"40", x"11", x"f5", x"70", x"c0", x"a8", x"02", x"02", x"c0", x"a8", x"02", x"01", x"ab", x"cd", x"00", x"ff", x"00", x"15", x"00", x"00", x"48", x"65", x"6c", x"6c", x"6f", x"20", x"57", x"6f", x"72", x"6c", x"64", x"21", x"0a", x"00", x"00", x"00", x"00", x"00", x"64", x"90", x"a9", x"ea")); + end process trashernet_tx_gen; + + rmii_receiver : process(mii_tx_clk) is + variable sr : byte; + variable cnt : integer range 0 to 1; + variable active : boolean := false; + begin + if rising_edge(mii_tx_clk) then + if mii_tx_en then + if not active then + report "RMII RX start"; + end if; + sr := mii_txd & sr(sr'high downto mii_txd'length); + if cnt = 1 then + cnt := 0; + report "RMII RX: " & to_hstring(sr); + else + cnt := cnt + 1; + end if; + active := true; + else + if active then + report "RMII RX stop"; + end if; + cnt := 0; + active := false; + end if; + end if; + end process rmii_receiver; + +end architecture bench; diff --git a/trashernet/trashernet_mii.vhd b/trashernet/trashernet_mii.vhd new file mode 100644 index 0000000..3d6cf55 --- /dev/null +++ b/trashernet/trashernet_mii.vhd @@ -0,0 +1,298 @@ + +-- -------------------------------------------------------------------------- -- +-- TRASHERNET - A Trashy Ethernet Stack for FPGAs -- +-- -------------------------------------------------------------------------- -- +-- trashernet_mii.vhd : Ethernet OSI Layer 1, Physical +-- Implements interface to an MII PHY (100 MBit/s only). +-- -------------------------------------------------------------------------- -- +-- Author : Markus Koch +-- Contributors : None +-- License : Mozilla Public License (MPL) Version 2 +-- -------------------------------------------------------------------------- -- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.math_real.all; + +use work.trashernet_pkg.all; + +entity trashernet_mii is + port( + -- Global + clk : in std_logic; -- Global clock (must not be slower than rmii_ref_clk) + rst : in std_logic; -- Asynchronous reset + + -- PHY application interface + phy_out : out phy_out_t; -- PHY application IF (out) + phy_in : in phy_in_t; -- PHY application IF (in) + + -- MII physical signals + mii_int_n : in std_logic; + mii_rst_n : out std_logic; + mii_rxd : in std_logic_vector(3 downto 0); + mii_rx_dv : in std_logic; + mii_rx_clk : in std_logic; + mii_rx_err : in std_logic; + mii_rx_col : in std_logic; + mii_rx_crs : in std_logic; + mii_txd : out std_logic_vector(3 downto 0); + mii_tx_en : out std_logic; + mii_tx_clk : in std_logic + ); +end entity trashernet_mii; + +architecture rtl of trashernet_mii is + +begin + receive : block + signal mii_rx_rst : std_logic; + + type state_t is (IDLE, DATA); + signal state : state_t; + signal sr : byte; + signal nibble_cnt : integer range 0 to 1; + + signal temp_carrier_detect : std_logic; + signal temp_rx_error : std_logic; + signal temp_rx_data : byte; + signal temp_rx_active : std_logic; + signal temp_rx_data_valid : std_logic; + + begin + mii_rx_reset_gen_p : process(mii_rx_clk, rst) is + begin + if rst then + mii_rx_rst <= '1'; + elsif rising_edge(mii_rx_clk) then + mii_rx_rst <= '0'; + end if; + end process mii_rx_reset_gen_p; + + rxp : process(mii_rx_clk, mii_rx_rst) is + begin + if mii_rx_rst then + state <= IDLE; + temp_carrier_detect <= '0'; + temp_rx_error <= '0'; + nibble_cnt <= 0; + sr <= (others => '0'); + + elsif rising_edge(mii_rx_clk) then + -- Shift in data bytes + sr <= mii_rxd & sr(sr'high downto mii_rxd'length); + if nibble_cnt = 1 then + nibble_cnt <= 0; + else + nibble_cnt <= nibble_cnt + 1; + end if; + + -- Sync header + case state is + when IDLE => + if sr = x"D5" then -- Sync header + state <= DATA; + nibble_cnt <= 1; + end if; + + when DATA => + null; + end case; + + -- Stop receiving whenever CRSDV disappears + if mii_rx_dv = '0' or mii_rx_err = '1' then + temp_rx_error <= mii_rx_err; + state <= IDLE; + end if; + + temp_carrier_detect <= mii_rx_crs; + end if; + end process rxp; + + temp_rx_data <= sr; + temp_rx_active <= '1' when (state = DATA) else '0'; -- TODO: We might want to delay this by one cycle + temp_rx_data_valid <= '1' when (state = DATA) and (nibble_cnt = 0) else '0'; + + -- CDC + synchronizer_rxa_inst : entity work.synchronizer + generic map( + SIZE => 4 + ) + port map( + clk => clk, + rst => rst, + data_in => temp_rx_active, + data_out => phy_out.rx_active + ); + + synchronizer_crs_inst : entity work.synchronizer + generic map( + SIZE => 4 + ) + port map( + clk => clk, + rst => rst, + data_in => temp_carrier_detect, + data_out => phy_out.carrier_detect + ); + + cdc_strobe_dv_inst : entity work.cdc_strobe + generic map( + SYNCHRONIZERS => 2 + ) + port map( + a_clk => mii_rx_clk, + a_rst => mii_rx_rst, + a_in => temp_rx_data_valid, + b_clk => clk, + b_rst => rst, + b_out => phy_out.rx_data_valid + ); + + cdc_strobe_err_inst : entity work.cdc_strobe + generic map( + SYNCHRONIZERS => 2 + ) + port map( + a_clk => mii_rx_clk, + a_rst => mii_rx_rst, + a_in => temp_rx_error, + b_clk => clk, + b_rst => rst, + b_out => phy_out.rx_error + ); + + -- Not a synchronizer, just a byte latch in the rmii_ref_clk domain + rx_data_reg : process(mii_rx_clk) is + begin + if rising_edge(mii_rx_clk) then + if temp_rx_data_valid then + phy_out.rx_data <= temp_rx_data; -- TODO: We are assuming that the system clock domains runs at least x times faster than the PHY clock domain + end if; + end if; + end process rx_data_reg; + + end block receive; + + transmitter : block + constant SYNC_HEADER_SIZE_BYTES : natural := 8 - 1; -- Sync header 0x55 bytes () + constant IPG_SIZE_BYTES : natural := 96 / 8; + + signal mii_tx_rst : std_logic; + + signal temp_tx_data : byte; + signal temp_tx_data_en : std_logic; + signal temp_tx_data_ack : std_logic; + signal temp_tx_active : std_logic; + + type state_t is (IDLE, DATA, IPG); + signal state : state_t; + signal sr : byte; + signal nibble_cnt : integer range 0 to 1; + signal byte_cnt : integer range 0 to maximum(SYNC_HEADER_SIZE_BYTES, IPG_SIZE_BYTES) - 1; + signal byte_done : std_logic; + signal block_done : std_logic; + + begin + rmii_tx_reset_gen_p : process(mii_tx_clk, rst) is + begin + if rst then + mii_tx_rst <= '1'; + elsif rising_edge(mii_tx_clk) then + mii_tx_rst <= '0'; + end if; + end process rmii_tx_reset_gen_p; + + synchronizer_txdv_inst : entity work.synchronizer + generic map( + SIZE => 2 + ) + port map( + clk => mii_tx_clk, + rst => mii_tx_rst, + data_in => phy_in.tx_data_en, + data_out => temp_tx_data_en + ); + temp_tx_data <= phy_in.tx_data; -- TODO: Again, we are making assumptions about the clock relationship here + + -- ----------------------------------------------------------------- + + cdc_strobe_inst : entity work.cdc_strobe + generic map( + SYNCHRONIZERS => 2 + ) + port map( + a_clk => mii_tx_clk, + a_rst => mii_tx_rst, + a_in => temp_tx_data_ack, + b_clk => clk, + b_rst => rst, + b_out => phy_out.tx_data_ack + ); + + synchronizer_inst : entity work.synchronizer + generic map( + SIZE => 2 + ) + port map( + clk => clk, + rst => rst, + data_in => temp_tx_active, + data_out => phy_out.tx_active + ); + + txp : process(mii_tx_clk, mii_tx_rst) is + begin + if mii_tx_rst then + state <= IDLE; + byte_cnt <= 0; + nibble_cnt <= 0; + sr <= (others => '-'); + + elsif rising_edge(mii_tx_clk) then + sr <= x"0" & sr(sr'high downto mii_txd'length); + if nibble_cnt = 1 then + nibble_cnt <= 0; + else + nibble_cnt <= nibble_cnt + 1; + end if; + if byte_done then + if byte_cnt > 0 then + byte_cnt <= byte_cnt - 1; + end if; + end if; + + case state is + when IDLE => + sr <= temp_tx_data; + nibble_cnt <= 0; + if temp_tx_data_en then + state <= DATA; + end if; + + when DATA => + if byte_done then + if temp_tx_data_en then + sr <= temp_tx_data; + else + state <= IPG; + byte_cnt <= IPG_SIZE_BYTES - 1; + end if; + end if; + when IPG => + if block_done then + state <= IDLE; + end if; + end case; + end if; + end process txp; + byte_done <= '1' when nibble_cnt = 1 else '0'; + block_done <= '1' when (byte_cnt = 0) and (byte_done = '1') else '0'; + temp_tx_data_ack <= '1' when (state = DATA) and (byte_done = '1') and (temp_tx_data_en = '1') else '0'; + temp_tx_active <= '1' when state = DATA else '0'; + + mii_txd <= sr(mii_txd'range); + mii_tx_en <= '1' when state = DATA else '0'; + end block transmitter; +end architecture rtl; +