소스코드
https://github.com/MainForm/STM32_LCD_I2C
참고 문서
PCF8574 datasheet
1602A datasheet
개요
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 파일 설정
I2C을 사용하기 위해 사용할 I2C를 선택한후 Diable를 I2C로 바꾸어줍니다.
Parameter Settings에 있는 설정은 기본값 그대로 사용합니다.
2. Header 및 Source 파일 추가
이번 실습에서는 C++를 사용하여 LCD를 사용하기 위한 클래스를 개발할 예정입니다.
그러므로 프로젝트의 언어를 C++로 변환 하드록 하겠습니다.
이제 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 데이터 출력 방법
위 그림에서 알 수 있듯이 먼저 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 수신
위 그림에서 중요한 사실은 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는 기본적으로 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 초기화 절차
위 절차를 그대로 코드로 옮겼습니다.
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 함수
문자 하나를 출력하는 명령어는 다음과 같다. 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로 변경합니다.
LCD_I2C.h를 include합니다.
#include "LCD_I2C.h"
lcd 객체를 생성합니다.
lcd를 초기화 합니다.
5.1. PFC8574의 Address 확인
I2C를 통해 Slave Device와 통신을 하기 위해서는 먼저 각 Slave Device가 가지고 있는 고유한 Address가 있어야 합니다. 같은 I2C 통신내 Address가 중복되지 않도록 해야 하므로 만약 여러개의 LCD를 사용중이라면 Address가 달라지도록 설정해주어야 합니다. 그러니 먼저 우리가 사용할 LCD의 Address를 확인하도록 하겠습니다. 먼저, 육안으로 PCF8574을 확인하면 아래와 같은 부분이 있습니다.
A0, A1, A2는 I2C의 주소를 설정하는 PCF8574의 pin입니다. 해당 pin의 상태는 기본적으로 High이지만 위아래를직접 납땜하여 연결하면 해당 pin의 상태가 Low로 변하게 됩니다.
현재 모든 pin이 연결되어 있지 않으므로 주소가 0x27이라는 것을 확인 할 수 있습니다. 하지만, I2C의 통신 방법을 보게 된다면 0번 비트는 R/W(Read/Write) 를 위한 bit이고 1번 bit 부터 주소가 시작되는 것을 확인 할 수 있습니다. 그러므로 실제 프로그래밍을 할 때는 왼쪽으로 1비트 쉬프트 해주어야 합니다.
5.2 Hello world 출력
'Embedded > STM32' 카테고리의 다른 글
[STM32] GPIO로 버튼 및 LED 입출력 실습 (0) | 2025.04.05 |
---|---|
[PlatformIO] STM32를 아두이노처럼 개발 (0) | 2025.03.26 |
[STM32] 간략한 Timer 사용 방법(1ms 주기 설정) (0) | 2025.02.16 |
[STM32] 인터럽트를 이용한 echo UART 통신 (0) | 2024.09.13 |
[STM32] C++에서 printf() 함수 사용 (0) | 2024.09.12 |