diff --git a/hdl/ip/vhd/common/BUCK b/hdl/ip/vhd/common/BUCK index b60a6fd5..2616180a 100644 --- a/hdl/ip/vhd/common/BUCK +++ b/hdl/ip/vhd/common/BUCK @@ -27,6 +27,14 @@ vhdl_unit( visibility = ['PUBLIC'] ) +vhdl_unit( + name = "tristate_if_pkg", + srcs = glob(["interfaces/tristate_if_pkg.vhd"]), + deps = [], + standard = "2019", + visibility = ['PUBLIC'] +) + # Strobe vhdl_unit( diff --git a/hdl/ip/vhd/common/interfaces/streaming_if_pkg.vhd b/hdl/ip/vhd/common/interfaces/streaming_if_pkg.vhd index ad1e9bab..caeeea44 100644 --- a/hdl/ip/vhd/common/interfaces/streaming_if_pkg.vhd +++ b/hdl/ip/vhd/common/interfaces/streaming_if_pkg.vhd @@ -20,12 +20,12 @@ package streaming_if_pkg is ready : std_logic; end record; - view st_source of data_channel is + view st_source_if of data_channel is valid, data : out; ready : in; end view; - alias st_sink is st_source'converse; + alias st_sink_if is st_source_if'converse; end package; diff --git a/hdl/ip/vhd/common/interfaces/tristate_if_pkg.vhd b/hdl/ip/vhd/common/interfaces/tristate_if_pkg.vhd new file mode 100644 index 00000000..9022ca73 --- /dev/null +++ b/hdl/ip/vhd/common/interfaces/tristate_if_pkg.vhd @@ -0,0 +1,25 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +-- This package relies on the VHDL 2019 feature for "interfaces" + +library ieee; +use ieee.std_logic_1164.all; + +package tristate_if_pkg is + + type tristate is record + i : std_logic; + o : std_logic; + oe : std_logic; + end record; + + view tristate_if of tristate is + i : in; + o, oe : out; + end view; + +end package; \ No newline at end of file diff --git a/hdl/ip/vhd/common/strobe/sims/strobe_tb.vhd b/hdl/ip/vhd/common/strobe/sims/strobe_tb.vhd index 837c2d1f..3892c790 100644 --- a/hdl/ip/vhd/common/strobe/sims/strobe_tb.vhd +++ b/hdl/ip/vhd/common/strobe/sims/strobe_tb.vhd @@ -31,6 +31,7 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; + alias enable is << signal th.dut_enable : std_logic >>; alias strobe is << signal th.dut_strobe : std_logic >>; begin -- Always the first thing in the process, set up things for the VUnit test runner @@ -41,11 +42,18 @@ begin while test_suite loop if run("test_strobe") then + enable <= '1'; check_equal(strobe, '0', "Strobe should be low after reset"); wait for 72 ns; -- CLK_PER_NS * (TB_TICKS - 1) ns check_equal(strobe, '0', "Strobe should be low after TB_TICKS-1"); wait for 8 ns; -- wait one more period, bringing us to TB_TICKs check_equal(strobe, '1', "Strobe should be high once the TICKS count is reached"); + elsif run("test_strobe_enable") then + wait for 80 ns; -- wait for TB_TICKs while disabled + check_equal(strobe, '0', "Strobe should be low after TB_TICKS when not enabled"); + enable <= '1'; + wait for 80 ns; -- wait for TB_TICKs while enabled + check_equal(strobe, '1', "Strobe should be high after TICKs when enabled"); end if; end loop; diff --git a/hdl/ip/vhd/common/strobe/sims/strobe_th.vhd b/hdl/ip/vhd/common/strobe/sims/strobe_th.vhd index 59ec690c..7b3afdd2 100644 --- a/hdl/ip/vhd/common/strobe/sims/strobe_th.vhd +++ b/hdl/ip/vhd/common/strobe/sims/strobe_th.vhd @@ -23,6 +23,7 @@ architecture th of strobe_th is signal clk : std_logic := '0'; signal reset : std_logic := '1'; + signal dut_enable : std_logic := '0'; signal dut_strobe : std_logic; begin @@ -39,6 +40,7 @@ begin port map ( clk => clk, reset => reset, + enable => dut_enable, strobe => dut_strobe ); diff --git a/hdl/ip/vhd/common/strobe/strobe.vhd b/hdl/ip/vhd/common/strobe/strobe.vhd index 625e6694..7b79c8f4 100644 --- a/hdl/ip/vhd/common/strobe/strobe.vhd +++ b/hdl/ip/vhd/common/strobe/strobe.vhd @@ -18,6 +18,7 @@ entity strobe is clk : in std_logic; reset : in std_logic; + enable : in std_logic; strobe : out std_logic ); end entity strobe; @@ -34,7 +35,7 @@ begin if strobe_counter = TICKS - 1 then strobe <= '1'; strobe_counter <= 0; - else + elsif enable = '1' then strobe <= '0'; strobe_counter <= strobe_counter + 1; end if; diff --git a/hdl/ip/vhd/i2c/BUCK b/hdl/ip/vhd/i2c/BUCK new file mode 100644 index 00000000..f8f59246 --- /dev/null +++ b/hdl/ip/vhd/i2c/BUCK @@ -0,0 +1,29 @@ +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") + +vhdl_unit( + name = "i2c_txn_layer", + srcs = glob([ + "link_layer/*.vhd", + "txn_layer/*.vhd", + "i2c_common_pkg.vhd"]), + deps = [ + "//hdl/ip/vhd/common:countdown", + "//hdl/ip/vhd/common:strobe", + "//hdl/ip/vhd/common:streaming_if_pkg", + "//hdl/ip/vhd/common:tristate_if_pkg", + "//hdl/ip/vhd/common:time_pkg", + "//hdl/ip/vhd/synchronizers:meta_sync" + ], + standard = "2019", + visibility = ['PUBLIC'] +) + +vunit_sim( + name = "i2c_txn_layer_tb", + srcs = glob(["sims/txn_layer/*.vhd", "sims/*.vhd"]), + deps = [ + ":i2c_txn_layer", + "//hdl/ip/vhd/vunit_components:basic_stream" + ], + visibility = ['PUBLIC'], +) \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/i2c_common_pkg.vhd b/hdl/ip/vhd/i2c/i2c_common_pkg.vhd new file mode 100644 index 00000000..183eea46 --- /dev/null +++ b/hdl/ip/vhd/i2c/i2c_common_pkg.vhd @@ -0,0 +1,92 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +package i2c_common_pkg is + + -- + -- Link Layer + -- + + type mode_t is ( + STANDARD, -- up to 100 Kbps + FAST, -- up to 400 Kbps + FAST_PLUS -- up to 1 Mbps + ); + + -- A group of settings for generics to generate constants + -- all times in nanoseconds (ns) + type settings_t is record + fscl_period_ns : positive; -- SCL clock period + sda_su_ns : positive; -- data set-up time + sta_su_hd_ns : positive; -- START set-up/hold time + sto_su_ns : positive; -- STOP set-up time + sto_sta_buf_ns : positive; -- bus free time between STOP and START + end record; + + function get_i2c_settings (constant mode : mode_t) return settings_t; + + -- + -- Transaction Layer + -- + + type op_t is ( + READ, + WRITE, + -- RANDOM_READ will write an address byte and one more byte (intended to set an internal + -- address register on a peripheral) before issuing a repeated start for a read. + RANDOM_READ + ); + + type cmd_t is record + op : op_t; + addr : std_logic_vector(6 downto 0); + reg : std_logic_vector(7 downto 0); + len : unsigned(7 downto 0); + end record; + constant CMD_RESET : cmd_t := (READ, (others => '0'), (others => '0'), (others => '0')); + +end package; + +package body i2c_common_pkg is + + function get_i2c_settings (constant mode : mode_t) return settings_t is + variable r : settings_t; + begin + case mode is + when STANDARD => + r := ( + 10_000, -- 10^9 / 100_000Hz + 250, + 4700, + 4000, + 4700 + ); + when FAST => + r := ( + 2500, -- 10^9 / 400_000Hz + 100, + 600, + 600, + 1300 + ); + when FAST_PLUS => + r := ( + 1000, -- 10^9 / 1_000_000Hz + 50, + 260, + 260, + 500 + ); + end case; + + return r; + end; + +end package body; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/i2c_core.vhd b/hdl/ip/vhd/i2c/i2c_core.vhd new file mode 100644 index 00000000..ced6d563 --- /dev/null +++ b/hdl/ip/vhd/i2c/i2c_core.vhd @@ -0,0 +1,52 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; + +use work.tristate_if_pkg; + +use work.i2c_common_pkg; + +entity i2c_core is + generic ( + CLK_PER_NS : positive; + MODE : mode_t; + ); + port ( + clk : in std_logic; + reset : in std_logic; + + -- Tri-state signals to I2C interface + scl_if : view tristate_if; + sda_if : view tristate_if; + + -- AXI register interface + ); +end entity; + +architecture rtl of i2c_core is + +begin + + i2c_txn_layer_inst: entity work.i2c_txn_layer + generic map( + CLK_PER_NS => CLK_PER_NS, + MODE => MODE + ) + port map( + clk => clk, + reset => reset, + scl_if => scl_if, + sda_if => sda_if, + cmd => cmd, + cmd_valid => cmd_valid, + core_ready => core_ready, + tx_st_if => tx_st_if, + rx_st_if => rx_st_if + ); + +end architecture; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/link_layer/i2c_link_layer.vhd b/hdl/ip/vhd/i2c/link_layer/i2c_link_layer.vhd new file mode 100644 index 00000000..a276de77 --- /dev/null +++ b/hdl/ip/vhd/i2c/link_layer/i2c_link_layer.vhd @@ -0,0 +1,428 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std_unsigned.all; + +use work.stream8_pkg; +use work.tristate_if_pkg.all; +use work.time_pkg.all; + +use work.i2c_common_pkg.all; + +entity i2c_link_layer is + generic ( + CLK_PER_NS : positive; + MODE : mode_t + ); + port ( + clk : in std_logic; + reset : in std_logic; + + -- Tri-state signals to I2C interface + scl_if : view tristate_if; + sda_if : view tristate_if; + + -- I2C framing + tx_start : in std_logic; -- send a start + tx_ack : in std_logic; -- send an ACK + tx_stop : in std_logic; -- send a stop + ready : out std_logic; -- ready for next action + + -- transmit data + tx_data : in std_logic_vector(7 downto 0); + tx_data_valid : in std_logic; + tx_ackd : out std_logic; -- received an ACK + tx_ackd_valid : out std_logic; + + -- receive data + rx_data : out std_logic_vector(7 downto 0); + rx_data_valid : out std_logic; + ); +end entity; + +architecture rtl of i2c_link_layer is + -- fetch the settings for the desired I2C mode + constant SETTINGS : settings_t := get_i2c_settings(MODE); + constant SCL_HALF_PER_TICKS : positive := + to_integer(calc_ns(SETTINGS.fscl_period_ns, CLK_PER_NS, 10)); + + -- The state machine's counter to enforce timing around various events + constant SM_COUNTER_SIZE_BITS : positive := 8; + constant START_SETUP_HOLD_TICKS : std_logic_vector(7 downto 0) := + calc_ns(SETTINGS.sta_su_hd_ns, CLK_PER_NS, SM_COUNTER_SIZE_BITS); + constant STOP_SETUP_TICKS : std_logic_vector(7 downto 0) := + calc_ns(SETTINGS.sto_su_ns, CLK_PER_NS, SM_COUNTER_SIZE_BITS); + constant STO_TO_STA_BUF_TICKS : std_logic_vector(7 downto 0) := + calc_ns(SETTINGS.sto_sta_buf_ns, CLK_PER_NS, SM_COUNTER_SIZE_BITS); + + -- The number of ticks after SCL falls until SDA should be transitioned. FastMode+ has the + -- tightest requirement here (obviously) at 450ns, so by design we will always transition well + -- prior to that since we don't have a reason not to. + constant SDA_TRANSITION_TICKS : positive := + to_integer(calc_ns(300, CLK_PER_NS, 5)); + + + + type state_t is ( + IDLE, + WAIT_TBUF, + START_SETUP, + START_HOLD, + WAIT_REPEAT_START, + HANDLE_NEXT_PRE, + HANDLE_NEXT, + BYTE_TX, + BYTE_RX, + ACK_TX, + ACK_RX, + STOP_SDA, + STOP_SCL, + STOP_SETUP + ); + + type sm_reg_t is record + -- state + state : state_t; + bits_shifted : natural range 0 to 8; + + -- control + ready : std_logic; + scl_start : std_logic; + scl_active : std_logic; + sda_hold : std_logic; + counter : std_logic_vector(SM_COUNTER_SIZE_BITS - 1 downto 0); + count_load : std_logic; + count_decr : std_logic; + count_clr : std_logic; + sda_changed : std_logic; + ack_sending : std_logic; + scl_fedge_seen : std_logic; + + -- interfaces + rx_data : std_logic_vector(7 downto 0); + rx_data_valid : std_logic; + tx_data : std_logic_vector(7 downto 0); + sda_oe : std_logic; + rx_ack : std_logic; + rx_ack_valid : std_logic; + end record; + + constant SM_REG_RESET : sm_reg_t := ( + IDLE, -- state + 0, -- bits_shifted + '0', -- ready + '0', -- scl_start + '0', -- scl_active + '0', -- sda_hold + (others => '0'),-- counter + '0', -- count_load + '0', -- count_decr + '0', -- count_clr + '0', -- sda_changed + '0', -- ack_sending + '0', -- scl_fedge_seen + (others => '0'),-- rx_data + '0', -- rx_data_valid + (others => '0'),-- tx_data + '0', -- sda_oe + '0', -- rx_ack + '0' -- rx_ack_valid + ); + + signal sm_reg, sm_reg_next : sm_reg_t; + + signal sm_count_done : std_logic; + + signal scl_toggle : std_logic; + signal scl_oe : std_logic; + signal scl_oe_last : std_logic; + signal scl_redge : std_logic; + signal scl_fedge : std_logic; + + signal transition_sda : std_logic; + signal sda_in_syncd : std_logic; +begin + + -- + -- SCL Control + -- + + scl_strobe: entity work.strobe + generic map ( + TICKS => SCL_HALF_PER_TICKS + ) + port map ( + clk => clk, + reset => reset, + enable => sm_reg.scl_active, + strobe => scl_toggle + ); + + scl_reg: process(clk, reset) + variable v_scl_oe_next : std_logic := '0'; + begin + if reset then + scl_oe <= '0'; + scl_oe_last <= '0'; + elsif rising_edge(clk) then + scl_oe_last <= scl_oe; + v_scl_oe_next := not scl_oe; + + if not sm_reg.scl_active then + scl_oe <= '0'; + elsif scl_toggle = '1' or sm_reg.scl_start = '1' then + scl_oe <= v_scl_oe_next; + end if; + end if; + end process; + + scl_redge <= '1' when scl_oe = '0' and scl_oe_last = '1' else '0'; + scl_fedge <= '1' when scl_oe = '1' and scl_oe_last = '0' else '0'; + + -- + -- SDA Control + -- + + sda_in_sync: entity work.meta_sync + generic map ( + STAGES => 2 + ) + port map( + async_input => sda_if.i, + clk => clk, + sycnd_output => sda_in_syncd + ); + + sda_transition: entity work.strobe + generic map ( + TICKS => SDA_TRANSITION_TICKS + ) + port map ( + clk => clk, + reset => reset, + enable => not sm_reg.sda_changed, + strobe => transition_sda + ); + + sm_countdown: entity work.countdown + generic map ( + SIZE => SM_COUNTER_SIZE_BITS + ) + port map ( + clk => clk, + reset => reset, + count => sm_reg.counter, + load => sm_reg.count_load, + decr => sm_reg.count_decr, + clear => sm_reg.count_clr, + done => sm_count_done + ); + + -- + -- Link State Machine + -- + + sm_next_state: process(all) + variable v : sm_reg_t; + begin + v := sm_reg; + + -- default to single cycle pulses to counter control + v.count_load := '0'; + v.count_decr := '0'; + v.count_clr := '0'; + + -- Signal defaults + v.scl_start := '0'; + v.rx_ack := '0'; + v.rx_ack_valid := '0'; + v.rx_data_valid := '0'; + + if scl_fedge then + v.sda_changed := '0'; + elsif transition_sda then + v.sda_changed := '1'; + end if; + + case sm_reg.state is + + -- Ready and awaiting the next transaction + when IDLE => + v.sda_oe := '0'; + + if tx_start then + v.state := WAIT_TBUF; + v.counter := STO_TO_STA_BUF_TICKS; + v.count_load := '1'; + end if; + + -- Wait out tbuf to ensure STOP/START spacing + when WAIT_TBUF => + if sm_count_done then + -- tbuf is always greater than or equal to the START setup requirement, skip to + -- hold + v.state := START_HOLD; + v.counter := START_SETUP_HOLD_TICKS; + v.count_load := '1'; + else + v.count_decr := '1'; + end if; + + when WAIT_REPEAT_START => + if not sm_reg.scl_fedge_seen then + v.scl_fedge_seen := scl_fedge; + elsif scl_redge then + v.state := START_SETUP; + v.scl_active := '0'; + end if; + + -- In the event of a repeated START account for setup requirements + when START_SETUP => + v.sda_oe := '0'; + + if sm_count_done then + v.state := START_HOLD; + v.counter := START_SETUP_HOLD_TICKS; + v.count_load := '1'; + else + v.count_decr := '1'; + end if; + + when START_HOLD => + v.sda_oe := '1'; + if sm_count_done then + v.state := HANDLE_NEXT_PRE; + v.scl_start := '1'; -- drop SCL to finish START condition + v.scl_active := '1'; -- begin free running counter for SCL transitions + else + v.count_decr := '1'; + end if; + + when HANDLE_NEXT_PRE => + v.state := HANDLE_NEXT; + + when HANDLE_NEXT => + if tx_start then + -- A repeated start was issued mid-transaction + v.state := WAIT_REPEAT_START; + v.counter := START_SETUP_HOLD_TICKS; + v.count_load := '1'; + elsif tx_stop then + v.state := STOP_SDA; + elsif tx_data_valid then + -- data to transmit + v.state := BYTE_TX; + v.tx_data := tx_data; + else + -- if nothing else, read + v.state := BYTE_RX; + end if; + + -- Clock out a byte and then wait for an ACK + when BYTE_TX => + if transition_sda = '1' then + if sm_reg.bits_shifted = 8 then + v.state := ACK_RX; + v.bits_shifted := 0; + else + v.sda_oe := not sm_reg.tx_data(0); + v.tx_data := '1' & sm_reg.tx_data(7 downto 1); + v.bits_shifted := sm_reg.bits_shifted + 1; + end if; + end if; + + -- See if the target ACKs + when ACK_RX => + v.sda_oe := '0'; + + if scl_redge then + v.state := HANDLE_NEXT_PRE; + v.rx_ack := not sda_in_syncd; + v.rx_ack_valid := '1'; + end if; + + -- Clock in a byte and then send an ACK + when BYTE_RX => + v.sda_oe := '0'; + + if sm_reg.bits_shifted = 8 then + v.state := ACK_TX; + v.rx_data_valid := '1'; + v.bits_shifted := 0; + elsif scl_redge then + v.rx_data := sda_in_syncd & sm_reg.rx_data(7 downto 1); + v.bits_shifted := sm_reg.bits_shifted + 1; + end if; + + -- ACK the target + when ACK_TX => + -- at the first transition_sda pulse start sending the (N)ACK + if transition_sda = '1' then + if sm_reg.ack_sending = '0' then + v.sda_oe := tx_ack; + v.ack_sending := '1'; + else + -- at the next transition point release the bus + v.sda_oe := '0'; + v.ack_sending := '0'; + v.state := HANDLE_NEXT_PRE; + end if; + end if; + + -- drive SDA through final SCL cycle + when STOP_SDA => + if transition_sda then + v.state := STOP_SCL; + v.sda_oe := '1'; + end if; + + when STOP_SCL => + if scl_redge then + v.state := STOP_SETUP; + v.scl_active := '0'; + v.counter := STOP_SETUP_TICKS; + v.count_load := '1'; + end if; + + when STOP_SETUP => + if sm_count_done then + v := SM_REG_RESET; + else + v.count_decr := '1'; + end if; + + end case; + + -- next state logic + v.ready := '1' when (v.state = IDLE or v.state = HANDLE_NEXT_PRE or v.state = HANDLE_NEXT) else '0'; + + sm_reg_next <= v; + end process; + + reg_sm: process(clk, reset) + begin + if reset then + sm_reg <= SM_REG_RESET; + elsif rising_edge(clk) then + sm_reg <= sm_reg_next; + end if; + end process; + + ready <= sm_reg.ready; + tx_ackd <= sm_reg.rx_ack; + tx_ackd_valid <= sm_reg.rx_ack_valid; + rx_data <= sm_reg.rx_data; + rx_data_valid <= sm_reg.rx_data_valid; + + -- I2C is open-drain, so we only ever drive low + scl_if.o <= '0'; + scl_if.oe <= scl_oe; + sda_if.o <= '0'; + sda_if.oe <= sm_reg.sda_oe; + +end rtl; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/sims/i2c_cmd_vc.vhd b/hdl/ip/vhd/i2c/sims/i2c_cmd_vc.vhd new file mode 100644 index 00000000..12ee43c4 --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/i2c_cmd_vc.vhd @@ -0,0 +1,67 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; +use vunit_lib.sync_pkg.all; + +use work.i2c_common_pkg.all; +use work.i2c_cmd_vc_pkg.all; + +entity i2c_cmd_vc is + generic ( + i2c_cmd_vc : i2c_cmd_vc_t + ); + port ( + cmd : out cmd_t := CMD_RESET; + valid : out std_logic := '0'; + ready : in std_logic; + ); +end entity; + +architecture model of i2c_cmd_vc is +begin + + handle_messages: process + variable msg : msg_t; + variable msg_type : msg_type_t; + variable command : cmd_t; + variable is_read : boolean; + variable is_random : boolean; + begin + receive(net, i2c_cmd_vc.p_actor, msg); + msg_type := message_type(msg); + + if msg_type = push_i2c_cmd_msg then + is_read := pop(msg); + is_random := pop(msg); + if is_read then + if is_random then + command.op := RANDOM_READ; + else + command.op := READ; + end if; + else + command.op := WRITE; + end if; + command.addr := pop(msg); + command.reg := pop(msg); + command.len := pop(msg); + cmd <= command; + valid <= '1'; + wait until ready; + valid <= '0'; + else + unexpected_msg_type(msg_type); + end if; + end process; + +end architecture; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/sims/i2c_cmd_vc_pkg.vhd b/hdl/ip/vhd/i2c/sims/i2c_cmd_vc_pkg.vhd new file mode 100644 index 00000000..2bb642cd --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/i2c_cmd_vc_pkg.vhd @@ -0,0 +1,82 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; +use vunit_lib.sync_pkg.all; + +use work.i2c_common_pkg.all; + +package i2c_cmd_vc_pkg is + + type i2c_cmd_vc_t is record + -- private + p_actor : actor_t; + p_logger : logger_t; + end record; + + constant i2c_cmd_vc_logger : logger_t := get_logger("work:i2c_cmd_vc_pkg"); + + impure function new_i2c_cmd_vc( + actor : actor_t := null_actor; + logger : logger_t := i2c_cmd_vc_logger; + ) return i2c_cmd_vc_t; + + constant push_i2c_cmd_msg : msg_type_t := new_msg_type("push_i2c_cmd"); + + procedure push_i2c_cmd( + signal net : inout network_t; + i2c_cmd_vc : i2c_cmd_vc_t; + cmd : cmd_t; + ); + +end package; + +package body i2c_cmd_vc_pkg is + + impure function new_i2c_cmd_vc( + actor : actor_t := null_actor; + logger : logger_t := i2c_cmd_vc_logger; + ) return i2c_cmd_vc_t is + variable p_actor : actor_t; + begin + p_actor := actor when actor /= null_actor else new_actor; + + return ( + p_actor => p_actor, + p_logger => logger + ); + end function; + + procedure push_i2c_cmd( + signal net : inout network_t; + i2c_cmd_vc : i2c_cmd_vc_t; + cmd : cmd_t + ) is + variable msg : msg_t := new_msg(push_i2c_cmd_msg); + variable is_read : boolean; + variable is_random : boolean; + begin + -- breaking down our type since we can't push enums in VUnit + is_read := false when cmd.op = WRITE else true; + is_random := true when cmd.op = RANDOM_READ else false; + push(msg, is_read); + push(msg, is_random); + push(msg, cmd.addr); + push(msg, cmd.reg); + push(msg, cmd.len); + send(net, i2c_cmd_vc.p_actor, msg); + end; + + +end package body; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/sims/i2c_peripheral.vhd b/hdl/ip/vhd/i2c/sims/i2c_peripheral.vhd new file mode 100644 index 00000000..bf5bf065 --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/i2c_peripheral.vhd @@ -0,0 +1,237 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; +use vunit_lib.sync_pkg.all; + +use work.tristate_if_pkg.all; + +use work.i2c_peripheral_pkg.all; + +entity i2c_peripheral is + generic ( + i2c_peripheral_vc : i2c_peripheral_t + ); + port ( + -- Tri-state signals to I2C interface + scl_if : view tristate_if; + sda_if : view tristate_if; + ); +end entity; + +architecture model of i2c_peripheral is + + type state_t is ( + IDLE, + START, + SEND_ACK, + SEND_NACK, + SEND_BYTE, + GET_START_BYTE, + GET_BYTE, + GET_ACK, + GET_STOP + ); + + signal state : state_t := IDLE; + + signal start_condition : boolean := FALSE; + signal stop_condition : boolean := FALSE; + signal sda_last : std_logic := '1'; + signal rx_data : std_logic_vector(7 downto 0) := (others => '0'); + signal rx_bit_count : unsigned(3 downto 0) := (others => '0'); + signal rx_done : boolean := FALSE; + signal rx_ackd : boolean := FALSE; + signal tx_data : std_logic_vector(7 downto 0) := (others => '0'); + signal tx_bit_count : unsigned(3 downto 0) := (others => '0'); + signal tx_done : boolean := FALSE; + + signal scl_oe : std_logic := '0'; + signal sda_oe : std_logic := '0'; + + signal reg_addr : unsigned(7 downto 0) := (others => '0'); + signal addr_set : boolean := FALSE; + signal addr_incr : boolean := FALSE; +begin + -- I2C interface is open-drain + scl_if.o <= '0'; + sda_if.o <= '0'; + + scl_if.oe <= scl_oe; + sda_if.oe <= sda_oe; + + start_condition <= sda_last = '1' and sda_if.i = '0' and scl_if.i = '1'; + stop_condition <= sda_last = '0' and sda_if.i = '1' and scl_if.i = '1'; + + -- sample SDA regularly to catch transitions + sda_monitor: process + begin + wait for 20 ns; + sda_last <= sda_if.i; + end process; + + transaction_sm: process + variable event_msg : msg_t; + variable is_read : boolean := FALSE; + variable reg_addr_v : unsigned(7 downto 0) := (others => '0'); + variable stop_during_write : boolean := FALSE; + begin + case state is + + when IDLE => + wait on start_condition; + state <= START; + + when START => + event_msg := new_msg(got_start); + send(net, i2c_peripheral_vc.p_actor, event_msg); + state <= GET_START_BYTE; + + when GET_START_BYTE => + wait on rx_done; + if rx_data(7 downto 1) = address(i2c_peripheral_vc) then + state <= SEND_ACK; + is_read := rx_data(0) = '1'; + event_msg := new_msg(address_matched); + send(net, i2c_peripheral_vc.p_actor, event_msg); + else + state <= SEND_NACK; + event_msg := new_msg(address_different); + send(net, i2c_peripheral_vc.p_actor, event_msg); + end if; + + when GET_BYTE => + wait until rx_done or start_condition or stop_condition; + + if start_condition then + state <= START; + elsif stop_condition then + state <= GET_STOP; + stop_during_write := TRUE; + else + state <= SEND_ACK; + + if addr_incr then + reg_addr_v := reg_addr + 1; + end if; + + if addr_set then + write_word(memory(i2c_peripheral_vc), to_integer(reg_addr_v), rx_data); + event_msg := new_msg(got_byte); + send(net, i2c_peripheral_vc.p_actor, event_msg); + addr_incr <= TRUE; + else + addr_set <= TRUE; + reg_addr_v := unsigned(rx_data); + end if; + end if; + + when SEND_ACK => + wait until falling_edge(scl_if.i) and tx_done; + if is_read then + state <= SEND_BYTE; + else + state <= GET_BYTE; + end if; + + when SEND_NACK => + wait until falling_edge(scl_if.i); + state <= GET_STOP; + + when SEND_BYTE => + wait until falling_edge(scl_if.i) and tx_done; + reg_addr_v := reg_addr + 1; + state <= GET_ACK; + + when GET_ACK => + wait on rx_done; + state <= SEND_BYTE when rx_ackd else GET_STOP; + + when GET_STOP => + wait until (stop_condition or stop_during_write); + event_msg := new_msg(got_stop); + send(net, i2c_peripheral_vc.p_actor, event_msg); + state <= IDLE; + addr_set <= FALSE; + addr_incr <= FALSE; + stop_during_write := FALSE; + + end case; + + reg_addr <= reg_addr_v; + + wait for 1 fs; + end process; + + receive_sm: process + begin + wait until rising_edge(scl_if.i); + rx_done <= FALSE; + if state = GET_ACK then + -- '0' = ACK, '1' = NACK + rx_ackd <= TRUE when sda_if.i = '0' else FALSE; + rx_bit_count <= to_unsigned(1, rx_bit_count'length); + elsif state = GET_START_BYTE or state = GET_BYTE then + rx_data <= sda_if.i & rx_data(7 downto 1); + rx_bit_count <= rx_bit_count + 1; + end if; + + wait until falling_edge(scl_if.i) or start_condition or stop_condition; + if not falling_edge(scl_if.i) then + rx_bit_count <= (others => '0'); + wait until falling_edge(scl_if.i); + end if; + + if ((state = GET_START_BYTE or state = GET_BYTE) and rx_bit_count = 8) or + (state = GET_ACK and rx_bit_count = 1) then + rx_done <= TRUE; + rx_bit_count <= (others => '0'); + end if; + end process; + + + transmit_sm: process + variable txd : std_logic_vector(7 downto 0) := (others => '0'); + begin + wait until falling_edge(scl_if.i); + tx_done <= FALSE; + -- delay the SDA transition to a bit after SCL falls to allow the controller to release SDA + wait for 100 ns; + if state = SEND_ACK or state = SEND_NACK then + sda_oe <= '1' when state = SEND_ACK else '0'; + tx_bit_count <= to_unsigned(1, tx_bit_count'length); + elsif state = SEND_BYTE then + if tx_bit_count = 0 then + txd := read_word(i2c_peripheral_vc.p_buffer.p_memory_ref, natural(to_integer(reg_addr)), 1); + else + txd := '1' & tx_data(7 downto 1); + end if; + sda_oe <= not txd(0); + tx_bit_count <= tx_bit_count + 1; + else + -- release the bus + sda_oe <= '0'; + end if; + tx_data <= txd; + + wait until rising_edge(scl_if.i); + + if ((state = SEND_ACK or state = SEND_NACK) and tx_bit_count = 1) or + (state = SEND_BYTE and tx_bit_count = 8) then + tx_done <= TRUE; + tx_bit_count <= (others => '0'); + end if; + + end process; + +end architecture; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/sims/i2c_peripheral_pkg.vhd b/hdl/ip/vhd/i2c/sims/i2c_peripheral_pkg.vhd new file mode 100644 index 00000000..31eafdfe --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/i2c_peripheral_pkg.vhd @@ -0,0 +1,165 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +package i2c_peripheral_pkg is + + -- Message definitions + constant got_start : msg_type_t := new_msg_type("got_start"); + constant address_matched : msg_type_t := new_msg_type("address_matched"); + constant address_different : msg_type_t := new_msg_type("address_different"); + constant send_ack : msg_type_t := new_msg_type("send_ack"); + constant got_ack : msg_type_t := new_msg_type("got_ack"); + constant got_byte : msg_type_t := new_msg_type("got_byte"); + constant got_stop : msg_type_t := new_msg_type("got_stop"); + + type i2c_peripheral_t is record + -- private + p_actor : actor_t; + p_buffer : buffer_t; + p_logger : logger_t; + -- I2C peripheral address + P_address : std_logic_vector(6 downto 0); + end record; + + constant i2c_peripheral_vc_logger : logger_t := get_logger("work:i2c_peripheral_vc"); + + impure function new_i2c_peripheral_vc ( + name : string; + address : std_logic_vector(6 downto 0) := b"1010101"; + logger : logger_t := i2c_peripheral_vc_logger + ) return i2c_peripheral_t; + + impure function address (i2c_periph: i2c_peripheral_t) return std_logic_vector; + impure function buf (i2c_periph: i2c_peripheral_t) return buffer_t; + impure function memory (i2c_periph: i2c_peripheral_t) return memory_t; + + procedure expect_message ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + constant expected_msg : msg_type_t; + ); + + procedure expect_stop ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + ); + + procedure start_byte_ack ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + variable ack : out boolean; + ); + + procedure check_written_byte ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + variable data : std_logic_vector; + variable addr : unsigned; + ); + +end package; + +package body i2c_peripheral_pkg is + + impure function new_i2c_peripheral_vc ( + name : string; + address : std_logic_vector(6 downto 0) := b"1010101"; + logger : logger_t := i2c_peripheral_vc_logger + ) return i2c_peripheral_t is + variable buf : buffer_t; + begin + -- I2C can address 256 bytes, so construct an internal buffer to reflect that + buf := allocate(new_memory, 256, name & "_MEM", 8, read_and_write); + + return ( + p_actor => new_actor(name), + p_buffer => buf, + p_logger => logger, + p_address => address + ); + end; + + impure function address (i2c_periph: i2c_peripheral_t) return std_logic_vector is + begin + return i2c_periph.p_address; + end function; + + impure function buf (i2c_periph: i2c_peripheral_t) return buffer_t is + begin + return i2c_periph.p_buffer; + end function; + + impure function memory (i2c_periph: i2c_peripheral_t) return memory_t is + begin + return i2c_periph.p_buffer.p_memory_ref; + end function; + + procedure expect_message ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + constant expected_msg : msg_type_t; + ) is + variable msg : msg_t; + variable matched : boolean; + begin + receive(net, vc.p_actor, msg); + matched := message_type(msg) = expected_msg; + check_true(matched, "Received message did not match expected message."); + end procedure; + + procedure expect_stop ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + ) is + begin + expect_message(net, vc, got_stop); + end procedure; + + procedure start_byte_ack ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + variable ack : out boolean; + ) is + variable msg : msg_t; + begin + -- receive START event + receive(net, vc.p_actor, msg); + if message_type(msg) = got_start then + -- receive START byte ack + receive(net, vc.p_actor, msg); + if message_type(msg) = address_matched then + ack := true; + elsif message_type(msg) = address_different then + ack := false; + end if; + end if; + end procedure; + + procedure check_written_byte ( + signal net : inout network_t; + constant vc : i2c_peripheral_t; + variable data : std_logic_vector; + variable addr : unsigned; + ) is + variable msg : msg_t; + begin + set_expected_word(memory(vc), to_integer(addr), data); + receive(net, vc.p_actor, msg); + if message_type(msg) = got_byte then + check_expected_was_written(buf(vc)); + end if; + end procedure; + +end package body; diff --git a/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.gtkw b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.gtkw new file mode 100644 index 00000000..7554dee6 --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.gtkw @@ -0,0 +1,161 @@ +[*] +[*] GTKWave Analyzer v3.3.104 (w)1999-2020 BSI +[*] Tue Nov 26 23:00:33 2024 +[*] +[dumpfile] "/home/aaron/Oxide/git/quartz/vunit_out/test_output/lib.i2c_tb.write_and_read_many_bytes_735b3cb8aa5b45ddaa977875ceec621188fd3a14/nvc/i2c_tb.fst" +[dumpfile_mtime] "Tue Nov 26 23:00:26 2024" +[dumpfile_size] 6733 +[savefile] "/home/aaron/Oxide/git/quartz/hdl/ip/vhd/i2c/sims/tb_i2c.gtkw" +[timestart] 0 +[size] 2592 1283 +[pos] -1 -1 +*-36.192200 430732000000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +[treeopen] i2c_tb. +[treeopen] i2c_tb.th. +[treeopen] i2c_tb.th.dut. +[treeopen] i2c_tb.th.dut.i2c_link_layer_inst. +[treeopen] i2c_tb.th.dut.sm_reg. +[treeopen] i2c_tb.th.dut.sm_reg_next. +[treeopen] i2c_tb.th.i2c_cmd_vc_inst. +[treeopen] i2c_tb.th.peripheral. +[sst_width] 389 +[signals_width] 361 +[sst_expanded] 1 +[sst_vpaned_height] 382 +@200 +-I2C_CMD_VC +@28 +i2c_tb.th.i2c_cmd_vc_inst.ready +i2c_tb.th.i2c_cmd_vc_inst.valid +@22 +i2c_tb.th.i2c_cmd_vc_inst.cmd.addr[6:0] +i2c_tb.th.i2c_cmd_vc_inst.cmd.len[7:0] +@28 +i2c_tb.th.i2c_cmd_vc_inst.cmd.op +@22 +i2c_tb.th.i2c_cmd_vc_inst.cmd.reg[7:0] +@200 +- +-Transaction Layer +@28 +i2c_tb.th.dut.clk +i2c_tb.th.dut.cmd_valid +i2c_tb.th.dut.core_ready +@200 +-I2C Interface +-SCL +@28 +i2c_tb.th.dut.scl_if.i +i2c_tb.th.dut.scl_if.oe +@200 +-SDA +@28 +i2c_tb.th.dut.sda_if.i +i2c_tb.th.dut.sda_if.oe +@200 +-State +@28 +i2c_tb.th.dut.sm_reg.state +i2c_tb.th.dut.sm_reg.do_start +i2c_tb.th.dut.sm_reg.do_ack +i2c_tb.th.dut.sm_reg.do_stop +i2c_tb.th.dut.ll_ready +@200 +- +-Link Layer +@28 +i2c_tb.th.dut.i2c_link_layer_inst.scl_toggle +i2c_tb.th.dut.i2c_link_layer_inst.scl_redge +i2c_tb.th.dut.i2c_link_layer_inst.scl_fedge +i2c_tb.th.dut.i2c_link_layer_inst.transition_sda +@200 +-Transmit Stream +@28 +i2c_tb.th.dut.i2c_link_layer_inst.tx_data_valid +@23 +i2c_tb.th.dut.i2c_link_layer_inst.tx_data[7:0] +@200 +-Receive Stream +@22 +i2c_tb.th.dut.i2c_link_layer_inst.rx_data[7:0] +@28 +i2c_tb.th.dut.i2c_link_layer_inst.rx_data_valid +@200 +-State +@28 +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.ready +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.state +@24 +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.bits_shifted +@28 +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.rx_ack_valid +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.rx_ack +i2c_tb.th.dut.i2c_link_layer_inst.sm_reg.ack_sending +@200 +-Counter +@22 +i2c_tb.th.dut.i2c_link_layer_inst.sm_countdown.counter[7:0] +@28 +i2c_tb.th.dut.i2c_link_layer_inst.sm_countdown.decr +i2c_tb.th.dut.i2c_link_layer_inst.sm_countdown.load +i2c_tb.th.dut.i2c_link_layer_inst.sm_countdown.done +@200 +- +-RX Stream VC +@28 +i2c_tb.th.rx_sink_vc.ready +i2c_tb.th.rx_sink_vc.valid +@22 +i2c_tb.th.rx_sink_vc.data[7:0] +@200 +-TX Stream VC +@28 +i2c_tb.th.tx_source_vc.ready +i2c_tb.th.tx_source_vc.valid +@22 +i2c_tb.th.tx_source_vc.data[7:0] +@200 +- +-Peripheral Model +-SCL +@28 +i2c_tb.th.peripheral.scl_if.i +i2c_tb.th.peripheral.scl_if.o +i2c_tb.th.peripheral.scl_if.oe +@200 +-SDA +@28 +i2c_tb.th.peripheral.sda_if.i +i2c_tb.th.peripheral.sda_if.o +i2c_tb.th.peripheral.sda_if.oe +i2c_tb.th.peripheral.state +i2c_tb.th.peripheral.start_condition +i2c_tb.th.peripheral.stop_condition +@22 +i2c_tb.th.peripheral.reg_addr[7:0] +@28 +i2c_tb.th.peripheral.addr_set +i2c_tb.th.peripheral.addr_incr +i2c_tb.th.peripheral.rx_done +@22 +i2c_tb.th.peripheral.rx_data[7:0] +i2c_tb.th.peripheral.rx_bit_count[3:0] +@28 +i2c_tb.th.peripheral.tx_done +@c00022 +i2c_tb.th.peripheral.tx_data[7:0] +@28 +(0)i2c_tb.th.peripheral.tx_data[7:0] +(1)i2c_tb.th.peripheral.tx_data[7:0] +(2)i2c_tb.th.peripheral.tx_data[7:0] +(3)i2c_tb.th.peripheral.tx_data[7:0] +(4)i2c_tb.th.peripheral.tx_data[7:0] +(5)i2c_tb.th.peripheral.tx_data[7:0] +(6)i2c_tb.th.peripheral.tx_data[7:0] +(7)i2c_tb.th.peripheral.tx_data[7:0] +@1401200 +-group_end +@22 +i2c_tb.th.peripheral.tx_bit_count[3:0] +[pattern_trace] 1 +[pattern_trace] 0 diff --git a/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.vhd b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.vhd new file mode 100644 index 00000000..9c6ab31a --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_tb.vhd @@ -0,0 +1,175 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; + +use work.i2c_cmd_vc_pkg.all; +use work.i2c_peripheral_pkg.all; +use work.basic_stream_pkg.all; + +use work.i2c_common_pkg.all; + +entity i2c_txn_layer_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of i2c_txn_layer_tb is + constant I2C_PERIPHERAL_VC : i2c_peripheral_t := new_i2c_peripheral_vc("I2C_PERIPH"); + constant TX_DATA_SOURCE_VC : basic_source_t := new_basic_source(8); + constant RX_DATA_SINK_VC : basic_sink_t := new_basic_sink(8); + constant I2C_CMD_VC : i2c_cmd_vc_t := new_i2c_cmd_vc; +begin + + th: entity work.i2c_txn_layer_th + generic map ( + tx_source => TX_DATA_SOURCE_VC, + rx_sink => RX_DATA_SINK_VC, + i2c_peripheral => I2C_PERIPHERAL_VC, + i2c_cmd_vc => I2C_CMD_VC + ); + + bench: process + alias reset is << signal th.reset : std_logic >>; + + variable command : cmd_t; + variable ack : boolean := false; + + variable data : std_logic_vector(7 downto 0); + variable expected_addr : unsigned(7 downto 0); + variable expected_data : std_logic_vector(7 downto 0); + variable byte_len : natural; + begin + -- Always the first thing in the process, set up things for the VUnit test runner + test_runner_setup(runner, runner_cfg); + -- Reach into the test harness, which generates and de-asserts reset and hold the + -- test cases off until we're out of reset. This runs for every test case + wait until reset = '0'; + wait for 500 ns; -- let the resets propagate + + while test_suite loop + if run("nack_wrong_address") then + command := CMD_RESET; + push_i2c_cmd(net, I2C_CMD_VC, command); + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_false(ack, "Peripheral did not NACK incorrect address"); + -- transaction is over, receive STOP event + expect_stop(net, I2C_PERIPHERAL_VC); + elsif run("write_and_read_one_byte") then + -- arbitrary for the test + expected_addr := X"9E"; + expected_data := X"A5"; + + -- write some data in + command := ( + op => WRITE, + addr => address(I2C_PERIPHERAL_VC), + reg => std_logic_vector(expected_addr), + len => to_unsigned(1, command.len'length) + ); + push_i2c_cmd(net, I2C_CMD_VC, command); + + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_true(ack, "Peripheral did not ACK correct address"); + + push_basic_stream(net, TX_DATA_SOURCE_VC, expected_data); + check_written_byte(net, I2C_PERIPHERAL_VC, expected_data, expected_addr); + + expect_stop(net, I2C_PERIPHERAL_VC); + + -- read it back out + command := ( + op => READ, + addr => address(I2C_PERIPHERAL_VC), + reg => X"--", -- READ uses internal address set by a WRITE to the peripheral + len => to_unsigned(1, command.len'length) + ); + push_i2c_cmd(net, I2C_CMD_VC, command); + + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_true(ack, "Peripheral did not ACK correct address"); + + pop_basic_stream(net, RX_DATA_SINK_VC, data); + check_equal(data, expected_data, "Expected read data to match"); + + expect_stop(net, I2C_PERIPHERAL_VC); + elsif run("write_and_read_many_bytes") then + -- arbitrary for the test + expected_addr := X"00"; + byte_len := 8; + + -- write some data in + command := ( + op => WRITE, + addr => address(I2C_PERIPHERAL_VC), + reg => std_logic_vector(expected_addr), + len => to_unsigned(byte_len, command.len'length) + ); + push_i2c_cmd(net, I2C_CMD_VC, command); + + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_true(ack, "Peripheral did not ACK correct address"); + + for byte_idx in 0 to byte_len - 1 loop + data := std_logic_vector(to_unsigned(byte_idx, data'length)); + expected_addr := to_unsigned(byte_idx, expected_addr'length); + push_basic_stream(net, TX_DATA_SOURCE_VC, data); + end loop; + + for byte_idx in 0 to byte_len - 1 loop + data := std_logic_vector(to_unsigned(byte_idx, data'length)); + expected_addr := to_unsigned(byte_idx, expected_addr'length); + check_written_byte(net, I2C_PERIPHERAL_VC, data, expected_addr); + end loop; + + expect_stop(net, I2C_PERIPHERAL_VC); + + -- read it back out + expected_addr := X"00"; + command := ( + op => RANDOM_READ, + addr => address(I2C_PERIPHERAL_VC), + reg => std_logic_vector(expected_addr), + len => to_unsigned(byte_len, command.len'length) + ); + push_i2c_cmd(net, I2C_CMD_VC, command); + + -- ACK WRITE portion of the random read + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_true(ack, "Peripheral did not ACK correct address"); + + -- ACK READ portion of the random read + start_byte_ack(net, I2C_PERIPHERAL_VC, ack); + check_true(ack, "Peripheral did not ACK correct address"); + + for byte_idx in 0 to byte_len - 1 loop + expected_data := std_logic_vector(to_unsigned(byte_idx, expected_data'length)); + pop_basic_stream(net, RX_DATA_SINK_VC, data); + check_equal(data, expected_data, "Expected read data to match"); + end loop; + + expect_stop(net, I2C_PERIPHERAL_VC); + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + -- Example total test timeout dog + test_runner_watchdog(runner, 1 ms); + +end tb; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_th.vhd b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_th.vhd new file mode 100644 index 00000000..6a525411 --- /dev/null +++ b/hdl/ip/vhd/i2c/sims/txn_layer/i2c_txn_layer_th.vhd @@ -0,0 +1,160 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +use work.i2c_common_pkg.all; +use work.i2c_common_pkg.all; +use work.tristate_if_pkg.all; +use work.stream8_pkg; + +use work.i2c_cmd_vc_pkg.all; +use work.i2c_peripheral_pkg.all; +use work.basic_stream_pkg.all; + +entity i2c_txn_layer_th is + generic ( + tx_source : basic_source_t; + rx_sink : basic_sink_t; + i2c_peripheral : i2c_peripheral_t; + i2c_cmd_vc : i2c_cmd_vc_t + ); +end entity; + +architecture th of i2c_txn_layer_th is + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + + -- I2C interfaces + signal ctrl_scl_tristate : tristate; + signal ctrl_sda_tristate : tristate; + signal periph_scl_tristate : tristate; + signal periph_sda_tristate : tristate; + + -- I2C bus + signal scl : std_logic; + signal sda : std_logic; + + -- command interface + signal command : cmd_t; + signal command_valid : std_logic; + signal core_ready : std_logic; + + -- streaming interfaces + signal tx_data_stream : stream8_pkg.data_channel; + signal rx_data_stream : stream8_pkg.data_channel; + +begin + + -- set up a fastish clock for the sim + -- env and release reset after a bit of time + clk <= not clk after 4 ns; + reset <= '0' after 200 ns; + + dut: entity work.i2c_txn_layer + generic map ( + CLK_PER_NS => 8, + MODE => STANDARD + ) + port map ( + clk => clk, + reset => reset, + scl_if => ctrl_scl_tristate, + sda_if => ctrl_sda_tristate, + cmd => command, + cmd_valid => command_valid, + core_ready => core_ready, + tx_st_if => tx_data_stream, + rx_st_if => rx_data_stream + ); + + i2c_cmd_vc_inst: entity work.i2c_cmd_vc + generic map ( + i2c_cmd_vc => i2c_cmd_vc + ) + port map ( + cmd => command, + valid => command_valid, + ready => core_ready + ); + + peripheral: entity work.i2c_peripheral + generic map ( + i2c_peripheral_vc => i2c_peripheral + ) + port map ( + scl_if.i => periph_scl_tristate.i, + scl_if.o => periph_scl_tristate.o, + scl_if.oe => periph_scl_tristate.oe, + sda_if => periph_sda_tristate + ); + + tx_source_vc : entity work.basic_source + generic map ( + source => tx_source + ) + port map ( + clk => clk, + valid => tx_data_stream.valid, + ready => tx_data_stream.ready, + data => tx_data_stream.data + ); + + rx_sink_vc : entity work.basic_sink + generic map ( + sink => rx_sink + ) + port map ( + clk => clk, + valid => rx_data_stream.valid, + ready => rx_data_stream.ready, + data => rx_data_stream.data + ); + + -- wire the bus to the tristate inputs + ctrl_scl_tristate.i <= scl; + periph_scl_tristate.i <= scl; + ctrl_sda_tristate.i <= sda; + periph_sda_tristate.i <= sda; + i2c_bus_resolver: process(all) + begin + if ctrl_scl_tristate.oe = '1' and periph_scl_tristate.oe = '0' then + -- controller has line + scl <= ctrl_scl_tristate.o; + elsif ctrl_scl_tristate.oe = '0' and periph_scl_tristate.oe = '1' then + -- peripheral has line + scl <= periph_scl_tristate.o; + elsif ctrl_scl_tristate.oe = '1' and periph_scl_tristate.oe = '1' then + -- contention + scl <= 'Z'; + else + -- line floats to pull-up + scl <= '1'; + end if; + + if ctrl_sda_tristate.oe = '1' and periph_sda_tristate.oe = '0' then + -- controller has line + sda <= ctrl_sda_tristate.o; + elsif ctrl_sda_tristate.oe = '0' and periph_sda_tristate.oe = '1' then + -- peripheral has line + sda <= periph_sda_tristate.o; + elsif ctrl_sda_tristate.oe = '1' and periph_sda_tristate.oe = '1' then + -- contention + sda <= 'Z'; + else + -- line floats to pull-up + sda <= '1'; + end if; + end process; + +end th; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/txn_layer/i2c_txn_layer.vhd b/hdl/ip/vhd/i2c/txn_layer/i2c_txn_layer.vhd new file mode 100644 index 00000000..ed888c93 --- /dev/null +++ b/hdl/ip/vhd/i2c/txn_layer/i2c_txn_layer.vhd @@ -0,0 +1,240 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2024 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.stream8_pkg; +use work.tristate_if_pkg.all; + +use work.i2c_common_pkg.all; +use work.i2c_common_pkg.all; + +entity i2c_txn_layer is + generic ( + CLK_PER_NS : positive; + MODE : mode_t; + ); + port ( + clk : in std_logic; + reset : in std_logic; + + -- Tri-state signals to I2C interface + scl_if : view tristate_if; + sda_if : view tristate_if; + + -- I2C command interface + cmd : in cmd_t; + cmd_valid : in std_logic; + core_ready : out std_logic; + + -- Transmit data stream + tx_st_if : view stream8_pkg.st_sink_if; + + -- Received data stream + rx_st_if : view stream8_pkg.st_source_if; + ); +end entity; + +architecture rtl of i2c_txn_layer is + + type state_t is ( + IDLE, + START, + WAIT_START, + WAIT_ADDR_ACK, + READ, + WRITE, + WAIT_WRITE_ACK, + STOP, + WAIT_STOP + ); + + type sm_reg_t is record + state : state_t; + cmd : cmd_t; + in_random_read : boolean; + bytes_done : unsigned(7 downto 0); + do_start : std_logic; + do_ack : std_logic; + do_stop : std_logic; + tx_byte : std_logic_vector(7 downto 0); + tx_byte_valid : std_logic; + end record; + + constant SM_REG_RESET : sm_reg_t := ( + IDLE, -- state + CMD_RESET, -- cmd + false, -- in_random_read + (others => '0'),-- bytes_done + '0', -- do_start + '0', -- do_ack + '0', -- do_stop + (others => '0'),-- tx_byte + '0' -- tx_byte_valid + ); + + signal sm_reg, sm_reg_next : sm_reg_t; + + signal ll_ready : std_logic; + signal ll_ackd : std_logic; + signal ll_ackd_valid : std_logic; + signal ll_rx_data : std_logic_vector(7 downto 0); + signal ll_rx_data_valid : std_logic; +begin + + -- The block that handles the link layer of the protocol + i2c_link_layer_inst: entity work.i2c_link_layer + generic map( + CLK_PER_NS => CLK_PER_NS, + MODE => MODE + ) + port map( + clk => clk, + reset => reset, + scl_if => scl_if, + sda_if => sda_if, + tx_start => sm_reg.do_start, + tx_ack => sm_reg.do_ack, + tx_stop => sm_reg.do_stop, + ready => ll_ready, + tx_data => sm_reg.tx_byte, + tx_data_valid => sm_reg.tx_byte_valid, + tx_ackd => ll_ackd, + tx_ackd_valid => ll_ackd_valid, + rx_data => ll_rx_data, + rx_data_valid => ll_rx_data_valid + ); + + reg_sm_next: process(all) + variable v : sm_reg_t; + variable is_read : std_logic; + variable txd_v : std_logic_vector(7 downto 0); + variable txd_valid_v : std_logic; + begin + v := sm_reg; + is_read := '1' when sm_reg.cmd.op = READ or sm_reg.in_random_read else '0'; + txd_valid_v := '0'; + + case sm_reg.state is + + -- watch for a new command to arrive then kick off a START + when IDLE => + if cmd_valid = '1' and ll_ready = '1' then + v.state := START; + v.cmd := cmd; + end if; + + -- single cycle state to initiate a START + when START => + v.state := WAIT_START; + + -- wait for link layer to finish START sequence and load up the address byte + when WAIT_START => + txd_v := sm_reg.cmd.addr & is_read; + txd_valid_v := '1'; + if ll_ready then + v.state := WAIT_ADDR_ACK; + end if; + + -- wait for address byte to have been sent and for the peripheral to ACK + when WAIT_ADDR_ACK => + if ll_ready = '1' and ll_ackd_valid = '1' then + if ll_ackd then + v.bytes_done := (others => '0'); + if sm_reg.cmd.op = Read or sm_reg.in_random_read then + v.state := READ; + -- nack after the first byte when only reading one byte + v.do_ack := '0' when sm_reg.cmd.len = 1 else '1'; + else + v.state := WAIT_WRITE_ACK; + -- load up the register address + txd_v := sm_reg.cmd.reg; + txd_valid_v := '1'; + end if; + else + -- TODO: address nack error + v.state := STOP; + end if; + end if; + + -- read as many bytes as requested + when READ => + if sm_reg.cmd.len = sm_reg.bytes_done and ll_ready = '1' then + v.state := STOP; + elsif ll_rx_data_valid then + v.bytes_done := sm_reg.bytes_done + 1; + -- nack the next byte if it is the last + v.do_ack := '0' when sm_reg.cmd.len = v.bytes_done else '1'; + end if; + + -- transmit the next byte + when WRITE => + if tx_st_if.valid then + v.state := WAIT_WRITE_ACK; + end if; + + -- take action based off of the operation type and the ACK + when WAIT_WRITE_ACK => + if ll_ackd_valid then + if ll_ackd then + v.bytes_done := sm_reg.bytes_done + 1; + + -- The register address was written, now go restart for a read + if sm_reg.cmd.op = RANDOM_READ then + v.state := START; + v.in_random_read := true; + elsif sm_reg.cmd.len = sm_reg.bytes_done then + v.state := STOP; + else + v.state := WRITE; + end if; + else + -- TODO: byte nack error + v.state := STOP; + end if; + end if; + + -- initiate a STOP and clear state + when STOP => + v.state := WAIT_STOP; + v.bytes_done := (others => '0'); + v.in_random_read := false; + + -- once STOP has finished move back to IDLE + when WAIT_STOP => + if ll_ready then + v.state := IDLE; + end if; + end case; + + -- next state logic + v.do_start := '1' when v.state = START else '0'; + v.do_stop := '1' when v.state = STOP else '0'; + + v.tx_byte := txd_v when sm_reg.state = WAIT_START or sm_reg.state = WAIT_ADDR_ACK else tx_st_if.data; + v.tx_byte_valid := txd_valid_v when sm_reg.state = WAIT_START or sm_reg.state = WAIT_ADDR_ACK else tx_st_if.valid; + + sm_reg_next <= v; + end process; + + + reg_sm: process(clk, reset) + begin + if reset then + sm_reg <= SM_REG_RESET; + elsif rising_edge(clk) then + sm_reg <= sm_reg_next; + end if; + end process; + + core_ready <= '1' when sm_reg.state = IDLE else '0'; + tx_st_if.ready <= '1' when sm_reg.state = WRITE else '0'; + rx_st_if.valid <= ll_rx_data_valid when sm_reg.state = READ else '0'; + rx_st_if.data <= ll_rx_data; + +end rtl; \ No newline at end of file diff --git a/hdl/ip/vhd/vunit_components/basic_stream/sims/th_basic_stream.vhd b/hdl/ip/vhd/vunit_components/basic_stream/sims/th_basic_stream.vhd index cc79baee..62be7f09 100644 --- a/hdl/ip/vhd/vunit_components/basic_stream/sims/th_basic_stream.vhd +++ b/hdl/ip/vhd/vunit_components/basic_stream/sims/th_basic_stream.vhd @@ -17,8 +17,8 @@ use work.basic_stream_pkg.all; entity th_basic_stream is generic ( - source : basic_source_t; - sink: basic_sink_t + source : basic_source_t; + sink : basic_sink_t ); end entity;