관리 메뉴

거북이처럼 천천히

Verilog RTL 설계(8월 21일 - 2, I2C 통신 - 2) 본문

RTL Design/Verilog RTL 설계

Verilog RTL 설계(8월 21일 - 2, I2C 통신 - 2)

유로 청년 2024. 8. 23. 20:13

1. I2C 통신

 

Verilog RTL 설계(8월 21일 - 1, I2C 통신 - 1)

1. I2C 통신 이란?I2C 통신은 Inter-Integrated Circuit의 약자로서 다음과 같은 의미를 갖는다.▶ Inter는 I2C 통신이 여러 장치들 사이에서 이루어진다.▶ Integrated Circuit는 하나의 집적 회로 안에 여러 기능

jbhdeve.tistory.com

 

 

 

 

 

2. I2C 통신을 통해 Master와 Slave 간에 통신 모듈 설계

  • Master와 Slave은 하나의 SCL 신호선과 SDA 신호선을 통해 데이터를 주고 받으며, 아래 그림과 같이 통신 과정을 거쳐 Master 측에서 데이터를 Read / Write 할 수 있다.

출처 : https://blog.naver.com/specialist0/220645221966

 

  • 위 그림을 토대로 FSM 방식으로 모듈을 설계할 때, State Machine을 정의할 것이다.
  • 모듈 설계에 대한 자세한 내용은 코드와 함께 설명을 이어가도록 하겠다.

 

 

< Source, Module의 Input / Output 변수 선언 >

module i2c_master(
    input clk, reset_p,
    input [6:0] addr,                  // 타겟인 Slave의 주소
    input read_write,                  // 타겟 Slave을 Read / Write할 것인지 여부
    input [7:0] data,                  // Slave에게 보낼 데이터 (8bit)
    input comm_go,                     // I2C 통신의 시작을 enable할지 여부
    output reg scl, sda,               // I2C 통신에 필요한 신호선
    output [7:0] state_led);           // 현재 어떤 상태있는지를 확인

endmoudle
  • Moudle 설계에 필요한 Input / Output 변수는 다음과 같다.
    ▶ addr : 타겟인 Slave 모듈의 주소
    ▶ read_write : 타겟인 Slave 모듈을 Read 할 것인지, Write 할 것인지를 결정하는 변수
    ▶ data : 타겟인 Slave 모듈에게 전달할 8bit 데이터 
    ▶ comm_go : 외부에서 I2C 통신의 시작을 컨트롤하는 변수

    ▶ scl, sda : I2C 통신에 필요한 신호선
    ▶ state_led : FSM 방식으로 설계했기 때문에 현재 상태 단계를 LED를 확인하기 위해 LED를 output으로 설정

    ★★★★★★★★★
    Q) SDA (Serial Data) 신호선이 왜 Output으로 설정되어 있는가? 양방향으로 송수신이 가능하기 때문에
         inout으로 설정해야하는 것이 아닌가?
    A)  그렇다. SDA (Serial Data) 신호선은 양방향으로 송수신이 가능한 신호선이기 때문에 inout으로 설정해야
         한다. 하지만, 우리가 나중에 사용할 Slave 모듈은 LCD display이며, LCD display에게 데이터를 전송하여
         디스플레이에 글자를 띄우는 것이 목표이기 때문에 inout이 아닌 output으로 설정했다.
        

 

 

< Source, State Machine 선언 >

 // Declare state machine 
parameter S_IDLE = 7'b000_0001;
parameter S_I2C_START = 7'b000_0010;
parameter S_SEND_ADDR = 7'b000_0100;
parameter S_WAIT_ACK = 7'b000_1000;
parameter S_SEND_DATA = 7'b001_0000;
parameter S_SCL_STOP = 7'b010_0000;
parameter S_I2C_END = 7'b100_0000;
  • I2C 통신 모듈의 State Machine을 위와 같이 설정하였으며, 각각의 상태 단계에서는 다음과 같은 작업을 수행한다.
    ▶ S_IDLE : 아직 comm_go 신호가 활성화 되지 않았기 때문에 I2C 통신이 이루어지기 전 대기 상태
    ▶ S_I2C_START : 
    comm_go 신호가 활성화 되어 I2C 통신의 시작을 알리기 위해 SCL 신호 값이
                                      High-Level 일 때, SDA 신호선의 값을 1 → 0으로 떨어뜨리게 된다.
    ▶ S_SEND_ADDR :
    Master 측에서 타겟으로하는 Slave 모듈을 호출하는 단계이며, SDA 신호선을 통해 
                                      타겟인 Slave의 주소와 Read / Write 정보를 보내게 된다. 
    ▶ S_WAIT_ACK :
    Slave 측으로부터 Response signal을 받길 기다리는 단계 
    ▶ S_SEND_DATA :
    Slave 측에게 Write할 8bit 데이터를 전송하는 단계
    ▶ S_SCL_STOP :
    SCL 을 비활성화 시켜 I2C 통신을 끝낼 준비를 하게 된다.
    ▶ S_I2C_END :
    Master 측에서 I2C 통신을 끝내기 위해 SCL 신호 값이 High-Level 일 때,
                                   SDA 신호선의 값을 0 → 1으로 올리게 된다.

 

 

 

 

< Source, state, next_state 변수 선언 후, 언제 다음 상태로 전이되는지를 정의  >

// Declare state, next state
reg [6:0] state, next_state;
    
// 다음 상태로 넘어가는가?
always @(posedge clk or posedge reset_p) begin
   if(reset_p) state = S_IDLE;
   else state = next_state;
end

 

 

 

 

< Source, 100kHz 주파수를 갖는 Clock Pulse 만들기 >

  • I2C 통신의 SCL 신호선을 통해 Master에서 Slave로 전달하는 Clock Pulse의 주파수는 Master에 의해 결정된다.
    ( 극단적으로 Master 측에서 1Hz 주파수 클럭 신호를 기준으로 I2C 통신할 수 있도록 할 수 있다. )
  • 이번 구현에서 사용할 Clock Pulse의 주파수는 100kHz 주파수를 갖는다.
    100kHz 주파수는 10us 주기를 갖기 때문에 분주화 작업을 통해 10us 주기를 갖는 Clock Pulse를 생성하도록 하겠다.

 

// ******** 100kHz 주파수를 갖는 Clock Pulse을 만들기  ********
// ******** 100kHz 주파수를 갖는 Clock Pulse의 주기 = 10usec  ********
// 100분주화 작업
wire clk_1usec;
clk_div_100 clk_div_usec (.clk(clk), .reset_p(reset_p), .clk_div_100_nedge(clk_1usec));
  •  100 분주화 작업을 통해 주기가 1us 인 One Cycle Pulse을 생성한다.

 

// 10분주화 작업
// 5usec 마다 scl 값을 Toggle 시킨다.
reg [2:0] counter_usec5;
reg scl_enable;  // 신호를 보내기 전, 후로 SCL의 clock을 사용하지 않기 때문에
                 // 사용할 때만 enable 시키고, 나머지는 disable 시켜 1로 초기화 시켜 준다.

always @(negedge clk or posedge reset_p) begin
    if (reset_p) begin 
        counter_usec5 = 0;
        scl = 1;
    end
    else if (scl_enable && clk_1usec) begin
        if (counter_usec5 >= 4) begin
            counter_usec5 = 0;
            scl = ~scl;
        end
        else begin
            counter_usec5 = counter_usec5 + 1;
        end
    end 
    else if (!scl_enable) begin
        counter_usec5 = 0;
        scl = 1;
    end
end
  • counter_usec5 Register 를 통해 5usec 마다 scl 값을 시켜 Toggle시켜 주기가 10us 인 Pulse Wave를 만든다.
  • Q) scl_enable 변수는 어떤 역활을 수행하게 되는가?
    A)
    scl_enable 변수는 "SCL 신호선에 전달되는 Clock Signal을 컨트롤하는 변수" 이다.
         scl_enable 변수를 통해 I2C 통신이 이루어지지 않는 경우, 해당 변수값을 0을 줌으로서 SCL 값을
         disable 으로 전환시켜 SCL (Serial Clock) 값을 1로 초기화 시킨다.

  • scl_enable 값이 1인 조건과 clk_usec5가 활성화되면 counter_usec5 레지스터 값을 1씩 증가시킨다.
  • counter_usec5 레지스터 값이 4 이상이면 scl 값을 Toggle시킨 뒤, counter_usec5 레지스터 값을 0으로 초기화 시켜준다.
  • scl 신호를 더 이상 사용하지 않고, 1로 초기화 시켜주고 싶은 경우에는 scl_enable 값을 0으로 만들어줌으로서 scl 값을 1로 초기화시키는 동시에 더 이상 counting을 하지 않게 된다.

 

 

 

 

< Source, 필요한 신호, 신호선의 Edge를 감지 >

// ******** 필요한 신호의 Edge를 잡기. ********
// comm_go 변수는 Master와 Slave 간에 데이터 송수신 시작을 결정하는 변수
// comm_go 변수가 positive edge일 때 시작
    
// SCL (Serial Clock) 신호선 값이 Low 이냐? High 이냐? 에 따라 SDA 신호선의 값이 설정
// 따라서 SCL 신호선의 Negative edge, Positive edge가 모두 필요
wire comm_go_pedge;
edge_detector edge_detector_comm_go (.clk(clk), .reset_p(reset_p), .cp(comm_go), .p_edge(comm_go_pedge));
    
wire scl_pedge, scl_nedge;
edge_detector edge_detector_scl (.clk(clk), .reset_p(reset_p), .cp(scl), .p_edge(scl_pedge), .n_edge(scl_nedge));
  • comm_go 변수는 "해당 변수가 활성화되면 Master와 Slave 모듈 간에 I2C 통신 시작한다." 의미를 갖고 있다.
  • comm_go 변수가 활성화되면 I2C 통신을 시작해야 하기 때문에 comm_go 변수의 positive edge를 검출함으로서 comm_go 변수 값이 0 → 1로 변화하여 활성화 되었음을 확인할 수 있다.

  • SCL (Serial Clock) 신호선이 Low-Level 일 때, SDA (Serial Data) 신호 값을 변경하고,
    SCL (Serial Clock) 신호선이 High-Level 일 때, SDA (Serial Data) 신호 값을 유지해야 한다.
  • 따라서 원하는 시점에 보내고자 하는 데이터를 변경하고, 데이터를 유지하기 위해서는 SCL의 edge를 감지할 필요가 있고, 이를 위해 edge_detector 모듈을 통해 SCL (Serial Clock) 신호의 Positive / Negative edge를 검출한다.

 

 

 

< Source, 결합 연산자를 통해 타겟인 Slave 모듈의 주소와 해당 모듈을 Read / Write 할 것인지에 대한 정보를 결합 >

// ******** Declare necessary variables ********
// S_SEND_ADDR 단계에서 Slave의 주소와 Read/Write를 보내기 위해 결합연산자를 이용하여 결합한다.
wire [7:0] data_addr_read_write;
assign data_addr_read_write = {addr, read_write};
  • S_SEND_ADDR 상태 단계에서 타겟인 Slave 모듈의 주소 (7bit)와 해당 Slave 모듈을 Read 할 것인지, Write 할 것인지를 갖고 있는 정보 (1bit)를 보내야 한다.
  • 따라서 사전에 해당 정보를 결합 연산자를 통해 결합하여 8bit register에 저장하여 보관한다.

 

 

 

< Source, 필요한 변수 선언 >

// Data와 data_addr_read_write을 8bit을 순차적으로 보내기 위해서 
// Register를 선언후, Counting한다.
reg [2:0] cnt_data;
    
// ACK State machine이 두 군데에서 사용하기 때문에 ACK State에서 다음 state로 갈 수 있는
// 선택지가 2개이다. 따라서 둘 중 하나를 선택해야하기 때문에 Flag register을 선언하여
// Flag register 값에 따라 다음 상태값을 결정하게 된다.
reg flag_ack;
  • cnt_data 변수 : 8bit 데이터를 Shift register에게 순차적으로 보내는 과정에서 "지금까지 Shfit register에게
                              몇 개의 데이터를 보내는가?"를 Counting 하는 변수
  • flag_ack 변수 : S_WAIT_ACK 상태 단계에서 넘어 갈 수 있는 다음 상태는 S_SEND_DATA 단계와
                              S_SCL_STOP 단계이기 때문에 flag_ack 레지스터 값에 따라 어느 상태로 이동할지
                              여부를 결정하게 된다.

 

 

< Source, 각 상태 단계에서의 동작과 다음 상태 단계로 전이되기 위한 조건을 정의 >

// ******** 각 상태 단계의 동작을 정의한다. ********
always @(negedge clk or posedge reset_p) begin
     if(reset_p) begin
         sda = 1;
         next_state = S_IDLE;
         scl_enable = 0;
         cnt_data = 7;
         flag_ack = 0;
      end
      else begin
           case(state)
             
               ''' 각 상태 단계의 동작, 다음 상태로 전이되기 위한 조건 '''
             
           endcase
      end
end

 

 

< Source, 1단계) S_IDLE 단계 >

    // 1단계) S_IDLE 단계
    S_IDLE : begin
        scl_enable = 0;
        sda = 1;    

        if (comm_go_pedge) begin
            next_state = S_I2C_START;
        end
    end

  • scl_enable 변수를 통해 SCL (Serial Clock) 값을 1로 초기화 시켜주고, SDA (Serial Data) 값을 1로 초기화한다.
  • Top module에서 comm_go 변수 값을 0 → 1으로 변화시켜줌으로서  I2C 통신의 시작한다.
  • comm_go 값에서 Positive edge가 발생하면 다음 상태인 S_I2C_START 로 전이된다.

 

 

 

 

 

< Source, 2단계) S_IDLE 단계 >

    // 2단계) S_I2C_START 단계
    S_I2C_START : begin
        sda = 0;
        scl_enable = 1;

        next_state = S_SEND_ADDR;
    end

  • scl_enable = 1 을 대입함으로서 SCL (Serial Clock Signal) 신호선에 10us 주기를 갖는 Clock Pulse을 인가시킨다.
  • sda = 0 을 대입함으로서 SCL (Serial Clock Signal) 신호선의 신호 값이 High-Level 일 때, sda 값을 Negative edge를 발생킨다.
  • scl 신호 값이 High-Level일 때, sda 값에서 Negative-edge가 발생하면 "I2C 통신 시작" 신호로 해석하여 I2C 통신이 시작한다.
  • 위 과정을 거친 뒤, 다음 상태인 S_SEND_ADDR 상태 단계로 전이한다.

 

 

 

< Source, 3단계) S_SEND_ADDR 단계 >

    // 3단계) S_SEND_ADDR 단계
    S_SEND_ADDR : begin
        if (scl_nedge) sda = data_addr_read_write[cnt_data];
        else if (scl_pedge) begin
            if (cnt_data == 0) begin
                next_state = S_WAIT_ACK;
                cnt_data = 7;
            end else begin
                cnt_data = cnt_data - 1; 
            end
        end
    end

  • cnt_data 값을 1씩 down-counting하여 data_addr_read_write 레지스터 값을 Shift register로 Serial 형식으로 전달한다.
  • cnt_data 값이 0에 도달하면 cnt_data 값을 7로 초기환 뒤, 다음 상태인 S_WAIT_ACK 단계로 전이 된다.

    ★★★★★★★★★★★★
  • 주의) SCL (Serial Clock) 신호 값이 Low-Level 일 때, SDA 값을 변화하고,
             SCL (Serial Clock)  신호 값이 High-Lvel 일 때, SDA 값을 유지해야 한다.

  • SCL 신호 값에 따라 SDA 값을 변화시키기 위해서 SCL의 edge을 알려주는 one cycle pulse을 이용하며,
    scl_nedge 일 때, sda 와이어를 통해 sda 값을 변화시키고, scl_pedge 일 때, 값을 유지시켜주는 동시에 cnt_data 값을 1씩 down-counting한다.

 

 

 

 

< Source, 4단계) S_WAIT_ACK 단계 >

    // 4단계) S_WAIT_ACK 단계
    S_WAIT_ACK : begin
        // 하나의 sda 신호선을 Master와 Slave 간에 공유하여 사용하기 때문에
        // Slave 측에서 Response signal을 보내기 위해서는 Master 측에서
        // 끊어줘야 데이터 간에 충돌을 막을 수 있다.
        if (scl_nedge) begin
            sda = 'bz;
        end else if (scl_pedge) begin
            if (!flag_ack) begin
                flag_ack = 1;
                next_state = S_SEND_DATA;
            end else begin
                flag_ack = 0;
                next_state = S_SCL_STOP;
            end
        end
    end

  • S_WAIT_ACK 단계 는 Master 측에서 Target인 Slave 모듈과 Read / Write 데이터에 대한 Slave 모듈의 Response signal을 보내는 단계이다.
  • Master와 Slave 모듈간에 하나의 SDA (Serial Data) 신호선을 통해 양방향으로 데이터를 송수신한다,
  • 따라서 Slave 모듈로 부터 Response signal을 받기 위해서는 Master 측에서 sda 신호선을 임피던스 상태로 전환하여 회로를 끊고, 읽기 모드로 전환해야 한다.

    ★★★★★★★★★★★★
  • 주의) sda = 'bz 을 통해 Master 측에서 sda 신호선을 끊어 줘야 하지만, scl_nedge가 발생했을 때, 수정해야 한다.
    sda = 'bz 을 통해 Master 측에서 sda 신호선을 끊어 줘야 하지만, 임의대로 회로를 끊으면 안되고, scl_nedge일 때, sda = 'bz  을 통해 Master측에서 SDA 신호선을 끊어줘야 한다.
    만약, scl_nedge 일 때 SDA 신호선을 끊어주지 않고, 임의대로 회로를 끊어준다면 S_WAIT_ACK 상태 단계 에 진입하자마자 회로가 끊어지기 때문에 이전 단계인 S_SEND_ADDR 단계 에서 1bit 데이터인 Read / Write 데이터를 전달하지 못하고, 임피던스 상태로 전환된다.
  • 따라서 SCL 신호선이 Negative edge일 때, SDA 신호선의 값을 변화시켜야 한다.


  • S_WAIT_ACK 상태 단계는 아래와 같이 두 번의 상태 단계에서 사용된다.

  • 따라서 S_WAIT_ACK 상태에서 갈 수 있는 다음 상태 단계는 1) S_SEND_DATA, 2) S_SCL_STOP 가 있다.

  • I2C 통신 절차에 따라서 S_WAIT_ACK → S_SEND_DATA → S_SCL_STOP 절차를 수행해야 하기 때문에 flag_ack 레지스터를 이용하여 flag_ack 값이 0이면 S_SEND_DATA 상태 단계, flag_ack 값이 1이면 S_SCL_STOP 상태 단계로 전이되도록 설계하였다.

 

 

 

 

 

< Source, 5단계) S_SEND_DATA 단계 >

// 5단계) S_SEND_DATA 단계
S_SEND_DATA : begin
    if (scl_nedge) 
        sda = data[cnt_data];
    else if (scl_pedge) begin
        if (cnt_data == 0) begin
            next_state = S_WAIT_ACK;
            cnt_data = 7;
        end
        else 
            cnt_data = cnt_data - 1; 
    end
end

 

  • S_SEND_DATA 단계는 Master측에서 Slave 모듈로 Write 할 8bit 데이터를 전송하는 단계이다.
  • S_SEND_ADDR 단계 와 동일하게 cnt_data 레지스터를 통해 1개씩 데이터를 보내는 동시에 1씩 down counting하여 보내야 하는 데이터를 counting한다.
  • 물론, scl_nedge 일때만 SDA 신호선의 값인 sda 값을 수정하고, scl_pedge 일 때에는 sda 값을 유지한다.
  • Slave 모듈로 Write 할 8bit 데이터을 모두 전송하면 다음 단계인 S_WAIT_ACK 단계로 이동한다.

 

 

 

 

< Source, 6단계) S_WAIT_ACK  단계 >

    // 4단계) S_WAIT_ACK 단계
    S_WAIT_ACK : begin
        // 하나의 sda 신호선을 Master와 Slave 간에 공유하여 사용하기 때문에
        // Slave 측에서 Response signal을 보내기 위해서는 Master 측에서
        // 끊어줘야 데이터 간에 충돌을 막을 수 있다.
        if (scl_nedge) begin
            sda = 'bz;
        end else if (scl_pedge) begin
            if (!flag_ack) begin
                flag_ack = 1;
                next_state = S_SEND_DATA;
            end else begin
                flag_ack = 0;
                next_state = S_SCL_STOP;
            end
        end
    end

  • S_WAIT_ACK  단계 에서는 Master 측에서 보낸 8bit 데이터에 대한 Slave 모듈이 잘 받았는지 여부를 나타내는 Response signal을 기다리는 단계이다.
  • 이 때, flag_ack 레지스터 값이 1이기 때문에 다음 상태를 S_SCL_STOP 단계로 전이되게 된다.

 

 

 

 

< Source, 7단계) S_SCL_STOP  단계 >

// 6단계) S_SCL_STOP 단계
S_SCL_STOP : begin
    if (scl_nedge) 
        sda = 0;    
    else if (scl_pedge) 
        next_state = S_I2C_END;
end

  • I2C 통신을 끝내기 위해서는 SCL 신호선이 High-Level 일 때,  SDA 신호선에서 Positive edge가 발생되어야 한다.
  • S_SCL_STOP 단계  I2C 통신을 끝내기 위한 준비를 하는 단계이며, I2C 통신을 끝내기 위해 scl_nedge 일때, sda = 0을 대입한다.
  • sda 값이 0이여야만 0 → 1 로 변화시키켜 Positive edge를 발생시킬 수 있기 때문에 sda값을 0을 대입한다.
  • sda 값을 0으로 수정한 뒤, 다음 scl_pedge일 때, 다음 상태로 넘어간다.

 

 

 

 

 

< Source, 8단계) S_I2C_END 단계 >

// 7단계) S_I2C_END 단계
S_I2C_END : begin
    if (cnt_data <= 3) begin
        cnt_data = cnt_data + 1;
        sda = 0;
    end
    else begin
        cnt_data = 0;
        sda = 1;
        next_state = S_IDLE;
        scl_enable = 0;
    end
end

  • Q) delay time을 갖은 뒤, sda 값을 0 → 1로 변화시키는가? 바로 변화시켜도 되지 않은가?
    A)
    바로 변화시키게 될 경우, SDA의 신호 값인 sda의 positive edge을 제대로 감지 못할 경우도 발생할 수 있기 때문에 delay time을 갖고, sda 값을 0 → 1로 변화시켜 주었다.

 

 

 

 

 

3. 전체 소스 코드

module i2c_master(
    input clk, reset_p,
    input [6:0] addr,
    input read_write,
    input [7:0] data,
    input comm_go,
    output reg scl, sda,
    output [7:0] state_led);
    
    // Declare state machine 
    parameter S_IDLE = 7'b000_0001;
    parameter S_I2C_START = 7'b000_0010;
    parameter S_SEND_ADDR = 7'b000_0100;
    parameter S_WAIT_ACK = 7'b000_1000;
    parameter S_SEND_DATA = 7'b001_0000;
    parameter S_SCL_STOP = 7'b010_0000;
    parameter S_I2C_END = 7'b100_0000;

    // Declare state, next state
    reg [6:0] state, next_state;
    
    // 다음 상태로 넘어가는가?
    always @(posedge clk or posedge reset_p) begin
        if(reset_p) state = S_IDLE;
        else state = next_state;
    end
    
    
    // ******** 100kHz 주파수를 갖는 Clock Pulse을 만들기  ********
    // ******** 100kHz 주파수를 갖는 Clock Pulse의 주기 = 10usec  ********
    // 100분주화 작업
    wire clk_1usec;
    clk_div_100 clk_div_usec (.clk(clk), .reset_p(reset_p), .clk_div_100_nedge(clk_1usec));
    
    // 10분주화 작업
    // 5usec 마다 scl 값을 Toggle 시킨다.
    reg [2:0] counter_usec5;
    reg scl_enable;  // 신호를 보내기 전, 후로 SCL의 clock을 사용하지 않기 때문에
                     // 사용할 때만 enable 시키고, 나머지는 disable시켜 1로 초기화시켜 준다.
    
    always @(negedge clk or posedge reset_p) begin
        if(reset_p) begin 
            counter_usec5 = 0;
            scl = 1;
        end
        else if(scl_enable && clk_1usec) begin
                if(counter_usec5 >= 4) begin
                    counter_usec5 = 0;
                    scl = ~scl;
                end
                else counter_usec5 = counter_usec5 + 1;
        end 
        else if(!scl_enable) begin
            counter_usec5 = 0;
            scl = 1;
        end
    end
    
    // ******** 필요한 신호의 Edge를 잡기. ********
    // comm_go 변수는 Master와 Slave 간에 데이터 송수신 시작을 결정하는 변수
    // comm_go 변수가 positive edge일 때 시작
    
    // SCL (Serial Clock) 신호선 값이 Low 이냐? High 이냐? 에 따라 SDA 신호선의 값이 설정
    // 따라서 SCL 신호선의 Negative edge, Positive edge가 모두 필요
    wire comm_go_pedge;
    edge_detector edge_detector_comm_go (.clk(clk), .reset_p(reset_p), .cp(comm_go), .p_edge(comm_go_pedge));
    
    wire scl_pedge, scl_nedge;
    edge_detector edge_detector_scl (.clk(clk), .reset_p(reset_p), .cp(scl), .p_edge(scl_pedge), .n_edge(scl_nedge));
    
    // ******** Declare necessary variables ********
    // S_SEND_ADDR 단계에서 Slave의 주소와 Read/Write를 보내기 위해 결합연산자를 이용하여 결합한다.
    wire [7:0] data_addr_read_write;
    assign data_addr_read_write = {addr, read_write};
    
    // Data와 data_addr_read_write을 8bit을 순차적으로 보내기 위해서 
    // Register를 선언후, Counting한다.
    reg [2:0] cnt_data;
    
    // ACK State machine이 두 군데에서 사용하기 때문에 ACK State에서 다음 state로 갈 수 있는
    // 선택지가 2개이다. 따라서 둘 중 하나를 선택해야하기 때문에 Flag register을 선언하여
    // Flag register 값에 따라 다음 상태값을 결정하게 된다.
    reg flag_ack;
    
    // ******** 각 상태 단계의 동작을 정의한다. ********
    always @(negedge clk or posedge reset_p) begin
        if(reset_p) begin
            sda = 1;
            next_state = S_IDLE;
            scl_enable = 0;
            cnt_data = 7;
            flag_ack = 0;
        end
        else begin
            case(state)
                // 1단계) S_IDLE 단계
                S_IDLE : begin
                    scl_enable = 0;
                    sda = 1;    
                    
                    if(comm_go_pedge) next_state = S_I2C_START;
                end
                
                // 2단계) S_I2C_START 단계
                S_I2C_START : begin
                    sda = 0;
                    scl_enable = 1;
                    
                    next_state = S_SEND_ADDR;
                end
                
                // 3단계) S_SEND_ADDR 단계
                S_SEND_ADDR : begin
                    if(scl_nedge) sda = data_addr_read_write[cnt_data];
                    else if(scl_pedge) begin
                        if(cnt_data == 0) begin
                            next_state = S_WAIT_ACK;
                            cnt_data = 7;
                        end
                        else cnt_data = cnt_data - 1; 
                    end
                end
                
                // 4단계) S_WAIT_ACK 단계
                S_WAIT_ACK : begin
                    // 하나의 sda 신호선을 Master와 Slave간에 공유하여 사용하기 때문에
                    // Slave 측에서 Response signal을 보내기 위해서는 Master 측에서
                    // 끊어줘야 데이터 간에 충돌을 막을 수 있다.
                    if(scl_nedge) sda = 'bz;
                    
                    else if(scl_pedge) begin
                        if(!flag_ack) begin
                            flag_ack = 1;
                            next_state = S_SEND_DATA;
                        end
                        else begin
                            flag_ack = 0;
                            next_state = S_SCL_STOP;
                        end
                    end
                end
                
                // 5단계) S_SEND_DATA 단계
                S_SEND_DATA : begin
                    if(scl_nedge) sda = data[cnt_data];
                    else if(scl_pedge) begin
                        if(cnt_data == 0) begin
                            next_state = S_WAIT_ACK;
                            cnt_data = 7;
                        end
                        else cnt_data = cnt_data - 1; 
                    end
                end
                
                // 6단계) S_SCL_STOP 단계
                S_SCL_STOP : begin
                    if(scl_nedge) sda = 0;    
                    
                    else if(scl_pedge) next_state = S_I2C_END;
                end
                
                // 7단계) S_I2C_END 단계
                S_I2C_END : begin
                    if(cnt_data <= 3) begin
                        cnt_data = cnt_data + 1;
                        sda = 0;
                    end
                    else begin
                        cnt_data = 0;
                        sda = 1;
                        next_state = S_IDLE;
                        scl_enable = 0;
                    end
                end
            endcase
        end
    end
endmodule


// Clock Divider 100
module clk_div_100 (
    input clk, reset_p,
    output clk_div_100, 
    output clk_div_100_nedge, clk_div_100_pedge );
    
    reg [6:0] counter;
    always @(posedge clk or posedge reset_p) begin
        if(reset_p) counter = 0;
        else begin
            if(counter >=99) counter = 0;
            else counter = counter + 1;
        end
    end
    
    assign clk_div_100 = (counter < 50) ? 0 : 1;
    edge_detector edge_detector_0(.clk(clk), .reset_p(reset_p), .cp(clk_div_100), .p_edge(clk_div_100_pedge), .n_edge(clk_div_100_nedge));
    
endmodule

// Edge detector
module edge_detector (
    input clk, reset_p,
    input cp,
    output p_edge, n_edge );
    
    reg flip_flop_current, flip_flop_old;
    
    always @(posedge clk or posedge reset_p) begin
        if(reset_p) begin
            flip_flop_current <= 0;
            flip_flop_old <= 0;
        end
        else begin
            flip_flop_current <= cp;
            flip_flop_old <= flip_flop_current;
        end
    end
    
    assign p_edge = ({flip_flop_current, flip_flop_old} == 2'b10) ? 1 : 0;
    assign n_edge = ({flip_flop_current, flip_flop_old} == 2'b01) ? 1 : 0;
    
endmodule

 

 

 

 

4. 시뮬레이션 결과

  • 타겟인 Slave 모듈의 주소 : 7'h27
  • Read / Write : Write
  • Write 하고자 하는 데이터 : 8'h40

  • 시뮬레이션에서 scl, sda 에 대해서 scl 값이 High-level일 때, sda 값을 읽었을 때, addr 데이터와 read / write 데이터, data 데이터와 동일함을 확인할 수 있다.
  • 또한 각각의 상태를 나누었을 때, I2C 통신 절차와 동일한 scl 신호 값과 sda 신호 값이 나타남을 확인 할 수 있다.