ESP32 + ST7789 (3) - drawBitmap()

스윗포테이토·2022년 10월 13일
1

TFT_eSPI 라이브러리를 사용하여 비트맵 이미지를 그려보려고 한다.

drawBitmap()

비트맵을 그리는 함수이다.

우선 TFT_eSPI.h 헤더파일에서 함수 원형을 보면 이렇게 정의되어 있다.

각 파라미터에 대한 설명이다.

x, y : 비트맵을 그릴 시작 위치
bitmap: 비트맵 배열
w, h: 비트맵의 너비와 높이
fgcolor: 1일 때 표시할 색
bgcolor: 0일 때 표시할 색

bitmap

일반적으로 비트맵이라 하면, 이미지의 각 픽셀값을 저장하는 .bmp 형식의 이미지 파일 포맷을 뜻한다.
보통은 각 픽셀당 1 byte를 할당하여 256가지 색을 표현할 수 있는데, 여기에서 우리가 지정한 색은 fgcolor, bgcolor 두가지 뿐이다. 즉, 한 픽셀 당 1 bit만을 할당했다고 생각하면 된다.
이걸 알아내려고 라이브러리를 계속 읽었다.

자료형 uint_8은 8비트짜리 unsigned int를 의미한다. 즉, 2진수로 표현하자면 0000 0000 ~ 1111 1111로 정수 하나에 8비트, 즉 8픽셀에 대한 정보를 가지고 있다는 것이다.

따라서,

w = 80, h = 40
전체픽셀 = 80 * 40 = 3200
bitmap 배열의 길이 = 10 (byte) x 40 = 400

이렇게 되는 것이다. 배열 길이 계산이 왜 이렇게 되는지는 라이브러리 코드를 보면 알 수 있다.

라이브러리

처음에 저 비트맵에 뭘 어떤 형식으로 넘겨줘야 하는건지 고민을 많이 했다.
투명도가 반영이 되나...? 했는데 이차원 배열로 작성해서 다 125 정도를 줬는데 이상하게 줄무늬가 난자했다.

TFT_eSPI.cpp에 정의되어 있는 원본 함수는 이렇게 된다.

void TFT_eSPI::drawBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, uint16_t color)
{
  //begin_tft_write();          // Sprite class can use this function, avoiding begin_tft_write()
  inTransaction = true;

  int32_t i, j, byteWidth = (w + 7) / 8;

  for (j = 0; j < h; j++) { // 행
    for (i = 0; i < w; i++ ) { // 열
      if (pgm_read_byte(bitmap + j * byteWidth + i / 8) & (128 >> (i & 7))) {
        drawPixel(x + i, y + j, color);
      }
    }
  }

  inTransaction = lockTransaction;
  end_tft_write();              // Does nothing if Sprite class uses this function
}

다른 부분은 다 그러려니 이해할 수 있는데,

if (pgm_read_byte(bitmap + j * byteWidth + i / 8) & (128 >> (i & 7)))

이 부분이 뭔소린가 싶다.
간단히 요약하면 pgm_read_byte()는 바이트 단위(8 bit)로 값을 읽어오는 함수이다.
넘겨주는 인자는 주소라고 짐작할 수 있었는데, 저 byteWidth가 대체 뭔지 알 수가 없었다.

byteWidth = (w + 7) / 8;

이건 w를 바이트 단위로 쪼갠 값이다. 너비를 표현하는데 필요한 픽셀이라고 생각하면 될거 같다.
예를 들어 가로 20픽셀, 세로 20픽셀로 이루어진 이미지가 있다고 가정하면 이미지의 한 열은 8 + 8 + 4로 총 3바이트가 필요하다. 수식으로 표현하자면

byteWidth = ceil(w / 8)

따라서 정리하면

bitmap - base address
j * byteWidth - 몇번째 행인지 (몇번째 가로줄인지)
i / 8 - 몇번째 바이트인지

이렇게 된다.

뒤에 있는 128 >> (i & 7)는 이진수로 표현하자면 1000 0000 >> ( i & 111 )이다.
조금 더 쪼개서 보자면 i & 111은 결과적으로 뒤에 있는 세비트만 남게 되므로, 결국 8로 MOD 연산한 결과이다.
따라서 1000 0000 >> (i % 8)로 볼 수 있다.
A >> B(오른쪽 시프트 연산자)는 A를 B만큼 오른쪽으로 미는 것으로, 예를 들어 1111 1111 >> 3 = 0001 1111이 된다. 따라서 8로 나눈 나머지에 따라 1의 위치가 바뀌게 된다.

8로 나눈 나머지결과
01000 0000
10100 0000
20010 0000
30001 0000
40000 1000
50000 0100
60000 0010
70000 0001

and 연산의 특성상 0 & x = 0, 1 & x = x이므로, 모두 0으로 채우고 한자리만 1로 바꾸어 and연산을 하면 해당 자리에 있는 비트 수만 그대로 남게 된다.

1010 & 1000 = 1000 (첫번째 비트 + 나머지 0)
1010 & 0100 = 0000 (두번째 비트 + 나머지 0)
1010 & 0010 = 0010 (세번째 비트 + 나머지 0)
1010 & 0001 = 0000 (네번째 비트 + 나머지 0)

결과적으로 1바이트를 읽어와서 1비트씩 보게 되는 것이다.

요약하면

if (pgm_read_byte(bitmap + j * byteWidth + i / 8) & (128 >> (i & 7)))
if (읽어온 1바이트 & (128 >> (i & 7)))
if (읽어온 1바이트 & (128 >> (8로 나눈 나머지)))
if (읽어온 1바이트 & (i번째 비트만 1인 수))
if (읽어온 1바이트의 i번째 비트)

이렇게 된다!! 즉, 1픽셀당 1비트를 할당한 형태이고, 가로는 바이트 단위로 끊어서 저장하면 된다!

w = 30, h = 30 일 때
전체픽셀 = 30 * 30 = 900
bitmap 배열의 길이 = 4 (byteWidth) x 30 = 120

사실 별거 아닌 분석인데 비트 연산에 전혀 익숙하지 않아서 엄청나게 오래 걸렸다ㅠㅠ
나중에 다시 보면 무슨 말인지 알아 먹기를... 바란다.

trouble shooting

처음에 이미지 비트맵을 함수 내에서 선언했더니

Stack canary watchpoint triggered (loopTask)

이런 에러가 났다. 찾아보니 메모리 초과라길래 전역변수로 선언하고 setUP() 함수를 통해 초기화해서 해결했다.

reference

TFT_eSPI
TFT_eSPI.h
TFT_eSPI.cpp

profile
나의 삽질이 미래의 누군가를 구할 수 있다면...

0개의 댓글