STM32G474 기반 0.96인치 OLED(SSD1306) 디스플레이 드라이버(4핀 I2C 인터페이스), 하드웨어 IIC/소프트웨어 IIC 지원, HAL 라이브러리 버전.
이 드라이버는 상당히 완성도가 높아 영문, 정수, 실수, 한자, 이미지, 2진수, 16진수 표시가 가능하며 점, 직선, 사각형, 원, 타원, 삼각형 등을 그릴 수 있고 여러 글꼴을 지원해 간단한 그래픽 라이브러리 수준입니다.
이 프로그램은 Jiangxie Technology의 코드를 2차 수정한 것으로, 원본은 STM32F103 기반이며 소프트웨어 I2C만 지원했으나, 수정 후 하드웨어 I2C를 지원하고 매크로 정의를 변경하면 소프트웨어 I2C로도 사용 가능합니다.
테스트 하드웨어는 NUCLEO-G474RE 개발보드입니다.
OLED 구동 원리 및 드라이버 사용 튜토리얼은 Jiangxie Technology의 영상을 참조하세요: https://url.zeruns.com/L7j6y
- STM32 하드웨어 I2C로 SHTC3 온습도 센서 읽기: https://blog.zeruns.com/archives/692.html
- U8g2 그래픽 라이브러리가 포팅된 STM32F407 표준 라이브러리 프로젝트 템플릿: https://blog.zeruns.com/archives/722.html
- STM32F1 기반 0.96인치 OLED 디스플레이 드라이버, 하드웨어/소프트웨어 I2C 지원: https://blog.zeruns.com/archives/769.html
전자/마이크로컨트롤러 기술交流群: 2169025065
예시 사진

I2C 프로토콜 개요
I2C 통신 프로토콜(Inter-Integrated Circuit)은 Philips사에서 개발했으며, 핀 수가 적고 하드웨어 구현이 간단하며 확장성이 뛰어나 USART, CAN 등 통신 프로토콜의 외부 트랜시버(전위 변환 칩)가 불필요해 현재 시스템 내 여러 집적 회로(IC) 간 통신에 폭넓게 사용됩니다.
I2C는 단일 데이터 버스 SDA(Serial Data Line)만 있어 직렬 데이터 버스로 한 번에 1비트씩만 데이터를 전송하며, 이는 직렬 통신이자 반이중 통신입니다.
반이중 통신: 양방향 통신이 가능하나 동시에 양 방향으로는 불가능하고 교대로 전송해야 하며, 방향을 전환할 수 있는 단방향 통신으로도 볼 수 있습니다. 동시에는 한 방향만 전송 가능하며 데이터선 1개만 필요합니다.
I2C 통신 프로토콜은 물리 계층과 프로토콜 계층으로 나뉩니다. 물리 계층은 통신 시스템의 기계적, 전자적 기능 부분(하드웨어)을 규정해 물리 매체에서 원시 데이터 전송을 보장합니다. 프로토콜 계층은 주로 통신 로직을 규정해 송수신 측의 데이터 패킹/언패킹 기준(소프트웨어)을 통일합니다.
I2C 물리 계층
I2C 통신 장치 간 일반적인 연결 방식
(1) 다중 장치를 지원하는 버스입니다. “버스”는 여러 장치가 공유하는 신호선을 의미하며, 하나의 I2C 통신 버스에는 여러 I2C 통신 장치를 연결해 여러 통신 마스터 및 슬레이브를 지원합니다.
(2) I2C 버스는 2개의 버스 선만 사용하며, 하나는 양방향 직렬 데이터선 SDA(Serial Data Line), 다른 하나는 직렬 클럭선 SCL(Serial Clock Line)입니다. 데이터선은 데이터를 나타내고 클럭선은 데이터 송수신 동기화에 사용됩니다.
(3) 버스는 풀업 저항을 통해 전원에 연결됩니다. I2C 장치가 유휴 상태일 때는 하이 임피던스를 출력하며, 모든 장치가 유휴 상태로 하이 임피던스를 출력할 때 풀업 저항이 버스를 하이 레벨로 당깁니다.
I2C 통신 시 MCU GPIO 핀은 반드시 오픈 드레인 출력으로 설정해야 하며, 그렇지 않으면 단락이 발생할 수 있습니다.
STM32 I2C 관련 추가 정보 및 사용법은 다음 글을 참조하세요: https://url.zeruns.com/JC0Ah
그리고 Jiangxie Technology의 STM32 입문 튜토리얼: https://www.bilibili.com/video/BV1th411z7sn?p=31
여기서는 자세히 설명하지 않겠습니다.
사용 설명
기본값은 하드웨어 IIC를 사용하며, I2C3를 사용하고 SCL은 PA8, SDA는 PC9입니다.
하드웨어 I2C
STM32CubeMX 설정: 사용할 I2C 외부 장치 핀을 찾아 SCL 및 SDA 기능으로 설정합니다. 아래 그림은 I2C3의 SCL입니다.
이어서 I2C 외부 장치를 설정해 해당 I2C 외부 장치를 활성화하고 속도 모드를 Fast Mode Plus로 설정하며 속도는 1000으로 변경하고 나머지는 기본값으로 둡니다.
GPIO 설정: 위 설정 후 해당 두 핀이 자동으로 복용 오픈 드레인 출력 모드로 설정되며, IO 출력 속도만 Very High로 변경하고 GPIO 라벨(User Label)을 각각 I2C3_SCL 및 I2C3_SDA로 정의합니다. 다른 I2C를 사용할 경우 다른 값으로 설정해도 되며, 코드에서 해당 부분을 수정하면 됩니다. 수정 후 코드 생성을 클릭합니다.
OLED.c 파일에서 #define OLED_USE_SW_I2C를 주석 처리하고 #define OLED_USE_HW_I2C 주석을 해제합니다. 다른 핀을 I2C 핀으로 사용하고 다른 이름을 정의한 경우 코드의 I2C3_SCL 및 I2C3_SDA도 변경합니다.
소프트웨어 I2C
STM32CubeMX 설정: 두 핀을 I2C의 SCL 및 SDA 신호선으로 설정하고 IO口的 User Label을 각각 I2C3_SCL 및 I2C3_SDA로 수정합니다. 다른 이름으로 변경할 경우 코드에서 수정이 필요하며, IO 모드를 오픈 드레인 출력으로 설정하고 기본 출력 전위를 하이로, 풀업 출력으로 설정한 후 속도를 최고로 설정합니다. 아래 그림과 같이 수정 후 코드 생성을 클릭합니다.
OLED.c 파일에서 #define OLED_USE_HW_I2C를 주석 처리하고 #define OLED_USE_SW_I2C 주석을 해제합니다. 다른 핀을 I2C 핀으로 사용하고 다른 이름을 정의한 경우 코드의 I2C3_SCL 및 I2C3_SDA도 변경합니다.
필요 부품
- STM32 개발보드 입문 키트: https://u.jd.com/fQS0YAe
- STM32G474 개발보드: https://s.click.taobao.com/8OwQ8vt
- OLED 모듈: https://s.click.taobao.com/EF0Evwt
- 점퍼 와이어: https://s.click.taobao.com/VMkDvwt
- 브레드보드: https://s.click.taobao.com/bhg8Txt
- DAPLink(ST-Link 대체 가능, 가상 COM 포트 포함): https://s.click.taobao.com/QVQ8Txt강협테크놀로지 STM32 입문 키트: https://s.click.taobao.com/NTn9Txt
프로그램
전체 프로젝트 다운로드 링크:
바이두 넷디스크: 링크: https://url.zeruns.com/0CQJG 추출 코드: 0169
123 넷디스크(속도 제한 없음): https://www.123pan.com/s/2Y9Djv-O0cvH.html 추출 코드: vvDt
Gitee 오픈소스 주소: https://gitee.com/zeruns/STM32-HAL-OLED-I2C
GitHub 오픈소스 주소: https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL
Star 하나만 눌러주세요
프로젝트는 Keil5로 생성되었으며, Vscode+EIDE로 개발되었으며 두 소프트웨어 모두 이 프로젝트를 열 수 있습니다.
프로젝트 파일은 모두 UTF-8 인코딩을 사용하며, 열었을 때 글자가 깨진다면 편집기의 인코딩을 UTF-8로 변경해야 합니다.
주요 파일 OLED.c:
/***************************************************************************************
* 본 프로그램은 강협테크놀로지에서 제작하여 무료로 오픈소스로 공개합니다
* 자유롭게 보고, 사용하고, 수정하여 자신의 프로젝트에 적용할 수 있습니다
* 프로그램의 저작권은 강협테크놀로지에 있으며, 누구도 이를 독점할 수 없습니다
*
* 프로그램 이름: 0.96인치 OLED 디스플레이 드라이버 (4핀 I2C 인터페이스)
* 프로그램 생성일: 2023.10.24
* 현재 프로그램 버전: V1.1
* 현재 버전 릴리스일: 2023.12.8
*
* 강협테크놀로지 공식 웹사이트: jiangxiekeji.com
* 강협테크놀로지 공식 Taobao 스토어: jiangxiekeji.taobao.com
* 프로그램 소개 및 업데이트 동향: jiangxiekeji.com/tutorial/oled.html
*
* 프로그램의 버그나 오타를 발견하신 경우 이메일로 피드백 주세요: [email protected]
* 이메일을 보내기 전, 업데이트 동향 페이지에서 최신 프로그램을 확인해 주세요. 이미 수정된 문제라면 이메일을 보내지 않으셔도 됩니다
***************************************************************************************
*/
/*
* 본 프로그램은 zeruns에 의해 2차 수정되었습니다
* 수정 내용: 표준 라이브러리 버전을 HAL 라이브러리 버전으로 변경, 하드웨어 I2C 지원 추가, 매크로 정의를 통해 하드웨어 I2C 사용 여부 선택 가능
* 수정일: 2024.3.16
* 블로그: https://blog.zeruns.com
* Bilibili 홈페이지: https://space.bilibili.com/8320520
*/
#include "main.h"
#include "OLED.h"
#include <str
```ing.h>
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
// 中文을 사용할 경우 컴파일러 추가 옵션에 --no-multibyte-chars 를 넣어야 함 (AC6 컴파일러는 생략 가능)
/*
OLED 드라이버 방식 선택, 기본값은 하드웨어 I2C. 소프트웨어 I2C를 쓰려면 하드웨어 I2C 매크로를 주석 처리하고 소프트웨어 I2C 매크로의 주석을 해제.
두 줄을 동시에 주석 해제하면 안 됨!
stm32cubemx 초기화 시 SCL·SDA 핀의 “user label”을 각각 I2C3_SCL, I2C3_SDA로 설정해야 함.
*/
#define OLED_USE_HW_I2C // 하드웨어 I2C
//#define OLED_USE_SW_I2C // 소프트웨어 I2C
/*핀 정의, I2C 통신 핀은 여기서 수정 가능*/
#define OLED_SCL I2C3_SCL_Pin // SCL
#define OLED_SDA I2C3_SDA_Pin // SDA
#define OLED_SCL_GPIO_Port I2C3_SCL_GPIO_Port
#define OLED_SDA_GPIO_Port I2C3_SDA_GPIO_Port
/*STM32G474 칩의 하드웨어 I2C3: PA8 -- SCL; PC9 -- SDA */
#ifdef OLED_USE_HW_I2C
/*I2C 인터페이스, OLED가 사용할 I2C 포트 지정*/
#define OLED_I2C hi2c3
extern I2C_HandleTypeDef hi2c3; // HAL 라이브러리용, 하드웨어 I2C 포트 지정
#endif
/*OLED 슬레이브 주소*/
#define OLED_ADDRESS 0x3C << 1 // 0x3C는 OLED 7비트 주소, 왼쪽으로 1비트 시프트해 최하위 비트를 읽기/쓰기 비트로 만들면 0x78
/*I2C 타임아웃 시간*/
#define OLED_I2C_TIMEOUT 10
/*소프트웨어 I2C용 딜레이 시간, 아래 값은 170 MHz 메인 클럭 기준. 클럭이 다르면 조정 필요, 100 MHz 이하는 0으로 설정*/
#define Delay_time 3
/**
* 데이터 저장 형식:
* 세로 8점, 상위 비트가 아래쪽, 왼쪽→오른쪽, 위→아래 순
* 각 비트는 한 픽셀에 대응
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 ------------> B3 B3 --
* B4 B4 B4 B4 |
* B5 B5 B5 B5 |
* B6 B6 B6 B6 |
* B7 B7 B7 B7 |
* |
* -----------------------------------
* |
* | B0 B0 B0 B0
* | B1 B1 B1 B1
* | B2 B2 B2 B2
* --> B3 B3 ------------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*
* 좌표축 정의:
* 좌상단이 (0, 0)
* 가로 방향이 X축, 범위: 0~127
* 세로 방향이 Y축, 범위: 0~63
*
* 0 X축 127
* .------------------------------->
* 0 |
* |
* |
* |
* Y축 |
* |
* |
* |
* 63 |
* v
*
*/
/*전역변수*********************/
/**
* OLED 프레임버퍼 배열
* 모든 표시 함수는 이 프레임버퍼에만 읽고 씀
* 이후 OLED_Update() 또는 OLED_UpdateArea() 호출 시
* 프레임버퍼 내용을 OLED 하드웨어로 전송해 표시
*/
uint8_t OLED_DisplayBuf[8][128];
/*********************전역변수*/
#ifdef OLED_USE_SW_I2C
/**
* @brief OLED_SCL에 고·저전압 출력
* BitValue 값에 따라 OLED_SCL을 HIGH 또는 LOW로 설정
* @param BitValue 비트값, 0 또는 1
*/
void OLED_W_SCL(uint8_t BitValue)
{
/*BitValue 값에 따라 SCL을 HIGH 또는 LOW로 설정*/
HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
/*MCU 속도가 너무 빠르면 적절한 딜레이 추가로 I2C 최대 속도 초과 방지*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
/**
* @brief OLED SDA에 고·저전압 출력
* @param 쓰려는 SDA 전平값, 범위: 0/1
* @return 없음
* @note 상위 함수가 SDA를 쓸 때 호출됨
* 인자가 0이면 SDA LOW, 1이면 SDA HIGH
*/
void OLED_W_SDA(uint8_t BitValue)
{
/*BitValue 값에 따라 SDA를 HIGH 또는 LOW로 설정*/
HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
/*MCU 속도가 너무 빠르면 적절한 딜레이 추가로 I2C 최대 속도 초과 방지*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
#endif
/**
* @brief OLED 핀 초기화
* @param 없음
* @retval 없음
* @note 상위 함수가 초기화 요청 시 호출,
* SCL·SDA 핀을 오픈드레인 모드로 초기화하고 핀을 릴리스해야 함
*/
void OLED_GPIO_Init(void)
{
uint32_t i, j;
/*초기화 전에 적절한 딜레이로 OLED 전원 안정 대기*/
for (i = 0; i < 1000; i++) {
for (j = 0; j < 1000; j++)
;
}
#ifdef OLED_USE_SW_I2C
__HAL_RCC_GPIOC_CLK_ENABLE(); // GPIOC 클럭 활성화
__HAL_RCC_GPIOA_CLK_ENABLE(); // GPIOA 클럭 활성화
GPIO_InitTypeDef GPIO_InitStruct = {0}; // GPIO 설정용 구조체
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 오픈드레인 출력 모드
GPIO_InitStruct.Pull = GPIO_PULLUP; // 내부 풀업
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 고속
GPIO_InitStruct.Pin = I2C3_SDA_Pin; // SDA 핀
HAL_GPIO_Init(I2C3_SDA_GPIO_Port, &GPIO_InitStruct); // GPIO 초기화
GPIO_InitStruct.Pin = I2C3_SCL_Pin;
HAL_GPIO_Init(I2C3_SCL_GPIO_Port, &GPIO_InitStruct);
/*SCL·SDA 릴리스*/
OLED_W_SCL(1);
OLED_W_SDA(1);
#endif
}
// https://blog.zeruns.com
/*통신 프로토콜*********************/
/**
* @brief I2C 시작 조건
* @param 없음
* @return 없음
*/
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(1); // SDA 릴리스, HIGH 확보
OLED_W_SCL(1); // SCL 릴리스, HIGH 확보
OLED_W_SDA(0); // SCL HIGH 상태에서 SDA 끌어내려 시작 신호 생성
OLED_W_SCL(0); // 시작 후 SCL도 끌어내려 버스 점유 및 타이밍 연결 용이
#endif
}
/**
* @brief I2C 정지 조건
* @param 없음
* @return 없음
*/
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(0); // SDA 끌어내려 LOW 확보
OLED_W_SCL(1); // SCL 릴리스, HIGH
OLED_W_SDA(1); // SCL HIGH 상태에서 SDA 릴리스해 정지 신호 생성
#endif
}
/**
* @brief I2C 1바이트 송신
* @param Byte 송신할 1바이트 데이터, 범위: 0x00~0xFF
* @return 없음
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
uint8_t i;
/*8번 반복, 호스트가 데이터 비트 하나씩 전송*/
for (i = 0; i < 8; i++)
{
/*마스킹으로 Byte의 지정 비트 추출 후 SDA 라인에 기록*/
/*이중 !은 0이 아닌 값을 1로 만듦*/
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1); // SCL 릴리스, 슬레이브가 SCL HIGH 시 SDA 읽음
OLED_W_SCL(0); // SCL 끌어내려 호스트가 다음 비트 전송
}
OLED_W_SCL(1); // 추가 클럭, ACK 무시
OLED_W_SCL(0);
#endif
}
/**
* @brief OLED 명령어 쓰기
* @param Command 쓸 명령값, 범위: 0x00~0xFF
* @return 없음
*/
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // I2C 시작
OLED_I2C_SendByte(0x78); // OLED I2C 슬레이브 주소 전송
OLED_I2C_SendByte(0x00); // 제어 바이트, 0x00 → 뒤이어 명령 쓰기
OLED_I2C_SendByte(Command); // 지정 명령 쓰기
OLED_I2C_Stop(); // I2C 정지
#elif defined(OLED_USE_HW_I2C)
uint8_t TxData[2] = {0x00, Command};
HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, 2, OLED_I2C_TIMEOUT);
#endif
}
/**
* @brief OLED 데이터 쓰기
* @param Data 쓸 데이터 시작 주소
* @param Count 쓸 데이터 개수
* @return 없음
*/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
uint8_t i;
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // I2C 시작
OLED_I2C_SendByte(0x78); // OLED I2C 슬레이브 주소 전송
OLED_I2C_SendByte(0x40); // 제어 바이트, 0x40 → 뒤이어 데이터 쓰기
/*Count번 반복해 연속 데이터 쓰기*/
for (i = 0; i < Count; i++) {
OLED_I2C_SendByte(Data[i]); // Data 배열 값 순차 전송
}
OLED_I2C_Stop(); // I2C 정지
#elif defined(OLED_USE_HW_I2C)
uint8_t TxData[Count + 1]; // 새 배열 할당, 크기 Count + 1
TxData[0] = 0x40; // 시작 바이트
// Data가 가리키는 내용을 TxData 나머지에 복사
for (i = 0; i < Count; i++) {
TxData[i + 1] = Data[i];
}
HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, Count + 1, OLED_I2C_TIMEOUT);
#endif
}
/*********************통신 프로토콜*/
/*하드웨어 설정*********************/
/**
* @brief OLED 초기화
* @param 없음
* @return 없음
* @note 사용 전 반드시 호출
*/
void OLED_Init(void)
{
OLED_GPIO_Init(); // 하단 포트 초기화 호출
/*일련의 명령어 전송으로 OLED 초기화*/
OLED_WriteCommand(0xAE); // 디스플레이 OFF(0xAE), ON(0xAF)
OLED_WriteCommand(0xD5); // 표시 클럭 분배비/발진기 주파수 설정
OLED_WriteCommand(0x80); // 0x00~0xFF
OLED_WriteCommand(0xA8); // 멀티플렉스 비율 설정
OLED_WriteCommand(0x3F); // 0x0E~0x3F
OLED_WriteCommand(0xD3); // 디스플레이 오프셋 설정
OLED_WriteCommand(0x00); // 0x00~0x7F
OLED_WriteCommand(0x40); // 디스플레이 시작 행, 0x40~0x7F
OLED_WriteCommand(0xA1); // 좌우 방향, 0xA1 정상, 0xA0 좌우 반전
OLED_WriteCommand(0xC8); // 상하 방향, 0xC8 정상, 0xC0 상하 반전
OLED_WriteCommand(0xDA); // COM 핀 하드웨어 설정
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); // 대비 설정
OLED_WriteCommand(0xCF); // 0x00~0xFF
OLED_WriteCommand(0xD9); // 프리차지 주기 설정
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); // VCOMH 디셀렉트 레벨 설정
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); // 전체 디스플레이 ON/OFF 설정
OLED_WriteCommand(0xA6); // 정상/반전 표시, 0xA6 정상, 0xA7 반전
OLED_WriteCommand(0x8D); // 충전 펌프 설정
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); // 디스플레이 ON
OLED_Clear(); // 프레임버퍼 클리어
OLED_Update(); // 표시 업데이트, 초기화 후 깨짐 방지
}
/**
* @brief OLED 커서 위치 설정
* @param Page 커서가 위치할 페이지, 범위: 0-7
* @param X 커서의 X축 좌표, 범위: 0-127
* @return 없음
* @note OLED 기본 Y축은 8비트 단위로만 쓰기 가능, 1페이지 = 8 Y좌표
*/
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
/*1.3인치 OLED 사용 시 아래 주석 해제*/
/*1.3인치 OLED 드라이버(SH1106)는 132열*/
/*화면 시작 열이 0열이 아닌 2열에 연결*/
/*따라서 X에 2를 더해야 정상 표시*/
// X += 2;
/*명령어로 페이지 주소와 열 주소 설정*/
OLED_WriteCommand(0xB0 | Page); // 페이지 위치 설정
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // X 위치 상위 4비트
OLED_WriteCommand(0x00 | (X & 0x0F)); // X 위치 하위 4비트
}/*********************하드웨어 설정*/
/*유틸리티 함수*********************/
/*유틸리티 함수는 내부 일부 함수에서만 사용*/
/**
* @brief 거듭제곱 함수
* @param X 밑수
* @param Y 지수
* @return X의 Y제곱에 해당하는 값
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; // 결과 기본값은 1
while (Y--) // Y번 반복하여 곱함
{
Result *= X; // 매번 X를 결과에 곱함
}
return Result;
}
/**
* @brief 지정된 점이 지정된 다각형 내부에 있는지 판단
* @param nvert 다각형의 꼭짓점 수
* @param vertx verty 다각형 꼭짓점의 x 및 y 좌표를 포함하는 배열
* @param testx testy 테스트 점의 X 및 y 좌표
* @return 지정된 점이 지정된 다각형 내부에 있는지 여부, 1: 내부, 0: 외부
*/
uint8_t OLED_pnpoly(uint8_t nvert, int16_t *vertx, int16_t *verty, int16_t testx, int16_t testy)
{
int16_t i, j, c = 0;
/*이 알고리즘은 W. Randolph Franklin이 제안함*/
/*참고 링크: https://wrfranklin.org/Research/Short_Notes/pnpoly.html*/
for (i = 0, j = nvert - 1; i < nvert; j = i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
/**
* @brief 지정된 점이 지정된 각도 내부에 있는지 판단
* @param X Y 지정된 점의 좌표
* @param StartAngle EndAngle 시작 각도 및 종료 각도, 범위: -180-180
* 수평 오른쪽이 0도, 수평 왼쪽이 180도 또는 -180도, 아래쪽이 양수, 위쪽이 음수, 시계 방향 회전
* @return 지정된 점이 지정된 각도 내부에 있는지 여부, 1: 내부, 0: 외부
*/
uint8_t OLED_IsInAngle(int16_t X, int16_t Y, int16_t StartAngle, int16_t EndAngle)
{
int16_t PointAngle;
PointAngle = atan2(Y, X) / 3.14 * 180; // 지정된 점의 라디안을 계산하고 도 단위로 변환
if (StartAngle < EndAngle) // 시작 각도가 종료 각도보다 작은 경우
{
/*지정된 각도가 시작 및 종료 각도 사이에 있으면 지정된 점이 지정된 각도에 있다고 판단*/
if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
return 1;
}
} else // 시작 각도가 종료 각도보다 큰 경우
{
/*지정된 각도가 시작 각도보다 크거나 종료 각도보다 작으면 지정된 점이 지정된 각도에 있다고 판단*/
if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
return 1;
}
}
return 0; // 위 조건을 만족하지 않으면 지정된 점이 지정된 각도에 없다고 판단
}
/*********************유틸리티 함수*/
/*기능 함수*********************/
/**
* @brief OLED 프레임 버퍼 배열을 OLED 화면에 업데이트
* @param 없음
* @return 없음
* @note 모든 표시 함수는 OLED 프레임 버퍼 배열에만 읽기/쓰기를 수행
* 이후 OLED_Update 함수 또는 OLED_UpdateArea 함수를 호출
* 프레임 버퍼 배열의 데이터를 OLED 하드웨어로 전송하여 표시
* 따라서 표시 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_Update(void)
{
uint8_t j;
/*각 페이지를 순회*/
for (j = 0; j < 8; j++) {
/*커서 위치를 각 페이지의 첫 번째 열로 설정*/
OLED_SetCursor(j, 0);
/*128개의 데이터를 연속으로 써서 프레임 버퍼 배열의 데이터를 OLED 하드웨어에 씀*/
OLED_WriteData(OLED_DisplayBuf[j], 128);
}
}
/**
* @brief OLED 프레임 버퍼 배열을 OLED 화면에 부분 업데이트
* @param X 지정 영역의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 영역의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Width 지정 영역의 너비, 범위: 0-128
* @param Height 지정 영역의 높이, 범위: 0-64
* @return 없음
* @note 이 함수는 매개변수로 지정된 영역을 최소한으로 업데이트
* 업데이트 영역의 Y축이 일부 페이지만 포함하는 경우, 동일 페이지의 나머지 부분도 함께 업데이트됨
* @note 모든 표시 함수는 OLED 프레임 버퍼 배열에만 읽기/쓰기를 수행
* 이후 OLED_Update 함수 또는 OLED_UpdateArea 함수를 호출
* 프레임 버퍼 배열의 데이터를 OLED 하드웨어로 전송하여 표시
* 따라서 표시 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t j;
/*매개변수 검사, 지정된 영역이 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
/*지정된 영역에 관련된 페이지를 순회*/
/*(Y + Height - 1) / 8 + 1의 목적은 (Y + Height) / 8을 올림하는 것*/
for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
/*커서 위치를 관련 페이지의 지정된 열로 설정*/
OLED_SetCursor(j, X);
/*Width개의 데이터를 연속으로 써서 프레임 버퍼 배열의 데이터를 OLED 하드웨어에 씀*/
OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
}
}
/**
* @brief OLED 프레임 버퍼 배열을 모두 지움
* @param 없음
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++) // 8페이지를 순회
{
for (i = 0; i < 128; i++) // 128열을 순회
{
OLED_DisplayBuf[j][i] = 0x00; // 프레임 버퍼 배열의 데이터를 모두 0으로 지움
}
}
}
/**
* @brief OLED 프레임 버퍼 배열을 부분적으로 지움
* @param X 지정 영역의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 영역의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Width 지정 영역의 너비, 범위: 0-128
* @param Height 지정 영역의 높이, 범위: 0-64
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t i, j;
/*매개변수 검사, 지정된 영역이 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
for (j = Y; j < Y + Height; j++) // 지정된 페이지를 순회
{
for (i = X; i < X + Width; i++) // 지정된 열을 순회
{
OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // 프레임 버퍼 배열의 지정된 데이터를 지움
}
}
}
/**
* @brief OLED 프레임 버퍼 배열을 모두 반전
* @param 없음
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_Reverse(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++) // 8페이지를 순회
{
for (i = 0; i < 128; i++) // 128열을 순회
{
OLED_DisplayBuf[j][i] ^= 0xFF; // 프레임 버퍼 배열의 데이터를 모두 반전
}
}
}
/**
* @brief OLED 프레임 버퍼 배열을 부분적으로 반전
* @param X 지정 영역의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 영역의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Width 지정 영역의 너비, 범위: 0-128
* @param Height 지정 영역의 높이, 범위: 0-64
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t i, j;
/*매개변수 검사, 지정된 영역이 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
for (j = Y; j < Y + Height; j++) // 지정된 페이지를 순회
{
for (i = X; i < X + Width; i++) // 지정된 열을 순회
{
OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // 프레임 버퍼 배열의 지정된 데이터를 반전
}
}
}
/**
* @brief OLED에 한 문자를 표시
* @param X 지정 문자의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 문자의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Char 표시할 문자, 범위: ASCII 코드의 표시 가능한 문자
* @param FontSize 글꼴 크기 지정
* 범위: OLED_8X16\t\t너비 8픽셀, 높이 16픽셀
* OLED_6X8\t\t너비 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
if (FontSize == OLED_8X16) // 글꼴이 너비 8픽셀, 높이 16픽셀인 경우
{
/*ASCII 글꼴 라이브러리 OLED_F8x16의 지정된 데이터를 8*16 이미지 형식으로 표시*/
OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
} else if (FontSize == OLED_6X8) // 글꼴이 너비 6픽셀, 높이 8픽셀인 경우
{
/*ASCII 글꼴 라이브러리 OLED_F6x8의 지정된 데이터를 6*8 이미지 형식으로 표시*/
OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
}
}
/**
* @brief OLED에 문자열을 표시
* @param X 지정 문자열의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 문자열의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param String 표시할 문자열, 범위: ASCII 코드의 표시 가능한 문자로 구성된 문자열
* @param FontSize 글꼴 크기 지정
* 범위: OLED_8X16\t\t너비 8픽셀, 높이 16픽셀
* OLED_6X8\t\t너비 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++) // 문자열의 각 문자를 순회
{
/*OLED_ShowChar 함수를 호출하여 각 문자를 순서대로 표시*/
OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
}
}
/**
* @brief OLED에 숫자 표시(십진수, 양의 정수)
* @param X 지정 숫자의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 숫자의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Number 표시할 숫자, 범위: 0-4294967295
* @param Length 숫자의 길이, 범위: 0-10
* @param FontSize 글꼴 크기 지정
* 범위: OLED_8X16\t\t너비 8픽셀, 높이 16픽셀
* OLED_6X8\t\t너비 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
for (i = 0; i < Length; i++) // 숫자의 각 자리를 순회
{
/*OLED_ShowChar 함수를 호출하여 각 숫자를 순서대로 표시*/
/*Number / OLED_Pow(10, Length - i - 1) % 10은 십진법으로 숫자의 각 자리를 추출*/
/*+ '0'은 숫자를 문자 형식으로 변환*/
OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
}
}
/**
* @brief OLED에 부호 있는 숫자 표시(십진수, 정수)
* @param X 지정 숫자의 왼쪽 위 모서리의 가로 좌표, 범위: 0-127
* @param Y 지정 숫자의 왼쪽 위 모서리의 세로 좌표, 범위: 0-63
* @param Number 표시할 숫자, 범위: -2147483648-2147483647
* @param Length 숫자의 길이, 범위: 0-10
* @param FontSize 글꼴 크기 지정
* 범위: OLED_8X16\t\t너비 8픽셀, 높이 16픽셀
* OLED_6X8\t\t너비 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수를 호출한 후 실제로 화면에 표시하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowSignedNum(uint8_t X, uint8_t Y, int32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0) // 숫자가 0 이상인 경우
{
OLED_ShowChar(X, Y, '+', FontSize); // + 기호 표시
Number1 = Number; // Number1은 Number와 같음
} else // 숫자가 0 미만인 경우
{
OLED_ShowChar(X, Y, '-', FontSize); // - 기호 표시
Number1 = -Number; // Number1은 Number의 음수
}
}for (i = 0; i < Length; i++) // 숫자의 각 자릿수를 순회
{
/*OLED_ShowChar 함수를 호출하여 각 숫자를 차례로 표시*/
/*Number1 / OLED_Pow(10, Length - i - 1) % 10은 10진법으로 숫자의 각 자릿수를 추출*/
/*+ '0'은 숫자를 문자 형식으로 변환*/
OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
}
}
/**
* @brief OLED에 16진수 표시(16진법, 양의 정수)
* @param X 숫자 왼쪽 상단의 x좌표, 범위: 0~127
* @param Y 숫자 왼쪽 상단의 y좌표, 범위: 0~63
* @param Number 표시할 숫자, 범위: 0x00000000~0xFFFFFFFF
* @param Length 숫자 길이, 범위: 0~8
* @param FontSize 글자 크기
* 범위: OLED_8X16 폭 8픽셀, 높이 16픽셀
* OLED_6X8 폭 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowHexNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++) // 숫자의 각 자릿수를 순회
{
/*16진법으로 숫자의 각 자릿수를 추출*/
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10) // 한 자릿수가 10 미만
{
/*OLED_ShowChar 함수를 호출하여 이 숫자를 표시*/
/*+ '0'은 숫자를 문자 형식으로 변환*/
OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
} else // 한 자릿수가 10 이상
{
/*OLED_ShowChar 함수를 호출하여 이 숫자를 표시*/
/*+ 'A'는 숫자를 A부터 시작하는 16진 문자로 변환*/
OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
}
}
}
/**
* @brief OLED에 2진수 표시(2진법, 양의 정수)
* @param X 숫자 왼쪽 상단의 x좌표, 범위: 0~127
* @param Y 숫자 왼쪽 상단의 y좌표, 범위: 0~63
* @param Number 표시할 숫자, 범위: 0x00000000~0xFFFFFFFF
* @param Length 숫자 길이, 범위: 0~16
* @param FontSize 글자 크기
* 범위: OLED_8X16 폭 8픽셀, 높이 16픽셀
* OLED_6X8 폭 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowBinNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
for (i = 0; i < Length; i++) // 숫자의 각 자릿수를 순회
{
/*OLED_ShowChar 함수를 호출하여 각 숫자를 차례로 표시*/
/*Number / OLED_Pow(2, Length - i - 1) % 2는 2진법으로 숫자의 각 자릿수를 추출*/
/*+ '0'은 숫자를 문자 형식으로 변환*/
OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
}
}
/**
* @brief OLED에 부동소수점 표시(10진법, 소수)
* @param X 숫자 왼쪽 상단의 x좌표, 범위: 0-127
* @param Y 숫자 왼쪽 상단의 y좌표, 범위: 0-63
* @param Number 표시할 숫자, 범위: -4294967295.0-4294967295.0
* @param IntLength 정수부 길이, 범위: 0-10
* @param FraLength 소수부 길이, 범위: 0-9, 소수는 반올림하여 표시
* @param FontSize 글자 크기
* 범위: OLED_8X16 폭 8픽셀, 높이 16픽셀
* OLED_6X8 폭 6픽셀, 높이 8픽셀
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowFloatNum(uint8_t X, uint8_t Y, double Number, uint8_t IntLength, uint8_t FraLength, uint8_t FontSize)
{
uint32_t PowNum, IntNum, FraNum;
if (Number >= 0) // 숫자가 0 이상
{
OLED_ShowChar(X, Y, '+', FontSize); // + 기호 표시
} else // 숫자가 0 미만
{
OLED_ShowChar(X, Y, '-', FontSize); // - 기호 표시
Number = -Number; // Number 음수 처리
}
/*정수부와 소수부 추출*/
IntNum = Number; // 정수 변수에 직접 대입하여 정수 추출
Number -= IntNum; // Number의 정수 부분 제거, 이후 소수를 정수로 올릴 때 수가 너무 커지는 오류 방지
PowNum = OLED_Pow(10, FraLength); // 지정된 소수 자릿수에 따라 곱셈 계수 결정
FraNum = round(Number * PowNum); // 소수를 정수로 올리고 반올림하여 표시 오차 방지
IntNum += FraNum / PowNum; // 반올림으로 인해 올림이 발생하면 정수에 추가
/*정수부 표시*/
OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);
/*소수점 표시*/
OLED_ShowChar(X + (IntLength + 1) * FontSize, Y, '.', FontSize);
/*소수부 표시*/
OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}
/**
* @brief OLED에 한자 문자열 표시
* @param X 문자열 왼쪽 상단의 x좌표, 범위: 0-127
* @param Y 문자열 왼쪽 상단의 y좌표, 범위: 0-63
* @param Chinese 표시할 한자 문자열, 범위: 반각 문자 없이 전체가 한자 또는 전각 문자
* 표시할 한자는 OLED_Data.c의 OLED_CF16x16 배열에 정의되어 있어야 함
* 지정된 한자를 찾지 못하면 기본 그림(물음표가 들어간 사각)이 표시됨
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowChinese(uint8_t X, uint8_t Y, char *Chinese)
{
uint8_t pChinese = 0;
uint8_t pIndex;
uint8_t i;
char SingleChinese[OLED_CHN_CHAR_WIDTH + 1] = {0}; // UTF8 인코딩은 3바이트, +1은 \0 종료 문자
for (i = 0; Chinese[i] != '\0'; i++) // 한자 문자열을 순회
{
SingleChinese[pChinese] = Chinese[i]; // 한자 문자열 데이터를 단일 한자 배열에 추출
pChinese++; // 카운트 증가
/*추출 횟수가 OLED_CHN_CHAR_WIDTH에 도달하면 하나의 완전한 한자를 추출한 것*/
if (pChinese >= OLED_CHN_CHAR_WIDTH) {
SingleChinese[pChinese + 1] = '\0'; // 한자 뒤에 널 문자 추가, 종료 표시
pChinese = 0; // 카운트 초기화
/*전체 한자 글꼴 라이브러리를 순회하며 일치하는 한자 찾기*/
/*마지막 한자(빈 문자열로 정의)를 찾으면 글꼴에 정의되지 않은 것으로 간주하고 탐색 중지*/
for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
/*일치하는 한자 찾음*/
if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
break; // 루프 탈출, 이때 pIndex는 지정된 한자의 인덱스 값
}
}
/*한자 글꼴 라이브러리 OLED_CF16x16의 지정 데이터를 16*16 이미지 형식으로 표시*/
OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
}
}
}
/**
* @brief OLED에 이미지 표시
* @param X 이미지 왼쪽 상단의 x좌표, 범위: 0-127
* @param Y 이미지 왼쪽 상단의 y좌표, 범위: 0-63
* @param Width 이미지 폭, 범위: 0-128
* @param Height 이미지 높이, 범위: 0-64
* @param Image 표시할 이미지
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
uint8_t i, j;
/*매개변수 검사, 지정된 이미지가 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return; }
if (Y > 63) { return; }
/*이미지가 위치한 영역을 지움*/
OLED_ClearArea(X, Y, Width, Height);
/*지정된 이미지가 관련된 페이지를 순회*/
/*(Height - 1) / 8 + 1의 목적은 Height / 8 후 올림*/
for (j = 0; j < (Height - 1) / 8 + 1; j++) {
/*지정된 이미지가 관련된 열을 순회*/
for (i = 0; i < Width; i++) {
/*경계를 벗어나면 표시 건너뜀*/
if (X + i > 127) { break; }
if (Y / 8 + j > 7) { return; }
/*현재 페이지에서 이미지 표시*/
OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);
/*경계를 벗어나면 표시 건너뜀*/
/*continue를 사용하는 이유는 다음 페이지가 경계를 벗어날 때 이전 페이지의 나머지 내용은 계속 표시해야 함*/
if (Y / 8 + j + 1 > 7) { continue; }
/*다음 페이지에서 이미지 표시*/
OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
}
}
}
/**
* @brief OLED에 printf 함수로 형식화된 문자열 출력
* @param X 형식화된 문자열 왼쪽 상단의 x좌표, 범위: 0-127
* @param Y 형식화된 문자열 왼쪽 상단의 y좌표, 범위: 0-63
* @param FontSize 글자 크기
* 범위: OLED_8X16 폭 8픽셀, 높이 16픽셀
* OLED_6X8 폭 6픽셀, 높이 8픽셀
* @param format 표시할 형식화된 문자열, 범위: ASCII 가시 문자로 구성된 문자열
* @param ... 형식화된 문자열 매개변수 목록
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
char String[30]; // 문자 배열 정의
va_list arg; // 가변 인자 목록 데이터 타입의 변수 arg 정의
va_start(arg, format); // format부터 시작하여 인자 목록을 arg 변수로 받음
vsprintf(String, format, arg); // vsprintf로 형식화된 문자열과 인자 목록을 문자 배열에 출력
va_end(arg); // arg 변수 종료
OLED_ShowString(X, Y, String, FontSize); // OLED에 문자 배열(문자열) 표시
}
/**
* @brief OLED에 지정된 위치에 점 그리기
* @param X 점의 x좌표, 범위: 0-127
* @param Y 점의 y좌표, 범위: 0-63
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
/*매개변수 검사, 지정된 위치가 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return; }
if (Y > 63) { return; }
/*디스플레이 버퍼 배열의 지정 위치 1비트 데이터를 1로 설정*/
OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}
/**
* @brief OLED에 지정된 위치의 점 값 얻기
* @param X 점의 x좌표, 범위: 0-127
* @param Y 점의 y좌표, 범위: 0-63
* @return 지정 위치 점이 켜져 있는지 여부, 1: 켜짐, 0: 꺼짐
*/
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
/*매개변수 검사, 지정된 위치가 화면 범위를 벗어나지 않도록 보장*/
if (X > 127) { return 0; }
if (Y > 63) { return 0; }
/*지정 위치 데이터 판단*/
if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
return 1; // 1이면 1 반환
}
return 0; // 아니면 0 반환
}
/**
* @brief OLED에 선 그리기
* @param X0 한 끝점의 x좌표, 범위: 0-127
* @param Y0 한 끝점의 y좌표, 범위: 0-63
* @param X1 다른 끝점의 x좌표, 범위: 0-127
* @param Y1 다른 끝점의 y좌표, 범위: 0-63
* @return 없음
* @note 이 함수 호출 후 실제 화면에 반영하려면 업데이트 함수를 호출해야 함
*/
void OLED_DrawLine(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1)
{
int16_t x, y, dx, dy, d, incrE, incrNE, temp;
int16_t x0 = X0, y0 = Y0, x1 = X1, y1 = Y1;
uint8_t yflag = 0, xyflag = 0;if (y0 == y1) // 가로선은 별도 처리
{
/* 0번 점의 X좌표가 1번 점의 X좌표보다 크면 두 점의 X좌표를 교환 */
if (x0 > x1) {
temp = x0;
x0 = x1;
x1 = temp;
}
/* X좌표를 순회 */
for (x = x0; x <= x1; x++) {
OLED_DrawPoint(x, y0); // 점을 순서대로 그림
}
} else if (x0 == x1) // 세로선은 별도 처리
{
/* 0번 점의 Y좌표가 1번 점의 Y좌표보다 크면 두 점의 Y좌표를 교환 */
if (y0 > y1) {
temp = y0;
y0 = y1;
y1 = temp;
}
/* Y좌표를 순회 */
for (y = y0; y <= y1; y++) {
OLED_DrawPoint(x0, y); // 점을 순서대로 그림
}
} else // 기울기 있는 선
{
/* Bresenham 알고리즘을 사용하여 직선을 그림, 비용이 큰 부동소수점 연산을 피해 효율이 높음 */
/* 참고 문서: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf */
/* 참고 튜토리얼: https://www.bilibili.com/video/BV1364y1d7Lo */
if (x0 > x1) // 0번 점의 X좌표가 1번 점의 X좌표보다 큰 경우
{
/* 두 점의 좌표를 교환 */
/* 교환 후 선 그리기에 영향은 없지만, 그리는 방향이 제1·2·3·4 사분면에서 제1·4 사분면으로 바뀜 */
temp = x0;
x0 = x1;
x1 = temp;
temp = y0;
y0 = y1;
y1 = temp;
}
if (y0 > y1) // 0번 점의 Y좌표가 1번 점의 Y좌표보다 큰 경우
{
/* Y좌표를 음수로 취함 */
/* 취한 후 선 그리기에 영향은 있지만, 그리는 방향이 제1·4 사분면에서 제1 사분면으로 바뀜 */
y0 = -y0;
y1 = -y1;
/* yflag 플래그를 설정하여 현재 변환을 기억, 이후 실제 선 그릴 때 좌표를 다시 되돌림 */
yflag = 1;
}
if (y1 - y0 > x1 - x0) // 선의 기울기가 1보다 큰 경우
{
/* X좌표와 Y좌표를 교환 */
/* 교환 후 선 그리기에 영향은 있지만, 그리는 방향이 제1 사분면 0~90도 범위에서 0~45도 범위로 바뀜 */
temp = x0;
x0 = y0;
y0 = temp;
temp = x1;
x1 = y1;
y1 = temp;
/* xyflag 플래그를 설정하여 현재 변환을 기억, 이후 실제 선 그릴 때 좌표를 다시 되돌림 */
xyflag = 1;
}
/* 아래는 Bresenham 알고리즘으로 직선을 그리는 부분 */
/* 알고리즘 요구사항: 선 그리는 방향은 반드시 제1 사분면 0~45도 범위여야 함 */
dx = x1 - x0;
dy = y1 - y0;
incrE = 2 * dy;
incrNE = 2 * (dy - dx);
d = 2 * dy - dx;
x = x0;
y = y0;
/* 시작점을 그리면서 플래그를 판단해 좌표를 되돌림 */
if (yflag && xyflag) {
OLED_DrawPoint(y, -x);
} else if (yflag) {
OLED_DrawPoint(x, -y);
} else if (xyflag) {
OLED_DrawPoint(y, x);
} else {
OLED_DrawPoint(x, y);
}
while (x < x1) // X축의 각 점을 순회
{
x++;
if (d < 0) // 다음 점이 현재 점의 동쪽
{
d += incrE;
} else // 다음 점이 현재 점의 북동쪽
{
y++;
d += incrNE;
}
/* 각 점을 그리면서 플래그를 판단해 좌표를 되돌림 */
if (yflag && xyflag) {
OLED_DrawPoint(y, -x);
} else if (yflag) {
OLED_DrawPoint(x, -y);
} else if (xyflag) {
OLED_DrawPoint(y, x);
} else {
OLED_DrawPoint(x, y);
}
}
}
}
/**
* @brief OLED 사각형
* @param X 사각형 왼쪽 위 모서리의 X좌표, 범위: 0~127
* @param Y 사각형 왼쪽 위 모서리의 Y좌표, 범위: 0~63
* @param Width 사각형의 너비, 범위: 0~128
* @param Height 사각형의 높이, 범위: 0~64
* @param IsFilled 사각형을 채울지 여부
* 범위: OLED_UNFILLED\t\t채우지 않음
* OLED_FILLED\t\t\t채움
* @return 없음
* @note 이 함수 호출 후 실제 화면에 표시하려면 업데이트 함수를 추가로 호출해야 함
*/
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
uint8_t i, j;
if (!IsFilled) // 사각형을 채우지 않음
{
/* 위아래 X좌표를 순회하며 사각형 위아래 두 선을 그림 */
for (i = X; i < X + Width; i++) {
OLED_DrawPoint(i, Y);
OLED_DrawPoint(i, Y + Height - 1);
}
/* 좌우 Y좌표를 순회하며 사각형 좌우 두 선을 그림 */
for (i = Y; i < Y + Height; i++) {
OLED_DrawPoint(X, i);
OLED_DrawPoint(X + Width - 1, i);
}
} else // 사각형을 채움
{
/* X좌표를 순회 */
for (i = X; i < X + Width; i++) {
/* Y좌표를 순회 */
for (j = Y; j < Y + Height; j++) {
/* 지정 영역에 점을 찍어 사각형을 가득 채움 */
OLED_DrawPoint(i, j);
}
}
}
}
/**
* @brief OLED 삼각형
* @param X0 첫 번째 꼭짓점의 X좌표, 범위: 0-127
* @param Y0 첫 번째 꼭짓점의 Y좌표, 범위: 0-63
* @param X1 두 번째 꼭짓점의 X좌표, 범위: 0-127
* @param Y1 두 번째 꼭짓점의 Y좌표, 범위: 0-63
* @param X2 세 번째 꼭짓점의 X좌표, 범위: 0-127
* @param Y2 세 번째 꼭짓점의 Y좌표, 범위: 0-63
* @param IsFilled 삼각형을 채울지 여부
* 범위: OLED_UNFILLED\t\t채우지 않음
* OLED_FILLED\t\t\t채움
* @return 없음
* @note 이 함수 호출 후 실제 화면에 표시하려면 업데이트 함수를 추가로 호출해야 함
*/
void OLED_DrawTriangle(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t X2, uint8_t Y2, uint8_t IsFilled)
{
uint8_t minx = X0, miny = Y0, maxx = X0, maxy = Y0;
uint8_t i, j;
int16_t vx[] = {X0, X1, X2};
int16_t vy[] = {Y0, Y1, Y2};
if (!IsFilled) // 삼각형을 채우지 않음
{
/* 선 그리기 함수를 호출해 세 점을 직선으로 연결 */
OLED_DrawLine(X0, Y0, X1, Y1);
OLED_DrawLine(X0, Y0, X2, Y2);
OLED_DrawLine(X1, Y1, X2, Y2);
} else // 삼각형을 채움
{
/* 세 점 중 가장 작은 X, Y좌표를 찾음 */
if (X1 < minx) { minx = X1; }
if (X2 < minx) { minx = X2; }
if (Y1 < miny) { miny = Y1; }
if (Y2 < miny) { miny = Y2; }
/* 세 점 중 가장 큰 X, Y좌표를 찾음 */
if (X1 > maxx) { maxx = X1; }
if (X2 > maxx) { maxx = X2; }
if (Y1 > maxy) { maxy = Y1; }
if (Y2 > maxy) { maxy = Y2; }
/* 최소~최대 좌표 사이의 사각형이 채워야 할 영역의 후보 */
/* 이 영역의 모든 점을 순회 */
/* X좌표를 순회 */
for (i = minx; i <= maxx; i++) {
/* Y좌표를 순회 */
for (j = miny; j <= maxy; j++) {
/* OLED_pnpoly를 호출해 지정 점이 삼각형 내부인지 판단 */
/* 내부이면 점을 찍고, 아니면 무시 */
if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
}
}
}
}
/**
* @brief OLED 원
* @param X 원의 중심 X좌표, 범위: 0~127
* @param Y 원의 중심 Y좌표, 범위: 0~63
* @param Radius 원의 반지름, 범위: 0~255
* @param IsFilled 원을 채울지 여부
* 범위: OLED_UNFILLED\t\t채우지 않음
* OLED_FILLED\t\t\t채움
* @return 없음
* @note 이 함수 호출 후 실제 화면에 표시하려면 업데이트 함수를 추가로 호출해야 함
*/
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
int16_t x, y, d, j;
/* Bresenham 알고리즘을 사용해 원을 그림, 비용이 큰 부동소수점 연산을 피해 효율이 높음 */
/* 참고 문서: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf */
/* 참고 튜토리얼: https://www.bilibili.com/video/BV1VM4y1u7wJ */
d = 1 - Radius;
x = 0;
y = Radius;
/* 8분원 각각의 시작 점을 그림 */
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X + y, Y + x);
OLED_DrawPoint(X - y, Y - x);
if (IsFilled) // 원을 채움
{
/* 시작 점의 Y좌표를 순회 */
for (j = -y; j < y; j++) {
/* 지정 영역에 점을 찍어 원의 일부를 채움 */
OLED_DrawPoint(X, Y + j);
}
}
while (x < y) // X축의 각 점을 순회
{
x++;
if (d < 0) // 다음 점이 현재 점의 동쪽
{
d += 2 * x + 1;
} else // 다음 점이 현재 점의 남동쪽
{
y--;
d += 2 * (x - y) + 1;
}
/* 8분원 각 점을 그림 */
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X + y, Y + x);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - y, Y - x);
OLED_DrawPoint(X + x, Y - y);
OLED_DrawPoint(X + y, Y - x);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X - y, Y + x);
if (IsFilled) // 원을 채움
{
/* 중간 부분을 순회 */
for (j = -y; j < y; j++) {
/* 지정 영역에 점을 찍어 원의 일부를 채움 */
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
/* 양쪽 부분을 순회 */
for (j = -x; j < x; j++) {
/* 지정 영역에 점을 찍어 원의 일부를 채움 */
OLED_DrawPoint(X - y, Y + j);
OLED_DrawPoint(X + y, Y + j);
}
}
}
}
/**
* @brief OLED 타원
* @param X 타원의 중심 X좌표, 범위: 0~127
* @param Y 타원의 중심 Y좌표, 범위: 0~63
* @param A 타원의 가로 반축 길이, 범위: 0~255
* @param B 타원의 세로 반축 길이, 범위: 0~255
* @param IsFilled 타원을 채울지 여부
* 범위: OLED_UNFILLED\t\t채우지 않음
* OLED_FILLED\t\t\t채움
* @return 없음
* @note 이 함수 호출 후 실제 화면에 표시하려면 업데이트 함수를 추가로 호출해야 함
*/
void OLED_DrawEllipse(uint8_t X, uint8_t Y, uint8_t A, uint8_t B, uint8_t IsFilled)
{
int16_t x, y, j;
int16_t a = A, b = B;
float d1, d2;
/* Bresenham 알고리즘을 사용해 타원을 그림, 일부 비용이 큰 부동소수점 연산을 피해 효율이 높음 */
/* 참고 링크: https://blog.csdn.net/myf_666/article/details/128167392 */
x = 0;
y = b;
d1 = b * b + a * a * (-b + 0.5);```c
if (IsFilled) // 지정 타원 채우기
{
/*시작점 Y좌표 순회*/
for (j = -y; j < y; j++) {
/*지정 영역에 점 그리기, 부분 타원 채우기*/
OLED_DrawPoint(X, Y + j);
OLED_DrawPoint(X, Y + j);
}
}
/*타원호 시작점 그리기*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
/*타원 중간 부분 그리기*/
while (b * b * (x + 1) < a * a * (y - 0.5)) {
if (d1 <= 0) // 다음 점이 현재 점 동쪽
{
d1 += b * b * (2 * x + 3);
} else // 다음 점이 현재 점 남동쪽
{
d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
y--;
}
x++;
if (IsFilled) // 지정 타원 채우기
{
/*중간 부분 순회*/
for (j = -y; j < y; j++) {
/*지정 영역에 점 그리기, 부분 타원 채우기*/
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
}
/*타원 중간 부분 호 그리기*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
}
/*타원 양쪽 부분 그리기*/
d2 = b * b * (x + 0.5) * (x + 0.5) + a * a * (y - 1) * (y - 1) - a * a * b * b;
while (y > 0) {
if (d2 <= 0) // 다음 점이 현재 점 동쪽
{
d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
x++;
} else // 다음 점이 현재 점 남동쪽
{
d2 += a * a * (-2 * y + 3);
}
y--;
if (IsFilled) // 지정 타원 채우기
{
/*양쪽 부분 순회*/
for (j = -y; j < y; j++) {
/*지정 영역에 점 그리기, 부분 타원 채우기*/
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
}
/*타원 양쪽 부분 호 그리기*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
}
}
/**
* @brief OLED 호 그리기
* @param X 지정 호의 원점 X좌표, 범위: 0~127
* @param Y 지정 호의 원점 Y좌표, 범위: 0~63
* @param Radius 지정 호의 반지름, 범위: 0~255
* @param StartAngle 지정 호의 시작 각도, 범위: -180~180
* 수평 오른쪽이 0도, 수평 왼쪽이 180도 또는 -180도, 아래쪽이 양수, 위쪽이 음수, 시계 방향 회전
* @param EndAngle 지정 호의 종료 각도, 범위: -180~180
* 수평 오른쪽이 0도, 수평 왼쪽이 180도 또는 -180도, 아래쪽이 양수, 위쪽이 음수, 시계 방향 회전
* @param IsFilled 지정 호 채우기 여부, 채우면 부채꼴
* 범위: OLED_UNFILLED\t\t채우지 않음
* OLED_FILLED\t\t\t채움
* @return 없음
* @note 이 함수 호출 후 실제 화면에 표시하려면 업데이트 함수 호출 필요
*/
void OLED_DrawArc(uint8_t X, uint8_t Y, uint8_t Radius, int16_t StartAngle, int16_t EndAngle, uint8_t IsFilled)
{
int16_t x, y, d, j;
/*이 함수는 Bresenham 알고리즘으로 원을 그리는 방법 차용*/
d = 1 - Radius;
x = 0;
y = Radius;
/*원을 그릴 때 각 점이 지정 각도 내에 있는지 판단, 있으면 점 그리기, 없으면 무시*/
if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
if (IsFilled) // 지정 호 채우기
{
/*시작점 Y좌표 순회*/
for (j = -y; j < y; j++) {
/*채우는 원의 각 점이 지정 각도 내에 있는지 판단, 있으면 점 그리기, 없으면 무시*/
if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
}
}
while (x < y) // X축의 각 점 순회
{
x++;
if (d < 0) // 다음 점이 현재 점 동쪽
{
d += 2 * x + 1;
} else // 다음 점이 현재 점 남동쪽
{
y--;
d += 2 * (x - y) + 1;
}
/*원을 그릴 때 각 점이 지정 각도 내에 있는지 판단, 있으면 점 그리기, 없으면 무시*/
if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
if (OLED_IsInAngle(x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y - y); }
if (OLED_IsInAngle(y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y - x); }
if (OLED_IsInAngle(-x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + y); }
if (OLED_IsInAngle(-y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + x); }
if (IsFilled) // 지정 호 채우기
{
/*중간 부분 순회*/
for (j = -y; j < y; j++) {
/*채우는 원의 각 점이 지정 각도 내에 있는지 판단, 있으면 점 그리기, 없으면 무시*/
if (OLED_IsInAngle(x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + j); }
if (OLED_IsInAngle(-x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + j); }
}
/*양쪽 부분 순회*/
for (j = -x; j < x; j++) {
/*채우는 원의 각 점이 지정 각도 내에 있는지 판단, 있으면 점 그리기, 없으면 무시*/
if (OLED_IsInAngle(-y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + j); }
if (OLED_IsInAngle(y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + j); }
}
}
}
}
/*********************기능 함수*/
/*****************강협기술|저작권所有****************/
/*****************jiangxiekeji.com*****************/
추천 읽기
- 고성능비·저렴한 VPS/클라우드 서버 추천: https://blog.zeruns.com/archives/383.html
- 3상 전력采集기 만들어 오픈소스 공개, 집 전력 사용량 쉽게 모니터링: https://blog.zeruns.com/archives/771.html
- 마인크래프트 서버 열기 튜토리얼: https://blog.zeruns.com/tag/mc/
- 팔월드 서버 열기 튜토리얼: https://blog.zeruns.com/tag/PalWorld/
- 루이덴 RD6012P CNC 가변 전원 간단 개봉 리뷰, 60V 12A CNC 직류 전원: https://blog.zeruns.com/archives/740.html
- 톱초우 P1SC 3D 프린터 개봉 체험: https://blog.zeruns.com/archives/770.html
- 브랜드·종류별 커패시터·인덕터 실제 측정 비교(D값·Q값·ESR·X): https://blog.zeruns.com/archives/765.html








