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

 





Additional Resources

I build a lot of RF circuits, and often it’s convenient to measure and log frequency with a computer. Previously I’ve built standalone frequency counters, frequency counters with a PC interface, and even hacked a classic frequency counter to add USB interface (twice, actually). My latest device uses only 2 microchips to provide a Raspberry Pi with RF frequency measurement capabilities. The RF signal clocks a 32-bit counter SN74LV8154 ($1.04 on Mouser) connected to a 16-bit IO expander MCP23017 ($1.26 on Mouser) accessable to the Raspberry Pi (via I²C) to provide real-time frequency measurements from a python script for $2.30 in components! Well, plus the cost of the Raspberry Pi. All files for this project are on my GitHub page.

img_8773

The entire circuit is only two microchips! I have a few passives to clean up the RF signal (the RF input is loaded with a 1k resistor to ground, decoupled through a series 100 nF capacitor, and balanced at VCC/2 through a voltage divider of two 47k resistors), but if the measured signal is already a strong square wave they could be omitted. The circuit requires a gate pulse which typically will be 1 pulse per second (1PPS) and can be generated by dividing-down a 32.768kHz oscillator, a spare pin on a microcontroller, a fancy 1PPS time reference, or like in my case a GPS module (Neo-6M) with 1PPS output to provide an extremely accurate gate.

schem

The connections are intuitive! The I2C address is 0x20 when A0, A1, and A2 are grounded. GPB(1-4) control the register select of the counter, and GPA(0-7) reads each bit of the selected register. The whole thing is controlled from Python, but could be trivially written in any language.

img_8777

Here’s a quick summary describing how the code works: First I send bytes to address 0 and 1 to set all pins of GPIO A as inputs, and GPIO B as outputs. Note that only 4 of 8 pins are used for the output, so technically 4 extra pins could be used for things like blinking LEDs or controlling other devices. I then set the register select pins by sending a value to 0x13 (GPIO B), and read the entire GPIO A bus (INTCAPB, 0x18). For address details, consult the datasheet. I do this 4 times (1 for each byte of the 32-bit counter), do a little math to turn it into a frequency value, and compare the current value with the last value and take the difference to display as the measured frequency.

screenshot

An advantage of this continuously running mode is that no clock cycles are lost, so a gate which accidentally fires a bit early due to jitter and cuts-off a cycle will compensate for it on a subsequent read. This is shown above, as a very stable 10MHz frequency reference is measured with this method. A “slow” 1PPS clock tick causes a reading slightly higher, compensated-for by the next reading being slightly lower. In this way, clock sources which are extremely accurate but suffer from low precision (like GPS time sources) are able to maximize the long-term measurement of frequency. Combining this frequency measurement technique with the ability to generate an analog voltage with a Raspberry Pi will allow me to perform some interesting experiments with a voltage controlled crystal oscillator.

Useful Links:

 





Additional Resources

I recently had the need to generate analog voltages from the Raspberry PI, which has rich GPIO digital outputs but no analog outputs. I looked into the RPi.GPIO project which can create PWM (which I wanted to smooth using a low pass filter to create the analog voltage), but its output on the oscilloscope looked terrible! It stuttered all over the place, likely because the duty is continuously under software control. I ended up solving my problem with a MCP4921 12-bit DAC chip (about $1.50 on eBay). It’s controlled via SPI, and although I could have written a python program to bit-bang its protocol with RPi.GPIO I realized I could write directly to the Raspberry Pi SPI device using the echo command. Dividing 3.3V into 12-bits (4096) means that I can control voltage in steps of less than 1mV each, right from the bash console!

img_8696

Video: The Problem (RPi PWM jitters)

Video: My Solution (SPI DAC)

Hardware Connection

There’s very little magic in how the microchip is connected to the Pi. It’s a straight shot to its SPI bus! Here’s a quick drawing showing which pins to connect. Check your device against the Raspberry Pi GPIO pinout diagram for different devices.

img_8701-1

Controlling the DAC with a Bus Pirate

Before I used a Raspberry Pi to control the DAC chip, I tested it out with a Bus Pirate. I don’t have a lot of pictures of the project, but I have a screenshot of a serial console used to send commands to the chip. One advantage of the Bus Pirate is that I can type bytes in binary, which helps to see the individual bits. I don’t have this ability when I’m working in the bash console.

serial

I’m less familiar with the Bus Pirate, but this was a good opportunity to get to know it a little better. It look me a long time (requiring I pull out the logic analyzer) to realize that I had to manually enable/disable the chip-select line, using the “[” and “]” commands. When I set up the SPI mode (command m5) I told it to use active low, but I wasn’t sure how to reverse the active level of the chip-select commands, so I just did ]this[ instead of [this] and it worked great.

frompi

This is the signal probed when it was controlled by the Raspberry Pi, but it looked essentially identical when values were sent via the Bus Pirate. The only difference is there was an appreciable delay between the “]” commands and each of the bytes. It worked fine though.

Controlling the DAC with Console Commands

Once the hardware was configured, the software was trivial. I could control analog voltages by sending two properly-formatted bytes to the SPI hardware device. Importantly, you must use raspi-config to enable SPI.

# set analog voltage to minimum value (about 0V)
echo -ne "\x30\x00" > /dev/spidev0.0 # minimum

# set analog voltage to something a little higher
echo -ne "\x30\xAB" > /dev/spidev0.0 

# set analog voltage to maximum value (about 3.3V)
echo -ne "\x3F\xFF" > /dev/spidev0.0

Helpful Links:

 





Additional Resources

I love analyzing data, so any time I see a cool device to measure something I usually want to save its output. I’ve lately come to enjoy the cheap panel-mount volt meters and current meters on eBay, and figured it would be cool to hack one to provide PC logging capability. After getting a few of these devices for ~$8 each on eBay and probing around, I realized they didn’t output measurement data on any of the pins (not that I really expected they would), so I coded a microcontroller to watch the lines of the multiplexed 7-segment display and figure out what the screen is displaying (an odd technique I’ve done once or twice before), then send its value to a computer using the microcontroller’s UART capabilities. Rather than interfacing a traditional serial port (using a MAX232 level converter, or even a TTL-level USB serial adapter) I decided to go full-scale-cool and make it wireless! I succeeded using a HC-06 Bluetooth serial adapter which you can find on eBay for ~$3. Although I have previously used custom software to hack the output of a TENMA multimeter to let me log voltage or current displayed on the multimeter, now I can measure current and voltage at the same time (wirelessly no less) and this is a far less expensive option than dedicating a multimeter to the task! The result is pretty cool, so I took pictures and am sharing the build log with the world.

The video summarizes the project, and the rest of this page details the build log. All of the code used to program the microcontroller (AVR-GCC), interface the device with the Bluetooth serial adapter, and plot the data (Python) is available as part of a GitHub project.

img_8436

This is what one of these modules looks like, and how it is intended to be used. One of the connectors has 3 wires (black = ground, red = power to run the display (anything up to 30V), and yellow = voltage sense wire). The other connector is thicker and is the current sense circuit. The black wire is essentially short-circuited to ground, so unfortunately this can only be used for low-side current sensing.

img_8134

The side of the display indicates which model it is. Note that if you wish to buy your own panel mount meters, look carefully at their current measuring range. Most of them measure dozens of amps with 0.1 A resolution. There are a few which only measure <1 A, but down to 0.1 mA resolution. This is what I prefer, since I rarely build equipment which draws more than 1 A.

img_8138

On the back you can see all of the important components. There’s a large current shunt resistor on the right, solder globs where the through-hole 4 character 7-segment displays fits in, and the microcontroller embedded in this device is a STM8S003 8-Bit MCU. This chip has UART, SPI, and I2C built-in, so it may be technically possible to have the chip output voltage and current digitally without the need for a man-in-the-middle chip like I’m building for this project. However, I don’t feel like reverse-engineering the hardware and software which takes measurements of voltage and current (which is an art in itself) and also figure out how to drive the display, so I’m happy continuing on developing my device as planned! I did probe all the pins just to be sure, and nothing looked like it was outputting data I could intercept. That would have been too easy!

img_8142

I snapped the device out of its plastic frame to be able to access the pins more easily.

img_8154

I then soldered-on headers to help with reverse-engineering the signals. Note that this was part of my investigation phase, and that these header pins were not needed for the end product. I have multiple panel mount ammeter / voltmeter modules on hand, so I left this one permanently “pinned” like this so I could access the pins if I needed to. A quick check with the continuity tester confirmed that every segment of every character (of both displays) is continuous (wired together).

img_8443

These headers made it easy to attach my 16-channel logic analyzer. I’m using an off-brand Saleae compatible logic analyzer. Their software is open source and very simple and easy to use. Saleae sells their official logic analyzers (which are well made and company supported) on their website, but they are expensive (although probably worth it). I purchased an eBay knock-off logic analyzer ($40) which “looks” like a Saleae device to the computer and works with the same open source software. If I were really serious about building professional products, I would certainly invest in an official Saleae product. For now, this is a good option for me and my hobby-level needs. An 8-channel version if as low as $10 on eBay, and $149 from Saleae.

img_8447

Connections are straightforward. I began probing only a single display. This is a good time to mention that an understanding of display multiplexing is critical to understanding how I’m reading this display! If you don’t know what a multiplexed display is, read up on the subject then come back here. It’s an important concept. While you’re at it, do you know what charlieplexing is?

img_8460

After gazing at the screen of squiggly lines, I was able to piece together which signals represented characters (due to their regularity) and which represented segments (which changed faster, and were more sporadic).

img_8463

I’ll be honest and say that I cheated a bit, using a very high value current limiting resistor and applying current (backwards) into the pins when the device was unplugged. I manged to illuminate individual segments of specific characters in the LCD. This supported what I recorded from the logic analyzer, and in reality could have been used to entirely determine which pins went to which characters/segments.

img_8594-1

Here’s what I came up with! It’s not that complicated: 16 pins control all the signals. The microcontroller raises all lines “high” to only one character at a time, then selectively grounds the segments (A-H) to pass current through only the LEDs intended to be illuminated. Characters are numbers and segments are letters. Note that “A” of the top display (voltage) is connected to the “A” of the second display (current), so both rows of 4 characters make 8 characters as far as the logic is concerned. The transistor isn’t really a discrete transistor, it’s probably the microcontroller sinking current. I used this diagram to conceptualize the directionality of the signals. The sample site of letters is high when a letter is illuminated, and the sample site of a segment is low when that segment is illuminated (the sample site of the segment is the base of the imaginary transistor).

logic

Knowing this, I can intentionally probe a few segments of a single character. Here is the logic analyzer output probing the second character (top), and two representative segments of that character (bottom). You can see the segments go nuts (flipping up and down) as other segments are illuminated (not shown). If you look closely at the blue annotations, you can see that each character is illuminated for about 1 ms and repeats every 13 ms.

img_8471

Now it was time to make my device! I started with a new panel meter and an empty project box. By this point I had reverse-engineered the device and concluded it would take 16 inputs of a microcontroller to read. I chose an ATMega328 which was perfect for the job (plenty of IO) although I could have used a much less powerful microcontroller if I wanted to interface an IO expander. The MCP23017 16-bit IO expander may have been perfect for the job! Anyway, I drilled a few circular holes in the back with a step-bit and cut-away a large square hole in the front with a nibbler so everything would snap-in nicely.

img_8482

I soldered wires to intercept the signal as it left the device’s microcontroller and went into the LED display.

img_8517

I then soldered the wires directly to my microcontroller. I also have an extra header available for programming (seen at the bottom) which I was able to remove once the software was complete. The red clip is clamping the serial Tx pin of the microcontroller and capturing the output into a USB serial adapter. Initially I debugged this circuit using the microcontroller’s on-board RC oscillator (1MHz) transmitting at 600 baud. I later realized that the serial bluetooth module requires 9600 baud. Although I could hack this with the internal RC clock, it was very unstable and garbage characters kept coming through. Luckily I designed around the potential of using an external crystal (pins 9 and 10 were unused) so it was an easy fix to later drop in a 11.0592 MHz crystal to allow stable transmission at 9600 baud.

img_8548

Now you can see the power regulation (LM7805) providing power to the MCU and wireless bluetooth module. Here’s the HC-06 datasheet (which is similar to HC-05) and another web page demonstrating how to use the breakout board. Also, I added a switch on the back which switches the voltage sense wire between the power supply and a sense connector which is on the back of the project box (red plastic banana jack).

img_8554

The bluetooth adapter expects 3.3V signals, so I added a quick and easy zener diode shunt regulator. I could have accomplished this by running my MCU on 3.3V (I didn’t have 3.3V regulators on hand though, and even so the module wants >3.6V to power the wireless transmitter) or perhaps a voltage divider on the output. On second thought, why did I use a zener ($!) over a resistor? Maybe my brain is stuck thinking about USB protocol standards.

img_8557

Since the chip was unstable transmitting 9600 baud, I tightened it up using a 11.0592 MHz crystal. The advantage of making your entire circuit look sketchy is that bodge jobs like this blend in perfectly and are unrecognizable!

img_8563

A quick reprogram to set the AVR fuses to switch from internal clock to external full-swing crystal was easy thanks to the female header I was able to pop out. I only recently started soldering-on headers like this with ribbon cable, but it’s my new favorite thing! It makes programming so easy.

img_8559

I packed it all in then added hot glue around the primary components (not shown). Again, if this were a production product I would have designed the hardware very differently. Since it’s a one-off job, I’m happy with it exactly like it is! It works, and it withstands bumps and shakes, so it’s good enough for me.

img_8581

I tested on a big piece of electrical equipent I’m building on the other side of the room. This device has its own 13.8V regulated power supply (and its own shelf!), so the wireless capability is fantastic to have. I just dropped this device between the power supply and the device under test. Rather than record the power supply voltage (which would always be a boring 13.8V) I decided to record a voltage test point of interest: the point just downstream of an LM7809 voltage regulator. I expected this voltage to swing wildly as current draw was high, and was very interested to know the voltage of this test point with respect to current draw. Although I have previously used custom software to hack the output of a TENMA multimeter to let me log voltage or current of this exact circuit, now I can measure both at the same time! Additionally, this is a far less expensive option than dedicating a multimeter to the task.

img_8584

I’m using RealTerm to access the serial port and log its output to a text file.

img_8590

A quick python script lets me graph the voltage/current relationship with respect to time. The (short) code to do this is on the GitHub page, and is demonstrated in the YouTube video.

demo

Here’s some data which shows the relationship between voltage (red trace) probed just downstream of an LM7809 voltage regulator and the total current draw of the system (blue trace). This data was recorded in real time, wirelessly, from across the room! This is exactly the type of interesting reading I was hoping to see.

img_8599

Now that it’s all together, I’m very happy with the result! This little device is happy serving as a simple voltage/current display (which is convenient in itself), but has the added benefit of continuously being available as a Bluetooth device. If I ever want to run an experiment to log/graph data, I just wirelessly connect to it and start recording the data. This build was a one-off device and is quite a hack (coding and construction wise). If I were interested in making a product out of this design, construction would greatly benefit from surface mount components and a PCB, and perhaps not necessitate super glue. For what it is, I’m happy how it came out, pleased to see it as a Bluetooth device I can connect to whenever I want, and I won’t tell anyone there’s super glue inside if you don’t.

Code used for this project is available at GitHub





Additional Resources

I recently had the need to carefully measure a voltage with a microcontroller which lacks an analog-to-digital converter (ADC), and I hacked together a quick and dirty method to do just this using a comparator, two transistors, and a few passives. The purpose of this project is to make a crystal oven controller at absolute minimal cost with minimal complexity. Absolute voltage accuracy is not of high concern (i.e., holding temperature to 50.00 C) but precision is the primary goal (i.e., hold it within 0.01 C of an arbitrary target I set somewhere around 50 C). Voltage measurement is usually a balance of a few factors: precision, accuracy, cost, simplicity, and speed. The method I demonstrate here maximizes precision and simplicity while minimizing cost. High speed operation is not of interest (1-2 measurements per second is fine), and as mentioned before accuracy is not a chief concern as long as precision is maximized. I would feel neglectful if I didn’t give a shout out to a few alternatives to this method: Using the 10-bit ADC built into most AVR microcontrollers (my go-to for ATMega328 at ATTiny85, but the ATTiny2313 doesn’t have any) often combined with an op-amp like this, using an IC like the MCP3208 8-channel 12-bit ADC (very expensive at $3.66 on mouser) are a good option, and fancy alternative dual slope methods as described in this really good youtube video and even mentioned nicely in the digital volt meter (DVM) / LCD driver ICL1706 datasheet. Those addressed, my quick and dirty idea uses only a couple cents of components and 3 pins of a microcontroller. There is much room for improvement (see my notes about a 555 timer, voltage reference, and operational amplifiers at the bottom) but this is a good minimal case starting point. This type of measurement is perfect for high precision temperature measuring using things like an LM335, LM35, or thermistor.

circuit

The concept behind this method is simple: use a current-limiting circuit to charge a capacitor at a constant rate so voltage rises linearly with time (rather than forming an exponential RC curve), and time how long that voltage takes to cross your test voltage.

A circuit which compares two voltages and outputs high when one voltage surpasses the other is called a comparator, and many microcontrollers (including ATMEL AVRs) have analog comparators built in (which compare AIN0 and AIN1, the result of which accessable by accessing the ACSR&(1<<ACO)) bit value (at least for the ATMega328, according to the datasheet). I can use the AVR’s comparator to time how long it takes a capacitor to charge to the test voltage, and output to that to the serial port. Note that I designed this entire circuit to use the most common transistor/resistors I could think of. It can be fine-tuned to increase speed or increase precision, but this is a great starting point. To generate a constant current I need a PNP transistor (I had a 2N2907 on hand) with a voltage divider on the base and a current limiting resistor above the transistor for good measure (in retrospect, with a more carefully chosen set of values this may not be needed). This is all that’s needed to charge the capacitor linearly and generate a positive ramp.

testrig-1

My test setup is a mess, but it demonstrates this idea works well, and is stable enough to run some experiments. In the frame you can see the ATMega328 microcontroller (big microchip), LM335 temperature sensor (the TO-92 closest to the MCU), a TTL FTDI serial/USB adapter (red board, top), and my USBTiny AVR programmer (blue board, right), and oscilloscope probes.

scope

To prevent this linear charger from charging forever, I make the microcontroller read the comparator which compares my test voltage with that of the ramp. If the test voltage is reached, or if the ramp reaches a cutoff voltage first (meaning the test voltage is too high to be measured), the count (time between last reset and now) is sent to the computer via serial port, and the capacitor is discharged through a PNP resistor. In the schematic, this is the “reset” pin. Note that the “measure” pin is AVR AC0, and AC1 is the test voltage. When all this is assembled, you can see how the linear ramps are created every time the reset transistor shuts off. Note that every 10th ramp is higher than the rest (shown here as the one left from center). This is because every 10th reading the data is summed and sent to the serial port, causing a little extra time before it is reset again. While the time value has been recorded of the comparator match of the test voltage and the ramp voltage, the capacitor is allowed to continue charging until the next cycle.

Interestingly, this method is largely insensitive to power supply noise. I’m using an extremely noisy environment (breadboard, DIP power regulator) but the recordings are rock solid. I suspect this is because the ramps are timed based on constant current, not abbsolute voltage, and that the ramps are fast enough to not be sensitive to slow changes in voltage. In reality, I don’t think I can adequately explain why the readings are so good when the supply is so shaky (the positive voltage rail is all over the place). It works, so I’m happy with it, and I’ll keep pushing forward.

miniterm

Lately I’ve been using RealTerm as a feature-rich alternative to HyperTerminal and a more convenient method than requiring custom python scripts be written every time I want to interact with the serial port in a way that involves debugging or logging or other advanced features. Here you can see the real time output of this device logging time to comparator match as it also logs to disk in a text file. This is great for simultaneously logging data (from RealTerm) and graphing it (from custom python scripts).

data_touch

This is what happens when I touch the temperature sensor for about 30s. I’m recording the time to voltage crossing of an LM335, so the number decreases as temperature increases. Also each data point is the average (actually the sum) of 10 points. It would be trivial to create some voltage test points, create a calibration curve, and infer the voltages involved, but this is more than enough already to prove that this method is robust and clean and precise and I couldn’t be more satisfied with the results! With a pair or capacitors and a few passives, this is totally implementable virtually anywhere. Considering my room is about 78F and my finger is about 98F, this 20F spread is about 1500 data points. That means each degree F is about 75 points, so I can resolve better 0.02 F (about 0.01 C) with this crude setup.

data_ac

If I let it run for about an hour, I catch my air conditioning coming on and off. Warmer temperature is higher voltage which means less time to charge, so the downslopes are my AC cooling my home and the up slope is my home passively warming. The fluctuations are only about 100 units which I (backwards calculate) assume are about 1-2 F.

These numbers seem so arbitrary! How can we calibrate this? This opens up a Pandora’s box of possible improvements. I’ll close by saying that this project works great exactly how it is to meet my needs. However, some modifications could be made to change the behavior of this device:

  • Slowing things down: A larger capacitor value (or higher resistor value) would increase the time or charging, lengthen the time to comparator threshold crossing, and increase precision. The readings would be slower (and more susceptible to noise), but it’s an option.
  • Self-calibration: Components (Rs and Cs) are sensitive to temperature and charge time can fluctuate with age, wear, temperature, etc. To self-calibrate with each sweep, add an additional comparator step which compares voltages between a precision voltage reference and your ramp would be a way to self-calibrate your ramp charge rate with each sweep. Optimally do this with two voltage references (3.3V and 1.8V are common) but comparing 0V to a single voltage reference would be a great step.
  • Don’t have the microcontroller gate: A 555 is perfectably capable of generating pulses to reset the ramp every so often, and frees up a pin of the microcontroller.
  • Use an op-amp for constant current charging. It seems like a lateral move, but if your deign already has an op-amp chances are there may be some unused amps, so eliminate a transistor for this purpose! Check out the constant current source section from TIs handbook on operational amplifier applications.
  • Use an op-amp for the comparator(s). The microcontroller’s comparator is handy, but if yours doesn’t have one (or you don’t feel like using one) configuring an unused op-amp stage as a comparator is a good option. The digital output could also trigger an interrupt on the digital input of a MCU pin as well!
  • Use timer and counters to measure time while using an external interrupt to gate the count. Your microcontroller’s on-board counter is likely extremely powerful so utilize it! This example doesn’t use it actually, but using it would let you count up to the CPU clock’s frequency of ticks between ramp starts and the comparator match.
  • Eliminate the microcontroller. Yeah, you heard me. If you use an op-amp keep resetting the ramps, and op-amp comparators to generate digital outputs of threshold crossings, you can use a standard counter (configured to latch then clear when the reset event is engaged by the 555 which induces resetting of the ramp by draining the capacitor), just use a counter IC to capture the value. You can clock it as fast as you want! You could even have it output its value directly to LED or LCD displays. In fact, this is how some digital volt meters work without the need for a microcontroller.

All code used in this project is available on its GitHub page