I2C 개념 및 코드

Seungyun Lee·2026년 2월 1일

Comm_Accel(UART/SPI/I2C)

목록 보기
10/15

I2C가 SPI보다 어려운 이유는 딱 하나입니다. "선 하나(SDA)로 말도 하고 듣기도 해야 한다(Bi-directional)"는 점 때문입니다.

이것 때문에 Tri-state buffer(3상 버퍼) 개념이 들어갑니다.

1. I2C 공략 핵심 개념: "Open Drain"

SPI는 "너는 말해(MOSI), 나는 들을게(MISO)"가 확실했습니다. 하지만 I2C는 무전기처럼 SDA 선 하나를 같이 씁니다. 그래서 충돌 방지를 위해 독특한 규칙을 씁니다.

  1. 기본 상태: 아무도 안 건드리면 1 (High) 입니다. (저항이 1로 당겨줌, Pull-up)
  2. 0을 보낼 때: 선을 강제로 GND(0) 로 끌어내립니다.
  3. 1을 보낼 때: 내가 1을 쏘는 게 아니라, "손을 뗍니다(High-Z)." 그러면 저항이 알아서 1로 올려줍니다.

그래서 코드를 짤 때 sda = 1이 아니라 sda_t = 1 (입력 모드/손 떼기) 로 제어해야 합니다. 이 개념만 잡으면 됩니다.

2. I2C Master 설계 전략

I2C는 타이밍이 복잡해서, 4분할박자로 쪼개서 설계하는 게 정석입니다. (SCL 한 주기 동안: Low -> Low -> High -> High)

  • 기능: 특정 주소(Address)의 슬레이브에게 데이터 1바이트 쓰기/읽기
  • 속도: 100kHz (Standard Mode)

i2c_master.sv

`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는 타이밍이 생명입니다. 클럭을 4등분(q_cnt 0,1,2,3)해서 0번일 때 데이터 바꾸고, 2번일 때 읽는 식으로 타이밍 마진을 확보했습니다.
  1. sda_t (Tri-state):
  • ADDR 상태를 보세요. shift_reg[7]이 1이면 sda_t <= 1'b1 (High-Z)로 설정합니다. "1을 보낸다"가 아니라 "손을 놓는다"는 개념입니다.
  1. scl_t:
  • SCL도 마찬가지로 Open Drain으로 구동하는 게 원칙입니다. (Clock Stretching 때문인데, 여기선 일단 Master가 주도하므로 거의 Output처럼 씁니다.)

i2c_ip.sv

I2C의 핵심인 "양방향 통신(Tri-state Buffer)"을 처리하는 AXI 껍데(i2c_ip.sv)

여기서 가장 중요한 부분은 IOBUF 라는 Xilinx 전용 부품을 사용하는 것입니다.

  • 우리가 만든 i2c_master는 선이 3개(_i, _o, _t)지만,
  • 보드 밖으로 나갈 때는 선이 1개(inout wire)여야 합니다.
  • 이걸 합쳐주는 게 IOBUF입니다.
`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가 뭔가요?

  • FPGA 내부 로직은 0과 1만 있지만, I2C 통신선은 "High-Z (끊어진 상태)"가 필요합니다.
  • i2c_master가 "손 떼(_t=1)"라고 하면, IOBUF가 실제 핀을 회로에서 끊어줘서 외부 저항(Pull-up)이 전압을 1로 올려주게 만듭니다.
  1. 포트 타입 inout:
  • output이나 input이 아니라 inout을 썼습니다. 데이터가 나갔다 들어왔다 하니까요.
profile
Design Verification engineer

0개의 댓글