#036 – How bit shifting is used in embedded systems

The bitwise shift operators << and >> move every bit in their left operand a fixed number of positions. They are most often encountered in C++ as overloads for stream I/O, however they are also overloaded to perform bit manipulation. This has widespread use in embedded systems.

They have 2 main strengths:

  1. They are the fastest way to multiply or divide by powers of two
  2. They are the standard tool for assembling and disassembling multi-byte values.

The Basics

Left shifting shifts the bits n positions of the left, towards MSB.

Right shifting shifts the bits n positions to the right, towards LSB.

Neither operator modifies its operand:

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<4> bits{ 0b0001 };

    std::cout << "Original:    " << bits         << '\\n';
    std::cout << "Shift by 1:  " << (bits << 1)  << '\\n';
    std::cout << "Shift by 2:  " << (bits << 2)  << '\\n';
    std::cout << "After both:  " << bits         << '\\n';
}

// Output
Original:    0001
Shift by 1:  0010
Shift by 2:  0100
After both:  0001

Each bits << n produces a new value; bits itself is unchanged. This explains why the last line prints 0100.

To shift in place (thereby permanently modifying the operand), use the bitwise assignment operators bits <<= 2.

NOTE: Bit-shifting in C++ is endian-agnostic. Left-shift is always towards the most significant bit, and right-shift towards the least significant bit.*

Performing mathematical operations via bit-shifting

Because binary is base-2, moving a bit one position to the left doubles its positional value. Thus, we can bit shift as a shortcut to performing multiplication and division.

0001 = 1
0010 = 2
0100 = 4
1000 = 8

// Mathematically, shifting left by n positions is equivalent to multiplying by 2^n.
3 << 1  // 3 * 2^1 = 6
3 << 2  // 3 * 2^2 = 12
3 << 3  // 3 * 2^3 = 24

// Similarly, shifting right by n positions is equivalent to integer division by 2^n.
24 >> 1 // 24 / 2^1 = 12
24 >> 2 // 24 / 2^2 = 6
24 >> 3 // 24 / 2^3 = 3

In embedded firmware, these shortcuts can matter. A bare-metal microcontroller without a hardware multiplier will execute x << 3 in a single cycle but x * 8 as a multi-cycle library call.

A real-life example

Sensors and peripherals routinely return multi-byte readings split across separate registers. A fuel gauge communicating over I²C, for example, might place a 16-bit voltage reading into a two-byte buffer, low byte first:

uint8_t buffer[2]{};   // [0] = low byte, [1] = high byte

uint16_t voltage_mV = (static_cast<uint16_t>(buffer[1]) << 8) | buffer[0];

The high byte is shifted left by eight positions to occupy bits 8–15 of the result, and the low byte is OR-ed in to fill bits 0–7. The final value is a single 16-bit reading reconstructed from two registers.

The static_cast<uint16_t> is not for looks. buffer[1] << 8 promotes uint8_t to int before shifting.

Another Example: Direct Digital Synthesis (DDS)

Direct Digital Synthesis (DDS) is a technique for generating analogue waveforms from a digital source. The premise is:

  1. Numerically step through a waveform that is stored in a lookup table (LUT)
  2. Convert those numbers into analog voltage (through PWM or a DAC)
  3. The result is a continuous analog signal.

A common DDS design uses a phase accumulator, which is basically an integer counter.

While the accumulator might be 16-bit or 32-bit for precision, your Waveform Lookup Table (LUT) is usually much smaller (e.g., 256 entries, or 8-bit) because embedded RAM is precious.

Thus, a need arises for bit-shifting.

In order to index the wavetable, we must convert a 16 or 32-bit value into a 8-bit value.

uint16_t phaseAccumulator {}; 
uint8_t waveTable[256] {};    // 8-bit lookup table (2^8 = 256)

// Our index requires an 8-bit value
// Right shifting discards the lower byte
uint8_t index = phaseAccumulator >> 8; // Equivalent to phaseAccumulat / 256

// The lower byte is discarded
uint8_t sample = waveTable[index];

In Conclusion

Bit shifting allows us to bridge the two worlds of hardware and software.

It is the lowest-friction way to multiply and divide by powers of two, and the standard idiom for assembling multi-byte values from hardware registers. Reach for it when the problem is genuinely about bits or byte layout — register reconstruction, fixed-point scaling, table indexing — and use the regular arithmetic operators when it is not