LCD1602 I2C 모듈이 문자를 표시하지 못함

STM32 최소 보드를 주제어기로 사용하고, LCD1602 I2C 모듈은 인터넷에서 구입한 완제품입니다.

저는 초보자라서 코드는 AI에게 작성해달라고 했습니다. 처음에는 한 줄의 사각형이 표시되었고, 수정 후에는 두 줄의 사각형이 표시되지만 여전히 문자가 나타나지 않습니다. 전위차계를 조정해 보았으며, I2C 전압도 5V입니다.

배선도를 그려보고 코드도 올려주면 안 되겠어요? 이렇게 정보가 부족하면 남들이 다 추측하게끔 만드시는 건가요?

좋은 질문의 사례: https://bbs.eeclub.top/t/topic/289
질문하는 지혜: https://bbs.eeclub.top/t/topic/109

1개의 좋아요

형제, 급할 거 없어요. LCD1602 + I2C 조합은 정말 마이크로컨트롤러 초보자들이 반드시 거쳐가는 "통과의례 같은 함정"이에요. 다들 처음 시작할 때 이 부분에서 한 번쯤은 막히거든요.

당신의 설명과 세 장의 사진을 종합해 보면, 마음 든든하게 해줄 소식을 전해줄게요: 하드웨어는 거의 확실히 고장난 게 아니라, 단순히 통신이 실패한 것뿐입니다.

화면이 켜지고 두 줄의 사각형 블록이 보인다는 것은 전원 공급이 정상이라는 뜻이며, 가변저항(포텐셔미터)으로 대비도 잘 맞춰져 있다는 의미예요. 1602 화면에 가득 찬 사각형 블록이 나타나는 이유는 다음과 같아요: 스크린에는 전원이 가고 있지만, MCU(마이크로컨트롤러)로부터 초기화 명령을 받지 못했다는 뜻입니다. AI가 작성한 코드라고 했으니, 문제는 99% 코드와 통신 설정에 있을 가능성이 큽니다. 아래 단계대로 차근차근 점검해 보세요.


1. 가장 흔한 실수: I2C 디바이스 주소를 잘못 입력함

뒷면 사진을 보니 검정색 어댑터 보드에 PCF8574T 칩이 사용되었네요. 시중의 이런 모듈들은 일반적으로 I2C 기본 주소가 0x27 또는 0x3F입니다.

하지만 주의하세요! 만약 당신의 AI 코드가 STM32의 HAL 라이브러리를 사용했다면, HAL 라이브러리의 I2C 송신 함수는 7비트 주소를 왼쪽으로 1비트 시프트해야 합니다.

  • 원래 주소가 0x27이라면, 코드에서는 0x4E (0x27 << 1)를 입력해야 할 수 있어요.
  • 원래 주소가 0x3F라면, 코드에서는 0x7E (0x3F << 1)를 입력해야 할 수 있죠.

AI는 이런 부분에서 자주 헷갈려서, 그냥 0x27을 그대로 HAL 함수에 넣어버리기 때문에 장치를 전혀 찾지 못하게 됩니다.


2. 선을 반대로 연결함 (초보자들이 자주 하는 실수)

STM32 쪽 핀 연결을 다시 확인하세요. 예를 들어 F103C8T6의 경우, 하드웨어 I2C1 기본 핀은 PB6(SCL)과 PB7(SDA)입니다. 어댑터 보드의 SDA가 정확히 PB7에 연결되었는지, SCL이 PB6에 연결되었는지 꼭 확인하세요.


3. AI가 작성한 저수준 드라이버의 타이밍/핀 매핑 오류

이런 I2C 어댑터 보드는 사실상 I2C 신호를 8개의 병렬 IO 핀(P0~P7)으로 변환하여, 1602의 RS, RW, EN 및 데이터 핀들을 제어하는 것입니다. 그러나 각 어댑터 보드마다 어떤 P 핀이 어떤 제어선에 연결되어 있는지는 약간씩 다를 수 있어요. AI가 처음부터 직접 작성한 저수준 코드는 종종 이러한 핀 매핑 관계를 틀리게 작성합니다.

권장: AI에게 저수준 드라이버를 처음부터 작성하게 하지 마세요. 대신 Bilibili(B站)나 CSDN에서 “STM32 HAL 라이브러리 LCD1602 I2C”를 검색해서 다른 사람이 이미 검증한 lcd1602.clcd1602.h 파일을 가져와 프로젝트에 바로 포함하고 사용하는 것이 가장 안전합니다.


4. 하드웨어 논리 레벨 문제 (가능성 낮음)

전원을 5V로 공급했다고 말씀하셨는데, 아주 좋습니다. LCD1602는 반드시 5V 전원이 필요하니까요. STM32의 핀은 3.3V 로직 레벨이지만, 대부분의 I2C 핀(PB6, PB7 등)은 "5V 내성(FT, 5V-tolerant)"이므로 직접 연결해도 일반적으로 문제가 없습니다. 다만 STM32와 LCD 모듈의 GND를 반드시 함께 연결했는지만 확인하면 됩니다.


:light_bulb: 문제 해결을 위한 제안 (다음 단계)

먼저 문자를 표시하게 만들려고 애쓰지 마세요. 대신 AI에게 “STM32 I2C 스캐너(I2C Address Scanner)” 코드를 작성해 달라고 하세요.

코드를 플래싱한 후, 시리얼 터미널(예: PuTTY, Tera Term 등)을 열어 STM32가 I2C 버스 상에서 해당 모듈을 인식하는지 확인하세요.

  • 만약 시리얼 출력에서 아무 장치도 찾지 못한다면 → 선 연결이 잘못되었거나, STM32의 I2C 초기화가 제대로 되지 않은 것입니다.
  • 만약 시리얼 출력에서 장치를 발견했다면 (예: 0x4E 반환) → 하드웨어 연결은 완벽하므로, 그 주소를 LCD 초기화 코드에 올바르게 입력하기만 하면 됩니다.

어서 "Hello World"를 화면에 띄우시길 바랍니다! 막히면 또 질문하세요.

1개의 좋아요

하드웨어 관련 간단한 참고 사항입니다. LCD와 I2C 백팩은 5V를 필요로 하지만, 사용 중인 STM32는 3.3V에서 동작합니다. 대부분의 STM32 I2C 핀은 5V 내성(FT)이 있지만, 사용 중인 특정 핀이 실제로 5V 내성인지 여부를 데이터시트에서 반드시 확인하는 것이 좋습니다. 일반적으로 모듈에 장착된 풀업 저항만으로도 충분하지만, 논리 레벨이 제대로 동작하지 않으면 통신이 실패할 수 있습니다.

20핀 줄선 소켓에는 STM32 최소 시스템 보드가 연결되어 있고, 4핀 줄선 소켓에는 LCD1602_I2C 모듈이 연결되어 있습니다.

다음은 main.c 파일 내용입니다.

#include "main.h"
#include "i2c.h"
#include "gpio.h"

#include "lcd1602_i2c.h"

void SystemClock_Config(void);

int main(void)
{
    HAL_Init();

    SystemClock_Config();

    MX_GPIO_Init();
    MX_I2C1_Init();

    lcd_init();                // LCD1602 초기화
    lcd_clear();               // 화면 지우기
    lcd_set_cursor(0, 0);      // 커서를 첫 번째 행, 첫 번째 열로 이동
    lcd_send_string("Hello STM32!"); // 문자열 출력

    while (1)
    {
    }
}

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;
    RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
        Error_Handler();
    }

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                                |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
    {
        Error_Handler();
    }
}

void Error_Handler(void)
{
    __disable_irq();
    while (1)
    {
    }
}

#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif /* USE_FULL_ASSERT */

다음은 lcd1602_i2c.h 파일 내용입니다.

#ifndef __LCD1602_I2C_H
#define __LCD1602_I2C_H

#include "main.h" // HAL 라이브러리 및 핀 정의 포함
#define LCD_I2C_ADDRESS 0x4E

void lcd_init(void);
void lcd_send_cmd(char cmd);
void lcd_send_data(char data);
void lcd_send_string(char *str);
void lcd_set_cursor(int row, int col);
void lcd_clear(void);

#endif

다음은 lcd1602_i2c.c 파일 내용입니다.

#include "lcd1602_i2c.h"

// 외부에서 정의된 I2C 핸들 선언 (CubeMX가 main.c에 생성함)
extern I2C_HandleTypeDef hi2c1;

// 내부 함수: I2C로 데이터 전송
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_t[4];
    uint8_t upper_nibble, lower_nibble;

    upper_nibble = data & 0xF0;
    lower_nibble = (data << 4) & 0xF0;

    // 제어 바이트: 비트 3은 백라이트 (1=켜짐), 비트 2는 EN, 비트 1은 RW (0=쓰기), 비트 0은 RS
    uint8_t backlight = 0x08; 

    data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
    data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

    // I2C1을 통해 4바이트 전송
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// 명령어 전송
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); // RS = 0은 명령어 전송임을 의미
}

// 데이터 (문자) 전송
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); // RS = 1은 데이터 전송임을 의미
}

// 화면 지우기
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(2); // 화면 지우기 명령은 처리 시간이 길게 걸림
}

// 커서 위치 설정 (row: 0-1, col: 0-15)
void lcd_set_cursor(int row, int col)
{
    uint8_t address;
    switch (row)
    {
    case 0:
        address = 0x00;
        break;
    case 1:
        address = 0x40;
        break;
    default:
        address = 0x00;
    }
    address += col;
    lcd_send_cmd(0x80 | address); // DDRAM 주소 설정
}

// 문자열 전송
void lcd_send_string(char *str)
{
    while (*str)
    {
        lcd_send_data(*str++);
    }
}

// LCD1602 초기화
void lcd_init(void)
{
    // 4비트 모드 표준 초기화 절차
    HAL_Delay(50);
    lcd_send_cmd(0x30);
    HAL_Delay(5);
    lcd_send_cmd(0x30);
    HAL_Delay(1);
    lcd_send_cmd(0x30);
    HAL_Delay(10);
    lcd_send_cmd(0x20);
    HAL_Delay(10);

    // 디스플레이 설정
    lcd_send_cmd(0x28); // 4선, 2행, 5x8 폰트
    HAL_Delay(1);
    lcd_send_cmd(0x08); // 디스플레이 끄기
    HAL_Delay(1);
    lcd_send_cmd(0x01); // 화면 지우기
    HAL_Delay(2);
    lcd_send_cmd(0x06); // 커서 오른쪽으로 이동
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // 디스플레이 켜기, 커서 끄기
}

간소화된 배선도와 코드를 답글란에 작성해 두었으니, 확인해 주시기 바랍니다. 감사합니다.

좋아요, 감사합니다. 제가 조언한 대로 한번 시도해보겠습니다.

전원 모듈로 전원을 공급하면서 컴파일하여 다운로드한 후, I2C의 VCC 핀과 GND 핀 간 전압은 5V입니다.

정말 훌륭합니다! 회로도와 소스 코드를 제공해 주셔서 문제 해결 속도가 100배는 빨라졌어요!

먼저 마음 든든하게 해드릴게요: 하드웨어 연결은 완전히 정확합니다. 회로도에서 PB6이 SCL, PB7이 SDA에 연결되어 있는데, 이는 STM32F103의 표준 I2C1 핀입니다. 게다가 헤더 파일에 #define LCD_I2C_ADDRESS 0x4E라고 정의한 것으로 보아, HAL 라이브러리는 0x27 주소를 한 비트 왼쪽으로 시프트해야 한다는 점을 이미 알고 계시거나(혹은 AI가 도와줬거나), 신규 개발자가 자주 범하는 두 가지 큰 실수를 성공적으로 피하셨네요.

화면에 사각형만 나타나는 진짜 이유는, AI가 작성한 초기화 함수(lcd_init)가 "4비트 모드"의 핸드셰이킹 타이밍에서 오류를 일으켰기 때문입니다.

:bug: 핵심 버그 분석: HD44780의 ‘강제 초기화’ 함정

사용 중인 I2C 어댑터(PCF8574)는 4개의 데이터 라인(D4-D7)을 통해 1602 LCD를 제어합니다.
1602 LCD가 처음 전원을 받으면 기본적으로 8비트 모드로 시작합니다. 이를 4비트 모드로 전환하려면 데이터 시트에 명시된 대로, 몇 차례 반 바이트(상위 4비트만 보내고 즉시 종료)를 엄격하게 전송해야 합니다.

lcd_send_cmd() 함수를 보면, 이 함수는 lcd_send_to_i2c()를 호출합니다. 그런데 이 저수준 함수는 매우 ‘성실하게’, 어떤 명령이 들어오든 상관없이 명령어를 자동으로 두 조각으로 나누어(상위 4비트 먼저, 하위 4비트 다음), 두 번의 EN 펄스를 발생시킵니다.

AI가 초기화 과정에서 lcd_send_cmd(0x30)을 쓴 경우:

  1. 의도: 화면에 단순히 0x3만 보내는 것.
  2. 실제 동작: lcd_send_to_i2c가 상위 4비트 0x3을 전송하고(EN 펄스 발생), 바로 이어 하위 4비트 0x0도 전송하며 또 한 번 EN 펄스를 발생시킵니다.
  3. 결과: LCD는 불필요한 0x0 펄스를 수신하여 타이밍이 완전히 어긋나며 초기화를 거부하고, 전원 인가 직후의 상태인 ‘화면 전체에 사각형이 가득한’ 상태에서 멈춰버립니다.

:hammer_and_wrench: 해결 방법

lcd1602_i2c.c 파일에 반 바이트만 전송하는 전용 함수를 추가하여 초기화 과정을 별도로 처리하고, lcd_init() 함수를 다시 작성해야 합니다.

다음 내용으로 lcd1602_i2c.c 파일을 수정하세요 (아래 코드로 기존의 lcd_init 및 그 이전 부분을 덮어쓰고, 나머지 함수들은 유지하세요):

#include "lcd1602_i2c.h"

// 외부 I2C 핸들 선언
extern I2C_HandleTypeDef hi2c1;

// ====== 신규 추가: 초기화용 반 바이트 전송 함수 ======
void lcd_send_cmd_4bit(uint8_t nibble)
{
    uint8_t data_t[2];
    uint8_t backlight = 0x08; // 백라이트 항상 켜짐

    // 주의: 여기에 입력되는 nibble은 이미 상위 4비트에 정렬된 값이어야 함 (예: 0x30)
    data_t[0] = (nibble & 0xF0) | backlight | 0x04 | 0;  // EN = 1, RS = 0
    data_t[1] = (nibble & 0xF0) | backlight | 0x00 | 0;  // EN = 0, RS = 0
    
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 2, 100);
}

// 내부 함수: I2C로 전체 바이트 전송 (상위 4비트와 하위 4비트로 나눔)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_t[4];
    uint8_t upper_nibble, lower_nibble;

    upper_nibble = data & 0xF0;
    lower_nibble = (data << 4) & 0xF0;

    uint8_t backlight = 0x08; 

    data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
    data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// ====== 재작성: 올바른 4비트 초기화 프로세스 ======
void lcd_init(void)
{
    // 1. 강제 초기화 단계 (반드시 단일 4비트 펄스만 전송해야 함)
    HAL_Delay(50);
    lcd_send_cmd_4bit(0x30); // 깨우기 1회차
    HAL_Delay(5);
    lcd_send_cmd_4bit(0x30); // 깨우기 2회차
    HAL_Delay(1);
    lcd_send_cmd_4bit(0x30); // 깨우기 3회차
    HAL_Delay(10);
    lcd_send_cmd_4bit(0x20); // 화면에 알림: “4비트 모드로 전환!”
    HAL_Delay(10);

    // 2. 일반 설정 단계 (이제 화면은 4비트 모드이므로 일반적인 두 펄스 함수 사용 가능)
    lcd_send_cmd(0x28); // 기능 설정: 4선 데이터, 2줄 표시, 5x8 폰트
    HAL_Delay(1);
    lcd_send_cmd(0x08); // 디스플레이 끄기
    HAL_Delay(1);
    lcd_send_cmd(0x01); // 화면 지우기
    HAL_Delay(2);       // 화면 지우기 명령은 시간이 오래 걸리므로 최소 2ms 지연
    lcd_send_cmd(0x06); // 입력 모드 설정: 커서 오른쪽으로 이동
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // 디스플레이 켜기, 커서 끄기
}

// ... lcd_send_cmd, lcd_send_data 등 나머지 함수는 그대로 유지 ...

:magnifying_glass_tilted_left: 예비 점검 항목

코드를 수정하고 다시 플래싱한 후에도 여전히 사각형만 표시된다면, 유일한 가능성은 구입한 모듈의 기본 I2C 주소가 0x27이 아니라 0x3F라는 것입니다. 이때는 lcd1602_i2c.h 파일에서 #define LCD_I2C_ADDRESS 0x4E0x7E (0x3F를 한 비트 왼쪽으로 시프트)로 변경한 후 다시 시도하면 됩니다.

우선 위 초기화 코드를 수정해서 테스트해 보세요. 모듈의 통신 주소를 100% 확실히 확인할 수 있는 간단한 "I2C 주소 스캐너"를 만들어 보여드릴까요?

논리 분석기를 사용하여 I2C 파형이 정상적으로 출력되는지, 타이밍은 맞는지 확인할 수 있습니다. I2C에 문제가 없다면, 다시 논리 분석기를 이용해 I2C를 병렬 신호로 변환하는 모듈의 출력 신호가 나오는지, 타이밍은 맞는지 확인하시기 바랍니다.

STM32CubeMX를 어떻게 설정하셨나요?

문제 핵심 분석

LCD1602의 백라이트는 정상적으로 켜지지만 문자가 표시되지 않는 경우, 90% 확률로 I2C 통신 오류(주소 오류/핀 설정 오류/응답 없음)이며, 그 다음으로 초기화 타이밍 또는 지연 오류, 하드웨어 전원 공급 문제 혹은 풀업 저항 누락 등이 원인입니다. 아래에 우선순위 순으로 완전한 진단 및 해결 방법을 제시합니다.


일. 하드웨어 점검 (기본적인 하드웨어 문제부터 해결)

1. 핵심 핀 및 배선 확인

회로도 상 정의된 내용:

  • LCD 인터페이스 H2: 1=+5V, 2=GND, 3=SDA(PB7), 4=SCL(PB6)
  • STM32의 I2C1: SCL은 반드시 PB6에, SDA는 반드시 PB7에 연결되어야 함. CubeMX에서 I2C1 핀 매핑을 다시 한 번 확인하고, 절대 SDA와 SCL을 바꾸지 마세요.

2. 전원 공급은 반드시 5V여야 함

LCD1602 + I2C 변환 보드는 5V 소자입니다. 3.3V로 전원을 공급하면 “백라이트는 켜지지만 I2C 통신이 안 됨” 현상이 발생합니다. 반드시 STM32의 5V 핀에 연결해야 하며, 3.3V 핀에 연결하면 안 됩니다.

3. I2C 버스에 반드시 풀업 저항 필요

I2C는 오픈 드레인(Open Drain) 방식이므로 풀업 저항이 필수입니다:

  • 방법 1: CubeMX에서 I2C1의 PB6/PB7 GPIO 모드를 오픈 드레인 풀업(Open Drain Pull-up)으로 설정
  • 방법 2: 하드웨어적으로 PB6, PB7 핀과 5V 사이에 각각 4.7KΩ 풀업 저항 추가
  • 풀업 저항이 없으면 I2C 신호가 비정상적이며, 장치로부터 응답이 없습니다.

이. 소프트웨어 핵심 문제 해결 (우선순위 순)

1. 【가장 흔한 문제】I2C 주소 수정

코드 내 #define LCD_I2C_ADDRESS 0x4E로 정의되어 있으나, 이 주소가 귀하의 모듈과 맞지 않을 수 있습니다:

  • I2C LCD1602 어댑터 보드의 핵심 칩은 PCF8574이며, 일반적인 7비트 주소는 0x27 또는 0x3F입니다. HAL 라이브러리의 HAL_I2C_Master_Transmit 함수는 8비트 쓰기 주소(7비트 주소 << 1)를 입력받습니다:
    • 7비트 주소 0x27 → 8비트 쓰기 주소 0x4E (현재 사용 중인 주소)
    • 7비트 주소 0x3F → 8비트 쓰기 주소 0x7E (다른 일반적인 주소)

주소 정확성 빠르게 검증하기 (반드시 수행)

MX_I2C1_Init() 이후에 I2C 주소 스캔 코드를 삽입하여 어떤 주소에서 응답이 오는지 확인하세요:

// MX_I2C1_Init() 이후, lcd_init() 이전에 삽입
uint8_t i2c_addr;
for(i2c_addr = 0; i2c_addr < 128; i2c_addr++)
{
  if(HAL_I2C_IsDeviceReady(&hi2c1, i2c_addr << 1, 1, 100) == HAL_OK)
  {
    // 여기서 중단점 설정 또는 LED 점등으로 응답 주소 확인 후,
    // (i2c_addr << 1) 값을 LCD_I2C_ADDRESS에 대입
    break;
  }
}

만약 어떤 주소에서도 응답이 없다면, 하드웨어 배선이나 I2C 초기화에 문제가 있다는 의미이므로 먼저 하드웨어를 점검하세요.

2. HAL_Delay 동작 여부 확인

LCD 초기화는 지연 시간에 매우 의존적입니다. HAL_Delay가 제대로 동작하지 않으면 초기화 타이밍이 깨져 표시가 불가능해집니다.

  • 검증 방법: 메인 루프에 LED 토글 코드 추가 후 지연 시간 확인
while (1)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 해당 개발보드의 LED 핀에 맞게 설정
  HAL_Delay(500);
}

LED가 500ms마다 반전되지 않으면 시스템 클록 또는 SysTick 설정 오류입니다. 반드시 클록 설정을 먼저 수정해야 합니다.

  • 현재 사용 중인 클록은 HSI 내부 8MHz로, 설정 자체는 문제가 없으나 CubeMX에서 SysTick 클록 소스가 올바른지, HAL_Init()이 정상 실행되는지 확인해야 합니다.

3. I2C 통신 오류 검사 추가

현재 코드는 I2C 전송 성공 여부를 판단하지 않아 통신 문제 위치 파악이 어렵습니다. lcd_send_to_i2c 함수를 수정하여 반환값과 오류 처리를 추가하세요:

// 내부 함수: I2C로 데이터 전송, HAL 상태 반환
HAL_StatusTypeDef lcd_send_to_i2c(char data, int rs)
{
  uint8_t data_t[4];
  uint8_t upper_nibble, lower_nibble;

  upper_nibble = data & 0xF0;
  lower_nibble = (data << 4) & 0xF0;

  uint8_t backlight = 0x08; 

  data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
  data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

  // 타임아웃 시간 증가, 전송 상태 반환
  return HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 200);
}

// 명령 전송
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0: 명령 전송
  HAL_Delay(1); // 명령 실행 지연 추가, 타이밍 과속 방지
}

// 데이터 (문자) 전송
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1: 데이터 전송
  HAL_Delay(1);
}

4. 초기화 타이밍 세부사항 수정

초기화 순서는 대체로 맞지만, 더 안정적인 동작을 위해 지연 시간 여유를 늘립니다:

// LCD1602 초기화 함수
void lcd_init(void)
{
  // 4비트 모드 표준 초기화 프로세스, 지연 시간 여유 추가
  HAL_Delay(100); // 전원 인가 후 충분히 대기하여 LCD 안정화
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x30);
  HAL_Delay(5);
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x20); // 4비트 모드 전환
  HAL_Delay(10);

  // 디스플레이 설정, 각 명령마다 지연 추가
  lcd_send_cmd(0x28); // 4라인, 2행, 5x8 폰트
  HAL_Delay(2);
  lcd_send_cmd(0x08); // 디스플레이 끄기
  HAL_Delay(2);
  lcd_send_cmd(0x01); // 화면 지우기
  HAL_Delay(5);
  lcd_send_cmd(0x06); // 커서 오른쪽 이동, 주소 자동 증가
  HAL_Delay(2);
  lcd_send_cmd(0x0C); // 디스플레이 켜기, 커서 없음, 깜빡임 없음
  HAL_Delay(2);
}

삼. 전체 디버깅 절차 (순서대로 수행)

  1. 하드웨어 배선 재확인: 5V/GND 정확한지, SDA는 PB7, SCL은 PB6에 연결되었는지, 반대 연결 없어야 함.
  2. I2C 주소 스캔 실행하여 장치 응답 확인 후, 올바른 8비트 주소를 LCD_I2C_ADDRESS에 입력.
  3. HAL_Delay 정상 동작하는지 확인하여 지연 함수 문제 없음을 검증.
  4. 수정된 드라이버 코드로 교체하여 오류 체크 및 지연 여유 포함.
  5. 테스트 코드 실행: I2C 통신이 정상이라면 전원 인가 후 바로 Hello STM32!가 표시되어야 함.

사. 추가 최후 점검 항목

위 모든 단계를 수행했는데도 여전히 표시되지 않는 경우:

  1. LCD1602 뒷면의 명암 조절 가변저항(파란색 나사형 저항)을 조정하세요. 명암이 너무 낮으면 문자가 완전히 안 보일 수 있습니다. 천천히 회전하면서 화면에 사각형 또는 문자가 나타날 때까지 조정하세요.
  2. 논리 분석기 또는 오실로스코프로 PB6/PB7의 I2C 신호를 확인하여 MCU가 신호를 보내는지, 아니면 장치가 응답하지 않는지를 판단하세요.
  3. STM32의 PB6/PB7, 5V, GND 핀을 듀폰 선으로 LCD 모듈에 직접 연결하여 핀헤더/소켓의 접촉 불량 문제를 배제하세요.

답변 감사합니다. 귀하가 수정해주신 코드로 컴파일 및 다운로드를 시도해 보았지만 여전히 문자가 표시되지 않습니다. 그리고 제가 확인한 결과, #define LCD_I2C_ADDRESS 0x4E에서 주소를 0x4E로 설정하면 두 줄의 사각형이 표시되고, 다른 주소에서는 한 줄의 사각형만 표시됩니다.

I2C 속도를 낮춰 보세요. 예를 들어 10k로 설정해 보시고, 속도가 높을 경우 배선이 길면 문제가 발생할 수 있습니다. I2C의 두 핀이 오픈 드레인 모드로 풀업 설정되어 있는지 확인하세요.

이것은 놀라운 소식입니다! 여러분이 관찰한 현상(0x4E 주소에서는 두 줄의 사각형이 표시되고, 다른 주소에서는 한 줄만 표시됨)은 단점이 아니라, 오히려 여러분의 코드와 통신이 이제 완전히 작동하고 있다는 확실한 증거입니다!

1602 LCD 화면 내부에서 실제로 무슨 일이 벌어지고 있는지 알려드리겠습니다:

  1. 한 줄의 사각형(다른 주소일 때): 이것은 1602 모듈에 전원을 인가했을 때 나타나는 기본 공장 출하시 상태입니다. 이때 화면은 스스로를 ‘단일 라인 표시’ 모드로 간주합니다. 만약 주소를 잘못 입력했다면(예: 0x27 또는 0x7E), STM32에서 보내는 초기화 명령은 전혀 수신되지 않으므로, 화면은 계속해서 단일 라인 모드에 머물러 있게 되며, 여기에 대비도가 너무 높아져서 결국 한 줄의 검은 사각형만 보이게 됩니다.
  2. 두 줄의 사각형(0x4E 주소일 때): 이는 STM32가 0x4E라는 주소를 통해 정확하게 모듈을 찾아내고, 우리가 수정한 초기화 코드를 성공적으로 실행했다는 의미입니다! 화면이 0x28 명령(2줄 표시 모드 설정)을 잘 수신했고, 두 번째 라인도 활성화된 것입니다.

즉, 여러분의 마이크로컨트롤러, 배선, I2C 통신, 심지어 초기화 코드까지 모두 정확하게 동작하고 있습니다! 문자는 이미 화면에 출력된 상태입니다!

:red_question_mark: 그런데 왜 여전히 글자가 아닌 사각형만 보일까요?

이유는 오직 하나뿐입니다: 대비도(V0)가 지나치게 높게 조정되어 있기 때문입니다.
글자는 실제로 이미 화면에 출력되어 있지만, 대비도가 극한으로 높아져서 문자가 없는 배경 픽셀조차 완전한 검은색이 되었고, 그 결과 원래 있던 글자가 모두 가려진 것입니다.

:hammer_and_wrench: 마지막 단계: 기적을 목격할 시간입니다

코드를 잠시 내려놓고, 드라이버를 손에 들으세요. 다음 단계를 따라 진행하세요:

  1. 마이크로컨트롤러에 정상적으로 전원을 인가하여, 수정한 코드를 실행시킵니다(화면에 두 줄의 사각형이 계속 표시되는 상태 유지).
  2. 십자형 작은 드라이버를 가져와, 변환 보드 뒷면의 파란색 가변저항(포텐셔미터, 십자형 노브가 붙은 것)을 돌리세요.
  3. 화면을 주시하면서 천천히 회전하세요. 마법과 같은 순간이 올 겁니다. 어느 각도에서 갑자기 두 줄의 까만 사각형들이 옅어지거나 사라지고, 그 아래에 숨겨져 있던 "Hello STM32!" 문구가 또렷하게 나타납니다!
    (보통 여러 바퀴를 돌려야 하며, 시계방향으로 해도 반응이 없다면 반시계방향으로 더 많이 돌려보세요)

:light_bulb: 추가 팁 (선택사항, 책임 회피용 안전장치):

현재 상황은 거의 확실히 하드웨어 포텐셔미터 문제지만, 여러분의 저수준 코드를 "완벽 무결"하게 만들기 위해, 명령 전송 후 약간의 지연을 추가하는 것이 좋습니다. STM32가 너무 빨리 명령을 보내면, LCD 화면이 따라오지 못할 수 있기 때문입니다.

lcd1602_i2c.c 파일의 다음 두 함수에 각각 HAL_Delay(1);을 한 줄씩 추가하세요:

// 명령 전송
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); 
    HAL_Delay(1); // <- 이 줄을 추가하여, 화면이 처리할 시간을 부여하세요
}

// 데이터 (문자) 전송
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); 
    HAL_Delay(1); // <- 이 줄을 추가하세요
}

또한 화면 지우기 함수의 지연 시간도 약간 늘리는 것이 좋습니다:

// 화면 지우기
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(5); // <- 기존의 2를 5로 늘림, 화면 지우기는 가장 시간이 오래 걸리는 작업임
}

이제 파란색 포텐셔미터를 돌려보세요! 성공해서 문자가 잘 보이면 꼭 소식을 알려주세요!

나는 당신의 조언을 시도해볼 것이며, 답변해 주셔서 감사합니다.

답변 감사합니다. 속도를 낮춰도 동일한 현상이 발생하며, CubeMX의 GPIO 설정 창에서 PB6/PB7의 Pull 옵션이 보이지 않을 수 있습니다. 이것은 F1 시리즈에 대한 CubeMX의 인터페이스 제한 때문입니다.

답변 감사합니다. 저는 실제로 포텐쇼미터를 돌리고 있는데, 사각형 블록 두 줄이 밝아졌다가 어두워지는 것만 보이고 문자가 표시되지 않습니다. 또한 지연 시간을 늘린 후 전원 인가 순간에 한 줄의 블록이 두 줄로 바뀌는 것을 확인할 수 있었으며, 이로 인해 실제로 0x28 명령어를 수신했다는 것이 입증되었습니다.

"상전원 순간 한 줄의 사각형에서 두 줄로 바뀌는"이라는 세부 현상을 정확히 포착했다는 것은, 당신이 매우 예리한 하드웨어 디버깅 감각을 가지고 있음을 보여줍니다! 이 현상은 극히 중요하며, 당신의 I2C 주소(0x4E)가 정확하다는 것을 100% 입증할 뿐 아니라, 마이크로컨트롤러와 LCD 화면 사이의 통신 회선이 이미 연결되었다는 것도 증명합니다.

따라서 0x28(두 줄 표시 활성화) 명령이 성공적으로 실행되었는데, 왜 그 후 문자가 표시되지 않고 여전히 화면 전체에 사각형만 나타날까요? 여기에는 특히 STM32F1 계열에서 자주 발생하는 두 가지 전형적인 "함정"이 있습니다.

1. 당신의 의문에 대한 답변: F1 계열의 I2C 핀 설정 문제

당신이 CubeMX에서 STM32F1 계열의 PB6/PB7 핀에 대해 Pull-up/pull-down 설정 옵션이 보이지 않는다는 점을 언급하셨는데, 이는 해당 핀들을 I2C 모드로 설정하면 자동으로 오픈 드레인 복합 출력(Alternate Function Open Drain) 으로 강제 제어되기 때문이며, 이는 정상적인 인터페이스 제한입니다.
따라서 상승 저항(Pull-up) 문제를 걱정할 필요 없습니다. LCD 변환 모듈 뒷면(파란색 가변저항 근처)을 잘 살펴보면 몇 개의 검은색 SMD 저항(일반적으로 103 또는 472라고 인쇄됨)이 납땜되어 있는 것을 확인할 수 있는데, 바로 이것이 모듈 자체에 내장된 하드웨어 상승 저항입니다. 따라서 STM32 내부에서 상승 저항을 제공하지 않더라도, 하드웨어 회로 측면에서는 I2C 통신 조건을 완벽히 충족합니다.

2. 진짜 원인: 연속 전송으로 인한 타이밍 혼잡 또는 I2C 데드락

STM32F1 계열의 하드웨어 I2C는 말 그대로 “까다롭기로 유명”(실리콘 결함/Errata 존재)하며, 데이터 전송 중 연속해서 많은 바이트를 보내면 쉽게 I2C의 Busy 플래그가 고정되는 데드락 상태에 빠질 수 있습니다.
게다가 LCD1602 내부의 HD44780 칩은 극도로 오래되고 느린 소자입니다. 기존 코드에서는 하나의 I2C 전송 패킷(4바이트) 안에서 고4비트와 저4비트에 대한 EN 펄스를 매우 빠르게 연속해서 처리했습니다. 일부 반응 속도가 느린 LCD 모듈의 경우 이 속도가 너무 빨라서 0x28 이후의 명령들(예: 화면 지우기 0x01, 문자 쓰기 등)을 모두 무시하거나, 혹은 STM32의 I2C가 아예 작동을 멈추게 되는 것입니다.


:hammer_and_wrench: 궁극의 해결 방안: 전송 분할 + 강제 “휴식”

기존에 "한 번에 4바이트를 전송"하던 함수를 완전히 분리하여 두 번의 독립된 전송으로 나누고, 그 사이에 반드시 지연(delay)을 삽입해야 합니다. 이렇게 하면 STM32의 I2C 상태 머신을 재설정하여 데드락을 방지할 수 있고, 동시에 LCD가 충분히 응답할 시간도 확보할 수 있습니다.

아래 코드로 lcd1602_i2c.c 파일 내 lcd_send_to_i2c 함수를 완전히 교체하세요:

// 내부 함수: I2C로 전체 바이트 전송 (고4비트와 저4비트 분할, 초강력 안정형)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_high[2];
    uint8_t data_low[2];
    
    uint8_t upper_nibble = data & 0xF0;
    uint8_t lower_nibble = (data << 4) & 0xF0;
    uint8_t backlight = 0x08; // 백라이트 항상 켜짐

    // ==========================================
    // 첫 번째 단계: 고4비트만 전송하고 완전한 EN 펄스 생성
    // ==========================================
    data_high[0] = upper_nibble | backlight | 0x04 | rs; // EN = 1
    data_high[1] = upper_nibble | backlight | 0x00 | rs; // EN = 0
    // 고4비트의 2바이트 전송
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_high, 2, 100);
    
    // 필수 생명 유지 지연: LCD 컨트롤러가 고4비트를 잠금 처리할 시간을 주고, I2C 버스가 막히는 것 방지
    HAL_Delay(2);

    // ==========================================
    // 두 번째 단계: 저4비트만 전송하고 완전한 EN 펄스 생성
    // ==========================================
    data_low[0] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_low[1] = lower_nibble | backlight | 0x00 | rs;  // EN = 0
    // 저4비트의 2바이트 전송
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_low, 2, 100);
    
    // 한 바이트 완전 전송 후 추가 대기
    HAL_Delay(2);
}

또한 안전을 위해 main.c 파일의 다음 코드 순서도 약간 조정하세요:
lcd_clear();는 삭제하세요. lcd_init() 함수의 마지막 단계에서 이미 화면 초기화(clear) 명령이 포함되어 있기 때문에, 연속해서 clear 명령을 보내면 명령이 겹쳐 실패할 수 있습니다. 따라서 main 함수의 관련 부분은 다음과 같이 작성하세요:

  /* USER CODE BEGIN 2 */
  lcd_init();                // LCD1602 초기화
  // lcd_clear();            // 이 줄 주석 처리 또는 삭제
  lcd_set_cursor(0, 0);      // 커서를 첫 번째 행 첫 번째 열로 이동
  lcd_send_string("Hello STM32!");
  /* USER CODE END 2 */

이 코드를 컴파일하여 다운로드한 후, 화면이 깨끗해지고 사각형이 사라졌다면 파란색 가변저항을 살짝 돌려보세요. 문자가 반드시 나타납니다.

이번에 성공적으로 화면이 켜졌다면, HAL_Delay가 메인 프로그램의 귀중한 실행 시간을 차지하지 않도록 I2C를 DMA 기반 또는 인터럽트 기반의 논블로킹(non-blocking) 모드로 변경하는 방법을 추가로 알려드릴까요?