ま"’s blog

電子工作の部屋?

NiMHバッテリーパック充電器の製作


秋月電子ニッケル水素電池パック 3.6V830mAh HHR-P104(パナソニック製) が100円と安いので、これ用の充電器が欲しいと思っていました。というのも:

  • 容量としてはNiMH単四電池並みと小さいけれど、3本セットなので、バッテリーケースと込みで使用すれば、交換が楽になる。
  • 3.3V稼働可能なマイコンなら、このバッテリーで駆動できる。

といった思惑があった訳ですが、 ネットで調べた結果、0.1C(容量の1/10程度の電流)で15時間ほど充電すれば良いらしい。

…が、定電流電源の方が高いし、充電停止のメカニズムの無いところに繋ぎっぱなしは気持ち悪いし、15時間で取り外すのも手間。 バッテリーの起電圧上昇に伴って電流を絞って、繋ぎっぱなしにできる構成もなくはないけれど、バッテリー毎のばらつきや季節の気温変化に伴う条件変化に完全に対応することは不可能。

いろいろ調べた結果MAX713 という専用チップがあるらしい。 しかしこのチップATmega328Pより高い!(TT。 面倒なUIは必要ないし、パラメーターは固定なので、

「ATmegaの安いチップでロジックだけ組めばいいんじゃね?」

ということで、秋月で一番安いチップのATmega8-16PUで充電器を作ることにしました。
ロジックとしては:

  1. トリクル充電(0.2C位)モードで開始。 できればNiMHでない場合は充電を中止してエラー表示する。
  2. ある程度の時間充電して起電力が十分になったら、急速充電モードに移行。
  3. 1C位の電流をPWM制御で流す。
  4. LED点灯の間に起電力を測定し、ーΔVや異常状態などを検知し、充電をトリクル充電モードに移行する。 Timeoutでも急速充電を終了する。
  5. 急速充電終了後は放電を防ぐトリクル充電モード(1/40C位)で待機する。 LEDの点滅周期は1s位で、PWM制御のduty比で点滅させる。

という程度にします。回路図は以下のようになりました。

HHR-P104用充電回路

MAXの回路図を参考に、電圧を要所要所で測れるようにしました。 抵抗値とか細かい数字の部品を揃えるのは大変なので、 そこはPWM制御で擬似的に電流値を調整する事にしました。 電流制御用の抵抗が電池のマイナス側にあるのが変だなぁとは思ったけれど、 「どうせPWM制御で正確な電圧測れないし、電流止めて測ればいいんじゃね?」 とうことで、とてもMAX713の標準回路に似た回路になりました(^^;。
充電電流をON/OFFするトランジスタを制御するピンと、LEDのピンが違うのは、トランジスタのON/OFFが負論理というのと、PWMの周波数が高すぎて点滅が人間には識別できないので、duty比は同じで1秒間隔程度で点滅させるようにしたかったからです。

スケッチは以下のようになります。

#define CHG_PIN   9
#define CHG_FULL  230
#define CHG_START 50
#define CHG_CONT  6
#define PRE_CHG   60
#define MAX_SEC   7200
#define REG_VAL   1.0
#define NORMAL_V  3.6
#define ERROR_V   4.5
#define ERR_REG   2.0

// #define DEBUG

const int dutyVals[] = {
  CHG_START,  // Initial state=0
  CHG_FULL,   // Quick charge state=1
  CHG_CONT,   // After quick charge state=2
  0           // Error state=3
};
int chgState = 0;

void setup() {

  pinMode(PC6, INPUT_PULLUP);   // Pullup ~reset line if possible
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
  // analogReference(DEFAULT);  // may be not neccessary
  // Initialize charge control pin to stop charging after reset.
  pinMode(CHG_PIN, OUTPUT);
  digitalWrite(CHG_PIN, HIGH);

# ifdef  DEBUG
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);
# endif  // DEBUG
}

int   chgSec = 0;
float vBMax = 0.0;

void loop() {
  // put your main code here, to run repeatedly:
  float Vcc, va0, va1, vBatt, iR, rBatt;
  int   htime;

  Vcc  = cpuVcc();                   // 電源電圧測定

  digitalWrite(CHG_PIN, LOW);
  va0 = (Vcc * getADC(A0)) / 1024.0;
  va1 = (Vcc * getADC(A1)) / 1024.0;
  digitalWrite(CHG_PIN, HIGH);
  vBatt = (Vcc * getADC(A1)) / 1024.0;

  if (Vcc < ERROR_V || va0 == 0.0) {    // May be battery connected first or removed.
    chgSec = chgState = vBMax = 0;      // Shift to initial state
    trickleCharge(0);
    delay(1000);
    return;
  }
  
  if (vBatt > vBMax)  vBMax = vBatt;
  iR = va0 / REG_VAL;   // I = V/R
  rBatt = ((va1 - va0) - vBatt) / iR; // R = V/I

  chgSec++;
  htime = dutyVals[chgState] * 4;
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)

# ifdef  DEBUG
  Serial.print("ChgState= ");         // シリアルに状態遷移を出力
  Serial.print(chgState);
  Serial.print(", Vcc= ");            // シリアルにVccを出力
  Serial.print(Vcc, 2);
  Serial.print(", VA0= ");            // シリアルにVa0を出力
  Serial.print(va0, 2);
  Serial.print(", VA1= ");            // シリアルにVa1を出力
  Serial.print(va1, 2);
  Serial.print(", VBatt= ");          // シリアルにVbattを出力
  Serial.print(vBatt, 2);
  Serial.print(", RBatt= ");          // シリアルにRbattを出力
  Serial.println(rBatt, 2);
# endif  // DEBUG

  switch (chgState) {
    case 0:   // Initial state
      trickleCharge(dutyVals[chgState]);
      delay(htime);
      digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
      delay(1024 - htime);
      if (chgSec > PRE_CHG) {
        chgState = 1;   // Shift to quick charge mode
      }
      break;
    case 1:   // Quick charge mode
      trickleCharge(dutyVals[chgState]);
      delay(1000);
      digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
      // LED消さなくてもいい?
      if (vBatt < NORMAL_V || vBatt > ERROR_V || rBatt > ERR_REG) {   // May be wrong battery
        chgState = 3;
        err();
      } else if (chgSec > MAX_SEC || (vBMax - vBatt) > 0.2) {   // Timeout or detect -DeltaV
        chgState = 2;   // Shift to trickle charge mode after charge completed
      }
      break;
    case 2:   // After charge completed
      trickleCharge(dutyVals[chgState]);
      delay(htime);
      digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
      delay(1024 - htime);
      break;
    case 3:   // Error
      trickleCharge(dutyVals[chgState]); // 電流が0にならないかもしれない
      digitalWrite(CHG_PIN, HIGH);
      err();
      break;
  }
}

void trickleCharge(int duty)
{
  duty = 255 - duty;  // 充電制御は負論理
  duty = constrain(duty, 0, 255);
  analogWrite(CHG_PIN, duty);
}

unsigned int getADC(int pin) {
  long sum = 0;

  for (int n = 0; n < 10; n++) {
    sum = sum + analogRead(pin);               // adcの値を読んで積分
  }
  sum = sum / 10;       // 平均値

  return constrain(sum, 0, 1023);
}

float cpuVcc() {                     // 電源電圧(AVCC)測定関数
  long sum = 0;
  adcSetup(0x4E);                    // Vref=AVcc, input=internal1.1V
  for (int n = 0; n < 10; n++) {
    sum = sum + adc();               // adcの値を読んで積分
  }
  return (1.27 * 10240.0) / sum;      // 電圧を計算して戻り値にする(ちょっと補正。正しくは1.1だけど対象チップによって異なる)
}

void adcSetup(byte data) {           // ADコンバーターの設定
  ADMUX = data;                      // ADC Multiplexer Select Reg.
  ADCSRA |= ( 1 << ADEN);            // ADC イネーブル
  ADCSRA |= 0x07;                    // AD変換クロック CK/128
  delay(10);                         // 安定するまで待つ
}

unsigned int adc() {                 // ADCの値を読む
  unsigned int dL, dH;
  ADCSRA |= ( 1 << ADSC);            // AD変換開始
  while (ADCSRA & ( 1 << ADSC) ) {   // 変換完了待ち
  }
  dL = ADCL;                         // LSB側読み出し
  dH = ADCH;                         // MSB側
  return dL | (dH << 8);             // 10ビットに合成した値を返す
}

void err() {
  digitalWrite(CHG_PIN, HIGH);  // 念の為
  while (1) {
    // for (int i = 0; i < 4; i++) {    // Test code
    digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
    delay(100);                       // wait for 100m second
    digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
    delay(100);                       // wait for 100m second
  }
}

当時、マイブームだったFRISKケースに収めてみました。

FRISKケースに収めた充電器

充電のためのUSBポートがケースの内側に引っ込んでしまい、 ケーブルの抜き差しはほぼできないため、100円ショップで新たにケーブルを用意することになりました。ケースの裏側に専用バッテリーケースが両面テープで貼り付けられています。

’23.3/23追記:
回路図にクリスタルが無いのは、内蔵クロック動作モードで稼働させているためです。
ArduinoIDEでは、MiniCoreボードマネージャを使って、Internal 8MHzクロックのブートローダーを焼き込んで作成しました。