I2C가 SPI보다 어려운 이유는 딱 하나입니다. "선 하나(SDA)로 말도 하고 듣기도 해야 한다(Bi-directional)"는 점 때문입니다.
이것 때문에 Tri-state buffer(3상 버퍼) 개념이 들어갑니다.

SPI는 "너는 말해(MOSI), 나는 들을게(MISO)"가 확실했습니다. 하지만 I2C는 무전기처럼 SDA 선 하나를 같이 씁니다. 그래서 충돌 방지를 위해 독특한 규칙을 씁니다.
그래서 코드를 짤 때 sda = 1이 아니라 sda_t = 1 (입력 모드/손 떼기) 로 제어해야 합니다. 이 개념만 잡으면 됩니다.
I2C는 타이밍이 복잡해서, 4분할박자로 쪼개서 설계하는 게 정석입니다. (SCL 한 주기 동안: Low -> Low -> High -> High)
`timescale 1ns / 1ps
module i2c_master (
input logic clk,
input logic rst_n,
// [User Interface]
input logic start,
input logic [6:0] addr,
input logic rw,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic busy,
output logic done,
output logic ack_error,
// [Physical Interface]
input logic scl_i,
output logic scl_o,
output logic scl_t,
input logic sda_i,
output logic sda_o,
output logic sda_t
);
// 100kHz 생성용 (100MHz / 1000분주 -> 4분할하므로 250)
localparam int CLK_DIV = 250;
logic [8:0] clk_cnt;
logic [1:0] q_cnt;
logic tick;
typedef enum logic [3:0] {
IDLE, START, ADDR, ACK_ADDR, DATA_TX, ACK_TX,
DATA_RX, ACK_RX, NACK_RX, STOP
} state_t;
state_t state;
logic [2:0] bit_cnt;
logic [7:0] shift_reg;
// ====================================================
// 1. 타이밍 생성기 (수정됨)
// ====================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 0; // [수정] '0 대신 0 사용
q_cnt <= 0; // [수정] '0 대신 0 사용
tick <= 1'b0;
end else begin
// busy 상태거나 start 명령이 오면 무조건 카운터 돌림
if (busy || start) begin
if (clk_cnt == CLK_DIV - 1) begin
clk_cnt <= 0; // [수정]
q_cnt <= q_cnt + 1;
tick <= 1'b1;
end else begin
clk_cnt <= clk_cnt + 1;
tick <= 1'b0;
end
end else begin
// 정말 아무일도 없을 때만 리셋
clk_cnt <= 0; // [수정]
q_cnt <= 0; // [수정]
tick <= 1'b0;
end
end
end
// ====================================================
// 2. FSM (수정됨)
// ====================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
scl_t <= 1'b1; sda_t <= 1'b1;
scl_o <= 1'b0; sda_o <= 1'b0;
busy <= 1'b0; done <= 1'b0;
ack_error <= 1'b0;
bit_cnt <= 3'd7;
shift_reg <= 0; // [수정]
rdata <= 0; // [수정]
end else begin
// IDLE 탈출 로직 (즉시 반응)
if (state == IDLE) begin
scl_t <= 1'b1;
sda_t <= 1'b1;
done <= 1'b0;
if (start) begin
state <= START;
busy <= 1'b1;
ack_error <= 1'b0;
shift_reg <= {addr, rw};
end else begin
busy <= 1'b0;
end
end
// 나머지 상태는 타이밍(Tick)에 맞춰서 동작
else if (tick) begin
// SCL 생성 로직
case (q_cnt)
0: scl_t <= 1'b0;
1: scl_t <= 1'b0;
2: scl_t <= 1'b1;
3: scl_t <= 1'b1;
endcase
case (state)
// IDLE은 위에서 처리했으므로 생략
START: begin
// q_cnt 0: SCL=H, SDA=H
// q_cnt 1: SCL=H, SDA=L (Start Condition!)
if (q_cnt == 0) begin scl_t <= 1'b1; sda_t <= 1'b1; end
if (q_cnt == 1) begin scl_t <= 1'b1; sda_t <= 1'b0; end
if (q_cnt == 2) begin scl_t <= 1'b1; sda_t <= 1'b0; end
if (q_cnt == 3) begin scl_t <= 1'b0; sda_t <= 1'b0; state <= ADDR; bit_cnt <= 7; end
end
ADDR: begin
if (q_cnt == 0) begin
if (shift_reg[7]) sda_t <= 1'b1;
else sda_t <= 1'b0;
end
if (q_cnt == 3) begin
if (bit_cnt == 0) state <= ACK_ADDR;
else begin
bit_cnt <= bit_cnt - 1;
shift_reg <= {shift_reg[6:0], 1'b0};
end
end
end
ACK_ADDR: begin
if (q_cnt == 0) sda_t <= 1'b1; // Release
if (q_cnt == 2) begin
if (sda_i == 1'b1) ack_error <= 1'b1;
end
if (q_cnt == 3) begin
if (ack_error) state <= STOP;
else if (shift_reg[0]) begin // Read Mode
state <= DATA_RX;
bit_cnt <= 7;
end else begin // Write Mode
state <= DATA_TX;
bit_cnt <= 7;
shift_reg <= wdata;
end
end
end
DATA_TX: begin
if (q_cnt == 0) begin
if (shift_reg[7]) sda_t <= 1'b1;
else sda_t <= 1'b0;
end
if (q_cnt == 3) begin
if (bit_cnt == 0) state <= ACK_TX;
else begin
bit_cnt <= bit_cnt - 1;
shift_reg <= {shift_reg[6:0], 1'b0};
end
end
end
ACK_TX: begin
if (q_cnt == 0) sda_t <= 1'b1;
if (q_cnt == 2) begin
if (sda_i == 1'b1) ack_error <= 1'b1;
end
if (q_cnt == 3) state <= STOP;
end
STOP: begin
if (q_cnt == 0) begin scl_t <= 1'b0; sda_t <= 1'b0; end
if (q_cnt == 1) begin scl_t <= 1'b1; sda_t <= 1'b0; end
if (q_cnt == 2) begin scl_t <= 1'b1; sda_t <= 1'b1; done <= 1'b1; busy <= 1'b0; state <= IDLE; end
end
endcase
end
end
end
endmodule
코드 해설
1. CLK_DIV와 q_cnt:
I2C의 핵심인 "양방향 통신(Tri-state Buffer)"을 처리하는 AXI 껍데(i2c_ip.sv)
여기서 가장 중요한 부분은 IOBUF 라는 Xilinx 전용 부품을 사용하는 것입니다.
`timescale 1ns / 1ps
module i2c_ip #
(
parameter integer C_S_AXI_DATA_WIDTH = 32,
parameter integer C_S_AXI_ADDR_WIDTH = 5 // 레지스터가 좀 늘어서 5비트로 늘림 (0x00 ~ 0x1F)
)
(
// [1] 시스템 신호
input logic s_axi_aclk,
input logic s_axi_aresetn,
// [2] AXI4-Lite 인터페이스
input logic [C_S_AXI_ADDR_WIDTH-1 : 0] s_axi_awaddr,
input logic [2 : 0] s_axi_awprot,
input logic s_axi_awvalid,
output logic s_axi_awready,
input logic [C_S_AXI_DATA_WIDTH-1 : 0] s_axi_wdata,
input logic [(C_S_AXI_DATA_WIDTH/8)-1 : 0] s_axi_wstrb,
input logic s_axi_wvalid,
output logic s_axi_wready,
output logic [1 : 0] s_axi_bresp,
output logic s_axi_bvalid,
input logic s_axi_bready,
input logic [C_S_AXI_ADDR_WIDTH-1 : 0] s_axi_araddr,
input logic [2 : 0] s_axi_arprot,
input logic s_axi_arvalid,
output logic s_axi_arready,
output logic [C_S_AXI_DATA_WIDTH-1 : 0] s_axi_rdata,
output logic [1 : 0] s_axi_rresp,
output logic s_axi_rvalid,
input logic s_axi_rready,
// [3] 사용자 포트 (보드 밖으로 나갈 물리적 핀: inout 타입)
inout wire i2c_scl,
inout wire i2c_sda
);
// ====================================================
// 레지스터 정의 (Address Map)
// ====================================================
// 0x00: Control (RW) - Bit0: Start, Bit1: R/W(0:Write, 1:Read)
// 0x04: Status (RO) - Bit0: Busy, Bit1: Done, Bit2: Ack_Error
// 0x08: Address (RW) - Slave Address (7-bit)
// 0x0C: WData (RW) - 쓸 데이터 (8-bit)
// 0x10: RData (RO) - 읽은 데이터 (8-bit)
logic [31:0] reg_ctrl;
logic [31:0] reg_addr;
logic [31:0] reg_wdata;
// Status와 RData는 Read Only라 레지스터 변수 선언 불필요 (Wire로 직결)
// AXI 핸드쉐이크 신호
logic axi_awready, axi_wready, axi_arready, axi_rvalid, axi_bvalid;
logic [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr;
logic [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr;
logic [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata;
// I2C Master 연결용 내부 신호 (3가닥으로 분리된 신호)
logic scl_i, scl_o, scl_t;
logic sda_i, sda_o, sda_t;
// 제어 신호 연결
logic i2c_start, i2c_rw, i2c_busy, i2c_done, i2c_ack_err;
logic [7:0] i2c_rdata_out;
// AXI 출력 연결
assign s_axi_awready = axi_awready;
assign s_axi_wready = axi_wready;
assign s_axi_bvalid = axi_bvalid;
assign s_axi_bresp = 2'b00;
assign s_axi_arready = axi_arready;
assign s_axi_rvalid = axi_rvalid;
assign s_axi_rdata = axi_rdata;
assign s_axi_rresp = 2'b00;
// ====================================================
// [1] IOBUF 인스턴스화 (가장 중요한 부분!)
// ====================================================
// Vivado가 "아, 이거 3상 버퍼구나" 하고 알아듣고 하드웨어 핀 처리를 해줌
IOBUF iobuf_scl (
.O (scl_i), // Buffer Output -> FPGA 내부 입력
.IO (i2c_scl), // Bidirectional Pin (보드 밖)
.I (scl_o), // FPGA Output -> Buffer Input
.T (scl_t) // Tri-state Enable (1=Input/Float, 0=Output)
);
IOBUF iobuf_sda (
.O (sda_i),
.IO (i2c_sda),
.I (sda_o),
.T (sda_t)
);
// ====================================================
// [2] I2C Master 모듈 연결
// ====================================================
i2c_master u_i2c_master (
.clk (s_axi_aclk),
.rst_n (s_axi_aresetn),
// User Interface
.start (i2c_start),
.addr (reg_addr[6:0]),
.rw (reg_ctrl[1]), // Control 레지스터 1번 비트가 R/W
.wdata (reg_wdata[7:0]),
.rdata (i2c_rdata_out),
.busy (i2c_busy),
.done (i2c_done),
.ack_error (i2c_ack_err),
// Physical Interface (3-wire) -> IOBUF와 연결
.scl_i (scl_i),
.scl_o (scl_o),
.scl_t (scl_t),
.sda_i (sda_i),
.sda_o (sda_o),
.sda_t (sda_t)
);
// 레지스터 비트 매핑
assign i2c_start = reg_ctrl[0]; // Control 레지스터 0번 비트가 Start
// ====================================================
// [3] AXI Write Logic
// ====================================================
always_ff @(posedge s_axi_aclk) begin
if (!s_axi_aresetn) begin
axi_awready <= 1'b0;
axi_wready <= 1'b0;
axi_bvalid <= 1'b0;
reg_ctrl <= 32'b0;
reg_addr <= 32'b0;
reg_wdata <= 32'b0;
end else begin
// Address Handshake
if (~axi_awready && s_axi_awvalid && s_axi_wvalid) begin
axi_awready <= 1'b1;
axi_wready <= 1'b1;
axi_awaddr <= s_axi_awaddr;
end else begin
axi_awready <= 1'b0;
axi_wready <= 1'b0;
end
// Data Latching
if (axi_awready && axi_wready) begin
case (axi_awaddr[4:2]) // 0x00, 0x04... 4바이트 단위
3'b000: reg_ctrl <= s_axi_wdata; // 0x00
// 0x04 Status는 Read Only
3'b010: reg_addr <= s_axi_wdata; // 0x08
3'b011: reg_wdata <= s_axi_wdata; // 0x0C
default: ;
endcase
end
// Response
if (axi_awready && axi_wready) begin
axi_bvalid <= 1'b1;
end else if (s_axi_bready && axi_bvalid) begin
axi_bvalid <= 1'b0;
end
end
end
// ====================================================
// [4] AXI Read Logic
// ====================================================
always_ff @(posedge s_axi_aclk) begin
if (!s_axi_aresetn) begin
axi_arready <= 1'b0;
axi_rvalid <= 1'b0;
axi_rdata <= 32'b0;
end else begin
if (~axi_arready && s_axi_arvalid) begin
axi_arready <= 1'b1;
axi_araddr <= s_axi_araddr;
end else begin
axi_arready <= 1'b0;
end
if (axi_arready && s_axi_arvalid && ~axi_rvalid) begin
axi_rvalid <= 1'b1;
case (axi_araddr[4:2])
3'b000: axi_rdata <= reg_ctrl;
3'b001: axi_rdata <= {29'b0, i2c_ack_err, i2c_done, i2c_busy}; // 0x04 Status
3'b010: axi_rdata <= reg_addr;
3'b011: axi_rdata <= reg_wdata;
3'b100: axi_rdata <= {24'b0, i2c_rdata_out}; // 0x10 Read Data
default: axi_rdata <= 32'b0;
endcase
end else if (axi_rvalid && s_axi_rready) begin
axi_rvalid <= 1'b0;
end
end
end
endmodule
포인트 (면접 질문 대비)
1.IOBUF가 뭔가요?