The challenge: Create a means to measure the frequency input from a waveform generator tied to an input on the ATMega324P running at 8MHz or 1Mhz. This measurement needs to be output using a serial connection and VT100 emulation to a serial console on your machine.
1. A function generator and cable
This is a waveform generator that will allow you to dial-a-frequency. I used the AWG in the lab at our university and currently in the process of making a waveform generator from an app on my phone. Either will do just fine - but keep in mind that an AWG from a smart phone is only going to get you as high as 22KHz or so, but it is great for testing when you don't want to be tied to a piece of lab equipment.
The cable should have the correct connector to the AWG and both the ground and power clips on the other end.
2. Your ATMega wired for UART, powered, and ready
See my pages for wiring the MAX232 chip and UART Serial Communication for details. You will see reference to my UART code in the form of the #include "uart.h" in the code below.
3. A few LEDs
I use these to verify that I am reaching my interrupts and other trouble shooting. You can think of them as your "printf debugging" for hardware. When your code is woking you'll remove the lines that take up clock cycles in your ISR routines.
4. Assorted bits of wires or jumpers
Nyquist's Theorem
This is your goal and your constraint/upper bound.
In short, the highest frequency you will be able to measure from the AWG is one-half the rate of your microcontroller's clock speed. So, if you are running your 324P at 8MHz - you'll only get a read up to 4Mhz. And that's IF you are running efficient code. If you are running at 1MHz, you can measure up to 500KHz. You can always add an external crystal to get a higher clock speed if you need to measure more than what your 324P's internal clock can be set to.
Frequencies
1Hz means 1 clock cycle per second, right? So, 1KHz is 1000 cycles per second and 1MHz 1 million cycles per second.
Timer 0 vs Timer 1
The main differences for my code were about hardware vs software interrupts and how high the timers count before "over flowing." Timer 0 will let you use a hardware interrupt driven by an external clock - i.e. your waveform generator. It will overflow every 256 counts (8-bit timer). An overflow can also generate an interrupt. And I use the overflow as part of my calculations.
Timer 1 is larger - 16 bits. While most people used this counter to count their frequency with software interrupts, I used it to simply interrupt my code once a second in order to calculate and print my frequency. Otherwise I waited for it to trigger again. Technically you can use this timer to set it to Input Capture Mode which is meant specifically for frequency analysis but after my hardware problems with this project I ran out time to figure it out - it's not as straight forward as what I ended up doing here - but I would enjoy getting it working.
Pre-scaling
When we use a pre-scaler, as we will do when we set up the timers below we are effectively dividing our 324P's clock speed by some factor so that it counts at some fraction of that frequency. In other words, if I am using a pre-scaler of 1024, I am effectively telling the timer to divide my 1MHz by 1024 for it's counts. To make this meaningful, how many clock cycles will my timer have to count to trigger an interrupt every second? Well, normally at 1MHz it would have to count up to one million! But if I use a pre-scaler of 1024 the timer counts up to 1,000,000/1024 = 976.56 to trigger an interrupt every second. This is because you are essentially saying - "Hey, wait 0.00124 seconds before you count up by one."
Think about it like this: Your pre-scaler is your "zoom enhance" in terms of your timebase for measuring signals. The bigger the number the better you can "see" really fast frequencies. If you have seen a high frequency scribble wildly across and oscilloscope, you know it's easier to see what the waveform is doing if you zoom in. So, if you want to get an accurate read of a wildly fast signal, "zoom in" with your pre-scaler. Likewise, if your frequency is really slow you may need less pre-scaling in order capture the entire wavelength in a given period of time.
Clock Cycles and Timing
To reach at - or close to - the theoretical limit of measuring a frequency you need to think *SIMPLE*. This is so important. Everything you ask your microcontroller to do costs you clock cycles. The less clock cycles you have available the less accurate and smaller your count of a frequency will be. So you want your microcontroller to not waste clock cycles.
So what wastes clock cycles? Interrupts, while loops ("blocking"), and anything else that forces your 324P to be busy until you complete some secondary task other than listening to the frequency it is trying to count. Hardware interrupts will always be less resource heavy than software interrupts. If timing is at issue and you can use hardware to trigger interrupts instead of software - do it.
In our class, most students were unable to reach much more than 130KHz on their 8MHz microcontrollers for this reason.
You want to listen, not block, trigger an interrupt(s) and get OUT of the interrupt as quickly as possible, then push your calculation to your serial console. DO NOT try to calculate anything in an interrupt or put delays in an interrupt unless you have a very good reason for it. There isn't a good reason to do it for this project. You'll see what I mean below.
Process Overview
Step One: Setup your AWG
Connect your cable and turn on the AWG. Don't connect it to your board yet.
Select the square wave waveform
Change your peak to peak voltage to 3.3V and offset to 1.65 DC
Set your frequency to something a bit low like 5Hz (5 rising edges a second)
You will connect the ground wire from the cable to ground on your breadboard and the positive to pin PB0 of your 324P (refered to in this case and for the purpose of the datasheet at "T0" ).
Don't forget to set the AWG to Output
Step Two: Debugging LED's
On PortD, wire an LED to PD4, PD5, PD6 using 150Ohm resisters tied to ground as I do in the image from the Hello World How-To.
PD6 will be tied to the interrupt overflow routine
PD5 will be tied the one-second compare interrupt routine
PD4 will be tied to an "is alive" loop in the main code
You can turn these off later.
Step Three: Serial Communication
Connect your UART circuit -> to your USB/DB9 -> to your machine.
You can see the VT100 commands in my UART_Print function in main. All I am using the VT100 emulation for is to clear the screen then print in the upper left-ish.
Step Four: Get your code written, connect your AWG to your breadboard, and download your code.
Keep the frequency low at first and see if your LEDs are firing the way you would expect. Then slowly raise the the frequency and watch for the values on your console. Are they accurate - jittery - dead on?
(See the UART tutorial if you can't remember how to get a serial console working.)
Setting Up Timer 0 and Timer 1
Timer 1 is set to trigger at a passed-in value. I do this because I was just editing a single file this way as I calibrated this "one-second" representation. In theory you could also control your pre-scaler settings this way, though I didn't have enough time to implement this idea. It turns out I had to do some tweaking of the OCR1A (On Compare Register) value around the 976 mark to get less error in the final calculation. This is likely due to some part of the timing I still haven't accounted for and probably is due to the less than awesome accuracy of the internal oscillator/clock. If I used an external crystal for the clock instead I would likely get better precision.
The OCIE1A (On Compare Interrupt Enable) flag set in the TIMSK1 register tells Timer 1 to generate an interrupt when it counts to the value stored in OCR1A. The pre-scaler here is set to 1024. (Note: 1024 * 976 = approx. 1Mhz which is the internal clock speed of my 324P.)
Timer 0 is tied to the signal coming in on pin T0 and set to not use a pre-scaler. Instead the timer will trigger a count increment (in the TCNT0 register) on every rising edge it detects. Rising edges mean the start of a cycle. So, if you count each rising edge you encounter for a one second, the count you get at the end of that second is your frequency coming from the AWG. What you calculate (and print to your serial console) should match what you are generating on the AWG.
All these registers and settings are in the datasheet.
ISR (Interrupt) routines
Note: I really don't need the LED toggle in the TIMER0_OVF_vect (the overflow interrupt routing for timer 0.)
The two interrupts here are very lean. When Timer 1 counts to the value that represents a second (around 976), it triggers TIMER1_COMPA_vect which reads the count still in TCNT0 and sets a variable to 1. TCNT0 holds whatever the Timer 0 counter has gotten to before it had a chance to overflow as the one-second timer went off and as my code disables the interrupts long enough to calculate the frequency. We need those extra "left-over" counts even though they are less than 255.
TIMER0_OVF_vect just increments a count that I later - outside the interrupt - multiply by 256. This overflow count represents the current frequency being fed to the T0 pin by the waveform generator divided by 256. I then will add in the "leftovers" from TCNT0 when the one-second timer goes off and I have a fairly decent measure of the frequency.
Main()
#includes:
avr/io.h
util/delay.h
stdio.h
stdlib.h
avr/interrupt.h
uart.h
timer.h
types:
flag: int
count: unsigned long
ovf_count: unsigned long
one_second: unsigned int
The max frequency I was able to read with this setup and code was just over 486KHz. Which is pretty close to the max I would be able to read theoretically.
The main part of my code, after initializing my UART and timers, is an endless loop. Inside this loop is another that waits for a flag to be set to 1 in the TIMER1_COMPA_vect.
Inside the loop that waits for the flag, I simply blink and LED and print a space to the UART. This is mostly to make me happy that the serial communication is working and that I can visually see I am in this particular loop. In other words - that all is "still alive." In the ERRATA section below I talk about why I didn't take this out of the final version.
When the one-second is up and I exit this "still alive" loop, the first thing I do is disable the interrupts - which are firing pretty quickly as the frequency is turn up. I want enough time to calculate my value before everything keeps going and I get an incorrect value. I don't print here because it mangles my UART communication. I immediately re-enable the interrupts and let the timers do their thing.
I clear the screen with a VT100 emulation sequence, print my label, adjust the cursor to below that label with another VT100 sequence and print my calculation using the ultoa() function. The unsigned long to ascii function wants the value you are printing, the buffer size, and the decimal base. Base 10 of course being our "usual" number system rather than octal or binary or whatever.
The last 4 lines in the main loop just clear the overflow counter, the count value that records TCNT0, TCNT0 (which in hindsight I may not need to do), and sets the flag to re-enter the "still alive" loop and wait for the next second to hit.
This is the version I came to after ditching an attempt to get fancy by dynamically changing my timebase/prescaler and ORC1A value. The adjustments would have been based on wether or not I hit an over flow (upper threshold) or hadn't counted enough rising edges in TCNT0 (lower threshold). If I hadn't picked up enough edges I would adjust the pre-scaler and OCR1A value to wait longer - meaning my input frequency was small. If I hit an overflow it meant I was waiting too long and should capture rising edges more quickly.
This approach was "getting there" and I really liked the dynamic auto-adjust idea - but...at a certain point I was getting lost in the details and I had only a few hours of thinking-capability left. So I ditched everything, started fresh and went with what I really understood and was the most simple.
FAIL: What the hardware is going on?!
After not touching my 324P and breadboard since my RFID project last year (and then losing a day by locking myself out of the office where I kept my hardware) I pulled it all out of its bubble wrap, attached my serial connections, wrote a "confidence builder" LED-Hello-World, programmed the chip and....nothing. Nothing, more nothing. I inspected the LED wiring & the power regulator wiring - even replaced my LM7805. Still nothing. The last thing I did after nearly tearing my eyeballs out was double check the wiring - one pin at a time - down each pin of the 324P. It should be the FIRST thing I do after not touching a project for a while. Turns out my entire microcontroller had been "mysteriously" shifted down the breadboard by one row. Effectively shorting out the chip by - among other things - connecting my VCC lines to ground and vice versa. Geek pranks - they'll get you - or at least waste precious time. Someone will pay in equal geek-pranks. I am lucky I didn't scare the clients at the coffeehouse I was at by strange smells, smoke, and foul language.
FAIL: UART what now?
This was my single biggest time killer, never mind the freak-out of not knowing why my 324P wouldn't blink at me that first day. This issue was nasty. I was reusing and writing code that had no reason NOT to work. I had a peer review every line, I had double-tripple checked my power input and wiring, my code compiled without errors, the status check LEDs I use like printf statements were working, debugging wasn't complaining at all. It was pretty obvious that there was something wrong with my serial communication. But where? The console settings, the baud rate, the word sizes, parity, stop bits, cable, wires, timing issues? Was my breadboard *that* worn out on the TX/RX rows? I tried all the "check list" items for debugging the broken.
I even considered maybe my board or serial circuit was being under-powered because my usb port on my machine might be pushing less power if my laptop was not plugged to wall power. I switched computers, plugged mine in, swapped cables, swapped wires, messed with the baud rate and the UART initializations. Nothing...then it would work...then stop...this went on for a couple days...again nothing. I still didn't have a working algorithm for the frequency calculation because I could not see what I was getting right or wrong. The trial day of our challenge came and went and the only reason I didn't lose points was because our professor considered it a "ramping up" trial run for the beginning of the semester. I would think I had solved the issue with whatever I happened to be trying just before it started working again - until I replugged the cable and everything would go dark then start randomly working again sometime later.
Finally, late in the lab with only a day left to finish, Andy from class saw me struggling and frustrated. We walked through many of the same things I had already tried. My code worked fine plugged to his machine with his cables. So I tried his cable and my machine and was thwarted by annoying driver issues - but this led him to try my integrated DB9/MAX232 board instead of his on his own setup and BAM! after all that time I spent trying to solve the issue I never thought it would be the UART board itself. The rest of the evening was wasted too because Radio Shack employees don't have the foggiest what a MAX232 is, or UART, or DB9 and they also don't carry the MAX232. One guy looked at me wide-eyed and said "I don't know what language you just spoke." The Reuseum was closed but the owner said he could meet me early the next morning and we would "dig one out of the boxes." My friend Adrian couldn't find his breadboard from a previous semester and was on his way out for the evening. So, the day before the competition, Adrian drops off the MAX232 he finally found and I am up and rolling with serial in 20 minutes. I swing by Reuseum to pick up an extra just in case - how did I not have a MAX232 back up in the first place?! Finally I could start writing code in ernest and most of my class mates were already tracking frequencies as high as 300KHz.
FAIL: What time is it? No really...what time is it?
My serial communication was working, I had a decent shell of code, I had done my research, asked questions of people who knew more about timing and embedded systems than I. I had read the datasheet several times, changed my approach twice and completely started over with my code a couple times as I sorted through what I thought would work and what was faster. I had started with one algorithm, tried it another way with an idea from a friend and finally ditched all the advice I didn't fully understand and went for what I could explain and understand in simple terms.
Right on.
Except my debug LED on the Capture ISR was blinking WAY too slow. WAY too slow. So I cut the one-second timer value in half, then in half again, then in half....oh weird...it starts blinking at one second when the number gets close to 1024 in OCR1A. Huh - that would mean I am not running at 8MHz, right? So I checked my fuses in my Makefile - they looked right according to what I understood. I asked a couple people who were running at 8MHZ what their fuses were set at - the same. I asked a classmate to double check my calculations of the internal clock speed at 1MHz. I had no choice but to reconfigure my settings in the code to maximize my readings for 500KHz since I wouldn't have time to both debug my tool chain AND finish this project with just a few hours to go. Luckily the change was easy.
FAIL: Longs, longs, use longs for numbers bigger than ints!! And then use ultoa() to print the value!
Printing negative numbers? Yeah, so was I until I adjusted my types correctly and then *printed* the right type. Thank you James for figuring out ultoa() and pointing it out to me. You can see how I use it in the main code.
FAIL: Dont reinitialize timers. Do it once and leave them alone.
ERRATA: If I take out that while(flag == 0) loop what creates a slight delay I can't get correct reads. Not sure why yet.
ERRATA: My numbers do still jitter around the actual frequency value the waveform generator is supplying when I print to my console.
I suspect this is related to the less than perfect internal clock, because I am not using the low-pass filter others use and there may be something in my own timing with my code.
I did not really understand the connections between Nyquist's Theorem, timing, timers, interrupts, hardware vs software interrupts, clock speeds, and simplicity until this project. Most of it seems so straight forward now but to start I was pretty daunted by how to identify and approach the problem.
Andy pointed out the offset with the Waveform Generators. That was great because as a CS student I hadn't really used them much so I don't always think of those details.
While I was being forced to spend more time thinking over the project because of the hardware issues, I wasn't really clear on what use the overflow on Timer 0 was going to be. Then I was calculating the tip on a check from a restaurant using a technique I learned as a kid and explaining it to a friend: "To calculate a tip you take the total, move the decimal to the left one and multiply that value by 2 for a 20% tip." That's when it hit me (face to plam) that the overflow just represented a fraction of the clock cycles registered by the counter and it could then be multiplied by the top value of the 8-bit counter (or 255) to get the total count over a second plus what was still in TCNT0. <-- which I just realized I set that to 256 not 255 in my calculation code.
Trouble shooting:
Check your hardware! See my annoying Hardware Fails above.
Read the Datasheet - no really RTFM before you Google.
Ask a guru! The worst thing you can do for your skills is be afraid to ask. My friend Adrian and another embedded guru reminded me about Nyquist, super simple interrupts, and even the ultoa() function for printing longs.