이 부분에서는 U8g2 라이브러리를 사용하여 널리 사용되는 SSD1306 OLED 디스플레이를 STM32와 연결해 보겠습니다. U8g2를 통해 디스플레이를 초기화하고 텍스트와 그래픽을 렌더링 하는 방법을 살펴보겠습니다.
이 가이드에서는 다음 내용을 다룹니다.
이전 튜토리얼 참고 U8G2 그래픽 라이브러리를 STM32로 포팅하기 1부: 소개
- 프로젝트에 U2G8 라이브러리를 추가합니다.
- 지연, GPIO 및 통신 콜백을 개발합니다.
- 펌웨어 개발.
- 결과.

5. 프로젝트에 U8G2 라이브러리 추가:
Github 저장소에서 라이브러리를 다운로드한 후 압축을 풉니다.
추출이 완료되면 다음과 같이 csrc 폴더를 driver 폴더로 복사합니다.

다음으로, 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 속성을 클릭합니다.

다음으로, 프로젝트 속성 -> C/C++ 빌드 -> 설정 -> GCC 컴파일러 -> 포함 경로를 열고 추가 버튼을 클릭하여 여기에 폴더 경로를 추가합니다.

다음과 같이 경로를 추가하고 적용 및 닫기를 클릭합니다.

이제 라이브러리가 추가되어 아무 문제 없이 컴파일할 수 있습니다.
6. 지연, GPIO 및 통신 콜백 함수 개발:
"uC 특정" GPIO 및 지연 콜백
GPIO 및 지연 콜백은 GPIO 및 지연과 관련된 모든 메시지를 처리합니다.
지연 메시지에는 디스플레이에 필요한 지연(대부분 밀리초)이나 소프트웨어 SPI 또는 I2C가 구현된 경우 SPI 또는 I2C 클록을 생성하는 데 필요한 지연(대부분 나노 또는 마이크로)이 포함될 수 있습니다.
GPIO 메시지 에는 디스플레이 인터페이스에 사용되는 GPIO 핀의 설정 및 재설정이 포함됩니다. 이러한 GPIO 핀에는 데이터 핀 (병렬 모드에서 D0, D1, D2 등)이나 제어 핀 (SPI 모드에서 CS, RST, DC)이 포함될 수 있습니다.
GPIO 및 지연 콜백 함수는 다음과 같습니다.
uint8_t u8x8_gpio_and_delay_template(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT: // called once during init phase of u8g2/u8x8
break; // can be used to setup pins
case U8X8_MSG_DELAY_NANO: // delay arg_int * 1 nano second
break;
case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
break;
case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
break;
case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
break;
case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
break; // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
case U8X8_MSG_GPIO_D0: // D0 or SPI clock pin: Output level in arg_int
//case U8X8_MSG_GPIO_SPI_CLOCK:
break;
case U8X8_MSG_GPIO_D1: // D1 or SPI data pin: Output level in arg_int
//case U8X8_MSG_GPIO_SPI_DATA:
break;
case U8X8_MSG_GPIO_D2: // D2 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D3: // D3 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D4: // D4 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D5: // D5 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D6: // D6 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D7: // D7 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_E: // E/WR pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS: // CS (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_DC: // DC (data/cmd, A0, register select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_RESET: // Reset pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS1: // CS1 (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS2: // CS2 (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
break; // arg_int=1: Input dir with pullup high for I2C clock pin
case U8X8_MSG_GPIO_I2C_DATA: // arg_int=0: Output low at I2C data pin
break; // arg_int=1: Input dir with pullup high for I2C data pin
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}
return 1;
}
하드웨어 i2c를 사용하고 올바르게 구성했기 때문에 다음과 같이 지연 함수만 포트에 연결하면 됩니다.
먼저, 다음과 같이 u8g2 라이브러리를 포함시킵니다.
#include "u8g2.h"
다음으로, u8g2 구조체를 다음과 같이 선언합니다.
u8g2_t myDisplay ;
다음과 같이 지연 및 gpio 함수를 선언합니다.
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_DELAY_MILLI:
HAL_Delay(arg_int);
break;
}
return 1;
}
i2c는 하드웨어 수준이기 때문에 지연 기능에만 관심이 있습니다.
함수 인수
u8x8_t *u8x8
- U8x8 구조체(라이브러리의 내부 상태)를 가리키는 포인터입니다.
- 고급 작업을 하지 않는 한 이것을 직접 사용할 필요는 없습니다.
uint8_t msg
U8g2가 보낸 메시지 코드로, 함수에 수행할 작업을 알려줍니다.
메시지의 예:
U8X8_MSG_GPIO_AND_DELAY_INIT → GPIO와 타이머를 초기화합니다.
U8X8_MSG_DELAY_MILLI → 지연 arg_int 시간(밀리초).
U8X8_MSG_DELAY_10MICRO → 지연 arg_int * 10 µs.
U8X8_MSG_GPIO_CS → 칩 선택(CS) 핀을 높음/낮음으로 설정합니다.
U8X8_MSG_GPIO_DC → 데이터/명령(DC) 핀을 설정합니다.
U8X8_MSG_GPIO_RESET → 리셋(RES) 핀을 설정합니다.
uint8_t arg_int
메시지와 함께 전달되는 정수 매개변수입니다.
사용법은 다음에 따라 달라집니다 msg:
지연 메시지의 경우 → 밀리초 또는 10µs 단위.
GPIO 메시지의 경우 → 값 0 (낮음) 또는 1 (높음).
void *arg_ptr
포인터 인수(GPIO/지연에는 거의 사용되지 않음).
주로 바이트 콜백에서 SPI 데이터 전송과 같은 것에 사용되며 여기서는 사용되지 않습니다.
GPIO/지연 핸들러의 경우 일반적으로 이를 무시합니다.
커뮤니케이션 콜백
통신 콜백은 디스플레이와의 통신 중에 사용되는 메시지를 처리하는 데 사용됩니다. 이러한 메시지에는 전송 시작, 바이트 전송, 전송 종료 등이 포함됩니다.
uint8_t u8x8_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch(msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
#define OLED_Addr (0x3D<<1)
HAL_I2C_Master_Transmit(&hi2c1, OLED_Addr, buffer, buf_idx, 100);
break;
default:
return 0;
}
return 1;
}
인수:
이는 I²C 바이트 핸들러 콜백입니다.
U8g2는 이를 사용하여 I²C를 통해 디스플레이로 데이터를 전송합니다.
- u8x8 → U8x8 객체(라이브러리 컨텍스트)를 가리키는 포인터입니다. 여기서는 사용되지 않습니다.
- msg → U8g2의 메시지 코드(어떤 작업을 해야 하는지).
- arg_int → 정수 값입니다. 메시지에 따라 다릅니다.
- arg_ptr → 포인터. 종종 전송할 데이터 바이트를 가리킨다.
함수 내부
로컬 정적 변수:
U8g2는 한 번에 32바이트 이상을 (START와 END 사이) 보내도록 요청하지 않을 것을 보장합니다.
이렇게 하면 버퍼가 항상 충분히 크게 유지됩니다.
static uint8_t buffer[32]; // Local buffer for I2C data
static uint8_t buf_idx; // Current write index
다양한 메시지 처리
1. U8X8_MSG_BYTE_SEND
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
arg_ptr U8g2가 보내려는 데이터 덩어리(1바이트 이상)를 가리킵니다.
arg_int 바이트 수를 알려줍니다.
코드는 해당 바이트를 로컬에 복사하여 buffer증가시킵니다 buf_idx.
아직 아무것도 전송되지 않았습니다. 단지 저장만 했을 뿐입니다.
2. U8X8_MSG_BYTE_시작_전송
buf_idx = 0 ;
I²C 전송의 시작을 표시합니다.
버퍼 인덱스를 재설정하여 데이터가 처음부터 채워지도록 합니다.
3. U8X8_MSG_BYTE_END_TRANSFER
#define OLED_Addr (0x3D<<1)
HAL_I2C_Master_Transmit(&hi2c1, OLED_Addr, buffer, buf_idx, 100);
I²C 전송의 끝을 표시합니다.
이제 모든 것을 buffer 한 번의 I²C 전송으로 OLED로 전송합니다.
OLED_Addr = 장치 주소 ( 0x3D 1만큼 왼쪽으로 이동 → 8비트 I²C 주소) (적절하게 변경)
HAL 함수를 사용합니다: HAL_I2C_Master_Transmit(&hi2c1, OLED_Addr, buffer, buf_idx, 100); 여기서
&hi2c1 → I²C 핸들(CubeMX에서 구성됨).
OLED_Addr → SSD1306의 I²C 주소.
buffer → 수집된 데이터를 가리키는 포인터.
buf_idx → 보낼 바이트 수.
100 → 시간 초과(ms)
4. 기본값
return 0;
인식 msg 되지 않으면 0(처리되지 않음)을 반환합니다.
반환 값
문제가 해결되면 항상 반환하세요 1.
흐름 요약
- START_TRANSFER → 버퍼 인덱스를 재설정합니다.
- BYTE_SEND → 들어오는 바이트를 버퍼에 복사합니다.
- END_TRANSFER → 모든 것을 다음을 통해 보냅니다 HAL_I2C_Master_Transmit.
따라서 이 함수는 U8g2와 STM32의 HAL I²C 드라이버를 연결하는 다리 역할을 합니다. 디스플레이 명령을 버퍼에 일괄 처리하여 한 번의 I²C 트랜잭션으로 효율적으로 전송합니다.
이것이 I2C 디스플레이 포팅에 대한 모든 내용입니다.
7. 펌웨어 개발:
사용자 코드 begin 2의 메인 함수에서 다음과 같이 디스플레이를 버스에 연결합니다.
u8g2_Setup_ssd1306_i2c_128x64_noname_f ( & myDisplay , U8G2_R0 , u8x8_i2c , u8x8_gpio_and_delay );
다음으로, 다음과 같이 디스플레이를 초기화합니다.
u8g2_InitDisplay ( & myDisplay ); // 디스플레이에 init 시퀀스를 전송하고, 이후 디스플레이는 절전 모드가 됩니다.
초기화 후에는 디스플레이가 절전 모드로 전환되므로 절전 모드를 종료하세요.
u8g2_SetPowerSave ( & myDisplay , 0 ); // 디스플레이 깨우기
다음으로, 디스플레이에 쓰기를 시작하겠습니다.
먼저, 다음과 같이 디스플레이를 지웁니다.
u8g2_ClearDisplay ( & myDisplay );
글꼴 유형 설정:
u8g2_SetFont ( & myDisplay , u8g2_font_ncenB14_tr );
다음과 같이 문자열을 표시합니다.
u8g2_DrawStr ( & myDisplay , 0 , 15 , "안녕하세요 세상" );
다음과 같이 모양(원)을 그립니다.
u8g2_DrawCircle ( & myDisplay , 60 , 30 , 10 , U8G2_DRAW_ALL );
다음과 같이 새로운 콘텐츠를 디스플레이에 보냅니다.
u8g2_SendBuffer ( & myDisplay );
펌웨어는 여기까지입니다. 프로젝트를 저장하고 다음과 같이 MCU에서 실행해 보세요.

8. 결과:
프로젝트를 실행하면 화면에 다음이 표시됩니다.
우리는 U8G2를 우리 프로젝트에 성공적으로 통합하여 디스플레이에 텍스트와 모양을 표시했습니다.
다음으로, 다양한 유형의 디스플레이에 대해 살펴보겠습니다. 계속 지켜봐 주세요.
즐거운 코딩되세요. 😉
튜토리얼 출처 참고 https://blog.embeddedexpert.io/?p=3700
'STM32' 카테고리의 다른 글
| STM32 U8G2 그래픽 라이브러리 사용하는 방법 (0) | 2025.09.14 |
|---|---|
| STM32 MAX485 사용한 RS485 통신 (3) | 2025.08.27 |
| STM32 펄스 카운터 사용 (2) | 2025.08.27 |
| STM32 타이머 애플리케이션: PWM 입력 모드 (1) | 2025.08.25 |
| 새로운 AI 가속 STM32N6 시작하기 (1) | 2025.08.22 |
| STM32 중급 과정 ADC 개요 8 (0) | 2025.08.22 |
| STM32 중급 과정 Peripheral Hands-On: I2C 7-1 (0) | 2025.08.21 |
| STM32 중급 과정 I2C 개요 7 (0) | 2025.08.20 |