Verilog_RTL 설계/Basys_3

[FSM] I2C_LCD_Control

juniha 2025. 7. 22. 20:48

목적

  • I2C LCD 디스플레이 제어:
    • FPGA에서 I2C 마스터 역할을 하여, LCD에 명령/문자를 전송
    • LCD 초기화, 문자 출력, 화면 좌/우 이동 등 다양한 제어 명령을 구현
  • 버튼 기반 동작:
    • 4개의 버튼에 각각 LCD 동작을 할당
    • 버튼을 누르면 지정된 명령(문자 출력, HELLO 출력, 화면 이동 등)이 LCD로 전송
  • 하드웨어 수준 I2C 통신:
    • MCU처럼 내부 I2C IP를 쓰지 않고, Verilog로 I2C 신호 생성(SCL, SDA)을 직접 구현
    • I2C 통신의 각 단계를 FSM으로 하드웨어화

사용 코드 _ 상위 모듈

module i2c_txtlcd_top(
    input clk, reset_p,               // 시스템 클록(예: 100MHz), 리셋(Active-high)
    input [3:0] btn,                  // 4개의 버튼 입력
    output scl, sda,                  // I2C 통신선(SCL:클록, SDA:데이터) - LCD와 연결
    output [15:0] debug_led           // 디버그용 LED 출력 (내부 상태 등 표시)
);

    // ----------------------------------------
    // 1. 딜레이 타이머: LCD 전원 투입 직후 안정화 대기(약 80ms)
    // LCD의 전원이 켜진 후 바로 명령을 보내면 오동작할 수 있으므로, 
    // 최초 리셋 후 약 80ms 동안 대기하는 카운터 회로
    // ----------------------------------------
    integer cnt_clk;           // 카운트 변수(정수형)
    reg count_clk_e;           // 카운트 활성화 신호
    always @(negedge clk or posedge reset_p) begin
        if (reset_p)
            cnt_clk = 0;       // 리셋 시 카운터 0으로 초기화
        else if (count_clk_e)
            cnt_clk = cnt_clk + 1; // count_clk_e=1이면 카운트 증가
        else
            cnt_clk = 0;       // count_clk_e=0이면 카운트 초기화(대기완료 후 재사용)
    end

    // ----------------------------------------
    // 2. 버튼 상승 에지 검출 회로 (debounce 포함)
    // 버튼이 눌릴 때마다 1클록 동안만 1로 출력 (btn_pedge)
    // 매번 버튼을 누를 때마다 중복동작/챠터링 없이 이벤트를 생성
    // ----------------------------------------
    wire [3:0] btn_pedge;   // 각 버튼에 대한 상승에지 출력
    button_cntr btn0(.clk(clk), .reset_p(reset_p), .btn(btn[0]), .btn_pedge(btn_pedge[0]));
    button_cntr btn1(.clk(clk), .reset_p(reset_p), .btn(btn[1]), .btn_pedge(btn_pedge[1]));
    button_cntr btn2(.clk(clk), .reset_p(reset_p), .btn(btn[2]), .btn_pedge(btn_pedge[2]));
    button_cntr btn3(.clk(clk), .reset_p(reset_p), .btn(btn[3]), .btn_pedge(btn_pedge[3]));

    // ----------------------------------------
    // 3. LCD 전송 관련 신호 정의
    // send_buffer : 전송할 데이터(명령/문자) 저장
    // send : 데이터 전송 요청 신호(1클록 펄스)
    // rs : 1이면 데이터, 0이면 명령어 전송 (LCD RS 신호 역할)
    // busy : LCD 전송중일 때 1 (완료까지 명령 추가 금지)
    // ----------------------------------------
    reg [7:0] send_buffer;   // I2C LCD에 전송할 1바이트 데이터
    reg send, rs;            // 전송 요청/데이터-명령 전환
    wire busy;               // LCD 송신기 busy 상태
    i2c_lcd_send_byte i2c_shift_SIPO(
        .clk(clk), .reset_p(reset_p),
        .addr(7'h3f),         // I2C LCD 주소(0x3F, 보드에 따라 0x27일 수도 있음)
        .send_buffer(send_buffer), .send(send), .rs(rs),
        .scl(scl), .sda(sda), .busy(busy),
        .debug_led(debug_led)
    );

    // ----------------------------------------
    // 4. FSM 상태 정의 (6비트 One-Hot 인코딩)
    // 각 상태는 1비트씩 할당하여 가독성과 안정성을 높임
    // ----------------------------------------
    parameter IDLE                 = 6'b00_0001; // 대기/초기화 전 상태
    parameter INIT                 = 6'b00_0010; // LCD 초기화 명령 송신 중
    parameter SEND_BYTE            = 6'b00_0100; // "0~9" 문자 출력
    parameter SHIFT_RIGHT_DISPLAY  = 6'b00_1000; // 화면 오른쪽 이동 명령
    parameter SHIFT_LEFT_DISPLAY   = 6'b01_0000; // 화면 왼쪽 이동 명령
    parameter SEND_STRING          = 6'b10_0000; // HELLO 문자열 출력 상태

    reg [5:0] state, next_state;   // 현재/다음 FSM 상태

    // ----------------------------------------
    // 5. FSM 현재 상태 레지스터 (비동기 리셋/네거티브 엣지 클록)
    // - Vivado 등에서는 항상 posedge clk 권장 (일부 IP/블록에 맞춘다면 예외)
    // ----------------------------------------
    always @(negedge clk or posedge reset_p) begin
        if (reset_p)
            state <= IDLE;
        else
            state <= next_state;
    end

    // ----------------------------------------
    // 6. FSM 동작/제어 변수 선언
    // - cnt_data: INIT, SEND_BYTE에서 송신 바이트 위치 인덱스
    // - init_flag: LCD 초기화 완료 플래그
    // - hello: "HELLO" 문자열 저장(ASCII, 5글자)
    // - cnt_string: HELLO 출력 시 문자의 위치 인덱스
    // ----------------------------------------
    reg [2:0] cnt_data;      // LCD 초기화 or 문자 출력시 데이터 인덱스
    reg init_flag;           // 초기화 완료 플래그
    reg [8*5-1:0] hello;     // "HELLO" 문자열 저장(40비트=5바이트)
    reg [2:0] cnt_string;    // HELLO 출력용 인덱스

    // ----------------------------------------
    // 7. FSM 메인 로직 (모든 상태 변화, 데이터 처리, LCD 전송 요청 제어)
    // 동기처리: posedge clk 기준으로 모든 제어 신호/상태 갱신
    // ----------------------------------------
    always @(posedge clk or posedge reset_p) begin
        if (reset_p) begin
            // 리셋 시 모든 변수/상태를 초기화
            next_state    <= IDLE;
            init_flag     <= 0;
            count_clk_e   <= 0;
            cnt_data      <= 0;
            send          <= 0;
            send_buffer   <= 0;
            rs            <= 0;
            hello         <= "HELLO";
            cnt_string    <= 5; // 5글자 모두 출력해야 하므로 5로 시작
        end else begin
            case (state)

                // -------------------------
                // IDLE: LCD 초기화 완료 전에는 딜레이 카운트,
                // 초기화 완료 후에는 각 버튼 이벤트 대기
                // -------------------------
                IDLE: begin
                    if (init_flag) begin
                        // 초기화 완료 시, 버튼 입력별로 상태 전이
                        if (btn_pedge[0]) next_state <= SEND_BYTE;
                        if (btn_pedge[1]) next_state <= SHIFT_RIGHT_DISPLAY;
                        if (btn_pedge[2]) next_state <= SHIFT_LEFT_DISPLAY;
                        if (btn_pedge[3]) next_state <= SEND_STRING;
                    end else begin
                        // 초기화 전: 8_000_000 클록 동안 카운트 (약 80ms, 100MHz 기준)
                        if (cnt_clk <= 32'd8_000_000)  
                            count_clk_e <= 1;  // 카운터 동작 시작
                        else begin
                            next_state <= INIT; // 시간 지나면 초기화 상태 진입
                            count_clk_e <= 0;   // 카운트 정지
                        end
                    end
                end

                // -------------------------
                // INIT: LCD 초기화 시퀀스 명령을 차례로 송신 (6단계)
                // -------------------------
                INIT: begin
                    if (busy) begin
                        send <= 0;  // busy 상태에서는 send 신호 내려줌
                        // 모든 명령 송신 완료하면 플래그 set, 상태 전이
                        if (cnt_data >= 6) begin
                            cnt_data   <= 0;
                            next_state <= IDLE;
                            init_flag  <= 1; // 초기화 완료 플래그 ON
                        end
                    end else if (!send) begin
                        // busy==0 & send==0이면 명령어 준비 및 전송
                        case (cnt_data)
                            0: send_buffer <= 8'h33; // Function set (재설정, 8비트)
                            1: send_buffer <= 8'h32; // Function set (4비트 모드)
                            2: send_buffer <= 8'h28; // 2라인, 5x8도트, 4비트
                            3: send_buffer <= 8'h0C; // Display ON, Cursor OFF
                            4: send_buffer <= 8'h01; // Display Clear
                            5: send_buffer <= 8'h06; // Entry Mode Set (오른쪽 이동)
                        endcase
                        send <= 1;          // 실제 전송 요청 신호 (1클록)
                        cnt_data <= cnt_data + 1; // 다음 명령 준비
                    end
                end

                // -------------------------
                // SEND_BYTE: 버튼0 누를 때 "0~9" 문자(ASCII) 출력 (한 번에 1개씩)
                // -------------------------
                SEND_BYTE: begin
                    if (busy) begin
                        send <= 0;          // busy==1일 때 send 신호 내림
                        next_state <= IDLE; // 완료되면 IDLE로 복귀
                        if (cnt_data >= 9)  // 9까지 출력하면 카운터 리셋
                            cnt_data <= 0;
                        else
                            cnt_data <= cnt_data + 1; // 다음 숫자 준비
                    end else begin
                        rs <= 1;            // 데이터 모드 (문자 출력)
                        send_buffer <= "0" + cnt_data; // 아스키 코드 '0'+n
                        send <= 1;          // 전송 요청
                    end
                end

                // -------------------------
                // SHIFT_RIGHT_DISPLAY: 버튼1 → 화면 우측 이동 명령 (한 칸)
                // -------------------------
                SHIFT_RIGHT_DISPLAY: begin
                    if (busy) begin
                        next_state <= IDLE;
                        send <= 0;
                    end else begin
                        rs <= 0;               // 명령어 모드
                        send_buffer <= 8'h1C;  // Shift Right 명령
                        send <= 1;
                    end
                end

                // -------------------------
                // SHIFT_LEFT_DISPLAY: 버튼2 → 화면 좌측 이동 명령 (한 칸)
                // -------------------------
                SHIFT_LEFT_DISPLAY: begin
                    if (busy) begin
                        next_state <= IDLE;
                        send <= 0;
                    end else begin
                        rs <= 0;               // 명령어 모드
                        send_buffer <= 8'h18;  // Shift Left 명령
                        send <= 1;
                    end
                end

                // -------------------------
                // SEND_STRING: 버튼3 → "HELLO" 문자열(5글자) 출력
                // cnt_string을 5~1까지 하나씩 감소하며 문자 전송
                // busy==0일 때만 실제 문자 전송, busy==1이면 send 내리고 대기
                // -------------------------
                SEND_STRING: begin
                    if (busy) begin
                        send <= 0; // busy==1이면 send 내림
                        if (cnt_string == 0) begin
                            cnt_string <= 5; // 모든 문자 송신 끝나면 인덱스 리셋
                            next_state <= IDLE;
                        end else begin
                            next_state <= SEND_STRING; // 아직 남아있으면 반복
                        end
                    end else if (send == 0) begin
                        // HELLO 각 문자 하나씩 전송 (MSB→LSB 순서)
                        case (cnt_string)
                            5: send_buffer <= hello[39:32]; // 'H'
                            4: send_buffer <= hello[31:24]; // 'E'
                            3: send_buffer <= hello[23:16]; // 'L'
                            2: send_buffer <= hello[15:8];  // 'L'
                            1: send_buffer <= hello[7:0];   // 'O'
                        endcase
                        rs <= 1;          // 데이터 모드
                        send <= 1;        // 전송 요청
                        cnt_string <= cnt_string - 1; // 다음 문자 준비
                        next_state <= SEND_STRING;
                    end
                end

            endcase
        end
    end

endmodule

사용 코드 _ 2c_lcd_send_byte

module i2c_lcd_send_byte(
    input clk, reset_p,              // 시스템 클록, 비동기 리셋(Active-high)
    input [6:0] addr,                // I2C LCD 슬레이브 주소 (7비트)
    input [7:0] send_buffer,         // 송신할 LCD 데이터(명령/문자)
    input send, rs,                  // 송신 트리거(1펄스), RS(Data/Command)
    output scl, sda,                 // I2C 신호(LCD 연결)
    output reg busy,                 // 동작중 busy 표시
    output [15:0] debug_led          // 디버깅용 LED 출력(상태 등)
);

    // ------------------------------------------------------------
    // FSM 상태 정의 (One-hot encoding: 각 상태마다 비트 1개)
    //   - IDLE: 대기
    //   - SEND_HIGH_NIBBLE_DISABLE: 상위 4비트, EN=0(Disable)
    //   - SEND_HIGH_NIBBLE_ENABLE:  상위 4비트, EN=1(Enable)
    //   - SEND_LOW_NIBBLE_DISABLE:  하위 4비트, EN=0(Disable)
    //   - SEND_LOW_NIBBLE_ENABLE:   하위 4비트, EN=1(Enable)
    //   - SEND_DISABLE: 마무리 EN=0(Disable)
    // HD44780 4bit 모드는 EN 신호(Enable)로 데이터 latch,  
    // EN=1 → 데이터 latch, EN=0 → 명령 전송 완료!
    // ------------------------------------------------------------
    parameter IDLE                      = 6'b00_0001;
    parameter SEND_HIGH_NIBBLE_DISABLE  = 6'b00_0010;
    parameter SEND_HIGH_NIBBLE_ENABLE   = 6'b00_0100;
    parameter SEND_LOW_NIBBLE_DISABLE   = 6'b00_1000;
    parameter SEND_LOW_NIBBLE_ENABLE    = 6'b01_0000;
    parameter SEND_DISABLE              = 6'b10_0000;

    reg [7:0] data;            // I2C 버퍼(실제 전송될 8비트)
    reg comm_start;            // I2C 마스터 송신 트리거

    // ------------------------------------------------------------
    // 송신 신호(send) 상승 에지 검출 (한 번만 트리거)
    // ------------------------------------------------------------
    wire send_pedge;
    edge_detector_p ed_start(.clk(clk), .reset_p(reset_p), .cp(send), .p_edge(send_pedge));

    // ------------------------------------------------------------
    // 100클록(1us) 주기의 내부 유닛 클록 생성 (전송 타이밍 제어용)
    // LCD에 데이터 전송시마다 최소 1us 이상 EN 신호를 유지해야 함
    // ------------------------------------------------------------
    wire clk_usec;
    clock_div_100 usec_clock(.clk(clk), .reset_p(reset_p), .nedge_div_100(clk_usec));

    // ------------------------------------------------------------
    // 딜레이 카운터 (count_usec)
    //   - 각 단계마다 일정 시간동안 데이터/EN 유지(약 200us)
    //   - count_usec_e=1일 때 동작, 0일 때 즉시 리셋
    // ------------------------------------------------------------
    reg [21:0] count_usec;     // 최대 4백만 us(4초)까지 카운트 가능
    reg count_usec_e;          // 카운트 enable 신호

    always @(negedge clk, posedge reset_p) begin
        if (reset_p) begin
            count_usec = 0;
        end else begin
            if (clk_usec && count_usec_e)
                count_usec = count_usec + 1; // 1us마다 증가
            else if (!count_usec_e)
                count_usec = 0; // 동작 끝나면 즉시 0으로 초기화
        end
    end

    // ------------------------------------------------------------
    // FSM 상태 레지스터(비동기 리셋, negedge clk) 
    //   - 보통 posedge clk 권장, 특이하게 negedge 사용
    // ------------------------------------------------------------
    reg [5:0] state, next_state;
    always @(negedge clk or posedge reset_p) begin
        if (reset_p) begin
            state = IDLE;
        end else begin
            state = next_state;
        end
    end

    // ------------------------------------------------------------
    // FSM 메인 로직
    // 각 상태마다 LCD용 4비트 데이터, EN 신호, 타이밍, busy/comm_start 제어
    // ------------------------------------------------------------
    always @(posedge clk or posedge reset_p) begin
        if (reset_p) begin
            next_state = IDLE;
            busy = 0;
            comm_start = 0;
            count_usec_e = 0;
            data = 0;
        end else begin
            case (state)
                // ------------------------------------------
                // 1. IDLE: 대기상태, 송신 트리거(send_pedge) 대기
                // ------------------------------------------
                IDLE: begin
                    if (send_pedge) begin
                        next_state = SEND_HIGH_NIBBLE_DISABLE;
                        busy = 1; // busy 시작
                    end
                end

                // ------------------------------------------
                // 2. 상위 nibble(4비트) 전송, EN=0 (Disable)
                //   - 하드웨어 LCD(HD44780)는 4비트 데이터+RS+RW+EN+BL 조합 사용
                //   - {D7~D4, BL=1, EN, RW=0, RS}
                //   - EN=0에서 데이터를 준비한 후 EN=1로 latch 시킨다
                // ------------------------------------------
                SEND_HIGH_NIBBLE_DISABLE: begin
                    if (count_usec <= 22'd200) begin // 200us 대기
                        data = {send_buffer[7:4], 3'b100, rs}; // EN=0
                        comm_start = 1;  // I2C 마스터 송신 요청
                        count_usec_e = 1;
                    end else begin
                        next_state = SEND_HIGH_NIBBLE_ENABLE;
                        count_usec_e = 0;
                        comm_start = 0;
                    end
                end

                // ------------------------------------------
                // 3. 상위 nibble(4비트) 전송, EN=1 (Enable)
                //   - EN=1로 데이터 latch
                // ------------------------------------------
                SEND_HIGH_NIBBLE_ENABLE: begin
                    if (count_usec <= 22'd200) begin
                        data = {send_buffer[7:4], 3'b110, rs}; // EN=1
                        comm_start = 1;
                        count_usec_e = 1;
                    end else begin
                        next_state = SEND_LOW_NIBBLE_DISABLE;
                        count_usec_e = 0;
                        comm_start = 0;
                    end
                end

                // ------------------------------------------
                // 4. 하위 nibble(4비트) 전송, EN=0 (Disable)
                // ------------------------------------------
                SEND_LOW_NIBBLE_DISABLE: begin
                    if (count_usec <= 22'd200) begin
                        data = {send_buffer[3:0], 3'b100, rs}; // EN=0
                        comm_start = 1;
                        count_usec_e = 1;
                    end else begin
                        next_state = SEND_LOW_NIBBLE_ENABLE;
                        count_usec_e = 0;
                        comm_start = 0;
                    end
                end

                // ------------------------------------------
                // 5. 하위 nibble(4비트) 전송, EN=1 (Enable)
                // ------------------------------------------
                SEND_LOW_NIBBLE_ENABLE: begin
                    if (count_usec <= 22'd200) begin
                        data = {send_buffer[3:0], 3'b110, rs}; // EN=1
                        comm_start = 1;
                        count_usec_e = 1;
                    end else begin
                        next_state = SEND_DISABLE;
                        count_usec_e = 0;
                        comm_start = 0;
                    end
                end

                // ------------------------------------------
                // 6. 마지막으로 EN=0 한번 더(Disable)
                //   - 프로토콜 규격에 따라 EN=1→EN=0 토글 필요
                // ------------------------------------------
                SEND_DISABLE: begin
                    if (count_usec <= 22'd200) begin
                        data = {send_buffer[3:0], 3'b100, rs}; // EN=0
                        comm_start = 1;
                        count_usec_e = 1;
                    end else begin
                        next_state = IDLE;
                        count_usec_e = 0;
                        comm_start = 0;
                        busy = 0; // busy 해제
                    end
                end

            endcase
        end
    end

    // ------------------------------------------------------------
    // 하위 모듈: I2C 마스터(별도 구현 필요)
    //   - comm_start=1일 때 data값을 addr 주소로 전송
    //   - rd_wr=0 (LCD는 항상 Write 동작만)
    // ------------------------------------------------------------
    I2C_master master (
        .clk(clk), .reset_p(reset_p),
        .addr(addr),
        .data(data), .rd_wr(0), .comm_start(comm_start),
        .scl(scl), .sda(sda), .debug_led(debug_led)
    );

endmodule

사용 코드 _ I2C_master

module I2C_master(
    input clk, reset_p,                // 시스템 클록, 비동기 리셋(Active-high)
    input [6:0] addr,                  // I2C 슬레이브 7비트 주소
    input [7:0] data,                  // I2C 송신 데이터(8비트)
    input rd_wr,                       // 0: Write, 1: Read
    input comm_start,                  // 송신 시작 트리거(1클록 펄스)
    output reg scl,                    // I2C 클록(Serial Clock Line)
    output reg sda,                    // I2C 데이터(Serial Data Line, open-drain)
    output reg [6:0] debug_led         // 디버깅(상태 표시 등)
);

    // --------------------------------------------------------------
    // 1. FSM 상태 정의 (One-hot 인코딩)
    // --------------------------------------------------------------
    parameter IDLE       = 7'b000_0001; // 대기
    parameter COMM_START = 7'b000_0010; // Start 조건 생성
    parameter SEND_ADDR  = 7'b000_0100; // 슬레이브 주소(7비트)+R/W 전송
    parameter RD_ACK     = 7'b000_1000; // ACK 비트 읽기 및 핸들링
    parameter SEND_DATA  = 7'b001_0000; // 데이터 8비트 전송
    parameter SCL_STOP   = 7'b010_0000; // Stop 조건 생성 준비
    parameter COMM_STOP  = 7'b100_0000; // Stop 조건(전송 완료)

    // --------------------------------------------------------------
    // 2. 1us 주기 내부 클록 생성 (I2C SCL 타이밍, 전송 속도 제어)
    //    I2C 표준모드 100kHz 이하 보장 (여유있게 클록 나눔)
    // --------------------------------------------------------------
    wire clk_usec;
    clock_div_100 usec_clock(
        .clk(clk),
        .reset_p(reset_p),
        .nedge_div_100(clk_usec)
    );

    // --------------------------------------------------------------
    // 3. 신호(에지) 검출: comm_start, scl (펄스, 상승/하강에지)
    // --------------------------------------------------------------
    wire comm_start_pedge, scl_nedge, scl_pedge;
    edge_detector_p ed_start(
        .clk(clk),
        .reset_p(reset_p),
        .cp(comm_start),
        .p_edge(comm_start_pedge)
    );
    edge_detector_p ed_scl(
        .clk(clk),
        .reset_p(reset_p),
        .cp(scl),
        .n_edge(scl_nedge),
        .p_edge(scl_pedge)
    );

    // --------------------------------------------------------------
    // 4. SCL(클록) 생성 FSM
    //    - scl_e=1: 내부 카운터(5us 단위)로 scl 토글(약 100kHz)
    //    - scl_e=0: scl을 1로 유지(Stop/Idle), 동작 멈춤
    // --------------------------------------------------------------
    reg [2:0] count_usec5;  // 5us 단위 카운터 (5클록: SCL period 조절)
    reg scl_e;              // SCL 토글 동작 enable
    always @(posedge clk, posedge reset_p) begin
        if (reset_p) begin
            count_usec5 = 0;
            scl = 0;
        end else if (scl_e) begin
            if (clk_usec) begin
                if (count_usec5 >= 4) begin // 5us마다 SCL 토글
                    count_usec5 = 0;
                    scl = ~scl;
                end else count_usec5 = count_usec5 + 1;
            end
        end else if (!scl_e) begin
            count_usec5 = 0;
            scl = 1; // SCL idle state(Stop 등에서 high 유지)
        end
    end

    // --------------------------------------------------------------
    // 5. FSM(상태전이) 레지스터 (negedge clk, 비동기 리셋)
    // --------------------------------------------------------------
    reg [6:0] state, next_state;
    always @(negedge clk, posedge reset_p) begin
        if (reset_p) state = IDLE;
        else         state = next_state;
    end

    // --------------------------------------------------------------
    // 6. 송신 데이터 구성 및 비트 전송 인덱스
    // --------------------------------------------------------------
    wire [7:0] addr_rd_wr;        // [7:1]: 주소, [0]: R/W
    assign addr_rd_wr = {addr, rd_wr};
    reg [2:0] cnt_bit;            // 7~0 (비트별 전송 인덱스)
    reg stop_flag;                // 마지막(데이터 후) Stop Condition 판별

    // --------------------------------------------------------------
    // 7. I2C 전송 FSM: 각 상태별 동작
    // --------------------------------------------------------------
    always @(posedge clk or posedge reset_p) begin
        if (reset_p) begin
            next_state = IDLE;
            scl_e = 0;
            sda = 1;
            cnt_bit = 7;        // 주소/데이터 7~0 순서 전송
            stop_flag = 0;
        end else begin
            case (state)
                // ------------------------------------------------------
                // IDLE: 대기, comm_start 펄스 들어오면 Start 조건 준비
                // ------------------------------------------------------
                IDLE: begin
                    if (comm_start_pedge) next_state = COMM_START;
                    scl_e = 0;      // SCL 동작 정지
                    sda = 1;        // SDA high (idle)
                end
                // ------------------------------------------------------
                // COMM_START: Start 조건 생성 (SCL=1 상태에서 SDA=1→0)
                //   I2C Start: SDA가 SCL high에서 1→0으로 변하면 생성
                // ------------------------------------------------------
                COMM_START: begin
                    sda = 0;       // SDA low(시작)
                    scl_e = 1;     // SCL 토글 enable (SCL: 1→0 변환시 동작)
                    next_state = SEND_ADDR;
                end
                // ------------------------------------------------------
                // SEND_ADDR: 슬레이브 주소+R/W 비트 송신 (MSB부터)
                //   SCL falling edge에서 데이터 셋, rising edge에서 샘플
                // ------------------------------------------------------
                SEND_ADDR: begin
                    if (scl_nedge) sda = addr_rd_wr[cnt_bit]; // 데이터 출력
                    if (scl_pedge) begin
                        if (cnt_bit == 0) begin
                            cnt_bit = 7;      // 데이터 전송 준비
                            next_state = RD_ACK; // ACK 비트 수신
                        end else cnt_bit = cnt_bit - 1;
                    end
                end
                // ------------------------------------------------------
                // RD_ACK: 슬레이브의 ACK 비트 수신
                //   송신 시 SDA를 high-Z(‘z’)로 두고, rising edge에서 ACK 감지
                // ------------------------------------------------------
                RD_ACK: begin
                    if (scl_nedge) sda = 'bz; // 버스 개방(ACK 받아야 함)
                    else if (scl_pedge) begin
                        if (stop_flag) begin  // 마지막 ACK(데이터 후)면 Stop으로
                            stop_flag = 0;
                            next_state = SCL_STOP;
                        end else begin        // 아니면 데이터 송신
                            stop_flag = 1;
                            next_state = SEND_DATA;
                        end
                    end
                end
                // ------------------------------------------------------
                // SEND_DATA: 8비트 데이터(MSB부터) 전송
                // ------------------------------------------------------
                SEND_DATA: begin
                    if (scl_nedge) sda = data[cnt_bit];
                    if (scl_pedge) begin
                        if (cnt_bit == 0) begin
                            cnt_bit = 7;          // 비트 인덱스 리셋
                            next_state = RD_ACK;  // 데이터 후 ACK
                        end else cnt_bit = cnt_bit - 1;
                    end
                end
                // ------------------------------------------------------
                // SCL_STOP: Stop 조건 준비
                //   SDA=0, SCL=1에서 SDA=0→1로 변할 때 Stop 조건
                // ------------------------------------------------------
                SCL_STOP: begin
                    if (scl_nedge) sda = 0;   // SDA low, SCL=0
                    if (scl_pedge) next_state = COMM_STOP;
                end
                // ------------------------------------------------------
                // COMM_STOP: Stop 조건 유지(3us) 후 FSM 리셋(Idle)
                // ------------------------------------------------------
                COMM_STOP: begin
                    if (count_usec5 >= 3) begin // 약 3us(짧은 여유시간)
                        scl_e = 0;  // SCL 동작 정지
                        sda = 1;    // SDA high(버스 idle)
                        next_state = IDLE;
                    end
                end
            endcase
        end
    end

endmodule

사용 코드 _XDC

# Basys3 XDC (I2C LCD 컨트롤러 - 최소 신호 버전)

# Clock
set_property -dict { PACKAGE_PIN W5 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]

# 7 Segment Display
set_property -dict { PACKAGE_PIN W7  IOSTANDARD LVCMOS33 } [get_ports {seg_7[7]}]
set_property -dict { PACKAGE_PIN W6  IOSTANDARD LVCMOS33 } [get_ports {seg_7[6]}]
set_property -dict { PACKAGE_PIN U8  IOSTANDARD LVCMOS33 } [get_ports {seg_7[5]}]
set_property -dict { PACKAGE_PIN V8  IOSTANDARD LVCMOS33 } [get_ports {seg_7[4]}]
set_property -dict { PACKAGE_PIN U5  IOSTANDARD LVCMOS33 } [get_ports {seg_7[3]}]
set_property -dict { PACKAGE_PIN V5  IOSTANDARD LVCMOS33 } [get_ports {seg_7[2]}]
set_property -dict { PACKAGE_PIN U7  IOSTANDARD LVCMOS33 } [get_ports {seg_7[1]}]
set_property -dict { PACKAGE_PIN V7  IOSTANDARD LVCMOS33 } [get_ports {seg_7[0]}]
set_property -dict { PACKAGE_PIN U2  IOSTANDARD LVCMOS33 } [get_ports {com[0]}]
set_property -dict { PACKAGE_PIN U4  IOSTANDARD LVCMOS33 } [get_ports {com[1]}]
set_property -dict { PACKAGE_PIN V4  IOSTANDARD LVCMOS33 } [get_ports {com[2]}]
set_property -dict { PACKAGE_PIN W4  IOSTANDARD LVCMOS33 } [get_ports {com[3]}]

# Buttons
set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports reset_p]
set_property -dict { PACKAGE_PIN T18 IOSTANDARD LVCMOS33 } [get_ports btn[0]]
set_property -dict { PACKAGE_PIN W19 IOSTANDARD LVCMOS33 } [get_ports btn[1]]
set_property -dict { PACKAGE_PIN T17 IOSTANDARD LVCMOS33 } [get_ports btn[2]]
set_property -dict { PACKAGE_PIN U17 IOSTANDARD LVCMOS33 } [get_ports btn[3]]

# I2C (Pmod Header JA1/JA2)
set_property -dict { PACKAGE_PIN J1 IOSTANDARD LVCMOS33 } [get_ports {scl}]
set_property -dict { PACKAGE_PIN L2 IOSTANDARD LVCMOS33 } [get_ports {sda}]

# Board Configuration
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
set_property BITSTREAM.CONFIG.CONFIGRATE 33 [current_design]
set_property CONFIG_MODE SPIx4 [current_design]

보드 연결

동작 영상

결과

1. 시스템 리셋/전원 투입

  • FPGA 리셋(혹은 전원 인가) 시 LCD는 자동으로 약 80ms 대기 후 초기화 시퀀스(4비트 모드 세팅, 클리어, 커서 모드 등)가 FSM에 의해 자동 수행됨.
  • 이 과정에서 LCD 화면이 잠깐 깜빡이거나 클리어되는 현상을 확인 할 수 있다.

2. 초기화 이후(버튼 반응)

  • 4개의 버튼을 누르면 각각 아래와 같은 동작이 LCD에 출력되는 것을 확인 할 수 있다.

3. 버튼별 동작 결과

  • 버튼 0:
    • 누를 때마다 LCD에 ‘0’, ‘1’, …, ‘9’ 순으로  숫자가 출력.
    • 다시 누르면 0부터 반복.
  • 버튼 1:
    • 누를 때마다 LCD 화면 전체가 오른쪽으로 한 칸씩 이동.
    • 문자 위치도 함께 오른쪽으로 이동.
  • 버튼 2:
    • 누를 때마다 LCD 화면 전체가 왼쪽으로 한 칸씩 이동.
  • 버튼 3:
    • 누르면 LCD에 "HELLO"라는 문자열이 한 글자씩 순서대로 출력.

4. I2C 신호 및 디버깅

  • 동작 중 debug_led 등의 신호를 통해 각 상태(초기화, 송신, 대기 등)를 LED로 확인할 수 있다.

5. 동작 영상 결과

  • btn[0]_(T18)을 누르면 숫자가 차례대로 나타나는 것을 확인 할 수 있다.
  • btn[3]_(U17)을 누르면 "HELLO"가 LCD에 출력되는 것을 확인 할 수 있다.
  • btn[1]_(W19)을 누르면 LCD에 출력된 숫자와 글자가 왼쪽으로 한칸씩 Shift되는 것을 확인 할 수 있다.
  • btn[2]_(T17)을 누르면 LCD에 출력된 숫자와 글자가 오른쪽으로 한칸씩 Shift되는 것을 확인 할 수 있다.
  • btn[reset_p]_(U18)을 누르면 LCD에 출력된 숫자와 글자가 초기화되어 처음 아무것도 출력되지 않은 상태가 되는 것을 확인 할 수 있다.

'Verilog_RTL 설계 > Basys_3' 카테고리의 다른 글

[PWM]LED_128_Step  (2) 2025.07.17
[FSM] Ultrasonic_sensor_(HC_SR04)  (0) 2025.07.16
[FSM] DTH11_Sensor  (0) 2025.07.14
[Sequential_Circuit] edge_detector  (0) 2025.07.13
[Shift_Register] PIPO_(Parallel-In Parallel-Out)  (0) 2025.07.12