[아두이노] 리튬이온 배터리 내부저항 테스터기 만들기 #01
- 키르히호프의 전압법칙(KVL) + 옴의 법칙 응용 -



며칠 전 아두이노로 배터리의 내부저항을 측정할 수 있다는 걸 알게 되고 쾌재를 불렀다.
부품도 간단하고 누구나 쉽게 만들 수가 있을 것 같다.


중고 리튬이온 배터리를 많이 사용하는 데 배터리의 상태를 체크하여 사용가부를 결정하는 작업이 대단히 번거롭고 오래걸린다.
일단 만충후 발란싱 작업을 거쳐 한달 이상을 숙성시켜서 자연드롭이 없는 배터리를 골라낸다.
가장 최소한의 작업이다.

하지만, 그렇게 정상적인 배터리라고 판단을 했던 배터리라도 실제 부하를 물리면 문제가 되는 배터리가 종종 나온다.
이러한 번거롭고 지리한 작업을 빠르고 쉽게 해주는 것이 배터리 내부저항을 측정하는 일이다.

보통 배터리는 출고시 내부저항값이 스펙에 준하는 값으로 되어 있는데,,
사용하면서 다양한 이유로 이 값이 증가하고, 발열과 효율에 영향을 미친게 된다.




약간의 이론



이론적이라면 배터리의 전압은 순수하게 배터리가 저장하고 있는 에너지(V_OC)를 그대로 방출하지만,,
실제는 배터리 내부에 전류의 이동을 방해하는 저항치(RI)가 존재한다.

RI (Resistance Internal) 값구하는 것이 목적이다.



고맙게도 이 값은 독일의 천재적인 옴(Ohm)키르히호프(Kirchhoff) 형님들이 만들어준 공식 덕에 정말 간단하게 구할 수 있다.
두 공식을 이용하여 무 부하시 측정한 전압과 특정값의 부하(LOAD)를 건 상태에서 측정한 전압만 있으면 된다.

실제 참고한 사이트는 다음과 같다.

https://learn.sparkfun.com/tutorials/measuring-internal-resistance-of-batteries/internal-resistance



연습장에 끼적이면서 구하는 건  한 번이면 족하고 우리는 마이크로 컴퓨터의 힘을 빌어,
배터리만 홀더에 꽂으면 바로 내부 저항값을 보여주는 테스터기를 만드는 것이 좋지 않겠는가!?




회로도


배터리의 전압은 A0핀에서 측청하고,
부하(LOAD)를 연결을 스위칭할 수 있는 소자인 MOSFET를 추가하고,
D10번에서 컨트롤 했다.

이게 전부다.

+
배터리 전압이 5V가 이상일 경우 전압분배를 위해 추가 저항이 필요하겠지만,
18650의 경우 Full-Storage(만충)전압이 4.2v이므로 굳이 필요없다.


주의사항

이 회로는 역전압 차단하는 회로가 없어 배터리를 반대로 꽂으면 아두이노가 소손될 수 있다.
또 하나의 모스펫을 이용하여 역전압 방지하는 기능을 추가 중입니다.




업데이트

부저와 OLED 추가







부품

아두이노 프로 미니(Arduino Pro Mini)
10K 10Ω 시멘트 저항
HY1808P (N-Channel MOSFET)


모스펫이 어디 붙이 있나 찾다가 배터리팩 분리하고 모아둔 밸런스보드에서 발견해서 적출했다.
HY1808P (N-Channel MOSFET) 이라는 녀석이고,,
스펙은 다음과 같다.


데이타 시트를 참고하여 결선해주자..
N채널 모스펫으로 Gate핀에 전압을 인가하면 S -- D 라인에 전류가 흐른다.




브레드보드 테스트
- 브레드보드는 동작여부만 확인하는 용도다. 실제 값을 신뢰할 수 없다. -

동작확인과 문제점 인지...

브레드보드에 간단하게 회로를 꾸며서 테스트를 해보니 일단 공식에 의해 저항값이 도출되었다.
하지만 0.7~많게는 1Ω을 넘나드는 걸 보고 디버깅에 들어갔다.


첫 번째 문제는 산출된 외부전압(배터리)의 전압의 오차다.
전압을 구하는 아두이노 예제가 아두이노에 공급되는 전압(VCC)5V라고 가정하고,
실제 계산식에 상수값(5.0)을 그대로 사용하고 있어 나타난 문제였다.

VCC값을 구해서 상수를 치환해주면 비교적 배터리의 전압과 근사치를 얻을 수 있었다.


이것과 관련된 정보는 요기서 구할 수 있었다.
https://majenko.co.uk/blog/making-accurate-adc-readings-arduino


+
하지만 이것도 오차범위를 출이기 위한 트릭으로 결국 근사치다.
즉 배터리의 정확한 전압이 아니라 오차 범위가 줄어들었다는 얘기고,
정밀한 전압을 측정할 수 있는 추가 모듈(ADS1115)을 추가로 이용해야 하는 것 같다.
일단 알리에서 주문해논 상태고  테스트를 이어갈 생각이다.



전압을 실제 측정치와 비슷하게 맞추었는데도 계산된 내부저항값이 많이 튄다.
두 번째 문제다.
아두이노 경험이 오래지 않다보니,,,
브레드보드의 부정확함을 인지하지 못하고 있었다.



결국 브레드 보드말고 직접 연결해서 테스트를 진행했다.
모스펫은 납땜을 나머지는 악어클립을 연결했다.
그러자 측정치의 출렁임이 눈에 띄게 줄어들었고 비교적 안정적인 값을 구할 수 있었다.


회로의 라우팅 라인의 저항은 기본적으로 0옴이라는 가정하게 공식을 대입하게 되는데,,
실제 케이블 마다 저항치를 띄고 있으니 그게 문제다.

실제 만들 때 케이블 저항치를 감안해서 최대한 짧고 저항치가 낮은 순도 높은 케이블을 사용하되,,
그 때 케이블의 저항치가 일정하다면 +/- 보정을 통해 잡을 수 있을 것 같기는 하다.







inResistance값을 기준으로 배터리 스펙을 참고하거나 각자의 데이타를 쌓아 배터리의 양부를 빠르게 판단하는 데 사용하면 될 것 같다.
참고로 가장 많이 사용하는 LG화학의 중방전 18650 리튬이온 전지인 B4의 경우 70mΩ(0.07Ω)이 출고시 내부저항값이다.





큼지막한 LCD가 있다면 다양한 값을 표기할 수 있겠으나,,
일단 이녀석 밖에 없어서 배터리의 전압과 내부저항값만 출력하도록 코딩했다..



다음은 사용된 아두이노 소스다.

#define DELAY(ms) {int kk=0; while(kk<ms){delayMicroseconds(1000);kk++;}}
#define RESIST_THRESHOLD  1.0

void beepSound(int times)
{
  analogWrite(PIN_BUZZER, 90); /* for PASSIVE Buzzer */
  //digitalWrite(PIN_BUZZER, HIGH); /* for ACTIVE Buzzer */
  for (int ii=0; ii< times; ii++) {
    DELAY(ii*100+30);
  }
  digitalWrite(PIN_BUZZER, LOW);
}


/*
   https://www.arduino.cc/en/Reference.AnalogReference
*/
long readVcc() {
  long result;
  // Read 1.1V reference against AVcc
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA, ADSC));
  result = ADCL;
  result |= ADCH << 8;
  result = 1125300L / result; // Back-calculate AVcc in mV
  return result;
}

void checkBattery()
{
  unsigned int aData;
  double tmp_r_data;
  double tmp_v_data;
  int ii;

  /* 1. 무부하 상태에서 전압측정 */
  for (ii=0, aData=0, Vcc=0.0; ii<RETRY_NUM; ii++)
  {
    aData += analogRead(PIN_VOLTAGE);
    Vcc += (readVcc()/1000.0);
  }
  aData /= RETRY_NUM;
  Vcc /= RETRY_NUM;

 
  tmp_v_data = ((double)aData / 1023.0) *  Vcc;
  if ( noLoadVoltage == 0.0 )
  {
    noLoadVoltage_pre = noLoadVoltage = tmp_v_data;
  }
  else
  {
    /* 0.8V Variation 은 이상동작으로 간주 */
    if ( abs(tmp_v_data-noLoadVoltage_pre) < 0.8 )
    {
      noLoadVoltage_pre = noLoadVoltage;
      noLoadVoltage = tmp_v_data;
    }
    else
    {
      noLoadVoltage_pre = tmp_v_data;
    }
  }

  /* 2. 무부하시 전압이 1.0V 이상일 경우 배터리라고 판단, 부하 상태에서 전압측정 */
  if ( noLoadVoltage > 1.0)
  {
    /* MOSFET Gate ON */
    digitalWrite(PIN_LOAD_TRIGGER, HIGH);
    digitalWrite(PIN_LED, HIGH);

    for (ii=0, aData=0, Vcc=0.0; ii<RETRY_NUM; ii++)
    {
      Vcc += (readVcc()/1000.0);
      aData += analogRead(PIN_VOLTAGE);
    }
    Vcc /= RETRY_NUM;
    aData /= RETRY_NUM;

    loadVoltage = ((double)aData / 1023.0) * Vcc;
    inCurrent = loadVoltage / LOAD_RESISTOR_VAL;

    /* Kirchhoff’s Voltage Law (KVL) : Voc = Vi + Vl  */
    inVoltage = noLoadVoltage - loadVoltage;

    /*
     * skip abnormal status
     * 부하저항이 연결되기 전 같은 값인데 미세하게 전압이 떨어지는 경우
     */
    if ( inVoltage < 0 )
      return;

    /* Ohm's Law : V = IR */
    tmp_r_data = inVoltage / inCurrent;
   
    if ( inResistance == 0.0 )
    {
        inResistance = tmp_r_data;
        inResistance_Pre = inResistance;
    }
    else
    {
        /* 2옴 이상의 Variation 은 이상동작으로 간주 */
        if (abs(inResistance_Pre - tmp_r_data) < 2.0 )
        {
          inResistance_Pre = inResistance;
          inResistance = tmp_r_data;
        }
        else
        {
          inResistance_Pre = tmp_r_data;
        }
    }

    /* 2019-09-23, for audible noti */
    if ( inResistance > RESIST_THRESHOLD )
      beepSound(1);


    if ( DEBUG ) {
      Serial.println("---------------------------------------------------");
      Serial.print("Vcc           : "); Serial.println(Vcc);
      Serial.print("noLoadVoltage : "); Serial.println(noLoadVoltage);
      Serial.print("LoadVoltage   : "); Serial.println(loadVoltage);
      Serial.print("inResistance  : ");
      Serial.print("inVOL("); Serial.print(inVoltage); Serial.print(") / ");
      Serial.print("inCur("); Serial.print(inCurrent); Serial.print(") = ");
      Serial.print(inResistance); Serial.println(" Ω");
    }
  }
  else
  {
    noLoadVoltage = 0.0;
    inResistance = 0.0;
   
    if ( DEBUG )
      Serial.println("No Battery!!");
  }

  /* MOSFET Gate OFF */
  digitalWrite(PIN_LOAD_TRIGGER, LOW);
  digitalWrite(PIN_LED, LOW);
}


void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_LOAD_TRIGGER, OUTPUT);
  pinMode(PIN_BUZZER, OUTPUT);
 
  drawLogo();

  if ( DEBUG )
    Serial.begin(9600);
}

void loop() {
  drawInfo();
  checkBattery();
  delay(100);
}

LOAD_RESISTOR_VAL값을 부하저항의 저항값인데 10W 10Ω 시멘트 저항 실측값인 10.7을 사용했다.

사용된 아두이노 프로미니에서 RETRY_NUM을 90회 이상 키우면 아날로그 핀으로 읽는 값이 이상해지는 증상이 있다.
따라서 현재는 50회를 읽어서 평균을 내어 사용하도록 했는데 많이 읽을 수록 평가치에 근사한 값을 얻을 수 있으니,,
위 문제의 원인을 파악하고 패치할 생각이다.


뭐 실 테스트해보니 50회를 읽어 사용한 코드도 충분히 안정적인 값을 보여주었다.


이상의 작업이 들어간 스케치소스 파일은 다음과 같다.



업데이트 내역


Version : 20190826_01
1. OLED의 정보를 9 Pixel --> 10 Pixel
2. 배터기 없을 경우 Garbage값 오류로 인해 배터리 측정 전압을 0.8V --> 1.0V로 조정
3. 배터리 탈착시 튀는 값을 위해, 전압과 내부저항값의 Variation 을 계산하여 이상동작을 방지
4. 콘솔을 디버깅을 DEBUG를 Define처리함 (디버깅시에만 이 값을 1로 설정함, 성능문제)


Sketch Source

battery_checker_v20190826_01.ino




Version : 20190923_01


1. Buzzer 추가
 부저는 GND와 Digital PIN 5번을 사용했다.
Passive가 Default로 적용되어 있으며 Active Buzzer인경우
beepSound()내의 analogWrite()대신 digitalWrite()를 사용해야함

내부저항 값이 RESIST_THRESHOLD (Default 1.0) 이상일 경우 비프(beep)음을 발생


2. 부팅시 비프음 삽입


Sketch Source

battery_checker_v20190923_01.ino






배터리 내부저항기 소스는 이 버전을 마지막으로 클로징하고,
방전기능이 추가된 새로운 버전을 아래 글에서 이어서 관리합니다.



[DIY] 18650 리튬이온 배터리 전용 배터리 체커 만들기 - 내부저항 및 용량 테스터(방전기)기를 위한 아두이노 스케치 파일 공유








이 회로를 작은 하우징에 넣어서 테스터기를 완성해보도록 할 생각이다.
각 사이즈의 홀더를 사용할지 바나나잭을 이용하여 테스트 리드선을 사용할 지는 고민중이다.

리드선의 저항치에 따라 값이 변할 수 있어,,
최대한 케이블 저항이 낮고 또 전용으로 사용하도록 해야할 것이다.


+
같은 회로를 조금 수정하여 용량테스트(방전기)를 제작하고 있다.
현재 90% 완료되었으며 완료되면 다른 포스트로 업데이트 할 계획이다.

다만 다른 사람에 의해 주석이 제거되고 수정되어 출처없이 배포되는 사례가 있어,,
방전기 관련 소스를 공유할 지는 조금 고민스럽다.




완성편


배터리 양부판단 지그, 18650 배터리 아두이노 내부저항 테스터기 만들기 - 완성편



HI!! 궁금한 점은 댓글로 문의주세요~

  1. 구미 2019.09.06 00:48  address  modify / delete  reply

    안녕하세요
    혹시 1개 구매할수있을까요?

    • Favicon of https://mindeater.tistory.com BlogIcon MindEater™ 2019.09.06 08:49 신고  address  modify / delete

      필요에 의해서 자작해서 쓰고 있어 판매는 안하고 있습니다.
      소스와 방법을 공유했으니 아두이노 컴파일과 업로딩 정도만 배우셔서 자체제작하시는 것을 추천드립니다.

      다만, 만드는 품질에 따라 결과치 신뢰도 차이가 널뛰는 편이라 본문의 배터리 홀더(스프링타입이 아닌 단자타입) 사용 과 되도록이면 저항치가 낮은 배선을 짧게 사용하시고, 혹시 필요에 의해서 구입을 원하시면 가격이 있는 브랜드의 제품을 구하시는 것을 추천드립니다.

  2. 2019.09.23 16:59  address  modify / delete  reply

    비밀댓글입니다

  3. choi 2019.09.26 11:31  address  modify / delete  reply

    근데 oled 가 0.96인치 그래도 상관없습니가? 고수님!!!

    • Favicon of https://mindeater.tistory.com BlogIcon MindEater™ 2019.09.26 11:37 신고  address  modify / delete

      4핀이면 상관없습니다만,,
      디스플레이 배치를 다시 해주셔야합니다.
      아마 아래로 공간이 남을 듯 싶은데 추가 정보를 넣어도 될 것 같구요..

  4. 2019.09.29 23:34  address  modify / delete  reply

    비밀댓글입니다

    • Favicon of https://mindeater.tistory.com BlogIcon MindEater™ 2019.09.29 23:49 신고  address  modify / delete

      저도 아두이노를 이번에 접해서 다양한 보드를 사용해보지 못했습니다.

      다만 찾아보니 칩에 따라 VCC를 읽는 로직이 조금 차이가 나는 듯 합니다.

      long readVcc() {
      // Read 1.1V reference against AVcc
      // set the reference to Vcc and the measurement to the internal 1.1V reference
      #if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
      ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
      #elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
      ADMUX = _BV(MUX5) | _BV(MUX0);
      #elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
      ADMUX = _BV(MUX3) | _BV(MUX2);
      #else
      ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
      #endif

      delay(2); // Wait for Vref to settle
      ADCSRA |= _BV(ADSC); // Start conversion
      while (bit_is_set(ADCSRA,ADSC)); // measuring

      uint8_t low = ADCL; // must read ADCL first - it then locks ADCH
      uint8_t high = ADCH; // unlocks both

      long result = (high<<8) | low;

      result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
      return result; // Vcc in millivolts
      }

      구글링 해서 찾은 코드입니다.
      이 코드를 사용해보시길 바래요.

      부저는 패시브부저와 액티브 부저에 따라 코드를 조금 수정해주어야 합니다.
      beepSound()에서 코멘트 참고해서 수정해보세요.

      +
      참고로 배터리 체결시 역전압 주의를 요합니다.
      역전압 방지를 위해 모스펫 하나를 추가해서 테스트 중인데 아직 해결하지 못한 상태입니다.

  5. tjtjrqo 2019.09.30 07:42  address  modify / delete  reply

    감사합니다.
    테스트 해보도록 하겠습니다.

    배터리 역삽 문제 A0 입력단에 다이오드 달면 어떨까요...
    전압체크는 로드상태든 아니든 동일조건 입력이라 값 변동량은 동일하고 역삽에 대해 다이오드가 방지....
    여건이 된다면 제가....TT

    • Favicon of https://mindeater.tistory.com BlogIcon MindEater™ 2019.09.30 09:05 신고  address  modify / delete

      네..
      그렇잖아도 문턱전압이 낮은 쇼트키 다이오드 - 0.45V 정도 - 연결해서 테스트 해보았습니다.
      테스터기로 정확하게 재서 측정값에 적용했지만 다이오드 자체가 저항값이 커서 실측값이 틀어지더군요.
      그래서 모스펫을 적용했는데 시간이 없어 결과를 내지 못했습니다.
      감사합니다.

  6. art0315 2019.10.18 13:37  address  modify / delete  reply

    좋은내용 잘보고 갑니다.

    혹시 OLED 대신 CLCD를 사용할 경우

    소스 어느 부분을 고쳐야 하는지 좀 알수 있을 까요?

    • Favicon of https://mindeater.tistory.com BlogIcon MindEater™ 2019.10.18 14:16 신고  address  modify / delete

      16x2의 기준으로 된 샘플입니다.

      #include <Wire.h>
      #include <LiquidCrystal_I2C.h>

      LiquidCrystal_I2C lcd(0x3F,16,2); // i2c address 0x3F 또는 0x27

      void setup()
      {
      lcd.init(); // initialize the lcd
      // Print a message to the LCD.
      lcd.backlight();
      lcd.setCursor(1,0);
      lcd.print("Hello, World!");
      lcd.setCursor(2,1);
      lcd.print("Alictronix!");
      }

      void loop()
      {
      }
      ==============================================


      라이브러리,
      draw() 함수 바디를 모두 lcd에 맞게 수정해주어야 합니다.
      저도 아직 써본적이 없네요. ^^;;