A PIC based Battery Management System

bquick said:
I'm thinking the logic would go something like this: When in charging mode, once the cell voltage gets higher than 3.2 volts, turn on the shunt with a duty cycle proportional to the difference between the cell's voltage and the lowest cell's voltage, with 100% duty cycle at a .2 volt difference.

How might you change these numbers?

- Brad


I was thinking the shunt stays off until a cell reaches 3.65v, then turns on. As the cell charges, a higher and higher duty cycle will be requred to keep the cell at 3.65v. Duty cycle could be changed in steps and does not need to be a constant frequency. As soon as the voltage goes over 3.65v, the duty cycle is increased. When the voltage drops below 3.65v, the duty cycle is decreased.

Once the duty cycle reaches 100% or so, the processor needs to send a signal to reduce the charging current to avoid overcharging a cell. Any cell might send this signal. This signal could be handled by a main processor and could either interface to the charger PWM, or could simply disconnect the charging source until the duty cycle on the shunt drops down to a lower chosen value. This would in effect be like a PWM for the charging source.

The shunt resistor can be chose for any desired current. If you have 10 or 16 channels crammed onto a small board, the total heat dissipation will be the limiting factor. You can do the math.
 
Since the charging current (10 amps?) is higher than the shunt current (1 amp?) we will need to disconnect (or throttle) the charger if we get up to 3.65. Rather than wait until we get there to start balancing, I wanted to start balancing a little earlier.

I'll play with PWM tonight.

- Brad
 
I made some progress over the weekend.

I experimented with PWM and I'm able to vary the shunt with it. Unfortunately, when I turn it on, my voltage readings start jumping around and sometimes jump up to where they shut the charging off. I may have to just turn it all the way on and toff. I will have to try it on an actual battery so that I can see if the problem is coming from my power supply reacting to the load.

The display is working well. I have an input set up to change display modes. It will be hooked up to the start button on the motorcycle. If the button is held in for more than two seconds, it starts the charging process.

- Brad
 
You might try using a large capacitor to simulate a battery. Once the voltage hits set point, the voltage should stay relatively steady and the current should be a function of the PWM duty cycle.

It may be necessary to filter the input to the A/D so the noise from the PWM does not interfere too much. Actual cell voltage will fluctuate a few millivolts during the cycle, which is fine.

Another possible way to do it is to just turn the shunt on or off based on voltage and let the hysteresis of the circuit provide the oscillation. The software could then detect whenever the shunt stays on for longer than a specified time to start throttling the charge current.
 
It looks like I can get a good voltage reading if I wait until the shunt has been turned off for 30ms or so.

- Brad
 
It looks like Maxim has been reading this 28 paga saga and is in the process of developing their own solution in the upcomming MAX11068. The specs are not available on-line but I've posted them here:

http://www.convertthefuture.com/files/MAX11068_Rev_5_4_April_8_2008.pdf

It looks like it supports 12 cells, under over voltage detection, and equalization up to 200mA.
Also it has a i2C interface for daisy-chaining 12 cell pack together.

Mark
 
I did some more shunt testing and decided I should use a 4 Ohm resistor for the shunt. This gives off a reasonable amount of heat and draws almost an amp. I have a bunch of .47 Ohm resistors that cost me nothing and I could use the PWM to throttle it, but I want a fail safe design. If my transistor gives up, I'll only draw an amp and it will take 40 hours for my battery to drain to zero.

The software is fairly complete now, with multiple viewing modes, watchdog timer, etc. I'm starting to build a circuit board.

I still haven't decided on a power supply. My choices are:

- A 72 Volt battery charger from Northern Tool. It will put out 10 amps, but it's relatively expensive and too big to put on my bike.

- Two 36V 6.9 amp switching power supplies from ebay in series. First, I don't know if they will work in series. Second, I don't know what will happen if I connect them to my battery and it tries to draw more than 6.9 amps. The power supplies are over current protected, but I don't know how they will behave.

- Three 24V 5 amp switching battery chargers from ebay in series. Again, I don't know if they will work in series. Second, they have automatic shutoff, which may be a problem if my circuit is expecting to the the shutoff.

Any ideas?

- Brad
 
most switching supplies have isolated outputs. odds are good but not guaranteed that you could use your eBay supplies in series.

i have used a pair of Soneil 3610SRF chargers in series for 72V packs. At 4.5A they are a little smaller than what you said you are looking at but they might be an idea. the outputs on these are definitely isolated.

http://www.soneil.com/Completesets/3610SRF.pdf

Rick
 
Here's my weekend's work. I didn't realize how many wires I would have to run.

It's shown with parts installed for 5 cells. I've tested this much and it works OK. I've been busy finishing up the rest of the wires.

The extra components to the lower right are for the display. The empty row in the sockets are for a diagnostics LED. One can be plugged in at any time. The LED's flash twice when the processor is powered up and then changes state any time it receives a complete communication message. This makes it easy to find faults.

This is the whole board. The ceramic resistors will be replaced with onces coming tomorrow.

BMS overall.jpg

Here's a closeup of the five working channels.

BMS closeup.jpg

Here's the back of the board. I currently have all of the positive and negative leads tied together so I can test the board from a single power supply.

BMS rear.jpg
 
I've binned individual cell temp monitoring as I already have a pack solution in the Insight which does this.

The 1.25v LM385 is used as a V ref but as the voltage of the cell changes so does the result from the PIC adc So I'll just use a lookup table in the master pic to get the correct cell V in a useable form to work out pack voltage.

If pic cell adc result is 200 then I know cell V is 2.00V (I've tested it)
If pic cell adc result is 500 then I know cell V is 3.80V (I've tested it)

The slave only needs to know that for instance at 3.8V the adc result will be X above X it needs to turn on load.

So this version has a reduced component count and slave program is incredibly simple.

New schematic is here

http://www.solarvan.co.uk/slave240608.jpg

I'm almost ready to go to PCB layout now, anyone help me with this?

I'm happy with my once a second cell polling, from my long experience with TS Lithium cells they are very rugged and don't need to be micromanaged to the Nth degree Yes lots of improvements could be made but again line has to be drawn and boards made up, I can tinker with software to my hearts content later. Looks like my new lifepo4 batts could be on way from China by the end of June so need to get on!

So long as the master pic knows if data is missing/invalid/out of range and signals the fact along with shutting down charger etc then the Human computer has to investigate.
 
Is there a pull-up resistor for the LM385, and a series resistor in the base of the transistor?
BC337 is rated 500mA, is this enough?

EDIT: looking at photo again, that's not a BC337!

Amanda
 
commanda said:
Is there a pull-up resistor for the LM385, and a series resistor in the base of the transistor?
BC337 is rated 500mA, is this enough?

EDIT: looking at photo again, that's not a BC337!

Amanda

I'm sure he's just enabling the weak pullup on the port line to power the LM385.

With the 10 ohm load resistor collector current won't be over 380mA, the BC337 I'm familiar with (Fairchild) are rated for 800ma.

You are correct in pointing out the lack of a series base resistor. Although it may very well work without one it may be asking for trouble relying on the P channel device in the port driver to limit the currents. The base will need 5-10mA of drive minimum, It would be prudent to put a base resistor in, or change the configuration to an emitter follower by putting the load resistor from the emitter to ground. An emitter follower will result in about 1/3 of the power being dissipated in the transistor so that also needs to be considered.

Personally for a low volume product I would pay the extra coin and use a logic level FET such as a FDS9926 and eliminate the drive currents and avoid the problems of localized die heating and A/D errors from internal voltage drops in the PIC. Although if you need something in a T0-92 package you may have to search a bit.
 
peterperkins said:
I'm almost ready to go to PCB layout now, anyone help me with this?

I've done a little of this. First up it looks like you have some software to generate schematics? can it generate a 'net list'? (a text file that describes what connects to what and names the packages of each part)

I use TinyCAD go generate schematics and net lists. After that I switch to FreePCB to manually layout the PCB and generate the "gerber" files that PCB houses use. If you're a linux nut gEDA is another option, but I really recommend FreePCB for layout. Manual layout is a bit of an art but not really that hard. I've had good luck just sticking to the basics like using a ground plane on the back of my PCBs, putting nearly all of the signal routing on the top layer, using bypassing capacitors at the power pins of each IC, and wide power traces.

At this point my two prime targets are BatchPCB for one-off stuff, and Advanced Circuits for rush or higher volume stuff. Nither of these guys are really UK friendly, but lots of these PCB houses exist on the 'net so a local one should exist. Specifically, the support forums over at BatchPCB had a large discussion on local PCB houses not too long ago, and I remember a discussion over at the Parallax Discussion Forum a while back too.

Marty
 
I got my board fired up last night. I don't have the balancing components all installed yet and it's all running off my power supply voltage, but it's working.

It has 20 processors, talking via serial ports through optos in a loop configuration. I'm currently running at 2400 baud. To simplify the software, I took out the overlapping messaging, so each processor receives a message containing the previous cell voltages, then sends a message with it's voltage included. Using this method, it's updating the display just about once a second, which I think is sufficient.

I currently have two display modes. One shows highest and lowest cell voltages and the other shows all 20 cell voltages.

- Brad
 
Here's my current code in case anyone is interested:

Code:
/* Name: main.c
 * Author: Brad Quick
 * Programmed for: Atmega48
 */

#include "stdio.h"
#include <avr/io.h>
#include <avr/interrupt.h> 
#include <avr/eeprom.h> 

// Timer Functions
//
// To use the timer:
//    starttimer(xxxx);  //where xxxx is when the timer will be finished in counts.  Each count is .0001024 seconds.
//    if (timerdone())   // check to see if the timer has completed.
//       { ... }
//

#define NUMTIMERS 2
#define GENERALTIMER 0

long timerdonetime[NUMTIMERS]; // this is where the timer data is stored

void updatetimers()
   { // this should only be called by the timer functions
	unsigned int elapsedtime=TCNT1; // get the counts
	if (elapsedtime>1000) 
	   {
		TCNT1=0; // reset the counts
	
	   int x;
	   for (x=0;x<NUMTIMERS;++x) if (timerdonetime[x]>0) timerdonetime[x]-=elapsedtime;
		}
	}
	
void starttimer(int timernumber,long donetime)
   // start a timer that will be done in donetime counts.  At 10 MHz, each count is .0001024 seconds.
   {
	updatetimers();
	
	timerdonetime[timernumber]=TCNT1+donetime;
	
   TCCR1B |= ((1 << CS10) | (1 << CS12)); // Set up timer at Fcpu/1024 if it's not already done
   }

int timerdone(int timernumber) 
   { // return 1 if the timer is done.
	updatetimers();

   if (TCNT1>timerdonetime[timernumber]) return(1);
   else return(0);
   }

#define DIGITALINPUT 0
#define DIGITALOUTPUT 1

void setupdigitalio(int pinnumber,int output)
   { // set pin pinnumber to be an output if output==1, othewise set it to be an input
   if (output)
	   {
		// this is an output.  13.2.3 of the datasheet says we shouldn't go from tri-state to output high, so we have to do pull up first
		PORTB |= (1 << pinnumber); // pull up resistor on
		DDRB |= (1 << pinnumber); // this is an output
		         
		}
   else
      {
	  DDRB &= ~(1 << pinnumber); // set it to an input
	  PORTB |= (1 << pinnumber); // pull up resistor on
	  }
   }

int getdigitalinput(int pinnumber)
   {
   if (PINB & (1 << pinnumber)) return(0);
   else return(1);
   }
   
void setdigitaloutput(int pinnumber,int value)
   {
   if (!value) PORTB |= (1 << pinnumber);
   else PORTB &= ~(1 << pinnumber);
   }

// Serial Port Functions
//
// The serial port sets up interrupts on the receiving end of things.  Recieved characters are put into a circular buffer.
// This way, we don't lose characters if we don't get to reading the serial port fast enough since we aren't doing handshaking.
//
// To use the serial port functions:
//
//    unsigned char buffer[100]; // the receive buffer must be supplied by the main program
//    unsigned int c; // we use an int for c so tha we can detect if reads time out.
//
//    initserialport(9600,buffer,100);  // 9600 baud, 100 byte buffer size
//    serialsendchar('x');
//    serialsendstring("test string");
//    serialsenddata("test string",11); // send eleven characters
//    if (serialnumcharsavailable()>0)    // global serialnumcharsavailable can be used to see how many characters have been received that haven't been read.
//       c=serialgetchar();             // read one character.  This won't time out since we know there is a character available.
//    c=serialgetchar();                // read one character.  This may time out since we know there is a character available.   
//    if (c==SERIALTIMEOUT) ...         // detecting a time out

#define SERIALTIMEOUT 1000 // value returned if we have to wait too long to receive a character on the serial port

void initserialport(int baud)
   { // initialize the serial port and set up a read buffer and interrupts so that we don't lose any data from reading too slowly
   unsigned long baudprescale=(((F_CPU / (baud * 16UL))) - 1);

   // these next 4 lines set up the input and output pins.  I don't think they are necessary
   DDRD &= ~(1 << 0); // set pin 0 to an input
   PORTD |= (1 << 0); // pull up resistor on
		
	PORTD |= (1 << 1); // pull up resistor on.  Shouldn't go directly from tri-state to output high
	DDRD |= (1 << 1); // set pin 1 as an output
   
   // set the receive pin as in input and turn on it's pull-up resistor
   UCSR0B |= (1 << RXEN0) | (1 << TXEN0);   // Turn on the transmission and reception circuitry 
   UCSR0C |= /*(1 << URSEL0) |*/ (1 << UCSZ00) | (1 << UCSZ01); // Use 8-bit character sizes 

   UBRR0L = baudprescale; // Load lower 8-bits of the baud rate value into the low byte of the UBRR register 
   UBRR0H = (baudprescale >> 8); // Load upper 8-bits of the baud rate value into the high byte of the UBRR register 
   }

void serialsendchar(unsigned char c)
   { // send c to the serial port
   while ((UCSR0A & (1 << UDRE0)) == 0) ; // Do nothing until UDR is ready for more data to be written to it 
   UDR0 = c; // send the character 
   }
   
void serialsendstring(char *string)
   { // send a null terminated string to the serial port
   while (*string) serialsendchar(*string++);
   }

void serialsenddata(unsigned char *data,int datalength)
   { // send datalength bytes of data to the serial port
   while (datalength-- >0) serialsendchar(*data++);
   }

int serialnumcharsavailable()
   { // returns 1 if there is a char available, 0 if not
	return((UCSR0A & (1 << RXC0)) != 0);
	}
	
unsigned int serialgetchar()
   { // get the next character from the serial port
   // time out if we don't receive a character in approximately .1 second
   starttimer(GENERALTIMER,1000);
   while ((UCSR0A & (1 << RXC0)) == 0) // wait for a character to arrive
      { // the if statement may seem a litte redundant, but this way we don't use the timer if we don't need to
      while (serialnumcharsavailable()==0)
         {
         if (timerdone(GENERALTIMER)) return(SERIALTIMEOUT);
         }
      }
	  
   return((unsigned int)UDR0);
	}

// ADC Functions:
//
// To use the ADC functions:
//
//    adcsetchannel(0,ADCREFVCC);         // initializes the ADC to read on channel zero, using Vcc as the reference
//                                        // special channel ADCCHANREF1POINT1 reads the 1.1V internal reference
//                                        // special channel ADCCHANSLEEP puts the adc to sleep to conserve power
//    adcstartreading();                  // starts a reading
//    unsigned int value=adcgetreading(); // gets the value when the reading is completed.

// reference voltages:
#define ADCREFVCC 0
#define ADCREF1POINT1 1
#define ADCREFEXT 2

// channels: (channels 0 throuh ? can also be used)
#define ADCCHANREF1POINT1 -1
#define ADCCHANSLEEP -2

void adcsetchannel(int channel,int reference)
   { // set up and enable the adc to read channel channel using reference reference
   if (channel==ADCCHANSLEEP)
      {
      ADCSRA &= ~(1 << ADEN);  // disable ADC to save power
      }
   else
      {
	  
      if (reference==ADCREFVCC)
         ADMUX=(1 << REFS0); // Set the reference voltage to Vcc
      else if (reference==ADCREF1POINT1)
         ADMUX=(1 << REFS0) | (1 << REFS1); // Set the reference voltage to 1.1v internal reference
	   else if (reference==ADCREFEXT)
		   ADMUX=0;
			
      if (channel==ADCCHANREF1POINT1)
//         ADMUX |= 0x0E; // Set ADC reference to AVCC and mux channel to 1.1v internal
         ADMUX |= (1<<MUX3) | (1<<MUX2) | (1<<MUX1); // Set ADC reference to AVCC and mux channel to 1.1v internal
      else
         ADMUX |= channel; // Set ADC reference to AVCC and mux channel to channel	  
	  
      ADCSRA = (1 << ADPS2) | (1 << ADPS1); // Set ADC prescalar to 64 - 125KHz sample rate @ 8MHz. Should end up between 50KHZ and 200 KHZ CHANGE WITH F_CPU
//   ADCSRA |= (1 << ADIE);  // Enable ADC Interrupt 
      ADCSRA |= (1 << ADEN);  // Enable ADC 
//      ADCSRA |= (1 << ADATE);  // set auto trigger (free run mode)

   // do one read to throw away in order to make sure we get an accurate reading
//   ADCSRA |= (1 << ADSC);  // start a reading 	  
//   while ((ADCSRA & (1 << ADIF)));  // wait for conversion to complete
//   char temp=ADCH;
// sei();
      }
   }
	      
unsigned int adcgetreading()
   { // adcstartreading must be called before adcread.  They are separated so that a reading can be started, you can do other stuff, then come back and get the result
   ADCSRA |= (1 << ADIF);  // reset the completion flag.  
   ADCSRA |= (1 << ADSC);  // Start a conversion 	  
//   while ((ADCSRA & (1 << ADIF))) {};  // wait for conversion to complete
//   temp=ADCH;
   // wait for the reading to complete. this could be eliminated if we used auto run mode
   while (!(ADCSRA & (1 << ADIF))) {}  // wait for conversion to complete 

   // get the reading
   unsigned int result=ADCL;
   result|=ADCH<<8;
   
   return(result);
   }
	   
// serial port messaging system:
//
// MESSAGECODEDCHAR is a special character.  It is always followed by another character. The character after MESSAGECODEDCHAR represents a data character that has
// been shifted to the left one bit so as not to be confused with other special characters, namely MESSAGESTART and MESSAGEEND.
// When MESSAGECODEDCHAR or MESSAGESTART or MESSAGEEND characters appear in the data, they are encoded by sending MESSAGECODEDCHAR followed by MESSAGECODEDCHAR<<1 or MESSAGESTART<<1 or MESSAGEEND<<1
//
// a message is composed of the following (each represents one char, except message_data):
//   MESSAGESTART  message_type  message_data... MESSAGEEND CHECKSUM
// 
// CHECKSUM is a single character CHECKSUM of the message data, before the data is encoded.  The CHECKSUM itself may be encoded.  It appears after MESSAGEEND so that we
// can receive data and transmit it right away, then append new data on the end.

// special characters sent and received
#define MESSAGECODEDCHAR '\1'
#define MESSAGESTARTCHAR '\2'
#define MESSAGEENDCHAR '\3'

#define MESSAGEBATTERYVOLTAGECHAR 'A'
#define MESSAGECHARGINGCHAR 'B'

// special codes returned by serialgetcodedchar (int addition to SERIALTIMEOUT)
#define MESSAGESTART 1001
#define MESSAGEEND 1002
#define SERIALCHECKSUMERROR 1003
#define SERIALTOOMUCHDATAERROR 1004

void serialsendcodedchar(unsigned char c)
   {
   if (c>=MESSAGECODEDCHAR && c<=MESSAGEENDCHAR)
      {
      serialsendchar(MESSAGECODEDCHAR);
      serialsendchar(c<<1);
      }
   else serialsendchar(c);
   }

unsigned int serialgetcodedchar()
   { // return the next char in c, also return MESSAGESTART or MESSAGEEND if found
   unsigned int c=serialgetchar();
   
   if (c==SERIALTIMEOUT) return(c);
   
   if (c==(unsigned int)MESSAGESTART) return(MESSAGESTART);
   if (c==(unsigned int)MESSAGEEND) return(MESSAGEEND);
   if (c==(unsigned int)MESSAGECODEDCHAR)
      {
      c=serialgetchar();
      if (c==SERIALTIMEOUT) return(c);
      else return(c>>1);
      }
   
   return(c);
   }
   
void serialsendcodeddata(unsigned char *data,int datalength)
   { // send datalength bytes of data to the serial port
   while (datalength-- >0) serialsendcodedchar(*data++);
   }

void serialprintnumber(int num,int digits,int decimals)
   // prints a long number, right justified, using digits # of digits, puting a
   // decimal decimals places from the end, and using blank
   // to fill all blank spaces
   {
   char stg[8];
   char *ptr;
   int x;

   ptr=stg+7;

   *ptr='\0';
   for (x=1;x<=digits;++x)
      {
      if (num==0)
         *(--ptr)=' ';
      else
         {
         *(--ptr)=48+num-(num/10)*10;
         num/=10;
         }
      if (x==decimals) *(--ptr)='.';
      }
   serialsendstring(ptr);
   }

// Digital io Functions:
//
// These functions currently only work on port B, pins PB0 through PB7
//
// To use these functions:
//    setupdigitialio(0,DIGITALINPUT);  // sets pin zero to be an input
//    setupdigitlalio(1,DIGITALOUTPUT); // sets pin 1 to be an output
//    if (getdigitalinput(0))           // read input number 0
//       setdigitaloutput(1,1);         // turn on output number 1


void pwmsetup()
   {   // set up timer2 for PWM.  Operates on pin PB3
	TCCR2A |=(1<<COM2A1); // mode for switching on and off
	// TCCR2A |=(1<<COM2A0); // use this instead for inverted output
	TCCR2A |=(1<<WGM21) | (1<<WGM20); // use fast PWM mode
	
	TCCR2B |=/*(1<<CS22) |(1<<CS21) | */(1<<CS20); // prescaler of 128
   }
	
void pwmsetduty(unsigned char value)
   {
   OCR2A=value;  // from 0 to 255 to vary duty cycle
	}
	
#define DISPLAYSERIALPORT 1
#define MESSAGESERIALPORT 2

// Define the io for this project
#define IAMMASTERINPUT 0
#define DISPLAYINHIBITOUTPUT 1
#define MESSAGEINHIBITOUTPUT 2
#define SHUNTOUTPUT 3
#define CALIBRATEINPUT 6
#define MISCOUTPUT 4
#define MODECHANGEINPUT 5
#define CHARGERENABLEOUTPUT 7
// modes
#define MODELOWESTVOLTAGE 0
#define MODEALLVOLTAGES 1
#define NUMMODES 2

#define MAXCELLS 30
#define SHUNTOFFTIME 400 // how long the shunt has to be off before we made a battery voltage reading
#define MAXDUTYVOLTAGEDIFFERENCE 200 // use maximum shunt duty cycle when a cell is .2 volts above the lowest cell
#define MAXDUTY 100 // maximum duty cycle we will use on the shunt (0 to 255)

#define SHUNTTIMER 1

void setserialoutport(int portnumber)
   { // switch the serial port to port number by inhibiting output on the other ports.
	// check the state of this port's inhibit output to see if we are changing it
	if ((portnumber==DISPLAYSERIALPORT && getdigitalinput(MESSAGEINHIBITOUTPUT))
	   || (portnumber==MESSAGESERIALPORT && getdigitalinput(DISPLAYINHIBITOUTPUT))) return;
	
	while ((UCSR0A & (1 << UDRE0)) == 0) ; // Do nothing until UDR is ready for more data to be written to it 
	
	// looks like we need to wait a little extra
	starttimer(GENERALTIMER,100);
	while (!timerdone(GENERALTIMER));
	
	setdigitaloutput(DISPLAYINHIBITOUTPUT,1);
	setdigitaloutput(MESSAGEINHIBITOUTPUT,1);
	setdigitaloutput(portnumber,0); // un-inhibit the port we want to write to
	}

void sendmessage(unsigned char messagetypechar,unsigned char *data,int datasize)
   {
	// send a battery voltage message with no data
   serialsendchar(MESSAGESTARTCHAR);
   serialsendchar(messagetypechar);
	unsigned char checksum=messagetypechar;
	while (datasize-- >0)
	   {
		serialsendcodedchar(*data);
		checksum+=*data++;
		}
   serialsendchar(MESSAGEENDCHAR);				
   serialsendcodedchar(checksum); 
	}

unsigned int receivemessage(unsigned char messagetypechar,unsigned char *data,int maxdatasize)
   { // we have already received MESSAGESTARTCHAR and the message type character before entering this function.  This allows the data space to be
	// set up in advance.
	// returns number of chars received or an error code.  Error codes are all >= 1000
	unsigned char checksum=messagetypechar;
	unsigned int count=0;
	for (;;)
	   {
      unsigned int c=serialgetcodedchar();
		
		if (c==SERIALTIMEOUT) return(SERIALTIMEOUT);
		if (c==MESSAGEENDCHAR)
		   {
			if (checksum==serialgetcodedchar()) return(count);
			else return(SERIALCHECKSUMERROR);
			}
		if (count++==maxdatasize) return(SERIALTOOMUCHDATAERROR);
		*data++=(unsigned char)c;
		checksum+=(unsigned char)c;
		}
	}

void watchdogsetup()
   {
   /* Enable the watchdog to do a system reset on timeout */ 
   WDTCSR |= (1<<WDCE) | (1<<WDE); 
   /* Set new prescaler(time-out) value = 1024K cycles (~8 seconds) */ 
   WDTCSR  = (1<<WDE) | (1<<WDP3) | (1<<WDP0); 
   }
	
#define 	watchdogreset()   __asm__ __volatile__ ("wdr")

void delay(long delaytime)
   {
	starttimer(GENERALTIMER,delaytime);
	while (!timerdone(GENERALTIMER));
	}
	
uint16_t EEMEM calibrationwordh; 
uint16_t EEMEM calibrationwordl; 

int main(void)
   {
   setupdigitalio(DISPLAYINHIBITOUTPUT,DIGITALOUTPUT); // set pin 1 as an output
   setupdigitalio(MESSAGEINHIBITOUTPUT,DIGITALOUTPUT); // set pin 2 as an output
   setupdigitalio(SHUNTOUTPUT,DIGITALOUTPUT); // set pin 3 as an output
   setupdigitalio(MISCOUTPUT,DIGITALOUTPUT); // set pin 4 as an output
   setupdigitalio(IAMMASTERINPUT,DIGITALINPUT);
   setupdigitalio(CALIBRATEINPUT,DIGITALINPUT);
   setupdigitalio(MODECHANGEINPUT,DIGITALINPUT);
   
   initserialport(2400);

   adcsetchannel(ADCCHANREF1POINT1,ADCREFVCC); // set the adc to read channel 0 with vcc as a reference

   // read the calibration factor from eeprom
   unsigned long calibrationfactor=((unsigned long)eeprom_read_word(&calibrationwordh))<<16 | eeprom_read_word(&calibrationwordl);
   
	unsigned char mode=MODELOWESTVOLTAGE;
	unsigned char chargingmode=0;
   unsigned char shuntdutyvalue=0;
	
   // flash the LED twice, for diagnostics sake
	setdigitaloutput(MISCOUTPUT,1);
	delay(500);
	setdigitaloutput(MISCOUTPUT,0);
	delay(500);
	setdigitaloutput(MISCOUTPUT,1);
	delay(500);
	setdigitaloutput(MISCOUTPUT,0);
	
   // check to see if we are the master and remember the result
	int iamthemaster=0;
	if (getdigitalinput(IAMMASTERINPUT))
	   {
		iamthemaster=1;
		starttimer(GENERALTIMER,5000); // wait a half second or so to make sure everybody is running
		}
	
   watchdogsetup();
	
	for (;;)
	   {
      watchdogreset();
		
		// any time SHUNTTIMER times out, turn the shunt off.  In order for the shunt to be on, it has to be continually set
		if (shuntdutyvalue && timerdone(SHUNTTIMER))
		   {
			shuntdutyvalue=0;
			pwmsetduty(0);
			// set the timer for a short period of time so that we don't try to read the ACD too soon after it's shut off 
			starttimer(SHUNTTIMER,SHUNTOFFTIME);
			}
			
		// see if the calibration input is on.  Only calibrate if we've never been calibrated before
	   if (getdigitalinput(CALIBRATEINPUT) && calibrationfactor==0xFFFFFFFF)
	      { // we will calibrate when the input is released
         // voltages are represented as integers, in 1000's, so 4000 represents 4.0 volts.
         // the calibration factor is the ADC reading times the calibration voltage. Below, we are adding up
		   // 3000 readings, which is the same as 3000 times one reading, assuming calibration voltage=3.0 volts
         calibrationfactor=0;
		   int x;
         for (x=0;x<3000;++x)
		      {
				calibrationfactor+=adcgetreading();
			   }
		 
			// remember the calibration in eeprom
			eeprom_write_word(&calibrationwordh,calibrationfactor>>16);
			eeprom_write_word(&calibrationwordl,calibrationfactor & 0xFFFF);
			}

      // check to see if any messages are coming in on the serial port
      if (serialnumcharsavailable())
         {
         int c=serialgetchar();
			
         if (c==(unsigned int)MESSAGESTARTCHAR)
            { // we are receiving a new message.  Get the message type
            int messagetype=serialgetchar();
			   
            if (messagetype==(unsigned int)MESSAGEBATTERYVOLTAGECHAR || messagetype==(unsigned int)MESSAGECHARGINGCHAR)
               { // this is a battery voltage message. or a Charging message. Read the message and pass it along to the next processor.
               // first, start measuring our batteries voltage.
					unsigned int cellvoltages[MAXCELLS];
					unsigned int numcells=receivemessage((unsigned char)messagetype,(unsigned char *)&cellvoltages[0],MAXCELLS*2);

               if (numcells<1000) // no error
                  { // we received the entire message
						numcells/=2;
						
						// change the state of the LED to indicate that we received a message
                  setdigitaloutput(MISCOUTPUT,!getdigitalinput(MISCOUTPUT));

                  // read our battery voltage
                  // make sure the shunt has been turned off for more than SHUNTOFFTIME before we read our own battery voltage
                  if (!timerdone(SHUNTTIMER))
						   { // the shunt is on or was recently turned off.  Wait before taking our reading
							pwmsetduty(0); // turn the shunt off temporarily
							starttimer(GENERALTIMER,SHUNTOFFTIME);
							while (!timerdone(GENERALTIMER) && (shuntdutyvalue!=0 || !timerdone(SHUNTTIMER)));
							}
						
						// average 100 readings
                 	unsigned long total=0;
						int x;
	               for (x=0;x<100;++x)
	                  total+=adcgetreading();
	
	               pwmsetduty(shuntdutyvalue);  // from 0 to 255 to vary duty cycle
						
                  int voltage=100*calibrationfactor/total;
		         
						if (messagetype==(unsigned int)MESSAGECHARGINGCHAR)
						   { // we got a complete charging message.  See if we need to activate the shunt
							starttimer(SHUNTTIMER,30000); // the shunt times out in 3 seconds
			   							
							// only activate the shunt if the voltage is above 3.2 volts.  Check for below 4.2 volts to eliminate acting on bad data
							if (voltage>3200 && voltage<4200 && cellvoltages[0]>2400 && cellvoltages[0]<4000)
							   {
								int difference=voltage-cellvoltages[0];
								if (difference<0) shuntdutyvalue=0;
								else if (difference>MAXDUTYVOLTAGEDIFFERENCE) shuntdutyvalue=MAXDUTY;
								else shuntdutyvalue=MAXDUTY*difference/MAXDUTYVOLTAGEDIFFERENCE;
								pwmsetduty(shuntdutyvalue);
								}
							
							if (iamthemaster) // I am the master
							   { // when the highest voltage is greater than 3.6 volts, turn charging off
								// move this to where we send the charging message from the master
								if (cellvoltages[1]>=3600)
								   {
									chargingmode=0;
									setdigitaloutput(CHARGERENABLEOUTPUT,0);
				               // set the serial port up to write to the display
                           setserialoutport(DISPLAYSERIALPORT);
									serialsendstring("?f?c0?<"); // clear the display, set to no cursor, small characters
									}
								}
								
							}
						else // this is a battery voltage message.
						   { // add our voltage reading onto the end of the list
                     cellvoltages[numcells++]=voltage;
							
			            if (iamthemaster)
						      {					 
				            // set the serial port up to write to the display
                        setserialoutport(DISPLAYSERIALPORT);
							
							   unsigned int lowestbatteryvoltage=voltage; // start with our voltage
					         unsigned int highestbatteryvoltage=voltage; // start with our voltage
							   int x;
							   for (x=0;x<numcells;++x)
								   {
                           if (cellvoltages[x]<lowestbatteryvoltage) lowestbatteryvoltage=cellvoltages[x];
                           if (cellvoltages[x]>highestbatteryvoltage) highestbatteryvoltage=cellvoltages[x];
								   }
								
							   if (mode==MODELOWESTVOLTAGE)
							      {
                           // output the lowest voltage to the display
								   serialsendstring("?y1?x04Lowest: ");
					            serialprintnumber(lowestbatteryvoltage/10,3,2);
								   serialsendstring("?y2?x03Highest: ");
					            serialprintnumber(highestbatteryvoltage/10,3,2);
								   }
							   else if (mode==MODEALLVOLTAGES)
							   	{
                           // output all voltages to the display
								   serialsendstring("?y0?x00");
								   int x;
								   for (x=0;x<numcells;++x)
								      {
					               serialprintnumber(cellvoltages[x]/10,4,0);
									   }
								   }
								
							   if (chargingmode)
							      { // we are in charging mode.  Tell each cell what the lowest voltage is
                           setserialoutport(MESSAGESERIALPORT);
								
				               // send a charging message with the lowest and highest voltages
									cellvoltages[0]=lowestbatteryvoltage;
									cellvoltages[1]=highestbatteryvoltage;
									sendmessage(MESSAGECHARGINGCHAR,(unsigned char *)&cellvoltages[0],4);
									}
								}
                     }
			         if (!iamthemaster)
						   { // forward the message to the next processor
                     sendmessage((unsigned char)messagetype,(unsigned char *)&cellvoltages[0],numcells*2);
						   }
					   }
					else // we didn't get a complete message
					   {
					   serialsendstring("?fBad Data");
					   }
							
               // wait .05 seconds and start another reading
               starttimer(GENERALTIMER,500);
					}
				}
			}

      if (iamthemaster)
         { // I AM the master!
			if (getdigitalinput(MODECHANGEINPUT))
			   {
				// wait until the button is released.  If the button is held for 2 seconds, toggle charging mode
				starttimer(GENERALTIMER,20000);
				while (getdigitalinput(MODECHANGEINPUT) && !timerdone(GENERALTIMER));
				
				if (timerdone(GENERALTIMER))
				   { // the button was held for 2 seconds
					chargingmode=!chargingmode;
					setdigitaloutput(CHARGERENABLEOUTPUT,chargingmode); // turn on the charger
					}
				else if (++mode>=NUMMODES) mode=0;
				
				// set the serial port up to write to the display
            setserialoutport(DISPLAYSERIALPORT);
				serialsendstring("?f?c0?<"); // clear the display, set to no cursor, small characters
            while (getdigitalinput(MODECHANGEINPUT)); // make sure he let's go of the button
				if (chargingmode) serialsendstring(" *** Charging ***");
				
				starttimer(GENERALTIMER,500);
				}
				
         if (timerdone(GENERALTIMER))
            { // it's time to trigger a new reading of battery voltages
            setserialoutport(MESSAGESERIALPORT);
			   
				// send a battery voltage message with no data
            sendmessage(MESSAGEBATTERYVOLTAGECHAR,0,0);
				
            starttimer(GENERALTIMER,10000); // time out in 1 seconds
            }

         // read the voltage drop across the shunt and display the current
         }
	   }
   return 0;               // never reached
   }
 
As a former programmer I can say that I really like your coding style. Your naming conventions make sense and you lay out your code very cleanly. It's even documented. It looks good. :)
 
Brad, How much (ballpark range) does it cost per channel? Say I have 16 cell-packs to control, what would be the entire parts costs without labor? I am trying to decide If I go your way or the discrete way... Will you be making your code available? Or at least the compiled file to flash?

Jeff K. "Deep Cycle"
 
Jeff, you are welcome to use any of this info. I'll continue to make the code available. Keep in mind that this is still very experimental.

Per channel, I currently have:

(1) ATmega 48 $1.69
(1) Opto $0.16
(3) Resistors
(1) TIP 142 transistor (I got free)
(1) 4 Ohm, 5W resistor ($.50?)
(1) socket

It's only a few dollars per cell. The display I use is about $40. The perforated board cost me an arm and a leg because I didn't shop around. The programer was $16.

Wiring by hand took me a few full days and I'm still not done.

Safe, thanks for the comments on the code. I've been programming for over 25 years. You learn to comment well after you go back to some old code you wrote 10 years ago and try to figure out what they heck you were thinking.

- Brad
 
Back
Top