Reading the BMP180 pressure sensor with an Attiny85

IMG_20160210_232932The BMP180 pressure sensor is a relatively cheap and popular sensor to read atmospheric pressure. Additionally it can read temperature.
If you want to use this sensor on an arduino, Adafruit has a library (for the BMP085 and the BMP180) that wil read it for you. However, the new library also needs their general ‘Sensor library’ and those are memory guzzlers. Perhaps OK on an Arduino, but not on an attiny. They do have one for the Tiny85 as well. Sparkfun also has a library  for the Arduino.
Furthermore, the BMP180 is an I2C device and I2C is not a standard on the Attiny series.

So, if you want to read the BMP180 sensor on an attiny, you would need to do some work yourself.
Fortunately, the datasheet is very very clear. Page 15 tells us exactly what to do.
The sequence is as follows:
1-Read the chip specific calibration data
2-Read the uncorrected temparature value
3-Read the uncorrected pressure value
4-Calculate true temperature
5-calculate true pressure

It also shows you what should be in a loop and what not:
reading the calibration data only needs to be done once and therefore goes in the ‘Setup’ routine. The rest is in a loop and therefore goes in the ‘loop’ routine.

So, programming is a breeze if you follow the flow chart on page 15…. we only need to ‘translate’ that into language the I2C protocol understands.
We therefore start the program with defining some general parameters:
For the Attiny there is the TinyWireM library that implements an I2C protocol on the attiny, so we need to load that library.
We need the I2C address of the BMP180 (which is 0x77), and we need to declare a whole bunch of variables. Most of the variables used will contain the chip specific calibration data that we will be reading from the chip’s EEPROM, we will need some variables for the various calculations and we will need some variables to contain the output (temperature and pressure)
To keep it easy, I have chosen names for the variables as mentioned in the datasheet.

So, the first lines of a program will look like this:

//The connection for  Attiny & BMP180 are  SDA pin 5 ,SCL pin 7 for I2C 
#include  <TinyWireM.h>
#define BMP180_ADDRESS 0x77  // I2C address of BMP180   
// define calibration data for temperature:
int ac1;
int ac2; 
int ac3; 
unsigned int ac4;
unsigned int ac5;
unsigned int ac6;
int b1; 
int b2;
int mb;
int mc;
int md;
long b5; 
//define variables for pressure and temperature calculation
long x1,x2;
//define variables for pressure calculation
long x3,b3,b6,p;
unsigned long b4,b7;
//define variables for temperature and pressure reading

short temperature;
long pressure;
const unsigned char OSS = 0;  // Oversampling Setting
/* blz 12 Datasheet
OSS=0 ultra Low Power Setting, 1 sample, 4.5 ms 3uA
OSS=1 Standard Power Setting, 2 samples, 7.5 ms 5uA
OSS=2 High Resolution,              4 samples, 13.5 ms 7uA
OSS=3 Ultra High Resolution,    2 samples, 25.5 ms 12uA
*/

Then we have to define the ‘Setup’ routine. Frankly, the only thing we have to do there is read the calibration data. To keep it simple, i will just call a procedure ‘bmp180ReadInt(address)’, which we then can implement later.
Our Setup therefore will look like this:

void setup() {
  TinyWireM.begin();
  // First read calibration data from EEPROM
  ac1 = bmp180ReadInt(0xAA);
  ac2 = bmp180ReadInt(0xAC);
  ac3 = bmp180ReadInt(0xAE);
  ac4 = bmp180ReadInt(0xB0);
  ac5 = bmp180ReadInt(0xB2);
  ac6 = bmp180ReadInt(0xB4);
  b1 = bmp180ReadInt(0xB6);
  b2 = bmp180ReadInt(0xB8);
  mb = bmp180ReadInt(0xBA);
  mc = bmp180ReadInt(0xBC);
  md = bmp180ReadInt(0xBE);

}

Ofcourse I could have just called 1 procedure and call that ‘bmp180ReadCalibration’ but that procedure then would do the same as I now defined already in the setup

The ‘loop’ procedure is equally simple. It is basically
Read uncorrected temperature
Correct that uncorrected temperature
Read uncorrected pressure
Correct that uncorrected pressure
But as no one is interested in the uncorrected data, we make that procedure:
Correct(Read Uncorrected temperature)
Correct(Read Uncorrected pressure)
like this:

void loop() {
 // first, read uncompensated temperature
 //temperature = bmp180ReadUT();
 //and then calculate calibrated temperature
 temperature = bmp180CorrectTemperature(bmp180ReadUT());
 // then , read uncompensated pressure
 //pressure = bmp180ReadUP();
 //and then calculate calibrated pressure
 pressure = bmp180CorrectPressure(bmp180ReadUP());
 
}

So that is it. We now only have to define the procedures that we call.
We will start with ‘bmp180ReadInt(address)’
This procedure will use the TinyWireM library to read an integer from a given address. In getting data from an I2C device, the general rule is to first write to that device to tell it what to do and then to read at a specific address for the outcome. As we will be reading from the EEPROM there is no specific command we have to send, other than to notify the I2C port where we want to be (at the I2C address of the chip) and send the address we want to read and how many bytes we want to read. We then combine those two butes in an integer and return that.
Our precedure will thus look like this:

int bmp180ReadInt(unsigned char address)
{
  unsigned char msb, lsb;
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(address);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BMP180_ADDRESS, 2);
  while(TinyWireM.available()<2);
  msb = TinyWireM.receive();
  lsb = TinyWireM.receive();
  return (int) msb<<8 | lsb;
}

The next procedure we need is to read the uncompensated temperature. To get that we have to first send the value of 0x2E to register 0xF4 and wait at least 4.5 msec. That is the time the chip needs to take 1 reading. After we waited we will read the uncompensated temperature from registers 0xF6 and 0xf7. That last read we do with the earlier defined ‘bmp180ReadInt’ procedure that reads 2 bytes and combines them into an integer.
The procedure thus will look like this:

unsigned int bmp180ReadUT()
{
  unsigned int ut;
  
  // Write 0x2E into Register 0xF4 and wait at least 4.5mS
  // This requests a temperature reading 
  // with results in 0xF6 and 0xF7
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF4);
  TinyWireM.send(0x2E);
  TinyWireM.endTransmission();
  
  // Wait at least 4.5ms
  delay(5);
  
  // Then read two bytes from registers 0xF6 (MSB) and 0xF7 (LSB)
  // and combine as unsigned integer
  ut = bmp180ReadInt(0xF6);
  return ut;
}

Subsequently we have to calculate the corrected temperature from the uncorrected temperature.
The datasheet defines that as follows:
UT=uncompensated temperature
X1=(UT-AC6)*AC5/2^15
X2=(MC * 2^11 /(X1+MD)
B5=X1+X2
T=(B5+8)/2^4
in software that looks like this

double bmp180CorrectTemperature(unsigned int ut)
{
  x1 = (((long)ut - (long)ac6)*(long)ac5) >> 15;
  x2 = ((long)mc << 11)/(x1 + md);  
  b5 = x1 + x2; 
  return (((b5 + 8)>>4));  
}

Well the temperature is done, now we need to read the uncompensated pressure. For that we need to write the value 0x34 in the register 0xF4, but we also have to set the value vor the oversampling rate.
The oversampling rate determines the amount of samples the chip needs to make before giving a result.
Page 4 of the datasheet tells we have 4 choices:
OSS=0 ultra Low Power Setting, 1 sample, 4.5 ms 3uA
OSS=1 Standard Power Setting, 2 samples, 7.5 ms 5uA
OSS=2 High Resolution, 4 samples, 13.5 ms 7uA
OSS=3 Ultra High Resolution, 12 samples, 25.5 ms 12uA
For this program I have chosen the OSS to be 0
The OSS contains bits 6 and 7 in register 0xF4. Bit 0-4 determine the control of the measurement.
if we write the value 0x34 that is in binary: 00110100. Bits 0 to 4 are not so important for now, but bit 5 will also be set and thus start the conversion. It will stay high during the conversion and reset to LOW after the conversion. In order to set the bits 6 and or 7 we have to left shift 6 the value of OSS. Suppose we had wanted to set OSS as 3. in binary that is 0b11 if we left shift 6 that, it will be 11000000 (=192d or 0xC0), which will set bits 6 and 7. 0x34+0xC0=0xF4=0b11110100 which as we can see is the same as 0x34 plus bit 6 and 7 set.
As we are using ‘0’ for the OSS value, both bit 6 and 7 will not be set.
after we start the conversion we have to wait between 4.5 and 25.5 msecs (depending on OSS). As we have OSS=0 we will wait 5msec.
Subsequently we will read 3 bytes as the temperature is a ‘long’ (4 bytes) not an integer, we will however only need 3 bytes.
With regard to the delay, it would be nice if we will define it as a dependency of the OSS so you do not need to manually change it when you change the OSS. The Adafruit library solevs this with some IF statements:
if (oversampling == BMP085_ULTRALOWPOWER)
delay(5);
else if (oversampling == BMP085_STANDARD)
delay(8);
else if (oversampling == BMP085_HIGHRES)
delay(14);
else
delay(26);
However, I hoped to find a formula that will determine it. As it isn’t a strict linear function, the closest one gets is the formula: 5+(OSS*5).
OSS=0->5
OSS=1->10
OSS=2->15
OSS=3->25
Well, I guess that would be close enough
The procedure is as follows

/-------------------------------------------
// Read the uncompensated pressure value
unsigned long bmp180ReadUP()
{
  unsigned char msb, lsb, xlsb;
  unsigned long up = 0;
  
  // Write 0x34+(OSS<<6) into register 0xF4
  // Request a pressure reading w/ oversampling setting
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF4);
  TinyWireM.send(0x34 + (OSS<<6));
  TinyWireM.endTransmission();
  
  // Wait for conversion, delay time dependent on OSS
  delay(5 + (5*OSS));
  
  // Read register 0xF6 (MSB), 0xF7 (LSB), and 0xF8 (XLSB)
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF6);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BMP180_ADDRESS, 3);
  
  // Wait for data to become available
  while(TinyWireM.available() < 3)
    ;
  msb = TinyWireM.receive();
  lsb = TinyWireM.receive();
  xlsb = TinyWireM.receive();
  
  up = (((unsigned long) msb << 16) | ((unsigned long) lsb << 8) | (unsigned long) xlsb) >> (8-OSS);
  
  return up;
}

Now that is done, we need to correct the uncompensated pressure. The result will be in Pascal

double bmp180CorrectPressure(unsigned long up)
{ 
  b6 = b5 - 4000;
  // Calculate B3
  x1 = (b2 * (b6 * b6)>>12)>>11;
  x2 = (ac2 * b6)>>11;
  x3 = x1 + x2;
  b3 = (((((long)ac1)*4 + x3)<<OSS) + 2)>>2;
  
  // Calculate B4
  x1 = (ac3 * b6)>>13;
  x2 = (b1 * ((b6 * b6)>>12))>>16;
  x3 = ((x1 + x2) + 2)>>2;
  b4 = (ac4 * (unsigned long)(x3 + 32768))>>15;
  
  b7 = ((unsigned long)(up - b3) * (50000>>OSS));
  if (b7 < 0x80000000)
    p = (b7<<1)/b4;
  else
  p = (b7/b4)<<1;   
  x1 = (p>>8) * (p>>8);
  x1 = (x1 * 3038)>>16;
  x2 = (-7357 * p)>>16;
  p += (x1 + x2 + 3791)>>4;
  
  return p;
}


With the above program one can decide for oneself what to do with the found data: either  send it to a display, or perhaps send it via an RF link to a base station.
As said, the output of the pressure reading is in Pascal (Pa). hectoPascals are a more convenient unit. Some other units it can be  calculated in are:
1 hPa = 100 Pa = 1 mbar = 0.001 bar
1 hPa = 0.75006168 Torr
1 hPa = 0.01450377 psi (pounds per square inch)
1 hPa = 0.02953337 inHg (inches of mercury)
1 hpa = 0.00098692 atm (standard atmospheres)

One last advice still:  When you use the BMP180, remember it needs 3.3 Volt. 5 Volt will kill it. Using it on the I2C from a 5 Volt microcontroller shouldnot cause a problem though. Various break outboards actually do have a  3.3 Voltage regulator on it.

Warning
When I wanted to display the values found by the BMP180, I  grabbed a two wire LCD interface that I ahd build with a 164 Shift Register. I subsequently tried to figure out for several hours why I wasnt getting any decent read out. In fact, the read out didnt change wether I connected the BMP180 or not. After many many trials I started to suspect my display interface and decided to  hook up an I2C LCD. That worked like a charm.
The LiquidCrystal_I2C from Francisco Malpertida doesn’t work on the Attiny85. I used the classic LiquidCrystal_I2C that is adapted by Bro Hogan to work on the Attiny85 as well.
He did that by changing the line:

#include <Wire.h>
into
#if defined(__AVR_ATtiny85__) || (__AVR_ATtiny2313__)
#include "TinyWireM.h"      // include this if ATtiny85 or ATtiny2313
#else
#include <Wire.h>           // original lib include
#endif

Advertisements

Servo on Attiny13

Wanted to use an Attiny13 to sweep a Servo. There is an 8 bits Servo library for the Attiny85 series, that will compile after  some changes (TIMSK->TIMSK0 and TIFR->TIFR0) but that will be over the memory limit of the Attiny13, some other programs I found that were playing directly with the timing registers didn’t really work and one even broke the gears on my servo, so I decided to just make  my own program, which is in fact much simpler than I thought once I started:

byte s=0;
void setup() {
pinMode(s, OUTPUT);

}

void loop() {
for (byte pos=0;pos<180;pos++)
{
pulseOut(s,pos);
delay(20);
}
}

void pulseOut( byte pin, byte p){
digitalWrite(pin,HIGH);
delayMicroseconds(300+p*(2500/180));
digitalWrite(pin,LOW);
//Serial.println(p*(1000/180));
}

The line “delayMicroseconds(300+p*(2500/180));” needs a bit of explanation:
“p” is the position in degrees, so if “p” is ‘0’, the pulsewidht for 0 degrees is sent to the server. In this case that is 300 uS. if p=180, the value for a full sweep is sent to the servo. In this case that is 2800uS.The values of 300 and 2500 are experimental: I started out with 1000 and 1000 but then the servo turned only some 90 degrees. Some programs for Attiny85 mention 60 and 4000uS as the min and max values for the pulse width
As  the values are  experimental, it doesn’t really matter if the  timing of the delayMicroseconds is off a bit on the Attiny13 core, but they seem OK for a UNO as well.
Though only a few lines, it still takes 540 bytes on the Attiny13.
True, due to the use of ‘delay’ the processor will mainly be ‘waiting’. Ofcourse one can solve this by using millis, or by setting up a timer, but it is an Attiny13…. there really isnt so much memory space to cramp a lot of other programming in it anyway, so sweeping the servo is the core  function.

High Voltage programming/Unbricking for Attiny

Yikes! Invalid device signature

It sometimes happens: you are trying to program an Attiny and you get an error. There might be many causes for errors: you may not have selected the proper board, your programmer might be rotten, you did something wrong or the connections of your programmer are not correct, but sometimes it can be caused by the wrong bits being set in your Attiny: e.g. you set pin 1 (reset) to be an I/O pin. That makes it impossible to program it with ISP, or you set the wrong oscillator. For these cases a High Voltage programmer can be of help.
It happened to me when i was trying to ‘burn the bootloader’ on my attiny85 (there is no bootlaoder for the attiny 85 but this means setting the fuses). My  computer’s memory was kinda full when I  was busy and suddenly something crashed and I got the dreaded error message Yikes! Invalid device signature. As said, often this is because of a bad connection, but another chip didnt have that problem so I knew something was very wrong.
Time to build a High Voltage Programmer
Below you will see such a circuit. It is fairly simple: 6 resistors, a transistor  a DIL foot  and a 12 Volt source from somewhere. As I  didn’t expect to have to use the HVP often, I opted for a battery, but as I displaced it, I ended up using a 75 euroct 5 to 12 Volt converter that I plugged in.

Circuit
Circuit

 

 

 

 

 

 

 

 

 

And this is an easy way to build it on stripboard.

High Voltage programmer Stripboard
On Stripboard
The finished board ready to use
The finished board

 

 

 

 

 

 

 

I have built it such that it sticks into the D8-D13,ground header of an Arduino UNO. The only thing  you need to do is to attach a 12 Volt battery, or an other 12 Volt source.
There are various programs that can reset the bits back to factory setting. Below you will find 2 of them that all  go back to some initial work by Jos Keyzer.
The first one expects you to  set the  factory bits in the program depending on the chip you are using, the 2nd program actually reads what chip you are using, so I ended up using that one. Both programs  start after you send a random character to the serial port.

 

 

 

 

 

 

 

 

 

fuseburningWell, as it turned out, my fuses were set for E4 and DF. That means that the Attiny was expecting a 128 kHz oscillator signal. No idea how that happened as I have disabled that choice in my menu so I guess it happened coz of my computer crashing. We will never know, but the HVP set it back to factory settings: i.e. 8MHz internal oscillator with a prescaler of 8.
After that, I could just program my Attiny again.

In principle this programmer can be used for Attiny 15 and 12 as well, but as far as I recall they have some wires crossed, so you would need to make a hardware change (connect the D12 resistor to pin 3 instead of pin2), but Attiny 13/25/45/85 should work like a charm.
It can also be done on the 24/44/84 series, but they need a 14 pins DIL:
fusebit

 

 

 

5 to 12 Volt converterShould you need a cheap 12 Volt source, consider this 5 to 12 Volt converter

 

 

 

 

 

Program:

// AVR High-voltage Serial Programmer
// Originally created by Paul Willoughby 03/20/2010
// http://www.rickety.us/2010/03/arduino-avr-high-voltage-serial-programmer/
// Inspired by Jeff Keyzer http://mightyohm.com
// Serial Programming routines from ATtiny25/45/85 datasheet

// Desired fuse configuration
#define  HFUSE  0xDF   // Defaults for ATtiny25/45/85
#define  LFUSE  0x62 
// For Attiny13 use
// #define HFUSE 0xFF
// #define LFUSE 0x6A  


#define  RST     13    // Output to level shifter for !RESET from transistor to Pin 1
#define  CLKOUT  12    // Connect to Serial Clock Input (SCI) Pin 2
#define  DATAIN  11    // Connect to Serial Data Output (SDO) Pin 7
#define  INSTOUT 10    // Connect to Serial Instruction Input (SII) Pin 6
#define  DATAOUT  9    // Connect to Serial Data Input (SDI) Pin 5 
#define  VCC      8    // Connect to VCC Pin 8

int inByte = 0;         // incoming serial byte Computer
int inData = 0;         // incoming serial byte AVR

void setup()
{
  // Set up control lines for HV parallel programming
  pinMode(VCC, OUTPUT);
  pinMode(RST, OUTPUT);
  pinMode(DATAOUT, OUTPUT);
  pinMode(INSTOUT, OUTPUT);
  pinMode(CLKOUT, OUTPUT);
  pinMode(DATAIN, OUTPUT);  // configured as input when in programming mode
  
  // Initialize output pins as needed
  digitalWrite(RST, HIGH);  // Level shifter is inverting, this shuts off 12V
  
  // start serial port at 9600 bps:
  Serial.begin(9600);
  
  establishContact();  // send a byte to establish contact until receiver responds 
  
}


void loop()
{
  // if we get a valid byte, run:
  if (Serial.available() > 0) {
    // get incoming byte:
    inByte = Serial.read();
    Serial.println(byte(inByte));
    Serial.println("Entering programming Mode\n");

    // Initialize pins to enter programming mode
    pinMode(DATAIN, OUTPUT);  //Temporary
    digitalWrite(DATAOUT, LOW);
    digitalWrite(INSTOUT, LOW);
    digitalWrite(DATAIN, LOW);
    digitalWrite(RST, HIGH);  // Level shifter is inverting, this shuts off 12V
    
    // Enter High-voltage Serial programming mode
    digitalWrite(VCC, HIGH);  // Apply VCC to start programming process
    delayMicroseconds(20);
    digitalWrite(RST, LOW);   //Turn on 12v
    delayMicroseconds(10);
    pinMode(DATAIN, INPUT);   //Release DATAIN
    delayMicroseconds(300);
    
    //Programming mode
    
    readFuses();
    
    //Write hfuse
    Serial.println("Writing hfuse");
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x40, 0x4C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, HFUSE, 0x2C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x74);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x7C);
    
    //Write lfuse
    Serial.println("Writing lfuse\n");
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x40, 0x4C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, LFUSE, 0x2C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x64);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x6C);

    readFuses();    
    
    Serial.println("Exiting programming Mode\n");
    digitalWrite(CLKOUT, LOW);
    digitalWrite(VCC, LOW);
    digitalWrite(RST, HIGH);   //Turn off 12v
  }
}


void establishContact() {
  while (Serial.available() <= 0) {
    Serial.println("Enter a character to continue");   // send an initial string
    delay(1000);
  }
}

int shiftOut2(uint8_t dataPin, uint8_t dataPin1, uint8_t clockPin, uint8_t bitOrder, byte val, byte val1)
{
	int i;
        int inBits = 0;
        //Wait until DATAIN goes high
        while (!digitalRead(DATAIN));
        
        //Start bit
        digitalWrite(DATAOUT, LOW);
        digitalWrite(INSTOUT, LOW);
        digitalWrite(clockPin, HIGH);
  	digitalWrite(clockPin, LOW);
        
	for (i = 0; i < 8; i++)  {
                
		if (bitOrder == LSBFIRST) {
			digitalWrite(dataPin, !!(val & (1 << i)));
                        digitalWrite(dataPin1, !!(val1 & (1 << i)));
                }
		else {
			digitalWrite(dataPin, !!(val & (1 << (7 - i))));
                        digitalWrite(dataPin1, !!(val1 & (1 << (7 - i))));
                }
                inBits <<=1;
                inBits |= digitalRead(DATAIN);
                digitalWrite(clockPin, HIGH);
		digitalWrite(clockPin, LOW);
                
	}

        
        //End bits
        digitalWrite(DATAOUT, LOW);
        digitalWrite(INSTOUT, LOW);
        digitalWrite(clockPin, HIGH);
        digitalWrite(clockPin, LOW);
        digitalWrite(clockPin, HIGH);
        digitalWrite(clockPin, LOW);
        
        return inBits;
}

void readFuses(){
     //Read lfuse
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x04, 0x4C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x68);
    inData = shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x6C);
    Serial.print("lfuse reads as ");
    Serial.println(inData, HEX);
    
    //Read hfuse
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x04, 0x4C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x7A);
    inData = shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x7E);
    Serial.print("hfuse reads as ");
    Serial.println(inData, HEX);
    
    //Read efuse
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x04, 0x4C);
    shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x6A);
    inData = shiftOut2(DATAOUT, INSTOUT, CLKOUT, MSBFIRST, 0x00, 0x6E);
    Serial.print("efuse reads as ");
    Serial.println(inData, HEX);
    Serial.println(); 
}

An other program is:

    // AVR High-voltage Serial Fuse Reprogrammer
    // Adapted from code and design by Paul Willoughby 03/20/2010
    // http://www.rickety.us/2010/03/arduino-avr-high-voltage-serial-programmer/
    // Fuse Calc:
    //   http://www.engbedded.com/fusecalc/

    #define  RST     13    // Output to level shifter for !RESET from transistor
    #define  SCI     12    // Target Clock Input
    #define  SDO     11    // Target Data Output
    #define  SII     10    // Target Instruction Input
    #define  SDI      9    // Target Data Input
    #define  VCC      8    // Target VCC

    #define  HFUSE  0x747C
    #define  LFUSE  0x646C
    #define  EFUSE  0x666E

    // Define ATTiny series signatures
    #define  ATTINY13   0x9007  // L: 0x6A, H: 0xFF             8 pin
    #define  ATTINY24   0x910B  // L: 0x62, H: 0xDF, E: 0xFF   14 pin
    #define  ATTINY25   0x9108  // L: 0x62, H: 0xDF, E: 0xFF    8 pin
    #define  ATTINY44   0x9207  // L: 0x62, H: 0xDF, E: 0xFFF  14 pin
    #define  ATTINY45   0x9206  // L: 0x62, H: 0xDF, E: 0xFF    8 pin
    #define  ATTINY84   0x930C  // L: 0x62, H: 0xDF, E: 0xFFF  14 pin
    #define  ATTINY85   0x930B  // L: 0x62, H: 0xDF, E: 0xFF    8 pin

    void setup() {
      pinMode(VCC, OUTPUT);
      pinMode(RST, OUTPUT);
      pinMode(SDI, OUTPUT);
      pinMode(SII, OUTPUT);
      pinMode(SCI, OUTPUT);
      pinMode(SDO, OUTPUT);     // Configured as input when in programming mode
      digitalWrite(RST, HIGH);  // Level shifter is inverting, this shuts off 12V
      Serial.begin(19200);
    }

    void loop() {
       if (Serial.available() > 0) {
        Serial.read();
        pinMode(SDO, OUTPUT);     // Set SDO to output
        digitalWrite(SDI, LOW);
        digitalWrite(SII, LOW);
        digitalWrite(SDO, LOW);
        digitalWrite(RST, HIGH);  // 12v Off
        digitalWrite(VCC, HIGH);  // Vcc On
        delayMicroseconds(20);
        digitalWrite(RST, LOW);   // 12v On
        delayMicroseconds(10);
        pinMode(SDO, INPUT);      // Set SDO to input
        delayMicroseconds(300);
        unsigned int sig = readSignature();
        Serial.print("Signature is: ");
        Serial.println(sig, HEX);
        readFuses();
        if (sig == ATTINY13) {
          writeFuse(LFUSE, 0x6A);
          writeFuse(HFUSE, 0xFF);
        } else if (sig == ATTINY24 || sig == ATTINY44 || sig == ATTINY84 ||
                   sig == ATTINY25 || sig == ATTINY45 || sig == ATTINY85) {
          writeFuse(LFUSE, 0x62);
          writeFuse(HFUSE, 0xDF);
          writeFuse(EFUSE, 0xFF);
        }
        readFuses();
        digitalWrite(SCI, LOW);
        digitalWrite(VCC, LOW);    // Vcc Off
        digitalWrite(RST, HIGH);   // 12v Off
      }
    }

    byte shiftOut (byte val1, byte val2) {
      int inBits = 0;
      //Wait until SDO goes high
      while (!digitalRead(SDO))
        ;
      unsigned int dout = (unsigned int) val1 << 2;
      unsigned int iout = (unsigned int) val2 << 2;
       for (int ii = 10; ii >= 0; ii--)  {
        digitalWrite(SDI, !!(dout & (1 << ii)));
        digitalWrite(SII, !!(iout & (1 << ii)));
        inBits <<= 1;         inBits |= digitalRead(SDO);
         digitalWrite(SCI, HIGH);
         digitalWrite(SCI, LOW);
       } 
       return inBits >> 2;
    }

    void writeFuse (unsigned int fuse, byte val) {
      shiftOut(0x40, 0x4C);
      shiftOut( val, 0x2C);
      shiftOut(0x00, (byte) (fuse >> 8));
      shiftOut(0x00, (byte) fuse);
    }

    void readFuses () {
      byte val;
            shiftOut(0x04, 0x4C);  // LFuse
            shiftOut(0x00, 0x68);
      val = shiftOut(0x00, 0x6C);
      Serial.print("LFuse "); // this line may show up corrupted in some browsers it is a Serial.print("LFuse: ");
      Serial.print(val, HEX);
            shiftOut(0x04, 0x4C);  // HFuse
            shiftOut(0x00, 0x7A);
      val = shiftOut(0x00, 0x7E);
      Serial.print(", HFuse: ");
      Serial.print(val, HEX);
            shiftOut(0x04, 0x4C);  // EFuse
            shiftOut(0x00, 0x6A);
      val = shiftOut(0x00, 0x6E);
      Serial.print(", EFuse: ");
      Serial.println(val, HEX);
    }

    unsigned int readSignature () {
      unsigned int sig = 0;
      byte val;
      for (int ii = 1; ii < 3; ii++) {
              shiftOut(0x08, 0x4C);
              shiftOut(  ii, 0x0C);
              shiftOut(0x00, 0x68);
        val = shiftOut(0x00, 0x6C);
        sig = (sig << 8) + val;
      }
      return sig;
    }

---

One may find this article interesting as well
If you want to see how this board (in a skightly  more luxurious build) is used in practice you may want to check a youtube video by Ralph Bacon.