자바스크립트(JavaScript)로 배우는 피지컬 컴퓨팅(Physical Computing) — (7/8) 그래픽 디스플레이 사용하기

Minkyu Lee
13 min readMay 15, 2021

--

이번 강좌에서는 고속 통신 방식인 I2C(Inter-Integrated Circuit)와 SPI(Serial Peripheral Interface)를 통해 그래픽 디스플레이(Graphic Display)를 제어해보자.

강좌 전체 목차

준비물

  • 라즈베리파이 피코 — 1개
  • 브레드보드(Breadboard) — 1개
  • 디지털 온습도 센서(DHT11) — 1개(모듈 형태도 무방)
  • 그래픽 디스플레이 모듈(SSD1306) — 1개(I2C, 1.3" — 128x64)
  • 점퍼 와이어 — 여러 개

이번 강좌에서는 SSD1306 드라이버를 사용하는 단색 OLED 디스플레이를 사용할 것이다. 이것은 여러가지 크기와 해상도(1.3" — 128x64, 0.96" — 128x64, 1" — 128x32) 그리고 통신 방식(I2C, SPI)을 제공한다. 구매할 때 꼭 크기와 해상도 그리고 통신 방식을 확인하기 바란다. 지난 강좌에서는 7-세그먼트 디스플레이(FND)를 사용하여 온도만 표시해보았는데, 이번에는 Kaluma의 다양한 그래픽 기능을 이용하여 좀 더 보기 좋게 온도와 습도를 표현해보도록 하겠다.

외부 장치와 어떻게 통신을 할까?

Pico는 다른 장치들을 연결 할 수 있도록 수십개의 핀을 제공하고 있다. 여기에 우리는 다양한 센서, 모터, LED, 그래픽 디스플레이, 네트워크 모듈 등을 연결해서 사용한다. 각각의 핀들은 HIGH 또는 LOW를 출력하거나 입력 받을 수 있을뿐인데, Pico는 이런 다양한 장치들과 어떻게 서로 통신을 하는 걸까?

  • 몇 개의 핀을 연결할까?
  • 데이터의 0과 1을 어떻게 표현될까?
  • 데이터를 1개 핀에 순서대로 보낼까? 아니면 여러 핀에 동시에 보낼까?
  • 데이터의 송신과 수신을 핀 하나에 같이 쓸까 아니면 따로 연결할까?

이러한 여러 가지 질문들이 생기게 되는데, 하나로 통일된 정답은 없다. 센서나 부품의 특성에 따라 적합한 통신 방법을 선택해야 하는 것이다. 온도 센서의 경우에는 어떤 방식이 적합할까? 우리는 DHT11 디지털 온습도 센서를 통해 1개의 연결만으로 어떻게 통신을 하는지 이미 간단히 배웠다. 이 센서는 위 질문에 아래의 답을 선택했다.

  • 1개의 핀을 연결한다(전력공급을 위한 3V3과 GND는 제외). 온도와 습도는 숫자 2개(40-bit)로 데이터양이 많지 않으므로 단자 1개의 연결만 사용해도 충분하다.
  • HIGH 신호를 길게(50us 이상) 주면1, 짧게(50us 이하) 주면 0 이다. HIGH =1, LOW=0으로 하지 않는 이유는, 데이터를 송수신 하지 않는 상태(LOW)와 데이터의 0을 구분하기 위함이다.
  • 1개의 핀에 순서대로 보낸다. 온도와 습도는 40-bit로 구성되어 있어서 순차적으로 보내더라도 대략 몇 밀리초(millisecond)이 시간이면 송수신 완료된다.
  • 송신과 수신은 하나의 핀에 같이 쓴다. 데이터를 보내달라는 요청을 보내고 난 뒤에 데이터를 수신한다.
1줄(One-Wire)로 숫자 97(이진수 01100001, ASCII ‘a’ 문자)을 전송하는 예시

그러면 우리가 이번 강좌에서 사용할 그래픽 디스플레이는 어떨까? 온도센서와 같이 1줄로 통신하면 과연 충분한걸까? 128x64 해상도에 단색으로 보더라도 8,192 bit가 필요하고, 같은 방식으로 통신을 한다면 화면의 픽셀 정보를 보내는데 거의 1초에 가까운 시간이 걸린다. 뭔가 더 많은 데이터를 빠르게 송수신할 필요가 있어 보인다. Pico에서는(다른 대부분의 마이크로컨트롤러에서도 동일) 직렬 통신을 위해서 몇 가지 프로토콜을 지원한다.

  • I2C (Inter-Integrated Circuit) — 통신을 위해 2개의 핀을 사용한다. 송수신의 타이밍 제어를 위한 클럭(SCL — Serial Clock) 1개 핀, 그리고 데이터 송수신(SDA — Serial Data)을 위한 1개 핀으로 구성된다.
  • SPI (Serial Peripheral Interface) — 통신을 위해 3개의 핀을 사용한다. 클럭 라인(SCK —Serial Clock)과 함께 송신용(MOSI — Master Out Slave In) 1개 핀, 수신용(MISO — Master In Slave Out) 1개 핀을 사용한다.
  • UART (Universal Asynchronous Receiver/Transmitter) — 통신을 위해 2개의 핀을 사용한다. 클럭 라인이 없고, 송신(TX — Transmitter)용 1개 핀줄, 수신(RX — Receiver)용 1개 핀을 사용한다. UART에 대한 자세한 내용은 다음 강좌에서 설명한다.
다양한 시리얼 통신 방식

I2C (Inter-Integrated Circuit)

I2C는 2개의 핀을 사용함으로써 좀 더 효율적인 통신을 위한 프로토콜이다. 1개의 핀으로 데이터를 송수신 하는 것은 DHT11과 동일하지만, 별도의 동기화를 위한 클럭(SCL)용 핀을 따로 쓰고 있다는 것이 다른점이다. 아래 그림과 같이 클럭의 신호가 LOW에서 HIGH로 바뀔 때, 데이터 신호가 LOW 인지 HIGH 인지로 01을 표현한다. 따라서 통신을 위한 두 개의 장치가 서로 송수신의 속도를 맞출 필요가 없고, 빠르게 송수신을 하고 싶으면 SCL 핀에서 클럭을 빠르게 해주면 된다(물론 I2C 장치마다 속도에 한계는 있다).

2줄(Two-Wire, I2C)로 숫자 97(이진수 01100001, ASCII ‘a’ 문자)을 전송하는 예시

I2C는 마스터(Master)와 슬레이브(Slave)가 있다. 주로 마이크로컨트롤러(Pico)가 마스터가 되고 센서가 슬레이브가 된다. 마스터는 클럭 신호를 생성함으로써 통신을 주도 한다. 그리고 기본적으로 I2C는 버스(bus)로 동작한다. 즉, 사용되는 2개의 핀에 여러 개의 장치를 동시에 연결할 수 있다는 뜻이다. 그렇다면 장치들을 어떻게 구분되는 것일까? 각 I2C 장치들은 고유의 주소(address)를 가지고 있다(예를 들자면 SSD1306의 주소는 0x3C). 그래서 통신을 할 때에는 먼저 주소를 지정해주고, 데이터를 송수신 해야 한다.

Kaluma에서 I2C를 사용하려면 아래와 같이 i2c 모듈을 이용한다. I2C 클래스를 생성한 다음 write() 또는 read() 함수로 데이터를 송수신한다. Pico에서는 I2C0, I2C1 총 2의 버스를 제공하므로, I2C 클래스 생성시에 사용할 버스 번호를 인자로 넘겨주면 된다.

const {I2C} = require('i2c');
let i2c = new I2C(0);
let data1 = new Uint8Array([0x6b, 0x00]);
i2c.write(data, 0x68);
// ...
let data2 = i2c.read(14, 0x68);
// ...
i2c.close();

이 정도만 이해해도 I2C 장치들을 연결해서 사용하는데는 무리가 없겠지만, 더 자세한 동작 원리를 이해하고 싶다면 관련 문서를 찾아보는 것을 추천한다.

SPI (Serial Peripheral Interface)

SPI도 I2C와 동일하게 동기화를 위한 클럭을 따로 사용한다. 다만, 송신과 수신을 위한 핀을 따로 사용한다. 슬레이브(slave) 장치로 데이터를 송신할 때에는 MOSI(Master Out Slave In)용 핀을, 반대로 수신할 때에는 MISO(Master In Slave Out)용 핀을 각각 사용한다. 종종 데이터 수신이 필요 없는 경우에는 MISO용 핀을 연결하지 않아도 된다.

SPI로 역시 버스(bus)의 역할을 하기 때문에 사용되는 3개의 핀에 여러 개의 장치를 동시에 연결할 수 있다. 다만 I2C와 다른 점은 장치를 구분하기 위해서 주소(address)를 사용하지 않고, 각 장치를 위한 별도의 CS(Chip Select) 핀을 연결해서 사용한다. 예를 들어 SCK, MISO, MOSI 핀에 3개의 서로 다른 장치를 연결했다면 각각의 장치를 위한 3개의 CS 핀이 따로 필요하다는 이야기다. 사용하고자 하는 장치의 CS를 HIGH로 두고, 다른 장치들의 CS는 LOW로 두어야 한다.

Kaluma에서 SPI를 사용하려면 아래와 같이 spi 모듈을 이용한다. SPI 클래스를 생성한 다음 send() 또는 recv() 함수로 데이터를 송수신한다. Pico에서는 SPI0, SPI1 총 2의 버스를 제공하므로, SPI 클래스 생성시에 사용할 버스 번호를 인자로 넘겨주면 된다.

const {SPI} = require('spi');
let spi0 = new SPI(0);
let data1 = new Uint8Array([0x6b, 0x00]);
spi0.send(data1);
// ...
let data2 = spi0.recv(10);
// ...
spi0.close();

더 자세한 동작원리는 관련 문서를 읽어보기를 추천한다.

SSD1306 (monochrome OLED)

자 이제 온도와 습도를 그래픽 디스플레이를 이용하여 좀 더 멋지게 온습도를 표시해보자. 아래 회로도와 같이 SSD1306을 I2C0 버스에 연결하고, DHT11을 GPIO15에 연결한다. 7-세그먼트를 이용할 때보다 연결이 훨씬 단순해졌다!

DHT11 센서와 SSD1306 디스플레이를 연결한 회로도

디스플레이를 제어하려면 I2C0 버스로 명령을 보내야 한다. 이 명령은 SSD1306 데이터시트에 설명되어 있다. 하지만, 그러한 어렵고 힘든 과정을 거칠 필요가 전혀없다. 이미 잘 작성된 라이브러리를 사용해서 API만 호출하면 된다. 먼저 새로운 프로젝트를 하나 생성(또는 이전 dht11-test 프로젝트를 재사용)하고 아래 두개의 라이브러리를 추가한다.

$ npm i https://github.com/niklauslee/ssd1306
$ npm i https://github.com/niklauslee/dht

그럼 아래 코드를 index.js 파일로 작성하고 CLI를 통해 업로드 하여 온도와 습도가 그래픽 디스플레이에 표시되도록 해보자.

SSD1306 디스플레이에 온도와 습도값 출력

코드를 살펴보면 SSD1306 클래스의 인스턴스를 생성한 후에 setup() 을 호출하여 디스플레이를 초기화 한다. 초기화가 완료된 이후에 먼저 getContext() 를 호출하여 그래픽 컨텍스트(Graphic Context) 객체를 가져온다. 이 그래픽 컨텍스트가 도형, 문자, 비트맵 등을 그릴 수 있는 함수들을 제공한다. 반드시 기억해야 할 것은 도형을 그리는 함수를 호출하면 내부적인 버퍼(buffer)에 기록이 되고 화면에는 나타나지 않는다. 그려진 이미지 버퍼를 화면에 나타나게 하기 위해서는 반드시 display()를 호출해야 한다는 것을 잊지말자. 그래픽 컨텍스트의 자세한 기능들은 레퍼런스를 참고하기 바란다.

그래픽 디스플레이에 표시된 온도와 습도

폰트(Font)와 비트맵(Bitmap)

그냥 저렇게 숫자만 표시할거면 그래픽 디스플레이를 사용하는 의미가 없다. 폰트도 바꾸고 이미지도 그려넣어서 좀 더 보기좋게 만들어보자. 먼저 몇가지 폰트를 포함하고 있는 모듈을 설치해보자.

$ npm i https://github.com/niklauslee/simple-fonts

그리고 아래와 같이 온도계 모양의 비트맵 파일 thermo.bmp.json 을 생성해보자. 데이터를 살펴보면 16x16 픽셀의 크기에 픽셀당 1-bit 그리고 데이터는 base64로 인코딩되어 있다. 본 강좌에서는 비트맵을 생성하는 방법에 대해서는 자세히 다루지 않는다.

{
"width": 16,
"height": 16,
"bpp": 1,
"data": "AcACLAIgAiACrAKgAqACrAKgAqAF0AXQBdACIAHAAAA="
}

이제 새로운 폰트와 비트맵을 화면에 잘 배치해보자. 온도를 좀 더 잘 볼 수 있도록 폰트를 3배로 확대하고, 습도는 2배정도로 표현했다. 비트맵의 크기(16x16)가 화면 크기(128x64)에 비해 좀 작기 때문에 가로 세로 각각 3배씩 확대해서 그려보았다.

폰드와 비트맵을 이용한 온습도 표시
폰드와 비트맵이 적용된 온도와 습도

이번 강좌에서는 I2C와 SPI 통신에 대해서 간략히 배우고 이것을 이용해서 SSD1306 그래픽 디스플레이를 연결해서 온도와 습도를 표시해보았다. 마지막으로 다음 강좌에서는 온도와 습도 정보를 인터넷으로 전송해서, 스마트폰이나 웹브라우저에서 확인이 가능하도록 해보자.

--

--

Minkyu Lee
Minkyu Lee

Written by Minkyu Lee

PhD in Computer Science, JavaScript enthusiast, Amateur Jazz drummer.