⚠️ WARNING: This page 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.
I came across the need for a quick and dirty display to show a 4 digit number from a microcontroller. The right way to do this would be to use a microcontroller in combination with a collection of transistors and current limiting resistors, or even a dedicated 7-segment LED driver IC. The wrong way to do this is to wire LEDs directly to microcontroller IO pins to source and sink current way out of spec of the microcontroller… and that’s exactly what I did! With no current limiting resistors, the AVR is sourcing and sinking current potentially far out of spec for the chip. But, heck, it works! With 2 components (just a microcontroller and a 4 digit, 7-segment LED display) and a piece of ribbon cable, I made something that used to be a nightmare to construct (check out this post from 3 years ago when I accomplished the same thing with a rats nest of wires - it was so much work that I never built one again!) The hacked-together method I show today might not be up to spec for medical grade equipment, but it sure works for my test rig application, and it’s easy and cheap to accomplish… as long as you don’t mind breaking some electrical engineering rules. Consider how important it is to know how to hack projects like this together: Although I needed this device, if it were any harder, more expensive, or less convenient to build, I simply wouldn’t have built it! Sometimes hacking equipment together the wrong way is worth it.
Segments are both current sourced and sunk directly from AVR IO pins. Digits are multiplexed with 1ms illumination duration. I don’t really have a part number for the component because it was a China eBay special. The display was $6.50 for 4 (free shipping). That’s ~$1.65 each. The microcontroller is ~$1.[/caption]
SCHEMATIC? If you want it, read this. It’s so simple I don’t feel like making it. Refer to an ATMega48 pin diagram. The LCD is common anode (not common cathode), and here’s the schematic on the right. I got it from eBay (link) for <$2. The connections are as follows:
-
Segments (-) A…H are directly wired to PD0…PD7
-
I call the decimal point (dp) segment “H” - I don’t use current limiting resistors. I’m not making a consumer product. It works fine, especially multiplexed. Yeah I could use transistors and CLRs to drive the segments to have them bright and within current specifications, but I’m not building an airplane or designing a pacemaker, I’m making a test device at minimum cost! Direct LED wiring to my microcontroller is fine for my purposes.
-
I am multiplexing the characters of my display. I could have used a driver IC to simplify my code and eliminate the current / wiring issues described above.
-
A MAX7219 or MAX7221 would have been easy choices for this (note common anode vs. common cathode drivers / displays). It adds an extra $5 to my project cost though, so I didn’t go with a driver. I drove the segments right out of my IO pins.
-
Characters (+) 1…4 are PC0…PC3
-
Obviously I apply +5V and GND to the appropriate AVR pins
Here it all is together in my microcontroller programming set up. I’ll place this device in a little enclosure and an an appropriate BNC connector and either plan on giving it USB power or run it from 3xAA batteries. For now, it works pretty darn well on the breadboard.
Here is my entire programming setup. On the top left is my eBay special USB AVR programmer. On the right is a little adapter board I made to accomodate a 6 pin ISP cable and provide a small breadboard for adding programming jumpers into a bigger breadboard. The breadboard at the bottom houses the microcontroller and the display. No other components! Well, okay, a 0.1uF decoupling capacitor to provide mild debouncing for the TTL input.
Let’s talk about the code. Briefly, I use an infinite loop which continuously displays the value of the volatile long integer “numba”. In the display function, I set all of my segments to (+) then momentarily provide a current sink (-) on the appropriate digit anode for 1ms. I do this for each of the four characters, then repeat. How is the time (the value of “numba”) incremented? Using a hardware timer and its overflow interrupt! It’s all in the ATMega48 datasheet, but virtually every microcontroller has some type of timer you can use to an equivalent effect. See my earlier article “Using Timers and Counters to Clock Seconds” for details. I guess that’s pretty much it! I document my code well enough below that anyone should be able to figure it out. The microcontroller is an ATMega48 (clocked 8MHz with an internal RC clock, close enough for my purposes).
#define F_CPU 8000000UL // 8mhz
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
// for simplicity, define pins as segments
#define A (1<<PD0)
#define B (1<<PD1)
#define C (1<<PD2)
#define D (1<<PD3)
#define E (1<<PD4)
#define F (1<<PD5)
#define G (1<<PD6)
#define H (1<<PD7)
void setDigit(char dig){ // set the digit starting at 0
PORTC=(1<<dig)|(1<<PC4); // always keep the PC4 pin high
}
void setChar(char c){
// given a number, set the appropraite segments
switch(c){
case 0: DDRD=A|B|C|D|E|F; break;
case 1: DDRD=B|C; break;
case 2: DDRD=A|B|G|E|D; break;
case 3: DDRD=A|B|G|C|D; break;
case 4: DDRD=F|G|B|C; break;
case 5: DDRD=A|F|G|C|D; break;
case 6: DDRD=A|F|G|E|C|D; break;
case 7: DDRD=A|B|C; break;
case 8: DDRD=A|B|C|D|E|F|G; break;
case 9: DDRD=A|F|G|B|C; break;
case 31: DDRD=H; break;
default: DDRD=0; break;
}
}
void flashNumber(long num){
char i;
for (i=0;i<4;i++){
setChar(num%10);
if (i==2){DDRD|=H;} // H is the decimal point
setDigit(3-i);
num=num/10;
_delay_ms(1); // time to leave the letter illuminated
}
}
volatile long numba = 0;
volatile long addBy = 1;
ISR(PCINT1_vect){ // state change on PC4
if ((PINC&(1<<PC4))==0) {addBy=0;} // pause
else {numba=0;addBy=1;} // reset to 0 and resume
}
ISR(TIMER1_OVF_vect){
TCNT1=65536-1250; // the next overflow in 1/100th of a second
numba+=addBy; // add 1 to the secound counter
}
int main(void)
{
DDRC=(1<<PC0)|(1<<PC1)|(1<<PC2)|(1<<PC3); // set all characters as outputs
DDRD=255;PORTD=0; // set all segments as outputs, but keep them low
TCCR1B|=(1<<CS11)|(1<<CS10); // prescaler 64
TIMSK1|=(1<<TOIE1); //Enable Overflow Interrupt Enable
TCNT1=65536-1250; // the next overflow in 1/100th of a second
// note that PC4 (PCINT12) is an input, held high, and interrupts when grounded
PCICR |= (1<<PCIE1); // enable interrupts on PCING13..8 -> PCI1 vector
PCMSK1 |= (1<<PCINT12); // enable PCINT12 state change to be an interrupt
sei(); // enable global interrupts
for(;;){flashNumber(numba);} // just show the current number repeatedly forever
}
I edit my code in Notepad++ by the way. To program the chip, I use a bash script…
avr-gcc -mmcu=atmega48 -Wall -Os -o main.elf main.c -w
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avrdude -c usbtiny -p m48 -F -U flash:w:"main.hex":a -U lfuse:w:0xe2:m -U hfuse:w:0xdf:m
Nothing here is groundbreaking. It’s simple, and convenient as heck. Hopefully someone will be inspired enough by this write-up that, even if they don’t recreate this project, they’ll jump at the opportunity to make something quick and dirty in the future. It’s another example that goes to show that you don’t have to draw schematics, run simulations, do calculations and etch boards to make quick projects. Just hack it together and use it.
__Update a two days later… __I found a similarly quick and dirty way to package this project in an enclosure. I had on hand some 85x50x21mm project boxes (eBay, 10 for $14.85, free shipping, about $1.50 each) so I used a nibbler to hack out a square to accomodate the display. After a little super glue, ribbon cable, and solder, we’re good to go!
Related reading for the technically inclined:
⚠️ WARNING: This page 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.
How long does a particular bit of Morse code take to transmit at a certain speed? This is a simple question, but when sitting down trying to design schemes for 10-minute-window QRSS, it doesn’t always have a quick and simple answer. Yeah, you could sit down and draw the pattern on paper and add-up the dots and dashes, but why do on paper what you can do in code? The following speaks for itself. I made the top line say my call sign in Morse code (AJ4VD), and the program does the rest. I now see that it takes 570 seconds to transmit AJ4VD at QRSS 10 speed (ten second dots), giving me 30 seconds of free time to kill.
Output of the following script, displaying info about “AJ4VD” (my call sign).
Here’s the Python code I whipped-up to generate the results:
xmit=" .- .--- ....- ...- -.. " #callsign
dot,dash,space,seq="_-","_---","_",""
for c in xmit:
if c==" ": seq+=space
elif c==".": seq+=dot
elif c=="-": seq+=dash
print "QRSS sequence:n",seq,"n"
for sec in [1,3,5,10,20,30,60]:
tot=len(seq)*sec
print "QRSS %02d: %d sec (%.01f min)"%(sec,tot,tot/60.0)
__How ready am I to implement this in the microchip? __Pretty darn close. I’ve got a surprisingly stable software-based time keeping solution running continuously executing a “tick()” function thanks to hardware interrupts. It was made easy thanks to Frank Zhao’s AVR Timer Calculator. I could get it more exact by using a /1 prescaler instead of a /64, but this well within the range of acceptability so I’m calling it quits!
Output frequency is 1.0000210 Hz. That’ll drift 2.59 sec/day. I’m cool with that.
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 page 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.
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:
- use inexpensive thermistors instead of linear temperature sensors (like LM335)
- use inexpensive quarter-watt resistors as heaters instead of power resistors
- be able to set temperature with a knob
- be able to monitor temperature of the heater
- be able to monitor power delivered to the heater
- 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"
⚠️ WARNING: This page 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.
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()