STM32 + ILI9341 SPI LCD 빠르게 붙이기 (검증 → TouchGFX 연동 1탄)
UI를 한땀 한땀 드로잉하면 결국 소스가 산으로 갑니다. 버튼 한 픽셀만 거슬려도 끝없는 수정…
그래서 TouchGFX Designer로 화면을 설계하고, MCU에서는 프레임 전송만 담당하는 구조가 효율적입니다. 다만 그 전에, LCD 드라이버가 SPI로 제대로 도는지를 먼저 확실히 검증해야 이후 문제가 꼬이지 않습니다. 이 글은 그 “0단계(하드/펌 검증)”를 깔끔히 끝내는 방법입니다.
1) 개발 환경 & 준비물
- OS: Windows 10/11
- Tool: STM32CubeIDE 1.18.1, TouchGFX Designer 4.25.0
- 대상 LCD: ILI9341(320×240, SPI 4-wire: SCK/MOSI/CS/DC/RESET, MISO는 선택)
- 권장: 사용할 폰트/사이즈/이미지를 미리 준비해 두면 이후 TouchGFX 작업이 빨라집니다.
설치 방법은 ST 공식 가이드/영상 참고. 여기선 생략합니다.
https://youtu.be/ShkpZ9328vY?si=P0UJFkqV2wNP4ReN 참조
2) 하드웨어 연결(예시)
보드별 Alternate Function 매핑은 다를 수 있으니 본인 보드의 데이터시트를 꼭 대조하세요.
SCK | PB3 (SPI1_SCK) | SPI Mode 0 |
MOSI | PB5 (SPI1_MOSI) | 필수 |
MISO | PA6 (SPI1_MISO) | 옵션(읽기 안 쓰면 미연결 가능) |
CS | PC10 (LCD_CS_PIN) | 소프트웨어 제어 |
DC | PF13 (LCD_DC_PIN) | Data/Command |
RESET | PF14 (LCD_HW_RESET_PIN) | 하드 리셋 |
BL(백라이트) | 보드/모듈 핀 | 필요시 GPIO/PWM 구동 |
- 전원: 대부분 3.3 V, I/O도 3.3 V 레벨 필요
- GND: 반드시 공통 접지
- MISO는 레지스터 읽기 안하면 생략 가능(그림의 “SDA”는 읽기 라인)
3) CubeMX 설정 요약
SPI1
- Mode: Half-Duplex Master
- Hardware NSS: Disable(SW로 CS 제어)
- Frame: Motorola / 8-bit / MSB first
- CPOL = Low, CPHA = 1Edge → SPI Mode 0
- Prescaler: 2(예: 32 Mbit/s). 시작은 8~16 Mbit/s로 안전하게, 이후 올리세요.
DMA (권장)
- SPI1_TX: Mem-to-Periph, Priority High, DataWidth=Byte, Normal
(이번 글의 기본 예제는 블로킹 전송, 뒤에서 DMA 대안 코드도 제공합니다)
GPIO
- SPI 핀은 Alternate Function, Very High Speed, No pull
- CS/DC/RESET/BL은 Output으로 설정
4) 최소 구동 코드(정리본)
아래는 사용하신 코드 흐름을 가독성↑/재사용성↑ 형태로 리팩터링한 예시입니다.
(핵심: CS/DC 토글 헬퍼, 커맨드/데이터 공통 루틴, 주소창 설정, 이미지 푸시)
// ---- Pins: 프로젝트에 맞게 수정 ----
#define CS_LOW() HAL_GPIO_WritePin(LCD_CS_PIN_GPIO_Port, LCD_CS_PIN_Pin, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PIN_GPIO_Port, LCD_CS_PIN_Pin, GPIO_PIN_SET)
#define DC_LOW() HAL_GPIO_WritePin(LCD_DC_PIN_GPIO_Port, LCD_DC_PIN_Pin, GPIO_PIN_RESET)
#define DC_HIGH() HAL_GPIO_WritePin(LCD_DC_PIN_GPIO_Port, LCD_DC_PIN_Pin, GPIO_PIN_SET)
#define RST_LOW() HAL_GPIO_WritePin(LCD_HW_RESET_PIN_GPIO_Port, LCD_HW_RESET_PIN_Pin, GPIO_PIN_RESET)
#define RST_HIGH() HAL_GPIO_WritePin(LCD_HW_RESET_PIN_GPIO_Port, LCD_HW_RESET_PIN_Pin, GPIO_PIN_SET)
extern SPI_HandleTypeDef hspi1;
static inline void lcd_delay(uint32_t ms) { HAL_Delay(ms); }
// ---- SPI write helpers ----
static void lcd_write_cmd(uint8_t cmd) {
DC_LOW(); CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
CS_HIGH();
}
static void lcd_write_data8(uint8_t data) {
DC_HIGH(); CS_LOW();
HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
CS_HIGH();
}
static void lcd_write_data_buf(const uint8_t *buf, uint32_t len) {
DC_HIGH(); CS_LOW();
HAL_SPI_Transmit(&hspi1, (uint8_t*)buf, len, HAL_MAX_DELAY);
CS_HIGH();
}
// ---- 주소창 & 회전 ----
static uint16_t LCD_W = 320, LCD_H = 240;
static void ili9341_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
lcd_write_cmd(0x2A); // CASET
uint8_t ca[4] = { x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };
lcd_write_data_buf(ca, 4);
lcd_write_cmd(0x2B); // RASET
uint8_t ra[4] = { y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };
lcd_write_data_buf(ra, 4);
lcd_write_cmd(0x2C); // RAMWR
}
typedef enum {
SCREEN_VERTICAL_1 = 0,
SCREEN_HORIZONTAL_1 = 1,
SCREEN_VERTICAL_2 = 2,
SCREEN_HORIZONTAL_2 = 3
} lcd_rot_t;
static void ili9341_set_rotation(lcd_rot_t r) {
lcd_write_cmd(0x36); // MADCTL
switch (r) {
case SCREEN_VERTICAL_1: lcd_write_data8(0x48); LCD_W=240; LCD_H=320; break;
case SCREEN_HORIZONTAL_1: lcd_write_data8(0x28); LCD_W=320; LCD_H=240; break;
case SCREEN_VERTICAL_2: lcd_write_data8(0x88); LCD_W=240; LCD_H=320; break;
case SCREEN_HORIZONTAL_2: lcd_write_data8(0xE8); LCD_W=320; LCD_H=240; break;
}
}
// ---- 초기화 시퀀스 ----
void LCD_Init(void) {
RST_LOW(); lcd_delay(50);
RST_HIGH(); lcd_delay(50);
// 일부 패널은 0xFE/0xEF로 확장모드 진입을 요구합니다(벤더별 상이).
lcd_write_cmd(0xFE);
lcd_write_cmd(0xEF);
lcd_write_cmd(0x36); lcd_write_data8(0x48); // MADCTL 기본
lcd_write_cmd(0x3A); lcd_write_data8(0x05); // 픽셀포맷(패널별 0x55를 요구하기도 함* 참고)
// ... (원문 시퀀스 그대로, 필요 지점에 코멘트 추가) ...
lcd_write_cmd(0x11); // Sleep Out
lcd_delay(120);
lcd_write_cmd(0x29); // Display ON
ili9341_set_rotation(SCREEN_HORIZONTAL_2);
}
// ---- RGB565 이미지 푸시 (Size = 픽셀 개수) ----
void LCD_DrawImage_RGB565(const uint16_t *img, uint32_t pixels) {
ili9341_set_window(0, 0, LCD_W - 1, LCD_H - 1);
// 전송은 big-endian 순서(상위바이트 먼저)
for (uint32_t i = 0; i < pixels; i++) {
uint8_t px[2] = { img[i] >> 8, img[i] & 0xFF };
lcd_write_data_buf(px, 2);
}
}
main() 초기화 순서 예시
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
// MX_DMA_Init(); // DMA 전송을 쓸 계획이면 활성화
LCD_Init();
while (1) {
// 테스트 이미지
// ili9341_set_window(0,0,LCD_W-1,LCD_H-1);
// LCD_DrawImage_RGB565(image_data_SI200ELCD320x240_Darkver, LCD_W * LCD_H);
MX_TouchGFX_Process(); // (다음 편에서 TouchGFX 연동)
}
}
DMA 전송(선택)
이미지 푸시 속도가 부족하면 아래처럼 DMA로 한 번에 밀 수 있습니다.
volatile bool spi_tx_done = true;
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi == &hspi1) spi_tx_done = true;
}
void LCD_DrawImage_DMA(const uint16_t *img, uint32_t pixels) {
ili9341_set_window(0,0,LCD_W-1,LCD_H-1);
DC_HIGH(); CS_LOW();
spi_tx_done = false;
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)img, pixels * 2); // RGB565: 2바이트/픽셀
while (!spi_tx_done) { /* optionally sleep or do other work */ }
CS_HIGH();
}
주의: DMA를 쓰면 프레임 버퍼가 캐시 일관성(일부 코어)과 메모리 정렬에 민감합니다. D-Cache가 있는 MCU라면 SCB_CleanDCache_by_Addr() 등으로 정리하거나, D-Cache 미사용/버퍼를 DTCM 등으로 배치하는 등 보드 특성을 반영하세요.
5) 동작 확인용 “단계별” 체크
- 전원/백라이트: BL이 PWM/스위칭이면 항상 ON으로 먼저 고정
- 리셋 타이밍: RST Low≥10 ms → High≥10 ms → Sleep Out(0x11) 후 120 ms 지연
- SPI 모드: Mode 0(CPOL=0, CPHA=0/1st edge)
- 픽셀 포맷(0x3A)
- 일부 **ILI9341 변종은 0x55(16bpp)**를 요구, 다른 변종은 **0x05**로 동작합니다.
- 화면이 깨지면 우선 0x55로 바꿔 테스트하세요.
- MADCTL(0x36): 축/RGB/BGR 설정. 좌우/상하 뒤집힘, 색상 뒤집힘 발생 시 값 변경
- 주소창(0x2A/0x2B/0x2C): 항상 x0≤x1, y0≤y1, 전체화면이면 (0,0)-(W-1,H-1)
- 바이트 순서: RGB565은 상위바이트 먼저. 색상이 틀리면 바이트 스왑 확인
- 속도: 초기에 Prescaler을 여유 있게(예: 8~16)→정상 확인 후 2까지 올리기
- 배선: CS/DC/RESET 신호가 떠 있지 않도록 풀업/풀다운 상태 확인
- MISO 미사용: Half-Duplex 설정 시 핀 충돌 없는지 확인
6) 정상 화면 예 & 이후 진행
초기화/이미지 푸시까지 OK라면 아래처럼 테스트 이미지가 출력됩니다.
이제 2탄에서 TouchGFX Designer로 만든 UI를 flushFrameBuffer() 경로로 SPI로 빠르게 밀어 넣는 방법(부분 갱신, 사각형 전송, FPS/전력 최적화, DMA 이중버퍼링)을 다룹니다.
7) 흔한 질문(FAQ)
- Q. MISO는 꼭 연결해야 하나요?
A. 보통 안 해도 됩니다. 레지스터 읽기/상태 폴링이 필요한 특수 루틴이 아니면 쓰지 않습니다. - Q. 화면이 까맣게만 보여요.
A. BL이 OFF이거나, 0x11 후 지연 부족, 픽셀포맷/바이트순서 오류, CS/DC 배선 문제 순으로 점검하세요. - Q. 속도가 너무 느립니다.
A. DMA 전송 + 큰 버스트(라인 단위)로 밀고, SPI 클록을 점진적으로 올리세요. 필요하면 부분 갱신만 전송.
8) 전체 예제(사용 코드 기반, 주석 보강)
원문 초기화 시퀀스를 살리되, 의미를 코멘트로 덧붙였습니다.
벤더 커맨드는 패널마다 조금 다르니 동작하는 값을 기준으로 사용하세요.
여기서 GFX Designer를 바로 들어가면 SPI 및 LCD 드라이버 검증이 안된 상태라 어느 쪽이 문제인지 파악하기 어렵다. 먼저 LCD 드라이버가 잘 동작하는 확인 한다.
void Lcd_Initial(void) {
// HW Reset
HAL_GPIO_WritePin(LCD_HW_RESET_PIN_GPIO_Port, LCD_HW_RESET_PIN_Pin, GPIO_PIN_RESET);
HAL_Delay(50);
HAL_GPIO_WritePin(LCD_HW_RESET_PIN_GPIO_Port, LCD_HW_RESET_PIN_Pin, GPIO_PIN_SET);
HAL_Delay(50);
// 확장커맨드 진입(패널 의존)
LCD_WriteCommand(0xFE);
LCD_WriteCommand(0xEF);
// MADCTL
LCD_WriteCommand(0x36); LCD_WriteData(0x48);
// Pixel Format: 16bpp (패널별 0x05 또는 0x55)
LCD_WriteCommand(0x3A); LCD_WriteData(0x05);
// 이하 전원/감마/인터페이스 튜닝(벤더 시퀀스)
LCD_WriteCommand(0x84); LCD_WriteData(0x61);
LCD_WriteCommand(0x8A); LCD_WriteData(0x40);
LCD_WriteCommand(0xB0); LCD_WriteData(0x60);
LCD_WriteCommand(0xF6); LCD_WriteData(0xC0);
LCD_WriteCommand(0x21);
LCD_WriteCommand(0x86); LCD_WriteData(0x98);
LCD_WriteCommand(0x89); LCD_WriteData(0x03);
LCD_WriteCommand(0xE8); LCD_WriteData(0x12); LCD_WriteData(0x00);
LCD_WriteCommand(0x8B); LCD_WriteData(0x80);
LCD_WriteCommand(0x8D); LCD_WriteData(0x22);
LCD_WriteCommand(0xC9); LCD_WriteData(0x0A);
LCD_WriteCommand(0xC3); LCD_WriteData(0x30);
LCD_WriteCommand(0xC5); LCD_WriteData(0x15);
LCD_WriteCommand(0xC6); LCD_WriteData(0x0A);
LCD_WriteCommand(0xC7); LCD_WriteData(0x0A);
LCD_WriteCommand(0xC8); LCD_WriteData(0x0E);
LCD_WriteCommand(0xFF); LCD_WriteData(0x62);
LCD_WriteCommand(0x99); LCD_WriteData(0x3E);
LCD_WriteCommand(0x9D); LCD_WriteData(0x4B);
LCD_WriteCommand(0x8E); LCD_WriteData(0x0F);
// Window: 0,0 ~ 319,239
LCD_WriteCommand(0x2A); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0xEF);
LCD_WriteCommand(0x2B); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x01); LCD_WriteData(0x3F);
LCD_WriteCommand(0x2C); // RAMWR
// 감마(벤더)
LCD_WriteCommand(0xF0); LCD_WriteData(0x82); LCD_WriteData(0x00); LCD_WriteData(0x0A);
LCD_WriteData(0x09); LCD_WriteData(0x07); LCD_WriteData(0x2F);
LCD_WriteCommand(0xF1); LCD_WriteData(0x47); LCD_WriteData(0x98); LCD_WriteData(0xB7);
LCD_WriteData(0x20); LCD_WriteData(0x25); LCD_WriteData(0xCF);
LCD_WriteCommand(0xF2); LCD_WriteData(0x45); LCD_WriteData(0x00); LCD_WriteData(0x0E);
LCD_WriteData(0x0C); LCD_WriteData(0x08); LCD_WriteData(0x30);
LCD_WriteCommand(0xF3); LCD_WriteData(0x40); LCD_WriteData(0xB4); LCD_WriteData(0x92);
LCD_WriteData(0x0F); LCD_WriteData(0x12); LCD_WriteData(0xBF);
// Sleep out & Display on
LCD_WriteCommand(0x11); HAL_Delay(120);
LCD_WriteCommand(0x29);
// 회전(필요 시)
ILI9341_Set_Rotation(SCREEN_HORIZONTAL_2);
}
위 코드가 정상적으로 동작 한다면 아래와 같은 이미지가 디스플레이 될 것이다.
위까지 정상적으로 진행적으로 진행되었다면 다음 2탄에 GFX Deisigner를 다루면 된다.
9) 마무리 & 다음 편 예고
여기까지 통과하면 하드/SPI/드라이버는 OK입니다.
2탄에서는 TouchGFX Designer로 만든 화면을 **flushFrameBuffer()**에서 사각형 단위 전송으로 연결하고, DMA 이중버퍼링으로 FPS와 전력까지 챙기는 구성을 다룹니다.
궁금한 점/막히는 지점은 댓글 주세요. 로그/사진과 함께 주시면 더 빨리 잡아드립니다!
'펌웨어 개발자의 팁' 카테고리의 다른 글
TouchGFX Designer & CubeIDE 설정 가이드 (10) | 2025.09.13 |
---|