Bit Operations in C¶
Learning Objectives: After completing this material, you will understand why bit operations are important in low-level programming and how to use them in the C programming language.
Memory-Mapped I/O¶
In embedded systems, communication with peripheral devices is often managed devices can be managed in two different ways Memory-Mapped I/O and Port-Mapped I/O.
In Port-Mapped I/O peripheral devices are accessed through a separate address space, distinct from the system's main memory. This means that the processor uses special I/O instructions (e.g., IN and OUT instructions on x86 architecture) to communicate with the devices. Each device is assigned a unique I/O port address, and the processor interacts with the device by reading from or writing to these ports.
However, most common way of communicating with peripherals is through Memory-Mapped I/O. As we remember from the previous discussion on general computer organization, peripheral devices are connected to the processor via buses. To make working with peripherals easier for programmers, a section of the computer's RAM can be reserved for use by peripheral devices as part of the computer's architecture. Conveniently, these blocks can be linked (via buses) directly to the internal memory of the peripheral device with the help of control logic. This arrangement is known as Memory-Mapped I/O.
GPIO port mapping in ATMega328P.
The brilliance of the idea lies in the fact that within the executable program, communication with the peripheral device can be done transparently by reading from or writing to these reserved memory locations. These memory locations reserved for device use are commonly referred to as registers.
For example, we can send a message to a peripheral device by writing data to the designated memory location, and the control logic ensures that the message reaches the peripheral. Similarly, messages from the peripheral device to the computer can be read from the corresponding memory location after the control logic has made the messages visible in the reserved memory space. A common approach is to control peripherals through these memory locations, for example, turning on the backlight of an LCD screen by changing the value of control bits in the allocated memory location. Hence, the importance of bit operations.
These memory locations are often standardized in the system (especially in embedded systems), and development environments usually provide support in the form of predefined constants and variables for use in programs.
Example: LCD screen control using memory-mapped I/O.
In the system's RAM, memory locations could be allocated for the display as follows:
- Two 8-bit memory locations:
- 8-bit control bus, although we only need lines/signals for E, RW, and RS
- 8-bit data bus, i.e., lines/signals D7-D0
- One 16-bit memory location: includes all lines D7-D0, E, RW, RS
Now we would only need to declare in the program either two 8-bit variables or one 16-bit variable, pointing to the desired memory location (how to do this will be explained in later lecture material). Then, using these variables, we could control the display and write text to it. But let's return to this example in a moment...
Bit Operations in C¶
Bit operations in C include logical operators derived from digital logic, such as AND, OR, NOT, XOR, and also bit shift operations.
Shift Operations¶
In C, the bit shift operators are:
n << m
meaning shifting the bits of n to the left (towards MSB) by m bits.n >> m
meaning shifting the bits of n to the right (towards LSB) by m bits.
Examples:
int8_t x = 5; // variable x has a value of 5 (i.e., 00000101)
x = x << 1; // shift one bit towards MSB
// the value changes from 00000101 -> 00001010 = 10
int8_t x = 5;
x = x << 2; // value changes from 00000101 -> 00010100 = 20
int8_t x = 23; // x has a value of 23 (00010111)
x = x >> 2; // shifts bits to the right, discarding lower bits
// value changes from 00010111 -> 00000101 = 5
int8_t x = 118; // x = 01110110 = 118
x = x << 1; // value changes from 01110110 -> 11101100
// the sign bit of a 2's complement number changes
// so x's value is now -20
Note: Shifting towards MSB by one bit corresponds to multiplying the number by two, and shifting towards LSB by one bit corresponds to dividing the number by two.
Example: Now, consider a 16-bit variable
uint16_t lcd
reserved for the display, from which we want to read the value of the character (D7-D0) displayed. In this case, we need to shift the bits so that we "drop" the control signals and extract the 8-bit data value from the variable.The required bit operation here is
lcd = lcd >> 3;
, meaning a right shift that discards bits E, RW, and RS.Let's break down the bit shift operation. Here,
d
represents the corresponding data bits (D7-D0), and x
represents bits we don't care about (e.g., E, RW, RS). We're only interested in the data bits.// Bit shift breakdown ddddddddxxx lcd >> 3 ----------- 000dddddddd lcd
As a result, only the data bits D7-D0 remain, with their positions adjusted so that D0 (the least significant data bit) now corresponds to the least significant bit (LSB) of the variable
lcd
. Thus, the variable now holds only the desired value.Logical Bitwise Operations¶
From basic programming and digital logic courses, you may already be familiar with logical operations (AND, OR, XOR, NOT). What distinguishes **bitwise operations** is that they are performed on each individual bit of a binary number, rather than evaluating the truth value of an entire variable (more on truth values later).
The syntax for logical bitwise operations in C is as follows:
- AND: operator
&
, true when both bits are one
00001111 = 15 & 10101010 = -86 -------- 00001010 = 10
- OR: operator
|
, true when either bit is one
00001111 | 10101010 -------- 10101111
- XOR: operator
^
, true if the corresponding bits are "different"
00001111 ^ 10101010 -------- 10100101
- NOT: operator
~
, inverts bits (ones to zeros and vice versa)
~ 00001111 -------- 11110000
ATTENTION: If you are working with signed numbers be careful!!! Any of those operations can change the sign of your numbers.
Note! It is very common in C code to confuse the & operator (bitwise AND) with the logical && operator (logical AND).
Bit Masks¶
Okay, with bitwise operations, we can go very far in programming embedded devices, especially when we use a tool called a bitmask. A bitmask is a (binary) number that "marks the desired bits", allowing the bitwise operation to target "only the bits we want"!
Now, what's the problem here? Why can't we just "assign" a new desired value to a variable? Let's see...
When controlling the display mentioned above, for example, we might want to change only the value of bit E and leave the rest unchanged. We could do this by first checking the value of bit E in the variable (whether it's 1 or 0), changing the value in the code (1 -> 0 or 0 -> 1), and then writing the modified value back to the variable. And the situation escalates quickly if we want to handle several bits from the above variable, and we need to figure out the value combinations one by one. Now, bitmasks offer a clever way to mark the desired bits and change their value straightforwardly.
Example: We want to handle the 1st and 6th bits of an 8-bit number, so the mask is as follows:
bit 87654321 -------- mask 00100001 as a binary number, i.e., 16 + 1 = 17 = 0x11
A bitmask can be created either by an assignment operation or a bitwise operation on a variable or a constant value.
uint8_t mask = 0x22; // bits marked in the mask 00100010 = 0x20 + 0x02
uint8_t mask = (1 << 5) | (1 << 1); // bit-shifting marks the 2nd and 6th bits (00100010)
#define MASK 0x22 // equivalent constant MASK
#define MASK ((1 << 5) | (1 << 1)) // equivalent macro MASK
// The OR operation opened up:
00100000 (1 << 5) = 0x20
| 00000010 (1 << 1) = 0x02
--------
00100010 = 0x22
Example: Let's go back to the display above and create masks for the data and control bits.
D7-D0: MASK_DATA -> 11111111000 = 0x7F8 E: MASK_E -> 00000000100 = 0x4 RW: MASK_RW -> 00000000010 = 0x2 RS: MASK_RS -> 00000000001 = 0x1
Example: Create a variable called
control_mask
that corresponds to all control bits.uint16_t control_mask = MASK_E | MASK_RW | MASK_RS;
// Alternatively, without constants:
uint16_t control_mask = 0x4 | 0x2 | 0x1;
// Now, the OR operation:
00000100 MASK_E
| 00000010 MASK_RW
| 00000001 MASK_RS
--------
00000111 control_mask
Using a Bit Mask¶
A bitmask can be used to modify a variable/register by combining bitwise operations in one or more statements.
Let's assume we have an 8-bit register variable called
lcd
, which represents control signals. In this case, some of the bits are represented by x
, meaning 'don't care'—their current value (whether 1 or 0) doesn't matter. The important point is that when we apply a bitmask, only the bits specified by the mask will be modified. The value of the 'don't care' bits will remain unchanged unless explicitly targeted by the mask.1. The OR operation sets the masked bits to **on** (logical state 1).
Example from the display above, setting bit E (Enable) to on without affecting the state of the other bits:
lcd = lcd | MASK_E;
// Operation broken down:
xxxxxxxx lcd (x = don't care)
| 00000100 MASK_E
--------
xxxxx1xx lcd
The result is that only the bit indicated by the mask in the
lcd
variable was "forced to 1", and the others remained in the state x they were in. As shown above, an OR operation with 0 does not change the value of the bit.2. A combination of AND and NOT operations turns off the masked bits (logical state 0).
lcd = lcd & ~(MASK_E);
// Negation of MASK_E
~ 00000100 MASK_E
--------
11111011 ~MASK_E
// AND operation with the variable
xxxxxxxx lcd
& 11111011 ~MASK_E
--------
xxxxx0xx lcd
The result is that only the bit indicated by the mask in the
lcd
variable was "forced to 0", and the others remained in the state x they were in. As shown above, an AND operation with 0 always changes the bit's value to 0.3. Extracting bits using a mask
When we want to check the value of an individual bit or multiple bits, we need a way to "extract" the bits from the variable's value. This can also be done with masks by combining bitwise operations.
// Checking the value of bit e uint16_t enable = (lcd & MASK_E) >> 2; // AND operation with the display variable xxxxxxxx lcd & 00000100 MASK_E -------- 00000e00 enable (value of e is either 0 or 1) // Bit shift operation 00000e00 enable >> 2 -------- 0000000e enable
After this, we can use an if statement to check whether the value of the
enable
variable is 0 or 1 (depending on the value of bit e, as the others are zeros). Note that in this case, it would be sufficient to check just the variable's value without bit shifting, because if e=1, then the value of the enable
variable is always > 0.Order of Operations in C¶
In the examples above, we have already used several different operations in the same C statement, such as assignment, bit shifting/logical operations, and negation. To avoid confusion, this is the right moment to present the general order of operations in C.
However, instead of memorizing the following table, it's always better to use parentheses to specify the desired order of operations, making the code more readable and preventing bugs from sneaking into your program.
Previous table has been obtained from this link (Credits to Wei-keng Liao)
The direction of execution in the table indicates how the order of operations is interpreted when there are multiple operators of the same precedence in a statement—whether it's from right to left or from left to right.
Conclusion¶
This material has provided us with important tools for embedded programming, as bit masks are one of the most commonly used methods in C for controlling peripherals.
Often, libraries for different peripherals hide these bitwise operations under nicer function calls or Macros, but when examining the source code of these libraries, you'll find them underneath...
Give feedback on this content
Comments about this material