diff --git a/rtl/i2c_mux.v b/rtl/i2c_mux.v new file mode 100644 index 0000000..9321264 --- /dev/null +++ b/rtl/i2c_mux.v @@ -0,0 +1,152 @@ +/* + +Copyright (c) 2023 Tobias Binkowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* A simple i2c mux implementing a compatible register access as the TCA9544A */ + +module i2c_mux #( + parameter FILTER_LEN = 4, + parameter RISE_LEN = 8, + parameter DEV_ADDR = 7'h70, + parameter PORTS = 4 +) +( + input wire clk, + input wire rst, + output wire [PORTS-1:0] selected_port, + + /* + * I2C slave interface + */ + input wire slave_scl_i, + output wire slave_scl_o, + output wire slave_scl_t, + input wire slave_sda_i, + output wire slave_sda_o, + output wire slave_sda_t, + + /* + * I2C master interfaces + */ + input wire [PORTS-1:0] master_scl_i, + output wire [PORTS-1:0] master_scl_o, + output wire [PORTS-1:0] master_scl_t, + input wire [PORTS-1:0] master_sda_i, + output wire [PORTS-1:0] master_sda_o, + output wire [PORTS-1:0] master_sda_t +); + +wire [7:0] mux_reg; +wire mux_scl_o; +wire mux_scl_t; +wire mux_sda_o; +wire mux_sda_t; + +i2c_single_reg #( + .FILTER_LEN(FILTER_LEN), + .DEV_ADDR(DEV_ADDR) +) i2c_single_reg_inst ( + .clk(clk), + .rst(rst), + .scl_i(slave_scl_i), + .scl_o(mux_scl_o), + .scl_t(mux_scl_t), + .sda_i(slave_sda_i), + .sda_o(mux_sda_o), + .sda_t(mux_sda_t), + .data_in(mux_reg), + .data_latch(1'b0), + .data_out(mux_reg) +); + +reg en; +reg [PORTS-1:0] port; + +reg [PORTS-1:0] master_scl_out; +reg [PORTS-1:0] master_sda_out; +reg master_scl_in; +reg master_sda_in; + +reg slave_scl_r; +reg slave_sda_r; +reg [RISE_LEN-1:0] master_scl_r; +reg [RISE_LEN-1:0] master_sda_r; + +assign slave_scl_o = slave_scl_r; +assign slave_sda_o = slave_sda_r; +assign master_scl_o = master_scl_out; +assign master_sda_o = master_sda_out; + +assign slave_scl_t = slave_scl_r; +assign slave_sda_t = slave_sda_r; +assign master_scl_t = master_scl_out; +assign master_sda_t = master_sda_out; + +assign selected_port = port; + +/* select master port if a port is selected */ +always @(*) begin + master_scl_out = {PORTS{1'b1}}; + master_sda_out = {PORTS{1'b1}}; + master_scl_in = 1'b1; + master_sda_in = 1'b1; + if (en) begin + master_scl_out[port] = master_scl_r[0]; + master_sda_out[port] = master_sda_r[0]; + master_scl_in = master_scl_i[port]; + master_sda_in = master_sda_i[port]; + end +end + +always @(posedge clk) begin + port <= 0; + en <= 1'b0; + if (mux_reg >= 4 && mux_reg < PORTS + 4) begin + port <= mux_reg - 4; + en <= 1'b1; + end +end + +/* need to do some filtering when going from driving low to high-z with real IO */ +wire master_scl_filter; +wire master_sda_filter; +assign master_scl_filter = (master_scl_r != {RISE_LEN{1'b1}}) ? 1'b1 : 1'b0; +assign master_sda_filter = (master_sda_r != {RISE_LEN{1'b1}}) ? 1'b1 : 1'b0; + +always @(posedge clk) begin + master_scl_r <= {master_scl_r[RISE_LEN-2:0], (slave_scl_i || !slave_scl_r)}; + master_sda_r <= {master_sda_r[RISE_LEN-2:0], (slave_sda_i || !slave_sda_r)}; + slave_scl_r <= (master_scl_in || master_scl_filter) & (mux_scl_t | mux_scl_o); + slave_sda_r <= (master_sda_in || master_sda_filter) & (mux_sda_t | mux_sda_o); +end + +endmodule + +`resetall + diff --git a/tb/i2c_master_model.v b/tb/i2c_master_model.v new file mode 100644 index 0000000..03b6464 --- /dev/null +++ b/tb/i2c_master_model.v @@ -0,0 +1,259 @@ +/* + +Copyright (c) 2023 Tobias Binkowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`timescale 1ns / 1ps + +module i2c_master_model #( + parameter I2C_FREQUENCY = 100000 +) ( + input wire sda, + output wire sda_o, + input wire scl, + output wire scl_o +); + +localparam I2C_PERIOD = 1000000000 / I2C_FREQUENCY; // 1ns / 100kHz +localparam I2C_HPERIOD = I2C_PERIOD / 2; +localparam I2C_QPERIOD = I2C_PERIOD / 4; + +reg sda_r, scl_r; + +assign sda_o = sda_r; +assign scl_o = scl_r; + +task write; + input [6:0] addr; + input [7:0] data; + integer I; + reg [17:0] shift_reg; +begin + $display("i2c_write @0x%0h data 0x%0h", addr, data); + // addr, write, ack, data, ack + shift_reg = { addr, 1'b0, 1'b1, data, 1'b1 }; + + // start + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + + for (I = 17; I >= 0 ; I = I-1) begin + sda_r = shift_reg[I]; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + // stop + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + sda_r = 1'b1; +end endtask + +task read; + input [6:0] addr; + output [7:0] data; + integer I; + reg [9:0] shift_reg; +begin + // addr, read, ack + shift_reg = { addr, 1'b1, 1'b1 }; + + // start + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + + for (I = 8; I >= 0 ; I = I-1) begin + sda_r = shift_reg[I]; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + sda_r = 1'b1; + + for (I = 7; I >= 0; I = I-1) begin + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + data[I] = (sda === 1'b0) ? 1'b0 : 1'b1; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + $display("i2c_read @0x%0h data 0x%0h", addr, data); + + // nack + sda_r = 1'b1; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + + // stop + scl_r = 1'b0; + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + sda_r = 1'b1; +end endtask + +task smbus_write; + input [6:0] addr; + input [7:0] cmd; + input [7:0] data; + integer I; + reg [26:0] shift_reg; +begin + $display("smbus_write @0x%0h cmd 0x%0h data 0x%0h", addr, cmd, data); + // addr, write, ack, cmd, ack, data, ack + shift_reg = { addr, 1'b0, 1'b1, cmd, 1'b1, data, 1'b1 }; + + // start + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + + for (I = 26; I >= 0 ; I = I-1) begin + sda_r = shift_reg[I]; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + // stop + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + sda_r = 1'b1; + +end endtask + +task smbus_read; + input [6:0] addr; + input [7:0] cmd; + output [7:0] data; + integer I; + reg [17:0] shift_reg; +begin + // addr, write, ack, cmd, ack + shift_reg = { addr, 1'b0, 1'b1, cmd, 1'b1 }; + + // start + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + + for (I = 17; I >= 0 ; I = I-1) begin + sda_r = shift_reg[I]; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + // repeated start + sda_r = 1'b1; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + + // addr, read, ack + shift_reg = { addr, 1'b1, 1'b1 }; + + for (I = 8; I >= 0 ; I = I-1) begin + sda_r = shift_reg[I]; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + sda_r = 1'b1; + + for (I = 7; I >= 0; I = I-1) begin + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + data[I] = (sda === 1'b0) ? 1'b0 : 1'b1; + #(I2C_QPERIOD); + scl_r = 1'b0; + #(I2C_QPERIOD); + end + + $display("smbus_read @0x%0h cmd 0x%0h data 0x%0h", addr, cmd, data); + + // nack + sda_r = 1'b1; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + #(I2C_QPERIOD); + + // stop + scl_r = 1'b0; + #(I2C_QPERIOD); + sda_r = 1'b0; + #(I2C_QPERIOD); + scl_r = 1'b1; + #(I2C_QPERIOD); + sda_r = 1'b1; +end endtask + + + +endmodule diff --git a/tb/tb_i2c_mux.v b/tb/tb_i2c_mux.v new file mode 100644 index 0000000..b799c9f --- /dev/null +++ b/tb/tb_i2c_mux.v @@ -0,0 +1,186 @@ +/* + +Copyright (c) 2023 Tobias Binkowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +/* to run this testbench using Icarus run: + * $ iverilog tb_i2c_mux.v i2c_master_model ../rtl/i2c_single_reg.v ../rtl/i2c_mux.v -o i2c_mux && ./i2c_mux + */ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +module tb(); + +initial begin + $dumpfile("i2c_mux.vcd"); + $dumpvars(0, tb); +end + +localparam I2C_PERIOD = 1000000000 / 100000; // 1ns / 100kHz +localparam I2C_HPERIOD = I2C_PERIOD / 2; +localparam I2C_QPERIOD = I2C_PERIOD / 4; + +reg clk = 0; +reg rst; +always #10 clk = ~clk; + +// upstream i2c bus wires +wire io_scl, io_sda; + +// downstream i2c bus wires +wire [3:0] s_scl, s_sda; + +// testbench access to upstream i2c +wire i2c_model_sda, i2c_model_scl; + +// iobuffer upstream +wire scl_i, scl_o, scl_t; +wire sda_i, sda_o, sda_t; +wire [3:0] s_scl_i, s_scl_o, s_scl_t; +wire [3:0] s_sda_i, s_sda_o, s_sda_t; + +assign scl_i = (io_scl === 1'b0) ? 1'b0 : 1'b1; +assign io_scl = scl_t ? 1'bz : scl_o; +assign io_scl = i2c_model_scl ? 1'bz : 1'b0; +assign sda_i = (io_sda === 1'b0) ? 1'b0 : 1'b1; +assign io_sda = sda_t ? 1'bz : sda_o; +assign io_sda = i2c_model_sda ? 1'bz : 1'b0; + +wire [3:0] selected_port; + +reg [7:0] temp; +wire [7:0] reg1_data; + +i2c_master_model #( + .I2C_FREQUENCY(100000) +) i2c_master ( + .sda(io_sda), .sda_o(i2c_model_sda), + .scl(io_scl), .scl_o(i2c_model_scl) +); + +initial begin + rst = 1'b1; + #1000; + rst = 1'b0; + #1000; + // check no port is selected after reset + if (selected_port != 4'b0000) + $error("selected_port is not zero after reset!"); + // select port index 0 + i2c_master.write(8'h70, 8'h04); + if (selected_port != 4'b0001) + $error("selected_port is not as expected"); + // readback mux register + i2c_master.read(8'h70, temp); + if (temp != 8'h04) + $error("readback from mux does not match"); + // read connected register connected to selected port + i2c_master.read(8'h42, temp); + if (temp != 0) + $error("read from address 0x42 at bus port 1 not matched expected value"); + // write to connected register + i2c_master.write(8'h42, 8'hde); + if (reg1_data != 8'hde) + $error("write to connected slave failed"); + // read connected register connected to selected port + i2c_master.read(8'h42, temp); + if (temp != 8'hde) + $error("read from address 0x42 at bus port 1 not matched expected value"); + i2c_master.write(8'h42, 8'h04); + // select port index 1 + i2c_master.write(8'h70, 8'h05); + if (selected_port != 4'b0010) + $error("selected_port is not as expected"); + // try to read from now not available address + i2c_master.read(8'h42, temp); + if (temp != 8'hff) + $error("read from non connected slave did not return 0xff"); + i2c_master.write(8'h70, 8'h06); + if (selected_port != 4'b0100) + $error("selected_port is not as expected"); + i2c_master.write(8'h70, 8'h07); + if (selected_port != 4'b1000) + $error("selected_port is not as expected"); + i2c_master.write(8'h70, 8'h00); + if (selected_port != 4'b0000) + $error("selected_port is not as expected"); + $finish; +end + +i2c_mux i2c_mux_inst ( + .clk(clk), + .rst(rst), + .slave_scl_i(scl_i), + .slave_scl_o(scl_o), + .slave_scl_t(scl_t), + .slave_sda_i(sda_i), + .slave_sda_o(sda_o), + .slave_sda_t(sda_t), + .master_scl_i(s_scl_i), + .master_scl_o(s_scl_o), + .master_scl_t(s_scl_t), + .master_sda_i(s_sda_i), + .master_sda_o(s_sda_o), + .master_sda_t(s_sda_t), + .selected_port(selected_port) +); + +// simple slave device on port 0 +wire reg1_scl_i, reg1_scl_o, reg1_scl_t; +wire reg1_sda_i, reg1_sda_o, reg1_sda_t; + +assign reg1_scl_i = s_scl_t[0] ? 1'b1 : s_scl_o[0]; +assign s_scl_i[0] = reg1_scl_t ? 1'b1 : reg1_scl_o; +assign reg1_sda_i = s_sda_t[0] ? 1'b1 : s_sda_o[0]; +assign s_sda_i[0] = reg1_sda_t ? 1'b1 : reg1_sda_o; + +assign s_scl_i[1] = 1'b1; +assign s_scl_i[2] = 1'b1; +assign s_scl_i[3] = 1'b1; +assign s_sda_i[1] = 1'b1; +assign s_sda_i[2] = 1'b1; +assign s_sda_i[3] = 1'b1; + +i2c_single_reg #( + .FILTER_LEN(2), + .DEV_ADDR(7'h42) +) i2c_single_reg_inst ( + .clk(clk), + .rst(rst), + .scl_i(reg1_scl_i), + .scl_o(reg1_scl_o), + .scl_t(reg1_scl_t), + .sda_i(reg1_sda_i), + .sda_o(reg1_sda_o), + .sda_t(reg1_sda_t), + .data_in(reg1_data), + .data_latch(1'b0), + .data_out(reg1_data) +); + +endmodule + +`resetall