1. 새로운 마음으로.
지금까지 만들었던 오실로스코프들을 뒤로 하고, 새로운 마음으로 기획하기로 결정하였다. 새롭게 만드는 버전은 어떠한 요구사항이 있을까 정의해 보았다. 일부 요구 사항은 이전 버전에 반영하기도 하였다.
[[ 새 버전의 요구사항 ]]
- 3개 이상의 측정용 채널을 가지면 100K 이상의 샘플링 속도를 가진다.
- 3.2인치의 TFT LCD를 이용하여 정보를 표시한다.
- 터치를 이용하여 사용자 입력을 받는다.
- 최대 30V의 전압이 측정가능하게 한다.
- 18650 배터리를 장착하여 휴대가 용이하게 한다.
- 18650의 배터리 용량을 체크하여 화면에 표시한다.
2. 필요 모듈 정의 및 구매
- 3.2인치 터치 가능한 TFT LCD ( TFT LCD 구매링크 )
크기가 커질수록 LCD는 비싸진다. 필요한 크기를 맞게 구매 하면 된다. 대략 2.8인치나 3.2 인치를 추천하며, 금번 제작에 사용한 모듈은 아래의 링크로 구매하였다. 터치가 있는 모델과 없는 모델은 가격이 1달러 정도 차이나며, 편의성을 고려하여 터치가 있는 모델을 구매하였다.
- Lolin ESP32 ( lolin32 구매 링크 )
사실 종류는 많긴 하지만, 이정도 가격이면 큰 문제 없을듯 하다. 이번 오실로스코프 제작에 사용한 ESP32는 링크한 것보다 대략 3배 정도 가격으로 아주 오래전에 구매한 모듈이다. 사용처를 못 찾고 있다가 금번에 사용하게된 케이스이다.
- 그외에 18650 배터리 충방전 보호모듈(대략 개당 250원), 스페이서(PCB 간격유지용), 배터리, 저항등이 필요.
3. 모듈 테스트
이미 제품화된 모듈이라도 각각 선행 연구를 진행하지 않으면 그 활용성은 제로에 가깝다. 어떠한 모듈이라도 선행 연구가 필요하다는 것이다.
먼저 esp32의 아날로그 핀에서 12비트로 전압을 측정하면 아래의 표에 따른다.
(이것은 esp32 제조사에서 제공하는 표이다.)
12비트로 데이터를 읽으면 완벽하게 선형에 가깝지 않고 살짝 노이즈가 들어간다는 느낌이 들기 때문에 선형에 근사시키기 위하여 10비트로 데이터를 읽으며, 아래의 표를 참조하면 측정시 6db로 읽으면 가장 유리하다는 것을 알 수 있다.
위의 표를 참조로 해서 ESP32의 아날로그핀으로 측정한 경우 전압은 아래와 같다는 것을 알 수 있다. (이 수식은 분압과 관련된 내용을 이미 포함하고 있다. 순수한 전압 입력이 아니며 대략 10:1 정도로 분압된 상태이다. )
VOLT = analogValue / 37.23 - 3.90
이 수식은 분압에 사용한 저항, 다이오드에 의하여 변경되며, 심지어 사용한 저항들의 오차에 의하여 변동되기 때문에 적정 수식을 찾는 것은 각자 노력해야 한다.
ESP32의 아날로그핀 하나로 장착된 배터리의 잔량을 표시하기 위해서도 18650 배터리와 관련된 특성을 연구하여야 했다. 배터리의 경우 전압과 남은 용량은 단순한 1차 방정식이 아니다. 배터리 관련하여 테스트할 장비가 지극히 부족한 내 입장에서 웹서핑을 데이터를 충당하였다.
ex ) http://www.batteries18650.com
https://lygte-info.dk/
구글이미지 검색 등
이리하여 도출한 방정식은 3차 방정식이다.
[배터리 잔량] = (-1.6919 * V^3 + 18.451 * V^2 - 65.405 * V + 75.55) * 100%
(V = Volt)
ESP32의 경우 (뭐든 그렇겠지만..) 아날로그핀으로 데이터를 읽을때, 갑자기 값이 튀어 오르거나 등의 기대하지 않은 동작을 하는 경우가 있으며, 보다 정확한 값을 위해서는 여러번 측정하여, 최대값, 최소값을 버리고 중간값들의 평균을 이용하는 방법이 있다. (뭐 통계학적 기술이며 큰 의미를 두지는 말자..)
4. 회로도
대단할 것도 없고, 어려운 것도 없는 회로도 이다.
(사실 회로도를 안그리고 납땜부터 해 버린지라... .나중에 그렸다.)
위의 그림대로 납땜을 하되, 충방전 보호 회로 + 스위치 + 배터리 홀더 정도를 붙이면 아래와 같다. 7 X 5 Cm 기판을 두장 붙여서 사용했다. 기판 두장의 서로 붙이는 면을 사포 또는 줄을 이용하여 평탄화 작업을 하고 강력 본드로 붙이면 된다. 물론 강력 본드로 붙이는 만큼 충격에 약하다. 취중 납땜이라 그닥 안이쁘다. ㅠㅠ.
듀얼레이어 이기 때문에 뒷편에 LED를 끼울수 있는 소켓 및 몇개의 배선을 했다.
사실 Lolin32에는 이미 18650 전용은 아니지만 충방전 보호회로가 존재한다. 다만 Lolin32의 micro usb 소켓 보호 차원에서 250원짜리 충방전 보호 모듈을 따로 붙인다. (소켓이 망가지면 그걸 떼내고 새로운것을 붙이면 되니까..)
실제 모듈을 탑재하면 아래와 같은 모습이 된다.
5. 기능 소개
스위치를 켜고 프로브를 연결하면 아래와 같이 표시되는데, 3채널중 1,3번 2개 채널의 프로브만 연결한 상태이다.
추후에 동영상을 촬영하여 붙이겠지만(언젠가는;;; ㅠㅠ), 위의 그림 기준으로 아래의 7개 버튼을 설명하자면 아래와 같다.
1. 트레킹 모드 버튼
한번 누르면 트레킹 대기 모드로 진입하며, 이때 부터 위상 변화를 추적하여, 위상 변화가 생기면 최대 1000번을 측정하여, 보여준다. 트래킹 대기 모드에서 한번더 누르면 빠져나온다.
2. 확대 버튼
가로 방향으로 확대하는 버튼. 최대 8배
3. 축소 버튼
가로 방향으로 축소하는 버튼. 최대 1/8배. 축소 버튼이 왜 필요하냐 하면, 내가 만든 오실로스코프는 자동 트리거 기능인데, 대략 2000 측정 포인트 내에 상승 파형이 두번 나와야 된다. 2000 측정 포인트 이내에 상승 파형이 나오지 않는 경우 측정해도 파형이 흐르는 상태가 된다. 이때 축소 버튼을 누르면 자동 트리거가 가능하게 된다. (2000 측정 포인트라 하면 화면 표시 범위의 몇배 수준이다...)
4. 좌 스크롤 버튼
말 그대로 왼쪽으로 스크롤 하는 기능
5. 우 스크롤 버튼
오른쪽으로 스크롤;;; 상시 측정 상태에서도 스크롤 버튼을 쓸수 있으나, 트래킹 모드에서 가장 유용하게 쓰인다.
6. 채널 통합 분리 기능
위의 그림은 3개 채널이 분리된 상태이다. 이 버튼을 누르면 3개 채널이 통합되어 1개의 최대값 기준으로 표시된다. 채널이 분리된 상태에서는 각 채널별로 최대값에 따라 세로 방향으로 자동스케일 된다.
7. 채널 켜기 끄기
누를때 마다 화면에 표시되는 채널 수가 1->2->3->1 의 순서로 바뀐다. 1개 채널만 측정할때와 3개 채널을 모두 측정할때는 측정시간이 차이가 난다. 이에 따라서 필요한 채널수 만큼만 켜고 측정하도록 만들었다. 최고 밀도로 측정하기 위해서는 1개 채널만 켤 필요가 있다.
아래에 youtube 동영상의 링크를 달았다. 동영상 전문가가 아니니, 이해를.... 또한 음성은 없으며, 향후 자막이라도 추가해야 할 듯 하다. 일단 그냥 그림만 보는 정도로...
6. 기타 컨셉을 추가
아래의 그림이 오실로스코프 밑판이다.
위의 사진에서 가운데 길다란 것은 터치펜의 보관을 용이하게 하기 위하여 추가한 컨셉이며, 왼쪽 아래 가로 방향의 검은색 작은 막대는 스페이서 이며, 받침대로 사용하기 위하여 추가한 컨셉이다.
이렇게 만든 밑판을 처음에 만든 상판과 마주게고 해서 고정하면 아래와 같이 되는 것이다. 특별히 아래의 사진은 받침대로 사용하기 위한 스페이서까지 연결하고 세운 형태이다.
아래에 트래킹 모드를 이용하여 측정한 몇개의 파형 사진을 올려본다. 개발 도중에 촬영한 사진이라 전압과 진동수는 맞지 않다.
Sign 파형
Square 파형
Triangle 파형
7. 코드 설명?
먼저 Display에 대한 소스코드는 생략 하기로 한다. 어차피 나 또한 인터넷에서 찾은 내용이며 향후 display에 대한 라이브러리가 업데이트되는 것을 고려한다면 원 제작자가 제공하는는 웹페이지에서 다운로드 받는 방법을 추천한다. 나는 이러한 라이브러리를 다운로드 받고, 이전에 만들어 놓은 한글 출력 코드를 추가하여 사용했다. (하지만 오실로스코프의 화면에서 한글을 볼 수는 없다. 쓰이는 글자라고는 Hz)
ILI9341_Oscilloscope_32.ino (크게... 대단한 내용은 없음.)
/*
ESP32기반의 Lolin32에 특화면 오슬로스코프 버전
320 * 200 ILI9341 TFT 에 특화 되었으며,
XPT2046의 터치 컨토롤러를 사용한다.
한글 폰트와 설정내용 저장을 위하여 SPIFFS 를 사용한다.
*/
#include "OscilloscopeClass.h"
OscilloscopeClass *Oscilloscope;
void setup() {
Serial.begin(115200); // 디버깅 정보 출력용
Oscilloscope = new OscilloscopeClass();
Oscilloscope->begin();
}
void loop(void) {
Oscilloscope->feed();
}
OscilloscopeClass.h
#ifndef _OSCILLOSCOPE_CLASS_H_
#define _OSCILLOSCOPE_CLASS_H_
#include <Arduino.h>
#include <SPI.h>
#include "./src/display/Display_ILI9341.h"
#include "./src/display/XPT2046_Touchscreen.h"
#define TFT_CS 5 //
#define TFT_DC 16
#define TFT_MOSI 23 // Lolin32 기본핀
#define TFT_CLK 18 // Lolin32 기본핀
#define TFT_RST 17 // TFT를 리셋하기 위한 핀..
#define TFT_MISO 19 // Lolin32 기본핀
#define TFT_LED 4 // TFT를 키거나 끌때 사용한다.
#define ANALOG_PIN0 34 // 아날로그 첫번째 핀.
#define ANALOG_PIN1 35
#define ANALOG_PIN2 32
#define BAT_CHECK_PIN 33 //연결된 배터리의 상태를 체크 하기 위한 내용
#define TFT_DIRECTION 1 // 이걸 3으로 주면 위아래가 바뀐다. 1 또는 3만 사용 가능
// For touch
#define TOUCH_IRQ 13 // TOUCH 역시 SPI통신이며.. TFT와 대부분 연결을 공유한다.
#define TOUCH_CS 2
#define MCP_CS 32 // for mcp3202 (ESP32 버전에서는 사용하지 않는다.)
#define CHART_WIDTH 280 // 차트 부분만 해당하는 가로 크기
#define CHART_HEIGHT 190 // 차트 부분만 해당하는 세로 크기
#define MAX_READ_NUM (CHART_WIDTH + 80) // 최대 데이터 읽기..화면에 표시하는 범위보다 살짝 넓게..
#define CHANNEL_NUM 3 // 현재는 3개 채널만 읽는다..(4개가 필요할까?)
#define BUTTON_NUM 7 // 전체 버튼은 7개...음..
static const uint16_t g_aryChannelColors[CHANNEL_NUM] = {0x051D, 0x26EA, 0xEBE4};
class OscilloscopeClass {
public:
OscilloscopeClass();
void begin();
void feed();
float getVolt(int ch);
private:
Adafruit_ILI9341 *m_pTFT ;
XPT2046_Touchscreen *m_pTouch;
// 현재 상황에서 ADC를 통하여 읽어야 하는 데이터 개수..
// Tracking mode인 경우 평소의 3배까지 읽는다.
uint32_t m_dNowReadNum;
// 차트를 제외한 부수적인 것들을 마지막으로 그린 시간
// 차트는 지속적으로 다시 그리고 있으며 그외에 부수적인 것들은 가끔씩 그린다.
uint32_t m_dPrevDrawTime;
// 화면이 껌뻑 거리기 때문에... 더블버퍼 컨셉을 이용한다.
// ESP32에 최적화 되어 있으며, 8266의 경우에는 일부 수정해야 한다.
// 화면 전체크기에 해당하는 내용을 가지고 있지 않으며, 차트 부분에 해당하는 크기만 할당 받음.
uint16_t *m_pFrameBuffer;
// ADC로 부터 데이터를 읽어서 저장하는 부분
// 앞쪽페이지는 실제 측정한 값, 뒤쪽 페이지는 화면 그리기를 위한 보정값 임
// 일반적으로는 MAX_READ_NUM 크기만큼 읽지만, Tracking모드인 경우 세배까지 읽음.
uint8_t m_aryDatas[MAX_READ_NUM * 3][CHANNEL_NUM * 2];
int m_aryMaxValues[CHANNEL_NUM]; // 각 채널별 측정된 최고값
bool m_bCh3On; // 세번째 채널 켜기 끄기 상태
bool m_bCollapseChannel; //모아 보기 인지, 나누어 보기 인지 ..
uint8_t m_dScreecDirection; // TFT 방향.. 1, 3만 지원함(즉. 가로로 긴 것만 지원)
uint8_t m_dTracking; // Tracking 모드인가 아닌가..
uint16_t m_dStart; // 측정된 데이터중에서 화면에 표시하는 시작 위치
// 정밀도.. 1이 가장 좋은 것이고.. 커질수록 나빠진다...
// 즉 m_dScale 이 3인 경우 ADC로 부터 세번 읽어서 두개는 버리고 하나의 값만 쓴다.
uint8_t m_dScale;
// 이건 한번 측정한 값을 얼마나..더 넓게 표시하냐의 단위이다
// 이 값이 3인 경우, 1번 측정한 값을 화면에 3개의 픽셀로 표시한다.
uint8_t m_dSubScale;
uint16_t m_dFrequency; // 패널 1 기준 주파수..
uint16_t m_dFps; // 화면 갱신 속도
uint32_t m_dPrevTouchTime; //마지막에서 터치를 손 뗸 시간...
int8_t m_dNowPressedButton; // 지금 눌려지고 있는 버튼..
// 이하 함수 설명은 cpp 파일 참조
void setRotations();
bool loadSettings();
bool saveSettings();
void drawTopSideBackbone();
void drawMiddleSideBackbone();
void drawButtons(int8_t target = -1);
void drawBackbone();
void drawVoltages();
void drawBufferedString(int16_t x, int16_t y, const char * text, uint16_t textColor, uint16_t backColor);
void drawBufferLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color, int16_t gap, int16_t width = CHART_WIDTH);
void drawChart();
void readADC();
void adjustADCValues();
void reRead();
void enterTrackingMode();
void leaveTrackingMode();
int8_t getButtonIndex(int16_t x, int16_t y);
void touchDownProcess(int16_t x, int16_t y);
void touchUpProcess(int16_t x, int16_t y);
};
#endif //_OSCILLOSCOPE_CLASS_H_
OscilloscopeClass.cpp
전체 소스코드를 올리려 했으나 아무 의미도 없어 보이고, 부분별 설명이 좋을듯 하여 핵심 기능을 발췌해 본다.
1. 설정내용을 SPIFFS에 저장하고 불러 오는 부분
설정 부분에서 저장하는 내용은 화면 방향하나 이다. 즉 자주 사용하는 방향을 바꾸어 놓으면 다음에 켰을때 기억하고 그 방향으로 표시한다. 터치 화면의 가장 오른쪽 윗부분(배터리 잔량이 표시되는 부분)을 터치하면 화면의 상하가 바뀜.
// 기 저장된 설정내용 불러오기
bool OscilloscopeClass::loadSettings() {
// always use this to "mount" the filesystem
bool result = SPIFFS.begin();
File f = SPIFFS.open("/settings.txt", "r");
if (!f) {
Serial.println("setting file not exist");
return false;
} else {
//Lets read line by line from the file
String strDirection = f.readStringUntil('\n');
//Serial.println(strDirection);
if (strDirection.length() > 0) {
m_dScreecDirection = strDirection.toInt();
}
}
f.close();
}
// 설정 내용 저장하기
bool OscilloscopeClass::saveSettings() {
bool result = SPIFFS.begin();
// open the file in write mode
File f = SPIFFS.open("/settings.txt", "w");
if (!f) {
Serial.println("file creation failed");
}
// now write two lines in key/value style with end-of-line characters
f.println(m_dScreecDirection);
f.close();
}
ESP32의 analog 핀으로 부터 정보를 읽는 함수
// 최대 3개의 ADC 핀으로 부터 정보를 읽는다.
// 읽은 것은 데이터 배열의 첫번째 Segment에 넣어둔다.
void OscilloscopeClass::readADC() {
int ch0, ch1;//, sum_ch0, sum_ch1; // ADC Value
int i, j;
// 파형의 올라 가는 부분 찾기...
if (m_dTracking == 0) {
int check = 0;
while (check++ < 2000) {
ch0 = analogRead(ANALOG_PIN0);
if (ch0 < 3 ) {
check = 0;
while (check++ < 2000) {
ch0 = analogRead(ANALOG_PIN0);
if (ch0 > 10) {
break;
}
}
break;
}
}
yield();
}
m_aryMaxValues[0] = 0;
m_aryMaxValues[1] = 0;
m_aryMaxValues[2] = 0;
uint32_t start = micros();
int a0_value = 0;
for (i = 0; i < m_dNowReadNum; i ++) {
for (j = 0; j < m_dScale; j++) {
ch0 = analogRead(ANALOG_PIN0);
ch1 = analogRead(ANALOG_PIN1);
// 살짝 노이즈를 제거해 준다.
if (ch0 < 3) ch0 = 0;
if (ch1 < 3) ch1 = 0;
m_aryMaxValues[0] = max(m_aryMaxValues[0], ch0);
m_aryMaxValues[1] = max(m_aryMaxValues[1], ch1);
if (m_bCh3On) {
a0_value = analogRead(ANALOG_PIN2) ;
// 살짝 노이즈를 제거해 준다.
if (a0_value < 3) a0_value = 0;
m_aryMaxValues[2] = max(m_aryMaxValues[2], a0_value);
}
}
// 최대값이 255가 되도록 저장한다.
m_aryDatas[i][0] = (uint8_t)(ch0 >> 2); // 203 - (int)(ch0 * 0.046142578125); // (uint8_t)(ch0 / 16);
m_aryDatas[i][1] = (uint8_t)(ch1 >> 2); // 203 - (int)(ch1 * 0.046142578125); //(uint8_t)(ch1 / 16);
m_aryDatas[i][2] = (uint8_t)(a0_value >> 2);
}
uint32_t endt = micros();
// find frequency
m_dFrequency = 10;
bool bottomFind = false;
for (i = 0; i < m_dNowReadNum; i ++) {
if (bottomFind == false && m_aryDatas[i][0] < 3 ) {
bottomFind = true;
continue;
} else if (bottomFind && m_aryDatas[i][0] > 10) {
double pulseTime = (double)(endt - start) / (double)m_dNowReadNum * (double)i;
m_dFrequency = 1001000 / pulseTime; //약간의 보정을 위해서 100.1%의 갑으로 계산
break;
}
}
}
핵심 부분이 feed() 함수 인데, 메인 loop에서 항상 불려져야 한다.
// 반드시 메인 Loop 함수에서 이 함수가 주기적으로 불려져야 한다.
void OscilloscopeClass::feed() {
uint32_t start = millis();
// 터치 관련해서 칼리브렝이션중이라면...
if (m_pTouch->isCalibration()) {
if (m_pTouch->feed()) { // 평상시인경우 true, 조정중이라면 false
setRotations();
drawChart();
m_pTouch->ignoreTouchInTime(500);
}
return;
} else {
// 터치 칼리브레이션 상황이 아닌경우..
if (m_dTracking == 0) {
readADC();
adjustADCValues();
drawChart();
}
}
// 매번 그려줘야 할 필요가 없는 부분들은 1초에 2번 정도 그려준다.
if (start - m_dPrevDrawTime > 500) {
m_dPrevDrawTime = start;
m_dFps = (int)(min(60.0, (float)1000 / float(millis() - start)));
drawTopSideBackbone();
drawVoltages();
}
// 터치 관련된 내용 체크..
if (m_pTouch->tirqTouched()) {
if (m_pTouch->touched()) {
uint32_t now = millis();
if (((double)now - m_dPrevTouchTime) > 150.0) {
m_dPrevTouchTime = now;
TS_Point p = m_pTouch->getPoint();
touchDownProcess(p.x, p.y);
}
} else {
// 버튼을 뗀 상황..
TS_Point p = m_pTouch->getPoint();
touchUpProcess(p.x, p.y);
}
}
}
이 코드는 사실 4번째 만든 오실로스코프이며, 다섯번째 만든 오실로스코프에 대한 포스트를 진행할 예정이다. 4편에 계속....
andy-power.blogspot.com/2018/11/esp-4_29.html