[아두이노] Smoothing의 원리 공부

IOT/아두이노|2019. 4. 18. 09:00

[아두이노] Smoothing의 원리 공부



오늘은 아두이노 공식 홈페이지에서 제공되는 튜토리얼 예제 중에서 Smoothing 예제가 있어서 알아 두면 좋은 예제라서 이렇게 포스팅을 하게 되었네요. Smoothing의 원리는 센서의 입력값을 보정해 주는 기능을 가지고 있어 제대로 이해하시면 활용 범위가 넓기 때문에 꼭 원리를 기억해 두셨다가 사용하시기 바랍니다. 이제 본격적으로 포스팅을 시작해 보도록 하겠습니다.

1. Smoothing의 원리


Smoothing은 말 그대로 부드럽게 하는 의미의 작업을 수행합니다 아두이노에서 Smoothing의 작업은 Sensor의 입력값을 부드럽게 받는 작업을 수행 합니다. 입력값을 부드럽게 받는다고 생각하면 의미 전달에 좀 그렇죠. 정확히 말하면 Sensor의 입력의 변화율을 줄인다고 생각하시면 됩니다. 오늘 사용하는 기법은 평균값을 이용했는데 이 평균값을 이용하여 부드럽게 Sensor의 값을 받게 됩니다. 하지만 이 평균값 구하는 로직이 딱 하나의 Smoothing 방법이라고 말 할 수 없으며 여러 방법중에 하나일 뿐입니다. 이거 딱 하나가 Smoothing이라고 생각하시면 안돼요.


평균은 대충 위 그림과 같은 형식으로 평균값을 구할 꺼에요.

1) Smoothing를 하는 이유

아두이노에서 Sensor를 통해 아날로그 신호로 읽을 때의 상황을 생각해 보세요. Sensor 주변 환경적 요인에 의해서 Sensor의 값은 달라 질 수 있습니다. Sensor 주변 환경에서 발생하는 노이즈 같은 이런 요인들이 Sensor 값에 영향을 주기 때문에 읽어들인 센서값의 변화가 일어나고 그로 인해 센서에 의한 동작 제어에도 영향을 미칠 수 밖에 없습니다.

예를 들어 Sensor에 9.9, 10.0, 10.1, 10.0, 10.2, 9.8 의 6개의 신호값이 들어온다고 칩시다. 여기서 우리가 Sensor의 값에 따라 동작을 제어를 할 때 10은 실제 Sensor의 값이고 나머지는 환경적 요인 노이즈에 의해 변형된 신호값이라면 그 변형된 값의한 동작을 아두이노가 하게 됩니다. 매 순간 측정 할 때 마다 정확하게 동일한 환경이 될 수 없습니다. 그래서 매 순간 Sensor의 환경요인에 의한 변화율이 발생합니다. 그부분을 줄이기 위해서 일정 측정한 수치의 평균값으로 아두이노에서 제어를 한다면 변화율이 최소화 될 수 있습니다. 그 변화의 폭을 부드러게 처리하기 위해 평균값이라는 원리를 이용하여 보정 작업을 수행을 합니다.

어떤 느낌이냐면 아래 그림이 좀 그런데 극단적으로 표현하자면 아래 그림과 같습니다.


검정선은 신호값들이 불규치적으로 매순간 들어온다고 생각하시고 빨간선은 그런 값들이 측정 신호를 묶음으로 해서 그 신호의 평균값으로 처리하면 부드럽게 곡선에 가까운 처리를 할 수 있게 되겠죠.

2) Smoothing의 평균값 구하기


float a[6] ={9.9, 10, 10.1, 10, 10.2, 9.8};
float total=0;
float avg=0;
for(int i=0;i<6;i++){

 total=tatal+a[i];

}
avg = toatal/6;

배열 변수에 저장된 6개의 값을 total 변수에 더한 값에서 6으로 나눈 값이 평균이 됩니다.

3) 센서로 Smoothing의 평균값 구하기

Sensor로 아날로그 신호 값은 0~1023 사이의 값이니깐 int형 변수로 선언하면 되고 아날로그 값을 읽는 analogRead()함수를 사용하면 됩니다. A0핀을 Sensor핀으로 사용한다고 하면 다음과 같이 코딩을 할 수 있겠죠.

int total = 0;
int avg = 0;

for(int i=0;i<6;i++){
 total=tatal+analogRead(A0);
}
avg = toatal/6;

for문으로 6번 analogRead(A0)의 아날로그 신호를 읽고 total에 저장합니다. 그리고 6번 반복이 끝나면 for문을 빠져 나와서 최종적으로 avg변수에 total값을 6으로 나눠서 평균값을 저장하게 됩니다.

평균값 구하는 방식을 알았으니깐 이제 회로도를 만들어서 실험을 해 봐야 겠지요.

2. 회로도 구성


  • 준비물 : 가변저항 1개, 아두이노우노
  • 내용 : 가변저항기로 Sensor 읽기를 대신 한다.


가변저항의 선 연결은 어려운 부분은 없네요.

2. 코딩



함수

  • analogRead(아날로그핀) : 아날로그 핀 값을 읽는다.
  • Serial.begin(9600) : 시리얼통신 시작
  • Serial.println(출력값) : 시리얼모니터로 출력

코딩은 공식 아두이노 홈페이지의 튜토리얼 소스를 기반으로 약간만 수정한 소스입니다. 가독성과 이해를 돕기 위해 수정이였을 참고하시고 보세요. 제가 만들어 낸 것이 아니라 기본 로직은 위에 링크된 참고소스의 출처에서 인용된 코딩입니다.

for(int i=0;i<6;i++){
 total=tatal+analogRead(A0);
}
avg = toatal/6;

이 수식을 이용할꺼에요. 표현은 좀 달라 보일 수 있겠네요.

아날로그 신호값을 읽기 전 처음 세팅을 먼저 해야겠지요.

const int sensorPin = A0; //측정 센서핀
const int readMax = 6;  //평균값을 구하기 위한 측정 횟수

int readings[readMax]; //측정값 저장 배열변수
int readIndex = 0; //reading[readIndex]로 측정된 값이 저장될 위치
int total = 0;  //측정한 값들의 전체합
int avg = 0; //측정한 값들의 평균값

void setup() {
  
  Serial.begin(9600);
  
  for (int i = 0; i < readMax; i++) {
    readings[i] = analogRead(sensorPin);  
        total = total + readings[i];
  }
}

출처 소스는 Sensor에서 측정된 값을 setup()함수에서 0으로 모두 초기화 작업을 수했습니다. 물론 그렇게 코딩하셔도 상관없습니다. 여기서 곰곰히 생각을 해보시면 이 상태로 loop()함수에 진입하게 되면 어떤 현상이 발생 할까요. total이 0인 상태에서 출발합니다. loop()함수가 6번 반복될 동안은 평균값을 만들어 낼 수 없습니다. 6번 반복이 된 후에야 평균값이 만들어지는 것이죠. 그 부분을 미리 setup()함수에서 total값을 만들어 낸다면 loop()함수가 시작되자마자 바로 Sensor의 측정된 평균값을 구할 수 있겠죠. 그렇게 되면 바로 아두이노는 그 평균값으로 동작 제어를 수행할 수 있겠죠.

그래서, 저는 수정하여 setup()함수에서 미리 loop()함수로 들어가기 전에 6번 Sensor의 값을 측정을 하여 total에 합산 한뒤에 평균값을 구할 수 있게 세팅 코딩을 하였습니다.

준비 세팅은 끝났으니 loop()함수로 진입해 볼까요.

측정 Sensor의 값은 readings[readMax]배열 변수에 저장되어 있습니다. 새로운 Sensor의 값을 읽기 위해서는 가장 오래된 Sensor의 값을 버려야 합니다. 가장 오래된 센서의 값은 readings[0]에 저장된 값이겠죠. 그래서 변수 선언할 때 readIndex값을 0으로 초기화 한 것이지요.


위 그림에서 보는 것처럼 첫번째 위치의 값을 버려야 합니다. 그러면 total에는 이전 6개의 측정 Sensor값이 합산되어 있으니간 뺄셈연산으로 reading[0]의 값을 빼주면 첫번째 위치의 값을 버린 효과가 되겠죠.

  total = total - readings[readIndex];  

이제 첫 번째 readings[readIndex]의 값을 버렸으니간 다시 이 곳에 새로운 Sensor값을 읽어와서 저장해야 겠죠.

    readings[readIndex] = analogRead(sensorPin);    

그리고 total은 다시 새로운 측정값을 더해야 평균값을 계산 할 수 있겠죠.

    total = total + readings[readIndex]; 

그러면 평균값을 구해볼까요.

  avg = total / readMax;

이렇게 해서 loop()함수는 'readIndex=0'인 위치에 새로 측정한 값을 배열공간의 저장하고 새로측정된 값과 이전 측정된 값을 합산한 값에 평균을 구할 수 있게 되었습니다.


대충 위 그림처럼 동작 했습니다. 처음 setup()함수로 6개의 측정 데이터를 읽고 각 측정 데이터를 읽은 순서대로 배열변수에 저장했습니다. 그리고 total 변수에 6개의 값이 합산되어 있습니다. 첫번째 값을 total 값에서 빼주면 나머지 5개의 데이터만 9, 11, 12, 10, 8 의 값만 합산되어 있게 됩니다. 첫번째 위치의 값을 새로운 11값이 채워질 경우 total은 새로 측정된 값을 더해주면 11, 9, 11, 12, 10, 8 의 값이 합산된 값이 되겟죠. 여기서 이 전체의 값을 6으로 나눠주면 현재 Sensor의 평균값이 나오게 됩니다.

이제 다음의 측정 할 Sensor 값은 어디에 저장되고 total값을 어느 값을 버리고 새로운 값을 더 할까요.

바로, 두번째인 'readIndex=1'인 위치에 reading[1]의 값을 빼주고 새로운 센서값을 저장해야 합니다. 가장 오래전에 측정한 값은 이제 reading[1]의 위치값이니깐요.

위치를 이동하기 위해서는 loop()함수에서 readIndex의 위치값에 대한 제어문이 필요 합니다. 다음 loop()문에서는 1씩 증가시켜서 reading[]배열의 위치를 오른쪽으로 시켜야겠죠.

readIndex++;

이렇게만 표현하면 문제점이 발생합니다. readIndex++로 무한 1씩 증가해버리게 됩니다. 배열변수 reading[6]은 총 6칸으로 이뤄졌습니다. 'readIndex=5'를 초과하면 안됩니다. 0~5까지만 증가해야 합니다. 그리고 마지막 5의 위치가 되면 다시 0으로 초기화 해서 0번째 위치를 가리키게 해야 합니다.

이 제어문을 만들려면 IF문을 활용하면 됩니다.

 if (readIndex >= readMax) {  
    readIndex = 0;
 }

위에 설명보다는 코딩이 아주 간단하죠. 읽을 최대 갯수 readMax(6)보다 '크거나 같을때' 0으로 초기화 하면 됩니다. 0~5까지 배열변수의 위치니깐 6이 되면 0으로 다시 초기화 하라는 의미인 것이죠.

코드를 종합해 보면,

const int sensorPin = A0; //측정 센서핀
const int readMax = 6;  //평균값을 구하기 위한 측정 횟수

int readings[readMax]; //측정값 저장 배열변수
int readIndex = 0; //reading[readIndex]로 측정된 값이 저장될 위치
int total = 0;  //측정한 값들의 전체합
int avg = 0; //측정한 값들의 평균값

void setup() {
  
  Serial.begin(9600);
  
  for (int i = 0; i < readMax; i++) {
    readings[i] = analogRead(sensorPin);  
        total = total + readings[i];
  }
}

void loop() {
  
  total = total - readings[readIndex];  
  readings[readIndex] = analogRead(inputPin);  
  total = total + readings[readIndex];  
  readIndex = readIndex + 1;
  
  if (readIndex >= readMax) {  
    readIndex = 0;
  }
  
  avg = total / readMax;
  
  Serial.println(avg);
  delay(1);       
}

3. 결과


가변저항기로 Sensor의 역활을 대신했습니다. 실제로 구현한다면 가변저항 부분에 Sensor를 부착하면 되겠죠.




4. 추가 내용


오늘 배운 Smoothing의 원리는 아래의 그림과 같은 방식이였습니다.

하지만 이런 방식만 있는게 아닙니다. 평균값을 어떤 기준으로 잡느냐에 따라 이렇게도 바뀔 수 있습니다.

아예 빠르게 6개의 측정을 수행하고 그 값의 평균값을 기준으로 아두이노에서 동작제어를 할 수도 있겠죠. 그런데, 이 경우는 문제점이 있습니다. 바로 평균값을 만들 측정 갯수가 늘어날 경우 측정 수 만큼 지연이 발생합니다. 짧은 측정은 상관없지만 많은 측정을 할 경우는 아두이노가 동작 제어를 할 때 느리게 반응을 하겠죠.

그래도 코딩은 어떻게 할까요. 아주 간단합니다 Setup()함수에서 했던 초기화 for문을 그대로 loop()로 옮겨오면 됩니다

void loop() {
  total=0;
    
  for (int i = 0; i < readMax; i++) {    
        total = total + analogRead(sensorPin);  
  }
  
  avg = total / readMax;
  
  Serial.println(avg);
  delay(1);       
}

코딩은 위와 같이 엄청 단순화 되었지만 별로 추천하는 코딩은 아닙니다. readMax 값이 커질 수록 측정하는 횟수가 늘어나면 그만큼 아두이노가 다른 동작제어를 할 때 대기시간이 늘어나기 때문에 추천되지 않는 코딩입니다. 그냥 이런식으로도 평균값을 구 할 수 있구나 정도로 생각하시면 되겠습니다. 이외에도 한번 어떤식으로 측정해볼까 하고 상상력을 발휘해 보세요. 제가 계속 열거하면 상상 표현이 줄어드니깐 여러분들이 직접 상상력을 발휘해 보세요.

마무리


오늘은 코딩은 별로 어렵지 않는데 설명이 좀 어렵게 되었네요. 괜히 혼동스럽게 되지 않았나 싶네요. 코딩한 로직만 보고 한번 한줄씩 어떤 동작을 하지니 해석을 하셨으면 합니다. 그러면 이해가 빠르게 되지 않을까 싶네요.

Sensor값을 단순히 이제까지 읽어 왔지만 Sensor를 어떻게 읽을 것인지에 대한 부분도 이제 배워야 합니다. 어떻게 읽느냐에 따라서 아두이노의 동작제어를 제대로 할 수 있겠죠.

마지막으로, Sensor의 값을 어떻게 읽어 들일지 상상의 나래를 펼쳐보세요.


댓글()