STM32 #8: Binary, Bitwise Operators, and set/get flags
For embedded C, bitwise operators are the key to understanding how to interact with communications protocols, data packets, and registers. So to start, a whistle stop tour through binary:
The binary system is base two, and so each value can only be stored as two digits; a 1, or a 0. Much like the decimal system (base 10) where there are 10 digits 0-9, to give meaning above the digit 9, a representative meaning is given to the order of the digits. Binary is used because of how information is stored in hardware, it is much easier to store a 1 or 0 than venturing into trits, dits, or even qbits and beyond. So, to count to higher numbers than one, multiple binary digitas are combined and given a representative value based on the digits order in a list. For example, an unsigned 8 bit binary storage list will typically have the attributed values of: 2^7, 2^6, 2^5, 2^4, 2^3, 2^2, 2^1, & 2^0 to each corresponding bit. To represent the unsigned integer 3 in 8bits you might set the 2^0 + 2^1 positions to a 1 and the rest by 0s: 00000011
Are you a big-endian or a little-endian? Wait waittt…. I don’t want to know, reality doesn’t exist until you measure it right, Schrodingers cat and all. Endianness is arbitrary. For this post big-endianness, is assumed; The most significant bit is first, so for a 8 bit unsigned integer such as the number 3 the binary is 00000011, also denoted as 0b11 in C23, and 119 is 01110111. Under this regime with 8 bits, the range of unsigned integers you can store will be from 0 to 255.
We’ve covered the “unsigned” integer, so what if you want to store both negative and positive numbers? You can use the Two’s compliment convention; where the most significant bit (the first one in big-endian convention) is used to denote if the integer is a positive or negative. This changes the amount of numbers than can be represented compared to an equivalent sized unsigned memory. See if you can work out the range of values if the most significant bit is used for the polarity and not for 2^7: Did you get -127 to 127? Wrong, you didn’t read the wiki did you! That would be the One’s compliment convention. In twos compliment 0 is only represented once, this is done by representing negative numbers by taking the bit compliments of their magnitude (flipping all the bits) and adding 1, for example an 8bit signed int 6 is represented as 00000110, the additional bits are padded with zeros for positive numbers and 1s for negative numbers, so -6 is represented as 11111010. This way an extra number can be squeezed into the - side bringing the range of numbers to: -128 to 127 for a signed 8bit int.
Oh dear that was longer than I had planned!
Bit wise operators
If you have come across logical operators before this will be fairly straight forward for most, instead of the integer, the operand is each bit, and the bitwise operator will run the operator sequentially on every bit that makes up the memory. So hee are the main ones and they are with their corresponding truth tables and unsigned 8bit examples:
Bitwise AND: & If both bits being compared match then a 1 is produced, if not then produce a zero.
A B = A&B
0 0 = 0
1 0 = 0
0 1 = 0
1 1 = 1
Example:
A = 00000011 = 3
B = 00001001 = 9
A&B = 00000001 = 1
Bitwise OR: | Logical inclusive OR operation on each pair of bits. If both bits are zero the result is 0, otherwise it is one:
A B = A|B
0 0 = 0
1 0 = 1
0 1 = 1
1 1 = 1
Example:
A = 00000011 = 3
B = 00001001 = 9
A|B = 00001011 = 11
Bitwise XOR: ^ Logical exclusive OR operation on each pair of bits. If both bits are zero, or both are 1 the result is 0, otherwise it is one:
A B = A^B
0 0 = 0
1 0 = 1
0 1 = 1
1 1 = 0
Example:
A = 00000011 = 3
B = 00001001 = 9
A|B = 00001010 = 10
Bitwise NOT: ~ Also known as the bitwise compliment which flips all the bits to the opposite value; A 1 becomes a 0, and a 0 becomes a 1.
A = ~A
0 = 1
1 = 0
Example:
A = 00000011 = 3
~ A = 11111100 = 252
Notice the resultant decimal number is 255 (the max possible respresentation) - 3 = 252
Bitwise shift left: « n Shift all bits left (increase) by n places, bits that shift past the bounds of the register are discarded
Example:
A = 00000011 = 3
A << 1 = 00000110 = 6
A << 2 = 00001100 = 12
A << 9 =
Each position the value is shifted to the left, the value doubles, or the value is multiplied by 2**n, as long as it does not exceed (overflow) the register max bit range. This requires less compute than a typical multiplication routine. Where the end bit, big or small, moves into the register a 0 is initialised in the now “empty” spot. There are exceptions for signed integers where the sign bit is preserved.
Bitwise shift right » n Like the left shift but shift all bits right (decrease) by n places.
Example:
A = 00000011 = 3
A >> 1 = 00000001 = 1
A >> 2 = 00000000 = 0
Each position that the value is shifted to the right, the value halves.
basics: https://www.youtube.com/watch?v=BGeOwlIGRGI Use for embedded: https://www.youtube.com/watch?v=6hnLMnid1M0
Types of shifts: Ends are always contentious! Above we described arithmetic shifting, where signed integers keep their sign bit when shifted right. The counterpart is the logical shift, used for unsigned integers, which always fills new bits with zeros regardless of sign.
There’s also the circular (or rotate) shift, where bits that fall off one end of the register are wrapped around to the other side. This is less common in standard C but widely used in embedded systems, cryptography, and signal processing.
Example of an 8-bit circular shift:
A = 00000011 (3)
A >> 1 = 10000001 (rotated right)
A << 1 = 00000110 (regular logical left shift)
The rotation shown above is illustrative and C’s » and « operators do not rotate.
In contrast, an arithmetic right shift on a signed number would preserve the sign bit:
A = 11111000 (-8)
A >> 1 = 11111100 (-4)
Registers
Now we know bit shifts, we can write a function in C that can represent a register in binary. Start with the basic template:
#include <stdio.h>
int main (void) {
return 0;
}
Write a function which will take an input register and print the bits, placing this above the main function. We start with a function that takes the register variable as an uint8_t from <stfint.h>, so an unsigned 8 bit integer type. Then inside a for loop, for each bit in the register, from largest, to smallist, we will right bit shift the register by that position to bring it to the least significant bit position, then & it with 1 (00000001) and print the result. If there is a 1 in the register at that position the result will be a decimal 1, if there is not a 1 the result will be a decimal 0. Then we print that result before iterating the loop for all 8 bits:
#include <stdint.h> // reveals the uint8_t data type for our 8bit register
void printBinary(uint8_t aRegister) {
for (int i = 7; i>=0; i--) {
printf("%d", (aRegister >> i) & 1);
}
printf("\n");
}
Back in the main loop lets set a value and call the print binary function:
uint8_t aRegister = 54;
printBinary(aRegister);
If you gcc this file and then run a.out (by default) you should have the binary representation of 54 printed out in the terminal. Here is the full code:
#include <stdio.h>
#include <stdint.h> // reveals the uint8_t data type for our 8bit register
void printBinary(uint8_t aRegister) {
for (int i = 7; i>=0; i--) {
printf("%d", (aRegister >> i) & 1);
}
printf("\n");
}
int main (void) {
uint8_t aRegister = 54;
printBinary(aRegister);
return 0;
}
Result:
>>> 00110110
How might it be useful for embedded… flags
Now that we can print bits, let’s see how to use them — flags are where bit-level logic becomes truly practical. You can take a 8bit register and store 8 flags inside it to denote a configuration in a memory and communication in efficient manner for example. You can use the bitwise operators discussed to set and read flags stored in this way much like the previous example.
Lets say you have a 8bit register holding the unsigned decimal value 2: 00000010. Now you want to find out if the 2^1 bit is a 1 or a 0. This one is simple, as all other bits are 0, so if you take another register that holds a single bit at 2^1 bit and bitwise AND (&) it with the original register if both bits are 1, the result will also be 1, and a decimal value of 2 in this case:
flag_register = 00000010 = 2;
check_for_flag = 00000010 = 2;
flag_register & check_for_flag = 00000010 = 2;
Lets setup a basic C program to implement this, we will include the printBinary function we developed earlier for help visualising the binary, set a flags register to the decimal value 54, then check the first two lest significant bits as the first and second flag let print if they are enabled or not:
#include <stdio.h>
#include <stdint.h>
void printBinary(uint8_t aRegister) {
for (int i = 7; i>=0; i--) {
printf("%d", (aRegister >> i) & 1);
}
printf("\n");
}
int main (void) {
uint8_t flags = 54; // binary: 00110110
printf("Flags: ");
printBinary(flags);
if ((flags & 1) !=0) {
printf("Flag 1 - enabled\n");
}
else if ((flags & 1) == 0) {
printf("Flag 1 - disabled\n");
}
if ((flags & 2) != 0) {
printf("Flag 2 - enabled\n");
}
else if ((flags & 2) == 0) {
printf("Flag 2 - disabled\n");
}
return 0;
}
Now this works because decimal 2 = 00000010, and decimal 1 is 00000001. So when we bitwise & these with the flag register they will only check for those positions. If you try this with the value 3 however, you will notice that it will now bitwise & successfully both 1 and 2, resulting in a decimal value of three. So this approach doesn’t quite scale for checking all of the bits. Lets write a checker which can scale a bit better and put it in its own function above main. We will do this by utilising the same method as the printBinary function, by left shifting the register to check the bit position against the decimal value 1 (0000001) with a bitwise &:
void checkFlag(int bit, uint8_t flagRegister) {
if ((flagRegister >> bit & 1) == 0) {
printf("Flag: %i - disabled\n", bit);
}
else {
printf("Flag: %i - enabled\n", bit);
}
}
Then modify main to call that function for each flag you want to read, lets assume the flagRegister has 8 flags, one for each bit in the uint8_t sized register:
int main (void) {
uint8_t flags = 54; // 00110110
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
return 0;
}
The result should look like this:
Flags: 00110110
Flag: 0 - disabled
Flag: 1 - enabled
Flag: 2 - enabled
Flag: 3 - disabled
Flag: 4 - enabled
Flag: 5 - enabled
Flag: 6 - disabled
Flag: 7 - disabled
Here is the full code:
#include <stdio.h>
#include <stdint.h>
void printBinary(uint8_t aRegister) {
for (int i = 7; i>=0; i--) {
printf("%d", (aRegister >> i) & 1);
}
printf("\n");
}
void checkFlag(int bit, uint8_t flagRegister) {
if ((flagRegister >> bit & 1) == 0) {
printf("Flag: %i - disabled\n", bit);
}
else {
printf("Flag: %i - enabled\n", bit);
}
}
int main (void) {
uint8_t flags = 54; // 00110110
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
return 0;
}
Lets change some flags now then, we will do this by using a bitwise or, and rather that using the decimal values will will use direct binary represention using the 0b prefix, so the decimal value 2 = 0b10, 3 = 0b11, 4 = 0b100, etc:
To set flag 0 from 0 to 1, without impacting the other flag bits, we simply bitwise OR it with 0b1, add this to the end of the main loop:
printf("\nEnabling flag 0...\n");
flags = flags | 0b1;
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
The result is now
Enabling flag 0...
Flags: 00110111
Flag: 0 - enabled
Flag: 1 - enabled
Flag: 2 - enabled
Flag: 3 - disabled
Flag: 4 - enabled
Flag: 5 - enabled
Flag: 6 - disabled
Flag: 7 - disabled
With this type of set statement we can compound the bitwise OR to set multiple flags in one line if we so wish, setting flags = flags | 0b1 | 0b1000 | 0b1000000; for example should now result in a new flag register of 01111111, give it a try!
What is you want to disable a flag? Lets take the 0b1000 flag at position 2^3 bit.
flags = 0b01111111 flag3 = 0b00001000
rather than bitwise OR these two together to enable the bit, we can NOT the flag 3 representation to flip all the bits ```~flag3`` which will result in 0b11110111 and then bitwise & that with the flags register. This result is that if the flag 3 bit was enabled it will now be set to 0, additionally all other bits will be left unchanged. Here is the code, have a go:
printf("\nDisabling flag 3...\n");
flags = flags & ~0b1000;
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
Furthermore this can be compounded to disable multiple flags at the same time.
Finally trying to remember the binary for each flag gets a bit tough and is not very readable so we can modify the code to use macros for each flag which makes the whole procedure much easier to read now we understand what is happening under the hood, here is the final code:
#include <stdio.h>
#include <stdint.h>
#define FLAG_0 0b00000001
#define FLAG_1 0b00000010
#define FLAG_2 0b00000100
#define FLAG_3 0b00001000
#define FLAG_4 0b00010000
#define FLAG_5 0b00100000
#define FLAG_6 0b01000000
#define FLAG_7 0b10000000
void printBinary(uint8_t aRegister) {
for (int i = 7; i>=0; i--) {
printf("%d", (aRegister >> i) & 1);
}
printf("\n");
}
void checkFlag(int bit, uint8_t flagRegister) {
if ((flagRegister >> bit & 1) == 0) {
printf("Flag: %i - disabled\n", bit);
}
else {
printf("Flag: %i - enabled\n", bit);
}
}
int main (void) {
uint8_t flags = 54; // 00110110
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
// Enable FLAG_0
printf("\nEnabling flag 0...\n");
flags = flags | FLAG_0;
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
// Disable FLAG_3
printf("\nDisabling flag 3...\n");
flags = flags & ~FLAG_3;
printf("Flags: ");
printBinary(flags);
for (int i=0; i<sizeof(flags)*8; i++) {
checkFlag(i, flags);
}
return 0;
}
Now it’s starting to look embedded…
In STM32, peripheral registers are memory-mapped. You read/write bits to configure hardware or check status flags. CMSIS gives you typed structs (e.g., GPIOA, USART2, RCC) so you can do clean and safe bit operations. Note that in the previous example we have used the pure C binary specifier 0bxx… however for embedded it is typically better to use hex as 0b can be compiler and implementaiton specific.
For example, you can directly enable the GPIOA clock via modifying the AHB1ENR register. Everything previous was pure C, below, these will have to be in a STM32 embedded project like the previousblog posts.
#include "stm32f1xx.h"
// Enable GPIOA clock by setting the GPIOAEN bit
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// Check if it is set by reading the flag
if (RCC->AHB1ENR & RCC_AHB1ENR_GPIOAEN) {
// Do some stuff because the flag is enabled
}
Or you might want to configure a pin mode by masking a multi-bit field. Many fields are 2 bit slices. For example, each GPIO pin has a 2-bit mode in GPIOx->MODER:
// Configure PA5 as general-purpose output
// MODER has 2 bits per pin: 00=input, 01=output, 10=AF, 11=analog.
GPIOA->MODER &= ~(0x3u << (5 * 2)); // clear the 2-bit field
GPIOA->MODER |= (0x1u << (5 * 2)); // set to 01 (output)
Mask then set is the safest pattern for multi-bit fields.
You can set and clear output bits automatically (BSRR). Writing to GPIOx->BSRR avoids read-modify-write hazards and is atomix on cortex-M:
// Set PA5 high
GPIOA->BSRR = (1u << 5);
// Set PA5 low (write to reset upper-half)
GPIOA->BSRR = (1u << (5*16));
Avoid GPIOA->ODR |= … in ISRs or multi-context code, BSRR is better.
You could also do something like read a pin (IDR) and a status flag (USART ISR)
// Read PA6 input state
uint32_t pa6 = (GPIOA->IDR >> 6) & 1u;
// USART2: check TXE (transmit data register empty) flag before sending
if (USART2->ISR & USART_ISR_TXE_TXFNF) {
USART2->TDR = 'A'; // write one byte
}
Some status flags are cleared by writing 1 to a dedicated clear register (e.g., USARTx->ICR). Always check the reference manual’s “flag clearing” table; it varies by flag.
Be careful using pure C bit-fields for registers. They can break with implementation specific layouts, padding, and non-atomic practices. For hardware registers, masks and shifts are the robust and portable way to do it.
Lastly, you can use C macros to define flag utilities to clean up code, for example:
#define BIT(n) (1u << (n))
#define SET_BITS(reg, m) ((reg) |= (m))
#define CLR_BITS(reg, m) ((reg) &= ~(m))
#define TST_BITS(reg, m) (((reg) & (m)) != 0)
// Example: toggle PA5 quickly (not atomic; fine in single context)
GPIOA->ODR ^= BIT(5);
Wow, okay, so we have covered:
- Bitwise AND/OR/XOR/NOT and shifts underpin register work.
- Using masks to set/clear/test single-bit flags and multi-bit fields.
- On STM32, using CMSIS registers: RCC->…, GPIOx->MODER, GPIOx->BSRR, USARTx->ISR/TDR/ICR.
Copyright © 2025 David O’Connor