diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK new file mode 100644 index 00000000..27ea5284 --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK @@ -0,0 +1,29 @@ +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:rdl.bzl", "rdl_file") + +rdl_file( + name = "pca9506_regs_pkg", + src = "pca9506_regs.rdl", + outputs = ["pca9506_regs_pkg.vhd", "pca9506_regs_pkg.html"], + visibility = ['PUBLIC'] +) + +vhdl_unit( + name = "pca9506_top", + srcs = glob(["*.vhd"]), + deps = [ + "//hdl/ip/vhd/i2c/target:i2c_target_phy", + ":pca9506_regs_pkg", + ], + visibility = ['PUBLIC'] +) + +vunit_sim( + name = "i2c_pca9506ish_tb", + srcs = glob(["sims/**/*.vhd"]), + deps = [ + ":pca9506_top", + "//hdl/ip/vhd/vunit_components:i2c_controller_vc", + ], + visibility = ['PUBLIC'], +) \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_function.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_function.vhd new file mode 100644 index 00000000..5a770e91 --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_function.vhd @@ -0,0 +1,201 @@ +-- 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 2025 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std_unsigned.all; + +use work.i2c_base_types_pkg.all; +use work.pca9506_pkg.all; + +entity pca9506ish_function is + generic( + -- i2c address of the mux + i2c_addr : std_logic_vector(6 downto 0) := 7x"70" + ); + port( + clk : in std_logic; + reset : in std_logic; + -- PHY interface + -- instruction interface + inst_data : in std_logic_vector(7 downto 0); + inst_valid : in std_logic; + inst_ready : out std_logic; + in_ack_phase: in std_logic; + ack_next : out std_logic; + txn_header : in i2c_header; + start_condition : in std_logic; + stop_condition : in std_logic; + -- response interface + resp_data : out std_logic_vector(7 downto 0); + resp_valid : out std_logic; + resp_ready : in std_logic; + + -- internal register interface + read_data : in std_logic_vector(7 downto 0); + write_data : out std_logic_vector(7 downto 0); + read_strobe : out std_logic; + write_strobe : out std_logic; + cmd_ptr : out cmd_t + ); +end entity; + +architecture rtl of pca9506ish_function is + + type state_t is (IDLE, WAIT_FOR_ACKNACK, WAIT_FOR_ACK_PHASE, ACK, NACK, COMMAND, DO_WRITE, DO_READ); + + type reg_t is record + state : state_t; + post_ack_state : state_t; + read_strobe : std_logic; + write_strobe : std_logic; + cmd_reg : cmd_t; + data : std_logic_vector(7 downto 0); + data_valid: std_logic; + increment : std_logic; + end record; + + constant rec_reset : reg_t := ( + state => IDLE, + post_ack_state => IDLE, + read_strobe => '0', + write_strobe => '0', + cmd_reg => default_reset, + data => (others => '0'), + data_valid => '0', + increment => '0' + ); + + signal r, rin : reg_t; + +begin + + --assign some outputs + read_strobe <= r.read_strobe; + write_strobe <= r.write_strobe; + cmd_ptr <= r.cmd_reg; + resp_data <= r.data; + ack_next <= '1' when r.state = ACK else '0'; + write_data <= r.data; + inst_ready <= '1' when r.state = COMMAND or r.state = DO_WRITE else '0'; + resp_valid <= r.data_valid; + + cm: process(all) + variable v : reg_t; + begin + v := r; + + case r.state is + when IDLE => + v.increment := '0'; + if txn_header.valid = '1' and txn_header.tgt_addr = i2c_addr then + v.state := ACK; + if txn_header.read_write_n = '0' then + -- all writes go through command state after target + v.post_ack_state := COMMAND; + else + -- post repeated start, reads bypass command state + -- and immediately do reads + v.post_ack_state := DO_READ; + -- pointer is valid + end if; + end if; + + + when COMMAND => + if inst_valid = '1' and inst_ready = '1' then + v.cmd_reg.ai := inst_data(7); + v.cmd_reg.pointer := inst_data(5 downto 0); + v.post_ack_state := DO_WRITE; + v.state := ACK; + end if; + + when DO_WRITE => + if inst_valid = '1' and inst_ready = '1' then + v.data := inst_data; + v.state := ACK; + v.write_strobe := '1'; + v.increment := v.cmd_reg.ai; + end if; + + when DO_READ => + v.read_strobe := '1'; + v.data_valid := '1'; + v.data := read_data; + v.state := WAIT_FOR_ACK_PHASE; + + when ACK => + -- clear any single-cycle strobes + v.write_strobe := '0'; + v.read_strobe := '0'; + -- wait for ack time to finish + if in_ack_phase = '0' then + v.state := r.post_ack_state; + if r.increment then + -- do a category-wrapping increment + v.cmd_reg.pointer := category_wrapping_increment(r.cmd_reg.pointer); + end if; + end if; + when NACK => + if in_ack_phase = '0' then + v.state := IDLE; + end if; + + when WAIT_FOR_ACK_PHASE => + -- clear any single-cycle strobes + v.write_strobe := '0'; + v.read_strobe := '0'; + if resp_valid = '1' and resp_ready = '1' then + v.data_valid := '0'; + v.increment := v.cmd_reg.ai; + end if; + if in_ack_phase = '1' then + v.state := WAIT_FOR_ACKNACK; + end if; + + when WAIT_FOR_ACKNACK => + -- clear any single-cycle strobes + v.write_strobe := '0'; + v.read_strobe := '0'; + if in_ack_phase = '0' then + v.state := DO_READ; + if r.increment then + -- do a category-wrapping increment + v.cmd_reg.pointer := category_wrapping_increment(r.cmd_reg.pointer); + end if; + end if; + + when others => + v.state := IDLE; + + end case; + + if resp_valid = '1' and resp_ready = '1' then + v.data_valid := '0'; + end if; + + -- No matter where we were, do cleanup if we see a stop + if start_condition = '1' or stop_condition = '1' then + v.state := IDLE; + v.write_strobe := '0'; + v.read_strobe := '0'; + end if; + + rin <= v; + end process; + + + reg: process(clk, reset) + begin + if reset = '1' then + r <= rec_reset; + elsif rising_edge(clk) then + r <= rin; + end if; +end process; + + +end rtl; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_pkg.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_pkg.vhd new file mode 100644 index 00000000..7024e1bd --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_pkg.vhd @@ -0,0 +1,77 @@ +-- 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 2025 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std_unsigned.all; + +use work.pca9506_regs_pkg.all; + +package pca9506_pkg is + + type cmd_t is record + ai : std_logic; -- auto-increment + pointer: std_logic_vector(5 downto 0); -- register number + end record; -- default to 0x80 + + constant default_reset : cmd_t := ( + ai => '1', + pointer => (others => '0') + ); + + type pca9506_pin_t is array (0 to 4) of std_logic_vector(7 downto 0); + + function get_irq_pend( + cur_reg: io_type; + reg_at_last_read: io_type; + reg_mask: io_type + ) return std_logic; + + function category_wrapping_increment( + pointer: std_logic_vector(5 downto 0) -- register number + ) return std_logic_vector; + + + +end package; + +package body pca9506_pkg is + + function get_irq_pend( + cur_reg: io_type; + reg_at_last_read: io_type; + reg_mask: io_type + ) return std_logic + is + + begin + -- bitwise XOR current register with last to detect any changes + -- then bitwise mask off any masked bits + -- for the mask, '1' indicates "masked" so we need a bitwise inversion + -- before the bitwise AND for masking. + -- Finally, we need to reduce this down to a single bit to return, + -- We don't currently support unary reduction operators for record + -- types generated by RDL so we use the "compress" function to get + -- back a std_logic_vector of the used bits, and then we use a unary + -- reduction "or" operator on that to return 1 if any bits were 1 + return or compress((cur_reg xor reg_at_last_read) and (not reg_mask)); + end function; + + function category_wrapping_increment( + pointer: std_logic_vector(5 downto 0) -- register number + ) return std_logic_vector is + variable lsbs : std_logic_vector(2 downto 0); + + begin + lsbs := pointer(2 downto 0) + 1; + if lsbs > 4 then + lsbs := (others => '0'); + end if; + + -- increment the 3 lsb by one from 0 to 4, wrapping around to 0 + return pointer(5 downto 3) & lsbs; + end function; +end package body pca9506_pkg; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.rdl b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.rdl new file mode 100644 index 00000000..c84ff15c --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.rdl @@ -0,0 +1,163 @@ +// Copyright 2025 Oxide Computer Company +// This is SystemRDL description of emulated i2c mux registers + +addrmap pca9506_regs { + name = "pca9506 registers"; + desc = "Registers accessible on the i2c bus for controlling the emulated PCA9545 i2c mux"; + + default regwidth = 8; + default sw = rw; + default hw = r; + + reg io { + field { + desc = "bits for current port"; + } bits[7:0]; + }; + + io ip0 @ 0x00; + ip0->name = "Ip0"; + ip0->desc = "I/O port input state after polarity inversion (if enabled)"; + ip0.bits->sw = r; + + io ip1; + ip1->name = "Ip1"; + ip1->desc = "I/O port input state after polarity inversion (if enabled)"; + ip1.bits->sw = r; + + io ip2; + ip2->name = "Ip2"; + ip2->desc = "I/O port input state after polarity inversion (if enabled)"; + ip2.bits->sw = r; + + io ip3; + ip3->name = "Ip3"; + ip3->desc = "I/O port input state after polarity inversion (if enabled)"; + ip3.bits->sw = r; + + io ip4; + ip4->name = "Ip4"; + ip4->desc = "I/O port input state after polarity inversion (if enabled)"; + ip4.bits->sw = r; + + io op0 @ 0x08; + op0->name = "Op0"; + op0->desc = "These registers reflect the outgoing logic levels of the pins defined as outputs by the +I/O Configuration register. Bit values in these registers have no effect on pins defined +as inputs. In turn, reads from these registers reflect the values that are in the flip-flops +controlling the output selection, not the actual pin values."; + + io op1; + op1->name = "Op1"; + op1->desc = "These registers reflect the outgoing logic levels of the pins defined as outputs by the +I/O Configuration register. Bit values in these registers have no effect on pins defined +as inputs. In turn, reads from these registers reflect the values that are in the flip-flops +controlling the output selection, not the actual pin values."; + + io op2; + op2->name = "Op2"; + op2->desc = "These registers reflect the outgoing logic levels of the pins defined as outputs by the +I/O Configuration register. Bit values in these registers have no effect on pins defined +as inputs. In turn, reads from these registers reflect the values that are in the flip-flops +controlling the output selection, not the actual pin values."; + + io op3; + op3->name = "Op3"; + op3->desc = "These registers reflect the outgoing logic levels of the pins defined as outputs by the +I/O Configuration register. Bit values in these registers have no effect on pins defined +as inputs. In turn, reads from these registers reflect the values that are in the flip-flops +controlling the output selection, not the actual pin values."; + + io op4; + op4->name = "Op4"; + op4->desc = "These registers reflect the outgoing logic levels of the pins defined as outputs by the +I/O Configuration register. Bit values in these registers have no effect on pins defined +as inputs. In turn, reads from these registers reflect the values that are in the flip-flops +controlling the output selection, not the actual pin values."; + + io pi0 @ 0x10; + pi0->name = "pi0"; + pi0->desc = "Polarity Inversion of inputs. 0: no inversion, 1: invert input"; + + io pi1 ; + pi1->name = "pi1"; + pi1->desc = "Polarity Inversion of inputs. 0: no inversion, 1: invert input"; + + io pi2 ; + pi2->name = "pi2"; + pi2->desc = "Polarity Inversion of inputs. 0: no inversion, 1: invert input"; + + io pi3 ; + pi3->name = "pi3"; + pi3->desc = "Polarity Inversion of inputs. 0: no inversion, 1: invert input"; + + io pi4 ; + pi4->name = "pi4"; + pi4->desc = "Polarity Inversion of inputs. 0: no inversion, 1: invert input"; + + io ioc0 @ 0x18; + ioc0->name = "ioc0"; + ioc0->desc = "Config registers. 0: output, 1: input"; + ioc0.bits->reset = 0xff; + + io ioc1; + ioc1->name = "ioc1"; + ioc1->desc = "Config registers. 0: output, 1: input"; + ioc1.bits->reset = 0xff; + + io ioc2; + ioc2->name = "ioc2"; + ioc2->desc = "Config registers. 0: output, 1: input"; + ioc2.bits->reset = 0xff; + + io ioc3; + ioc3->name = "ioc3"; + ioc3->desc = "Config registers. 0: output, 1: input"; + ioc3.bits->reset = 0xff; + + io ioc4; + ioc4->name = "ioc4"; + ioc4->desc = "Config registers. 0: output, 1: input"; + ioc4.bits->reset = 0xff; + + io msk0 @ 0x20; + msk0->name = "msk0"; + msk0->desc = "Mx[y] = 0: A level change at the I/O will generate an interrupt if IOx_y defined as input +(Cx[y] in IOC register = 1). +Mx[y] = 1: A level change in the input port will not generate an interrupt if IOx_y +defined as input (Cx[y] in IOC register = 1)."; + msk0.bits->reset = 0xff; + + io msk1; + msk1->name = "msk1"; + msk1->desc = "Mx[y] = 0: A level change at the I/O will generate an interrupt if IOx_y defined as input +(Cx[y] in IOC register = 1). +Mx[y] = 1: A level change in the input port will not generate an interrupt if IOx_y +defined as input (Cx[y] in IOC register = 1)."; + msk1.bits->reset = 0xff; + + io msk2; + msk2->name = "msk2"; + msk2->desc = "Mx[y] = 0: A level change at the I/O will generate an interrupt if IOx_y defined as input +(Cx[y] in IOC register = 1). +Mx[y] = 1: A level change in the input port will not generate an interrupt if IOx_y +defined as input (Cx[y] in IOC register = 1)."; + msk2.bits->reset = 0xff; + + io msk3; + msk3->name = "msk3"; + msk3->desc = "Mx[y] = 0: A level change at the I/O will generate an interrupt if IOx_y defined as input +(Cx[y] in IOC register = 1). +Mx[y] = 1: A level change in the input port will not generate an interrupt if IOx_y +defined as input (Cx[y] in IOC register = 1)."; + msk3.bits->reset = 0xff; + + io msk4; + msk4->name = "msk4"; + msk4->desc = "Mx[y] = 0: A level change at the I/O will generate an interrupt if IOx_y defined as input +(Cx[y] in IOC register = 1). +Mx[y] = 1: A level change in the input port will not generate an interrupt if IOx_y +defined as input (Cx[y] in IOC register = 1)."; + msk4.bits->reset = 0xff; + +}; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.vhd new file mode 100644 index 00000000..ee6952fc --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_regs.vhd @@ -0,0 +1,326 @@ +-- 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 2025 Oxide Computer Company + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std_unsigned.all; + +use work.pca9506_pkg.all; +use work.pca9506_regs_pkg.all; + +entity pca9506_regs is + port ( + clk : in std_logic; + reset : in std_logic; + + -- i2c control interface + cmd_ptr : in cmd_t; + write_strobe: in std_logic; + read_strobe: in std_logic; + data_in: in std_logic_vector(7 downto 0); + data_out: out std_logic_vector(7 downto 0); + + -- io interface + output_disable: in std_logic; + io : in pca9506_pin_t; + io_oe : out pca9506_pin_t; + io_o : out pca9506_pin_t; + int_n : out std_logic + + + ); +end entity; + +architecture rtl of pca9506_regs is + + signal ip0_reg : io_type; + signal ip1_reg : io_type; + signal ip2_reg : io_type; + signal ip3_reg : io_type; + signal ip4_reg : io_type; + + signal ip0_reg_irq_at_last_read : io_type; + signal ip1_reg_irq_at_last_read : io_type; + signal ip2_reg_irq_at_last_read : io_type; + signal ip3_reg_irq_at_last_read : io_type; + signal ip4_reg_irq_at_last_read : io_type; + + -- We have to monitor each input port for changes independently + signal int_pend : std_logic_vector(4 downto 0); + + signal op0_reg : io_type; + signal op1_reg : io_type; + signal op2_reg : io_type; + signal op3_reg : io_type; + signal op4_reg : io_type; + + signal pi0_reg : io_type; + signal pi1_reg : io_type; + signal pi2_reg : io_type; + signal pi3_reg : io_type; + signal pi4_reg : io_type; + + signal ioc0_reg : io_type; + signal ioc1_reg : io_type; + signal ioc2_reg : io_type; + signal ioc3_reg : io_type; + signal ioc4_reg : io_type; + + signal msk0_reg : io_type; + signal msk1_reg : io_type; + signal msk2_reg : io_type; + signal msk3_reg : io_type; + signal msk4_reg : io_type; + +begin + + -- assign register outputs to output pins unconditionally + io_o(0) <= pack(op0_reg); + io_o(1) <= pack(op1_reg); + io_o(2) <= pack(op2_reg); + io_o(3) <= pack(op3_reg); + io_o(4) <= pack(op4_reg); + + -- register the output enables since they might have + -- some logic associated with them + output_pins_reg: process(clk, reset) + begin + if reset then + io_oe <= (others => (others =>'0')); + elsif rising_edge(clk) then + if output_disable = '1' then + io_oe <= (others => (others =>'0')); + else + -- ioc bit = 0 means output, so invert here + io_oe(0) <= not pack(ioc0_reg); + io_oe(1) <= not pack(ioc1_reg); + io_oe(2) <= not pack(ioc2_reg); + io_oe(3) <= not pack(ioc3_reg); + io_oe(4) <= not pack(ioc4_reg); + end if; + + end if; + end process; + + interrupt_logic: process(clk, reset) + begin + if reset then + int_n <= '1'; + int_pend <= (others => '0'); + + elsif rising_edge(clk) then + -- each register gets its own pending interrupt bit + -- here. These will clear once the corresponding register + -- is read, or the input state returns to the previously read state + int_pend(0) <= get_irq_pend(ip0_reg, ip0_reg_irq_at_last_read, msk0_reg); + int_pend(1) <= get_irq_pend(ip1_reg, ip1_reg_irq_at_last_read, msk1_reg); + int_pend(2) <= get_irq_pend(ip2_reg, ip2_reg_irq_at_last_read, msk2_reg); + int_pend(3) <= get_irq_pend(ip3_reg, ip3_reg_irq_at_last_read, msk3_reg); + int_pend(4) <= get_irq_pend(ip4_reg, ip4_reg_irq_at_last_read, msk4_reg); + + int_n <= '1' when int_pend = 0 else '0'; + end if; + end process; + + + + + i2c_write_regs: process(clk, reset) + begin + + if reset then + ip0_reg <= reset_0s; + ip1_reg <= reset_0s; + ip2_reg <= reset_0s; + ip3_reg <= reset_0s; + ip4_reg <= reset_0s; + op0_reg <= reset_0s; + op1_reg <= reset_0s; + op2_reg <= reset_0s; + op3_reg <= reset_0s; + op4_reg <= reset_0s; + pi0_reg <= reset_0s; + pi1_reg <= reset_0s; + pi2_reg <= reset_0s; + pi3_reg <= reset_0s; + pi4_reg <= reset_0s; + ioc0_reg <= reset_1s; + ioc1_reg <= reset_1s; + ioc2_reg <= reset_1s; + ioc3_reg <= reset_1s; + ioc4_reg <= reset_1s; + msk0_reg <= reset_1s; + msk1_reg <= reset_1s; + msk2_reg <= reset_1s; + msk3_reg <= reset_1s; + msk4_reg <= reset_1s; + + elsif rising_edge(clk) then + -- deal with inputs: + --TODO deal with inversion + ip0_reg <= unpack(io(0)); + ip1_reg <= unpack(io(1)); + ip2_reg <= unpack(io(2)); + ip3_reg <= unpack(io(3)); + ip4_reg <= unpack(io(4)); + -- deal with registers that are writeable by the + -- i2c system + -- TODO: deal with ext_reset + if write_strobe = '1' then + case to_integer(cmd_ptr.pointer) is + -- IP registers don't accept writes + when OP0_OFFSET => + op0_reg <= unpack(data_in); + when OP1_OFFSET => + op1_reg <= unpack(data_in); + when OP2_OFFSET => + op2_reg <= unpack(data_in); + when OP3_OFFSET => + op3_reg <= unpack(data_in); + when OP4_OFFSET => + op4_reg <= unpack(data_in); + + when PI0_OFFSET => + pi0_reg <= unpack(data_in); + when PI1_OFFSET => + pi1_reg <= unpack(data_in); + when PI2_OFFSET => + pi2_reg <= unpack(data_in); + when PI3_OFFSET => + pi3_reg <= unpack(data_in); + when PI4_OFFSET => + pi4_reg <= unpack(data_in); + + when IOC0_OFFSET => + ioc0_reg <= unpack(data_in); + when IOC1_OFFSET => + ioc1_reg <= unpack(data_in); + when IOC2_OFFSET => + ioc2_reg <= unpack(data_in); + when IOC3_OFFSET => + ioc3_reg <= unpack(data_in); + when IOC4_OFFSET => + ioc4_reg <= unpack(data_in); + + when MSK0_OFFSET => + msk0_reg <= unpack(data_in); + when MSK1_OFFSET => + msk1_reg <= unpack(data_in); + when MSK2_OFFSET => + msk2_reg <= unpack(data_in); + when MSK3_OFFSET => + msk3_reg <= unpack(data_in); + when MSK4_OFFSET => + msk4_reg <= unpack(data_in); + + when others => + null; + end case; + end if; + + end if; + + end process; + + i2c_read_regs: process(clk, reset) + begin + if reset then + data_out <= (others => '0'); + ip0_reg_irq_at_last_read <= reset_0s; + ip1_reg_irq_at_last_read <= reset_0s; + ip2_reg_irq_at_last_read <= reset_0s; + ip3_reg_irq_at_last_read <= reset_0s; + ip4_reg_irq_at_last_read <= reset_0s; + + elsif rising_edge(clk) then + + -- no need to gate on writes + -- here so always assign output + -- but deal with read-strobe for side-effects: + -- we have to keep track of the last-read + -- register state for interrupt generation. + case to_integer(cmd_ptr.pointer) is + when IP0_OFFSET => + -- deal with inversion, but only for the read + -- interface, the irq detection stuff deals + -- with the raw values (non-inverted) + data_out <= pack(ip0_reg xor pi0_reg); + if read_strobe then + ip0_reg_irq_at_last_read <= ip0_reg; + end if; + when IP1_OFFSET => + data_out <= pack(ip1_reg xor pi1_reg); + if read_strobe then + ip1_reg_irq_at_last_read <= ip1_reg; + end if; + when IP2_OFFSET => + data_out <= pack(ip2_reg xor pi2_reg); + if read_strobe then + ip2_reg_irq_at_last_read <= ip2_reg; + end if; + when IP3_OFFSET => + data_out <= pack(ip3_reg xor pi3_reg); + if read_strobe then + ip3_reg_irq_at_last_read <= ip3_reg; + end if; + when IP4_OFFSET => + data_out <= pack(ip4_reg xor pi4_reg); + if read_strobe then + ip4_reg_irq_at_last_read <= ip4_reg; + end if; + + when OP0_OFFSET => + data_out <= pack(op0_reg); + when OP1_OFFSET => + data_out <= pack(op1_reg); + when OP2_OFFSET => + data_out <= pack(op2_reg); + when OP3_OFFSET => + data_out <= pack(op3_reg); + when OP4_OFFSET => + data_out <= pack(op4_reg); + + when PI0_OFFSET => + data_out <= pack(pi0_reg); + when PI1_OFFSET => + data_out <= pack(pi1_reg); + when PI2_OFFSET => + data_out <= pack(pi2_reg); + when PI3_OFFSET => + data_out <= pack(pi3_reg); + when PI4_OFFSET => + data_out <= pack(pi4_reg); + + when IOC0_OFFSET => + data_out <= pack(ioc0_reg); + when IOC1_OFFSET => + data_out <= pack(ioc1_reg); + when IOC2_OFFSET => + data_out <= pack(ioc2_reg); + when IOC3_OFFSET => + data_out <= pack(ioc3_reg); + when IOC4_OFFSET => + data_out <= pack(ioc4_reg); + + when MSK0_OFFSET => + data_out <= pack(msk0_reg); + when MSK1_OFFSET => + data_out <= pack(msk1_reg); + when MSK2_OFFSET => + data_out <= pack(msk2_reg); + when MSK3_OFFSET => + data_out <= pack(msk3_reg); + when MSK4_OFFSET => + data_out <= pack(msk4_reg); + + when others => + data_out <= (others => '0'); + end case; + + end if; + end process; + +end rtl; diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_top.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_top.vhd new file mode 100644 index 00000000..41732302 --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/pca9506_top.vhd @@ -0,0 +1,140 @@ +-- 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 2025 Oxide Computer Company + +-- This provides an I/O expander sw-compatible with the PCA9506 memory map +-- upper level logic will decide whether or not to use the tri-state signals +-- and wether the individual pins are inputs or outputs. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std_unsigned.all; + +use work.i2c_base_types_pkg.all; +use work.pca9506_pkg.all; + +entity pca9506_top is + generic( + -- i2c address of the mux + i2c_addr : std_logic_vector(6 downto 0); + giltch_filter_cycles : integer := 5 + ); + port( + clk : in std_logic; + reset : in std_logic; + + -- I2C bus mux endpoint for control + -- Does not support clock-stretching + scl : in std_logic; + scl_o : out std_logic; + scl_oe : out std_logic; + sda : in std_logic; + sda_o : out std_logic; + sda_oe : out std_logic; + + -- Ports (5x 1 byte array, from pca9506_pkg) + io : in pca9506_pin_t; + io_oe : out pca9506_pin_t; + io_o : out pca9506_pin_t; + + int_n: out std_logic --can tri-state at top if desired using 1='Z' + + ); + +end entity; + +architecture rtl of pca9506_top is + signal inst_data : std_logic_vector(7 downto 0); + signal inst_valid : std_logic; + signal inst_ready : std_logic; + signal in_ack_phase : std_logic; + signal ack_next : std_logic; + signal txn_header : i2c_header; + signal start_condition : std_logic; + signal stop_condition : std_logic; + signal resp_data : std_logic_vector(7 downto 0); + signal resp_valid : std_logic; + signal resp_ready : std_logic; + signal read_strobe : std_logic; + signal write_strobe : std_logic; + signal cmd_ptr : cmd_t; + signal read_data : std_logic_vector(7 downto 0); + signal write_data : std_logic_vector(7 downto 0); + + +begin + + -- basic i2c state machine + i2c_target_phy_inst: entity work.i2c_target_phy + generic map( + giltch_filter_cycles => giltch_filter_cycles + ) + port map( + clk => clk, + reset => reset, + scl => scl, + scl_o => scl_o, + scl_oe => scl_oe, + sda => sda, + sda_o => sda_o, + sda_oe => sda_oe, + inst_data => inst_data, + inst_valid => inst_valid, + inst_ready => inst_ready, + in_ack_phase => in_ack_phase, + do_ack => ack_next, + start_condition => start_condition, + stop_condition => stop_condition, + txn_header => txn_header, + resp_data => resp_data, + resp_valid => resp_valid, + resp_ready => resp_ready + ); + + -- pca9506 functional block + pca9506ish_function_inst: entity work.pca9506ish_function + generic map( + i2c_addr => i2c_addr + ) + port map( + clk => clk, + reset => reset, + inst_data => inst_data, + inst_valid => inst_valid, + inst_ready => inst_ready, + in_ack_phase => in_ack_phase, + ack_next => ack_next, + txn_header => txn_header, + start_condition => start_condition, + stop_condition => stop_condition, + resp_data => resp_data, + resp_valid => resp_valid, + resp_ready => resp_ready, + read_strobe => read_strobe, + write_strobe => write_strobe, + read_data => read_data, + write_data => write_data, + cmd_ptr => cmd_ptr + ); + + -- registers block + pca9506_regs_inst: entity work.pca9506_regs + port map( + clk => clk, + reset => reset, + cmd_ptr => cmd_ptr, + write_strobe => write_strobe, + read_strobe => read_strobe, + data_in => write_data, + data_out => read_data, + output_disable => '0', + io => io, + io_oe => io_oe, + io_o => io_o, + int_n => int_n + ); + + +end rtl; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_sim_pkg.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_sim_pkg.vhd new file mode 100644 index 00000000..658887e1 --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_sim_pkg.vhd @@ -0,0 +1,124 @@ +-- 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 2025 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; + +use work.i2c_ctrl_vc_pkg.all; + +package i2c_pca9506ish_sim_pkg is + + constant i2c_tgt_addr: std_logic_vector(6 downto 0) := 7x"20"; + + constant i2c_ctrl_vc : i2c_ctrl_vc_t := new_i2c_ctrl_vc("i2c_ctrl_vc"); + + -- Some helper functions for writing test benches. + -- want to be able to read/write arbitrary registers + -- want to be able to read/write categories of registers (wrapping) + procedure single_write_pca9506_reg( + signal net : inout network_t; + constant register_addr : in integer range 0 to 16#27#; + constant write_data: in std_logic_vector(7 downto 0); + variable did_ack: inout boolean; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ); + procedure write_pca9506_reg( + signal net : inout network_t; + constant starting_reg : in integer range 0 to 16#27#; + constant byte_queue: in queue_t; + constant ack_queue: in queue_t; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ); + + procedure read_pca9506_reg( + signal net : inout network_t; + constant starting_reg : in integer range 0 to 16#27#; + constant num_regs_to_read : in integer; + constant response_queue: in queue_t; + constant ack_queue: in queue_t; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ); + +end package; + +package body i2c_pca9506ish_sim_pkg is + + procedure single_write_pca9506_reg( + signal net : inout network_t; + constant register_addr : in integer range 0 to 16#27#; + constant write_data: in std_logic_vector(7 downto 0); + variable did_ack: inout boolean; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ) is + constant tx_queue : queue_t := new_queue; + constant ack_queue: queue_t := new_queue; + begin + push_byte(tx_queue, to_integer(write_data)); + write_pca9506_reg(net, register_addr, tx_queue, tx_queue, auto_inc, i2c_addr); + did_ack := true; + while not is_empty(ack_queue) loop + if pop_byte(ack_queue) = 0 then + did_ack := false; + end if; + end loop; + + end procedure; + + procedure write_pca9506_reg( + signal net : inout network_t; + constant starting_reg : in integer range 0 to 16#27#; + constant byte_queue: in queue_t; + constant ack_queue: in queue_t; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ) is + variable cmd_reg : std_logic_vector(7 downto 0); + constant tx_queue : queue_t := new_queue; + begin + -- set up the cmd register + cmd_reg := std_logic_vector(to_unsigned(starting_reg, 8)); + if auto_inc then + cmd_reg(7) := '1'; + end if; + push_byte(tx_queue, to_integer(cmd_reg)); + while not is_empty(byte_queue) loop + push_byte(tx_queue, pop_byte(byte_queue)); + end loop; + i2c_write_txn(net, i2c_addr, tx_queue, ack_queue); + end procedure; + + procedure read_pca9506_reg( + signal net : inout network_t; + constant starting_reg : in integer range 0 to 16#27#; + constant num_regs_to_read : in integer; + constant response_queue: in queue_t; + constant ack_queue: in queue_t; + constant auto_inc: in boolean := true; + constant i2c_addr : in std_logic_vector(6 downto 0) := i2c_tgt_addr + ) is + variable cmd_reg : std_logic_vector(7 downto 0); + constant tx_queue : queue_t := new_queue; + begin + -- set up the cmd register + cmd_reg := std_logic_vector(to_unsigned(starting_reg, 8)); + if auto_inc then + cmd_reg(7) := '1'; + end if; + push_byte(tx_queue, to_integer(cmd_reg)); + i2c_mixed_txn(net, i2c_addr, tx_queue, num_regs_to_read, response_queue, ack_queue); + end procedure; + +end package body; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_tb.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_tb.vhd new file mode 100644 index 00000000..dd04518d --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_tb.vhd @@ -0,0 +1,313 @@ +-- 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 2025 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_ctrl_vc_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; +use work.pca9506_regs_pkg.all; +use work.pca9506_pkg.all; + +entity i2c_pca9506ish_tb is + generic ( + + runner_cfg : string + ); +end entity; + +architecture tb of i2c_pca9506ish_tb is + + +begin + + th: entity work.i2c_pca9506ish_th; + + bench: process + alias reset is << signal th.reset : std_logic >>; + alias int_n is << signal th.int_n : std_logic >>; + alias io is << signal th.io : pca9506_pin_t >>; + alias io_o is << signal th.io_o : pca9506_pin_t >>; + alias io_oe is << signal th.io_oe : pca9506_pin_t >>; + variable exp_queue : queue_t := new_queue; + variable tx_queue : queue_t := new_queue; + variable rx_queue : queue_t := new_queue; + variable ack_queue : queue_t := new_queue; + variable expected_ack_queue : queue_t := new_queue; + variable ack_status : boolean := false; + variable user_int : integer; + variable user_slv8 : std_logic_vector(7 downto 0); + + 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("check-read") then + -- read a default value from an internal register + read_pca9506_reg(net, IP0_OFFSET, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), 0, "Non-reset value read from IP0_OFFSET"); + check_true(is_empty(rx_queue), "rx queue not empty"); + -- check one of the default 1 registers + read_pca9506_reg(net, IOC0_OFFSET, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), 255, "Non-reset value read from IOC0_OFFSET"); + check_true(is_empty(rx_queue), "rx queue not empty"); + elsif run("check-write") then + -- write something else to a register that defaults to 1's + push_byte(tx_queue, to_integer(std_logic_vector'(8x"aa"))); + write_pca9506_reg(net, IOC0_OFFSET, tx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + -- read it back and check + read_pca9506_reg(net, IOC0_OFFSET, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), 16#AA#, "Non-matching register at IOC0_OFFSET"); + check_true(is_empty(rx_queue), "rx queue not empty"); + elsif run("check-auto-inc-no-sub-cat-no-wrap") then + -- write something else to a register that defaults to 1's + -- category size is 5 register, we'll write all 3 so we don't write + -- all and don't wrap + -- also put bytes in an expected queue so we can check them later + user_int := 16#a#; + for i in 0 to 2 loop + push_byte(tx_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + push_byte(exp_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + end loop; + write_pca9506_reg(net, IOC0_OFFSET, tx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + + -- we'll read them out 1-by-one so we're not confounding any + -- auto-inc or wrap issues + for i in 0 to 2 loop + -- read it back and check + read_pca9506_reg(net, IOC0_OFFSET + i, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), pop_byte(exp_queue), "Non-matching register at IOC" & to_string(i) & "_OFFSET"); + end loop; + elsif run("check-auto-inc-no-full-cat-no-wrap") then + -- verify write category with auto increment, write-size < full category + -- write something else to a register that defaults to 1's + -- category size is 5 register, we'll write all 5 so we don't write + -- all and don't wrap + -- also put bytes in an expected queue so we can check them later + user_int := 16#a#; + for i in 0 to 4 loop + push_byte(tx_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + push_byte(exp_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + end loop; + write_pca9506_reg(net, IOC0_OFFSET, tx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + + -- we'll read them out 1-by-one so we're not confounding any + -- auto-inc or wrap issues + for i in 0 to 4 loop + -- read it back and check + read_pca9506_reg(net, IOC0_OFFSET + i, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), pop_byte(exp_queue), "Non-matching register at IOC" & to_string(i) & "_OFFSET"); + end loop; + + elsif run("check-auto-inc-cat-wrap") then + -- verify write category with auto increment, write-size > full category + -- write something else to a register that defaults to 1's + -- category size is 5 register, we'll write all 6 so we don't write + -- all and don't wrap + -- also put bytes in an expected queue so we can check them later + user_int := 16#a#; + push_byte(tx_queue, to_integer(To_StdLogicVector(user_int, 4) & To_StdLogicVector(user_int, 4))); + -- we expect this value to be wrapped and over-written so we're going to not push it into the expected queue + user_int := user_int + 1; + -- loop through (and wrap) the final 5 bytes + for i in 0 to 4 loop + push_byte(tx_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + push_byte(exp_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + end loop; + write_pca9506_reg(net, IOC0_OFFSET, tx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + + -- we'll read them out 1-by-one so we're not confounding any + -- auto-inc or wrap issues. we also expect a wrap here so the last expected value + -- should be at the first register due to the wrap. We'll read that at the end out + -- of the loop + for i in 1 to 4 loop + -- read it back and check + read_pca9506_reg(net, IOC0_OFFSET + i, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), pop_byte(exp_queue), "Non-matching register at IOC" & to_string(i) & "_OFFSET"); + end loop; + read_pca9506_reg(net, IOC0_OFFSET, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), pop_byte(exp_queue), "Non-matching register at IOC0_OFFSET"); + + elsif run("check-no-inc-cat-writes") then + -- verify write category without auto increment, write-size > full category + user_int := 16#a#; + -- Put 3 non-wrapping bytes in, expect only the last one at the og register + for i in 0 to 2 loop + push_byte(tx_queue, to_integer(To_StdLogicVector(user_int+i, 4) & To_StdLogicVector(user_int+i, 4))); + end loop; + push_byte(exp_queue, to_integer(To_StdLogicVector(user_int+2, 4) & To_StdLogicVector(user_int+2, 4))); + push_byte(exp_queue, 16#FF#); -- expect reset vals for the remaining registers + push_byte(exp_queue, 16#FF#); -- expect reset vals for the remaining registers + push_byte(exp_queue, 16#FF#); -- expect reset vals for the remaining registers + push_byte(exp_queue, 16#FF#); -- expect reset vals for the remaining registers + + write_pca9506_reg(net, IOC0_OFFSET, tx_queue, ack_queue, auto_inc => false); + flush(ack_queue); -- don't want lingering acks from this transaction + + for i in 0 to 4 loop + -- read it back and check + read_pca9506_reg(net, IOC0_OFFSET + i, 1, rx_queue, ack_queue); + check_equal(pop_byte(rx_queue), pop_byte(exp_queue), "Non-matching register at IOC" & to_string(i) & "_OFFSET"); + end loop; + elsif run("check-basic-irq") then + -- verify basic irq functionality + check_equal('1', int_n, "irq is asserted incorrectly at reset"); + -- everything is an input and driven to '0' by the th initialization, but all irqs should be masked, + -- toggle an input and we should still see no irq fire. + io(0)(0) <= '1'; + wait for 200 ns; -- allow synchronization etc + check_equal('1', int_n, "irq is asserted incorrectly after i/o toggle while masked"); + io(0)(0) <= '0'; -- set back to default + wait for 200 ns; -- allow synchronization etc + + -- de-mask this irq + single_write_pca9506_reg(net, MSK0_OFFSET, X"FE", ack_status); + wait for 200 ns; -- make sure things propagate. + check_equal('1', int_n, "irq is asserted incorrectly after de-masking with no toggle"); + io(0)(0) <= '1'; -- toggle pin + wait for 200 ns; -- allow synchronization etc + check_equal('0', int_n, "irq did not assert after de-masking and i/o toggle"); + + -- return the I/O to its previous state, irq should go away + io(0)(0) <= '0'; -- toggle pin + wait for 200 ns; -- allow synchronization etc + check_equal('1', int_n, "irq still asserted after returning I/O to previous state"); + elsif run("irq-clears-on-read") then + -- de-mask this irq + single_write_pca9506_reg(net, MSK0_OFFSET, X"FE", ack_status); + wait for 200 ns; -- make sure things propagate. + check_equal('1', int_n, "irq is asserted incorrectly after de-masking with no toggle"); + io(0)(0) <= '1'; -- toggle pin + wait for 200 ns; -- allow synchronization etc + check_equal('0', int_n, "irq did not assert after de-masking and i/o toggle"); + + -- now read the register, this should clear the irq + read_pca9506_reg(net, IP0_OFFSET, 1, rx_queue, ack_queue); + check_equal('1', int_n, "irq still asserted (incorrectly) after reading IP0_OFFSET"); + + -- now toggle i/o again this should generate a new irq + io(0)(0) <= '0'; -- toggle pin low again + wait for 200 ns; -- allow synchronization etc + check_equal('0', int_n, "irq did not assert after i/o toggle"); + + elsif run("multi-register-irq") then + -- verify multi-register irq functionality + -- de-mask i/o (0,0) + single_write_pca9506_reg(net, MSK0_OFFSET, X"FE", ack_status); + -- de-mask i/o (4,0) + single_write_pca9506_reg(net, MSK4_OFFSET, X"FE", ack_status); + check_equal('1', int_n, "irq is asserted incorrectly after de-masking with no toggle"); + io(0)(0) <= '1'; -- toggle pin + io(4)(0) <= '1'; -- toggle pin + wait for 200 ns; -- allow synchronization etc + check_equal('0', int_n, "irq did not assert after de-masking and i/o toggle"); + + -- read one of the registers, not the other, irq should still be asserted + read_pca9506_reg(net, IP0_OFFSET, 1, rx_queue, ack_queue); + check_equal('0', int_n, "irq incorrectly de-asserted after reading only IP0_OFFSET"); + + -- now read other register, irq should clear + read_pca9506_reg(net, IP4_OFFSET, 1, rx_queue, ack_queue); + check_equal('1', int_n, "irq still asserted (incorrectly) after reading IP4_OFFSET"); + -- verify all inputs are read + elsif run("read_inputs") then + -- verify all inputs are read + -- going to do this bit-wise per port so we make sure everything is matching correctly + for i in 0 to 4 loop -- Loop each port + for j in 0 to 7 loop -- Loop each bit + -- everything is an input so we should not see any io_oe bits set + check_equal(io_oe(i)(j), '0', "io_oe bit " & to_string(j) & " on port " & to_string(i) & " is set for an input"); + -- Set the bit under test to '1' + io(i)(j) <= '1'; + wait for 200 ns; -- allow synchronization etc + -- Read the corresponding input register and check the value + read_pca9506_reg(net, IP0_OFFSET + i, 1, rx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + user_int := to_integer(shift_left(std_logic_vector'("00000001"), j)); + check_equal(pop_byte(rx_queue), user_int, "Input bit " & to_string(j) & " on port " & to_string(i) & " not reading correctly"); + -- Reset the input to its default state + io(i)(j) <= '0'; + wait for 200 ns; -- allow synchronization etc + end loop; + end loop; + elsif run("set_outputs") then + -- verify all outputs can be set to both 1 and 0 + -- turn all the ports to outputs + for i in 0 to 4 loop + -- set port under test to all outputs + single_write_pca9506_reg(net, IOC0_OFFSET + i, X"00", ack_status); + + for j in 0 to 7 loop + check_equal(io_oe(i)(j), '1', "Output enable bit " & to_string(j) & " on port " & to_string(i) & " not set correctly for output pin"); + -- Set the bit under test to '1' + user_int := to_integer(shift_left(std_logic_vector'("00000001"), j)); + single_write_pca9506_reg(net, OP0_OFFSET + i, to_std_logic_vector(user_int, 8), ack_status); + wait for 200 ns; -- allow synchronization etc + -- check the output port + check_equal(io_o(i)(j), '1', "Output bit " & to_string(j) & " on port " & to_string(i) & " not setting correctly"); + -- set back to 0 + single_write_pca9506_reg(net, OP0_OFFSET + i, to_std_logic_vector(0, 8), ack_status); + wait for 200 ns; -- allow synchronization etc + -- check the output port + check_equal(io_o(i)(j), '0', "Output bit " & to_string(j) & " on port " & to_string(i) & " not clearing correctly"); + end loop; + + end loop; -- + + elsif run("read_inputs_inverted") then + -- verify all inputs are read + -- going to do this bit-wise per port so we make sure everything is matching correctly + for i in 0 to 4 loop -- Loop each port + -- invert all inputs + single_write_pca9506_reg(net, PI0_OFFSET + i, X"FF", ack_status); + + for j in 0 to 7 loop -- Loop each bit + -- everything is an input so we should not see any io_oe bits set + check_equal(io_oe(i)(j), '0', "io_oe bit " & to_string(j) & " on port " & to_string(i) & " is set for an input"); + -- Set the bit under test to '1' + io(i)(j) <= '1'; + wait for 200 ns; -- allow synchronization etc + -- Read the corresponding input register and check the value + read_pca9506_reg(net, IP0_OFFSET + i, 1, rx_queue, ack_queue); + flush(ack_queue); -- don't want lingering acks from this transaction + user_slv8 := (others => '1'); + user_slv8(j) := '0'; + user_int := to_integer(user_slv8); + check_equal(pop_byte(rx_queue), user_int, "Input bit " & to_string(j) & " on port " & to_string(i) & " not reading correctly"); + -- Reset the input to its default state + io(i)(j) <= '0'; + wait for 200 ns; -- allow synchronization etc + end loop; + end loop; + end if; + + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + -- Example total test timeout dog + test_runner_watchdog(runner, 10 ms); + +end tb; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_th.vhd b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_th.vhd new file mode 100644 index 00000000..c232af1e --- /dev/null +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/sims/i2c_pca9506ish_th.vhd @@ -0,0 +1,80 @@ +-- 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 2025 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_ctrl_vc_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; + +use work.pca9506_pkg.all; + +entity i2c_pca9506ish_th is +end entity; + +architecture th of i2c_pca9506ish_th is + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + + signal i2c_bus_scl : std_logic := 'Z'; + signal i2c_bus_sda : std_logic := 'Z'; + signal tgt_scl_o : std_logic; + signal tgt_scl_oe : std_logic; + signal tgt_sda_o : std_logic; + signal tgt_sda_oe : std_logic; + + signal io : pca9506_pin_t := (others => (others => '0')); + signal io_oe : pca9506_pin_t; + signal io_o : pca9506_pin_t; + signal int_n : std_logic; + +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; + + i2c_controller_vc_inst: entity work.i2c_controller_vc + generic map( + i2c_ctrl_vc => i2c_ctrl_vc + ) + port map( + scl => i2c_bus_scl, + sda => i2c_bus_sda + ); + + i2c_bus_scl <= tgt_scl_o when tgt_scl_oe = '1' else 'H'; + i2c_bus_sda <= tgt_sda_o when tgt_sda_oe = '1' else 'H'; + + pca9506_top_inst: entity work.pca9506_top + generic map( + i2c_addr => 7x"20", + giltch_filter_cycles => 3 + ) + port map( + clk => clk, + reset => reset, + scl => i2c_bus_scl, + scl_o => tgt_scl_o, + scl_oe => tgt_scl_oe, + sda => i2c_bus_sda, + sda_o => tgt_sda_o, + sda_oe => tgt_sda_oe, + io => io, + io_oe => io_oe, + io_o => io_o, + int_n => int_n + ); + +end th; \ No newline at end of file diff --git a/hdl/ip/vhd/i2c/target/i2c_target_phy.vhd b/hdl/ip/vhd/i2c/target/i2c_target_phy.vhd index c079d8da..e03615b4 100644 --- a/hdl/ip/vhd/i2c/target/i2c_target_phy.vhd +++ b/hdl/ip/vhd/i2c/target/i2c_target_phy.vhd @@ -39,6 +39,8 @@ entity i2c_target_phy is in_ack_phase: out std_logic; do_ack : in std_logic; txn_header : out i2c_header; + stop_condition : out std_logic; + start_condition : out std_logic; -- response interface resp_data : in std_logic_vector(7 downto 0); @@ -82,8 +84,6 @@ architecture rtl of i2c_target_phy is constant BYTE_DONE : integer := 8; - signal start_condition : std_logic; - signal stop_condition : std_logic; signal sda_redge : std_logic; signal sda_fedge : std_logic; signal scl_fedge : std_logic; @@ -178,7 +178,6 @@ begin -- the registers are updated if v.txn_hdr.valid = '1' and v.txn_hdr.read_write_n = '1' then v.post_ack_nxt_state := TX_DATA; - v.tx_ready := '1'; else v.post_ack_nxt_state := RX_DATA; end if; @@ -191,14 +190,12 @@ begin v.tx_reg := resp_data; v.tx_ready := '0'; end if; - -- this biffs a shift after an ack if scl_fedge then v.tx_reg := shift_left(r.tx_reg, 1); v.cntr := r.cntr + 1; end if; -- deal with stop or ack/nack if r.cntr = BYTE_DONE then - v.tx_ready := '1'; v.tx_reg := shift_left(r.tx_reg, 1); -- ack if upstream allows, else nack v.state := GET_ACKNACK; @@ -212,6 +209,9 @@ begin end if; if scl_fedge then v.state := r.post_ack_nxt_state; + if r.post_ack_nxt_state = TX_DATA then + v.tx_ready := '1'; + end if; end if; when GET_ACKNACK => diff --git a/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc.vhd b/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc.vhd index 14e5f938..a97f3383 100644 --- a/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc.vhd +++ b/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc.vhd @@ -45,6 +45,8 @@ architecture model of i2c_controller_vc is gen_start : boolean; gen_stop : boolean; end record; + signal i2c_tx_data : std_logic_vector(7 downto 0); -- for debug/tracing etc + signal i2c_rx_data : std_logic_vector(7 downto 0); -- for debug/tracing etc function decode(byte : integer range 0 to 255) return flags_t is variable byte_as_bits : std_logic_vector(7 downto 0); @@ -106,6 +108,10 @@ begin procedure gen_start is -- High to low transition of SDA while SCL is high begin + if scl = '0' then + sda <= 'Z'; + wait until rising_edge(aligner_int); + end if; state <= START; scl <= 'Z'; sda <= 'Z'; @@ -135,12 +141,13 @@ begin ) is begin state <= OUT_DATA; + i2c_tx_data <= payload; -- assume we're immediately after a start condition or -- after the sclk fedge of an ack/nack -- scl must be low coming in and going out -- msb first for i in 7 downto 0 loop - sda <= payload(i); -- clock is low put data out on sda + sda <= '0' when payload(i) = '0' else 'Z'; -- clock is low put data out on sda wait until rising_edge(aligner_int); scl <= 'Z'; wait until falling_edge(aligner_int); @@ -164,6 +171,7 @@ begin wait until falling_edge(aligner_int); scl <= '0'; end loop; + i2c_rx_data <= payload; end procedure; procedure send_ack is begin diff --git a/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc_pkg.vhd b/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc_pkg.vhd index c1f84ee8..5bfff30c 100644 --- a/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc_pkg.vhd +++ b/hdl/ip/vhd/vunit_components/i2c_controller/i2c_ctrlr_vc_pkg.vhd @@ -91,6 +91,21 @@ package i2c_ctrl_vc_pkg is constant user_actor : in actor_t := null_actor ); + -- send a mixed transaction to the i2c bus + -- a mixed transaction has some number of writes + -- followed by a restart with read + -- and the some number of reads + procedure i2c_mixed_txn ( + signal net : inout network_t; + constant target_addr : std_logic_vector(6 downto 0); + constant tx_data : queue_t; + constant bytes_to_read : integer; + constant rx_data : queue_t; + constant ack_queue : queue_t; + constant ack_last_read : boolean := false; + constant user_actor : in actor_t := null_actor + ); + end package; @@ -277,5 +292,93 @@ package body i2c_ctrl_vc_pkg is send(net, actor, request_msg); end procedure; + procedure i2c_mixed_txn ( + signal net : inout network_t; + constant target_addr : std_logic_vector(6 downto 0); + constant tx_data : queue_t; + constant bytes_to_read : integer; + constant rx_data : queue_t; + constant ack_queue : queue_t; + constant ack_last_read : boolean := false; + constant user_actor : in actor_t := null_actor + ) is + variable request_msg : msg_t := new_msg(i2c_send_start); + variable reply_msg : msg_t; + variable actor : actor_t; + variable tgt_addr_rw : std_logic_vector(7 downto 0); + begin + if user_actor = null_actor then + actor := find("i2c_ctrl_vc"); + else + actor := user_actor; + end if; + + -- send start (msg defined in variable initialization) + send(net, actor, request_msg); + -- send target address + request_msg := new_msg(i2c_send_byte); + -- set rw bit , and put in the target address + tgt_addr_rw := target_addr & WRITE_BIT; + push(request_msg, to_integer(tgt_addr_rw)); + send(net, actor, request_msg); + -- Get Ack/Nack from target + request_msg := new_msg(i2c_get_ack_nack); + send(net, actor, request_msg); + receive_reply(net, request_msg, reply_msg); + -- Put Ack/Nack into ack queue + push_boolean(ack_queue, pop_boolean(reply_msg)); + delete(reply_msg); + -- loop through wr sending and waiting for acks + while not is_empty(tx_data) loop + -- send data byte + request_msg := new_msg(i2c_send_byte); + push(request_msg, pop_byte(tx_data)); + send(net, actor, request_msg); + -- Get Ack/Nack from target, expecting ack + request_msg := new_msg(i2c_get_ack_nack); + send(net, actor, request_msg); + receive_reply(net, request_msg, reply_msg); + -- Put Ack/Nack into ack queue + push_boolean(ack_queue, pop_boolean(reply_msg)); + delete(reply_msg); + end loop; + -- send restart + request_msg := new_msg(i2c_send_start); + send(net, actor, request_msg); + -- send target address, now as a read + request_msg := new_msg(i2c_send_byte); + -- set rw bit to 1 (read), and put in the target address + tgt_addr_rw := target_addr & READ_BIT; + push(request_msg, to_integer(tgt_addr_rw)); + send(net, actor, request_msg); + -- Get Ack/Nack from target + request_msg := new_msg(i2c_get_ack_nack); + send(net, actor, request_msg); + receive_reply(net, request_msg, reply_msg); + -- Put Ack/Nack into ack queue + push_boolean(ack_queue, pop_boolean(reply_msg)); + delete(reply_msg); + + -- Now we read target bytes on these transactions the controller should ack + for i in 0 to bytes_to_read-1 loop + -- get data byte + request_msg := new_msg(i2c_get_byte); + send(net, actor, request_msg); + receive_reply(net, request_msg, reply_msg); + -- store data into rx queue + push_byte(rx_data, pop(reply_msg)); + -- Ack/Nack data + if i = bytes_to_read-1 and ack_last_read = false then + request_msg := new_msg(i2c_send_nack); + else + request_msg := new_msg(i2c_send_ack); + end if; + send(net, actor, request_msg); + end loop; + -- send stop + request_msg := new_msg(i2c_send_stop); + send(net, actor, request_msg); + end procedure; + end package body; \ No newline at end of file