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/

 


     

1 Rotary Encoder, 3 Pins, 6 Inputs

Rotary encoders are a convenient way to add complex input functionality to small hardware projects with a single component. Rotary encoders (sometimes called shaft encoders, or rotary shaft encoders) can spin infinitely in both directions and many of them can be pressed like a button. The volume knob on your car radio is probably a rotary encoder.

With a single component and 3 microcontroller pins I can get six types of user input: turn right, turn left, press-and-turn right, press-and-turn left, press and release,  and press and hold. Let’s pretend “press and hold and turn” is not a thing…

A few years ago I [posted a video] on YouTube discussing how rotary shaft encoders work and how to interface them with microcontrollers. Although I’m happy it has over 13,000 views, I’m disappointed I never posted the code or schematics on my website (despite the fact I said on the video I would). A few years later I couldn’t find the original code anymore, and now that I’m working on a project using these devices I decided to document a simple case usage of this component. This post is intended to be a resource for future me just as much as it is anyone who finds it via Google or YouTube. This project will permanently live in a “rotary encoder” folder of my AVR projects GitHub page: AVR-projects. For good measure, I made a follow-up YouTube video which describes a more simple rotary encoder example and that has working links to this code.

At about $.50 each, rotary encoders are certainly more expensive than other switches (such as momentary switches). A quick eBay search reveals these components can be purchased from china in packs of 10 for $3.99 with free shipping. On Mouser similar components are about $0.80 individually, cut below $0.50 in quantities of 200. The depressible kind have two pins which are shorted when the button is pressed. The rotary part has 3 pins, which are all open in the normal state. Assuming the center pin is grounded, spinning the knob in one direction or the other will temporarily short both of the other pins to ground, but slightly staggered from each other. The order of this stagger indicates which direction the encoder was rotated.

I typically pull these all high through 10k series resistors (debounced with a 0.1uF capacitor to ground to reduce accidental readings) and sense their state directly with a microcontroller. Although capacitors were placed where they are to facilitate a rapid fall time and slower rise time, their ultimate goal is high-speed integration of voltage on the line as a decoupling capacitor for potential RF noise which may otherwise get into the line. Extra hardware debouching could be achieved by adding an additional series resistor immediately before the rotary encoder switch. For my simple application, I feel okay omitting these. If you want to be really thorough, you may benefit from adding a Schmidt trigger between the output and the microcontroller as well. Note that I can easily applying time-dependent debouncing via software as well.

Quick Code Notes

Setting-up PWM on ATTiny2313

I chose to use the 16-bit Timer/Counter to generate the PWM. 16-bits of duty control feels excessive for controlling an LED brightness, but my ultimate application will use a rotary encoder to finely and coarsely adjust a radio frequency, so there is some advantage to having this fine level of control. To round things out to a simple value, I’m capping the duty at 10,000 rather than the full 65,535. This way I can set the duty to 50% easily by setting OCR1A to 5,000. Similarly, spinning left/right can adjust duty by 100, and push-and-turn can adjust by 1,000.

void setupPWM_16bit(){
    DDRB|=(1<<PB3); // enable 16-bit PWM output on PB3
	TCCR1A|=(1<<COM1A1); // Clear OC1A/OC1B on Compare Match
	TCCR1B|=(1<<WGM13); // enable "PWM, phase and frequency correct"
	TCCR1B|=(1<<CS10); // enable output with the fastest clock (no prescaling)
	ICR1=10000; // set the top value (could be up to 2^16)
	OCR1A=5000; // set PWM pulse width (starts at 50% duty)
}

Simple (spin only) Rotary Encoder Polling

void poll_encoder_v1(){
	// polls for turns only
	if (~PINB&(1<<PB2)) {
		if (~PINB&(1<<PB1)){
			// left turn
			duty_decrease(100);
		} else {
			// right turn
			duty_increase(100);
		}			
		_delay_ms(2); // force a little down time before continuing 
		while (~PINB&(1<<PB2)){} // wait until R1 comes back high
	}
}

Simple (spin only) Rotary Encoder Polling

void poll_encoder_v2(){
	// polls for turns as well as push+turns
	if (~PINB&(1<<PB2)) {
		if (~PINB&(1<<PB1)){
			if (PINB&(1<<PB0)){
				// left turn
				duty_decrease(100);
			} else {
				// left press and turn
				duty_decrease(1000);
			}
		} else {
			if (PINB&(1<<PB0)){
				// right turn
				duty_increase(100);
			} else {
				// right press and turn
				duty_increase(1000);
			}
		}			
		_delay_ms(2); // force a little down time before continuing 
		while (~PINB&(1<<PB2)){} // wait until R1 comes back high
	}
}

What about an interrupt-based method?

A good compromise between continuous polling and reading pins only when we need to is to take advantage of the pin change interrupts. Briefly, we import avr/interrupt.h, set GIMSK, EIFR, and PCMSK (definitely read the datasheet) to trigger a hardware interrupt when a pin state change is detected on any of the 3 inputs. Then we run sei(); to enable global interrupts, and our functionality is identical without having to continuously call our polling function!

// run this only when pin state changes
ISR(PCINT_vect){poll_encoder_v2();}

int main(void){
	setupPWM_16bit();
	
	// set up pin change interrupts
	GIMSK=(1<<PCIE); // Pin Change Interrupt Enable 
	EIFR=(1<<PCIF); // Pin Change Interrupt Flag
	PCMSK=(1<<PCINT1)|(1<<PCINT2)|(1<<PCINT3); // watch these pins
	sei(); // enable global interrupts
	
	for(;;){} //forever
}

All code for this project is available on the GitHub:

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