STM32 최소 보드를 주제어기로 사용하고, LCD1602 I2C 모듈은 인터넷에서 구입한 완제품입니다.
저는 초보자라서 코드는 AI에게 작성해달라고 했습니다. 처음에는 한 줄의 사각형이 표시되었고, 수정 후에는 두 줄의 사각형이 표시되지만 여전히 문자가 나타나지 않습니다. 전위차계를 조정해 보았으며, I2C 전압도 5V입니다.
STM32 최소 보드를 주제어기로 사용하고, LCD1602 I2C 모듈은 인터넷에서 구입한 완제품입니다.
저는 초보자라서 코드는 AI에게 작성해달라고 했습니다. 처음에는 한 줄의 사각형이 표시되었고, 수정 후에는 두 줄의 사각형이 표시되지만 여전히 문자가 나타나지 않습니다. 전위차계를 조정해 보았으며, I2C 전압도 5V입니다.
배선도를 그려보고 코드도 올려주면 안 되겠어요? 이렇게 정보가 부족하면 남들이 다 추측하게끔 만드시는 건가요?
좋은 질문의 사례: https://bbs.eeclub.top/t/topic/289
질문하는 지혜: https://bbs.eeclub.top/t/topic/109
형제, 급할 거 없어요. 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.c와 lcd1602.h 파일을 가져와 프로젝트에 바로 포함하고 사용하는 것이 가장 안전합니다.
4. 하드웨어 논리 레벨 문제 (가능성 낮음)
전원을 5V로 공급했다고 말씀하셨는데, 아주 좋습니다. LCD1602는 반드시 5V 전원이 필요하니까요. STM32의 핀은 3.3V 로직 레벨이지만, 대부분의 I2C 핀(PB6, PB7 등)은 "5V 내성(FT, 5V-tolerant)"이므로 직접 연결해도 일반적으로 문제가 없습니다. 다만 STM32와 LCD 모듈의 GND를 반드시 함께 연결했는지만 확인하면 됩니다.
문제 해결을 위한 제안 (다음 단계)
먼저 문자를 표시하게 만들려고 애쓰지 마세요. 대신 AI에게 “STM32 I2C 스캐너(I2C Address Scanner)” 코드를 작성해 달라고 하세요.
코드를 플래싱한 후, 시리얼 터미널(예: PuTTY, Tera Term 등)을 열어 STM32가 I2C 버스 상에서 해당 모듈을 인식하는지 확인하세요.
0x4E 반환) → 하드웨어 연결은 완벽하므로, 그 주소를 LCD 초기화 코드에 올바르게 입력하기만 하면 됩니다.어서 "Hello World"를 화면에 띄우시길 바랍니다! 막히면 또 질문하세요.
하드웨어 관련 간단한 참고 사항입니다. 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비트 모드"의 핸드셰이킹 타이밍에서 오류를 일으켰기 때문입니다.
사용 중인 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)을 쓴 경우:
0x3만 보내는 것.lcd_send_to_i2c가 상위 4비트 0x3을 전송하고(EN 펄스 발생), 바로 이어 하위 4비트 0x0도 전송하며 또 한 번 EN 펄스를 발생시킵니다.0x0 펄스를 수신하여 타이밍이 완전히 어긋나며 초기화를 거부하고, 전원 인가 직후의 상태인 ‘화면 전체에 사각형이 가득한’ 상태에서 멈춰버립니다.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 등 나머지 함수는 그대로 유지 ...
코드를 수정하고 다시 플래싱한 후에도 여전히 사각형만 표시된다면, 유일한 가능성은 구입한 모듈의 기본 I2C 주소가 0x27이 아니라 0x3F라는 것입니다. 이때는 lcd1602_i2c.h 파일에서 #define LCD_I2C_ADDRESS 0x4E를 0x7E (0x3F를 한 비트 왼쪽으로 시프트)로 변경한 후 다시 시도하면 됩니다.
우선 위 초기화 코드를 수정해서 테스트해 보세요. 모듈의 통신 주소를 100% 확실히 확인할 수 있는 간단한 "I2C 주소 스캐너"를 만들어 보여드릴까요?
논리 분석기를 사용하여 I2C 파형이 정상적으로 출력되는지, 타이밍은 맞는지 확인할 수 있습니다. I2C에 문제가 없다면, 다시 논리 분석기를 이용해 I2C를 병렬 신호로 변환하는 모듈의 출력 신호가 나오는지, 타이밍은 맞는지 확인하시기 바랍니다.
STM32CubeMX를 어떻게 설정하셨나요?
LCD1602의 백라이트는 정상적으로 켜지지만 문자가 표시되지 않는 경우, 90% 확률로 I2C 통신 오류(주소 오류/핀 설정 오류/응답 없음)이며, 그 다음으로 초기화 타이밍 또는 지연 오류, 하드웨어 전원 공급 문제 혹은 풀업 저항 누락 등이 원인입니다. 아래에 우선순위 순으로 완전한 진단 및 해결 방법을 제시합니다.
회로도 상 정의된 내용:
LCD1602 + I2C 변환 보드는 5V 소자입니다. 3.3V로 전원을 공급하면 “백라이트는 켜지지만 I2C 통신이 안 됨” 현상이 발생합니다. 반드시 STM32의 5V 핀에 연결해야 하며, 3.3V 핀에 연결하면 안 됩니다.
I2C는 오픈 드레인(Open Drain) 방식이므로 풀업 저항이 필수입니다:
코드 내 #define LCD_I2C_ADDRESS 0x4E로 정의되어 있으나, 이 주소가 귀하의 모듈과 맞지 않을 수 있습니다:
HAL_I2C_Master_Transmit 함수는 8비트 쓰기 주소(7비트 주소 << 1)를 입력받습니다:
0x4E (현재 사용 중인 주소)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 초기화에 문제가 있다는 의미이므로 먼저 하드웨어를 점검하세요.
LCD 초기화는 지연 시간에 매우 의존적입니다. HAL_Delay가 제대로 동작하지 않으면 초기화 타이밍이 깨져 표시가 불가능해집니다.
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 해당 개발보드의 LED 핀에 맞게 설정
HAL_Delay(500);
}
LED가 500ms마다 반전되지 않으면 시스템 클록 또는 SysTick 설정 오류입니다. 반드시 클록 설정을 먼저 수정해야 합니다.
HAL_Init()이 정상 실행되는지 확인해야 합니다.현재 코드는 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);
}
초기화 순서는 대체로 맞지만, 더 안정적인 동작을 위해 지연 시간 여유를 늘립니다:
// 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);
}
LCD_I2C_ADDRESS에 입력.HAL_Delay 정상 동작하는지 확인하여 지연 함수 문제 없음을 검증.Hello STM32!가 표시되어야 함.위 모든 단계를 수행했는데도 여전히 표시되지 않는 경우:
답변 감사합니다. 귀하가 수정해주신 코드로 컴파일 및 다운로드를 시도해 보았지만 여전히 문자가 표시되지 않습니다. 그리고 제가 확인한 결과, #define LCD_I2C_ADDRESS 0x4E에서 주소를 0x4E로 설정하면 두 줄의 사각형이 표시되고, 다른 주소에서는 한 줄의 사각형만 표시됩니다.
I2C 속도를 낮춰 보세요. 예를 들어 10k로 설정해 보시고, 속도가 높을 경우 배선이 길면 문제가 발생할 수 있습니다. I2C의 두 핀이 오픈 드레인 모드로 풀업 설정되어 있는지 확인하세요.
이것은 놀라운 소식입니다! 여러분이 관찰한 현상(0x4E 주소에서는 두 줄의 사각형이 표시되고, 다른 주소에서는 한 줄만 표시됨)은 단점이 아니라, 오히려 여러분의 코드와 통신이 이제 완전히 작동하고 있다는 확실한 증거입니다!
1602 LCD 화면 내부에서 실제로 무슨 일이 벌어지고 있는지 알려드리겠습니다:
0x4E라는 주소를 통해 정확하게 모듈을 찾아내고, 우리가 수정한 초기화 코드를 성공적으로 실행했다는 의미입니다! 화면이 0x28 명령(2줄 표시 모드 설정)을 잘 수신했고, 두 번째 라인도 활성화된 것입니다.즉, 여러분의 마이크로컨트롤러, 배선, I2C 통신, 심지어 초기화 코드까지 모두 정확하게 동작하고 있습니다! 문자는 이미 화면에 출력된 상태입니다!
이유는 오직 하나뿐입니다: 대비도(V0)가 지나치게 높게 조정되어 있기 때문입니다.
글자는 실제로 이미 화면에 출력되어 있지만, 대비도가 극한으로 높아져서 문자가 없는 배경 픽셀조차 완전한 검은색이 되었고, 그 결과 원래 있던 글자가 모두 가려진 것입니다.
코드를 잠시 내려놓고, 드라이버를 손에 들으세요. 다음 단계를 따라 진행하세요:
"Hello STM32!" 문구가 또렷하게 나타납니다!
추가 팁 (선택사항, 책임 회피용 안전장치):
현재 상황은 거의 확실히 하드웨어 포텐셔미터 문제지만, 여러분의 저수준 코드를 "완벽 무결"하게 만들기 위해, 명령 전송 후 약간의 지연을 추가하는 것이 좋습니다. 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 계열에서 자주 발생하는 두 가지 전형적인 "함정"이 있습니다.
당신이 CubeMX에서 STM32F1 계열의 PB6/PB7 핀에 대해 Pull-up/pull-down 설정 옵션이 보이지 않는다는 점을 언급하셨는데, 이는 해당 핀들을 I2C 모드로 설정하면 자동으로 오픈 드레인 복합 출력(Alternate Function Open Drain) 으로 강제 제어되기 때문이며, 이는 정상적인 인터페이스 제한입니다.
따라서 상승 저항(Pull-up) 문제를 걱정할 필요 없습니다. LCD 변환 모듈 뒷면(파란색 가변저항 근처)을 잘 살펴보면 몇 개의 검은색 SMD 저항(일반적으로 103 또는 472라고 인쇄됨)이 납땜되어 있는 것을 확인할 수 있는데, 바로 이것이 모듈 자체에 내장된 하드웨어 상승 저항입니다. 따라서 STM32 내부에서 상승 저항을 제공하지 않더라도, 하드웨어 회로 측면에서는 I2C 통신 조건을 완벽히 충족합니다.
STM32F1 계열의 하드웨어 I2C는 말 그대로 “까다롭기로 유명”(실리콘 결함/Errata 존재)하며, 데이터 전송 중 연속해서 많은 바이트를 보내면 쉽게 I2C의 Busy 플래그가 고정되는 데드락 상태에 빠질 수 있습니다.
게다가 LCD1602 내부의 HD44780 칩은 극도로 오래되고 느린 소자입니다. 기존 코드에서는 하나의 I2C 전송 패킷(4바이트) 안에서 고4비트와 저4비트에 대한 EN 펄스를 매우 빠르게 연속해서 처리했습니다. 일부 반응 속도가 느린 LCD 모듈의 경우 이 속도가 너무 빨라서 0x28 이후의 명령들(예: 화면 지우기 0x01, 문자 쓰기 등)을 모두 무시하거나, 혹은 STM32의 I2C가 아예 작동을 멈추게 되는 것입니다.
기존에 "한 번에 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) 모드로 변경하는 방법을 추가로 알려드릴까요?