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년만에 오타를 발견 수정 완료. ㅠㅠ 

파워뱅크를 만들어 보자

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