NiMHバッテリーパック充電器の製作
秋月電子のニッケル水素電池パック 3.6V830mAh HHR-P104(パナソニック製) が100円と安いので、これ用の充電器が欲しいと思っていました。というのも:
- 容量としてはNiMH単四電池並みと小さいけれど、3本セットなので、バッテリーケースと込みで使用すれば、交換が楽になる。
- 3.3V稼働可能なマイコンなら、このバッテリーで駆動できる。
といった思惑があった訳ですが、 ネットで調べた結果、0.1C(容量の1/10程度の電流)で15時間ほど充電すれば良いらしい。
…が、定電流電源の方が高いし、充電停止のメカニズムの無いところに繋ぎっぱなしは気持ち悪いし、15時間で取り外すのも手間。 バッテリーの起電圧上昇に伴って電流を絞って、繋ぎっぱなしにできる構成もなくはないけれど、バッテリー毎のばらつきや季節の気温変化に伴う条件変化に完全に対応することは不可能。
いろいろ調べた結果MAX713 という専用チップがあるらしい。 しかしこのチップATmega328Pより高い!(TT。 面倒なUIは必要ないし、パラメーターは固定なので、
「ATmegaの安いチップでロジックだけ組めばいいんじゃね?」
ということで、秋月で一番安いチップのATmega8-16PUで充電器を作ることにしました。
ロジックとしては:
- トリクル充電(0.2C位)モードで開始。 できればNiMHでない場合は充電を中止してエラー表示する。
- ある程度の時間充電して起電力が十分になったら、急速充電モードに移行。
- 1C位の電流をPWM制御で流す。
- LED点灯の間に起電力を測定し、ーΔVや異常状態などを検知し、充電をトリクル充電モードに移行する。 Timeoutでも急速充電を終了する。
- 急速充電終了後は放電を防ぐトリクル充電モード(1/40C位)で待機する。 LEDの点滅周期は1s位で、PWM制御のduty比で点滅させる。
という程度にします。回路図は以下のようになりました。
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ケースに収めてみました。
充電のためのUSBポートがケースの内側に引っ込んでしまい、 ケーブルの抜き差しはほぼできないため、100円ショップで新たにケーブルを用意することになりました。ケースの裏側に専用バッテリーケースが両面テープで貼り付けられています。
’23.3/23追記:
回路図にクリスタルが無いのは、内蔵クロック動作モードで稼働させているためです。
ArduinoIDEでは、MiniCoreボードマネージャを使って、Internal 8MHzクロックのブートローダーを焼き込んで作成しました。