100% found this document useful (2 votes)
34 views32 pages

C Bitwise Operations and Structures Guide

The document covers various concepts related to bit-level operations in C programming, including bitwise operators, bitmasks, and accessing device registers. It explains structure padding, packing, and the use of bit fields to optimize memory usage. Additionally, it discusses processor endianness and bit endianness, highlighting their importance in data representation and manipulation.

Uploaded by

Chidu
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (2 votes)
34 views32 pages

C Bitwise Operations and Structures Guide

The document covers various concepts related to bit-level operations in C programming, including bitwise operators, bitmasks, and accessing device registers. It explains structure padding, packing, and the use of bit fields to optimize memory usage. Additionally, it discusses processor endianness and bit endianness, highlighting their importance in data representation and manipulation.

Uploaded by

Chidu
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Contents

• Bit level operations


• Bit level operators
• Bitmasks
• Accessing Registers
• Padding & Packing of data
• Bit fields, Structures & Union
• Processor Endianness
• Bit Endianness
• Handling special registers
• FIFO
• Register Windows
• Data Alignment
• Type Qualifiers
Bit level operations
• In C programming operations can be performed on a bit level using bitwise operators.
• Bitwise operations are contrasted by byte-level operations.
• Instead of performing on individual bits, byte-level operators perform on strings of eight bits (known as bytes) at a
time.
• Reason for this is that a byte is normally the smallest unit of addressable memory.
• C provides six operators for bit manipulation:

Operator Description Example

& Binary AND Operator copies a bit to the result if it exists in both operands. (A & B) = 12, i.e., 0000 1100

| Binary OR Operator copies a bit if it exists in either operand. (A | B) = 61, i.e., 0011 1101

^ Binary XOR Operator copies the bit if it is set in one operand but not both. (A ^ B) = 49, i.e., 0011 0001

~ Binary One's Complement Operator is unary and has the effect of 'flipping' bits. (~A ) = ~(60), i.e,. 1100 0011

<< Binary Left Shift Operator. The left operands value is moved left by the number of
A << 2 = 240 i.e., 1111 0000
bits specified by the right operand.

>> Binary Right Shift Operator. The left operands value is moved right by the number
A >> 2 = 15 i.e., 0000 1111
of bits specified by the right operand.
Bit level operations
Consider the Program: test_bitwise_operations.c
The program shows the use of Bit level operators
Bitwise shift operations
• The bitwise shift operators move the bit values of a binary object.
• The left operand specifies the value to be shifted.
• The right operand specifies the number of positions that the bits in the value are to be shifted.
• The result is not an lvalue.
• Both operands have the same precedence and are left-to-right associative.
• Operator << Indicates the bits are to be shifted to the left.
• Operator >> Indicates the bits are to be shifted to the Right.
• The right operand should not have a negative value or a value that is greater than or equal to the width in bits of the expression
being shifted.
• The result of bitwise shifts on such values is unpredictable.
• If the right operand has the value 0, the result is the value of the left operand.
• The << operator fills vacated bits with zeros.
Bitwise shift operations
Consider the example program: test_ShiftOperators.c
It shows how to use the Shift operators.
Bitmasks
• In computer science, a bitmask is data that is used for bitwise operations, particularly in a bit field.
• Using a mask, multiple bits in a byte, nibble, word, etc. can be set either on or off, or inverted from on to off (or vice
versa) in a single bitwise operation.
• The idea for bit masking is based on boolean logic.
• One of these true/false values is a bit.
• Primitives in C (int, float, etc) are made up of some number of bits:
• Char – 1 Byte - 8 bits
• Int – 4 Bytes - 32 bits
• Uint16_t – 2 Bytes - 16 bits
• Masking is a general concept in which we keep, change, or remove some part of the information.
Bitmasks
Common Bitmask functions:
Masking bits to 1:
• To turn certain bits on, the bitwise OR operation can be used.
10010101 10101010
• When a bit A OR 1 = 1 and A OR 0 = A.
OR 11110000 11110000
• Therefore, to make sure a bit is on, OR can be used with a 1. To
leave a bit unchanged, OR is used with a 0.
= 11110101 11111010
• Example : Masking bits 4,5,6,7 to 1 while leaving
bits 0, 1,2,3 unchanged.

Masking bits to 0:
• To "masked off" (or masked to 0) than "masked on" (or masked
to 1).
10010101 10101010
• When a bit is ANDed with a 0, the result is always 0, i.e. A AND 0
= 0. AND 00001111 00001111
• To leave the other bits as they were originally, they can be ANDed
= 00000101 00001010
with 1 as A AND 1 = A
• Example: Masking off bits 4,5,6,7 & while leaving
bits 0, 1,2,3 unchanged.
Bitmasks
Querying the status of a bit:
• It is possible to use bitmasks to easily check the state of
10010101 10010101
individual bits regardless of the other bits.
• To do this turning off all the other bits using the bitwise AND is AND 00010000 00001000
done
= 00010000 00000000
• The value is compared with 0.
• If it is equal to 0, then the bit was off, but if the value is any
other value, then the bit was on.

• Toggling bit values:


• To make the bits opposite of what it currently is it calles toggling.
• This can be achieved using the XOR (exclusive or) operation. 10011101 10010101
• XOR returns 1 if and only if an odd number of bits are 1.
XOR 00001111 11111111
• Therefore, if two corresponding bits are 1, the result will be a 0, but if
only one of them is 1, the result will be 1. = 10010010 01101010
• If the original bit was 1, it returns 1 XOR 1 = 0.
• If the original bit was 0 it returns 0 XOR 1 = 1.
Bitmasks
Consider the example program: test_bitmasks.c
The program shows how to use different bitwise operation to operate in bits.
Accessing Registers
Device Registers:
• A peripheral device is likely to have a number of internal registers, which may be read from or written to by software.
• These normally appear just like memory locations and can, for the most part, be treated in the same way.
• Typically a device register will have bit fields – groups of bits that contain or receive specific information.
• Such fields may be single bits, groups of bits, or a whole word.
• There may also be bits that are unused – reading from them or writing to them normally has no effect.
• For example : Consider UART Device Control Register
Accessing Registers
Addressing the Register
• Typically when we access registers in C based on memory-mapped IO we use a pointer notation.
• It ‘trick’ the compiler into generating the correct load/store operations at the absolute address needed.
• For Example : UART0 UARTCTL register:
• The peripheral Base Address for UART0 = 0x4000C000
• The Register Offset = 0x030
• This means UART0 UARTCTL register is located at 0x4000C030
• The declaration of the pointer could include an initialization, as:
volatile uint32_t* const Uart0_UARTCTL_reg = ((uint32_t*) 0x4000C030);
#define UART0_UARTCTL_REG (*((volatile uint32_t*) 0x4000C030))
Accessing Registers
Reading the Register
Using the pointer definition from last slide we can read the reg data.
uint32_t UartCTL_Val = UART0_UARTCTL_REG;

Writing to Registers:
• Consider we want to want to set following bits in the UARTCTL reg
RXE - bit 9
TXE - bit 8 // Read the value
UARTEN - bit 0 uint32_t UartCTL = UART0_UARTCTL_REG;
• All the Bitwise operators can be used to manipulate the bits.
/* Set the bits 9, 8 & 0*/
UartCTL |= (1 << 9); // Set RXE
UartCTL |= (1 << 8); // Set TXE
UartCTL |= (1 << 0); // Set UARTEN

// Write value to register :


UART0_UARTCTL_REG = UartCTL;
Structure Padding
• Structure members are assigned to memory addresses in increasing order, with the first component starting at
the beginning address of the structure name itself.
• Structure padding is a concept in C that adds the one or more empty bytes between the memory addresses to
align the data in memory.
• The processor does not read 1 byte at a time. It reads 1 word at a time.
• What does the 1 word mean?
If we have a 32-bit processor, then the processor reads 4 bytes at a time 1 word = 4 bytes.
If we have a 64-bit processor, then the processor reads 8 bytes at a time, 1 word = 8 bytes.
• As we know that structure occupies the contiguous block of memory as shown in diagram. Size = 6 bytes
• Consider the 32 bit architecture:
0 1 2 3 4 5
The problem is that in one CPU cycle we can access:
• one byte of char a, a b c

• one byte of char b,


• 2 bytes of int c.

• We will not face any problem while accessing the char a and char b as both the variables can be accessed in one
CPU Cycle.
• We will face the problem when we access the int c variable as 2 CPU cycles are required to access the value of the 'c'
variable.
Structure Padding contd..
• Suppose we only want to access the variable 'c', which requires two cycles.
• The variable 'c' is of 4 bytes, so it can be accessed in one cycle also, but in this scenario,
it is utilizing 2 cycles.
• This is an unnecessary wastage of CPU cycles.
• Due to this reason, the structure padding concept was introduced to save the number of Size = 8 bytes
CPU cycles.
• How is structure padding done? 0 1 2 3 4 5 6 7

To achieve the structure padding, an empty row is created on the left, as shown in the diagram. a b Empty c
The two bytes which are occupied by the 'c' variable on the left are shifted to the right.
Structure Padding
So, all the four bytes of 'c' variable are on the right.
Now, the 'c' variable can be accessed in a single CPU cycle.
After structure padding, the total memory occupied by the structure is 8 bytes (1 byte+1 byte+2
bytes+4 bytes).
Although the memory is wasted in this case, the variable can be accessed within a single cycle.
Structure Padding contd..
• The structural padding is an in-built process that is automatically done
by the compiler.
• Sometimes it required to avoid the structure padding in C as it makes
the size of the structure greater than the size of the structure members.

Packing :
• Packing, prevents compiler from doing padding means remove the
unallocated space allocated by structure.
• We can avoid the structure padding in C in two ways:
• Using #pragma pack(1) directive
• Using __attribute__((__packed__))

Example Program: Demo for strcut padding:


test_structure_padding.c
Bit Fields, Structures & Union
Bit Fields :
• A bit field is a data structure that consists of one or more adjacent bits which have been allocated for specific purposes, so that
any single bit or group of bits within the structure can be set or inspected.
• A bit field is most commonly used to represent integral types of known, fixed bit-width, such as single-bit Booleans.
• Bit fields can be used to reduce memory consumption when a program requires a number of integer variables which always will
have low values.
• For example, in many systems storing an integer value requires two bytes (16-bits) of memory; sometimes the values to be stored
actually need only one or two bits.
• Having a number of these tiny variables share a bit field allows efficient packaging of data in the memory.
• In C, we can specify the size (in bits) of the structure and union members.
• Declaration of bit-fields in C

Syntax:

struct
{
data_type member_name : width_of_bit-field;
};
Bit Fields, Structures & Union
Consider the sample program : test_bitfeilds.c
It shows use of bit fields for efficient packaging of data in the memory.
Bit Fields, Structures & Union
• Bitfeilds can be used for accessing the device registers in C.
• For Example : Bitfeild_UARTCTL_REG.c
• The code shows the representation of UARTCTL 32bit register(4 bytes).

We can manipulate bits by


accessing the structure fields--->>>

Using the Device register pointer


We can write data to register as below:
Processor Endianness
• Endianness is a term that describes the order in which a sequence of bytes is stored in computer memory.
• Types of Endianness:
• Little endian:
Last byte of binary representation of the multibyte data-type is stored first.
X86 processor and most of the ARM Cortex-M3 based microcontrollers use the little-endian format.
• Big endian:
First byte of binary representation of the multibyte data-type is stored first.
Motorola 6800 / 6801 processors use the big-endian format.
• Example:
Integer is stored as 4bytes. will be stored as following:
Little Endian int x = 0x01234567; Big Endian
Address Data Address Data
0x100 0x67 0x100 0x01
0x101 0x45 0x101 0x23
0x102 0x23 0x102 0x45
0x103 0x01 0x103 0x67
Processor Endianness contd..
• Most of the times compiler takes care of endianness.
Example Program: To find Endianness of system
• When we perform bit-wise operation on integer then compiler
automatically handles the endianness. test_endianness.c
• However, endianness becomes an issue in following cases:
• Writing raw bytes of data to a file in one processor and
you send it to a system that uses different endian
processor.
• Send bytes of data over network as a serialized steam
of data from one endian processor system to other
endian processor system.
• In network communication, TCP/IP suites are defined to Example Program:
be big-endian. test_endianness_issue.c
• Sometimes it matters when you are using type casting,
below program is an example.
Consider the example in image:
A char array is typecasted to an integer type.
On little endian machine we get:
DataInt = 1
On Big endian machine we get:
DataInt = 16777216
Processor Endianness contd..
• Sometimes you may need to convert the endianness of data.
• Using the union we change the endianness of data:
• Example program:
test_change_endianness_union.c
Bit endianness
• In computing, bit numbering is the convention used to identify the bit positions in a binary number.
• Bit numbering is a concept similar to endianness, but on a level of bits, not bytes.
• The least significant bit (LSB) is the bit position in a binary integer representing the binary 1s place of the integer.
• Similarly, the most significant bit (MSB) represents the highest-order place of the binary integer.
• The LSB is sometimes referred to as the low-order bit or right-most bit, due to the convention in positional notation of
writing less significant digits further to the right.
• The MSB is similarly referred to as the high-order bit or left-most bit.
• In both cases, the LSB and MSB correlate directly to the least significant digit and most significant digit of
a decimal integer.
• Bit indexing correlates to the positional notation of the value in base 2.
• For this reason, bit index is not affected by how the value is stored on the device, such as the value's byte order.
• For Example
Binary (Decimal: 149) 1 0 0 1 0 1 0 1

Bit weight for given bit position


27 26 25 24 23 22 21 20
n ( 2n )

Bit position label MSB LSB


Handling special registers
FIFO
• A FIFO buffer is a useful way to store data that arrives at
a microcontroller peripheral asynchronously but cannot
be read immediately.
• An example of this is storing bytes that are incoming on
Enqueue Dequeue
a UART.
• Buffering the bytes can make it easier for the embedded
firmware to handle the incoming data in real time.
• A FIFO buffer is a type of data storage that operates on DATA n ….. Data3 Data2 Data1
a first-in, first-out basis.
• It is a very common construct used in digital systems, Back Front
• FIFOs can be implemented with software or hardware.
• The choice between a software and a hardware solution
depends on the application and the features desired.
Handling special registers
FIFO
• An example of this is storing bytes that are incoming on a UART.
• Buffering the bytes can make it easier for the embedded firmware to handle the incoming data in real time.
• Many microcontroller designs have limited buffer space for data arriving on the UART.
• Using a FIFO in the UART ISR can make it easier to manage incoming data.
• Using a FIFO as described above can reduce the real-time requirements for an application, as well as give the application
developer more flexibility in handling incoming data.
• Because the FIFO buffer stores the data until it can be processed by the application, the application does not need to handle
each byte as it arrives.
• This can make it easier to develop real-time applications that must process large amounts of data.
Alignment
• One of the low-level features of C is the ability to specify the precise alignment of objects in memory to take
maximum advantage of the hardware architecture.
• CPUs read and write memory more efficiently when they store data at an address that's a multiple of the data size.
• For example, a 4-byte integer is accessed more efficiently if it's stored at an address that's a multiple of 4.
• When data isn't aligned, the CPU does more address calculation work to access the data.
• By default, the compiler aligns data based on its size:
• char on a 1-byte boundary,
• short on a 2-byte boundary,
• int, long, and float on a 4-byte boundary,
• double on 8-byte boundary, and so on.
• The compiler generally aligns data on natural boundaries that are based on the target processor and the size of the data.
• Data is aligned on up to 4-byte boundaries on 32-bit processors, and 8-byte boundaries on 64-bit processors.
• In some cases, however, you can achieve performance improvements, or memory savings, by specifying a custom alignment for
your data structures.
Type Qualifiers
Const:
• The const type qualifier is used to create constant variables.
• When a variable is created with const keyword, the value of that variable can't be changed once it is defined.
• That means once a value is assigned to a constant variable, that value is fixed and cannot be changed throughout the
program.
• The default value of const variables is 0.
• The keyword const is used at the time of variable declaration.
• Syntax : const datatype variableName ;

Consider the Example: test_const.c


Type Qualifiers
Volatile:
• The volatile keyword is intended to prevent the compiler from
applying any optimizations on variables that can change in
ways that cannot be determined by the compiler.
• Variables declared as volatile are omitted from optimization Useful optimization
because their values can be changed by code outside Optimization goal
levels
the scope of current code at any time.
• The system always reads the current value of a volatile object Smaller code size -Oz
from the memory location rather than keeping its value in a
temporary register at the point it is requested. Faster performance -O2, -O3, -Ofast, -Omax

• Syntax: volatile datatype variableName; Better debug experience -O1


Optimization levels:
Better correlation between source code and
• If you use a higher optimization level for performance, then this generated code
-O0
has a higher impact on the other goals such as degraded debug
experience, increased code size, and increased build time. Faster compile and build time -O0

• If your optimization goal is code size reduction, then this has an


Balanced code size reduction and fast
impact on the other goals such as degraded debug experience, performance
-Os
slower performance, and increased build time.
Type Qualifiers
Volatile:
For example consider Program: test_volatile.c & test_NoVolatile.c
Type Qualifiers test_NoVolatile_sample2.c

Volatile Keyword with Loops:


Consider the example:
The example uses the SIGINT (Interrupt the process)
SIGINT is the signal sent if we press Ctrl+C.
Inside the SIGINT handler we –Count (decrement).
In main() while loop is expected to run unless Count > 0.
But after applying Compiler optimization –O3 we can see test_volatile_sample2.c
the results below:

Here Compiler
optimized the Count so
the loop never exit
even if the value of
Count < 0
Type Qualifiers
restrict:
• restrict keyword is mainly used in pointer declarations as a type qualifier for pointers.
• It doesn’t add any new functionality. It is only a way for programmer to inform about an optimization that compiler can
make.
• By adding this type qualifier, a programmer hints to the compiler that for the lifetime of the pointer, no other pointer will
be used to access the variable to which it points.
• If the compiler knows that there is only one pointer to a memory block, it can produce better optimized code.
• The programmer, not the compiler, is responsible for ensuring that the pointers do not point to identical locations.
• The compiler can e.g. rearrange the code, first loading all memory locations, then performing the operations before
committing the results back to memory.
• Since the compiler can rearrange the code more freely, the compiler can generate code that executes faster.
Type Qualifiers
restrict:
Consider the example : test_restrict.c
• It has two function which copy the array from source to For copyArray() the code
destination. is optimised
• We optimize the code with following settings -std=c17 –O3 But for FuncCopyArray()
• We can output assembler code file using -S with gcc. It it less optimised
gcc -std=c17 -O3 test_restrict.c -S
From test_restrict.s check the asm code for both
functions
Thank You!

32

You might also like