Digital Logic

Contributors: SFUptownMaker
Favorited Favorite 35

Boolean Logic in Programming

All of this can be applied in the programming world, as well. Most programs are simply decision trees: "if this is true, then do this". To explain this, we'll use C-code in an Arduino context.

Bitwise Logic

When we talk about "bitwise" logic, what we really mean is logical operations which return a value. Take, for example, this piece of code:

byte a = b01010101;
byte b = b10101010;
byte c;

We can do a bitwise operation using 'a' and 'b' and putting the result into 'c'. Here's what that looks like:

c = a & b;  // bitwise AND-ing of a and b; the result is b00000000
c = a | b;  // bitwise OR-ing of a and b; the result is b11111111
c = a ^ b; // bitwise XOR-ing of a and b; the result is b11111111
c = ~a;  // bitwise complement of a; the result is b10101010

In other words, the each bit in the result is equal to the operation applied to the two corresponding bits in the operands:

Okay, that's great, but what of it? It turns out we can do some pretty useful things by using bitwise operators to manipulate registers: we can selectively clear, set, or toggle single bits, check to see if a bit is set or clear, or if several bits are set or clear. Here are some examples using these operations:

c = b00001111 & a; // clear the high nibble of a, but leave the low nibble alone.
                                // the result is b00000101.
c = b11110000 | a; // set the high nibble of a, but leave the low nibble alone.
                                // the result is b11110101.
c = b11110000 ^ a; // toggle all the bits in the high nibble of a.
                                // the result is b10100101.

Any bitwise operation can be self-applied by combining it with the equal sign:

a ^= b11110000; // XOR a with b11110000 and store the result back in a
b |= b00111100; // OR b with b00111100 and store the result back in b

Bit Shifting

Another useful bitwise operation that can be performed on a piece of data is a bit shift. This is simply a slide of the data left or right by a certain number of places; data which is shifted out disappears and is replaced by a 0 being shifted in from the other end.

byte d = b11010110;
byte e = d>>2;  // right-shift d by two positions; e = b00110101
e = e<<3; // left-shift e by three positions; e = b10101000

We'll demonstrate some uses of bit shifting later. One very useful application for bit shifts is multiplication and division: each right shift is the same as a division by two (although remainder information is lost) and each left shift is the same as a multiplication by two. This is useful because multiply and divide are often very time expensive operations on small processors, like the Arduino's, but bit shifts are usually very efficient.

Comparison and Relational Operators

We'll want some way to compare two values: there is a family of operators which do just that and return "TRUE" or "FALSE" depending on the result of the comparison.

  • == "is equal to" (true if values are equal, false otherwise)
  • != "is not equal to" (true if values are different)
  • > "is greater than" (true if left operand is greater than right operand)
  • < "is less than" (true if left operand is less than right operand)
  • >= "is greater than, or equal to" (true if left operand is greater than, or exactly equal to, right operand)
  • <= "is less than, or equal to" (true if left operand is less than, or exactly equal to, right operand)

It is generally quite important that the values compared be of the same data type; unexpected things can happen if you compare a "byte" and an "int", for example.

Logical Operators

Logical operators are operators which produce a "TRUE" or "FALSE", rather than a new value of the same type. They're more like what we tend to think of as conjunctions: "If it's not raining, and it's windy, go fly a kite". Translated into C, that sentence might look like this:

if ( (raining != true) && (windy == true) ) flyKite();

Note the parentheses around the two subclauses. Though not strictly necessary, it's good practice to keep your code as readable as possible by grouping subclauses together.

Also note that the logical AND operator (&&) produces a true/false answer based on whether or not the subclauses produced true/false answers. We could just as easily have had a numeric value in one of the subclauses:

if ( (raining != true) && ( (windSpeed >= 5) || (reallyBusy != true) ) ) flyKite();

This clause will send me out to fly a kite, so long as it's not raining, but only if there's some wind or I'm not busy (I will try and fly with no wind).

Again, note the parentheses. If we remove the parentheses around "(windSpeed >= 5) || (reallyBusy != true)" -- with || representing the OR operator -- we create an ambiguous statement which may or may not do what we want it to do.

Flow Control

Now that we can create complex logical statements, let's look at the things we can do with the answers to those questions.

if/else if/else Statements

The simplest decision is "if/else". If/else if/else allow you to set up a series of tests, of which only one can ever be executed at any time:

if ( reallyBusy == true ) workHarder();
else if ( (raining != true) && (windy == true) ) flyKite();
else work();

With those three statements, I'll never fly a kite if I'm really busy, and if I'm not really busy, and it's not a good day for it, I'll just keep working. Let's change the else if() to if(), like so:

if ( reallyBusy == true ) workHarder();
if ( (raining != true) && (windy == true) ) flyKite();
else work();

Now, if we've got a nice kite flying day on our hands, even if I'm really busy, I'll only work harder for a very, very short period--basically, right up until I notice that it's nice out. Furthermore, if it's not a nice day, my work harder status will be downgraded to just plain old work immediately after I start working harder!

You can imagine what would happen if we replaced "workHarder()" with "turnLEDOn()" and "work()" with "turnLEDOff()". In the first case, the LED may be on for some time, or off for some time. In the second case, however, regardless of the state of the "reallyBusy" flag, the LED will turn off almost instantly after the first if() statement turned it on, and you'd find yourself sitting around wondering why the "reallyBusy" light never turns on!

switch/case/default Statements

Less powerful but more readable than a long chain of if/else statements, switch/case/default allows you to make a decision based on the value of a variable:

switch(menuSelection) {
  case '1':
  case '2':
  case '3':

The switch() statement only allows us to check equivalence, but since that's a fairly common thing to want to do, it comes in pretty handy. There are two really important things to notice about this: the "break;" statements and the "default:" case.

"default:" is what will be executed if none of the others match. It's not strictly necessary; if there's no default case, than nothing happens if all the matches fail. Of course, you usually want something to happen, and it's best not to assume that it's impossible for all matches to fail.

"break;" jumps out of the current conditional. It can be used inside of any type of conditional (more on that later), and in this case, a failure to include a break at the end of each case will result in code after the case being executed, even if subsequent case matches fail.

while/do...while Loops

So far, we've looked at code for making a decision once. What if you want to repeat an action, over and over, as long as a condition holds? That's where while() and do...while() come into play.

while (windy == true) flyKite();

When your code reaches a while() statement, the program evaluates the conditional ("Is it windy?") and, if it evaluates to "TRUE", executes the code. Once code execution is complete, the conditional will be evaluated once more. If the conditional is still "TRUE", the code will execute again. This repeats over and over, until the conditional evaluates to "FALSE" or a break statement is encountered.

You can nest an if() statement (or a switch(), or another while(), or in fact anything you want) inside your while() loop:

while (windy == true) {
  if (bossIsMad == true) break;

So, with that loop, I'll fly my kite until the wind gives out or my boss gets mad at me.

A variation on while() loops is the do...while() loop.

do {
} while (windy == true);

In this case, the code inside the brackets runs once, even if the conditional is false. In other words, regardless of the state of the wind, I'll go out and drag a kite around, but if the wind isn't there, I'll give up.

Finally, by sticking "TRUE" into the conditional, it's possible to create code that will execute forever:

while(true) {

With that chunk of code, I'll just keep dragging my kite around the field forever, regardless of wind, my boss's satisfaction with it, hunger, cougars, etc. It's still possible to break out of that code using the break statement, of course; it will just never cease execution on its own.

for() Loops

The last type of conditional execution we need to consider is the for() loop. for() loops allow us to execute a chunk of code a specific number of times. The syntax of a for loop looks like this:

for (byte i = 0; i < 10; i++) {
  Serial.print("Hello, world!");

Within the for() loop parentheses are three semicolon separated statements. The first one is the iterator: the variable which we are changing with each pass. It's also where the iterator's initial value is set. The center one is the comparison we'll do after each pass. Once that comparison fails, we break out of the loop. The last statement is what we want to do after each pass through the loop. In this case, we want to increment the iterator by one.

The most common error in a for() loop is an off-by-one error: you mean for the code to execute 10 times, but it ends up executing 9 times, or 11 times. This is usually a result of using a "<=" instead of "<" or vice versa.