2018년 8월 8일 수요일

OLED에 한글을 출력하자.

1. 늘 고생이다.

 Display 장치에 한글을 표현하기 위해서 고생한 경우가 한두번이 아니다. DOS 시절부터 프로그래밍을 했고, 게임도 만들었던 세대라 한글이 늘 문제였다. 꽤 오래전에 마지막으로 한글 출력 때문에 고생한 것은 Sony의 PSP 때문이었다. PSP용 Home Brew가 시작 되던 시기에 그당시 좋은 Display를 가지고 있는 게임기 였지만, 한글로된  txt 파일을 볼 수 있는 방법은 없었다. 게임을 주로 하는 게임기였지만, 가끔 소설을 보고 싶을 때가 있어서 결국 한글 출력을 만들게 되었다. 그때 당시 HanView라는 이름으로 릴리즈하기 시작했고, 꽤 많은 인기가 있었다. 마지막 부분에 전체 소스를 공개하였고, 그 뒤로 내가 만든 라이브러리는 PSP의 동영상 플레이어의 제작자가 자막 표시용으로 사용한다는 이야기를 얼핏 들은것 같다.

최종 결과 (사진을 제대로 못 찍었군. ㅠㅠ)

2. 조합형이냐 완성형이냐 이것이 문제로다.

 PSP 시절에도 이게 문제였다. 처음에는 조합형으로 시작했다가 결국 완성형으로 다시 만들어 버린 기억이 있다. (물론 이유는 txt파일이라도 한자나, 일본글자 등이 많기 때문에 unicode에 포함된 약 6만5천자를 표현하기 위하여 완성형으로 돌아 서게 되었다)
 ESP2866 이라도 그렇게 여유롭지 못한 메모리 및 스케치 영역 때문에 처음 기획은 조합형 이었다.  조합형 폰트의 경우 초성 8벌, 중성 4벌, 종성 4벌로 구성되기 때문에 360개 정도의 폰트 정보만 있으면, 평소에 사용되는 모든 글자를 표현할 수 있다.  하여간 아주 오래전 시절을 떠올리며 조합형 글자 표현이 가능한 한글 라이브러리를 만들었다. 하지만 최근 10년을 넘게 완성형 글자만을 보아와서 그런지,  완성형 폰트의 미려함을 포기할 수가 없었다. (지금 조합형 글자들을 보면 먼가 어설퍼 보인다... 예전에는 그런 생각을 가지지 않았지만...)
 결국 완성형 폰트로 다시 시작 하게 되었다.

3. 얼마 만큼의 문자수를 지원할까 그것도 문제로다.

 최신 버전의 Arduino IDE는 한글은 UTF8로 저장된다.  UTF8은  Unicode와 일맥상통하고, 결국 6만자 이상의 폰트 정보가 필요한 상황이지만, 결국 활용 빈도가 높은 EUC-KR에 포함된 한글의 2350글자(0xb0a1 ~ ) 만을 표시 하기로 결정했다. 2350글자라는 것은 이 블로그에 글을 적을때 사용하는 모든 종류의 한글 글자수 이다. 이것 이상의 글자를 블로그에서 사용하지 않는다. 

4. 폰트정보를 어디에 적재할 것인가 이것 역시 문제로다.

 이론상 16px X 16px 한글 1글자의 폰트 정보는 32바이트가 필요하고, 영숫자는 16바이트의 정보가 필요하다.  대충 76KB의 공간이 필요하다.  ESP8266의 경우 스케치 공간에 올릴수는 있다. 하지만 이건 16px 크기의 폰트 1종류에 필요한 공간이며, 폰트 크기가 3종류만 되어도 여러가지 불편한 진실이 되어 버린다. 그래서 SPIFSS 공간에 올리기로 결정하였다. 
 이전 포스트를 통하여 SPIFSS공간에 데이터를 올리는 방법을 소개한 적이 있다.
https://andy-power.blogspot.com/2018/07/esp8266-esp32-spifss.html

5. 폰트 정보를 만들다.

 그나마 편리한 언어를 이용하여 폰트 저장 하는 프로그램을 만들었다. 폰트 크기는 12px, 14px, 16px의 3개 크기로 만들었다. 폰트 크기가 12px이하가 되면 한글은 가독성이 아주 떨어져서 특별히 만들지 않았다.  폰트가 12px 크기인 경우 hfont12.dat라는 이름으로 저장되고, 파일은 앞부분에 128 * 12bytes로 구성된 ascii 폰트와 그 뒤로 EUC-KR의 한글 코드 0xb0a1 부터 유효한 한글 글자의 폰트가  2350 * 12 * 2bytes 형태로 기록되어 있다. (즉 폰트 크기에 따라서 데이터 파일 크기가 다르다는 이야기 이다.)
한글이 시작되는 위치에는 아래와 같이 '가' 라고 기록되어 있다.  (1행 = 2bytes)
    0000000000010000
    0111111000010000
    0000000100010000
    0000000100010000
    0000000100010000
    0000000100011100
    0000000100010000
    0000001000010000
    0000001000010000
    0000010000010000
    0001100000010000
    0110000000010000
    0000000000010000
    0000000000000000
    0000000000000000
    0000000000000000

특별히  ascii 코드 쪽에 몇글자는 사용의 편리를 위하여 몇개 번외로 기록하였다.
(첫부분에 소개한 이미지에 포함된다. )
아래 첨자 4개  0x0b 0x0c  0x0d  0x0e
                      ₁      ₂      ₃        ₄
윗 첨자 4개     0x15 0x16  0x17  0x18
                      ¹      ²      ³        ⁴
특수 문자 5개  0x0f 0x10  0x11  0x12   0x13
                    ℉      ℃    ‰      μ       ° 

이렇게 매핑 시켰더니 특수문자는 영숫자보다 더 넓은 글자폭이 필요하여, 결국 2350글자 뒷부분에 폰트 정보를 넣어 버렸다.

6. Unicode -> EUC-KR 정보를 만들다.

UTF8의 한글은 각 글자당 3바이트 이고,  Unicode로는 아래의 코드로 변환 가능하다.
unicode = (byte1 & 0b00001111) << 12 | (byte2 & 0b00111111) << 6 | (byte3 & 0b00111111);
하지만 Unicode를 EUC-KR로 변환하는 공식은 없다. (처음에 이부분을 고려 못했다.) 1대1 매핑으로만 가능하다. 어쩔수 없이 Unicode의 11172 글자에 해당하는  EUC-KR 매핑표를 따로 파일로 만들었다.


7. 소스 코드를 기록하다.

 여러 고민을 하고 준비하고 해서 최종적으로 HanDrawClass를 만들게 되었다.

1. HanDrawClass.h
/*------------------------------------------------------------------
   BSD License

   Copyright (c) 2018 terminal0070@gmail.com

   OLED, LCD등에 한글을 출력하기 위한 라이브러리이다.
   ESP8266 SPIFFS 사용하여야만 한다.
   (아래의 링크에 가면 쉽게 데이터 파일을 업로드 있다)
   https://github.com/esp8266/arduino-esp8266fs-plugin
   ( 소스 디렉토리의 data라는 서브폴더에 위치해 있어야 업로드 가능하다)
   유효한 한글은 euc-kr 정의된 한글만 표현된다(물론 영숫자는 별도..)
  ------------------------------------------------------------------*/

#ifndef handraw_h
#define handraw_h
#include <Arduino.h>
#include "FS.h"

#ifndef DEBUG
//#define DEBUG
#endif

// define callback function
typedef void (*CLEAR_FUNC)(void);
typedef void (*DRAW_PIXEL_FUNC)(int16_t x, int16_t y, uint16_t color);
typedef void (*DISPLAY_FUNC)(void);
// 간단 버전 해쉬테이블..
// 폰트의 경우 일부 내용에 대하여 미리 로딩해 둔다...(어차피 화면에 쓰는 글자는 일부일테니까..)
#define BUCKET_SIZE 5     // 크기는 화면에 얼마나 많은 글자를 사용하냐에 따라서 다르다.
#define MAX_LIST_SIZE 30  // 크기가 적다면... 매번 화면을 그릴때 마다 특정 글자는 다시 읽어야 되며, 화면 갱신 속도가 느려질 것이다.

class FontHashMap {
  public:
    FontHashMap() {};   ~FontHashMap() {};
    void setValue(unsigned int key, unsigned char *value) {
      byte bucket =  getBucketIndex(key);
      // 버킷이 차있으면 맨처음으로 돌리고.. 그리고 free해서 비운다...
      if (addIndex[bucket] >= MAX_LIST_SIZE)  addIndex[bucket] = 0;
      if (valueArray[bucket][addIndex[bucket]] != NULL) free(valueArray[bucket][addIndex[bucket]]);
      keyArray[bucket][addIndex[bucket]] =  key;
      valueArray[bucket][addIndex[bucket]++] = value;
    }
    unsigned char * getValue(unsigned int key) {
      byte bucket =  getBucketIndex(key);
      for (int i = 0; i < MAX_LIST_SIZE; i ++) {
        if (key == keyArray[bucket][i] )   return valueArray[bucket][i];
      }
      return NULL;
    }
    uint8_t getBucketIndex(unsigned int key) {
      return (key / 100) % BUCKET_SIZE;
    }
  private:
    byte addIndex[BUCKET_SIZE];
    unsigned int   keyArray[BUCKET_SIZE][MAX_LIST_SIZE];
    unsigned char *valueArray[BUCKET_SIZE][MAX_LIST_SIZE];
};

class HanDrawClass  {
  protected:
    int8_t m_dCharSize = 16// 한글 기준 픽셀 크기
    CLEAR_FUNC      m_callbackClearFunction;     // 화면 지우기용 콜백
    DRAW_PIXEL_FUNC m_callbackDrawPixelFunction; // 1 점찍기용 콜백
    DISPLAY_FUNC    m_callbackDisplayFunction;   // 버퍼 -> Display 콜백

    // 아래 두개 숫자는 오래 걸리는 것은 아니나 항상 쓰는 숫자 이기 때문에. 미리 계산
    int8_t m_dCharHalfSize = 8; // 영문 기준 픽셀 크기 (자동 계산됨)
    int8_t m_dCharSecondSize = 8; // 한글기준 두번째 바이트 그리는 범위 (자동 계산됨) 한글자 크기가 14 이값은 14 - 8 = 6
    File m_fpFontData;               // 한글 폰트 정보가 있는 파일
    File m_fpUtf8_Euckr_mapper;      // UTF8 Euckr 바꾸기 위한 매핑 테이블 파일
    FontHashMap m_HanFontHashMap[3]; // 한글폰트 캐시를 위하여 읽은 문자를 저장해 두는곳...
    FontHashMap m_EngFontHashMap[3]; // 영어폰트 캐시를 위하여 읽은 문자를 저장해 두는곳...

  private:
    void unicode2Euckr(unsigned int unicode, int8_t *out1, int8_t *out2);
    void drawOneHan(int16_t x, int16_t y, int8_t byte1, int8_t byte2, int8_t byte3);
    void drawOneSpecial(int16_t x, int16_t y, int8_t byte1, int8_t byte2, int8_t byte3);
    void drawOneEng(int16_t x, int16_t y, int16_t start, int16_t end, int8_t ascii);

  public:
    HanDrawClass();
    virtual ~HanDrawClass();
    int8_t begin(int8_t fontSize, CLEAR_FUNC clearFunction , DRAW_PIXEL_FUNC drawPixelFunction, DISPLAY_FUNC displayFunction );
    void setFontSize(int8_t fontSize);
    int8_t end();
    void clear();
    void display();
    void drawPixel(int16_t x, int16_t y, uint16_t color);
    void drawString(int16_t x, int16_t y, char* message);
    void drawString(int16_t x, int16_t y, const char* message);
    void drawString(int16_t x, int16_t y, String message);
};
extern HanDrawClass HanDraw;
#endif


2. HanDrawClass.cpp
/*
   BSD License
 
   Copyright (c) 2018 terminal0070@gmail.com
*/

#include "HanDraw.h"

#ifndef BLACK
#define BLACK 0
#endif

#ifndef WHITE
#define WHITE 1
#endif

HanDrawClass::HanDrawClass() {}
HanDrawClass::~HanDrawClass() { end();}

// 시작시 사이즈의 폰트 크기를 사용할 것인가를 정한다.
// 3개의 callback function 이용하여 디스플레이를 제어한다.
int8_t HanDrawClass::begin(int8_t fontSize, CLEAR_FUNC clearFunction, DRAW_PIXEL_FUNC drawPixelFunction, DISPLAY_FUNC displayFunction) {
  m_callbackClearFunction = clearFunction;
  m_callbackDrawPixelFunction = drawPixelFunction;
  m_callbackDisplayFunction = displayFunction;
  bool result = SPIFFS.begin();
#ifdef DEBUG
  Serial.println("SPIFFS opened: " + result);
#endif
  if (result) {
    setFontSize(fontSize);
    m_fpUtf8_Euckr_mapper = SPIFFS.open("/utf-euc-map.dat", "r");
#ifdef DEBUG
    Serial.print("Font file handle: ");
    Serial.println(m_fpFontData);
    Serial.print("map file handle: ");
    Serial.println(m_fpUtf8_Euckr_mapper);
#endif
  }
  return result;
}

// 폰트 크기를 정한다.
void HanDrawClass::setFontSize(int8_t fontSize) {
  m_dCharSize = fontSize;
  m_dCharHalfSize = (int8_t)(fontSize / 2);
  m_dCharSecondSize = (int8_t)(fontSize - 8);
  if (m_fpFontData != 0x00)
    m_fpFontData.close();
  char fontFileName[64];
  sprintf(fontFileName, "/hfont%d.dat", m_dCharSize);
  m_fpFontData = SPIFFS.open(fontFileName, "r");
}

// 종료되는 시점에는 파일을 닫아 버린다.
int8_t HanDrawClass::end() {
  m_fpFontData.close();
  m_fpUtf8_Euckr_mapper.close();
  return 1;
}

// 화면을 지우는 것은 콜백 함수를 부른다.
void HanDrawClass::clear() {
  m_callbackClearFunction();
}

// 역시 화면에 표시하는 것은 콜백 함수를 부른다
void HanDrawClass::display() {
  m_callbackDisplayFunction();
}

void HanDrawClass::drawPixel(int16_t x, int16_t y, uint16_t color) {
#ifdef DEBUG
  Serial.print(color);
#endif
  m_callbackDrawPixelFunction(x, y, color);
}

// 아래 특수 문자는 좀더 특별한 방법으로 .. 표시한다.
//  0x0f    0x10  0x11  0x12   0x13   0x14
//                    μ      °
void HanDrawClass::drawOneSpecial(int16_t x, int16_t y, int8_t byte1, int8_t byte2, int8_t byte3) {
  unsigned int unicode = (byte1 & 0b00001111) << 12 | (byte2 & 0b00111111) << 6 | (byte3 & 0b00111111);
  unsigned char *ch;
  int hashIndex = (m_dCharSize - 12) / 2//지원하는 폰트 사이즈가 12, 14,16 이며.. 해쉬테이블의 0,1,2 배열에 해당
  // 문자 크기를 찾으면 ... 무시..
  if (hashIndex < 0 || hashIndex > 2 )
    return;

  ch = m_HanFontHashMap[hashIndex].getValue(unicode);
  // 찾았으면..... 읽어서 해쉬 테이블에 세팅하고..
  if (NULL == ch) {
    int16_t charIndex = 2350 + (byte3 - 0x0f);
    ch = (unsigned char *)malloc(m_dCharSize * 2);
    m_fpFontData.seek(charIndex * m_dCharSize * 2 + 128 * m_dCharSize, SeekSet); // 앞에 영문자 뺴고.. (스킵해야지...)
    m_fpFontData.read(ch, m_dCharSize * 2);
    m_HanFontHashMap[hashIndex].setValue(unicode, ch);
  }

  uint8_t dataIdx = 0;
  for ( int16_t i = 0; i < m_dCharSize; i++) {
    for ( int16_t k = 0; k < 8; k++) {
      if ( ( ( ch[dataIdx] >> (7 - k) ) & 0b00000001 ) != 0 )
        drawPixel(x + k, y + i, WHITE);
#ifdef DEBUG
      else
        drawPixel(x + k, y + i, BLACK);
#endif
    }
    dataIdx++;

    for ( int16_t k = 0; k < m_dCharSecondSize; k++)  {
      if ( ( ( ch[dataIdx] >> (7 - k) ) & 0b00000001 ) != 0 )
        drawPixel(x + k + 8, y + i, WHITE);
#ifdef DEBUG
      else
        drawPixel(x + k + 8, y + i, BLACK);
#endif
    }
    dataIdx++;

#ifdef DEBUG
    Serial.println("");
#endif
  }
#ifdef DEBUG
  Serial.println("");
#endif

}

// x, y, euc-kr 상위 바이트하위 바이트 순서
void HanDrawClass::drawOneHan(int16_t x, int16_t y, int8_t byte1, int8_t byte2, int8_t byte3) { //, int8_t upbyte, int8_t downbyte, unsigned int unicode) {
  // utf-8 3byte byte1, byte2, byte3 1110xxxx, 10xxxxxx, 10xxxxxx 부분만 가져오면 유니코드
  unsigned int unicode = (byte1 & 0b00001111) << 12 | (byte2 & 0b00111111) << 6 | (byte3 & 0b00111111);
  unsigned char *ch;
  int hashIndex = (m_dCharSize - 12) / 2//지원하는 폰트 사이즈가 12, 14,16 이며.. 해쉬테이블의 0,1,2 배열에 해당
  // 문자 크기를 찾으면 ... 무시..
  if (hashIndex < 0 || hashIndex > 2 )
    return;

  // 해쉬 테이블에서 먼저 찾고
  ch = m_HanFontHashMap[hashIndex].getValue(unicode);
  // 찾았으면..... 읽어서 해쉬 테이블에 세팅하고..
  if (NULL == ch) {
    int8_t upbyte, downbyte; // euc-kr  first, second byte
    unicode2Euckr(unicode, &upbyte, &downbyte);
    int16_t charIndex = (((uint8_t)upbyte - 0xb0)  * 94) + ((uint8_t)downbyte - 0xa1);
    ch = (unsigned char *)malloc(m_dCharSize * 2);
    m_fpFontData.seek(charIndex * m_dCharSize * 2 + 128 * m_dCharSize, SeekSet); // 앞에 영문자 뺴고.. (스킵해야지...)
    m_fpFontData.read(ch, m_dCharSize * 2);
    m_HanFontHashMap[hashIndex].setValue(unicode, ch);
  }
  //출력 못하는 글자는 대충 짝대기를 쭈욱 그어 준다..
  //  if ( (uint8_t)upbyte < 0xb0 || (uint8_t)downbyte < 0xa0 ) {
  //    drawOneEng(x, y, 0, m_dCharHalfSize,  0x2d);
  //    drawOneEng(x + (int16_t)(m_dCharHalfSize), y, 0, m_dCharHalfSize, 0x2d);
  //    return;
  //  }

  uint8_t dataIdx = 0;
  for ( int16_t i = 0; i < m_dCharSize; i++) {
    for ( int16_t k = 0; k < 8; k++) {
      if ( ( ( ch[dataIdx] >> (7 - k) ) & 0b00000001 ) != 0 )
        drawPixel(x + k, y + i, WHITE);
#ifdef DEBUG
      else
        drawPixel(x + k, y + i, BLACK);
#endif
    }
    dataIdx++;

    for ( int16_t k = 0; k < m_dCharSecondSize; k++)  {
      if ( ( ( ch[dataIdx] >> (7 - k) ) & 0b00000001 ) != 0 )
        drawPixel(x + k + 8, y + i, WHITE);
#ifdef DEBUG
      else
        drawPixel(x + k + 8, y + i, BLACK);
#endif
    }
    dataIdx++;

#ifdef DEBUG
    Serial.println("");
#endif
  }
#ifdef DEBUG
  Serial.println("");
#endif
}

void HanDrawClass::drawOneEng(int16_t x, int16_t y, int16_t start, int16_t end, int8_t ascii) {
  if (ascii == 0x20)
    return; // 스페이스 문자임...

  unsigned char *ch;//[m_dCharSize * 2];
  int hashIndex = (m_dCharSize - 12) / 2//지원하는 폰트 사이즈가 12, 14,16 이며.. 해쉬테이블의 0,1,2 배열에 해당

  // 문자 크기를 찾으면 ... 무시..
  if (hashIndex < 0 || hashIndex > 2 )
    return;
  // 해쉬 테이블에서 먼저 찾고
  ch = m_EngFontHashMap[hashIndex].getValue(ascii * 100);
  // 찾았으면..... 읽어서 해쉬 테이블에 세팅하고..
  if (NULL == ch) {
    ch = (unsigned char *)malloc(m_dCharSize);
    m_fpFontData.seek(ascii * m_dCharSize, SeekSet);
    m_fpFontData.read(ch, m_dCharSize);
    m_EngFontHashMap[hashIndex].setValue(ascii * 100, ch);
  }
  uint8_t dataIdx = 0;
  for ( int16_t i = 0; i < m_dCharSize; i++) {
    //상대적으로 너비가 좁은  i, l, 1, I 등은 좁게 그림...
    for ( int16_t k = start; k < end; k++) {
      if ( ( ( ch[dataIdx] >> (7 - k) ) & 0b00000001 ) != 0 )
        drawPixel(x + k, y + i, WHITE);
#ifdef DEBUG
      else
        drawPixel(x + k, y + i, BLACK);
#endif
    }
    dataIdx++;
#ifdef DEBUG
    Serial.println("");
#endif
  }
#ifdef DEBUG
  Serial.println("");
#endif
}

// 유니코드를 주면 2바이트의 euc-kr 계산 해주는...함수
void HanDrawClass::unicode2Euckr(unsigned int unicode, int8_t *out1, int8_t *out2) {
  // 여기에서  0xac00 빼주면 0 부터의 인덱스가 된다.
  m_fpUtf8_Euckr_mapper.seek((unicode - 0xac00) * 2, SeekSet); // 2바이트씩 들어감
  *out1 = m_fpUtf8_Euckr_mapper.read();
  *out2 = m_fpUtf8_Euckr_mapper.read();
}

// UTF8 문자열을 주면.. euc-kr 문자로 변환하여, 폰트에서 인덱스를 찾아서 출력
void HanDrawClass::drawString(int16_t x, int16_t y, const char* message) {
  // 굴림체 사용하며, 컬럼의 폭은 기본적으로 영숫자에 맞추어 시작점을 잡는다.
  size_t in_size = strlen(message);
  int16_t first = (int16_t)(m_dCharHalfSize / 3);
  int16_t second = (int16_t)(m_dCharHalfSize * 0.666) + 1;

  for (int i = 0; i < in_size; i++) {
    if ((message[i] & 0b10000000) > 0) { // 이건 한글이다.
      drawOneHan(x, y, message[i], message[i + 1], message[i + 2]);
      x += (int16_t)(m_dCharSize);
      i += 2;
    } else { // ascii
      // 상상도 못할 비밀.. 몇개의 특수 문자는 아스키 테이블에 매핑 버렸슴.
      // 게다가 폰트 데이터는 아스키 영역에 있는 것도 있고... 심지어....한글 영역에 있는 것도 있슴..
      //    (11 12 13 14       15      16     17    18    19    20          21 22 23 24 25 )
      //   0x0b c  d  e       0x0f    0x10  0x11  0x12   0x13   0x14       16 17 18  19
      //   (₁  ₂  ₃  ₄                   μ    °              ¹  ²  ³      )
      // 특별히 사용할 만한 문자 모양 몇개를 아스키 테이블에 매칭 시켜 버렸슴.
      if (message[i] >= 0x0f && message[i] <= 0x14) {
        drawOneSpecial(x, y, 0xED, 0x9f , message[i]);
        x += (int16_t)(m_dCharSize);
      } else {
        switch (message[i]) {
          case 0x2E: case 0x31: case 0x3A: case 0x49: case 0x69: case 0x6c : // '.1:Iil'
            drawOneEng(x, y, first , second, message[i]);   // 상대적으로 폭이 좁은 문자는
            x += (int16_t)(second - first + 3);
            break;
          default :
            drawOneEng(x, y, 0, m_dCharHalfSize,  message[i]);
            x += (int16_t)(m_dCharHalfSize);
            break;
        }
      }
    }
    // 가로의 최대 화면 크기가 128 pixel 가정한다.
    //if (x > 128)
    //  break;
  }
}

void HanDrawClass::drawString(int16_t x, int16_t y, char* message) {
  return drawString(x, y, (const char*)message);
}
void HanDrawClass::drawString(int16_t x, int16_t y, String message) {
  return drawString(x, y, message.c_str());
}
HanDrawClass HanDraw;


8. 사용법을 기록하다.

 대단한 예제는 아니지만 아래의 내용으로 컴파일 해서 실행하면 맨 처음에 소개한 사진2장에 포함된 내용이 화면에 표시된다.

#include <Arduino.h>
#include "Display_SSD1306.h"
#include "HanDraw.h"

#define OLED_RESET -1 //4  S/W 리셋..
Display_SSD1306 Oled(OLED_RESET);

/************************* Global variables *********************************/
unsigned long startTime = 0;
uint32_t loopcnt = 0;
char fpsbuf[128] = "FPS:";
bool invert = true;  // 화면을 역상으로 표시
void setup() {
  Serial.begin(115200);
  delay(50);

  // Oled I2C 방식으로 연결하고, 주소는 0x3c
  Oled.begin(SSD1306_SWITCHCAPVCC, 0x3C, false);
 
  // Callback 함수를 설정해 주면 필요시 호출 하여 사용함.
  HanDraw.begin(12,
    // 화면 지우는 콜백함수
    [](void) {  Oled.clearDisplay(); },
    // 1 픽셀을 그리는 콜백 함수
    [](int16_t x, int16_t y, uint16_t color) { Oled.drawPixel(x, y, color); },
    // 메모리에서 Display 표시 버퍼까지 보내는 함수
    [](void) { Oled.display(); }
  );

  HanDraw.display(); // 사실상 Oled.display() 동일....
  delay(2000);

  HanDraw.clear();
  HanDraw.setFontSize(12);
  // 특수한 문자 몇개는... 아래와 같이 주면 출력된다. (통용되는 코드가 아니라 변칙 코드임)
  // 12px:한글 21℃℉‰μ°   라고 출력
  HanDraw.drawString(1, 0, "12px:한글 21\x10\x0f\x11\x12\x13");
  HanDraw.setFontSize(14);
  // 14:아래첨자 A₁₂₃₄   라고 출력
  HanDraw.drawString(1, 13, "14:아래첨자 A\x0b\x0c\x0d\x0e");
  HanDraw.setFontSize(16);
  // 16:위첨자 M¹²³⁴   라고 출력
  HanDraw.drawString(1, 28, "16:위첨자 M\x15\x16\x17\x18");
  HanDraw.display();
  delay(3000);

  HanDraw.setFontSize(12);
  startTime = millis();
  delay(1);
  Serial.println("Setup All done");
}

String getTime(unsigned long  ttime) {
  int sec = ttime / 1000; int min = sec / 60; int hr = min / 60;
  String ts = "";
  if (hr < 10) ts += "0";
  ts += hr;   ts += ":";
  if ((min % 60) < 10) ts += "0";
  ts += min % 60;   ts += ":";
  if ((sec % 60) < 10) ts += "0";
  ts += sec % 60;
  return (ts);
}

void loop() {
  unsigned long  ttime = millis();
  dtostrf(loopcnt * 1000.0 / (ttime - startTime), 5, 2,   fpsbuf + 4);

  HanDraw.clear();
  HanDraw.drawString(15, 2, "[[ 화면 정보 ]]");
  HanDraw.drawString(2, 14, "--------------------");
  HanDraw.drawString(2, 24, "한글 출력 테스트");
  HanDraw.drawString(2, 38, fpsbuf);
  HanDraw.drawString(2, 51, getTime(ttime));
  HanDraw.display();

  loopcnt++;
  if (loopcnt % 100 == 0) {
    Oled.invertDisplay(invert);
    invert = !invert;
  }
}



9. 끝으로 


ESP8266의 CPU Frequency를 160MHz로 변경하고 테스트해보니, IIC 통신으로도 이 작은 디스플레이는 53fps(아마도 최대 60fps일듯)가 가능했다.


- 2019.01.04 추가 
위의 코드로  사실 컴파일이 가능하긴 하나 어려움을 겪고 계신분이 있는듯 하여,
디렉토리를 압축해서 공유한다. 아래의 링크로 다운로드 가능하다.

- 2021.01.04 수정  
2년만에 오타를 발견 수정 완료. ㅠㅠ 

댓글 33개:

  1. \ESP_HanOLED\ESP_HanOLED.ino:3:21: fatal error: HanDraw.h: No such file or directory
    컴파일에서 에러가 나는데, 전체 소스를 부탁해도 될까요?
    수고하세요.

    답글삭제
    답글
    1. HanDraw.h 라는 내용도 이 포스트에 있습니다. 몇개의 파일을 만드셔야 하는데 깜빡 하신듯 하네요. 이와 별개로 즉시 컴파일 가능한 형태로 압축한 파일을 을 다운로드 할 수 있는 링크를 이 포스트의 마지막에 추가 하였습니다.

      삭제
  2. 몇번의 삽질 끝에 OLED에 한글이 출력되는 것을 보니 기쁩니다.
    좋은 프로그램 소스를 공개해 주셔서 감사드립니다.

    답글삭제
  3. 감사합니다. 잘 참고하겠습니다.

    답글삭제
  4. 좋은 글과 소스 감사합니다. 감동적인 작업을 공유해 주신점 더더욱 감사드립니다.

    한가지만 말씀드리자면... HanDraw.h 에서 화면(블로그)에는 #include "FS.h" 라고 되어 있는데, 압축해주신 소스코드에는 #include "SPIFFS.h" 라고 되어 있네요. 이부분 찾아내서 수정하는데 살짝 애먹었었습니다. 압축해주신 소스코드에도 반영되어 있으면 좋겠다는 생각이 들었습니다.

    답글삭제
    답글
    1. 아마도 spiffs 관련된 라이브러리가 바뀐듯 하네요. 이거 버전마다 차이가 있어요.

      삭제
    2. 저도 같은 에러가 발생 했습니다. #include "SPIFFS.h" 를 주석처리 하면 되나요

      삭제
    3. 답변이 늦었네요. 주석처리가 아니라 #include "FS.h" 로 변경하시면 될듯 하네요.

      삭제
  5. 좋은글 글과 소스 감사드립니다.^^

    답글삭제
    답글
    1. 도움이 도셨다면 저도 기쁩니다.

      삭제
  6. 감사합니다. 군더더기 없이 깨끗하게 실행이 되네요.

    답글삭제
  7. 취미로 전광판을 가지고 놀고 있습니다. esp32로 전광판에 한글을 표시하려고 하는데 이 라이브러리를 사용해보려 합니다. 그런데 기존 예제를 조금씩 수정해보는 실력밖에 되지 않아 어려움이 있네요. drawPixel 이라는 전광판에 점을 찍는 함수가 있는데요. 이걸 HanDraw.h에서 어떻게 사용하면 될까요? 간단한 조언 부탁드립니다. 전광판에 사용 성공한다면 감사 표시로 작은 선물 보내드리고 싶습니다.

    답글삭제
    답글
    1. 핵심은 HanDraw.h를 수정하는 것이 아니라 UTF8_HanDraw.ino 파일을 수정하는 것입니다. setup() 함수 내에
      HanDraw.begin(12,
      // 화면 지우는 콜백함수
      [](void) { Oled.clearDisplay(); },
      // 1개 픽셀을 그리는 콜백 함수
      [](int16_t x, int16_t y, uint16_t color) { Oled.drawPixel(x, y, color); },
      // 메모리에서 Display 표시 버퍼까지 보내는 함수
      [](void) { Oled.display(); }
      );
      부분이 있는데요.
      [](int16_t x, int16_t y, uint16_t color) { Oled.drawPixel(x, y, color); }, 부분을 수정하시면 됩니다. Oled.drawPixel 이라는 함수를 다시 부르게 되어 있는데 이 부분을 기존에 가지고 계신 drawPixel 함수로 대체 하시면 됩니다. 수정하시게 되는 부분은 Handraw라는 Class에서 호출하게 되는 callback 함수를 가지고 계신 함수로 바꾸는 형태입니다.
      즉 HanDraw class는 수정하지 않으시고 사용해야 하는 형태 입니다. 저는 이 글에 포함된 display 장치 이외에 여러 종류의 display 장치를 사용하며 HandDraw class는 수정하지 않고 그 외의 것들을 수정하여 한글을 출력합니다.
      이 블로그에 소개된 오실로스코프도 같은 라이브러리를 사용한 결과압니다.
      (오타가 있어서 지우고 다시 답글을 달았습니다.)

      삭제
    2. 늦게서야 답글을 확인했습니다. 나중에 시도해보고 알려드리겠습니다. 감사합니다~!

      삭제
  8. 혹시 제가 한글 폰트? 를 만들어 추가해보려면 어떻게 하면 될까요? 너무 무식한 질문이죠 ㅜㅜ

    답글삭제
    답글
    1. 그렇게 어려운 것만은 아니지만, 그렇다고 쉽지는 않습니다.
      이 글의 5번 항목인 "폰트정보를 만들다" 라는 항목을 잘 읽어 보시면 됩니다. 소스코드 구조상 16 X 16 크기 까지의 폰드를 만들 수 있습니다.
      https://m.blog.naver.com/clous02/110008727032 이런 사이트를 방문해 보시면 euc-kr 코드표가 있고 '가' 라는 글자가 시작되는 부분부터 저장하시면 됩니다.
      혹시나 시도하시게 된다면, 한번더 알려주시면 좀더 자세한 설명을 드리도록 하겠습니다.
      (추석 연휴라 답변이 좀 늦었네요.)

      삭제
    2. 네~ 설명 감사합니다. 차분히 읽어보고 시도? 하게 되면 죄송하지만 도움 부탁드려용~

      좋은 일들만 가득하세요~

      삭제
  9. 다른 한글 라이브러리보다 훨씬 간결하고 빠릅니다!!
    그런데 화면 표시된 스트링만큼만 지우려 하는데요.
    픽셀 크기가 한글 한자당 알파벳 두자의 길이이네요.
    그래서 스트링 길이를 구해보니 아두이노에서는 한글이 3바이트로 계산됩니다.
    한글은 2바이트, 영문은 1바이트로 길이를 구하는 어떤 방법이 있을까요?

    답글삭제
    답글
    1. 편집기를 어떤것을 사용하시는지 모르지만, 아마도 문자 인코딩이 UTF8일듯 합니다. 즉 화면에 출력하는 EUC-KR과 다르기 때문에 단순 곱셈으로는 힘들듯합니다.
      소스코드를 보시면 HanDrawClass::drawString 함수에 그 힌트가 있습니다. 그함수를 약간 수정하여 UTF8문자열을 주면 전체적으로 몇 픽셀인지 계산하도록 하면 됩니다.
      그리고 한글과 영문은 픽셀크기가 2:1이 기본이긴 합니다만, 상대적으로 좁은 글자(.1:Iil)는 3 픽셀에 매핑시킵니다. 이부분 역시 위에 소개드린 함수에 다 기록되어 있는 내용입니다.
      결론은 위의 메소드 부분에서 drawOneHan, drawOneEng, drawOneSpecial 등의 내용을 삭제하면 결국 x 좌표가 문자열의 너비가 됩니다.
      도움이 되셧기를 기원합니다.

      삭제
    2. 자문자답입니다.
      drawString() 함수를 수정해서 스트링을 입력하면 스트링의 x 좌표 바운더리를 구하는 함수를 라이브러리에 추가했습니다.
      (Adafruit GFX 라이브러리의 getTextBounds() 함수처럼...)
      이 함수가 있으면 스트링이 표시된 부분만 빠르게 지울 수 있어서 텍스트 스크롤 등에 있어서 장점이 있네요.

      삭제
    3. 제가 답글을 다는 사이에 답을 주셨네요~ 감사합니다!!

      삭제
  10. 귀한 자료 보고 갑니다. 감사합니다.

    답글삭제
  11. 안녕하세요!!! 현직 고등학교 교사입니다. 학생들 대상으로 프로젝트수업 진행하려고 하는데 한글로 OLED에 표기하는 방법을 찾던 중 우연히 발견했는데 이거 완전 신세계네요!!!!!!! 학생들에게 좀 쉽게 설명 할 방법이 없어 엄청 고생하다가 올려주신 내용을 토대로 하면 학생들이 간단하게 값만 수정해서 코딩이 가능 할 것으로 보입니다!! 그래서 nodemcu나 wemos d1 mini를 활용해서 업로드하고 가능하다면 웹서버와 시계, 온습도센서까지 연동해서 해보려고 합니다!! 혹시 학생들이 좀 쉽게 이해할 수 있게 사용 방법에 대한 내용을 간략하게 정리해서 제 블로그에 업로드해도 될까요??

    답글삭제
  12. 작성자가 댓글을 삭제했습니다.

    답글삭제
  13. 안녕하세요 아두이노 1602구입했는데 영어 100단어 화면 출력도 가능한지요?

    답글삭제
    답글
    1. 문의하신 내용이 아주 애매해서 어떤 부분을 질문한 것인지 잘 모르지만 짐작해서 답변드립니다. 1602 라는 디스플레이는 표현가능한 픽셀이 적어서 한번에 100단어는 못 올릴듯 하고, 스크롤이나 Next 버튼등을 만드셔야 겠네요. 아두이노 입장에서선 100단어라 해봐야 1K Bytes 이내의 작은 정보일듯 하니, 올리는 것은 문제가 되지 않을듯 하네요.

      삭제

파워뱅크를 만들어 보자

- 방전률이 높은 배터리를 이용하는 작업은 화재, 폭발의 위험이 있습니다. 충분한 지식을 가지고 있더라도, 잠깐의 부주의로 사고가 발생할 수 있습니다. 사고는 본인의 책임입니다.  소재로 시작된 만들기 일전에 어머님의 전동휠체어 배터리를 만들어서 교체...