Overview:

The display shows the following:

-Battery Voltage
-Input Voltage (wind, solar, etc) (averaged over the last ten readings)
-Amperage (averaged over the last ten readings)
-Wattage (averaged)
-Transistor Temperature and Duty Cycle (these two values alternate between each other)
-Amp-Hour counter

As far as features go:

-Automatic backlight control; if no input voltage for 60 seconds, backlight turns off
-Automatic reset of Amp-hour counter if an hour elapses with no input voltage

There might be enough overhead left to stream serial data to a PC, but I would recommend using a 16 mHz MCU. This code is written specifically for a 12 volt system with the charger in charge control mode. There are options for 24 and 48 volt settings which do change the scale of some of the voltage readings as well as other unknowns.

Schematic:

The schematics are fairly straightforward. The important bit is the wiring of the RJ25 plug:

  1. LED A
  2. Comm 1
  3. Comm 2
  4. Ground
  5. V+ (8 volts)
  6. Led B

Pardon the LCD, Fritzing doesn't have a serial LCD I could pick from. The purple lead is merely a serial Tx from the Arduino. The two LED leads are currently not being used, but otherwise are used to relocate the status LED that is normally found on the charger.

Source Code:

Ultimately the code is not as clean as I care for, but with the challenge of not using any interrupts it was necessary to break up functions and control when they could execute. This is done by using the time between bytes (which is longer than the time between bits) to perform calculations and stream data to the LCD. I tried to be healthy with the use of comments so hopefully it makes a little sense!

This is all written under the Arduino 1.0.6 IDE.

/*Digital Volt Meter for the Xantrex C-series charge/load controllers. Configured to read the obscure communication
protocol they use and display the information on a 2x16 Parallax LCD. Displays battery voltage, average amperage,
watts (calculated from average amps), input voltage, transistor temperature, duty cycle and total amp-hours. LCD
backlight is programmed to turn off after a period of no charging to conserve power and serve as a night mode, amp-hour
counter is also programmed to reset at night so amp-hour counter will perform as a daily counter, not total.*/

#include SoftwareSerial lcdSerial(11,12); //rx,tx configure softwareserial for LCD

//pin declarations
#define comm1pin 7
#define comm2pin 8
#define LED 13
#define LCDrx 11
#define LCDtx 12

//configurations declarations
#define noPowerTimeLimit 60000 //time in milliseconds in which display backlight turns off if not charging
#define noPowerAHTimeLimit 3600000 //time in milliseconds in which Amp-Hour counter resets (1 hour = 3600000)
#define yesPowerTimeLimit 5000 //time in milliseconds in which display backlight turns on if charging begins
#define avgAmpArraySize 10 //number of readings to keep in avgAmp (rolling average)
#define avgPvArraySize 10
#define commTimeout 5 //time in milliseconds both comm can be low before it's considered a timeout (time between bytes)

boolean tempOrDuty = true; //boolean for selecting whether temp or duty cycle is currently displayed
boolean commCheck = true; //boolean for latching communicatons logic, when true MCU will check for comm high
boolean resetTimeLimit = true;
boolean backLightOff = false; //status of whether backlight is enabled or not
boolean lastStateAmp = true;
byte syncCount = 0; //status as to whether a sync signal has been received, used to prevent writing to LCD until good data is present
byte temp = 0; //temp storage for byte in progress
byte comm1=0; //register for comm1 status
byte comm2=0; //register for comm2 status
byte count = 0; //count of number of bits stored in temp
byte byteCount = 0; //count of number of bytes collected since last sync
byte transTemp = 0; //transistor temp of charger
byte battTemp = 0; //battery temp
byte toggleCount = 0; //counter for how long to display temp or duty cycle; incremented by number of sync cycles
byte dutyCycle = 0; //duty cycle of charger circuit
unsigned int pvVoltage = 0; //solar/input voltage to charger
unsigned int avgAmps = 0; //average amperage
unsigned int avgPv = 0; //average pv voltage
unsigned int amperage = 0;
unsigned int battVoltage = 0;
unsigned int bulkVoltage = 0;
unsigned int floatVoltage = 0;
unsigned long mAmpSeconds = 0; //milli-amp-second counter
unsigned int watts = 0;
unsigned long lastTimeComm = 0;
unsigned long lastTimeNoPower = 0;
unsigned long currentTimeAH = 0; //used for calculating how long since last mAH update
unsigned long lastTimeAH = 0; //used for calculating how long since last mAH update
unsigned long mAmpHours = 0; //milli-amp-hour counter
byte stats[23];
byte arrayAmps[avgAmpArraySize];
byte arrayPv[avgPvArraySize];
//definition for custom lcd characters, first byte is character #, next 8 are the 8 lines, only last 5 bits per byte matter as chars are 5 bits wide
byte char0[] = {248, 14, 31, 17, 17, 17, 31, 31, 31}; //battery icon
byte char1[] = {249, 7, 6, 12, 15, 31, 6, 12, 8}; //lightning bolt icon
byte char2[] = {250, 8, 20, 28, 20, 5, 5, 7, 5}; //AH icon
byte char3[] = {251, 20, 20, 20, 8, 2, 5, 7, 5}; //VA icon
byte char4[] = {252, 18, 4, 4, 9, 0, 14, 10, 27}; //duty cycle icon
byte char5[] = {253, 28, 20, 28, 0, 7, 4, 6, 4}; //amp icon
byte char6[] = {254, 0, 0, 0, 0, 2, 5, 7, 5}; //temp icon
//lcd setup arrays, cleaner coding than writing a serial.write for each one
byte setup1[] = {12, 17, 22}; //clear display, turn on backlight, turn off cursor
byte setup2[] = {12, 0, 134, 6, 140, 3, 148, 1, 154, 5, 159, 2}; //clear display, then place cursor positions and print custom characters

void setup() {
  // initialize serial:
  Serial.begin(115200);
  lcdSerial.begin(19200);
  
  //clear arrayAmps
  for(int x= 0; x < (avgAmpArraySize-1); x++) {
    arrayAmps[x] = 0;
  }
  
  //configure pins
  pinMode(LED, OUTPUT);
  pinMode(comm1pin, INPUT_PULLUP);
  pinMode(comm2pin, INPUT_PULLUP);
  pinMode(LCDrx, INPUT);
  pinMode(LCDtx, OUTPUT);
  
  //enter custom characters
  lcdSerial.write(char0, 9); //array to read from, number of bytes
  lcdSerial.write(char1, 9);
  lcdSerial.write(char2, 9);
  lcdSerial.write(char3, 9);
  lcdSerial.write(char4, 9);
  lcdSerial.write(char5, 9);
  lcdSerial.write(char6, 9);
  
  //set up display
  lcdSerial.write(setup1, 3); //do initial LCD setup (see array above)
  lcdSerial.print("Xantrex DVM");
  delay(1000);
  lcdSerial.write(setup2, 12); //do final LCD setup
   
}

void loop() {

  //check if no input voltage, if true, do a delayed turn-off of the lcd backlight to save power (night mode). Also reset amp-hour counter
  if(pvVoltage == 0) { 
    //detect if state of amperage has changed, if so reset booleans so counters will be reset properly
    if(lastStateAmp == true) { 
      resetTimeLimit = true;
      lastStateAmp = false;
    }
    if((millis()-lastTimeNoPower) > noPowerAHTimeLimit) { //reset amp hour counter after no power for a long period of time 
      mAmpHours = 0;
    }
    if(backLightOff == false) { //if backlight is currently on
      if(resetTimeLimit == true) { //if the timer has not been reset yet, do so
        lastTimeNoPower = millis();
        resetTimeLimit = false; //toggle reset status to prevent it from constantly rolling lastTimeNoPower
      } else if((millis() - lastTimeNoPower) > noPowerTimeLimit) { //if enough time has elapsed, turn off backlight and reset status bits
        lcdSerial.write(18); //turn off backlight
        backLightOff = true;
        resetTimeLimit = true;
      }
    }
  } else {
    if(lastStateAmp == false) {
      resetTimeLimit = true;
      lastStateAmp = true;
    }
    if(backLightOff == true) { //same basic concept; except configured for turning the backlight on
      if(resetTimeLimit == true) {
        lastTimeNoPower = millis();
        resetTimeLimit = false;
      } else if((millis() - lastTimeNoPower) > yesPowerTimeLimit) {
        lcdSerial.write(17); //turn on backlight
        backLightOff = false;
        resetTimeLimit = true;
      }
    }
  }

  //collect current status of comm pins
  comm1 = digitalRead(comm1pin);
  comm2 = digitalRead(comm2pin);
    
  if(commCheck == true) { //make a one-shot gate so cycles aren't wasted checking after a bit is captured
      if(comm1 == 1) { //delay and then check other comm pin for activity; otherwise it was possible to miss the slight variance between both comms going high for the sync bit
        delay(1);
        comm2 = digitalRead(comm2pin);
      }
      if(comm2 == 1) {
        delay(1);
        comm1 = digitalRead(comm1pin);
      }

      switch (comm1 + (comm2 * 2)) { //use a switch state; cleaner appearance than the numerous ifs otherwise required
      case 0: //if  both comms are low, check if sufficient delay has occurred to denote a new byte
        //if too much time has elasped since both comm went low, assume this is the space between bytes and reset temp register and bit counter
        if((millis()-lastTimeComm) > commTimeout) { 
          temp = 0; //reset temp variable
          count = 0; //reset bit count
        }
        break;
      case 1: //if only comm1 is high, add a 1 to the byte
        temp *= 2; //multiply temp by 2 to bitshift
        temp += 1; //add 1 to the byte
        commCheck = false; //set one-shot so we only spend the next cycles waiting for both comms to go low
        count++; //increment bit counter
        break;
      case 2: //if only comm2 is high, add a 0
        temp *= 2;
        commCheck = false;
        count++;
        break;
      case 3: //if comm1 and comm2 are high, this is the sync signal to indicate first byte
        byteCount = 23;
        break;
    }

    if(count > 7) { //once enough bits have been captured for a byte
      if(byteCount > 22) { //Check if 23 bytes have been made; start new line and do watt/amp-hour calculations so they're as up-to-date as possible
        byteCount = 0; //reset byte counter
        if(syncCount < 5) {
          syncCount ++;
        }
      }
      //use a switch case to split up updates to the LCD during time between bytes. Doing all at once takes too much time, causing the MCU to miss bytes.
      if(syncCount > 2) { //don't start calculating and printing data until sync has been received a couple times; prevents garbage data
        switch(byteCount) { 
          case 1:
            amperage = stats[1] / 2; //calculate amperage, will only get a whole number result
            avgAmps = 0; //reset avgAmps
            for(int x = (avgAmpArraySize-1); x > 0; x--) { //use a for loop to roll the avg array and fill the average value with the sum
              arrayAmps[x] = arrayAmps[x-1];
              avgAmps += arrayAmps[x]*10; //multiply by ten to increase resolution when doing division math
            }
            arrayAmps[0] = amperage; //add the new value to the array
            avgAmps += amperage*10; //add the new value to the sum
            avgAmps /= avgAmpArraySize; //divide the sum by number of samples to get average
            lcdSerial.write(135);//move cursor to row 0, position 7
            if(avgAmps < 100) { //if value is less than 10 (keep in mind 10.0 is stored as 100, add a preceding space; this takes less time than clearing the field with spaces, then returning to the start point
              lcdSerial.print(" ");
            }
            lcdSerial.print(avgAmps/10); //divide by ten to get whole number
            lcdSerial.print("."); //decimal point
            lcdSerial.print((avgAmps - (avgAmps/10)*10)); //do yet more math to get the decimal
            break;
          case 4:
            pvVoltage = ((unsigned long)stats[4]*10000)/2857; //do conversion math, result is double point precision (IE, 12.54 volts is 1254)
            avgPv = 0;
            for(int x = (avgPvArraySize-1); x > 0; x--) { //use a for loop to roll the avg array and fill the average value with the sum
              arrayPv[x] = arrayPv[x-1];
              avgPv += arrayPv[x]; //multiply by ten to increase resolution when doing division math
            }
            arrayPv[0] = pvVoltage; //add the new value to the array
            avgPv += pvVoltage; //add the new value to the sum
            avgPv /= avgPvArraySize; //divide the sum by number of samples to get average
            lcdSerial.write(149); //row 1 pos 1
            if(avgPv < 100) {
              lcdSerial.print(" ");
            }
            lcdSerial.print(avgPv/10);
            lcdSerial.print(".");
            lcdSerial.print((avgPv - (avgPv/10)*10));
            break;
          case 10:
            if(tempOrDuty == true) { //perform a check as to whether temp or duty cycle should be displayed
              if(toggleCount < 4) { //show for a period of time before clearing the counter and then setting temp to display for a period
                dutyCycle = ((unsigned int)stats[16]*100)/128;
                lcdSerial.write(154); //row 1, pos 6
                lcdSerial.write(4); //insert duty cycle icon
                lcdSerial.write(155); //row 1 pos 7
                if(dutyCycle < 10) {
                  lcdSerial.print("  ");
                } else if(dutyCycle < 100) {
                  lcdSerial.print(" ");
                }
                lcdSerial.print(dutyCycle);
                toggleCount++;
              } else {
                toggleCount = 0;
                tempOrDuty = false;
              }
            }
            break;
          case 11:
            if(tempOrDuty == false) {
              if(toggleCount < 4) {
                transTemp = (((255-(unsigned long)stats[11])*10000)/161+2622)/100;
                lcdSerial.write(154); //row 1, pos 6
                lcdSerial.write(5); //insert temp icon
                lcdSerial.write(155); //row 1 pos 7
                if(transTemp < 10) {
                  lcdSerial.print("  ");
                } else if(transTemp < 100) {
                  lcdSerial.print(" ");
                }
                lcdSerial.print(transTemp);
                toggleCount++;
              } else {
                toggleCount = 0;
                tempOrDuty = true;
              }
            }
            break;
          case 12:
            battVoltage = (((unsigned long)stats[0]*100000)/1325+464)/10; //calculate batt voltage to 2 point precision
            lcdSerial.write(129);//move cursor to row 0, position 1
            if(battVoltage < 1000) {
              lcdSerial.print(" ");
            }
            lcdSerial.print(battVoltage/100); //display the whole numbers for batt voltage
            lcdSerial.print(".");
            lcdSerial.print((battVoltage - (battVoltage/100)*100)/10); //exploit arduinos rounding methods to display tenths of a volt
            break;
          case 13:
            watts =((unsigned long)battVoltage * avgAmps)/1000;
            lcdSerial.write(141); //row 0 pos 13
            if(watts < 10) {
              lcdSerial.print("  ");
            } else if(watts < 100) {
              lcdSerial.print(" ");
            }
            lcdSerial.print(watts);
            break;
          case 14:
            currentTimeAH = millis(); //store current time to a variable, this is better than using millis twice as the time calcs will be more accurate
            mAmpSeconds += (currentTimeAH - lastTimeAH) * amperage; //check how much time since last measurement to calc milliAmp-Seconds
            lastTimeAH = currentTimeAH; //update last time variable
            while( mAmpSeconds > 3600) { //once 3600 mAs have been collected, increment amp hour meter
              mAmpSeconds -= 3600; //remove 1 mAH from mAs
              mAmpHours += 1; //increment amp hours
            }
            lcdSerial.write(160); //row 1 pos 12
            if(mAmpHours/1000 < 10) {
              lcdSerial.print("   ");
            } else if(mAmpHours/1000 < 100) {
              lcdSerial.print("  ");
            } else if(mAmpHours/1000 < 1000) {
              lcdSerial.print(" ");
            }
            lcdSerial.print(mAmpHours/1000);
            break;
          default:
            break;
        }
      }
      
      stats[byteCount] = temp; //add temp variable to array
      temp = 0; //reset temp
      count = 0; //reset bit count
      byteCount++; //increment byte counter
    }
  } else { //if commcheck is false, wait for both comms to be low before resuming next commcheck
    if((comm1 + comm2) == 0) {
       commCheck = true;
       lastTimeComm = millis(); //start timer for checking if register must be cleared (lost or unsync comm)
    }
  }
    
}

//other values 
    //battTemp = stats[6]; //conversion formula not determined yet
    //bulkVoltage = (((unsigned long)stats[9]*100000)/1325+464)/10;
    //floatVoltage = (((unsigned long)stats[10]*100000)/1325+464)/10;