Battery Stack Sensor

From Stu2
Jump to: navigation, search
Finished PC Board Layout for the Battery Monitor
Finished and populated board.

This project will use a beaglebone to measure individual cell voltages in a battery string, where the cells are connected in series. Since each battery is not referenced to ground, we need a method of isolating the voltage probe for each cell. A Google search turned up two basic methods. The first was a chip from Linear. This chip is used in hybrid cars to measure and discharge the individual cells of the car's motor battery bank. The second method I found, uses an optical coupler. Turns out, the optical coupler has a linear transfer function when driven between 0 and 8mA. (Although, it does vary with temperature.) The second method seemed more interesting because it was a better project to learn how the beaglebone's I/O works. The beaglebone has A/D converters, Dallas 1-wire bus, plenty of GPIO pins and uses the Linux operating system. (Openembedded)

An optical coupler (PS8501) is connected to each cell. The output voltage of the optical coupler is linear for a specific range of input current. The beaglebone has 7 A/D converters, which will be muxed with an analog switch between 32 cells. A D/A converter with a R-2R ladder will be used as to help calibrate each sensor and the transfer curve will be calculated to produce an absolute voltage reading. The cell voltage range will be 2.25 Volts while charging. 1.75 Volts represents fully discharged. Data will be stored in a round robin database (RRD) and the results will be displayed on a web page. One year's worth of data will be kept at one minute intervals. When the batteries fall below a threshold, e-mail will be sent to the admin.

Cacti will gather data via SNMP, which will provide a nice interface to the data. However, we plan to continue to write the data locally, since we are most interested in the discharge curves when the power goes out. The Cacti server may not be able to collect data during a power outage.

To Do

  • Build and test opitical coupler sensor (1 per cell)
  • Verify opical coupler is linear (Use an equation or lookup table?)
  • Figure out how to measure voltage with beaglebone A/D
  • Figure out how to use GPIO pins
  • Add analog switch to select different cells
  • Move to a 'cape' (breadboard to perfboard)
  • Build D/A converter using R-R2 ladder
  • Draw schematics and block diagrams
  • Set up web server
  • Build a real time web based gauge
  • Set up RRD, create crontab measurement scripts and build web pages to read data
  • Build custom image of Angstrom so we can patch things as necessary and build our own bb files.
  • Add 1 wire temperature sensor
  • Clean up web pages
  • Build calibration routine using D/A
  • Move measurement value to a RAMDISK - just symlinked to /tmp for battery.txt
  • Complete single cell prototype
  • Update code and expand to handle 24 cells
  • Create circuit board layout and make circuit boards - that was a lot of work!
  • Order parts
  • Build circuit boards, test and tweak
  • Build D/A calibration cable/switch
  • Package


  • Beaglebone - setting up the pins, SD cards
  • Embedded Linux - creating a useful image for the beaglebone
  • A/D conversion - Exploring the linearity of the optical coupler
  • D/A conversion - producing accurate calibration voltages
  • Scilab - curve fitting
  • Perl programming
  • Ajax - updating the web page from dynamic files
  • Flash - using flash based gauges
  • RRD - round robin database for storing data and displaying graphs
  • PCB - making PCB boards
  • geda - creating schematics, custom symbols and hierarchical schematics
  • Dallas 1-wire - Measuring the temperature of the ambient air
  • Web server - serving the pages for the user

Building the Beaglebone Cape

Device Tree

The beaglebone provides two 0.1" connectors for connecting a daughter card, or 'cape' in beagle parlance. Using gEDA tools, I created a circuit board using a modular approach, which allows stacking additional boards on the beaglebone. Each board is capable of measuring 7 batteries. The batteries are connected to the board through a DB-9 connector. An eighth sensor is available with it's own connector. The output of the optical couplers are muxed through a 74hc4051 switch. The output of the switch is fed to one of the beaglebone's A/D converter through a jumper. Since there are 7 A/D converters, this method allows for up to seven battery monitoring capes. (49 cells, plus 7 completely isolated sensors.) The boards are interconnected through a wirewrap socket.

Each board has room for a D/A converter, which is built using an R-2R ladder network. The ladder network is in a DIP chip and is connected to 8 GPIO pins. The output of the ladder network feeds a LM328 OP AMP and the output is available on a set of pins. We built a calibration switch, which connects to D/A converter and feeds the DB-9. The switch connects the output of the D/A to one set of pins to emulate a cell. A calibration script runs the D/A converter through a range of voltages (1.5v-2.3v). A script reads the A/D converter value a builds a calibration table using the known input voltage values. Only one D/A converter is populated per set of boards because only one is required for calibrating the set.

The board contains pins for a Dallas 1-wire temperature sensor. The bus is extended through the inter-board connector.

A LED output is available (of course!), which we intend to use as warning light to let the casual observer know they should check the web pages for anomalies. The LED is connected through a NPN driver to provide enough current to light the LED. This could be used as a relay control, too. But only one output per set is available.

Power is provided through the 5VDC beagleboard connector. The cape uses both 3.3 and 5 volt system power. The optical couplers use the 5 volt supply to ensure there is enough current available for a complete stack of boards.

Schematics were generated using gEDA schematic capture software running under Linux. I had to create a few parts, but it's worth the trouble because the resulting 'rats nest' in gEDA's PCB program ensures all the connections are made. PCB was used to build the circuit board. I used the default trace settings, clearances and via sizes. Building the gerber files was a snap. I used GoldPhoenix to manufacture the boards. Cost $100 for 10 boards, which included a solder mask and silkscreening.

Total cost is about $30 per board with parts. The optical couplers cost about $2 each. The beagelebone is $90. So a single sensor costs about $120. Doing the work part time caused me to spend about 5 months working on the project, which includes prototyping, building the boards and writing the software. I don't think this is too bad considering a computer was needed to archive and display the data anyway.

Beaglebone Setup

After many hours, I was able to download the Angstrom distro sources and compile the image plus packages. Wow - that is a big job. Compiling all this stuff took about 14 hours. After I was done compiling, I created my own ipk repository and was able to 'opkg install perl' on the beaglebone. I used lightthd on my local computer and mapped the perl distro directory. I went through all this so I would have control over the Dallas 1-wire driver. Plus, I thought it was an interesting exercise and it might be useful for future projects.

To get the images loaded, see the notes under Beagleboard. They show you how to install the OE image on the SD card. Note, it takes several minutes to boot the first time. After wrestling with the microSD cards, I decided to try putting the filesystem on a USB stick and only use the SD card for booting. This seems solid. On my monitor, I don't store anything on the card or stick.

Use a serial connection for the first time connect. The USB connection provides a serial console connection. /dev/ttyUSB1. Use 'screen /dev/ttyUSB1 115200' and CTL-a k to kill the screen session. Then, shutdown the BB (shutdown now). Connect an Ethernet cable. BB uses zeroconf. Sometimes, ttyUSB0 worked, but most of the time ttyUSB1 was the right device.

ssh beaglebone.local -l root

Adding stuff

To set up your own repository, visit: WillWare. I used this to get locally compiled perl and net-snmp modules. Then I did the following:

  • Fix the repos in /etc/opkg - I added my feed, but left the base and noarch angstrom feeds so I could get some other packages.
  • "opkg update" - I did NOT do an opkg upgrade.
  • Installed these packages from my feed: nano, perl, rrdtool, lighttpd, cronie, ntp, connman-tests, net-snmp-server, ntp-systemd, cronie-systemd, msmtp (bitbaked) - I tried to bitbake as many of these as I could so I would have up to date binaries. Some didn't have source recipes. So I pulled the ones I couldn't compile from the Angstrom site. the -systemd and task-gnome-fonts aren't in my feed. You need base and noarch in /etc/opkg. lighttpd hangs after installation. Not sure why, yet.
  • Installed these from the Angstrom feeds (base/noarch) task-gnome-fonts
  • Once connman-tests is installed, you can set the network address. You may want to do that via a serial cable incase things get screwed up. (see below)
  • Installed perl modules (see next paragraph)
  • Copied files to /home/root and /www/pages/web from local backup.
  • Setup crontab to run once per minute
  • fix the tab in nanorc.
  • Change the hostname to somethign other than beaglebone.
  • Set a root password.

To install 'perl-modules-time-hires', which I need for one of my scripts, I had to go through an iterative approach to installing the right perl modules. Essentially, I ran my script, let it break and added modules. At the end, I actually had to uninstall perl-module-time-hires, install perl-module-time and then fix it, then reinstall perl-module-time-hires. So essentially, the dependencies for this module install seem to be broken. (These are the modules I needed: perl-module-strict,vars, warnings-register, config, carp, time, time-hires, exporter-heavy)

To get net-smtp to work I needed: perl-module-net-smtp, socket, io-socket, io-handle, symbol, selectsaver, io, errno, net-cmd, net-config, io-select.

Running rrdtool the first time gave me a message like: Pango-WARNING **: failed to choose a font, expect ugly output. engine-type='PangoRenderFc', script='common' So after a Google search, I loaded task-gnome-fonts, but needed the angstrom feeds base and noarch.

Here's a script to load all the packages from my feed.

opkg install nano perl rrdtool cronie ntp msmtp net-snmp-server
opkg install connman-tests
opkg install perl-module-vars perl-module-strict
opkg install perl-module-warnings-register perl-module-config
opkg install perl-module-carp perl-module-time
opkg install perl-module-time-hires
opkg install perl-module-exporter-heavy
opkg install perl-module-net-smtp perl-module-socket
opkg install perl-module-io-socket perl-module-io-handle
opkg install perl-module-symbol
opkg install perl-module-selectsaver
opkg install perl-module-io perl-module-errno
opkg install perl-module-net-cmd perl-module-net-config
opkg install perl-module-io-select
opkg install ntp-systemd cronie-systemd task-gnome-fonts

Network Configuration using connman

Setting up the network is different than I'm used to. This uses 'connman' for configuration. Assign address Essentially, install connman-tests and use the scripts to set the address.The 'service' is found ./get-services. It looks like: ethernet_d494a191cd10_cable.

cd /usr/lib/connman/test/
./set-ipv4-method <service> [off|dhcp|manual <address> [netmask] [gateway]]
./set-nameservers <service> [nameserver]

I rebooted after making these settings. If you don't the routing may not be correct. For example, the default route didn't get set right. All is fine after a reboot.

Systemd is pretty cool. It replaces SysV init scripts. When you install Angstrom packages with daemons, grab the -systemd version. (e.g. ntp-systemd)

Key directories:

/usr/lib/connman/test - contains the scripts to set various services.
/lib/systemd/system - contains the service files

ntp on beaglebone

I had a hard time setting the system clock on reboot. After reading all about systemd, I figured it out. Once the network is set up, do this stuff.

  • Link the timezone file. ln -s /usr/share/zoneinfo/EST5EDT /etc/localtime.
  • Edit /etc/ntp.conf to comment out the server and fudge lines.
  • Enable the ntpdate service using systemctl. This will set the clock on reboot.
systemctl enable ntpdate.service

Kill the ntp service and run 'ntpd -q -g -x' manually to see if it works.

Note, ntpdate.service actually calls 'ntpd -q -g -x', which allows ntp to set the clock once and then exits. It uses /etc/ntp.conf. (Comment out the lines about using the loopback.) Using ntpd this way replaces ntpdate, which is why you wont' find ntpdate in the package lists. ;)


opkg install lighttpd

I installed this package last because the install hangs after the process creates the symlink. Don't know why yet. Also, re-untarred the backup because installing lighttpd adds a new index.html file.


1) There is a backport for version 8.8.

2) You need to add soft links to make it work.

More on local repository and SNMP

I built net-snmp this way:

. ~/.oe/environment-oecore
bitbake net-snmp
cd ~/setup-scripts/build/tmp-angstrom_2010_x-eglibc/deploy/ipk/armv7a

To udpate the Packages files, remove the Packages files in the directory and run "bitbake package-index". (I did this from the setup-scripts directory and wrapped with

My lighttpd configuration on the server points a subirectory to the .../ipk/armv7a directory (same path as just above). So you need to add the subdirectory to the URL in the opkg configuration file on the beagleboard.

src/gz mystuff

Update the packages: opkg update and you should be ready to go with opkg install net-snmp-server. Google net-snmp for configuration instructions. This link was helpful because it has a tutorial about setting up the configuration file and adding your own SNMP variables. (Which is the whole point for this project. I want to grab the battery voltages from the device using cacti.)

I used the 'extend' keyword in snmpd.conf to execute an external command, which allows an snmp agent to get the output of the /tmp/snmp.dat file. /tmp.snmp.dat holds the date, temp and battery voltages. The OIDs are available at: For the output, I used:

In /etc/snmp/snmpd.conf:
extend batdata /bin/cat /tmp/snmp.dat

While you are in snmpd.conf, fix the access permissions. If you don't, when you do an snmp query, you'll get 'MIB END' (or something like that) and none of the values return. Also, add the following to make the daemon listen on tcp. You can only send TCP over ssh.

agentaddress tcp:161

If all goes well:

snmpwalk -v 1 -c public tcp: iso.

iso. = STRING: "Sun Aug 12 13:11:03 EDT 2012"
iso. = STRING: "77.9"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.34"
iso. = STRING: "2.24"

Note that I set up ssh (using autossh) to map a port on the local cacti server to do a TCP snmp query over the tunnel. All of the tunnel, host and user details are set up in ~/.ssh/config so the autoshell comand line is simple.

autossh -M 20000 -f -N -q host

I needed to create the snmpd.service file because the bare bones bone uses systemd, not sysvinit scripts. This file goes in /etc/systemd/system/snmpd.service. Enable the service 'systemctl enable snmpd.service' and check status 'systemctl status snmpd.service'.


ExecStart=/usr/sbin/snmpd -p /var/run/
ExecReload=/bin/kill -HUP $MAINPID



syslog.busybox is installed by default. /etc/syslog-startup.conf is the configuration file. I had to 'touch /var/log/messages' to get the daemon to work. Edit the conf file to set the rotation limits or perhaps forward the syslog stuff to a remote computer. For my readonly version, I didn't touch this file for no syslog.


Just a running list of things to fix

  • After running for several weeks, I had a problem with the microSD card. It started taking errors. I don't know if this was due to the 100+ degree temps in my garage or if simply 'pulling the plug' on the beaglebone was the culprit.
  • So warning - connect to the BB and issue a shutdown command before unplugging the device. This will require more research.
  • Temperature variability - Now that it's summer time, the battery monitor is warm. This changes the battery monitoring calibration.
  • Some of the old scripts had the wrong path to the AIN files. The symptom was 'can't find ain2'. Check and make sure the path is:
open(A,"cat /sys/devices/platform/omap/tsc/ain$ADC|");


I put all the perl scripts, rrd databases, webpages, etc into a tar file. Copy the tar file over and untar it in the root directory. Copy ~/ to the perl library directory (/usr/lib/perl/5.14.2). Run /home/root/ to initialize all the A/D pins. Execute ./measure to make sure everything works, then update the crontab file.

* * * * * /home/root/measure

Note - the crontab is stored in /var/log/cron. So don't make this directory temp, unless you copy the crontab file (root) to the temp directory on start up.

utils/rrdsetup - bash script to set up rrd databases in /data/
utils/ - sets the D/A convert to a specific voltage, e.g. 2.00
utils/ - reads the A/D converter for a certain board. e.g. 1
utils/ - sets the address on the switch. e.g. 1
utils/ - build the calibration file. Requires the calibration switch. 1 - call to set up GPIO pins.
measure - call this from cron every minute. Bash file to measure the battery and temp
pack - shell script to build a single file with all the data. Manually edit to add boards. - measures the battery voltages and adds to rrd db. [board#] - measure the temp sensor. Manually add the device found in /sys/bus/w1/devices/

DALLAS 1-wire

I was after the 1-wire patch. It's now included in the kernel, but it's not on the same pin as the web reference. Use 'dmesg | grep w1-gpio' to see if the driver is loaded. If it is, you'll see something like:

dmesg | grep w1-gpio
[    1.016526] w1-gpio connected to P8_6

Which says connected to P8 pin 6.

I'm using pin 6 for my D/A converter, so I moved my D/A converter P8 pin 8 so I can use the 1-wire bus. If the 1 wire device is found, you will see:

[email protected]:/sys/bus/w1/devices/28-000001284447# ls /sys/bus/w1/devices
28-000001284447  w1_bus_master1

[email protected]:/sys/bus/w1/devices/28-000001284447# cat /sys/bus/w1/devices/28-000001284447/w1_slave
d0 01 4b 46 7f ff 10 10 db : crc=db YES
d0 01 4b 46 7f ff 10 10 db t=29000

The temperature is t=X/1000 in degrees Celsius.

Scilab tips

I used scilab to calculate the transfer curve. The curve was linear between 0 and 8 mA of input current to the optical coupler. The slope and y-intercept are fed into the perl script on the beaglebone manually. After this rabbit hole, I decided to use a D/A converter to provide known voltages to the optical coupler. Then read the AD value and create a lookup table. This means we don't really care if the transfer curve is linear. However, it looks like the sensor may be temperature sensitive. After I gather more data, I can revisit this idea.

// read the file
-->fid = file('open','/home/stu/Projects/VoltageMonitor/ps8501.dat','unknown')
-->data = read(fid, -1,2)
// change the column vector to a row vector
// find line

Used function available in sciab08.pdf to generate and plot.

-->exec('/home/stu/linreg.sci')  - This loads a function
-->genlib("mylib","/home/stu/")  - This adds all the functions (.sci) macros into a library)
-->mylib                         - uses the library
// use the function linreg

Reading the AD Converter

To read the AD converter, I had to update to the latest image on the beaglebone. The ainx devices didn't exist on the demo image. Here is a small perl script to read the value of ain1 and write it to a file in /www/pages, which can be read through some ajax magic. I used a guage from Bindows. $m and $b belong to the equation y=mx+b and are calculated elsewhere to fit the A/D values to a real world measured voltage.



while(1) {

open(A,"cat /sys/devices/platform/omap/tsc/ain1|");
$ain1 = <A>;

$x = ($ain1 - $b) / $m;

$x = sprintf '%.2f',$x;
print FH "$x\n";
print "$x\r";

sleep 1

/www/pages/cache is a symbolic link to /tmp. This means the data is written to RAM instead of the disk, which keeps the disk from wearing out.

The beaglebone driver included with Angstrom doesn't match the manual regarding the a/d pins on header P9. In /sys/devices/platform/omap/tsc/ there are eight ainX files, which are used to read the a/d values. As far as I can tell, there are only 7 A/D converter pins available on P9. So only ain1-ain7 are valid function identifiers. The table in the manual identifies the pin-function mapping as ain0-ain6. I reported this to the author of the manual. Note, the tsc directory changed from the winter time. It was in /sys/devices/platform/tsc. Now, it is in /sys/devices/platform/omap/tsc. The correct mapping is shown below.

P9 Pin  Function
33      ain5
35      ain7
36      ain6
37      ain3
38      ain4
39      ain2
40      ain1

Writing to the GPIO Pins

Here is a perl script to write to the GPIO pins. Get a copy of the beaglebone reference manual. The GPIO pins are exposed on headers P8 and P9. I started with this reference: It has examples in python, but I already started working in Perl.

The general idea is to do the following:

  • Identify the muxlabel - this is the mode 0 name (e.g. mcasp0_fsr for P9 Pin 27)
  • Identify the GPIO label - this is the mode 7 name (e.g. gpio3[19])
  • Calculate the gpio pin number. The IO value comes from the square brackets. e.g. if gpio3[19], the IO value=19. The pin # is 96 + 19 = 115. See the code below.
  • Devise the pin name which cooresponds to 'gpio' concatenated with the pin #. e.g. 'gpio115. Eventually, this will identify a directory under /sys/class/gpio/ e.g. /sys/class/gpio/gpio115/

Armed with that info:

  • Check for /sys/kernel/debug/omap_mux/$muxlabel - if it exists, set the mode to mode 7. If it doesn't, you may have the wrong mode0/muxlabel name.
  • Check if the pin files exists by looking for the 'direction' file under /sys/class/gpio/$gpioName/ where $gpioName is "gpio" concatenated with the gpio pin number from above. For example, check for /sys/class/gpio/gpio115/direction.
    • If it exists, great - move on
    • If it does NOT exists, export the pin by writing the pin number to /sys/class/gpio/export. This will populate the director /sys/class/gpio/gpio115/ if the pin number is 115.
  • Set the pin direction (in or out) by writing to /sys/class/gpio/$gpioName/direction
  • Set the pin value (1 or 0 = On or Of) by writing to /sys/class/gpio/$gpioName/value

The following code sets up a pin as mode 7, output and toggles between on and off. Note the backticks.


# To change the pin, you have to set the
# muxlabel and calculate the gpio pin #

# set muxlabel, which is mode 0 name.

# set gpio pin number
# Use this to figure out the pin #
#   var gpio0 = 0;
#   var gpio1 = gpio0+32; -> 32 + IO
#   var gpio2 = gpio1+32; -> 64 + IO
#   var gpio3 = gpio2+32; -> 96 + IO
# For example, P8_pin3 is gpio1[6] (under mode 7 column)
# so gpio is 32 + 6 = 38
$gpio = "115";

# Create the pin name
$gpioName = "gpio".$gpio;

#Set the pin mode to mode 7. Use the mode 0 name (muxlabel) from the manual
 or die "Can't open muxlable $muxlabel\n";
printf PIN "%X", 7;

# Check for the pin filename, if doesn't exist, export it.
unless(-e "/sys/class/gpio/$gpioName/direction")
        print "Exporting Pin\n";
        system("echo $gpio > /sys/class/gpio/export");


`echo "out" > /sys/class/gpio/$gpioName/direction`;

# toggle the pin on and off
while() {
`echo "1" > /sys/class/gpio/$gpioName/value`;
sleep 1;
`echo "0" > /sys/class/gpio/$gpioName/value`;
sleep 1;

Maximum current per GPIO pin is 6mA. For the external status LED, I used a NPN transistor switch and the 5VDC power supply. LED is on P8-28, lcd_pclk, gpio2[24], 88.

D/A Converter

I built a R-2R 8 bit D/A converter to use a voltage source to calibrate the A/D converter systems. A script steps through a series of voltages, the A/D converter reads the value and I create a lookup table for that particular cell sensor. R=10K and 2R=20K. This connects to P8 on the Beaglebone.The output is provided through a two pin header, where a cable with a switch connects. The switch applies the voltage to the proper pins on the DB-9 input connector. Calibration only needs to be done once per sensor. I kept the D/A converter on the PC board in case that isn't true and to make controlling the voltage source a little more automatic. In a multi board installation, only the bottom board will have this area populated on the PCB board. The calibration values are stored in a lookup table. Absolute voltage readings are interpolated using a linear function defined by the closest calibration values.

The pins are:

Pin   Bit  Signal         GPIO       Offset
3     0    gpmc_ad6       gpio1[6]   38
5     1    gpmc_ad2       gpio1[2]   34
7     2    gpmc_advn_ale  gpio2[2]   66
8     3    gpmc_oen_ren   gpio2[3]   67
11    4    gpmc_ad13      gpio1[13]  45
12    5    gpmc_ad12      gpio1[12]  44
14    6    gpmc_ad10      gpio0[26]  26
13    7    gpmc_ad9       gpio0[23]  23

The D/A converter is followed by a LM358 OP AMP, wired as a voltage follower. It can provide up to 10mA of current, which is limited by the OP AMP. This idea came from this URL:

Note the pins aren't in order. The Dallas 1-wire connection uses Pin 6 of P8. I spent a tremendous amount of time compiling OE so I could enable the 1-wire driver. When I was done compiling, I discovered the 1-wire pin was hard coded to Pin6. My original design needed to change slightly. I'm sure it's possible to change the pin in the OE configuration, but that would require re-compiling the image. My guess is someday this will be userland configuration much like setting up the beagelbone I/O pins.


A 8:1 switch is used to select any one of 8 sensors and direct it to one of the A/D converters pins through a jumper. The address lines are connected to each module.

P9 muxlabel       gpio function
23  gpmc_a1       49   A LSB
25  mcasp0_ahclkx 117  B
27  mcasp0_fsr    115  C MSB
15  gpmc_a0       48   Inhibit

DB-9 Connector Pins

The battery cells are connected to DB-9 connectors.

pin   Function
1     GND
2     Cell 1
3     Cell 2
4     Cell 3
5     Cell 4
6     Cell 5
7     Cell 6
8     Cell 7
9     Cell 8