Learn binary!
October 18, 2019 • tutorial
We all know decimal numbers, but computers work with binary numbers which are formed from 0's and 1's. When working with microcontrollers it is extremely helpful to know a little bit more about binary numbers as well as operations that can be performed on them (called Boolean operations). In this tutorial we will go through the basics so that we can face any binary number that crosses our path!
This is a bit like in elementary school when we learn how to add, subtract, multiply, and divide. At first we might not understand why it is important, but I am sure all of us learned to appreciate it at some later stage in life :) So let's do it!
Number systems: decimal, binary, and hexadecimal
In everyday life we use the decimal number system that is based on the number 10 (presumably because humans have ten fingers). Let's take the number 1263. We can write it like this:
1263 = 1×1000 + 2×100 + 6×10 + 3×1
We see: the number decomposes in a sum of different terms, and each of these terms has a prefactor between 0 and 9, as well as a power of ten attached to it:
1263 = 1×103 + 2×102 + 6×101 + 3×100
I hope you can see that this works for any number you can think of. But nobody stops us to do the same thing for a number system that is not based on the number 10. We can do it for anything else! The binary number system, as the name suggests, is based on the number 2. So let us try to express the decimal number 1263 in the binary system. As a first step, we can write it like this:
1263 = 1×1024 + 0×512 + 0×256 + 1×128 + 1×64 + 1×32 + 0×16 + 1×8 + 1×4 + 1×2 + 1×1
See what we did there? We simply went through all powers of 2 and added them up, with a prefactor of either 0 or 1. Why can't we have a prefactor 3 or 4 or 7? Because the binary system does not need it! Having a prefactor 3 in front of a power of 2 in the binary system would be similarly crazy as having the prefactor 27 in front of a power of 10 in the decimal system :)
Let's write 1263 explicitly in powers of 2:
1263 = 1×210 + 0×29 + 0×28 + 1×27 + 1×26 + 1×25 + 0×24 + 1×23 + 1×22 + 1×21 + 1×20
Now we can read off the individual prefactors and put them next to each other. This is how we write a number in binary:
1263 = 0b10011101111
What does that mean? Let's highlight some parts:
1263 = 0b10011101111
The orange part means that whatever follows is a binary number. This notation is specific to microcontrollers, but it is quite useful. Otherwise, if you are confronted with the expression 11 you would not know if it is the number eleven in decimal, or the number 3 in binary, would you? :)
The magenta digit is what we call the most significant digit, in this case it is the prefactor of 2 to the tenth power, which is 1024. Just like in the decimal system, where the leftmost digit is the prefactor of the largest power of ten. Conversely, the green digit is the least significant digit which stands in front of 2 to the zeroth power, which is just 1. This is identical to the decimal system!
I hope things are becoming clearer now. Let's do something fun, as a small test if we understood everything! Let's look at the hexadecimal system, which is based on the number 16! Here you can see the number 1263 expressed in powers of 16:
1263 = 4×256 - 14×16 - 15×1
Here it is with the powers explicitly spelled out:
1263 = 4×162 - 14×161 - 15×160
In general, the prefactors can range from 0 to 15. But now we have a small problem, since we want to have every prefactor to be only one symbol long. This is why in hexadecimal we use the digits 0-9 as prefactors zero to nine, and then the symbols a, b, c, d, e, f for ten to fifteen, respectively. Here is what this means:
1263 = 0x4ef
Let us highlight some stuff again:
1263 = 0x4ef
The colors have the same meanings as above: 0x is the abbreviation for hexadecimal numbers, and the magenta as well as the green digits are the most and least significant digit, respectively.
Mini binary dictionary
Let us move back to the binary system for the rest of this tutorial. There are a few technical terms that are thrown around in the binary system, and while they are rather simple in their meaning it can be quite confusing for a beginner when you have never heard them. Here is a small list of important expressions:
- Bit: A bit is the smallest unit of the binary system, it is what we called “prefactor” above. It can be either 0 or 1.
- Byte: A byte is the collection of 8 bits. This means that a byte can represent a decimal number from 0 to 255. Many PIC microcontrollers (all of the ones we use) are based on an 8-bit architecture, and many computers from the 1970s and 1980s are based on it as well.
- MSB and LSB: MSB stands for “most significant bit” (what we called most significant digit above), and LSB stands for—you guessed it!—“least significant bit” (and we called it least significant digit in the above).
- Variable<m:l> is a confusing notation, but it simply means to isolate the bits m to l out of the variable (m is the MSB and l is the LSB). For example, if X = 0b1001, then X<2:0> = 0b001. Or, if Y = 0b1001111000101, then Y<8:6> = 0b111. This notation simply isolates a part of a larger expression.
- Integer: In the context of programming languages (not in mathematics!) an integer is a collection of 32 bits. This means it can represent a decimal number from 0 to 4,294,967,295. Yep, it's large.
- Signed vs unsigned: This is another important detail in programming languages. An unsigned byte represents decimal numbers from 0 to 255. A signed byte, on the other hand, represents decimal numbers from -127 to 127. Do you see what happened? We lost one bit of information, but converted it into the sign of the number! The same is true for unsigned integers, which represent decimal numbers from 0 to 4,294,967,295, and signed integers on the other hand, which represent decimal numbers from -2,147,483,648 to 2,147,483,648.
I think this is some of the most important stuff. If I forgot something terribly important, please let me know on social media and I will add it here :)
Boolean operations on binary numbers
Binary numbers are intimately related to logic. Typically, the number 1 is associated to the logical value TRUE and the number 0 or related to the logical value FALSE. I am sure Spock would approve.
Logical values can be connected in special logical operations, which are most often called Boolean operations. It's just a name. Typical operations are NOT, AND, OR, exclusive or (XOR), negated AND (NAND), and negated OR (NOR). This sounds terribly complicated, but it is very simple. Let's understand it step by step!
Our setup is very simple. Imagine that you have two logical variables. One of them is called a, the other one is called b. Each one of these variables can be either 0 or 1. Then we can define the following Boolean operations:
The NOT operation gives the opposite of the variable:
a | NOT a |
0 | 1 |
1 | 0 |
The AND operation gives 1 if and only if both a and b are 1, and gives 0 otherwise:
a | b | a AND b |
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
The OR operation gives 1 if and only if a or b are 1, and gives 0 otherwise:
a | b | a OR b |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
The XOR (exclusive OR) operation gives 1 if and only if either a or b are 1, but gives 0 of they are both 1, and also gives 0 if both a and b are 0. This sounds complicated, but you can compare it easily to the above OR function, which is an inclusive or. The exclusive OR works like this:
a | b | a XOR b |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
The NAND (negated AND) operation gives the opposite of the AND function:
a | b | a NAND b |
0 | 0 | 1 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
The NOR (negated OR) operation gives the opposite of the OR function:
a | b | a NOR b |
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 0 |
Now comes the crazy part! Are you ready? We can also apply logic functions to entire binary numbers! For example we can calculate 0b10101100 XOR 0b10111110! How would that work?
It is very simple, we do it digit by digit! Here is an example that visualizes the operation 0b10101100 XOR 0b10111110:
1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | |
XOR | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 0 |
0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
We just applied the XOR function digit by digit, according to our table above. Similarly we can evaluate 0b10101100 AND 0b10111110:
1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | |
AND | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 0 |
1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
You get the idea! You might have one question: What if the binary numbers have different lengths? For example, what is 0b101 AND 0b10101011? This is simple, because we can always add more zeros to the left of a binary number. Here is what I mean:
0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | |
AND | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
We just added a bunch of zeros to the left. And clearly 0b101 is the same number as 0b00000101 :)
Arithmetic shifts
There is one operation that we have not yet taught about, but it is very very useful. This is called arithmetic shift. Imagine you have a decimal number and you multiply it by ten:
10 × 1263 = 10 × ( 1×103 + 2×102 + 6×101 + 3×100 )
Factoring in that extra 10 just shifts all exponents up by one:
10 × 1263 = 1×104 + 2×103 + 6×102 + 3×101 + 0×100
In plain decimal it just looks like this:
10 × 1263 = 12630
All prefactors have been shifted to the left! Multiplying by the base number is the same as an arithmetic left shift. This arithmetic left shift operation is usually abbreviated as SHL. Therefore we can also write
1263 SHL 1 = 12630
Multiplying by 10 means that everything gets shifted to the left by one! How about binary? It is very similar, but in the binary system a left shift means the multiplication by the factor 2:
2 × 1263 = 2 × ( 1×210 + 0×29 + 0×28 + 1×27 + 1×26 + 1×25 + 0×24 + 1×23 + 1×22 + 1×21 + 1×20 )
Again, we can write this as
2 × 1263 = 1×211 + 0×210 + 0×29 + 1×28 + 1×27 + 1×26 + 0×25 + 1×24 + 1×23 + 1×22 + 1×21 + 0×20
All expressed in binary this means
0b10011101111 SHL 1 = 0b100111011110
In other words: an arithmetic left shift always attaches a zero at the right.
Now it is easy to imagine that there is probably also such a thing as an arithmetic right shift. This is just the opposite. Instead of multiplying by ten (in the binary system) it divides by ten. This is not entirely true, though, because it strictly corresponds to an integer division.
This is why it is probably better to imagine the right shift really in the literal sense:
1263 SHR 1 = 126
The same is true in binary:
0b10011101111 SHR 1 = 0b1001110111
We see: the arithmetic right shift always removes the least significant digit.
Applications to microcontrollers
Okay, so why all this nonsense? We have actually used some of these operations in the source code of our previous projects! Here I want to go through three interesting examples.
But first, let's translate the Boolean operations and arithmetic shifts in programming language notation (here, a and b are two variables, and n is a number).
Boolean operation | symbol | C code |
negation | NOT a | !a (when it acts on one bit) |
~a (when it acts on a binary number) |
||
and operation | a AND b | a & b |
or operation | a OR b | a | b |
exclusive or operation | a XOR b | a ^ b |
negated and operation | a NAND b | !(a & b) |
negated or operation | a NOR b | !(a | b) |
arithmetic left shift | a SHL n | a << n |
arithmetic right shift | a SHR n | a >> n |
Okay, now some time for examples!
- Complex if-clauses: When we learned how to debounce a pushbutton in software, we used a combination of the NOT operation as well as the AND operation. It is important to pay close attention to the placement of the parentheses, since they determine the order in which the operations are evaluated:
You may have noticed that we useif ((!SW) && (SW_debounce == 0)) { status = !status; }
&&
for the AND operation. This is a small detail - Arithmetic shifts, AND operation: Let's look at the code used in the pulse width modulation article where we dim the brightness of an LED using a 10-bit duty-cycle value. The PIC16F627A is an 8-bit device, so it cannot handle 10-bit values. Therefore, the 10-bit value is shared between an 8-bit register (called CCPR1L) as well as two 1-bit registers (called CCP1X and CCP1Y):
What happens here? CCPR1L contains DC<10:2> (meaning bits 2 through 10) of the 10-bit duty cycle value. The AND operation, on the other hand, checks if certain bits are set in the duty cycle value. In line 45 we check whether bit number 0 is set and write it to the register CCP1X. In line 46 we check if bit number 1 is set. We do that by first shifting DC to the right by one (eliminating the former bit number 0) and then checking for bit 1. We could have alternatively written the following as well:// set the 10-bit duty cycle value CCPR1L = DC >> 2; CCP1X = DC & 1; CCP1Y = ( DC >> 1 ) & 1;
CCP1Y = ( DC & 2 ) >> 1;
- Setting and resetting one bit in a register: In this hypothetical example, we want to set bit number n in the register
REG
. This is how we do it:
This is how we set it to 0 (reset it):// set bit number n REG = REG | ( 1 << n );
// reset bit number n REG = REG & ( ~ (1 << n) );
- Toggling one bit in a register: In this hypothetical example, we want to toggle bit number n in the register
REG
. This is how we do it:// toggle bit number n REG = REG ^ ( 1 << n );
- Reading one bit in a register: In this hypothetical example, we want to read the status of bit number n in the register
REG
. This is how we do it:// read bit number n (the variable 'result' will be either 0 or 1) result = ( REG >> n ) & 1;
That's a few examples! I hope they make it a bit more illuminating why it is useful to know your way around 1s and 0s when you play with microcontrollers :)
Final thoughts
You did it! You survived this tutorial! I know it is probably not the most interesting stuff to read about, but we probably felt the same way in elementary school when we first learned about numbers. A couple of years later, and we need them every day. But they have also become a lot simpler, because we got used to them :)
I hope I could motivate you with this tutorial to lear a bit about binary numbers, so that they don't scare you when you have to use them some time. If something was not clear then please let me know and I will do my best to improve this tutorial! Thanks for reading!