Logging I2C Data with Bus Pirate and Python

I’m working on a project which requires I measure temperature via a computer, and I accomplished this with minimal complexity using a BusPirate and LM75A I2C temperature sensor. I already had some LM75A breakout boards I got on eBay (from China) a while back. A current eBay search reveals these boards are a couple dollars with free shipping. The IC itself is available on Mouser for $0.61 each. The LM75A datasheet reveals it can be powered from 2.8V-5.5V and has a resolution of 1/8 ºC (about 1/4 ºF). I attached the device to the Bus Pirate according to the Bus Pirate I/O Pin Descriptions page (SCL->CLOCK and SDA->MOSI) and started interacting with it according to the Bus Pirate I2C page. Since Phillips developed the I2C protocol, a lot of manufacturers avoid legal trouble and call it TWI (two-wire interface).

Here I show how to pull data from this I2C device directly via a serial terminal, then show my way of automating the process with Python. Note that there are multiple python packages out there that claim to make this easy, but in my experience they are either OS-specific or no longer supported or too confusing to figure out rapidly. For these reasons, I ended up just writing a script that uses common Python libraries so nothing special has to be installed.

Reading data directly from a serial terminal

Before automating anything, I figured out what I2C address this chip was using and got some sample temperature readings directly from the serial terminal. I used RealTerm to connect to the Bus Pirate. The sequence of keystrokes I used are:

  • # – to reset the device
  • m – to enter the mode selection screen
    • 4 – to select I2C mode
    • 3 – to select 100KHz
  • W – to turn the power on
  • P – to enable pull-up resistors
  • (1) – to scan I2C devices
    • this showed the device listening on 0x91
  • [0x91 r:2] – to read 2 bytes from I2C address 0x91
    • this showed bytes like 0x1D and 0x20
    • 0x1D20 in decimal is 7456
    • according to datasheet, must divide by 2^8 (256)
    • 7456/256 = 29.125 C = 84.425 F

Automating Temperature Reads with Python

There should be an easy way to capture this data from Python. The Bus Pirate website even has a page showing how to read data from LM75, but it uses a pyBusPirateLite python package which has to be manually installed (it doesn’t seem to be listed in pypi). Furthermore, they only have a screenshot of a partial code example (nothing I can copy or paste) and their link to the original article is broken. I found a cool pypy-indexed python module pyElectronics which should allow easy reading/writing from I2C devices via BusPirate and Raspberry Pi. However, it crashed immediately on my windows system due to attempting to load Linux-only python modules. I improved the code and issued a pull request, but I can’t encourage use of this package at this time if you intend to log data in Windows. Therefore, I’m keeping it simple and using a self-contained script to interact with the Bus Pirate, make temperature reads, and graph the data over time. You can code-in more advanced features as needed. The graphical output of my script shows what happens when I breathe on the sensor (raising the temperature), then what happens when I cool it (by placing a TV dinner on top of it for a minute). Below is the code used to set up the Bus Pirate to log and graph temperature data. It’s not fast, but for temperature readings it doesn’t have to be! It captures about 10 reads a second, and the rate-limiting step is the timeout value which is currently set to 0.1 sec.

NOTE: The Bus Pirate has a convenient binary scripting mode which can speed all this up. I’m not using that mode in this script, simply because I’m trying to closely mirror the functionality of directly typing things into the serial console.

import serial
import matplotlib.pyplot as plt

BUSPIRATE_PORT = 'com3' #customize this! Find it in device manager.

def send(ser,cmd,silent=False):
    """
    send the command and listen to the response.
    returns a list of the returned lines. 
    The first item is always the command sent.
    """
    ser.write(str(cmd+'\n').encode('ascii')) # send our command
    lines=[]
    for line in ser.readlines(): # while there's a response
        lines.append(line.decode('utf-8').strip())
    if not silent:
        print("\n".join(lines))
        print('-'*60)
    return lines

def getTemp(ser,address='0x91',silent=True,fahrenheit=False):
    """return the temperature read from an LM75"""
    unit=" F" if fahrenheit else " C"
    lines=send(ser,'[%s r:2]'%address,silent=silent) # read two bytes
    for line in lines:
        if line.startswith("READ:"):
            line=line.split(" ",1)[1].replace("ACK",'')
            while "  " in line:
                line=" "+line.strip().replace("  "," ")
            line=line.split(" 0x")
            val=int("".join(line),16)
            # conversion to C according to the datasheet
            if val < 2**15:
                val = val/2**8
            else:
                val =  (val-2**16)/2**8
            if fahrenheit:
                val=val*9/5+32
            print("%.03f"%val+unit)
            return val
    
    
# the speed of sequential commands is determined by this timeout
ser=serial.Serial(BUSPIRATE_PORT, 115200, timeout=.1)

# have a clean starting point
send(ser,'#',silent=True) # reset bus pirate (slow, maybe not needed)
#send(ser,'v') # show current voltages

# set mode to I2C
send(ser,'m',silent=True) # change mode (goal is to get away from HiZ)
send(ser,'4',silent=True) # mode 4 is I2C
send(ser,'3',silent=True) # 100KHz
send(ser,'W',silent=True) # turn power supply to ON. Lowercase w for OFF.
send(ser,'P',silent=True) # enable pull-up resistors
send(ser,'(1)') # scan I2C devices. Returns "0x90(0x48 W) 0x91(0x48 R)"

data=[]
try:
    print("reading data until CTRL+C is pressed...")
    while True:
        data.append(getTemp(ser,fahrenheit=True))
except:
    print("exception broke continuous reading.")
    print("read %d data points"%len(data))

ser.close() # disconnect so we can access it from another app

plt.figure(figsize=(6,4))
plt.grid()
plt.plot(data,'.-',alpha=.5)
plt.title("LM75 data from Bus Pirate")
plt.ylabel("temperature")
plt.xlabel("number of reads")
plt.show()

print("disconnected!") # let the user know we're done.

Experiment: Measuring Heater Efficacy

This project now now ready for an actual application test. I made a simple heater circuit which could be driven by an analog input, PWM, or digital ON/OFF. Powered from 12V it can pass 80 mA to produce up to 1W of heat. This may dissipate up to 250 mW of heat in the transistor if partially driven, so keep this in mind if an analog signal drive is used (i.e., thermistor / op-amp circuit). Anyhow, I soldered this up with SMT components on a copper-clad PCB with slots drilled on it and decided to give it a go. It’s screwed tightly to the temperature sensor board, but nothing special was done to ensure additional thermal conductivity. This is a pretty crude test.

I ran an experiment to compare open-air heating/cooling vs. igloo conditions, as well as low vs. high heater drive conditions. The graph below shows these results. The “heating” ranges are indicated by shaded areas. The exposed condition is when the device is sitting on the desk without any insulation. A 47k resistor is used to drive the base of the transistor (producing less than maximal heating). I then repeated the same thing after the device was moved inside the igloo. I killed the heater power when it reached the same peak temperature as the first time, noticing that it took less time to reach this temperature. Finally, I used a 1k resistor on the base of the transistor and got near-peak heating power (about 1W). This resulted in faster heating and a higher maximum temperature. If I clean this enclosure up a bit, this will be a nice way to test software-based PID temperature control with slow PWM driving the base of the transistor.

Code to create file logging (csv data with timestamps and temperatures) and produce plots lives in the ‘file logging’ folder of the Bus Pirate LM75A project on the GitHub page.

Experiment: Challenging LM7805 Thermal Shutdown

The ubiquitous LM7805 linear voltage regulator offers internal current limiting (1.5A) and thermal shutdown. I’ve wondered for a long time if I could use this element as a heater. It’s TO-220 package is quite convenient to mount onto enclosures. To see how quickly it heats up and what temperature it rests at, screwed a LM7805 directly to the LM75A breakout board (with a dab of thermal compound). I soldered the output pin to ground (!!!) and recorded temperature while it was plugged in.

Power (12V) was applied to the LM7805 over the red-shaded region. It looks like it took about 2 minutes to reach maximum temperature, and settled around 225F. After disconnecting power, it settled back to room temperature after about 5 minutes. I’m curious if this type of power dissipation is sustainable long term…

Update: Reading LM75A values directly into an AVR

This topic probably doesn’t belong inside this post, but it didn’t fit anywhere else and I don’t want to make it its own post. Now that I have this I2C sensor mounted where I want it, I want a microcontroller to read its value and send it (along with some other data) via serial USART to an FT232 (USB serial adapter). Ultimately I want to take advantage of its comparator thermostat function so I can have a USB-interfaced PC-controllable heater with multiple LM75A ICs providing temperature readings at different sites in my project. To do this, I had to write code to interface my microcontroller to the LM75A. I am using an ATMega328 (ATMega328P) with AVR-GCC (not Arduino). Although there are multiple LM75A senor libraries for Arduino [link] [link] [link] I couldn’t find any examples which didn’t rely on Arduino libraries. I ended up writing functions around g4lvanix’s L2C-master-lib.

Here’s a relevant code snippit. See the full code (with compile notes) on this GitHub page:

uint8_t data[2]; // prepare variable to hold sensor data
uint8_t address=0x91; // this is the i2c address of the sensor
i2c_receive(address,data,2); // read and store two bytes
temperature=(data[0]*256+data[1])/32; // convert two bytes to temperature

 

 

This project lives on my growing GitHub page for microcontroller projects:

https://github.com/swharden/AVR-projects/

 


     

2 thoughts on “Logging I2C Data with Bus Pirate and Python