|FIGURE 1. World's Geekiest Shirt Contest.|
Recently, we learned about both of those binary 10 kinds of people. This time, we will apply it to understanding Binary Coded Decimal (BCD) to use in code for a wearable Butterfly Alarm Clock (see the safety pin shown in Figure 2). We will also discuss functions and variables and learn that 'automatic' variables can only be used in the function where they are declared but 'external' variables — declared outside any function — can be used anywhere in the software module.
We've been using functions for a while, so you should have a feel for them. But let’s go ahead and take a more detailed look. Functions encapsulate a computation. Think of them as building material for C programs. A house is built of studs, nails, and panels. The architect is assured that all 2x4 studs are the same, as are each of the nails and the panels, so there is no need to worry about how to make a 2x4 or a nail or a panel — you just stick them where needed. Likewise, a function can be considered a standard component for building software. It will always do whatever it does and you don't have to worry about how it does it.
|FIGURE 2. Butterfly Back: Safety Pin and Piezo Speaker.|
Encapsulation is a key idea in programming and provides the possibility of making chunks of code convenient to use. And just as important, it provides a way to make tested code reusable while not allowing the programmer to mess with it and chance breaking something. We saw some of this in our earlier introduction to libraries.
Functions also help clarify code. A function should do only one thing. If you find yourself writing a function that seems to be doing two separable things, try separating it into two functions for clarity.
A function must be declared or defined in a module (a single text file of code) — usually in a header file or before the main() function — before it is encountered by the compiler. In Workshop 4, you saw a function declaration in smws4.h:
This told the compiler that we would be using a function named receiveByte that takes no parameters (void) and returns a byte; that to prevent confusion, we call uint8_t (a standard data type for avrlibc). Okay, this seems like a mighty confusing way to prevent confusion, but in regular old-fashioned C we would call a byte an 'unsigned char,’ however, there is no guarantee what size a char is, and having an unsigned character doesn't make a lot of sense anyway. What would a character '-A' be? So anyway, we use the uint8_t telling us that our data type is an unsigned integer made of eight bits and God only knows what the '_t' is there for other than to make typing harder.
Functions can have parameters — a list of variables that may be used by the function — such as a1 and a2 in:
uint8_t adder(uint8_t a1, uint8_t a2)
They can return values, such as a uint8_t in the adder() function.
One thing that often confuses folks about function parameters is that the function only sees a copy of the parameter, not the original variable. The confusion comes when one thinks that by changing the received parameter in the function body that on return, the caller will see that parameter changed. It won't.
Let’s look at a bad function: adder() that has three parameters — adds the first two and puts the results into the third parameter:
// bad function
void adder(uint8_t a1, uint8_t a2,
results = a1 + a2;
Let's call it with:
uint8_t add1 = 1;
uint8_t add2 = 1;
uint8_t results = 0;
if(results == 2) getRewarded();
If you think 1 + 1 = 2 in this example, then prepare to get boinked. In the adder function, 'results' = 2, but this doesn't change anything in the parameter list in the adderTest() function that called the adder() function.
Ouch! Boinking hurts, so let's make adder work right. We change the return type from void to uint8_t:
// good function
uint8_t adder(uint8_t a1, uint8_t a2)
results = a1 + a2;
Now you will get rewarded. (And we have a useless function. If we want to add 1 and 1, we just use the + operator to add them, but you get the point.)
Another way to do the adder() thing would be to use an external variable (global). These are variables defined outside any function, usually in a header or before main(), and are available for any function to use. We contrast external variable to automatic variable, such as the uint8_t results declared in the above adder() function. This variable is created 'automatically' every time the function is called and disappears when the function is exited.
Using external variables, we could write:
// define the function
void adder(uint8_t, uint8_t);
// create an external variable
uint8_t results = 0;
unsigned char add1 = 1;
unsigned char add2 = 1;
if(results == 2) getrewarded();
// This works because 'results' is external
void adder(unsigned char a1, unsigned char a2)
results = a1 + a2;
This would work fine, unless an interrupt triggered right after we set results in adder() and changed external variable 'results' to 3. Then, when the interrupt finishes and we look at results in main() we get boinked again.
Be very careful using external variables. You never know where they've been or what kind of nasty stuff they might track in. And, unlike automatic variables, they permanently occupy memory. Carefully used however (as in our wearable clock), they are just fine.
Variable names have scope, meaning the sections of code where they are recognized that determine how they can be used. External variables can be used by anything in the module, but variables defined within a function can only be used by that function.
We can add the qualifier 'volatile' to a variable name to tell the compiler to leave it alone when it is optimizing the code (it’s complicated and compiler-dependent). Usually, we only need to do this when the variable will be changed by an interrupt as in our clock code. Newbies using interrupts often forget about volatile and have mysterious difficulties to finding bugs as a result.
Due to space limitations, we will put some additional C syntax and compiler topics into a downloadable supplement listed at the end of this article.
Computers are made of electric circuits that rely on one sequence of events completing before the next sequence of events can be looked at (sequential state machines). Changes in voltages and currents take time to stabilize so for each change in the state of the microcontroller, it must wait a bit of time to let things settle down before allowing states to change again. The quickest that the computer circuits can settle determines the fastest speed the computer can run. The datasheets provide this value, for instance, some AVRs have a maximum frequency of 20 MHz, meaning that they will run reliably at that speed. They might run just fine at 25 MHz, but Atmel won't guarantee it and it is kind of foolish to try to 'overclock' a computer, it might seem to run perfectly until a critical event and run off crazy and raise the landing gear moments before touchdown.
Also, the faster you run a CPU, the more power it uses. The Butterfly uses an ATmega169 that can run at 8 MHz, but what's the hurry? Many AVRs can run off an internal oscillator (cheap, but can be inaccurate) or an external clock (costs extra, but can be much more precise). The Butterfly uses the interesting method of doing both. It has an external watch crystal that runs at 32768 beats per second — way slow for a computer, but very cheap and accurate. It uses this external crystal to calibrate the internal oscillator to run at 2 MHz. This provides us with a fast and accurate CPU clock and gives us the opportunity to use the crystal to generate pulses for a real time clock.
Think for a moment about the watch crystal frequency 32768. Seem weird? Well, remembering our Workshop on binary numbers we see that it is binary:
Binary 1000 0000 0000 0000
But more importantly, 32767 — one beat short of 32768 is:
Binary 0111 1111 1111 1111
If we think electronics, we note that the highest bit changes from 0 to 1 once each second, so if we hook up a circuit that can keep a binary count (piece of cake) that can interrupt our code each time this bit changes from 0 to 1, then we can keep a count of seconds. Our main program will run along merrily doing whatever it does and once each second, we can have the current state stored, run an interrupt handler that can add one second to an external seconds variable, and then restore the main program state which will resume whatever it was doing — unaware that a second has passed. That is, until it gets to some code that checks the external seconds variable against the local (automatic) seconds variable that it uses so that it would note that a second has passed and do whatever it does for that event. For instance, change the seconds value on the Butterfly LCD.
We will simplify our lives a bit by using the library libsmws7.a (see end of article for download information) that hides all the timer interrupt and LCD driver stuff. (You are welcome!)
To Human Readable Time We can keep a count of seconds, but what good does it do us if our watch reads 40241? If the count started at midnight, then this number of seconds would indicate that the time is ten minutes and 41 seconds after 11:00 in the morning. So, we are going to need to do some computing to convert the count to something we can read.
Binary Coded Decimal is a coding trick (or 'algorithm' for the more OCD among us) that eases the storage and conversion of binary numbers to decimal numbers. Say you have a count of the watch crystal beats in binary and want to display this number on an LCD in human readable decimal numbers. Using BCD, we can divide an eight bit byte into two, four bit nibbles and store them as single decimal integers — 0 to 9 — in each nibble. Since 9 is the largest decimal digit and we can store two digits per byte, 99 is the largest decimal value we can store. Yes, using a byte to encode a maximum value of 99 when it could encode up to 256 values wastes space, but it provides a good way to store human readable decimal digits.
If a decimal number in a byte is less than 99, we can convert it to a BCD byte using the following algorithm (or 'trick' for the less OCD among us):
Set the initial byte (uint8_t) to some decimal two-digit value:
uint8_t initialByte = 54;
Declare a variable for the upper nibble value:
uint8_t high = 0;
Count the tens in initialByte:
while (initialByte >= 10)
initialByte -= 10;
After this runs, the initialByte now contains only the ones integer from the original byte and 'high' variable contains the tens, that is: high = 5 and initialByte = 4. We combine the high and low nibbles to get the converted byte:
convertedByte = (high << 4) |
You do remember the << shift operator from an earlier workshop, don't you? Here it moves the high value into the high nibble. The | OR operator then combines it with the initialByte which contains only the low nibble.
We define two bytes — tens and ones — and a third byte — number — which we set to a value in the range of 0 to 99:
uint8 tens = 0;
uint8 ones = 0;
uint8 number = 54;
We use the character to BCD algorithm written as the function byte2BCD2(uint8_t) to convert the 'number' to the BCD equivalent in tens:
tens = byte2BCD2(Number);
Now tens has the BCD of the tens in the upper nibble and of the ones in the lower nibble.
We can convert this to an ASCII character for the integer by remembering that the numerical value of the ASCII character '0' is 48 and each following character value is a simple increment of the previous one. Meaning that adding the number 4 to the value of the ASCII character '0' — which is 48 — yields the ASCII character '4' (48+4 = 52 which is the ASCII code value of the character '4'). This is a good time to look at the ASCII chart included in this month's workshop downloads. So, the conversion of a decimal integer to its ASCII equivalent character code is the simple addition of 48 to the decimal integer.
Since the byte2BCD2 function loaded both the tens and ones parts of the number into the high and low nibble of tens, we need to extract the ones and the tens so that we can add 48 (ASCII '0') to get the ASCII characters for Number:
// Load the ones variable with
// the high and low nibble
ones = tens;
// Mask off the high nibble
// and add 48 ('0') to get the ASCII
ones = (ones & 0x0F) + '0';
Finally, we get the tens by right shifting the byte four-bits, which we use as the ASCII character offset:
// Shift the high nibble to the low nibble
// and add '0' for ASCII
tens = (tens >> 4) + '0';
We'll use these ideas in the showClock function in the software.
Oh, great! Now that we've gotten some basics down, we are out of space again. Looks like the detailed software discussion will have to be deferred to a supplement where we will discuss creating functions to keep track of seconds, minutes, and hours; show them on the LCD; and set an alarm time and when that time comes, beep the Butterfly piezo element. We will have the source code and two supplements for this workshop: Smiley's Workshop 7 — Supplement 1: Some More C Syntax; and Supplement 2: The Butterfly Alarm Clock Software available in the workshop7.zip file in the downloads section of this article's page at www.nutsvolts.com or www.smileymicros.com.
Next time, we will finish the introductory C syntax and learn how to use the Butterfly sensors to measure light, temperature, and voltage. NV
Joe Pardue has a BSEE and operates www.smileymicros.com from the shadows of the Great Smokey Mountains in Tennessee. He is author of Virtual Serial Port Cookbook and C Programming for Microcontrollers
|WORKSHOP BOOKS & KITS|
If you’re following this series and want to read further or jump in and try the experiments, click this banner to go to the Nuts & Volts webstore where you’ll find all the hardware kits being used throughout, as well as Joe’s books. It’s a great way to learn microcontroller programming with C and get the kind of hands on experience, as you go, that brings the lessons to life.
Smileys Workshop 200902 (Workshop7.zip)