description: Single-byte SPI master controller. On start, latch data_in, assert busy, idle spi_clk at cpol, shift out MSB-first on mosi, and shift in miso into data_out. When cpha=0, sample on the first active edge; when cpha=1, sample on the second edge. After 8 bits, deassert busy and return to IDLE with spi_clk back at cpol. If start is asserted while a transfer is already in progress, enter a recoverable ERROR state and then return to IDLE.

module_code:
`timescale 1ns/1ps

module spi_controller (
    input clk,
    input rst_n,
    input [7:0] data_in,
    input start,
    input cpol,
    input cpha,
    output reg [7:0] data_out,
    output reg busy,
    output reg spi_clk,
    output reg mosi,
    input miso
);
    localparam IDLE  = 3'd0;
    localparam LOAD  = 3'd1;
    localparam LEAD  = 3'd2;
    localparam SAMPLE = 3'd3;
    localparam SHIFT = 3'd4;
    localparam DONE  = 3'd5;
    localparam ERROR = 3'd6;

    reg [2:0] state;
    reg [2:0] bit_count;
    reg [7:0] tx_shift;
    reg [7:0] rx_shift;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            bit_count <= 3'd0;
            tx_shift <= 8'd0;
            rx_shift <= 8'd0;
            data_out <= 8'd0;
            busy <= 1'b0;
            spi_clk <= 1'b0;
            mosi <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    busy <= 1'b0;
                    spi_clk <= cpol;
                    mosi <= 1'b0;
                    if (start) begin
                        busy <= 1'b1;
                        bit_count <= 3'd7;
                        tx_shift <= data_in;
                        rx_shift <= 8'd0;
                        state <= LOAD;
                    end
                end

                LOAD: begin
                    busy <= 1'b1;
                    spi_clk <= cpol;
                    mosi <= tx_shift[7];
                    if (start) begin
                        state <= ERROR;
                    end else begin
                        state <= LEAD;
                    end
                end

                LEAD: begin
                    busy <= 1'b1;
                    spi_clk <= ~cpol;
                    if (start) begin
                        state <= ERROR;
                    end else if (!cpha) begin
                        rx_shift <= {rx_shift[6:0], miso};
                        if (bit_count == 3'd0) begin
                            data_out <= {rx_shift[6:0], miso};
                            state <= DONE;
                        end else begin
                            state <= SHIFT;
                        end
                    end else begin
                        state <= SAMPLE;
                    end
                end

                SAMPLE: begin
                    busy <= 1'b1;
                    spi_clk <= ~cpol;
                    if (start) begin
                        state <= ERROR;
                    end else begin
                        rx_shift <= {rx_shift[6:0], miso};
                        if (bit_count == 3'd0) begin
                            data_out <= {rx_shift[6:0], miso};
                            state <= DONE;
                        end else begin
                            state <= SHIFT;
                        end
                    end
                end

                SHIFT: begin
                    busy <= 1'b1;
                    spi_clk <= cpol;
                    tx_shift <= {tx_shift[6:0], 1'b0};
                    bit_count <= bit_count - 1'b1;
                    mosi <= tx_shift[6];
                    if (start) begin
                        state <= ERROR;
                    end else begin
                        state <= LEAD;
                    end
                end

                DONE: begin
                    busy <= 1'b0;
                    spi_clk <= cpol;
                    mosi <= 1'b0;
                    if (start) begin
                        state <= ERROR;
                    end else begin
                        state <= IDLE;
                    end
                end

                default: begin
                    busy <= 1'b0;
                    spi_clk <= cpol;
                    mosi <= 1'b0;
                    if (!start) begin
                        state <= IDLE;
                    end
                end
            endcase
        end
    end

endmodule

testbench:
`timescale 1ns/1ps

module tb_spi_controller;
    reg clk;
    reg rst_n;
    reg start;
    reg cpol;
    reg cpha;
    reg [7:0] data_in;
    reg [7:0] current_rx;
    reg miso;

    wire [7:0] data_out;
    wire busy;
    wire spi_clk;
    wire mosi;

    integer errors;
    integer checks;
    reg saw_error;

    localparam IDLE   = 3'd0;
    localparam LEAD   = 3'd2;
    localparam SAMPLE = 3'd3;
    localparam ERROR  = 3'd6;

    spi_controller dut (
        .clk(clk),
        .rst_n(rst_n),
        .data_in(data_in),
        .start(start),
        .cpol(cpol),
        .cpha(cpha),
        .data_out(data_out),
        .busy(busy),
        .spi_clk(spi_clk),
        .mosi(mosi),
        .miso(miso)
    );

    always #5 clk = ~clk;

    always @(*) begin
        miso = 1'b1;
        if (dut.state == LEAD || dut.state == SAMPLE) begin
            miso = current_rx[dut.bit_count];
        end
    end

    always @(posedge clk) begin
        if (dut.state == ERROR) begin
            saw_error <= 1'b1;
        end
    end

    task check;
        input cond;
        input string message;
        begin
            checks = checks + 1;
            if (!cond) begin
                errors = errors + 1;
                $display("CHECK FAILED: %0s", message);
            end
        end
    endtask

    task reset_dut;
        begin
            rst_n = 1'b0;
            start = 1'b0;
            cpol = 1'b0;
            cpha = 1'b0;
            data_in = 8'h00;
            current_rx = 8'h00;
            repeat (3) @(posedge clk);
            rst_n = 1'b1;
            @(posedge clk);
        end
    endtask

    task launch_transfer;
        input [7:0] tx_byte;
        input [7:0] rx_byte;
        input mode_cpol;
        input mode_cpha;
        begin
            current_rx = rx_byte;
            data_in = tx_byte;
            cpol = mode_cpol;
            cpha = mode_cpha;
            start = 1'b1;
            @(posedge clk);
            start = 1'b0;
        end
    endtask

    task wait_for_idle;
        integer timeout;
        begin
            timeout = 0;
            while ((busy || dut.state != IDLE) && timeout < 200) begin
                @(posedge clk);
                timeout = timeout + 1;
            end
            check(timeout < 200, "SPI transfer timed out");
        end
    endtask

    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        start = 1'b0;
        cpol = 1'b0;
        cpha = 1'b0;
        data_in = 8'h00;
        current_rx = 8'h00;
        errors = 0;
        checks = 0;
        saw_error = 1'b0;

        reset_dut();

        launch_transfer(8'hA5, 8'h3C, 1'b0, 1'b0);
        wait_for_idle();
        check(data_out == 8'h3C, "CPOL=0 CPHA=0 readback mismatch");
        check(spi_clk == 1'b0, "spi_clk should idle low when cpol=0");
        check(busy == 1'b0, "busy should deassert after transfer");

        launch_transfer(8'h5A, 8'hC3, 1'b1, 1'b1);
        wait_for_idle();
        check(data_out == 8'hC3, "CPOL=1 CPHA=1 readback mismatch");
        check(spi_clk == 1'b1, "spi_clk should idle high when cpol=1");

        saw_error = 1'b0;
        launch_transfer(8'h96, 8'hF0, 1'b0, 1'b1);
        @(posedge clk);
        start = 1'b1;
        @(posedge clk);
        start = 1'b0;
        wait_for_idle();
        check(saw_error, "start during busy should reach ERROR recovery");
        check(busy == 1'b0, "busy should clear after error recovery");

        if (errors == 0) begin
            $display("Hint: Total mismatched samples is 0 out of %0d samples", checks);
        end else begin
            $display("Hint: Total mismatched samples is %0d out of %0d samples", errors, checks);
        end
        $display("Mismatches: %0d in %0d samples", errors, checks);
        $finish;
    end

endmodule


description: Single-byte I2C master controller with an explicit FSM and open-drain scl/sda behavior. On start_tx, if the bus is idle high, generate START, send {addr,rw}, check ACK1, then either write one byte from data_in or read one byte from sda into data_out. For write mode, sample ACK2 after the data byte. During STOP, if scl is externally held low, stay in CLOCK_STRETCH until the line is released. If the controller releases sda for a logic-1 bit but observes sda low during address transmission, treat this as arbitration loss and set ack_err. Any NACK or illegal START condition should also set ack_err before returning to IDLE.

module_code:
`timescale 1ns/1ps

module i2c_master (
    input clk,
    input rst_n,
    input [6:0] addr,
    input rw,
    input [7:0] data_in,
    input start_tx,
    output reg [7:0] data_out,
    output reg busy,
    output reg ack_err,
    inout scl,
    inout sda
);
    localparam IDLE             = 4'd0;
    localparam START_A          = 4'd1;
    localparam START_B          = 4'd2;
    localparam ADDR_DRIVE       = 4'd3;
    localparam ADDR_SAMPLE      = 4'd4;
    localparam ACK1             = 4'd5;
    localparam WRITE_DRIVE      = 4'd6;
    localparam WRITE_SAMPLE     = 4'd7;
    localparam READ             = 4'd8;
    localparam ACK2             = 4'd9;
    localparam STOP_A           = 4'd10;
    localparam STOP_B           = 4'd11;
    localparam CLOCK_STRETCH    = 4'd12;
    localparam ARBITRATION_LOST = 4'd13;
    localparam ERROR            = 4'd14;

    reg [3:0] state;
    reg [2:0] bit_count;
    reg [7:0] tx_shift;
    reg [7:0] rx_shift;
    reg scl_drive_low;
    reg sda_drive_low;
    reg latched_rw;

    wire scl_in;
    wire sda_in;

    assign scl = scl_drive_low ? 1'b0 : 1'bz;
    assign sda = sda_drive_low ? 1'b0 : 1'bz;
    assign scl_in = scl;
    assign sda_in = sda;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            bit_count <= 3'd0;
            tx_shift <= 8'd0;
            rx_shift <= 8'd0;
            scl_drive_low <= 1'b0;
            sda_drive_low <= 1'b0;
            latched_rw <= 1'b0;
            data_out <= 8'd0;
            busy <= 1'b0;
            ack_err <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    busy <= 1'b0;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    bit_count <= 3'd7;
                    rx_shift <= 8'd0;
                    if (start_tx) begin
                        busy <= 1'b1;
                        ack_err <= 1'b0;
                        tx_shift <= {addr, rw};
                        latched_rw <= rw;
                        state <= START_A;
                    end
                end

                START_A: begin
                    busy <= 1'b1;
                    if (scl_in && sda_in) begin
                        sda_drive_low <= 1'b1;
                        scl_drive_low <= 1'b0;
                        state <= START_B;
                    end else begin
                        ack_err <= 1'b1;
                        state <= ERROR;
                    end
                end

                START_B: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b1;
                    bit_count <= 3'd7;
                    sda_drive_low <= ~tx_shift[7];
                    state <= ADDR_DRIVE;
                end

                ADDR_DRIVE: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= ~tx_shift[7];
                    state <= ADDR_SAMPLE;
                end

                ADDR_SAMPLE: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    if (tx_shift[7] && !sda_in) begin
                        ack_err <= 1'b1;
                        state <= ARBITRATION_LOST;
                    end else if (bit_count == 3'd0) begin
                        sda_drive_low <= 1'b0;
                        state <= ACK1;
                    end else begin
                        tx_shift <= {tx_shift[6:0], 1'b0};
                        bit_count <= bit_count - 1'b1;
                        scl_drive_low <= 1'b1;
                        sda_drive_low <= ~tx_shift[6];
                        state <= ADDR_DRIVE;
                    end
                end

                ACK1: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    bit_count <= 3'd7;
                    if (!sda_in) begin
                        if (latched_rw) begin
                            rx_shift <= 8'd0;
                            state <= READ;
                        end else begin
                            tx_shift <= data_in;
                            sda_drive_low <= ~data_in[7];
                            state <= WRITE_DRIVE;
                        end
                    end else begin
                        ack_err <= 1'b1;
                        state <= ERROR;
                    end
                end

                WRITE_DRIVE: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= ~tx_shift[7];
                    state <= WRITE_SAMPLE;
                end

                WRITE_SAMPLE: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    if (tx_shift[7] && !sda_in) begin
                        ack_err <= 1'b1;
                        state <= ARBITRATION_LOST;
                    end else if (bit_count == 3'd0) begin
                        sda_drive_low <= 1'b0;
                        state <= ACK2;
                    end else begin
                        tx_shift <= {tx_shift[6:0], 1'b0};
                        bit_count <= bit_count - 1'b1;
                        scl_drive_low <= 1'b1;
                        sda_drive_low <= ~tx_shift[6];
                        state <= WRITE_DRIVE;
                    end
                end

                READ: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    rx_shift <= {rx_shift[6:0], sda_in};
                    if (bit_count == 3'd0) begin
                        data_out <= {rx_shift[6:0], sda_in};
                        state <= ACK2;
                    end else begin
                        bit_count <= bit_count - 1'b1;
                    end
                end

                ACK2: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    if (!latched_rw && sda_in) begin
                        ack_err <= 1'b1;
                    end
                    state <= STOP_A;
                end

                STOP_A: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b1;
                    if (!scl_in) begin
                        state <= CLOCK_STRETCH;
                    end else begin
                        state <= STOP_B;
                    end
                end

                CLOCK_STRETCH: begin
                    busy <= 1'b1;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b1;
                    if (scl_in) begin
                        state <= STOP_B;
                    end
                end

                STOP_B: begin
                    busy <= 1'b0;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    state <= IDLE;
                end

                ARBITRATION_LOST: begin
                    busy <= 1'b0;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    ack_err <= 1'b1;
                    state <= IDLE;
                end

                default: begin
                    busy <= 1'b0;
                    scl_drive_low <= 1'b0;
                    sda_drive_low <= 1'b0;
                    ack_err <= 1'b1;
                    state <= IDLE;
                end
            endcase
        end
    end

endmodule

testbench:
`timescale 1ns/1ps

module tb_i2c_master;
    reg clk;
    reg rst_n;
    reg [6:0] addr;
    reg rw;
    reg [7:0] data_in;
    reg start_tx;

    tri1 scl;
    tri1 sda;

    reg slave_scl_low;
    reg slave_sda_low;
    reg nack_first_ack;
    reg arbitration_request;
    reg [7:0] read_value;
    reg saw_arbitration_lost;

    wire [7:0] data_out;
    wire busy;
    wire ack_err;

    integer errors;
    integer checks;

    localparam IDLE             = 4'd0;
    localparam ADDR_DRIVE       = 4'd3;
    localparam ADDR_SAMPLE      = 4'd4;
    localparam ACK1             = 4'd5;
    localparam READ             = 4'd8;
    localparam ACK2             = 4'd9;
    localparam STOP_A           = 4'd10;
    localparam CLOCK_STRETCH    = 4'd12;
    localparam ARBITRATION_LOST = 4'd13;

    assign scl = slave_scl_low ? 1'b0 : 1'bz;
    assign sda = slave_sda_low ? 1'b0 : 1'bz;

    i2c_master dut (
        .clk(clk),
        .rst_n(rst_n),
        .addr(addr),
        .rw(rw),
        .data_in(data_in),
        .start_tx(start_tx),
        .data_out(data_out),
        .busy(busy),
        .ack_err(ack_err),
        .scl(scl),
        .sda(sda)
    );

    always #5 clk = ~clk;

    always @(*) begin
        slave_sda_low = 1'b0;

        if (dut.state == ACK1) begin
            if (!nack_first_ack) begin
                slave_sda_low = 1'b1;
            end
        end else if (dut.state == ACK2 && !dut.latched_rw) begin
            slave_sda_low = 1'b1;
        end else if (dut.state == READ) begin
            slave_sda_low = ~read_value[dut.bit_count];
        end

        if (arbitration_request &&
            (dut.state == ADDR_DRIVE || dut.state == ADDR_SAMPLE) &&
            dut.tx_shift[7]) begin
            slave_sda_low = 1'b1;
        end
    end

    always @(posedge clk) begin
        if (dut.state == ARBITRATION_LOST) begin
            saw_arbitration_lost <= 1'b1;
        end
    end

    task check;
        input cond;
        input string message;
        begin
            checks = checks + 1;
            if (!cond) begin
                errors = errors + 1;
                $display("CHECK FAILED: %0s", message);
            end
        end
    endtask

    task reset_dut;
        begin
            rst_n = 1'b0;
            start_tx = 1'b0;
            addr = 7'h00;
            rw = 1'b0;
            data_in = 8'h00;
            nack_first_ack = 1'b0;
            arbitration_request = 1'b0;
            slave_scl_low = 1'b0;
            read_value = 8'h00;
            repeat (3) @(posedge clk);
            rst_n = 1'b1;
            @(posedge clk);
        end
    endtask

    task launch_transfer;
        input [6:0] tx_addr;
        input tx_rw;
        input [7:0] tx_data;
        begin
            addr = tx_addr;
            rw = tx_rw;
            data_in = tx_data;
            start_tx = 1'b1;
            @(posedge clk);
            start_tx = 1'b0;
        end
    endtask

    task wait_for_idle;
        integer timeout;
        begin
            timeout = 0;
            while ((busy || dut.state != IDLE) && timeout < 200) begin
                @(posedge clk);
                timeout = timeout + 1;
            end
            check(timeout < 200, "I2C transfer timed out");
        end
    endtask

    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        addr = 7'h00;
        rw = 1'b0;
        data_in = 8'h00;
        start_tx = 1'b0;
        slave_scl_low = 1'b0;
        nack_first_ack = 1'b0;
        arbitration_request = 1'b0;
        read_value = 8'h00;
        saw_arbitration_lost = 1'b0;
        errors = 0;
        checks = 0;

        reset_dut();

        launch_transfer(7'h42, 1'b0, 8'hA5);
        wait_for_idle();
        check(ack_err == 1'b0, "write transaction should complete without ack_err");
        check(busy == 1'b0, "busy should deassert after write");
        check(scl === 1'b1 && sda === 1'b1, "bus should return high after STOP");

        read_value = 8'h3C;
        launch_transfer(7'h42, 1'b1, 8'h00);
        wait_for_idle();
        check(data_out == 8'h3C, "read transaction should capture slave data");
        check(ack_err == 1'b0, "read transaction should complete without ack_err");

        nack_first_ack = 1'b1;
        launch_transfer(7'h42, 1'b0, 8'h55);
        wait_for_idle();
        check(ack_err == 1'b1, "NACK on first ACK should set ack_err");
        nack_first_ack = 1'b0;

        fork
            begin
                wait (dut.state == STOP_A);
                slave_scl_low = 1'b1;
                repeat (3) @(posedge clk);
                slave_scl_low = 1'b0;
            end
        join_none
        launch_transfer(7'h42, 1'b0, 8'hC3);
        wait_for_idle();
        check(ack_err == 1'b0, "clock stretch should not force ack_err");

        saw_arbitration_lost = 1'b0;
        fork
            begin
                wait (dut.state == ADDR_SAMPLE && dut.tx_shift[7]);
                arbitration_request = 1'b1;
                repeat (2) @(posedge clk);
                arbitration_request = 1'b0;
            end
        join_none
        launch_transfer(7'h60, 1'b0, 8'hF0);
        wait_for_idle();
        check(saw_arbitration_lost, "arbitration loss path should be reachable");
        check(ack_err == 1'b1, "arbitration loss should set ack_err");

        if (errors == 0) begin
            $display("Hint: Total mismatched samples is 0 out of %0d samples", checks);
        end else begin
            $display("Hint: Total mismatched samples is %0d out of %0d samples", errors, checks);
        end
        $display("Mismatches: %0d in %0d samples", errors, checks);
        $finish;
    end

endmodule