인공위성 프로젝트
https://www.bhoite.com/sculptures/boron-lander/
인터넷을 하다가 이런 iot 프로젝트를 봤다
MCU 보드와 ST7789 디스플레이, 셀룰러 통신을 이용하여 데이터를 수신하고, 이를 출력하는 프로젝트였다
프로젝트 설명을 보면 다음과 같이 Particle Boron 404x를 이용하여,
LTE CAT M1 안테나와, SHT 31 온습도 센서를 이용해 데이터를 수신하고,
ST7789 240x240 IPS와 SPI 통신을 이용하여 데이터를 출력하는 간단해보이는 프로젝트였다
이를 따라 구현해보고 싶어 도전해보았다
1. 목표
프로젝트 목표는 간단히 데이터를 받아와서
스크린에 날짜, 온도 등의 환경 정보들을 출력하는 것으로 설정하였다
소프트웨어 구현에 목표를 두었기 때문에, 전원 공급이나 하드웨어 구현은 생략하였다
2. 프로젝트 구현 과정
2-1. 하드웨어 구현
블록 다이어그램
블록 다이어그램은 다음과 같다
ESP32를 WiFi 이용하여 날씨 정보를 받아오고,
SHT31과 I2C 통신을 이용해서 온습도 정보를 받아와
TFT 스크린에 SPI 통신으로 출력한다
컨트롤러
ESP32 보드
아두이노, 라즈베리파이를 제외한 다루어보지 않은 보드를 사용해보기 위해 ESP32를 선택하였다
라즈베리파이보다는 가볍고, 아두이노보다는 성능이 좋은 ESP32가 적당하다고 생각했다
또한 무선 와이파이 연결이 가능하므로, 별도의 네트워크 연결이 필요없다는 점도 장점이다
디스플레이
저렴하고 쉽게 구할 수 있는 1.44인치 디스플레이를 선택하였다
ESP32의 전력공급으로도 출력이 가능하여 이를 선택하였다
온습도 센서
SHT31 온습도 센서를 사용하였다
많이 사용하고, 소형이기 때문에 선택했다
핀맵
핀맵은 다음과 같다
별도의 하드웨어 구현없이 소프트웨어 구현이 목표이므로 브레드보드에서 개발을 진행하였다
2-2. 소프트웨어 구현
전체적인 흐름도는 다음과 같다
1
2
3
4
1. 부팅 시 와이파이 연결 설정
2. OpenWeatherAPI를 통해 날씨와 시간 정보 받아오기
3. SHT31로 온습도 정보 받아오기
4. 받아온 정보를 TFT 스크린에 출력한다
세부적인 내용은 개발하면서 차차 수정하였다
1. 와이파이 세팅
기본적으로 제공되는 와이파이 예제를 참조하였다
와이파이는 스마트폰의 테더링을 이용하였고,
별도의 입력장치가 존재하지 않았기 때문에 이름과 비밀번호를 하드코딩했다
나중에 기회가 된다면 별도의 입력장치를 두어서, 이를 개선해보고자 한다
2. OpenWeatherAPI를 통해 날씨 정보 받아오기
이전 프로젝트에서 OpenWeatherAPI를 통해 날씨 정보를 받아와,
Vue로 제작한 웹에 출력하는 프로젝트를 한 경험이 있었다
이를 이용하여, ESP32에서 위치정보, 토큰과 함께 요청을 보냈다
1
https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}&units=metric
이렇게 요청하면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"coord": {
"lon": 7.367,
"lat": 45.133
},
"weather": [
{
"id": 501,
"main": "Rain",
"description": "moderate rain",
"icon": "10d"
}
],
"base": "stations",
"main": {
"temp": 284.2,
"feels_like": 282.93,
"temp_min": 283.06,
"temp_max": 286.82,
"pressure": 1021,
"humidity": 60,
"sea_level": 1021,
"grnd_level": 910
},
"visibility": 10000,
"wind": {
"speed": 4.09,
"deg": 121,
"gust": 3.47
},
"rain": {
"1h": 2.73
},
"clouds": {
"all": 83
},
"dt": 1726660758,
"sys": {
"type": 1,
"id": 6736,
"country": "IT",
"sunrise": 1726636384,
"sunset": 1726680975
},
"timezone": 7200,
"id": 3165523,
"name": "Province of Turin",
"cod": 200
}
이런식으로 응답이 온다
자세한 내용은 https://openweathermap.org/current 참조
또한 아두이노 IDE에서 JSON을 사용하기 위해서 Arduino_JSON 라이브러리를 설치하여 JSON을 다루었다
이를 이용해 원하는 데이터인 온도, 습도, 기압, 풍속, 일출, 일몰 등의 날씨 정보들을 JSON 데이터로 얻을 수 있었다
그러나 가장 중요한 시간 정보가 누락되어있는것을 알았다
3. ntp 시간 정보
시간을 구하기 위해서 크게 두가지 방법을 고려했다
하드웨어적으로 RTC 모듈을 이용하여 자체적으로 계산하는 방법
ntp 서버를 통해 시간 정보를 받아오는 방법
첫번째 하드웨어적인 방법은 인터넷 연결 필요없이 독립적으로 동작하고, 구현이 간편하다는 장점이 있지만
이미 날씨 정보를 얻기 위해 WiFi를 연결해 두었기 때문에, ntp 서버를 이용하는 방법을 사용하였다
또한, ntp 서버를 이용해 시간 오차를 보정할 수 있다는 추가적인 장점이 있다
4. SHT31 온습도 센서
온습도 정보도 날씨 API를 통해 얻을 수 있지만, 현 장소의 온습도 정보를 얻는것이 중요하다고 생각했다
온습도 정보를 수신하는 위치와, 실제 장소간의 차이가 발생하는 경우가 종종 있기 때문
이를 위해 SHT31을 이용하여 직접 온습도 정보를 얻기로 결정했다
아두이노에서 SHT31을 사용하는 것 처럼 ESP32에서도 SHT31을 사용하기 위한 라이브러리가 있다
Adafruit_SHT31.h
라이브러리는 I2C통신을 이용하여 SHT31와 MCU간의 통신을 연결해주는 라이브러리다
이를 이용해서 현재 장소의 정확한 온습도 정보를 얻을 수 있었다
5. SPI 출력
이렇게 얻은 정보들을 TFT 1.44인치 디스플레이에 출력해야한다
TFT 스크린은 다루어본 적이 없어서 꽤나 고생하였다
임베디드 개발을 하다보면 항상 겪는 문제인데
이게 하드웨어적인 문제인지, 소프트웨어적인 문제인지 구분하기가 어렵다는 것이다 (보통은 내 잘못임)
데이터핀을 잘못꽂은건가 싶어서 배선도 바꾸어보고, 선이 단선된건가 싶어서 멀티미터기로 찍어도보고
엄청 고생을 하고 나서야, 라이브러리를 잘못 썼다는것을 알았다
여러 디스플레이를 테스트하다보니 라이브러리가 꼬였는데
128 * 160 스크린은 ST7735
라이브러리를
128 * 128 스크린은 ILI9163C
라이브러리를 써야한다는 것을 알았다
여튼 이렇게 라이브러리를 겨우 설정하고 나서야 예제가 출력이 되었다
OFFSET 처리
그러나 여기서 또 문제가 발생하는데, 화면 상단에 노이즈가 3픽셀 줄 정도 발생했다
검색해보니, 해당 문제를 겪는 사람들이 꽤 있었다 링크
해당 디스플레이의 초기 모델은 OFFSET에 문제가 있었고, 현재는 개선되어 판매되었지만, 내가 가진 모델은 구버전이라 이런 문제가 발생한 것이였다
이를 해결하기 위해서 TFT_ILI9163C.h
헤더 파일에 #define __OFFSET 0
를 추가했다
이로 해결된 것 처럼 보였지만 화면을 회전하니까 이번에는 왼쪽으로 3픽셀정도가 잘리는 문제가 발생했다
링크를 참고해서
colstart, rowstart
변수에 문제가 있을수도 있다는 것을 알았다
확인해보니 xt라이브러리 내 cpp 파일에서 rowstart, colstart값이 회전시 OFFSET 값이 갱신되지 않는 것을 확인하고 수정하니
이를 수정하여 상단이 짤리는 문제 해결할 수 있었다
정상적으로 예제가 출력되는 모습
6. 병렬 수행의 필요성
TFT 스크린을 정상적으로 출력하는데 한주정도 소요했다
이젠 얻은 데이터들을 원하는대로 출력만 하면 되겠지 싶었는데 새로운 문제가 발생했다
날씨 정보는 5분, NTP 시간 정보는 5분 간격으로, 온습도 정보는 1분 간격으로 갱신하려했다
간단하게 1초당 1씩 늘어나는 타이머 변수를 두고,
타이머가 60이 되면 온습도 정보를, 300이 되면 날씨, ntp 정보를 받아오게 구현했다
시간 체크를 위해 delay
를 사용해서 구현하였는데, 이는 많은 문제점들을 발생시켰다
delay를 사용해버리면, 프로그램 자체가 멈춰버리는데, 이동안 다른 작업 수행이 불가능하기 때문이다
이를 해결하기 위해서 millis()
함수를 이용하여 시간을 계산하였다
millis
함수를 사용하면 현재 프로그램을 돌리기 시작한 후 지난 밀리초를 반환하는데
이는 약 50일까지 계산할 수 있다고 한다
여튼 이를 이용하여 시간 간격을 구하고, 1분 간격, 5분 간격으로 수행할 내용을 별도의 함수로 구현했다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void loop() {
unsigned long currentMillis = millis(); // 현재 밀리초 구하기
...
// 1초 간격 수행 : 시간 업데이트 및 그리기
if (currentMillis - previousMillisSec >= intervalSec) {
previousMillisSec = currentMillis;
updateLocalTime();
drawTime();
}
// 1분 간격 수행 - 온습도 갱신
if(currentMillis - previousMillisSec >= intervalTempHumi){
checkTemperature();
drawTempAndHumi();
}
// 5분 간격 : NTP 서버에서 시간 보정
if (currentMillis - previousMillisNTP >= intervalNTP) {
previousMillisNTP = currentMillis;
syncTimeWithNtp();
}
// 5분 간격 : 날씨 정보
if (currentMillis - previousMillisWeather >= intervalWeather) {
previousMillisWeather = currentMillis;
checkWeatherInfo();
}
}
이후에는 데이터를 가져와서 변수로 출력한다
JSON 데이터를 가져와서 float로 저장 후, 이를 snprintf 함수를 통해 포메팅 후
이후 이를 tft.print()의 매개변수로 넘겨주니 TFT 스크린에 출력이 가능했다
이제는 이를 디자인적으로 잘 배치해서 출력만 해주면 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 시간 출력하는 예시
void drawTime(){
if(prevMin != timeinfo.tm_min){
tft.fillRect(14,54, 70, 25, ST7735_BLACK); // TFT 스크린의 {14, 54}에서 70, 25만큼 검은색으로 칠함
tft.setTextSize(3); // 텍스트 크기는 3으로 설정
tft.setCursor(14, 54); // 텍스트 시작 위치는 {14, 54}
char timeStr[5];
snprintf(timeStr, sizeof(timeStr), "%02d%02d", timeinfo.tm_hour, timeinfo.tm_min);
tft.print(timeStr);
prevMin = timeinfo.tm_min;
}
if(prevSec != timeinfo.tm_sec){
tft.fillRect(88,58, 30, 25, ST7735_BLACK);
tft.setTextSize(2);
tft.setCursor(92, 61);
char secStr[3];
snprintf(secStr, sizeof(secStr), "%02d", timeinfo.tm_sec);
tft.print(secStr);
prevSec = timeinfo.tm_sec;
}
}
// 다른 함수들도 이와 유사하게 동작한다
7. UI 디자인
Figma를 통해서 간단히 어떻게 배치할 것인지 디자인해보고, 이를 TFT 스크린에 출력했다
다른 디지털 디스플레이에서는 이런 인포들을 어떻게 배치하는지 참고했다
그 중 가장 많은 도움이 됬던 것은, 스마트워치의 디스플레이였다
피그마로 제작한 좌표를 tft.setCursor로 맞추니 약간 안맞는 문제가 발생했는데,
이는 세부 조절로 간단히 해결할 수 있었다
그러나 또 다른 새로운 문제가 또 발생했다
8. Flickering
화면 전체를 일일히 다시 그리다보니, 화면이 깜빡이는 현상이 발생했다
그릴 내용이 너무 많다보니, 그리는데 시간이 꽤 걸려서 깜빡임이 눈에 보이는 것이였다
이를 해결하기 위해 찾아보니 partial update
라는 기법이 있다는 것을 알게 되었다
즉, 전체를 다시 그리는게 아닌 필요한 부분단 다시 덧그리는 것
텍스트를 갱신할 부분에 검은 직사각형을 덧그리고 위에 글씨를 다시 쓰는 방식을 사용했다
이를 이용해서 플리커링 현상을 해결할 수 있었다
9. 아이콘 추가
텍스트만 있다보니 화면이 너무 심심한 것 같아 그림을 추가하고자 하였다
TFT 스크린에 그림을 출력하는 방법을 몇가지 찾아보니
직접 점을 찍어 도트 그래픽으로 구현하는 방법
SPI 통신을 이용해 MCU로 부터 그림을 가져와서 출력하는 방법
폰트로 추가하여 이를 출력하는 방법
등 여러 방법이 있었다
첫번째 방법은, 픽셀 아트처럼 커서를 옮기고, 점하나 찍는 과정을 직접 일일히 구현해야하므로
너무 노가다가 심하다고 생각하여 패스했다
두번째 방법은, 고화질 이미지를 출력하는 방식인데, 이 프로젝트에서는 간단한 아이콘정도만 필요하므로
이미지를 불러오는데 너무 오버헤드가 발생한다고 생각하여 패스했다
마지막으로 선택한 방법은 폰트에 직접 커스텀한 아이콘을 넣고 이를 텍스트처럼 불러오는 방식이였다
처음 폰트 작성할 때만 조금 수고스럽고, 출력은 매우 간단해보였으므로 이 방식을 선택했다
자세한 과정은 Adafruit GFX 커스텀 폰트에 정리해 두었다
간단히 설명하면 16*16 픽셀 그림을 하나씩 넣는 방법
날씨에서 사용할 아이콘들이 필요했으므로
번개, 안개, 비, 눈, 바람, 해, 구름 정도만 간단히 그렸다
이렇게 원하는 정보들을 디스플레이에 출력하는 소프트웨어의 개발을 완료했다
10. 케이싱 도전
블렌더까지 써가면서 배선 및 케이싱을 디자인했지만 손이 못따라갔다
맨 처음 본 사진처럼 구리선을 이용하여 케이싱 및 배선을 구현하려 하였으나
직접 납땜에 도전했다가 ESP32칩 2개, 온습도 센서 1개, 구리선, 기타 등등 많이 해먹었다
맨날 빵판에 꼽다가, 직접 납땜을 하려하니 쉽지 않았다
기판에 인두기를 너무 지져서 기판에 손상이 가기도 하고, 구리선으로 배선을 하는 것도 엄청나게 힘들었다
거의 두세달간 기판 해먹고, 재구매하고, 또 해먹고를 반복하다가
조금 더 납땜에 익숙해지면 그 때 다시 도전하기로 결정했다
3. 배운점
이번 프로젝트를 진행하면서 많은것들을 배웠다
아두이노 IDE에서 ESP32를 이용한 개발 진행
Adafruit GFX 라이브러리를 이용한 TFT 스크린 제어
SPI 통신 및 I2C 통신
millis() 함수를 이용한 병렬 통신
등등 다양한 것들을 배웠고, 특히 TFT 스크린을 다루면서
디지털 스크린이 어떻게 출력되는지, 레이아웃들을 배치할 때 어떤걸 신경써야하는지 등을 많이 배울 수 있었다
기회가 된다면 케이싱 제작까지 해서 기념품으로 남기고 싶었지만
나중에 기회가 된다면 하는걸로…