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, the most common way of communicating with peripherals in embedded systems is through Memory-Mapped I/O. Recall from our earlier discussion of computer architecture that the processor communicates with memory and peripheral devices through buses. The processor places an address on the address bus, transfers data through the data bus, and uses control signals to indicate operations such as reading or writing.
In a memory-mapped system, not every address in the processor's memory address space corresponds to RAM. Some address ranges are assigned to peripheral devices instead. When the processor reads from or writes to an address in one of these ranges, address-decoding and control logic route the operation through the system buses to the corresponding peripheral rather than to RAM.
From the point of view of a C program, accessing a peripheral can therefore look very similar to accessing a value stored in memory. The hardware locations exposed by a peripheral in this way are commonly called peripheral registers.
GPIO port mapping in RP2040.
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.
Suppose the display exposes registers whose individual bits correspond to the signals shown in the figure. The interface could, for example, be represented using:
- Two 8-bit memory-mapped registers:
- a control register containing bits for E, RW, and RS
- a data register containing bits for D7-D0
- Or one 16-bit memory-mapped register containing bits for both D7-D0 and the control signals E, RW, and RS
The important idea is that the program does not manipulate the physical signal lines directly. Instead, it reads or writes register bits, and the hardware connects those register values to the corresponding signals.
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...
Note! Peripheral registers may change independently of the program because they are connected to hardware. For this reason, memory-mapped registers are typically accessed in C using the
volatile qualifier. It tells the compiler that the value may change unexpectedly and that accesses to it must not be optimized away like accesses to an ordinary variable.We will return to the exact C syntax for accessing memory-mapped registers later in the course.
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 << mmeaning shifting the bits of n to the left (towards MSB) by m bits.n >> mmeaning 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 a value one bit towards the MSB corresponds to multiplying it by two, provided that no significant bit is shifted out. Shifting one bit towards the LSB corresponds to integer division by two, with the least significant bit being discarded.
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 76543210
--------
mask 00100010
A bitmask can be created using a literal value:
uint8_t mask = 0x22;
or using bit shifts:
uint8_t mask = (1 << 5) | (1 << 1);
The OR operation can be expanded as follows:
00100000 (1 << 5) = 0x20 | 00000010 (1 << 1) = 0x02 -------- 00100010 = 0x22
The same mask can also be defined as a macro:
#define MASK 0x22
or:
#define MASK ((1 << 5) | (1 << 1))
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 the following examples, x means 'don't care': the bit may currently be either 0 or 1. A bitmask is used to select the bit positions we want to work with. The mask itself does not define what happens to those bits. The bitwise operation combined with the mask determines whether the selected bits are set, cleared, toggled, or extracted. In the first two examples below, the operations are constructed so that the bits not selected by the mask remain unchanged.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; // equivalent shorter form 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); // equivalent shorter form lcd &= ~(MASK_E);
// Negation of MASK_E
// Only the relevant 8 bits are shown here.
~ 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.4. Toggling bits with XOR
The XOR operation can be used to toggle selected bits, meaning that their current state is reversed:
- 0 becomes 1
- 1 becomes 0
Bits not selected by the mask remain unchanged because XOR with 0 leaves the original bit unchanged.
Example: Toggle bit E:
lcd ^= MASK_E;
// Operation broken down:
xxxxx1xx lcd
^ 00000100 MASK_E
--------
xxxxx0xx lcd
Here
!e means the opposite value of bit e.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 associativity shown in the table indicates how operators with the same precedence are grouped in an expression: either from left to right or from right to left.
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