library ieee;
use ieee.std_logic_1164.all;

library vunit_lib;
context vunit_lib.vunit_context;

library i2c;

use work.tb_pkg.all;
use work.tb_i2c_pkg.all;
use work.tb_i2c_master_pkg.all;
use work.tb_i2c_slave_pkg.all;

entity master_tb is

  generic (
    runner_cfg : string);

end entity master_tb;

architecture tb of master_tb is
  constant CLK_PERIOD : time := 10 ns;
  signal rst_n : std_logic := '0';

  signal master_sda_enable : std_logic;
  signal master_sda : std_logic;

  signal slave_address : std_logic_vector(6 downto 0);

  signal master_scl_enable : std_logic;
  signal master_scl : std_logic;

  signal dev_busy, bus_busy : std_logic;
  signal err_noack_address, err_noack_data, err_arbitration, err_general : std_logic;
  signal rw : std_logic;

  signal master_stop, master_start, master_run : std_logic := '0';
  signal waiting : std_logic;

  signal rx_confirm : std_logic := '0';

  signal tx_valid : std_logic := '0';
  signal tx_data : std_logic_vector(7 downto 0) := (others => '0');

  signal one  : std_logic := '1';
  signal zero : std_logic := '0';

  constant SCL_MIN_STABLE_CYCLES : natural := 10;
  constant TIMEOUT : time := SCL_MIN_STABLE_CYCLES * CLK_PERIOD * 2;

  signal stable_check : std_logic := '0';
begin  -- architecture tb

  clk <= not clk after CLK_PERIOD / 2;
  rst_n <= '1' after 2 * CLK_PERIOD;

  sda <= 'H';
  scl <= 'H';

  sda <= '0' when master_sda_enable = '1' else 'Z';
  scl <= '0' when master_scl_enable = '1' else 'Z';

  master_sda <= '1' when sda = 'H' else sda;
  master_scl <= '1' when scl = 'H' else scl;

  uut : entity i2c.master
    generic map (
      SCL_FALLING_DELAY     => 1,
      SCL_MIN_STABLE_CYCLES => SCL_MIN_STABLE_CYCLES)
    port map (
      clk_i               => clk,
      rst_in              => rst_n,
--
      slave_address_i     => slave_address,
--
      generate_ack_i      => '1',
      expect_ack_i        => '1',
--
      rx_valid_o          => rx_valid,
      rx_data_o           => rx_data,
      rx_confirm_i        => rx_confirm,
--
      tx_ready_o          => tx_ready,
      tx_valid_i          => tx_valid,
      tx_data_i           => tx_data,
      tx_clear_buffer_i   => '0',
--
      err_noack_data_o    => err_noack_data,
      err_noack_address_o => err_noack_address,
      err_arbitration_o   => err_arbitration,
      err_general_o       => err_general,
--
      stop_i              => master_stop,
      start_i             => master_start,
      run_i               => master_run,
      rw_i                => rw,
--
      dev_busy_o          => dev_busy,
      bus_busy_o          => bus_busy,
      waiting_o           => waiting,
--
      sda_i               => master_sda,
      scl_i               => master_scl,
      sda_enable_o        => master_sda_enable,
      scl_enable_o        => master_scl_enable);

  -- stable sda_enable when scl high
  -- TODO ensure active only when no start/stop
  -- conditions should be generated...
  -- sda_stability_check: check_stable(clk, one, scl, not_scl, master_sda_enable);
  --
  sda_stable: check_stable(clk, stable_check, one, zero, master_sda);
  scl_stable: check_stable(clk, stable_check, one, zero, master_scl);

  main: process is
    procedure request_start(
      constant address : in std_logic_vector(6 downto 0);
      constant rw_i : in std_logic;
      constant stop : in std_logic := '0') is
    begin  -- procedure request_start
      slave_address <= address;
      rw <= rw_i;

      master_start <= '1';
      master_stop <= stop;
      master_run <= '1';
      wait until falling_edge(clk);
      master_start <= '0';
      master_stop <= '0';
    end procedure request_start;

    procedure request_stop is
    begin  -- procedure request_stop
      master_start <= '0';
      master_stop <= '1';
      master_run <= '0';
      wait until falling_edge(clk);
      master_stop <= '0';
    end procedure request_stop;

    procedure check_errors (
      constant exp_noack_address : in std_logic := '0';
      constant exp_noack_data    : in std_logic := '0';
      constant exp_arbitration   : in std_logic := '0';
      constant exp_general : in std_logic := '0') is
    begin  -- procedure check_errors
      check_equal(err_noack_address, exp_noack_address, "Noack address error not as expected.");
      check_equal(err_noack_data, exp_noack_data, "Noack data error not as expected.");
      check_equal(err_arbitration, exp_arbitration, "Arbitration error not as expected.");
      check_equal(err_general, exp_general, "Gneral error not as expected.");
    end procedure check_errors;
  begin  -- process main
    wait until rst_n = '1';
    wait until falling_edge(clk);

    test_runner_setup(runner, runner_cfg);
    set_stop_level(failure);

    while test_suite loop
      if run("simple_read") then
        request_start("1110101", '1', stop => '1');
        i2c_slave_check_start("1110101", '1', TIMEOUT, scl, sda);
        i2c_slave_transmit("11101010", TIMEOUT, scl => scl, sda => sda);
        rx_read_data("11101010", rx_confirm);
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("simple_write") then
        request_start("1110101", '0', stop => '1');
        tx_write_data("11101010", tx_data, tx_valid);
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        i2c_slave_receive("11101010", TIMEOUT, scl, sda);
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("multi_read") then
        request_start("1110101", '1');
        i2c_slave_check_start("1110101", '1', TIMEOUT, scl, sda);
        i2c_slave_transmit("11101010", TIMEOUT, scl => scl, sda => sda);
        rx_read_data("11101010", rx_confirm);

        i2c_slave_transmit("00001111", TIMEOUT, scl => scl, sda => sda);
        rx_read_data("00001111", rx_confirm);
        i2c_slave_transmit("11110000", TIMEOUT, scl => scl, sda => sda);
        rx_read_data("11110000", rx_confirm);
        request_stop;
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("multi_write") then
        tx_write_data("11101010", tx_data, tx_valid);
        tx_write_data("00011100", tx_data, tx_valid);
        request_start("1110101", '0');
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        i2c_slave_receive("11101010", TIMEOUT, scl, sda);
        tx_write_data("00000000", tx_data, tx_valid);
        i2c_slave_receive("00011100", TIMEOUT, scl, sda);
        i2c_slave_receive("00000000", TIMEOUT, scl, sda);
        request_stop;
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("waiting_write_tx") then
        request_start("1110101", '0');
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        check_errors;
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        for i in 0 to 100 loop
          check_equal(waiting, '1', "Waiting wrong");
          check_equal(scl, '0', "SCL wrong");
          wait until falling_edge(clk);
        end loop;  -- i

        check_errors;
        tx_write_data("00000000", tx_data, tx_valid);
        check_equal(waiting, '0');
        i2c_slave_receive("00000000", 2 * TIMEOUT, scl, sda);
        request_stop;
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("waiting_stop") then
        tx_write_data("00000000", tx_data, tx_valid);
        request_start("1110101", '0');
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        i2c_slave_receive("00000000", 2 * TIMEOUT, scl, sda);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        check_equal(waiting, '1');
        request_stop;
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;

      elsif run("waiting_right_away_stop") then
        request_start("1110101", '0', stop => '1');
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;

      elsif run("write_read") then
        tx_write_data("11101010", tx_data, tx_valid);
        tx_write_data("00001111", tx_data, tx_valid);
        request_start("0001011", '0');
        i2c_slave_check_start("0001011", '0', TIMEOUT, scl, sda);
        i2c_slave_receive("11101010", TIMEOUT, scl, sda);
        i2c_slave_receive("00001111", TIMEOUT, scl, sda);
        check_errors;
        request_start("0001011", '1', stop => '1');
        i2c_slave_check_start("0001011", '1', 2 * TIMEOUT, scl, sda);
        check_errors;
        i2c_slave_transmit("00010101", TIMEOUT, scl, sda);
        rx_read_data("00010101", rx_confirm);
        check_errors;
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors;
      elsif run("lost_arbitration_early") then
        request_start("1110101", '0', stop => '1');
        i2c_master_start("0000000", '0', scl, sda, exp_ack => '0');
        check_errors(exp_arbitration => '1');
      elsif run("lost_arbitration_late") then
        request_start("1110101", '0', stop => '1');
        tx_write_data("11101010", tx_data, tx_valid);
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);

        sda <= '0';
        wait_for_scl_rise(timeout, scl);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        scl_fall(scl); -- have to do manually (simulate another master)
        sda <= 'Z';

        check_errors(exp_arbitration => '1');
      elsif run("unexpected_start") then
        request_start("1110101", '0', stop => '1');
        tx_write_data("11101010", tx_data, tx_valid);
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);

        wait_for_scl_rise(timeout, scl);
        wait_for_scl_fall(timeout, scl);
        wait_for_scl_rise(timeout, scl);
        wait until falling_edge(clk);
        sda <= '0'; -- start
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        check_errors(exp_general => '1');
      elsif run("noack_address") then
        request_start("1110101", '0');
        tx_write_data("11101010", tx_data, tx_valid);
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda, ack => '0');
        check_errors(exp_noack_address => '1');
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors(exp_noack_address => '1');
      elsif run("noack_data") then
        request_start("1110101", '0');
        tx_write_data("11101010", tx_data, tx_valid);
        i2c_slave_check_start("1110101", '0', TIMEOUT, scl, sda);
        i2c_slave_receive("11101010", TIMEOUT, scl, sda, ack => '0');
        check_errors(exp_noack_data => '1');
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        check_errors(exp_noack_data => '1');
      elsif run("bus_busy") then
        check_equal(bus_busy, '0');
        check_equal(dev_busy, '0');
        i2c_master_start("0000000", '0', scl, sda, exp_ack => '0');
        check_equal(bus_busy, '1', "Bus busy wrong.");
        check_equal(dev_busy, '0');
        i2c_master_stop(scl, sda);
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        check_equal(bus_busy, '0');
        check_equal(dev_busy, '0');
        check_errors;
      elsif run("bus_busy_start_request") then
        check_equal(bus_busy, '0');
        check_equal(dev_busy, '0');
        i2c_master_start("0000000", '0', scl, sda, exp_ack => '0');
        request_start("1111100", '0');
        check_equal(bus_busy, '1', "Bus busy wrong.");
        check_equal(dev_busy, '0');
        i2c_master_stop(scl, sda);      -- give bus away
        i2c_slave_check_start("1111100", '0', TIMEOUT, scl, sda);
        check_errors;
      elsif run("arbitration_lost_start_request") then
        check_equal(bus_busy, '0');
        check_equal(dev_busy, '0');

        request_start("1110101", '1');
        i2c_master_start("0000000", '1', scl, sda, exp_ack => '0');

        check_errors(exp_arbitration => '1');
        check_equal(bus_busy, '1', "Bus busy wrong.");
        check_equal(dev_busy, '0');

        request_start("1110101", '1', stop => '1');

        i2c_master_transmit("11111111", scl, sda, exp_ack => '0');
        i2c_master_stop(scl, sda);      -- give bus away
        wait until falling_edge(clk);
        wait until falling_edge(clk);
        check_errors(exp_arbitration => '1');

        i2c_slave_check_start("1110101", '1', TIMEOUT, scl, sda);
        i2c_slave_transmit("11111111", TIMEOUT, scl, sda);
        i2c_slave_check_stop(TIMEOUT, scl, sda);
        rx_read_data("11111111", rx_confirm);
      end if;
    end loop;

    stable_check <= '1';
    wait for TIMEOUT * 10;

    test_runner_cleanup(runner);
  end process main;
end architecture tb;
