The personal website of Scott W Harden
⚠️ Warning: This article is obsolete.
Articles typically receive this designation when the technology they describe is no longer relevant, code provided is later deemed to be of poor quality, or the topics discussed are better presented in future articles. Articles like this are retained for the sake of preservation, but their content should be critically assessed.

Crystal Oven Testing

To maintain high frequency stability, RF oscillator circuits are sometimes "ovenized" where their temperature is raised slightly above ambient room temperature and held precisely at one temperature. Sometimes just the crystal is heated (with a "crystal oven"), and other times the entire oscillator circuit is heated. The advantage of heating the circuit is that other components (especially metal core instructors) are temperature sensitive. Googling for the phrase "crystal oven", you'll find no shortage of recommended circuits. Although a more complicated PID (proportional-integral-derivative) controller may seem enticing for these situations, the fact that the enclosure is so well insulated and drifts so little over vast periods of time suggests that it might not be the best application of a PID controller. One of my favorite write-ups is from M0AYF's site which describes how to build a crystal oven for QRSS purposes. He demonstrates the MK1 and then the next design the MK2 crystal oven controller.

Briefly, desired temperature is set with a potentiometer. An operational amplifier (op-amp) compares the target temperature with measured temperature (using a thermistor - a resistor which varies resistance by tempearture). If the measured temperature is below the target, the op-amp output goes high, and current flows through heating resistors. There are a few differences between the two circuits, but one of the things that struck me as different was the use of negative feedback with the operational amplifier. This means that rather than being on or off (like the air conditioning in your house), it can be on a little bit. I wondered if this would greatly affect frequency stability. In the original circuit, he mentions

The oven then cycles on and off roughly every thirty or forty seconds and hovers around 40 degrees-C thereafter to within better than one degree-C. I wondered how much this on/off heater cycle affected temperature. Is it negligible, or could it affect frequency of an oscillator circuit? Indeed his application heats an entire enclosure so small variations get averaged-out by the large thermal mass. However in crystal oven designs where only the crystal is heated, such as described by Bill (W4HBK), I'll bet the effect is much greater. Compare the thermal mass of these two concepts.

How does the amount of thermal mass relate to how well it can be controlled? How important is negative feedback for partial-on heater operation? Can simple ON/OFF heater regulation adequately stabalize a crystal or enclosure? I'd like to design my own heater, pulling the best elements from the rest I see on the internet. My goals are:

  1. use inexpensive thermistors instead of linear temperature sensors (like LM335)
  2. use inexpensive quarter-watt resistors as heaters instead of power resistors
  3. be able to set temperature with a knob
  4. be able to monitor temperature of the heater
  5. be able to monitor power delivered to the heater
  6. maximum long-term temperature stability

Right off the bat, I realized that this requires a PC interface. Even if it's not used to adjust temperature (an ultimate goal), it will be used to log temperature and power for analysis. I won't go into the details about how I did it, other than to say that I'm using an ATMEL ATMega8 AVR microcontroller and ten times I second I sample voltage on each of it's six 10-bit ADC pins (PC0-PC5), and send that data to the computer with USART using an eBay special serial/USB adapter based on FTDI. They're <$7 (shipped) and come with the USB cable. Obviously in a consumer application I'd etch boards and use the SMT-only FTDI chips, but for messing around at home I a few a few of these little adapters. They're convenient as heck because I can just add a heater to my prototype boards and it even supplies power and ground. Convenient, right? Power is messier than it could be because it's being supplied by the PC, but for now it gets the job done. On the software side, Python with PySerial listens to the serial port and copies data to a large numpy array, saving it every once and a while. Occasionally a bit is sent wrong and a number is received incorrectly (maybe one an hour), but the error is recognized and eliminated by the checksum (just the sum of all transmitted numbers). Plotting is done with numpy and matpltolib. Code for all of that is at the bottom of this post.

That's the data logger circuit I came up with. Reading six channels ten times a second, it's more than sufficient for voltage measurement. I went ahead and added an op-amp to the board too, since I knew I'd be using one. I dedicated one of the channels to serve as ambient temperature measurement. See the little red thermistor by the blue resistor? I also dedicated another channel to the output of the op-amp. This way I can measure drive to whatever temperature controller circuity I choose to use down the road. For my first test, I'm using a small thermal mass like one would in a crystal oven. Here's how I made that:

I then build the temperature controller part of the circuit. It's pretty similar to that previously published. it uses a thermistor in a voltage divider configuration to sense temperature. It uses a trimmer potentiometer to set temperature. An LED indicator light gives some indication of on/off, but keep in mind that a fraction of a volt will turn the Darlington transistor (TIP122) on slightly although it doesn't reach a level high enough to drive the LED. The amplifier by default is set to high gain (55x), but can be greatly lowered (negative gain actually) with a jumper. This lets me test how important gain is for the circuitry.

When using a crystal oven configuration, I concluded high high gain (cycling the heater on/off) is a BAD idea. While average temperature is held around the same, the crystal oscillates. This is what is occurring above when M0AYF indicates his MK1 heater turns on and off every 40 seconds. While you might be able to get away with it while heating a chassis or something, I think it's easy to see it's not a good option for crystal heaters. Instead, look at the low gain (negative gain) configuration. It reaches temperature surprisingly quickly and locks to it steadily. Excellent.

high gain configuration tends to oscillate every 30 seconds

low gain / negative gain configuration is extremely stable (fairly high temperature)

Here's a similar experiment with a lower target temperature. Noise is due to unregulated USB power supply / voltage reference. Undeniably, this circuit does not oscillate much if any

Clearly low (or negative) gain is best for crystal heaters. What about chassis / enclosure heaters? Let's give that a shot. I made an enclosure heater with the same 2 resistors. Again, I'm staying away from expensive components, and that includes power resistors. I used epoxy (gorilla glue) to cement them to the wall of one side of the enclosure.

I put a "heater sensor" thermistor near the resistors on the case so I could get an idea of the heat of the resistors, and a "case sensor" on the opposite side of the case. This will let me know how long it takes the case to reach temperature, and let me compare differences between using near vs. far sensors (with respect to the heating element) to control temperature. I ran the same experiments and this is what I came up with!

heater temperature (blue) and enclosure temperature (green) with low gain (first 20 minutes), then high gain (after) operation. High gain sensor/feedback loop is sufficient to induce oscillation, even with the large thermal mass of the enclosure

CLOSE SENSOR CONTROL, LOW/HIGH GAIN: TOP: heater temperature (blue) and enclosure temperature (green) with low gain (first 20 minutes), then high gain (after) operation. High gain sensor/feedback loop is sufficient to induce oscillation, even with the large thermal mass of the enclosure. BOTTOM: power to the heater (voltage off the op-amp output going into the base of the Darlington transistor). Although I didn't give the low-gain configuration time to equilibrate, I doubt it would have oscillated on a time scale I am patient enough to see. Future, days-long experimentation will be required to determine if it oscillates significantly.[/caption]

Even with the far sensor (opposite side of the enclosure as the heater) driving the operational amplifier in high gain mode, oscillations occur. Due to the larger thermal mass and increased distance the heat must travel to be sensed they take much longer to occur, leading them to be slower and larger than oscillations seen earlier when the heater was very close to the sensor.

FAR SENSOR CONTROL, HIGH GAIN: Even with the far sensor (opposite side of the enclosure as the heater) driving the operational amplifier in high gain mode, oscillations occur. Blue is the far sensor temperature. Green is the sensor near the heater temperature. Due to the larger thermal mass and increased distance the heat must travel to be sensed they take much longer to occur, leading them to be slower and larger than oscillations seen earlier when the heater was very close to the sensor.

Right off the bat, we observe that even with the increased thermal mass of the entire enclosure (being heated with two dinky 100 ohm 1/4 watt resistors) the system is prone to temperature oscillation if gain is set too high. For me, this is the final nail in the coffin - I will never use a comparator-type high gain sensor/regulation loop to control heater current. With that out, the only thing to compare is which is better: placing the sensor near the heating element, or far from it. In reality, with a well-insulated device like I seem to have, it seems like it doesn't make much of a difference! The idea is that by placing it near the heater, it can stabilize quickly. However, placing it far from the heater will give it maximum sensation of "load" temperature. Anywhere in-between should be fine. As long as it's somewhat thermally coupled to the enclosure, enclosure temperature will pull it slightly away from heater temperature regardless of location. Therefore, I conclude it's not that critical where the sensor is placed, as long as it has good contact with the enclosure. Perhaps with long-term study (on the order of hours to days) slow oscillations may emerge, but I'll have to build it in a more permanent configuration to test it out. Lucky, that's exactly what I plan to do, so check back a few days from now!

Since the data speaks for itself, I'll be concise with my conclusions:

  • two 1/4 watt 100 Ohm resistors in parallel (50 ohms) are suitable to heat an insulated enclosure with 12V
  • two 1/4 watt 100 Ohm resistors in parallel (50 ohms) are suitable to heat a crystal with 5V
  • low gain or negative gain is preferred to prevent oscillating tempeartures
  • Sensor location on an enclosure is not critical as long as it's well-coupled to the enclosure and the entire enclosure is well-insulated.

I feel satisfied with today's work. Next step is to build this device on a larger scale and fix it in a more permanent configuration, then leave it to run for a few weeks and see how it does. On to making the oscillator! If you have any questions or comments, feel free to email me. If you recreate this project, email me! I'd love to hear about it.

Here's the code that went on the ATMega8 AVR (it continuously transmits voltage measurements on 6 channels).

#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

/*
8MHZ: 300,600,1200,2400,4800,9600,14400,19200,38400
1MHZ: 300,600,1200,2400,4800
*/
#define USART_BAUDRATE 38400
#define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1)

/*
ISR(ADC_vect)
{
    PORTD^=255;
}
*/

void USART_Init(void){
    UBRRL = BAUD_PRESCALE;
    UBRRH = (BAUD_PRESCALE >> 8);
    UCSRB = (1<<TXEN);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // 9N1
}

void USART_Transmit( unsigned char data ){
    while ( !( UCSRA & (1<<UDRE)) );
    UDR = data;
}

void sendNum(long unsigned int byte){
    if (byte==0){
        USART_Transmit(48);
    }
    while (byte){
        USART_Transmit(byte%10+48);
        byte-=byte%10;
        byte/=10;
    }
}

int readADC(char adcn){
    ADMUX = 0b0100000+adcn;
    ADCSRA |= (1<<ADSC); // reset value
    while (ADCSRA & (1<<ADSC)) {}; // wait for measurement
    return ADC>>6;
}

int sendADC(char adcn){
    int val;
    val=readADC(adcn);
    sendNum(val);
    USART_Transmit(',');
    return val;
}

int main(void){
    ADCSRA = (1<<ADEN)  | 0b111;
    DDRB=255;
    USART_Init();
    int checksum;

    for(;;){
        PORTB=255;
        checksum=0;
        checksum+=sendADC(0);
        checksum+=sendADC(1);
        checksum+=sendADC(2);
        checksum+=sendADC(3);
        checksum+=sendADC(4);
        checksum+=sendADC(5);
        sendNum(checksum);
        USART_Transmit('n');
        PORTB=0;
        _delay_ms(200);
    }
}

Here's the command I used to compile the code, set the AVR fuse bits, and load it to the AVR.

del *.elf
del *.hex
avr-gcc -mmcu=atmega8 -Wall -Os -o main.elf main.c -w
pause
cls
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avrdude -c usbtiny -p m8 -F -U flash:w:"main.hex":a -U lfuse:w:0xe4:m -U hfuse:w:0xd9:m

Here's the code that runs on the PC to listen to the microchip, match the data to the checksum, and log it occasionally.

import serial, time
import numpy
ser = serial.Serial("COM16", 38400, timeout=100)

line=ser.readline()[:-1]
t1=time.time()
lines=0

data=[]

def adc2R(adc):
    Vo=adc*5.0/1024.0
    Vi=5.0
    R2=10000.0
    R1=R2*(Vi-Vo)/Vo
    return R1

while True:
    line=ser.readline()[:-1]
    lines+=1
    if "," in line:
        line=line.split(",")
        for i in range(len(line)):
            line[i]=int(line[i][::-1])

    if line[-1]==sum(line[:-1]):
        line=[time.time()]+line[:-1]
        print lines, line
        data.append(line)
    else:
        print  lines, line, "<-- FAIL"

    if lines%50==49:
        numpy.save("data.npy",data)
        print "nSAVINGn%d lines in %.02f sec (%.02f vals/sec)n"%(lines,
            time.time()-t1,lines/(time.time()-t1))

Here's the code that runs on the PC to graph data.

import matplotlib
matplotlib.use('TkAgg') # <-- THIS MAKES IT FAST!
import numpy
import pylab
import datetime
import time

def adc2F(adc):
    Vo=adc*5.0/1024.0
    K=Vo*100
    C=K-273
    F=C*(9.0/5)+32
    return F

def adc2R(adc):
    Vo=adc*5.0/1024.0
    Vi=5.0
    R2=10000.0
    R1=R2*(Vi-Vo)/Vo
    return R1

def adc2V(adc):
    Vo=adc*5.0/1024.0
    return Vo

if True:
    print "LOADING DATA"
    data=numpy.load("data.npy")
    data=data
    print "LOADED"

    fig=pylab.figure()
    xs=data[:,0]
    tempAmbient=data[:,1]
    tempPower=data[:,2]
    tempHeater=data[:,3]
    tempCase=data[:,4]
    dates=(xs-xs[0])/60.0
    #dates=[]
    #for dt in xs: dates.append(datetime.datetime.fromtimestamp(dt))

    ax1=pylab.subplot(211)
    pylab.title("Temperature Controller - Low Gain")
    pylab.ylabel('Heater (ADC)')
    pylab.plot(dates,tempHeater,'b-')
    pylab.plot(dates,tempCase,'g-')
    #pylab.axhline(115.5,color="k",ls=":")

    #ax2=pylab.subplot(312,sharex=ax1)
    #pylab.ylabel('Case (ADC)')
    #pylab.plot(dates,tempCase,'r-')
    #pylab.plot(dates,tempAmbient,'g-')
    #pylab.axhline(0,color="k",ls=":")

    ax2=pylab.subplot(212,sharex=ax1)
    pylab.ylabel('Heater Power')
    pylab.plot(dates,tempPower)

    #fig.autofmt_xdate()
    pylab.xlabel('Elapsed Time (min)')

    pylab.show()

print "DONE"

Adding USB to a Cheap Frequency Counter (Again)

Today I rigineered my frequency counter to output frequency to a computer via a USB interface. You might remember that I did this exact same thing two years ago, but unfortunately I fell victim to accidental closed source. When I rigged it the first time, I stupidly tried to get fancy and add USB interface with V-USB requiring special drivers and special software code to retrieve the data. The advantage was that the microcontroller spoke directly to the PC USB port via 2 pins requiring no extra hardware. The stinky part is that I've since lost the software I wrote necessary to decode the data. Reading my old post, I see I wrote "Although it’s hard for me, I really don’t think I can release this [microchip code] right now. I’m working on an idiot’s guide to USB connectivity with ATMEL microcontrollers, and it would cause quite a stir to post that code too early." Obviously I never got around to finishing it, and I've since lost the code. Crap! I have this fancy USB "enabled" frequency counter, but no ability to use it. NOTE TO SELF: NEVER POST PROJECTS ONLINE WITHOUT INCLUDING THE CODE! I guess I have to crack this open again and see if I can reprogram it...

My original intention was just to reprogram the IC and add serial USART support, then use a little FTDI adapter module to serve as a USB serial port. That will be supported by every OS on the planet out of the box. Upon closer inspection, I realized I previously used an ATMega48 which has trouble being programmed by AVRDUDE, so I whipped up a new perf-board based around an ATMega8. I copied the wires exactly (which was stupid, because I didn't have it written down which did what, and they were in random order), and started attacking the problem in software.

The way the microcontroller reads frequency is via the display itself. There are multiplexed digits, so some close watching should reveal the frequency. I noticed that there were fewer connections to the microcontroller than expected - a total of 12. How could that be possible? 8 seven-segment displays should be at least 7+8=15 wires. What the heck? I had to take apart the display to remind myself how it worked. It used a pair of ULN2006A darlington transistor arrays to do the multiplexing (as expected), but I also noticed it was using a CD4511BE BCD-to-7-segment driver to drive the digits. I guess that makes sense. That way 4 wires can drive 7 segments. 8+4=12 wires, which matches up. Now I feel stupid for not realizing it in the first place. Time to screw things back together.

Here's the board I made. 3 wires go to the FTDI USB module (GND, VCC 5V drawn from USB, and RX data), black wires go to the display, and the headers are to aid programming. I added an 11.0592MHz crystal to allow maximum serial transfer speed (230,400 baud), but stupidly forgot to enable it in code. It's all boxed up now, running at 8MHz and 38,400 baud with the internal RC clock. Oh well, no loss I guess.

I wasted literally all day on this. It was so stupid. The whole time I was kicking myself for not posting the code online. I couldn't figure out which wires were for the digit selection, and which were for the BCD control. I had to tease it apart by putting random numbers on the screen (by sticking my finger in the frequency input hole) and looking at the data flowing out on the oscilloscope to figure out what was what. I wish I still had my DIY logic analyzer. I guess this project was what I built it for in the first place! A few hours of frustrating brute force programming and adult beverages later, I had all the lines figured out and was sending data to the computer.

With everything back together, I put the frequency counter back in my workstation and I'm ready to begin my frequency measurement experiments. Now it's 9PM and I don't have the energy to start a whole line of experiments. Gotta save it for another day. At least I got the counter working again!

Here's the code that goes on the microcontroller (it sends the value on the screen as well as a crude checksum, which is just the sum of all the digits)

#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define USART_BAUDRATE 38400
#define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1)

void USART_Init(void){
    UBRRL = BAUD_PRESCALE;
    UBRRH = (BAUD_PRESCALE >> 8);
    UCSRB = (1<<TXEN);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // 9N1
}

void USART_Transmit( unsigned char data ){
    while ( !( UCSRA & (1<<UDRE)) );
    UDR = data;
}

void sendNum(int byte){
    if (byte==0){
        USART_Transmit(48);
    }
    while (byte){
        USART_Transmit(byte%10+48);
        byte-=byte%10;
        byte/=10;
    }
}

void sendBin(int byte){
    char i;
    for (i=0;i<8;i++){
        USART_Transmit(48+((byte>>i)&1));
    }
}

volatile char digits[]={0,0,0,0,0,0,0,0};
volatile char freq=123;

char getDigit(){
    char digit=0;
    if (PINC&0b00000100) {digit+=1;}
    if (PINC&0b00001000) {digit+=8;}
    if (PINC&0b00010000) {digit+=4;}
    if (PINC&0b00100000) {digit+=2;}
    if (digit==15) {digit=0;} // blank
    return digit;
}

void updateNumbers(){
    while ((PINB&0b00000001)==0){} digits[7]=getDigit();
    while ((PINB&0b00001000)==0){} digits[6]=getDigit();
    while ((PINB&0b00010000)==0){} digits[5]=getDigit();
    while ((PINB&0b00000010)==0){} digits[4]=getDigit();
    while ((PINB&0b00000100)==0){} digits[3]=getDigit();
    while ((PINB&0b00100000)==0){} digits[2]=getDigit();
    while ((PINC&0b00000001)==0){} digits[1]=getDigit();
    while ((PINC&0b00000010)==0){} digits[0]=getDigit();
}

int main(void){
    USART_Init();
    char checksum;
    char i=0;
    char digit=0;

    for(;;){
        updateNumbers();
        checksum=0;
        for (i=0;i<8;i++){
            checksum+=digits[i];
            sendNum(digits[i]);
        }
        USART_Transmit(',');
        sendNum(checksum);
        USART_Transmit('n');
        _delay_ms(100);
    }
}

Here's the Python code to listen to the serial port, though you could use any program (note that the checksum is just shown and not verified):

import serial, time
import numpy
ser = serial.Serial("COM15", 38400, timeout=100)

line=ser.readline()[:-1]
t1=time.time()
lines=0

data=[]

def adc2R(adc):
    Vo=adc*5.0/1024.0
    Vi=5.0
    R2=10000.0
    R1=R2*(Vi-Vo)/Vo
    return R1

while True:
    line=ser.readline()[:-1]
    print line

This is super preliminary, but I've gone ahead and tested heating/cooling an oscillator (a microcontroller clocked with an external crystal and outputting its signal with CKOUT). By measuring temperature and frequency at the same time, I can start to plot their relationship...

⚠️ Warning: This article is obsolete.
Articles typically receive this designation when the technology they describe is no longer relevant, code provided is later deemed to be of poor quality, or the topics discussed are better presented in future articles. Articles like this are retained for the sake of preservation, but their content should be critically assessed.

Precision Temperature Measurement

In an effort to resume previous work [A, B, C, D] on developing a crystal oven for radio frequency transmitter / receiver stabilization purposes, the first step for me was to create a device to accurately measure and log temperature. I did this with common, cheap components, and the output is saved to the computer (over 1,000 readings a second). Briefly, I use a LM335 precision temperature sensor ($0.70 on mouser) which outputs voltage with respect to temperature. It acts like a Zener diode where the breakdown voltage relates to temperature. 2.95V is 295K (Kelvin), which is 22ºC / 71ºF. Note that Kelvin is just ºC + 273.15 (the difference between freezing and absolute zero). My goal was to use the ADC of a microcontroller to measure the output. The problem is that my ADC (one of 6 built into the ATMEL ATMega8 microcontroller) has 10-bit resolution, reporting steps from 0-5V as values from 0-1024. Thus, each step represents 0.0049V (0.49ºC / 0.882ºF). While ~1ºF resolution might be acceptable for some temperature measurement or control applications, I want to see fractions of a degree because radio frequency crystal temperature stabilization is critical. Here's a video overview.

This is the circuit came up with. My goal was to make it cheaply and what I had on hand. It could certainly be better (more stable, more precise, etc.) but this seems to be working nicely. The idea is that you set the gain (the ratio of R2/R1) to increase your desired resolution (so your 5V of ADC recording spans over just several ºF you're interested in), then set your "base offset" temperature that will produce 0V. In my design, I adjusted so 0V was room temperature, and 5V (maximum) was body temperature. This way when I touched the sensor, I'd watch temperature rise and fall when I let go. Component values are very non-critical. LM324 is powered 0V GND and +5V Vcc. I chose to keep things simple and use a single rail power supply. It is worth noting that I ended-up using a 3.5V Zener diode for the positive end of the potentiometer rather than 5V. If your power supply is well regulated 5V will be no problem, but as I was powering this with USB I decided to go for some extra stability by using a Zener reference.

On the microcontroller side, analog-to-digital measurement is summed-up pretty well in the datasheet. There is a lot of good documentation on the internet about how to get reliable, stable measurements. Decoupling capacitors, reference voltages, etc etc. That's outside the scope of today's topic. In my case, the output of the ADC went into the ATMega8 ADC5 (PC5, pin 28). Decoupling capacitors were placed at ARef and AVcc, according to the datasheet. Microcontroller code is at the bottom of this post.

To get the values to the computer, I used the USART capability of my microcontroller and sent ADC readings (at a rate over 1,000 a second) over a USB adapter based on an FTDI FT232 chip. I got e-bay knock-off FTDI evaluation boards which come with a USB cable too (they're about $6, free shipping). Yeah, I could have done it cheaper, but this works effortlessly. I don't use a crystal. I set fuse settings so the MCU runs at 8MHz, and thanks to the nifty online baud rate calculator determined I can use a variety of transfer speeds (up to 38400). At 1MHz (if DIV8 fuse bit is enabled) I'm limited to 4800 baud. Here's the result, it's me touching the sensor with my finger (heating it), then letting go.

Touching the temperature sensor with my finger, voltage rose exponentially. When removed, it decayed exponentially - a temperature RC circuit, with capacitance being the specific heat capacity of the sensor itself. Small amounts of jitter are expected because I'm powering the MCU from unregulated USB +5V.[/caption]

I spent a while considering fancy ways to send the data (checksums, frame headers, error correction, etc.) but ended-up just sending it old fashioned ASCII characters. I used to care more about speed, but even sending ASCII it can send over a thousand ADC readings a second, which is plenty for me. I ended-up throttling down the output to 10/second because it was just too much to log comfortable for long recordings (like 24 hours). In retrospect, it would have made sense to catch all those numbers and do averaging on the on the PC side.

I keep my house around 70F at night when I'm there, and you can see the air conditioning kick on and off. In the morning the AC was turned off for the day, temperature rose, and when I got back home I turned the AC on and it started to drop again.[/caption]

On the receive side, I have nifty Python with PySerial ready to catch data coming from the microcontroller. It's decoded, turned to values, and every 1000 receives saves a numpy array as a NPY binary file. I run the project out of my google drive folder, so while I'm at work I can run the plotting program and it loads the NPY file and shows it - today it allowed me to realize that my roommate turned off the air conditioning after I left, because I saw the temperature rising mid-day. The above graph is temperature in my house for the last ~24 hours. That's about it! Here's some of the technical stuff.

AVR ATMega8 microcontroller code:

#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

/*
8MHZ: 300,600,1200,2400,4800,9600,14400,19200,38400
1MHZ: 300,600,1200,2400,4800
*/
#define USART_BAUDRATE 38400
#define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1)

/*
ISR(ADC_vect)
{
    PORTD^=255;
}
*/

void USART_Init(void){
    UBRRL = BAUD_PRESCALE;
    UBRRH = (BAUD_PRESCALE >> 8);
    UCSRB = (1<<TXEN);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // 9N1
}

void USART_Transmit( unsigned char data ){
    while ( !( UCSRA & (1<<UDRE)) );
    UDR = data;
}

void sendNum(long unsigned int byte){
    if (byte==0){
        USART_Transmit(48);
    }
    while (byte){
        USART_Transmit(byte%10+48);
        byte-=byte%10;
        byte/=10;
    }

}

unsigned int readADC(char adcn){
    ADMUX = 0b0100000+adcn;
    ADCSRA |= (1<<ADSC); // reset value
    while (ADCSRA & (1<<ADSC)) {}; // wait for measurement
    return ADC>>6;
}

void ADC_Init(){
    // ADC Enable, Prescaler 128
    ADCSRA = (1<<ADEN)  | 0b111;
}

int main(void){
    //DDRD=255;
    USART_Init();
    ADC_Init();
    for(;;){
        sendNum(readADC(5));
        USART_Transmit('n');
        _delay_ms(100);
    }
}

Here is the Python code to receive the data and log it to disk:

import serial, time
import numpy
ser = serial.Serial("COM15", 38400, timeout=100)

line=ser.readline()[:-1]
t1=time.time()
lines=0

data=[]

while True:
    line=ser.readline()[:-1]

    if "," in line:
        line=line.split(",")
        for i in range(len(line)):
            line[i]=line[i][::-1]
    else:
        line=[line[::-1]]
    temp=int(line[0])
    lines+=1
    data.append(temp)
    print "#",
    if lines%1000==999:
        numpy.save("DATA.npy",data)
        print
        print line
        print "%d lines in %.02f sec (%.02f vals/sec)"%(lines,
                time.time()-t1,lines/(time.time()-t1))

Here is the Python code to plot the data that has been saved:

import numpy
import pylab

data=numpy.load("DATA.npy")
print data
data=data*.008 #convert to F
xs=numpy.arange(len(data))/9.95  #vals/sec
xs=xs/60.0# minutes
xs=xs/60.0# hours

pylab.plot(xs,data)
pylab.grid(alpha=.5)
pylab.axis([None,None,0*.008,1024*.008])
pylab.ylabel(r'$Delta$ Fahrenheit')
pylab.xlabel("hours")
pylab.show()
⚠️ Warning: This article is obsolete.
Articles typically receive this designation when the technology they describe is no longer relevant, code provided is later deemed to be of poor quality, or the topics discussed are better presented in future articles. Articles like this are retained for the sake of preservation, but their content should be critically assessed.

Realtime image pixelmap from Numpy array data in Qt

Consider realtime spectrograph software like QRSS VD. It's primary function is to scroll a potentially huge data-rich image across the screen. In Python, this is often easier said than done.__ If you're not careful, you can tackle this problem inefficiently and get terrible frame rates (<5FPS) or eat a huge amount of system resources (I get complaints often that QRSS VD takes up a lot of processor resources, and 99% of it is drawing the images). In the past, I've done it at least 4 different ways (one, two, three, four, five). Note that "four" seems to be the absolute fastest option so far. I've been keeping an eye out for a while now contemplating the best way to rapidly draw color-mapped 8-bit data in a python program. Now that I'm doing a majority of my graphical development with PyQt and QtDesigner (packaged with PythonXY), I ended-up with a solution that looks like this (plotting random data with a colormap):

1.) in QtDesigner, create a form with a scrollAreaWidget

2.) in QtDesigner, add a label inside the scrollAreaWidget

3.) in code, resize label and also scrollAreaWidgetContents to fit data (disable "widgetResizable")

4.) in code, create a QImage from a 2D numpy array (dtype=uint8)

5.) in code, set label pixmap to QtGui.QPixmap.fromImage(QImage)

That's pretty much it! Here are some highlights of my program. Note that the code for the GUI is in a separate file, and must be downloaded from the ZIP provided at the bottom. Hope it helps someone else out there who might want to do something similar!

import ui_main
import sys
from PyQt4 import QtCore, QtGui

import sys
from PyQt4 import Qt
import PyQt4.Qwt5 as Qwt
from PIL import Image
import numpy
import time

spectroWidth=1000
spectroHeight=1000

a=numpy.random.random(spectroHeight*spectroWidth)*255
a=numpy.reshape(a,(spectroHeight,spectroWidth))
a=numpy.require(a, numpy.uint8, 'C')

COLORTABLE=[]
for i in range(256): COLORTABLE.append(QtGui.qRgb(i/4,i,i/2))

def updateData():
    global a
    a=numpy.roll(a,-5)
    QI=QtGui.QImage(a.data, spectroWidth, spectroHeight, QtGui.QImage.Format_Indexed8)
    QI.setColorTable(COLORTABLE)
    uimain.label.setPixmap(QtGui.QPixmap.fromImage(QI))

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    win_main = ui_main.QtGui.QWidget()
    uimain = ui_main.Ui_win_main()
    uimain.setupUi(win_main)

    # SET UP IMAGE
    uimain.IM = QtGui.QImage(spectroWidth, spectroHeight, QtGui.QImage.Format_Indexed8)
    uimain.label.setGeometry(QtCore.QRect(0,0,spectroWidth,spectroHeight))
    uimain.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0,0,spectroWidth,spectroHeight))

    # SET UP RECURRING EVENTS
    uimain.timer = QtCore.QTimer()
    uimain.timer.start(.1)
    win_main.connect(uimain.timer, QtCore.SIGNAL('timeout()'), updateData)

    ### DISPLAY WINDOWS
    win_main.show()
    sys.exit(app.exec_())
⚠️ Warning: This article is obsolete.
Articles typically receive this designation when the technology they describe is no longer relevant, code provided is later deemed to be of poor quality, or the topics discussed are better presented in future articles. Articles like this are retained for the sake of preservation, but their content should be critically assessed.

Wireless Microcontroller / PC Interface for $3.21

Here I demonstrate a dirt-cheap method of transmitting data from any microchip to any PC using $3.21 in parts. I've had this idea for a while, but finally got it working tonight. On the transmit side, I'm having a an ATMEL AVR microcontroller (ATMega48) transmit data (every number from 0 to 200 over and over) wirelessly using 433mhz wireless modules. The PC receives the data through the microphone port of a sound card, and a cross-platform Python script I wrote decodes the data from the audio and graphs it on the screen. I did something similar back in 2011, but it wasn't wireless, and the software wasn't nearly as robust as it is now.

This is a proof-of-concept demonstration, and part of a larger project. I think there's a need for this type of thing though! It's unnecessarily hard to transfer data from a MCU to a PC as it is. There's USB (For AVR V-USB is a nightmare and requires a precise, specific clock speed, DIP chips don't have native USB, and some PIC DIP chips do but then you have to go through driver hell), USART RS-232 over serial port works (but who has serial ports these days?), or USART over USB RS-232 interface chips (like FTDI FT-232, but surface mount only), but both also require precise, specific clock speeds. Pretend I want to just measure temperature once a minute. Do I really want to etch circuit boards and solder SMT components? Well, kinda, but I don't like feeling forced to. Some times you just want a no-nonsense way to get some numbers from your microchip to your computer. This project is a funky out-of-the-box alternative to traditional methods, and one that I hope will raise a few eyebrows.

Ultimately, I designed this project to eventually allow multiple "bursting" data transmitters to transmit on the same frequency routinely, thanks to syncing and forced-sync-loss (read on). It's part of what I'm tongue-in-cheek calling the Scott Harden RF Protocol (SH-RFP). In my goal application, I wish to have about 5 wireless temperature sensors all transmitting data to my PC. The receive side has some error checking in that it makes sure pulse sizes are intelligent and symmetrical (unlike random noise), and since each number is sent twice (with the second time being in reverse), there's another layer of error-detection. This is *NOT* a robust and accurate method to send critical data. It's a cheap way to send data. It is very range limited, and only is intended to work over a distance of ten or twenty feet. First, let's see it in action!

The RF modules are pretty simple. At 1.56 on ebay (with free shipping), they're cheap too! I won't go into detail documenting the ins and out of these things (that's done well elsewhere). Briefly, you give them +5V (VCC), 0V (GND), and flip their data pin (ATAD) on and off on the transmitter module, and the receiver module's DATA pin reflects the same state. The receiver uses a gain circuit which continuously increases gain until signal is detected, so if you're not transmitting it WILL decode noise and start flipping its output pin. Note that persistent high or low states are prone to noise too, so any protocol you use these things for should have rapid state transitions. It's also suggested that you maintain an average 50% duty cycle. These modules utilize amplitude shift keying (ASK) to transmit data wirelessly. The graphic below shows what that looks like at the RF level. Transmit and receive is improved by adding a quarter-wavelength vertical antenna to the "ANT" solder pad. At 433MHz, that is about 17cm, so I'm using a 17cm copper wire as an antenna.

Transmitting from the microcontroller is easy as pie! It’s just a matter of copying-in a few lines of C. It doesn’t rely on USART, SPI, I2C, or any other protocol. Part of why I developed this method is because I often use ATTiny44A which doesn’t have USART for serial interfacing. The “SH-RFP” is easy to implement just by adding a few lines of code. I can handle that. How does it work? I can define it simply by a few rules:

  • Pulses can be one of 3 lengths: A (0), B (1), or C (break).
  • Each pulse represents high, then low of that length.

To send a packet:

  • prime synchronization by sending ten ABCs
  • indicate we’re starting data by sending C.
  • for each number you want to send:
  • send your number bit by bit (A=0, B=1)
  • send your number bit by bit (A=1, B=0)
  • indicate number end by sending C.
  • tell PC to release the signal by sending ten Cs.

Decoding is the same thing in reverse. I use an eBay sound card at $1.29 (with free shipping) to get the signal into the PC. Synchronization is required to allow the PC to know that real data (not noise) is starting. Sending the same number twice (once with reversed bit polarity) is a proof-checking mechanisms that lets us throw-out data that isn’t accurate.

From a software side, I’m using PyAudio to collect data from the sound card, and the PythonXY distribution to handle analysis with numpy, scipy, and plotting with QwtPlot, and general GUI functionality with PyQt. I think that’s about everything.

The demonstration interface is pretty self-explanatory. The top-right shows a sample piece of data. The top left is a histogram of the number of samples of each pulse width. A clean signal should have 3 pulses (A=0, B=1, C=break). Note that you’re supposed to look at the peaks to determine the best lengths to tell the software to use to distinguish A, B, and C. This was intentionally not hard-coded because I want to rapidly switch from one microcontroller platform to another which may be operating at a different clock speed, and if all the sudden it’s running 3 times slower it will be no problem to decide on the PC side. Slick, huh? The bottom-left shows data values coming in. The bottom-right graphs those values. Rate reporting lets us know that I'm receiving over 700 good data points a second. That's pretty cool, especially considering I'm recording at 44,100 Hz.

All source code (C files for an ATMega48 and Python scripts for the GUI) can be viewed here: SHRFP project on GitHub

If you use these concepts, hardware, or ideas in your project, let me know about it! Send me an email showing me your project – I’d love to see it. Good luck!

page 1, page 2, page 3, page 4, page 5, page 6, page 7, page 8, page 9, page 10, page 11, page 12, page 13, page 14, page 15, page 16, page 17, page 18, page 19, page 20, page 21, page 22, page 23, page 24, page 25, page 26, page 27, page 28, page 29, page 30, page 31, page 32, page 33, page 34, page 35, page 36, page 37, page 38, page 39, page 40, page 41, page 42, page 43, page 44, page 45, page 46, page 47, page 48, page 49
All Blog Posts