2013年10月18日金曜日

Ni-MH充電器の製作(6)


再び体調の思わしくない日々が続き(言い訳です…)、更新が遅れまして申訳ありません。
ようやく、プログラムの本検討にとりかかることができましたので、以下の通り報告します。


6 プログラム設計検討

6.1 基本仕様

6.1.1 内部ステータス

内部ステータスを以下の通り定義します。

・「設定中」(充電中止)
⇒ 充電が行われず、設定の変更ができる状態。

・「充電中」
⇒ 設定した内容に従って、充電を実行している状態。

6.1.2 ステータス遷移

上記で決めたステータスと、関連するトリガイベントの遷移を、以下の通り整理します。

トリガー
STARTボタン押下
充電完了
電池取り外し
ステータス
「設定中」「充電中」遷移
「充電中」「設定中」遷移「設定中」遷移「設定中」遷移


6.1.3 ユーザーI/O

各キー・各LED毎に、以下の通り整理します。
キー・LED共、処理方法の組み合わせで複数の状態を定義して、数を減らすこともできますが、処理が複雑になるのと、ポートに余裕があるため、これまでの回路図通り、単純に機能の数だけスイッチとLEDを設けました。

I/O要素
電流設定キー
電圧設定キー
STARTキー
電流設定LED
電圧設定LED
充電中LED
ステータス
「設定中」設定値をサイクリックに変更※1設定値をサイクリックに変更※2「充電中」遷移※3設定値の変更に従い、該当LED点灯設定値の変更に従い、該当LED点灯消灯
「充電中」キー無効キー無効「設定中」遷移※3該当LED点灯維持該当LED点灯維持一定間隔で点滅

※1 押下に従って、設定値を 75mA(AAA) ⇒ 130mA(AA小容量) ⇒ 200mA(AA標準) ⇒ 240mA(AA pro) ⇒ … と、サイクリックに変更します。
※2 押下に従って、設定値を 1.3V(1本) ⇒ 2.6V(2本) ⇒ 5.2V(4本) ⇒ … と、サイクリックに変更します。
※3 6.1.2項の通り


6.2 個別機能

6.2.1 キー入力処理

6.1.3項の通り、今回は、キーの押下(OFF ⇒ ON)のみ処理します(開放・押し続けは無視)。
キーは機械接点のため、必ずチャタリング(接点あばれ)が発生して誤検出の原因となりますので、一定間隔の割り込みを利用して、スイッチ入力のサンプリングを行い、チャタリング除去を行います。
割り込みは、TIMER0 CTCモードを使って、10msのインターバルでサンプリングします。
キーのOFF ⇒ ON を検出するために、直前の状態を都度記憶しておき、比較します。


6.2.2 電流・電圧検出

回路図のうち、電流・電圧検出部を抽出した物を以下に示し、当該図を参照しながら検討を進めます(計算が長くなりますが、ご容赦願います)。















充電池両端の電圧 Va・Vb は、

Va = Va' * (1 + R15/R16) … ①
Vb = Vb' … ②

ADCの出力データ Da・Db と電圧の関係は、ADCが10ビット(=1024)であることから、

Da = (1024/Vref) * Va' … ③
Db = (1024/Vref) * Vb' … ④

以降、(1024/Vref) を T 、(1 + R15/R16) を R とおき、上記式を変形して、

②④より、
Vb = (1/T) * Db … ⑤

①③より、
Va = Va' * R * Da = (R/T) * Da … ⑥

検出電流 Ibatt は、⑤より、

Vb = Ibatt * R17 = (1/T) * Db … ⑦

検出電圧 Vbatt は、⑤⑥より、

Vbatt = Va - Vb = (R/T) * Da - (1/T) * Db … ⑧

ここで、プログラム中において、浮動小数点演算をなるべく排除することを考えます。
すなわち、検出電流および電圧を、早いうちにADCデータと直接比較できる10ビットデータに変換しておきます。

上記実現のために、ADCデータを左辺、定数項を右辺にまとめると、

⑦より、
Db = T * R17 * Ibatt

⑧より、
R * Da - Db = T * Vbatt

Ibattは、例えば75mAの場合、

(1024 / 5V) * 4.7 *75mA = 72(小数点四捨五入)

をあらかじめ計算しておいて、Da と比較すればよいことになります。
Vbattのほうに、一部小数演算が残りましたが、他は、10ビットデータに揃った状態で取り扱うことができました。


6.2.3 電流・電圧制御

これまでの試作通り、TIMER1のPWM機能を使って、DC-DCコンバータのスイッチング制御を行います。
制御方法は、今回、充電という機能に特化しますので、汎用電源に要求されるような厳密な過渡特性までは検討しませんが(本当はよくわかっていません…)、ゲインだけは適切でないと、安定負荷でも出力が不安定となるので、以下の通り検討しておきます。

最低限のゲイン制御要件として、ADC及びPWMの分解能の観点で検討を進めます。

ADC 1ビットの分解能 Sadc は、1 / T なので、
Sadc = 1 / T = 5V/1024 = 4.9mV

一方、PWMの分解能 Spwm は、8ビット幅で、入力電圧が8.4V、分圧比が 1 / R であることより、
Spwm = 8.4V / R / 256 = 10.5mV

このままでは、4.9mVの変動に対して、10.5mVの補正を行うということになり、制御ゲインが過大となってしまいます。
よって、ゲインを下げるための何らかの方策を考えます。
最も適切なのは、PWMの分解能を上げることです。今回の構成では、もう1ビット分解能を上げることはできます(9ビット=512)が、外部クロックにしたり、回路を変更する必要があるので、これは最後の手段にします。
今回は、簡単に、プログラムにおいて変動幅を許容する方法で対処したいと思います。


6.2.4 充電状態判定

充電完了と、電池を故意に外してしまった場合の状態を判定します。

充電完了は、検出した電圧が、Ni-MH電池の満充電電圧(今回は、1.3V)になったら、PWMをオフして、「設定中」ステータスに戻ります。

電池が外れた状態は、充電電流がゼロになる状態とする方法がありますが、充電開始直後、充電電流が流れ始めない状態と区別できない恐れがあります。
他に、充電を維持しようとして、電圧が上昇してしまった状態を判定する方法がありますが、これは満充電と同一事象のため、結局、充電完了検出で両方を兼ねることができます。

6.3 試作版プログラム

全機能を実装した、試作版ソースリストを以下に示します。


//////////////////////////////////////////////////////////////////////////////
//
// Ni-MH Battery Charger "experiment" version
//
// Programmed by (c)ota957
//
// MPU =  ATmega168P
// lfuse = 0xe2(CKDIV8 disable)
// hfuse = 0xdf(default)
// efuse = 0xf9(default)
//
//////////////////////////////////////////////////////////////////////////////
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define _SBI(p, q) ((p) |= _BV(q))
#define _CBI(p, q) ((p) &= ~_BV(q))

//////////////////////////////////////////////////////////////////////////////
// グローバル変数及び各定義
//////////////////////////////////////////////////////////////////////////////
volatile uint8_t uSW_FUNC_A;
volatile uint8_t uSW_FUNC_B;
volatile uint8_t uSW_FUNC_C;
volatile uint8_t uREQUEST;
volatile uint8_t uSTATUS;
volatile uint8_t uTIMERBLINK;

#define SW_LOW 0
#define SW_HIGH 1
#define SW_ON 2
#define SW_OFF 3

#define REQUEST_IDLE 0
#define REQUEST_FUNC_A 1
#define REQUEST_FUNC_B 2
#define REQUEST_FUNC_C 3

#define STATUS_IDLE 0
#define STATUS_SETTING 1
#define STATUS_CHARGING 2

/*
#define V_REF 5.0

#define R_17 4.7
*/
#define R_15 1000
#define R_16 470

const uint16_t arCURRENT[] = {
    72,     // 1024 / Vref * R17 *  75mA
    125,    // 1024 / Vref * R17 * 130mA
    193,    // 1024 / Vref * R17 * 200mA
    231     // 1024 / Vref * R17 * 240mA
};

const uint16_t arVOLTAGE[] = {
    266,    // 1024 / Vref * 1.3V * 1
    532,    // 1024 / Vref * 1.3V * 2
    1065    // 1024 / Vref * 1.3V * 4
};

//const double arVOLTAGE[] = {(1.3 * 1), (1.3 * 2), (1.3 * 4)};
const uint8_t uINDEX[] = {1, 2, 4, 8};

volatile uint8_t uISET;
volatile uint8_t uVSET;

 
//////////////////////////////////////////////////////////////////////////////
// 各割り込み処理
//////////////////////////////////////////////////////////////////////////////
// ---------------------------------------------------------------------------
// タイマー0 比較一致割り込み
// ---------------------------------------------------------------------------
ISR(TIMER0_COMPA_vect)
{
    // swich sampling
    static uint8_t uPrevFuncA = SW_OFF;
    static uint8_t uPrevFuncB = SW_OFF;
    static uint8_t uPrevFuncC = SW_OFF;

   uint8_t uSwitchFuncA = bit_is_clear(PIND, PIND6) ? SW_ON : SW_OFF;
    uint8_t uSwitchFuncB = bit_is_clear(PIND, PIND7) ? SW_ON : SW_OFF;
    uint8_t uSwitchFuncC = bit_is_clear(PINB, PINB0) ? SW_ON : SW_OFF;

     // set status
    if(uPrevFuncA == SW_OFF && uSwitchFuncA == SW_ON)
        uREQUEST = REQUEST_FUNC_A;
    if(uPrevFuncB == SW_OFF && uSwitchFuncB == SW_ON)
        uREQUEST = REQUEST_FUNC_B;
    if(uPrevFuncC == SW_OFF && uSwitchFuncC == SW_ON)
        uREQUEST = REQUEST_FUNC_C;

     // memory present switch
    uPrevFuncA = uSwitchFuncA;
    uPrevFuncB = uSwitchFuncB;
    uPrevFuncC = uSwitchFuncC;

     // 10ms timer countup
    uTIMERBLINK++;
}
/*
ISR(TIMER1_OVF_vect)
{

 }
*/
//////////////////////////////////////////////////////////////////////////////
// 各関数
//////////////////////////////////////////////////////////////////////////////
// ---------------------------------------------------------------------------
// LED on/off
// ---------------------------------------------------------------------------
void DispBarLED8(uint8_t uData)
{
    (uData & 0b00000001) ? _SBI(PORTC, PC2) : _CBI(PORTC, PC2);
    (uData & 0b00000010) ? _SBI(PORTC, PC3) : _CBI(PORTC, PC3);
    (uData & 0b00000100) ? _SBI(PORTC, PC4) : _CBI(PORTC, PC4);
    (uData & 0b00001000) ? _SBI(PORTC, PC5) : _CBI(PORTC, PC5);
    (uData & 0b00010000) ? _SBI(PORTD, PD5) : _CBI(PORTD, PD5);
    (uData & 0b00100000) ? _SBI(PORTD, PD4) : _CBI(PORTD, PD4);
    (uData & 0b01000000) ? _SBI(PORTD, PD3) : _CBI(PORTD, PD3);
    (uData & 0b10000000) ? _SBI(PORTD, PD2) : _CBI(PORTD, PD2);
}

 void DispLED_A(uint8_t uData)
{
    (uData & 0b00000001) ? _SBI(PORTC, PC5) : _CBI(PORTC, PC5);
    (uData & 0b00000010) ? _SBI(PORTC, PC4) : _CBI(PORTC, PC4);
    (uData & 0b00000100) ? _SBI(PORTC, PC3) : _CBI(PORTC, PC3);
    (uData & 0b00001000) ? _SBI(PORTC, PC2) : _CBI(PORTC, PC2);
}

 void DispLED_B(uint8_t uData)
{
    (uData & 0b00000001) ? _SBI(PORTD, PD2) : _CBI(PORTD, PD2);
    (uData & 0b00000010) ? _SBI(PORTD, PD3) : _CBI(PORTD, PD3);
    (uData & 0b00000100) ? _SBI(PORTD, PD4) : _CBI(PORTD, PD4);
    (uData & 0b00001000) ? _SBI(PORTD, PD5) : _CBI(PORTD, PD5);
}

// ---------------------------------------------------------------------------
// ADC 入力選択
// ---------------------------------------------------------------------------
void ADC_Select(uint8_t uChannel)
{
    uint8_t uTemp = ADMUX;

     ADMUX = (uTemp & 0xf0) | (uChannel & 0x0f);
//  DispBarLED8(ADMUX);
}

// ---------------------------------------------------------------------------
// ADC 変換スタート
// ---------------------------------------------------------------------------
uint16_t ADC_Start(void)
{
    _SBI(ADCSRA, ADIF);
    _SBI(ADCSRA, ADSC);

     loop_until_bit_is_set(ADCSRA, ADIF);

     return ADCW;
}

// ---------------------------------------------------------------------------
// ADC 変換値取得ラッパー
// ---------------------------------------------------------------------------
uint16_t ADC_GetData(uint8_t uChannel)
{
    ADC_Select(uChannel);
    _delay_us(10);

     return ADC_Start();
}

 
//////////////////////////////////////////////////////////////////////////////
// メインループ
//////////////////////////////////////////////////////////////////////////////
int main(void)
{
    // I/O Setting
    _SBI(DDRC, DDC2);
    _SBI(DDRC, DDC3);
    _SBI(DDRC, DDC4);
    _SBI(DDRC, DDC5);
    _SBI(DDRD, DDD2);
    _SBI(DDRD, DDD3);
    _SBI(DDRD, DDD4);
    _SBI(DDRD, DDD5);

    _CBI(DDRD, DDD6);
    _SBI(PORTD, PD6);
    _CBI(DDRD, DDD7);
    _SBI(PORTD, PD7);
    _CBI(DDRB, DDB0);
    _SBI(PORTB, PB0);

     // Timer0 Setting
    _SBI(TCCR0A, WGM01);    // TIMER0 CTCMode
    _SBI(TCCR0B, CS02);     // TIMER0 Prescaler = 1/1024
    _SBI(TCCR0B, CS00);     // ↑
    OCR0A = 79;             // TIMER0 CTC Top(1024 * 79 / 8MHz = 10ms)
    _SBI(TIMSK0, OCIE0A);   // TIMER0 CTC Interrut Enable

     // Timer1 Setting
    _SBI(DDRB, DDB1);       // OC1A(PB1) output
//  _SBI(TCCR1A, COM1A1);   // OC1A Non-Invert on
    _SBI(TCCR1A, WGM10);    // 8Bit Highspeed PWM
    _SBI(TCCR1B, WGM12);    // ↑
    _SBI(TCCR1B, CS10);     // TIMER1 Prescaler = 1/1
//  _SBI(TIMSK1, TOIE1);    // Overflow Interrupt Enable
//  OCR1A = 0;

     // ADC Setting
    _SBI(ADMUX, REFS0);     // Vref = AVCC(5V)
    _SBI(ADCSRA, ADPS2);    // ck = 1/64
    _SBI(ADCSRA, ADPS1);    // ↑
    _SBI(ADCSRA, ADEN);     // ADC Enable

 
     uint16_t uDataI = 0x00;
    uint16_t uDataV = 0x00;

     uint16_t uIref = arCURRENT[0];
    uint16_t uVref = arVOLTAGE[0];

     uSTATUS = STATUS_SETTING;

     sei();

     _delay_ms(10);

     while(1){

         // --------- Setting Status --------

         if(uSTATUS == STATUS_SETTING){

             if(uREQUEST == REQUEST_FUNC_A){

                uISET = uISET < 3 ? uISET + 1 : 0;
                uIref = arCURRENT[uISET];
                uREQUEST = REQUEST_IDLE;

             }else if(uREQUEST == REQUEST_FUNC_B){

                uVSET = uVSET < 2 ? uVSET + 1 : 0;
                uVref = arVOLTAGE[uVSET];
                uREQUEST = REQUEST_IDLE;

             }else if(uREQUEST == REQUEST_FUNC_C){

                uSTATUS = STATUS_CHARGING;
                OCR1A = 0;              // PWM width = min
                _SBI(TCCR1A, COM1A1);   // OC1A Non-Invert on
//              _SBI(TIMSK1, TOIE1);    // Overflow Interrupt Enable
                uTIMERBLINK = 0;
                uREQUEST = REQUEST_IDLE;

         }

             // display status
            DispLED_A(uINDEX[uISET]);
            DispLED_B(uINDEX[uVSET]);

         // --------- Charge Running Status ---------

         }else if(uSTATUS == STATUS_CHARGING){

             if(uREQUEST == REQUEST_FUNC_C){

            uSTATUS = STATUS_SETTING;
                OCR1A = 0;              // PWM width = min
                _CBI(TCCR1A, COM1A1);   // OC1A Non-Invert off
                uREQUEST = REQUEST_IDLE;

             }else{

                 uDataI = ADC_GetData(1); // get current(ADC1)

                 //DispBarLED8(uDataI);  // for debug

                 if(uDataI + 2 < uIref)
                    OCR1A = OCR1A < 0xff ? OCR1A + 1 : 0xff;
                else if(uIc - 2 > uIref)
                    OCR1A = OCR1A > 0x00 ? OCR1A - 1 : 0x00;

                 uDataV = ADC_GetData(0); // get voltage(ADC0)

                 uint16_t uDataB = (uint16_t)(1 + R_15 / R_16) * uDataV - UdataI;

                 if(uDataB > uVref){         // Charge complete or Battery rejected

                     uSTATUS = STATUS_SETTING;
                    OCR1A = 0;              // PWM width = min
                    _CBI(TCCR1A, COM1A1);   // OC1A Non-Invert off
                    uREQUEST = REQUEST_IDLE;

                 }
            }

             // display status
            DispLED_A(uINDEX[uISET]);
            uTIMERBLINK & 0x40 ? DispLED_B(uINDEX[uVSET]) : DispLED_B(uINDEX[uVSET] | 0x08);

         // --------- Other Status (no defined) ---------

         }else{
            ;
        }

         //_delay_ms(10);
    }
}

//////////////////////////////////////////////////////////////////////////////
// End
//////////////////////////////////////////////////////////////////////////////

※本ソースでは、まだ充分なテストを行っておりません。ご了承願います。

次回の予定

デバッグ及びテストを行い、できれば、マイコン部の基板実装まで進みたいと思います。