昨日は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
主な変更点:
動作の流れ:
この実装では、スレーブからのACK/NACK応答を確認し、NACKが検出された場合は転送を中止してIDLE状態に戻ります。ステータスレジスタを通じてNACKの状態をホストに通知します。
これも検証していませんがシーケンス的には良さそうです。
こういう風に、必要な機能を追加していくことも可能です。