Grokで回路生成➁

昨日はGrokでI2Cの回路を生成したわけだが、基本的な実装とはいえ、I2CにACK/NACK処理はないのは痛いなぁと思って、その続きを行ってみた。

質問内容はつぎのとおりです。

作成したsystemverilogにACK/NACKの処理を追加してください

Grokの回答はつぎのとおりです。

ここから

ACK/NACK処理を追加したバージョンのコードを示します。主にステートマシンとデータ転送部分を修正し、ACK/NACKの確認機能を追加しています。 systemverilog

module apb_i2c_master (
    // APB Interface
    input  logic        pclk,
    input  logic        presetn,
    input  logic [31:0] paddr,
    input  logic        psel,
    input  logic        penable,
    input  logic        pwrite,
    input  logic [31:0] pwdata,
    output logic [31:0] prdata,
    output logic        pready,

    // I2C Interface
    inout  wire         scl,
    inout  wire         sda
);

    // レジスタ定義
    localparam ADDR_CTRL    = 8'h00;
    localparam ADDR_STATUS  = 8'h04;
    localparam ADDR_DATA    = 8'h08;
    localparam ADDR_CONFIG  = 8'h0C;

    // 内部レジスタ
    logic [31:0] ctrl_reg;
    logic [31:0] status_reg;
    logic [31:0] data_reg;
    logic [31:0] config_reg;

    // I2C制御信号
    logic scl_out;
    logic sda_out;
    logic sda_oe;
    logic sda_in;
    logic i2c_busy;
    logic nack_detected;

    // APBバス制御
    logic [7:0] addr_byte;
    assign addr_byte = paddr[7:0];
    assign pready = 1'b1;

    // APB書き込み処理
    always_ff @(posedge pclk or negedge presetn) begin
        if (!presetn) begin
            ctrl_reg   <= 32'h0;
            data_reg   <= 32'h0;
            config_reg <= 32'h0;
        end else if (psel && penable && pwrite) begin
            case (addr_byte)
                ADDR_CTRL:   ctrl_reg   <= pwdata;
                ADDR_DATA:   data_reg   <= pwdata;
                ADDR_CONFIG: config_reg <= pwdata;
            endcase
        end
    end

    // APB読み出し処理
    always_comb begin
        case (addr_byte)
            ADDR_CTRL:   prdata = ctrl_reg;
            ADDR_STATUS: prdata = status_reg;
            ADDR_DATA:   prdata = data_reg;
            ADDR_CONFIG: prdata = config_reg;
            default:     prdata = 32'h0;
        endcase
    end

    // I2C制御ロジック
    typedef enum logic [3:0] {
        IDLE,
        START,
        DATA,
        ACK_WAIT,
        ACK_CHECK,
        STOP,
        WAIT
    } i2c_state_t;

    i2c_state_t state, next_state;
    logic [7:0] bit_counter;
    logic [7:0] tx_data;
    logic [31:0] clock_div;
    logic [31:0] clock_count;

    // クロック分周
    always_ff @(posedge pclk or negedge presetn) begin
        if (!presetn) begin
            clock_count <= 32'h0;
            scl_out <= 1'b1;
        end else begin
            if (clock_count >= config  begin
                clock_count <= 32'h0;
                scl_out <= ~scl_out;
            end else begin
                clock_count <= clock_count + 1;
            end
        end
    end

    // SDA入力のサンプリング
    always_ff @(posedge pclk) begin
        sda_in <= sda;
    end

    // I2Cステートマシン
    always_ff @(posedge pclk or negedge presetn) begin
        if (!presetn) begin
            state <= IDLE;
            sda_oe <= 1'b0;
            sda_out <= 1'b1;
            bit_counter <= 8'h0;
            nack_detected <= 1'b0;
        end else begin
            state <= next_state;

            case (state)
                IDLE: begin
                    if (ctrl_reg[0]) begin
                        sda_oe <= 1'b1;
                        sda_out <= 1'b1;
                        tx_data <= data_reg[7:0];
                    end
                end
                START: begin
                    sda_out <= 1'b0;
                end
                DATA: begin
                    if (scl_out && bit_counter < 8) begin
                        sda_out <= tx_data[7-bit_counter];
                        if (scl_out && !scl_out_prev) begin
                            bit_counter <= bit_counter + 1;
                        end
                    end
                end
                ACK_WAIT: begin
                    sda_oe <= 1'b0;  // SDAを入力モードに
                end
                ACK_CHECK: begin
                    if (scl_out && !scl_out_prev) begin
                        nack_detected <= sda_in;  // SDA=1ならNACK
                    end
                end
                STOP: begin
                    sda_out <= 1'b0;
                    sda_oe <= 1'b1;
                end
            endcase
        end
    end

    logic scl_out_prev;
    always_ff @(posedge pclk) begin
        scl_out_prev <= scl_out;
    end

    // 次状態ロジック
    always_comb begin
        next_state = state;
        i2c_busy = 1'b1;

        case (state)
            IDLE: begin
                i2c_busy = 1'b0;
                if (ctrl_reg[0]) next_state = START;
            end
            START: if (!scl_out) next_state = DATA;
            DATA: if (bit_counter == 8 && scl_out && !scl_out_prev) next_state = ACK_WAIT;
            ACK_WAIT: if (!scl_out) next_state = ACK_CHECK;
            ACK_CHECK: if (scl_out && !scl_out_prev) next_state = nack_detected ? IDLE : STOP;
            STOP: if (scl_out) next_state = IDLE;
        endcase
    end

    // I2Cピンの割り当て
    assign scl = scl_out ? 1'bz : 1'b0;
    assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz;

    // ステータス更新
    assign status_reg = {29'h0, nack_detected, i2c_busy, ctrl_reg[0]};

endmodule

主な変更点:

  1. ステートマシンの拡張:
  • ACK_WAIT:ACKビットを待つ状態
  • ACK_CHECK:ACK/NACKを確認する状態を追加
  1. ACK/NACK処理:
  • nack_detected信号を追加し、NACK検出を記録
  • SDA入力をサンプリング(sda_in)
  • ACKビットのタイミングでSDAの状態を確認
  1. データ転送の改良:
  • SCLの立ち上がりエッジ検出を追加(scl_out_prev使用)
  • より正確なタイミング制御
  1. ステータスレジスタの更新:
  • NACK状態をステータスレジスタに反映

動作の流れ:

  1. 8ビットのデータ送信後、ACK_WAIT状態に移行
  2. SDAを入力モードに切り替え(sda_oe = 0)
  3. SCLがHighのタイミングでSDAをチェック
  4. SDA=0ならACK、SDA=1ならNACKとして処理
  5. NACKの場合は即座にIDLEに戻り、ACKの場合はSTOPシーケンスへ

この実装では、スレーブからのACK/NACK応答を確認し、NACKが検出された場合は転送を中止してIDLE状態に戻ります。ステータスレジスタを通じてNACKの状態をホストに通知します。

ここまで

これも検証していませんがシーケンス的には良さそうです。

こういう風に、必要な機能を追加していくことも可能です。