본문 바로가기
Embedded/STM32

[STM32] I2C 통신으로 1602 Character LCD 출력

by Gordon_ 2025. 2. 22.

소스코드

https://github.com/MainForm/STM32_LCD_I2C


참고 문서

PCF8574 datasheet

PCF8574_TI.PDF
1.05MB
PCF8574_PHILIPS.PDF
0.16MB

 

1602A datasheet

CFAH1602A-AGB-JP.PDF
0.25MB


개요

  I2C통신은 2개의 선(SDA, SCL)을 통해 많은 Slave Devices과 통신할 수 있는 Protocol입니다. 앞으로 이 통신으로 EEPROM이나 센서에서 데이터를 송수신하는데 많이 사용하게 될 것입니다. 많은 디바이스를 2개의 선으로 통신할 수 있다는 장점이 있지만 통신속도가 다른 통신에 느릴 수 밖에 없어 만약 속도를 중요시 해야하는 통신의 경우 SPI 통신을 고려해야합니다. 

  이 포스트에서는 I2C의 장점이 가장 또렷하게 드러나는 LCD 출력을 해보려고 합니다. LCD의 경우 작동을 하기 위해서 최소한으로 8pin(데이터 4pin + 제어용 3pin + 백라이트 1pin)이 필요합니다. 하지만 8pin은 MCU의 입장에서는 너무 부담이 될 수 밖에 없습니다. 많이 사용하는 Arduino uno의 경우 13pin 밖에 없는데 이중 8pin를 사용한다면 다른 제품을 제어하기 힘들어 질 수 밖에 없습니다. 이러한 문제점을 해결하고자 I2C통신을 통해 2pin으로 LCD를 제어하는 방법을 소개드리고자 합니다.

 


하드웨어

개발 보드 : Nucleo-F429ZI

LCD : 1602A

I2C를 통한 LCD 제어 : PCF8574

 

하드웨어 개략도

 


1. ioc 파일 설정

CubeMX에서 I2C 설정

I2C을 사용하기 위해 사용할 I2C를 선택한후 Diable를 I2C로 바꾸어줍니다.

Parameter Settings에 있는 설정은 기본값 그대로 사용합니다.

 

2. Header 및 Source 파일 추가

이번 실습에서는 C++를 사용하여 LCD를 사용하기 위한 클래스를 개발할 예정입니다.

그러므로 프로젝트의 언어를 C++로 변환 하드록 하겠습니다.

프로젝트 언어를 C++로 변환

 

이제 Header 파일과 Source 파일을 추가하도록 하겠습니다.

Header 및 Source 파일 추가

3. Header 파일 코딩

#ifndef INC_LCD_I2C_H_
#define INC_LCD_I2C_H_

#include "stm32f4xx_hal.h"

namespace CharacterLCD {
    class LCD_I2C {
    private:
        // static variables
        static const uint8_t LCD_BACKLIGHT = 0x08;
        static const uint8_t LCD_EN = 0x04;
        static const uint8_t LCD_RW = 0x02;
        static const uint8_t LCD_RS = 0x01;

        enum COMMANDS : uint8_t{
            CLEAR_DISPLAY = 0x01,
            RETURN_HOME = 0x02,
            ENTRY_MODE_SET = 0x04,
            DISPLAY_CONTROL = 0x08,
            CURSOR_SHIFT = 0x10,
            FUNCTION_SET = 0x20,
            SET_CGRAM_ADDR = 0x40,
            SET_DDRAM_ADDR = 0x80,
        };

        // variables
        I2C_HandleTypeDef* i2c;
        uint8_t i2cAddress;
        uint8_t cols;
        uint8_t rows;

        uint8_t backlight = 0x08;

        //functions
        void sendData(uint8_t data, uint8_t mode) const;
    public:
        LCD_I2C() = default;
        void initialize(I2C_HandleTypeDef* i2c, uint8_t i2cAddress, uint8_t cols = 16, uint8_t rows = 2);

        void sendCommand(uint8_t command) const;
        void sendChar(uint8_t ch) const;

        void print(const char * str) const;

        void clearDisplay() const;
        void returnHome() const;
        void setCursor(uint8_t col, uint8_t row) const;
    };
}

#endif /* INC_LCD_I2C_H_ */

 

위 클래스는 단순히 LCD 내 문자열을 출력할 수 있는 최소한의 메소드만 제공하도록 하고 있습니다.

추가적인 기능에 대해서는 추후 개발할 예정입니다.

 

각 맴버 함수에 대해서는 직접 구현하면서 설명하도록 하겠습니다.

4. Source 파일 코딩

#include "LCD_I2C.h"

namespace CharacterLCD {
    //public functions
    void LCD_I2C::initialize(I2C_HandleTypeDef* i2c, uint8_t i2cAddress, uint8_t cols, uint8_t rows){
        // Initialize the LCD
        this->i2c = i2c;
        this->i2cAddress = i2cAddress;
        this->cols = cols;
        this->rows = rows;

        HAL_Delay(50);
        sendData(0x03, backlight);
        HAL_Delay(5);
        sendData(0x03, backlight);
        HAL_Delay(1);
        sendData(0x03, backlight);
        HAL_Delay(1);

        // set lcd to 4 bit mode
        sendData(0x02, backlight);
        HAL_Delay(1);

        sendCommand(0x28);
        HAL_Delay(1);

        sendCommand(0x08);
        HAL_Delay(1);

        sendCommand(0x01);
        HAL_Delay(1);

        sendCommand(0x03);
        HAL_Delay(1);

        sendCommand(0x0C);
        HAL_Delay(1);
    }

    void LCD_I2C::sendCommand(uint8_t command) const{
        // Send a command to the LCD

        sendData(command >> 4, backlight);
        sendData(command, backlight);
    }

    void LCD_I2C::sendChar(uint8_t ch) const{
        // Send a charater to the LCD

        sendData(ch >> 4, backlight | LCD_RS);
        sendData(ch, backlight | LCD_RS);
    }

    void LCD_I2C::print(const char * str) const{
        // Print a string to the LCD

        while(*str){
            sendChar(*str);

            ++str;
        }
    }

    void LCD_I2C::clearDisplay() const{
        // Clear the display
        sendCommand(CLEAR_DISPLAY);
        HAL_Delay(2);
    }

    void LCD_I2C::returnHome() const{
        // Return the cursor to the home position
        sendCommand(RETURN_HOME);
        HAL_Delay(2);
    }

    void LCD_I2C::setCursor(uint8_t col, uint8_t row) const{
        // Set the cursor to a specific position

        uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};

        if(row >= rows){
            row = rows - 1;
        }

        sendCommand(SET_DDRAM_ADDR | (col + row_offsets[row]));
    }

    // private functions

    void LCD_I2C::sendData(uint8_t data, uint8_t mode) const{
        // Send 4 bits to the LCD

        uint8_t arr[2] = {0x00,};

        data <<= 4;

        arr[0] = (data | mode) | LCD_EN;
        arr[1] = (data | mode) & ~LCD_EN;

        HAL_I2C_Master_Transmit(i2c, i2cAddress, arr, 2,100);
    }
}

4.1 sendData함수

  LCD를 사용하기 위해서는 먼저 MCU에서 LCD에 데이터를 보내는 방법에 대해서 살펴보아야 한다. 

  4.1.1 PCF8574 데이터 출력 방법

데이터 흐름에 대한 개략도
PCF8574에서 I2C로 입력된 데이터를 출력 하는 방법

  위 그림에서 알 수 있듯이 먼저 STM32 Board에서 I2C 통신으로 Data를 PCF8574에 송신하게 됩니다. 하지만 해당 데이터가 실제로 LCD에 송신 되기 위해서는 DATA 이후 ACK(Acknowlege) bit이후 LCD에 데이터가 송신되는 것을 확인 할 수 있습니다.

arr[0] = (data | mode) | LCD_EN;
arr[1] = (data | mode) & ~LCD_EN;

//PCF8574에 2개의 Data 송신
HAL_I2C_Master_Transmit(i2c, i2cAddress, arr, 2,100);

  위 코드에서는 DATA1이 arr[0]에 해당하고 DATA2가 arr[1]에 해당합니다. 그러므로 arr 배열 데이터가 순차적으로 DATA OUT으로 출력 됨을 알 수 있습니다.

 

4.1.2 LCD에서 Data 수신 

DATA OUT 각 비트의 목적
E 신호에 따라 LCD에 Data 수신

  위 그림에서 중요한 사실은 E 신호가 High에서 Low로 바뀌는 순간에 DB에 대한 데이터를 수신한다는 것입니다. 그러므로 우리는 먼저 DB에 보낼 데이터를 보낸 다음 DB에 데이터를 유지한 채 E 신호를 High에서 Low로 변경해야 한다는 것입니다. 

arr[0] = (data | mode) | LCD_EN;  // EN을 High로 한채 DB에 데이터 전송
arr[1] = (data | mode) & ~LCD_EN; // DB에 데이터를 유지한 채로 EN를 Low로 변경

  위 코드를 통행 data의 내용은 유지한채 LCD_EN를 OR하여 E 신호를 High로 한후 AND하여 LCD_EN만을 Low로 변경하였습니다.

  여기서 mode변수는 LCD의 백라이트나 RS 및 RW를 제어하기 위한 변수입니다.

 

4.2 Initialize 함수

이제 LCD를 사용하기 전 필요한 절차를 통해 LCD를 초기화 하도록 하겠습니다.

4.2.1 sendCommand 함수

LCD 제어 명령어

  LCD는 기본적으로 8bit로 제어하게 됩니다. 하지만 상위 4bit 하위 4bit으로 나누어 4bit을 두번 송신하여 8bit처럼 제어할 수 있습니다. DB7~DB4를 먼저 보내고 그다음 DB3~DB0를 보내어 제어하는 sendCommand 함수를 살펴보겠습니다.

void LCD_I2C::sendCommand(uint8_t command) const{
	// Send a command to the LCD

	//DB7~DB4 송신
	sendData(command >> 4, backlight);
	//DB3~DB0 송신
	sendData(command, backlight);
}

  command 변수를 오른쪽으로 4번 쉬프트 하여 상위 4비트를 옮겨주고 송신합니다. 그 다음 command를 그대로 송신합니다. 결국 sendData는 하위 4비트만 LCD에 보내기 때문에 상위 비트를 0으로 변경하지 않아도 상관없습니다.

 

4.2.2 LCD 초기화 절차

4bit Interface를 사하기 위한 초기화 절차

 

위 절차를 그대로 코드로 옮겼습니다.

        HAL_Delay(50);
        sendData(0x03, backlight);
        HAL_Delay(5);
        sendData(0x03, backlight);
        HAL_Delay(1);
        sendData(0x03, backlight);
        HAL_Delay(1);

        // set lcd to 4 bit mode
        sendData(0x02, backlight);
        HAL_Delay(1);
        
        sendCommand(0x28);
        HAL_Delay(1);

        sendCommand(0x08);
        HAL_Delay(1);

        sendCommand(0x01);
        HAL_Delay(1);

        sendCommand(0x03);
        HAL_Delay(1);

        sendCommand(0x0C);
        HAL_Delay(1);

  HAL_Delay를 사이사이에 넣어논 이유는 해당 명령을 수행하는 Executtion Time이 있기에 Delay를 주어 LCD에서 해당 명령어를 수행 완료 할때까지 기다리는 것입니다.

 

4.3 sendChar 함수

LCD에 문자를 출력하기 위한 명령어 우

문자 하나를 출력하는 명령어는 다음과 같다. RS를 High로 한 후 ASCII 코드를 DB로 전달하면 됩니다.

    void LCD_I2C::sendChar(uint8_t ch) const{
        // Send a charater to the LCD
		
        // LCD_RS를 or로 하여 RS 신호를 High 설정
        sendData(ch >> 4, backlight | LCD_RS);
        sendData(ch, backlight | LCD_RS);
    }

4.4 print 함수

문자열 끝을 의미하는 null 문자가 나올 때까지 sendChar함수를 출력하는 함수입니다.

    void LCD_I2C::print(const char * str) const{
        // Print a string to the LCD

        while(*str){
            sendChar(*str);

            ++str;
        }
    }

 

 

5. LCD를 테스트 하기 위한 코딩

  지금까지 개발한 LCD_I2C 클래스를 사용하기 위해 main 소스파일도 c++로 빌드해야합니다. 그러기 위해 main 소스 파일의 확장자를 .cpp로 변경합니다.

main.cpp로 확장자 변경

 

LCD_I2C.h를 include합니다.

LCD_I2C 헤더 추가

#include "LCD_I2C.h"

 

lcd 객체를 생성합니다.

lcd 객체 생성

lcd를 초기화 합니다.

 

5.1. PFC8574의 Address 확인

  I2C를 통해 Slave Device와 통신을 하기 위해서는 먼저 각 Slave Device가 가지고 있는 고유한 Address가 있어야 합니다. 같은 I2C 통신내 Address가 중복되지 않도록 해야 하므로 만약 여러개의 LCD를 사용중이라면 Address가 달라지도록 설정해주어야 합니다. 그러니 먼저 우리가 사용할 LCD의 Address를 확인하도록 하겠습니다. 먼저, 육안으로 PCF8574을 확인하면 아래와 같은 부분이 있습니다.

 

PCF8574의 Address설정을 위한 부분 확인

 

  A0, A1, A2는 I2C의 주소를 설정하는 PCF8574의 pin입니다. 해당 pin의 상태는 기본적으로 High이지만 위아래를직접 납땜하여 연결하면 해당 pin의 상태가 Low로 변하게 됩니다.

PFC8574의 주소

 현재 모든 pin이 연결되어 있지 않으므로 주소가 0x27이라는 것을 확인 할 수 있습니다. 하지만, I2C의 통신 방법을 보게 된다면 0번 비트는 R/W(Read/Write) 를 위한 bit이고 1번 bit 부터 주소가 시작되는 것을 확인 할 수 있습니다. 그러므로 실제 프로그래밍을 할 때는 왼쪽으로 1비트 쉬프트 해주어야 합니다.

I2C Address 구조

 

 

5.2 Hello world 출력