반도체, 임베디드 Study/STM32

STM32 - LCD 문자 출력 (I2C)

잇(IT) 2024. 6. 9. 18:39
728x90

https://insoobaik.tistory.com/626

 

STM32 - I2C 이론

보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.

insoobaik.tistory.com

I2C 이론에 이어 I2C 통신을 이용하여 LCD에 문자를 출력해볼 것이다.


LCD에 문자를 출력하기 위해 아두이노 LCM1602 IIC 쉴드를 사용할 것이다. 해당 쉴드에는 PCF8574가 내장되어 있다.

PCF8574는 I2C 통신 프로토콜을 이용하여 데이터를 주고 받는다.

 

Slave 주소를 정하는 7bit는 앞에 0100은 고정이며 A0, A1, A2에 의해 Slave가 정해진다.

슬레이브 주소를 정하는 A0, A1, A2의 경우 Pull Up 저항이 달려있기 때문에 A0, A1, A2에는 1의 값이 전달된다.

코드를 생성하게 되면 I2C1_Init() 함수가 생성되고, 위와 같이 Address를 7bit로 받는 등 설정사항들이 추가된다. 

위 DataSheet는 LCM1602IICShield이며, PCF8574를 내장하기 때문에 A0, A1, A2에 의해 Slave 주소가 결정되는 것을 확인했다.

즉, 위 Shield는 Slave이며 주소는 01001110(0x4e)인 것을 알 수 있다.

 

https://github.com/afiskon/stm32-i2c-lcd-1602

위 LCM1602 모듈 예제를 참고하여 LCD를 문자 출력 코드를 작성하였다.

 

lcd.c

#include "lcd.h"

#include "string.h"

extern I2C_HandleTypeDef hi2c1;
extern UART_HandleTypeDef huart3;

#define LCD_ADDR (0x27 << 1)

#define PIN_RS    (1 << 0)
#define PIN_EN    (1 << 2)
#define BACKLIGHT (1 << 3)

#define LCD_DELAY_MS 5

void I2C_Scan() {
    char info[] = "Scanning I2C bus...\r\n";
    HAL_UART_Transmit(&huart3, (uint8_t*)info, strlen(info), HAL_MAX_DELAY);

    HAL_StatusTypeDef res;
    for(uint16_t i = 0; i < 128; i++) {
        res = HAL_I2C_IsDeviceReady(&hi2c1, i << 1, 1, 10);
        if(res == HAL_OK) {
            char msg[64];
            snprintf(msg, sizeof(msg), "0x%02X", i);
            HAL_UART_Transmit(&huart3, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
        } else {
            HAL_UART_Transmit(&huart3, (uint8_t*)".", 1, HAL_MAX_DELAY);
        }
    }

    HAL_UART_Transmit(&huart3, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}

HAL_StatusTypeDef LCD_SendInternal(uint8_t lcd_addr, uint8_t data, uint8_t flags) {
    HAL_StatusTypeDef res;
    for(;;) {
        res = HAL_I2C_IsDeviceReady(&hi2c1, lcd_addr, 1, HAL_MAX_DELAY);
        if(res == HAL_OK)
            break;
    }

    uint8_t up = data & 0xF0;
    uint8_t lo = (data << 4) & 0xF0;

    uint8_t data_arr[4];
    data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
    data_arr[1] = up|flags|BACKLIGHT;
    data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
    data_arr[3] = lo|flags|BACKLIGHT;

    res = HAL_I2C_Master_Transmit(&hi2c1, lcd_addr, data_arr, sizeof(data_arr), HAL_MAX_DELAY);
    HAL_Delay(LCD_DELAY_MS);
    return res;
}

void LCD_SendCommand(uint8_t lcd_addr, uint8_t cmd) {
    LCD_SendInternal(lcd_addr, cmd, 0);
}

void LCD_SendData(uint8_t lcd_addr, uint8_t data) {
    LCD_SendInternal(lcd_addr, data, PIN_RS);
}

void LCD_Init(uint8_t lcd_addr) {
    // 4-bit mode, 2 lines, 5x7 format
    LCD_SendCommand(lcd_addr, 0b00110000);
    // display & cursor home (keep this!)
    LCD_SendCommand(lcd_addr, 0b00000010);
    // display on, right shift, underline off, blink off
    LCD_SendCommand(lcd_addr, 0b00001100);
    // clear display (optional here)
    LCD_SendCommand(lcd_addr, 0b00000001);
}

void LCD_SendString(uint8_t lcd_addr, char *str) {
    while(*str) {
        LCD_SendData(lcd_addr, (uint8_t)(*str));
        str++;
    }
}

void init() {
    I2C_Scan();
    LCD_Init(LCD_ADDR);

    // set address to 0x00
    LCD_SendCommand(LCD_ADDR, 0b10000000);
    LCD_SendString(LCD_ADDR, "Baek in soo");

    // set address to 0x40
    LCD_SendCommand(LCD_ADDR, 0b11000000);
    LCD_SendString(LCD_ADDR, "1995 04 17");
}

void loop() {
    HAL_Delay(100);
}

 

위 코드는 LCD 문자열 출력을 위한 전체 코드다 

extern I2C_HandleTypeDef hi2c1;
extern UART_HandleTypeDef huart3;

#define LCD_ADDR (0x27 << 1)

#define PIN_RS    (1 << 0)
#define PIN_EN    (1 << 2)
#define BACKLIGHT (1 << 3)

#define LCD_DELAY_MS 5

hi2c1, huart3는 I2C와 관련된 구조체를 의미한다.

LCD_ADDR은 DataSheet를 통해 확인한 LCD(Slave)의 Address에 해당한다.

PIN_RS(Register Select), PIN_EN(Enable), BACKLIGHT, LCD_DELAY_MS 5는 아래 사용하기 위한 정의다.

 

void I2C_Scan() {
    char info[] = "Scanning I2C bus...\r\n";
    HAL_UART_Transmit(&huart3, (uint8_t*)info, strlen(info), HAL_MAX_DELAY);

    HAL_StatusTypeDef res;
    for(uint16_t i = 0; i < 128; i++) {
        res = HAL_I2C_IsDeviceReady(&hi2c1, i << 1, 1, 10);
        if(res == HAL_OK) {
            char msg[64];
            snprintf(msg, sizeof(msg), "0x%02X", i);
            HAL_UART_Transmit(&huart3, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
        } else {
            HAL_UART_Transmit(&huart3, (uint8_t*)".", 1, HAL_MAX_DELAY);
        }
    }

    HAL_UART_Transmit(&huart3, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}

I2C 버스를 스캔하여 연결된 디바이스를 찾는다.

(위에서 LCD의 주소가 0x4e인데 0x27 << 1로 하는 이유는 마지막 비트는 R/W이기 때문에 R/W를 제외한 7bit address만을 표현하기 위함이다.)

Scan 함수의 HAL_I2C_IsDeviceReady함수를 통해 LCD 장치의 주소를 찾는다.

Scan 함수에 의해 LCD 장치의 주소가 발견된 것을 확인할 수 있다.

 

HAL_StatusTypeDef LCD_SendInternal(uint8_t lcd_addr, uint8_t data, uint8_t flags) {
    HAL_StatusTypeDef res;
    for(;;) {
        res = HAL_I2C_IsDeviceReady(&hi2c1, lcd_addr, 1, HAL_MAX_DELAY);
        if(res == HAL_OK)
            break;
    }

    uint8_t up = data & 0xF0;
    uint8_t lo = (data << 4) & 0xF0;

    uint8_t data_arr[4];
    data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
    data_arr[1] = up|flags|BACKLIGHT;
    data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
    data_arr[3] = lo|flags|BACKLIGHT;

    res = HAL_I2C_Master_Transmit(&hi2c1, lcd_addr, data_arr, sizeof(data_arr), HAL_MAX_DELAY);
    HAL_Delay(LCD_DELAY_MS);
    return res;
}

LCD 내부 전송 함수에 해당하는 코드다. 

 

SendInternal 함수를 통해 Master에게 데이터를 전달하는 코드를 작성하게 된다.

간단하게 설명하자면 LCD는 8비트씩 데이터가 전달되지만 핀 수를 최소화 하기 위해 상위 4비트 하위 4비트씩 데이터를 전달하게 된다.

up, lo를 통해 상위 하위 비트를 나누고, 시프트 레지스터를 통해 전체 8비트의 데이터를 전달한다.

 

해당 데이터를 Master_Transmit 함수를 통해 전달하게 된다.

 

I2C를 통해 데이터 내부에 데이터를 전달하는 구체적인 방식에 대해선 추후에 자세히 작성할 것이다.

 

void LCD_SendCommand(uint8_t lcd_addr, uint8_t cmd) {
    LCD_SendInternal(lcd_addr, cmd, 0);
}

void LCD_SendData(uint8_t lcd_addr, uint8_t data) {
    LCD_SendInternal(lcd_addr, data, PIN_RS);
}

void LCD_Init(uint8_t lcd_addr) {
    // 4-bit mode, 2 lines, 5x7 format
    LCD_SendCommand(lcd_addr, 0b00110000);
    // display & cursor home (keep this!)
    LCD_SendCommand(lcd_addr, 0b00000010);
    // display on, right shift, underline off, blink off
    LCD_SendCommand(lcd_addr, 0b00001100);
    // clear display (optional here)
    LCD_SendCommand(lcd_addr, 0b00000001);
}

void LCD_SendString(uint8_t lcd_addr, char *str) {
    while(*str) {
        LCD_SendData(lcd_addr, (uint8_t)(*str));
        str++;
    }
}

 

SendInternal 즉, 내부에 데이터를 전달하는 함수를 기반으로

SendCommand, SendData, LCD_Init, LCD_SendString / 명령어 전달, 데이터 전달, LCD 초기화, 문자 전달 함수를 작성한다.

 

void init() {
    I2C_Scan();
    LCD_Init(LCD_ADDR);

    // set address to 0x00
    LCD_SendCommand(LCD_ADDR, 0b10000000);
    LCD_SendString(LCD_ADDR, "Baek in soo");

    // set address to 0x40
    LCD_SendCommand(LCD_ADDR, 0b11000000);
    LCD_SendString(LCD_ADDR, "1995 04 17");
}

void loop() {
    HAL_Delay(100);
}

init() 함수를 통해 I2C 통신 가능 여부 확인 및 LCD를 초기화 하고, LCD에 데이터를 전송하게 된다.

728x90