PICでUSB電圧・電流モニター

USBの電圧・電流モニターは1000円以下で買えますが、PICで自作してみました。

通常はLDOレギュレーターで5Vから3.3Vを作りPICを動作させるのですが、余っていたPIC16F88を使用したため4V以下では動作保証されません。このため可変電圧LDOレギュレーターで4Vを作っています。今回はLEDを点灯させているので、3.3Vよりは4Vの方が使いやすいです。
ZXCT1021はハイサイド電流検出専用ICで、電流センス抵抗両端電圧差の10倍の電圧が出力され(0〜1.5V)また電源電圧の絶対最大定格は20Vです。今回は20mΩの電流検出抵抗器を使ったため、これらの制約により測定範囲は4.5〜20V、0〜7.5Aとなり、USB Quick Charge2.0/3.0のClass A(最大12V)が測定可能です。USB以外のコネクターを使えば他のDC電圧・電流簡易測定にも使えると思います。表示更新周期が1秒なので、A/D入力にはノイズ取り用に大きな抵抗とコンデンサーを入れています。

やっている事が電圧を2つADコンバーターで取り込んでLEDに出力しているだけで、あまり面白くないためフォトカプラーを追加して絶縁型シリアルデーター出力を追加しました。フォーマットは9600/8/1/Nで、 000:00:10,08.93,0.413 この形式で時分秒、電圧、電流がコンマ区切りで出力されます。この出力はシリアル⇔USB変換LSI Prolific PL-2303HXに接続してUSB経由でPCへ接続しているため正論理の出力にしていますが、フォトカプラーの出力で反転させれば直接PCのRS-232Cへ接続しても短いケーブル長なら読み取れると思います。この回路の消費電力は実測15mA、LEDを消灯した場合は4mAでした。アナログ入力ポートがまだ余っているので、モバイルバッテリーを接続して絶縁型簡易電圧測定にも使えるかもしれません。

フォアグラウンドプログラム main() はI/Oや変数の初期化を行った後Timer1割り込みを許可し、グローバル変数に値がセットされるまで待ち続けます。
2.36msごとのTimer1割り込みでバックグラウンドの割り込み処理プログラムが走り、まず6個の7セグメントLEDのうち1つに順番にデーターを表示させます。次にシリアルポート出力が未完了の場合は出力します。その後電圧・電流をローカル変数に加算します。電圧・電流サンプリングが#define SAMPLINGで規定された回数になったら電圧・電流の加算値をグローバル変数に格納してローカル変数をクリアします。
フォアグラウンドプログラムは、グローバル変数に値がセットされたら表示桁を合わせてからBCD変換してLED表示バッファーに格納し、シリアルポート出力文字列をセットし、処理終了後にグローバル変数をクリアして再度セットされるまで待ちます。これを1秒周期で繰り返す事になります。

調整点は2つで、まず抵抗値のばらつきにより測定値に若干の誤差が出るため、テスターで測定したUSB電源電圧とLED表示が「ほぼ」合うように#define SAMPLINGの値を増減させます。この回路で抵抗の誤差が全く無い場合はVDD=4.064Vで電圧の分圧比が10/101=0.099なので4.064÷0.099✕10(表示する小数点の位置を調整)でSAMPLING=410が理論値になるのですが、今回の製作例では424になりました。次にこのサンプリング数の取り込みが終了し、出力処理が終了した時にちょうど1秒経過するようにタイマーT1の周期を#define TMR1H_VAL/TMR1L_VALの値で設定します。今回はSAMPLINGが424回+結果の出力1回なので割り込み周期は1÷(424+1)=2.353msになります。これで1秒おきに電圧・電流値がシリアルポートへ出力されます。

#include 
#include 
#include 

//__CONFIG( UNPROTECT & BOREN & OSC_8MHZ & MCLRDIS & PWRTEN & WDTDIS & INTIO );
#pragma config FCMEN=OFF
#pragma	config IESO=OFF
#pragma	config CPD=OFF
#pragma	config CP=OFF
#pragma	config BOREN=OFF
#pragma	config MCLRE=OFF
#pragma	config PWRTE=OFF
#pragma config LVP=OFF  //
#pragma config WDTE=OFF
#pragma config FOSC=INTOSCIO

#define _XTAL_FREQ 8000000
#define TMR1H_VAL 0xf6      // 65536-2353=0xf6cf
#define TMR1L_VAL 0xcf      // 2353 * 1us = 2.353ms period
#define ADC_V 2             // Vsense ADC ch.
#define ADC_C 3             // Csense ADC ch.
#define SAMPLING 424;       // no. of sampling

unsigned char h, m, s;
unsigned char disp[6];      // display line buffer
unsigned char ser_out[] = "\n\r000.0,00.00,00:00:000\0";  // serial out buffer, reverse order
unsigned char ser_ctr;
unsigned int r_voltage;
unsigned int r_current;

void init(void)
{
    OSCCON = 0x70;      // INTOSC 8MHz
    ANSEL = 0x0c;       // RA3:Current(20A@full scale), RA2:Voltage(40V@full scale)
    TRISA = 0x6d;       // RA7:LED_data bit2 RA4:dot pos @Voltage 1:left dig. 0:mid dig. RA1:debug
    TRISB = 0x00;       // RB7,6,4:LED_select bit2,1,0 RB3,1,0:LED_data RB5:TX
    PORTA = 0x00;
    PORTB = 0x00;
    OPTION_REG = 0xff;  // PORT B weak pull up disable

                            // adc setup
    ADCON0 = 0b00100001;    // clock=fosc/16, a/d enable
    ADCON1 = 0b01000000;    // left justified, Vdd/Vss ref

                            // timer1 setup
    T1CON = 0b00010100;     // T1CKPS:1/2 T1OSCEN:F *T1SYNC:F TMR1CS:INT TMR1ON:F
                            // timer1 clock freq. is 8MHz/4/2=1MHz
    PEIE = 1;               // all peripheral interrupts are enabled
    GIE = 1;                // global interrupt enabled
    TMR1H = TMR1H_VAL;      // set timer1 interrupt period
    TMR1L = TMR1L_VAL;
    TMR1IE = 1;             // timer1 interrupt enable
    
                            //serial port setup
    TXSTA = 0b00100000;     // tx9:0, txen:1, sync:0, brgh:0
    RCSTA = 0b10000000;     // spen:1, rx9:0, cren:0
    SPBRG = 12;              // 9600bps

    h = m = s = 0;
    ser_ctr = 0;
    r_voltage = 0;
    r_current = 0;
    TMR1ON = 1;             // timer1 start
 
}

// read adc
unsigned int adc_read( unsigned char ch )
{
    unsigned int value;	// adc value
    unsigned char i;
    
    RA1 = 1;
    ADCON0 = (ADCON0&0xc7)|(ch<<3);
    __delay_us(16);         // Tacq
    
    for(i=0, value=0; i<32; i++){
        GO_nDONE = 1;       // ADC start
        while( GO_nDONE )
            continue;
        value += ADRESH;
    }
    RA1 = 0;
    return value>>5;
}


interrupt void timer1_overflow(void)
{
    static unsigned int voltage = 0;
    static unsigned int current = 0;
    static unsigned char disp_ctr = 0;
    static unsigned int meas_ctr = SAMPLING;      // mesurement counter

    TMR1ON = 0;         // stop timer1
    TMR1IF = 0;         // clear timer1 interrupt flag
    TMR1H = TMR1H_VAL;  // reset timer1
    TMR1L = TMR1L_VAL;
    TMR1ON = 1;         // start timer1

    PORTB = disp[disp_ctr];             // LED display
    RA7 = (disp[disp_ctr++]&0x04)?1:0;
    if(disp_ctr == 6)
        disp_ctr = 0;

    if(ser_ctr){        // serial port output
        ser_ctr--;
        TXREG = ser_out[ser_ctr];
    }
    
    if(meas_ctr){
        if(meas_ctr&0x01){
            voltage += adc_read(ADC_V);
        }else{
            current += adc_read(ADC_C);
        }
        meas_ctr--;
    }else{          // measurement complete, copy data
        if(s!=59){  // timestamp
            s++;
        }else{
            s = 0;
            if(m!=59){
                m++;
            }else{
                m=0;
                h++;
            }
        }
        r_voltage = voltage;
        r_current = current;
        voltage = 0;
        current = 0;
        meas_ctr = SAMPLING;
    }
    return;
}

void bin2bcd(unsigned int val, unsigned char *b){
    union{
        unsigned long int bd32;
        struct{
            unsigned bd8l : 8;
            unsigned bd8h : 8;
            unsigned bcd0 : 4;
            unsigned bcd1 : 4;
            unsigned bcd2 : 4;
            unsigned bcd3 : 4;
        }bcd;
    }work;
    char i;

    work.bd32 = (unsigned long int)val;
    for(i = 0; i < 16; i++){
        if(work.bcd.bcd0 >= 5)
            work.bcd.bcd0 += 3;
        if(work.bcd.bcd1 >= 5)
            work.bcd.bcd1 += 3;
        if(work.bcd.bcd2 >= 5)
            work.bcd.bcd2 += 3;
        if(work.bcd.bcd3 >= 5)
            work.bcd.bcd3 += 3;
        work.bd32 <<= 1;
    }
    b[0] = work.bcd.bcd0;
    b[1] = work.bcd.bcd1;
    b[2] = work.bcd.bcd2;
    b[3] = work.bcd.bcd3;
    return;
}

int main(int argc, char** argv) {
    
    unsigned char bcd[4];
    
    init();
    
    while(1){
        while(!r_voltage){
            // wait until measurement complete
        }

        bin2bcd(s, bcd);              // second
        ser_out[14] = bcd[0] + '0';
        ser_out[15] = bcd[1] + '0';
        bin2bcd(m, bcd);              // minutes
        ser_out[17] = bcd[0] + '0';
        ser_out[18] = bcd[1] + '0';
        bin2bcd(h, bcd);              // hour
        ser_out[20] = bcd[0] + '0';
        ser_out[21] = bcd[1] + '0';
        ser_out[22] = bcd[2] + '0';
        
        // calculate Current, r_current / 128 * 100
         bin2bcd((r_current>>2)+(r_current>>3)+(r_current>>6), bcd);
        ser_out[2] = bcd[0] + '0';
        ser_out[3] = bcd[1] + '0';
        ser_out[4] = bcd[2] + '0';
        ser_out[6] = bcd[3] + '0';
        disp[0] = bcd[1];
        disp[1] = 0x10 | bcd[2];
        disp[2] = 0x40 | bcd[3];
        
        // caluculate Voltage, r_voltage / 128 * 10
        bin2bcd((r_voltage>>4)+(r_voltage>>6), bcd);
        ser_out[8] = bcd[0] + '0';
        ser_out[9] = bcd[1] + '0';
        ser_out[11] = bcd[2] + '0';
        ser_out[12] = bcd[3] + '0';
        if(bcd[3]){
            RA4 = 0;    // Mid LED dot on
            disp[3] = 0x80 | bcd[1];
            disp[4] = 0x90 | bcd[2];
            disp[5] = 0xc0 | bcd[3];
        }else{
            RA4 = 1;    // left LED dot on
            disp[3] = 0x80 | bcd[0];
            disp[4] = 0x90 | bcd[1];
            disp[5] = 0xc0 | bcd[2];
        }
        
        ser_ctr = 23;
        r_current = r_voltage = 0;
    }


    return (EXIT_SUCCESS);
}

BCD変換は以下のサイトのプログラムを参考にさせて頂きました。
プログラミングな日々: PICマイコンでBCD変換

また私はProlific PL-2303HXというチップを使ったシリアル→USB変換アダプターを使っているのですが、Windows10ではドライバーが提供されていないため以下のサイト経由でパッチ当てドライバーを入手してインストールしています。
Windows10でPL2303を無理やり動かす | なんでも独り言

※7セグメントLEDを6個並べると配線工数のほとんどがLEDですね。次はI2C接続の液晶使おうと思います。