2018년 11월 13일 화요일

ESP로 오실로스코프를 만들자 - #1 (ESP8266편)

1. 오실로스코프는 왜?

 나는 오실로스코프를 실제 사용해본적이 없으며, 심지어 오실로스코프를 지인에게 만들어서 배달하면서 지인의 집에서 20년 정도나 되어 보이는 아날로그형 오실로스코프를 직접 본 것이 처음일 정도이다. (실제 그 전에 봤을지도 모르는 기계일지도 모르겠다. 다만 관심이 없었던 터라 인지하지 못하였을 수도 있다)  이러한 내가 지인의 관심 분야를 듣게 되면서 오실로스코프를 만들어 되었다.

2. 쓸만한가?

 내가 지인에게 배달한 오실로스코프는 총 3개의 버전이다. (나는 5개의 버전을 만들었으며,  배달하지 않은 2개의 버전은 집에서 쓰레기 아닌 쓰레기 취급을 받고 있다) 지인은 1차 버전부터 맘에 들어 하였지만, 지인과 지속적인 대화를 통하여 업그레이드가 된 형태이다.  몇개의 포스트를 통하여 수개월간 작업한 내용을 기록할 예정이다.

3.  ESP8266으로 만들기
 내가 가지고 있는 MCU중에서 가장 흔하게 사용 가능한 것으로 시작하였다. 언제나 대량생산이 아닌 관계로 Wemos D1 mini로 만들었었다. (요새는 Wemos라는 이름이 lolin으로 변경된듯 하다.)

이전에 이 블로그에서 소개한 한글 및 OLED 출력용 라이브러리를 이용하여 오실로스코프를 제작하였다. 제작 과정중에 몇개의 살짝 스샷을 찍어 보았다.


위의 첫번째 사진은 측정된 값을 점으로만 표현한 형태이며, 두번째 세번째 사진은 각 측정된 값을 직선으로 연결한 형태이다.


4. 회로도

 크게 복잡할 것 없는 회로이며, 손으로 쓱쓱 그린 버전을 아래에 넣었다.


0.98인치 OLED를 쓰는 것 이외에 Prove로 연결하는 회로가 있는데, 입력 전압을 30V정도까지 고려하였기 때문에 분압 하였다.  프로브 + 측 단자에서 10K 저항을 먼저 연결하고 중간부분을 ESP8266의 A0 핀에 연결, 1K 저항을 직렬연결후 Ground에 연결하면 된다.
 위의 그림에는 트래킹모드(=로깅모드) 시작 버튼에 대한 내용이 없는데, 실제 적용한 모델에서는 D0핀에 푸쉬버튼 하나를 달아서 사용하는 형태이다.

5. 소스코드

 소프트웨어적으로 간단하긴 하지만, 사실 트래킹 모드라는 컨셉을 생각하여 적용한 터라 아주 짧지 않은 코드이다. 트래킹모드(=로깅모드)라 하면 측정 시작 이후 위상 변화가 생길때까지 대기하다가, 위상 변화가 생기면 그때부터 설정된 시간만큼 기록하고 화면을 스크롤 하면서 보여주는 모드이다.

아래에 있는 코드가 이번 오실로스코프제작에 생성한 코드의 전체 이다.
(한글출력이나 OLED 제어용 코드들은 이전 포스트에 소개한바 있으므로 생략한다.)

Oscilloscope.ino
#include <Arduino.h>
#include "Display_SSD1306.h"
#include "OscilloscopeClass.h"

#define TOUCH_BUTTON    D0  // 트래킹 시작 버튼
Display_SSD1306 Oled(-1);

void setup() {
  Serial.begin(115200);
  pinMode(TOUCH_BUTTON, INPUT);

  // Oled I2C 방식으로 연결하고, 주소는 0x3c
  Oled.begin(SSD1306_SWITCHCAPVCC, 0x3C, false);

  // Callback 함수를 설정해 주면 필요시 호출 하여 사용함.
  Oscilloscope.begin(12,
      [](void) { Oled.clearDisplay(); },
      [](int16_t x, int16_t y, uint16_t color) { Oled.drawPixel(x, y, color);  },
      [](void) { Oled.display(); }
  );

  delay(100);
}

void loop() {
  if (Oscilloscope.feed() == 0) { // 트래킹 상태가 아니면
    if (digitalRead(TOUCH_BUTTON)) { // 버튼 상태가 변경되었으면..
      Oscilloscope.startLogging(2000);     // 0.20 로깅
      Oscilloscope.showLogging(); // 측정 결과를 보여줌.
    }   
  }
}



OscilloscopeClass.h
/*------------------------------------------------------------------
   BSD License

   Copyright (c) 2018 terminal0070@gmail.com 
  ------------------------------------------------------------------*/

#ifndef OscilloscopeClass_h
#define OscilloscopeClass_h
#include <Arduino.h>
#include "HanDraw.h"

#define MAX_LOGGING_NUM           20000  //최대 2만개의 데이터를 읽는다.
#define LOGGING_START_WAIT_TIME   2000   //로깅 요청시 얼마나 기다릴것인다.. 밀리세컨드 단위 (500 단위로 설정바람)
#define READ_PIN                  A0    // Wemos d1 mini 아날로그 핀이 하나다.
enum oscilloscope_status {
  WAITING = 0,
  LOGGING = 1,
  SHOWING_LOG = 2
};

class OscilloscopeClass  {
  protected:
    uint8_t m_aryLogginData[MAX_LOGGING_NUM + 128 * 4];
    uint16_t m_dLogginIndex; // 현재 몇번째 읽엇는가 또는 몇번째 부터 표시하고 있는가.
    uint16_t m_dLogginSize; // 전체적으로 몇개의 데이터를 읽을 것인가
    uint16_t m_dMaxValue;   // 측정한 값중에서 최대값
    uint16_t m_dTotalLoggingTime; // 실제 로깅 하는데 소요한 시간
    oscilloscope_status m_dStatus;    // 현재 진행중인 모드
    float m_fFrequency;   // 주파수
  private:
    unsigned long m_dLLastFeedingTIme;//로깅 데이터를 보여주기 시작한 시간
    unsigned long m_dLLastCheckingTIme;//로깅 데이터를 보여주기 시작한 시간
    void displayChart();
    int16_t findUpPosition(int16_t startPos);
    void findFrequence();
   
  public:
    OscilloscopeClass();
    virtual ~OscilloscopeClass();
    void begin(int8_t fontSize, CLEAR_FUNC clearFunction , DRAW_PIXEL_FUNC drawPixelFunction, DISPLAY_FUNC displayFunction );
    void end();
    void clear();
    void display();
    void drawPixel(int16_t x, int16_t y, uint16_t color);
    void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, 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);
    float getVolt();
    void startLogging(int16_t loggingSize);
    void showLogging();
    void displayFreqAndVolt();
    void showCurrent();
    int feed();

};
extern OscilloscopeClass Oscilloscope;

#endif



OscilloscopeClass.cpp
/*
   BSD License

   Copyright (c) 2018 terminal0070@gmail.com
*/

#include "OscilloscopeClass.h"

#ifndef SWAP_INT16_T
#define SWAP_INT16_T(a, b) { int16_t t = a; a = b; b = t; }
#endif
#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif

#ifndef BLACK
#define BLACK 0
#endif

#ifndef WHITE
#define WHITE 1
#endif

int loopX; // for loop
// 범용 사용.. 특히나..차트 그릴때 사용
uint8_t prev1, prev2, prev3, prev4, now1, now2, now3, now4;

OscilloscopeClass::OscilloscopeClass() {}
OscilloscopeClass::~OscilloscopeClass() {
  end();
}
void OscilloscopeClass::end() {}

// 시작시 사이즈의 폰트 크기를 사용할 것인가를 정한다.
// 3개의 callback function 이용하여 디스플레이를 제어한다.
void OscilloscopeClass::begin(int8_t fontSize,
CLEAR_FUNC clearFunction, DRAW_PIXEL_FUNC drawPixelFunction, DISPLAY_FUNC displayFunction) {
  HanDraw.begin(12, clearFunction, drawPixelFunction, displayFunction);
  m_dLogginIndex = 0;
  m_dLogginSize = MAX_LOGGING_NUM;
  m_dStatus = WAITING;
  m_fFrequency = 1.0;
  pinMode(READ_PIN, INPUT); // bolt frequency
}

// 화면을 지우는 것은 콜백 함수를 부른다.
void OscilloscopeClass::clear() {
  HanDraw.clear();
}

// 역시 화면에 표시하는 것은 콜백 함수를 부른다
void OscilloscopeClass::display() {
  HanDraw.display();
}

void OscilloscopeClass::drawPixel(int16_t x, int16_t y, uint16_t color) {
  HanDraw.drawPixel(x, y, color);
}

void OscilloscopeClass::drawString(int16_t x, int16_t y, char* message) {
  HanDraw.drawString(x, y, (const char*)message);
}
void OscilloscopeClass::drawString(int16_t x, int16_t y, String message) {
  HanDraw.drawString(x, y, message.c_str());
}

void OscilloscopeClass::drawString(int16_t x, int16_t y, const char* message) {
  HanDraw.drawString(x, y, message);
}

void OscilloscopeClass::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
  int16_t steep = abs(y1 - y0) > abs(x1 - x0);
  if (steep) {
    SWAP_INT16_T(x0, y0);
    SWAP_INT16_T(x1, y1);
  }

  if (x0 > x1) {
    SWAP_INT16_T(x0, x1);
    SWAP_INT16_T(y0, y1);
  }

  int16_t dx, dy;
  dx = x1 - x0;
  dy = abs(y1 - y0);

  int16_t err = dx / 2;
  int16_t ystep;

  if (y0 < y1) {
    ystep = 1;
  } else {
    ystep = -1;
  }

  for (; x0 <= x1; x0++) {
    if (steep) {
      HanDraw.drawPixel(y0, x0, color);
    } else {
      HanDraw.drawPixel(x0, y0, color);
    }
    err -= dy;
    if (err < 0) {
      y0 += ystep;
      err += dx;
    }
  }
}

// loggingSize 10000 경우 1초간 로깅한다.
void OscilloscopeClass::startLogging(int16_t loggingSize) {
  if (loggingSize > MAX_LOGGING_NUM) {
    loggingSize = MAX_LOGGING_NUM;
  } else  if (loggingSize < 500) {
    loggingSize = 500;
  }
  m_dLogginIndex = 0;
  m_dLogginSize = loggingSize;
  m_dStatus = LOGGING;
  int waitCnt = LOGGING_START_WAIT_TIME /  500;
  char buf[128];

  HanDraw.clear();
  HanDraw.drawString(22, 22, "시그널 대기중");
  HanDraw.display();
  m_dMaxValue = 1.0;
  uint16_t check = analogRead(READ_PIN);
  m_dMaxValue = MAX(m_dMaxValue, check);
  m_aryLogginData[m_dLogginIndex++] = (check / 10);

  if (check < 25) check = 0;
  uint16_t current = 0;
  while (true) {
    yield();
    current = analogRead(READ_PIN);
    if (check == 0) {
      if (current > 25) break;
    } else {
      if (current < 15) break;
    }
  }

  uint16_t startTime = millis();

  m_dMaxValue = MAX(m_dMaxValue, current);
  m_aryLogginData[m_dLogginIndex++] = (current / 10);

  for (loopX = 1; loopX < m_dLogginSize; loopX ++) {
    current = analogRead(READ_PIN); // ESP8266 기준으로 1회에 100마이크로초 정도 걸린다.
    m_dMaxValue = MAX(m_dMaxValue, current);
    // 굳이 10으로 나누어서 넣는 이유는 바이트 범위 넣기 위함이다.
    m_aryLogginData[m_dLogginIndex++] = (current / 10);
  }
  m_dTotalLoggingTime = millis() - startTime;
}
void OscilloscopeClass::showLogging() {
  if (0 == m_dLogginSize) {
    return; // 보여줄 것이 없음으로 포기~
  }
  m_dStatus = SHOWING_LOG;
  HanDraw.clear();
  HanDraw.drawString(31, 46, "표시 준비중");
  HanDraw.display();

  m_dLLastCheckingTIme = millis();
  m_dLogginIndex = 0;
  findFrequence();
  displayChart();
  delay(500);
}

void OscilloscopeClass::displayChart() {
  HanDraw.clear();
  float fDrawScale = 1024 / m_dMaxValue / 5.13;   // 최대측정값 대비 그리기 비율
  uint16_t hop = m_dLogginSize / 2;

  now1 = m_aryLogginData[hop * 0 + m_dLogginIndex] * fDrawScale;
  now2 = m_aryLogginData[hop * 1 + m_dLogginIndex] * fDrawScale;
  for (loopX = 1; loopX <= 127; loopX++) {
    prev1 = now1;
    prev2 = now2;
    now1 = m_aryLogginData[hop * 0 + m_dLogginIndex + loopX] * fDrawScale;
    now2 = m_aryLogginData[hop * 1 + m_dLogginIndex + loopX] * fDrawScale;
    drawLine(loopX , 35 - prev1, loopX , 35 - now1, WHITE);
    drawLine(loopX , 63 - prev2, loopX , 63 - now2, WHITE);
  }

  // 차트에서 표시 범위가 얼만큼인지.스크롤 바로 표시.
  int scrollPos = 39 * m_dLogginIndex / (hop - 128);
  drawLine(87 , 6, 127 , 6, WHITE);
  drawLine(86 + scrollPos , 4, 86 + scrollPos , 8, WHITE);
  drawLine(87 + scrollPos , 2, 87 + scrollPos , 10, WHITE);
  drawLine(88 + scrollPos , 4, 88 + scrollPos , 8, WHITE);

  displayFreqAndVolt();
  HanDraw.display();
}

void OscilloscopeClass::displayFreqAndVolt() {
  if (m_fFrequency > 0) {
    HanDraw.drawString(0, 0, String((int)m_fFrequency) + "Hz " + String(getVolt()) + "V");
  }  else {
    HanDraw.drawString(0, 0, String("--Hz ") + String(getVolt()) + "V");
  }
}

// 파형의 아래 부분에서 올라가기 시작 하는 부분을 찾는다.
int16_t OscilloscopeClass::findUpPosition(int16_t startPos) {
  uint16_t i;
  if (startPos < 0)
    return -1;
  for (i = startPos; i > 0; i--) {
    if (m_aryLogginData[i] < 2) {
      while (--i > 0) {
        if (m_aryLogginData[i] > 2) {
          return i;
        }
      }
      break;
    }
  }
  return -1;
}

// 주파수를 찾으면서 최대 값도 찾는다.
void OscilloscopeClass::findFrequence() {
  double stime, ttime, ttime2;
  int16_t j;
  int16_t etime = m_dLogginSize - 1;
  //float frequencyArray[10]; // frequency 10개의 측정 값을 저장하고 이중 제일 큰것을 화면에표시한다.
  //int   frequencyIndex = 0;        // 배열의 어느 부분에 저장할 것인가를 표시
  double factor = m_dTotalLoggingTime * 996.15 / m_dLogginSize; // 원래는 1000 곱함. 약간의 보정으로 996정도;;;
  ttime = 1;
  for (j = 0; j < 24; j ++) {
    stime = findUpPosition(etime);
    etime = findUpPosition(stime);
    if (etime <= 0 | stime <= 0)
      ttime2 = -1000000;
    else
      ttime2 = (stime - etime) * factor; // 90.91;
    //    Serial.println(ttime2);
    ttime = MAX(ttime, ttime2);
  }
  if (1 == ttime)
    m_fFrequency = 0.0;
  else
    m_fFrequency = (float)1000000 / (float)ttime;
}

/*
  // 이건 저항 연결 상태에 따라서 다르다.
  // 3.3v 전압이 직접  A0 연결되는 경우
  float OscilloscopeClass::getVolt(int16_t val) {
  return  val * 3.3 / 1024.0;
  }

  // 100k 저항 하나, 21.5k 저항 하나를 연결하고 중간에서 체크할경우
  // 약간의 보정을 통해서... 3.3v ~ 6v 정도 구간이 테스터기와
  // 비슷한 값이 나오도록 했다.
  float OscilloscopeClass::getVolt(int16_t val) {
  float volt = val / 1024.0;
  volt += (1024 - volt) * 0.000019;
  volt *= 13.815;
  volt -= 0.02;
  if (volt < 0.25)
    volt = 0.0;
  return volt;
  }
*/

// 100k 저항 하나, 43k 저항 3개를 병렬 연결하고 중간에서 체크할경우
// 약간의 보정을 통해서... 3.3v ~ 6v 정도 구간이 테스터기와
// 비슷한 값이 나오도록 했다.
float OscilloscopeClass::getVolt() {
  float volt = m_dMaxValue / 1024.0;
  volt += (1024 - volt) * 0.0000042;
  //  volt *= 28.035;
  volt *= 28.135;
  volt -= 0.02;
  if (volt < 0.25)
    volt = 0.0;
  return volt;
}

// 현재 측정하는 값을 바로 바로 보여주는 경우
void OscilloscopeClass::showCurrent() {
  unsigned long etime, stime, ttime, xtime, scnt, ecnt, pos;
  scnt = 0;
  ecnt = 0;
  etime = 0;
  stime = 0;
  // 파형이 처음 올라가는 부분을 찾는다.
  while (scnt++ < 400) {
    pos = analogRead(READ_PIN);
    if (pos < 2) {
      while (ecnt++ < 400) {
        pos = analogRead(READ_PIN);
        if (pos > 2) {
          stime = micros();
          break;
        }
      }
      break;
    }
  }
  xtime = micros() - stime;
  scnt = 0;
  ecnt = 0;
  // 파형의 두번째 올라가는 부분을 찾는다.
  while (scnt++ < 400) {
    pos = analogRead(READ_PIN);
    if (pos < 2) {
      while (ecnt++ < 400) {
        pos = analogRead(READ_PIN);
        if (pos > 2) {
          etime = micros();
          break;
        }
      }
      break;
    }
  }

  if (etime == 0 | stime == 0) {
    ttime = -1000000;
  }
  else {
    ttime = etime - stime - xtime;
  }
  m_fFrequency = (float)1000000 / (float)ttime;
  m_dLogginIndex = 0;
  m_dLogginSize = 128;
  uint16_t current = 0;
  m_dMaxValue = 1.0;
  uint16_t startTime = millis();
  for (loopX = 0; loopX < m_dLogginSize; loopX ++) {
    current = analogRead(READ_PIN);
    m_dMaxValue = MAX(m_dMaxValue, current);
    m_aryLogginData[m_dLogginIndex++] = (current / 10);
  }
  m_dTotalLoggingTime = millis() - startTime;
  m_dLogginIndex = 0;
  HanDraw.clear();
  float fDrawScale = 1024 / m_dMaxValue / 2.2;   // 최대측정값 대비 그리기 비율
  now1 = m_aryLogginData[0] * fDrawScale;
  for (loopX = 1; loopX <= 127; loopX++) {
    prev1 = now1;
    now1 = m_aryLogginData[loopX] * fDrawScale;
    drawLine(loopX , 63 - prev1, loopX , 63 - now1, WHITE);
  }
  findFrequence() ;
  displayFreqAndVolt();
  HanDraw.display();
}

// 오실로스코프 기능이 동작하도록 주기적으로 시간을 배분한다.
int OscilloscopeClass::feed() {
  m_dLLastFeedingTIme = millis();
  // 1초에 최대 20 진행 한다.
  if (m_dStatus == SHOWING_LOG) {
    if ((m_dLLastFeedingTIme - m_dLLastCheckingTIme) < 30) {
      return 2;
    }
    m_dLLastCheckingTIme = m_dLLastFeedingTIme;
    m_dLogginIndex += 2;
    displayChart();
    if (m_dLogginIndex  >= (m_dLogginSize / 2 - 128)) {
      m_dStatus = WAITING;
      delay(2000);
      HanDraw.clear();
      HanDraw.display();
    }
    return 1;
  } else if (m_dStatus == WAITING) {
    showCurrent();
  }
  return 0;
}

OscilloscopeClass Oscilloscope;




6. 실제 납땜하고 적용한 모습.


[ 그림 추가 예정]


7. 끝으로

  이 글을 적는 시점 기준으로 수개월 전에 Tiny Oscilloscope라는 이름을 붙여서 배달했다. 지인은 상당히 만족해 했으며, 나에게 소스코드를 받아가서 아래의 그림과 같이 3개를 연결하여 쓰리채널 오실로스코프로서 사용하였다. (숨은그림 찾이인가... 아래 그림을 잘 찾아 보면 0.98인치 OLED가 3개 있는 곳을 찾을 수 있다.)

지인이 실제 사용하는 모습
1 채널짜리 간이형 오실로스코프를 만들어서 배달 했더니, 바로 3 채널 구성하여 사용하시는 지인의 모습을 보면서 2차 버전의 오실로스코프를 제작하게 되는 계기가 되었다.





댓글 없음:

댓글 쓰기

활용도가 높은 파워뱅크를 만들어 보자 - 제1편 (설계 및 재료 조달편)

미안함 때문에...  파워뱅크 1차 버전을 만들어서 선물하였다 (총 2개 제작) . 파워뱅크를 먼저 선물받은 사람은 캠핑을 좋아하는 사람이기에 엄청 좋아했다. 컨셉 자체가 전등을 켤 수 있게 만들었고, 필요시 핸드폰등을 충전할 수 있으며, 쌀쌀한 날씨...