Electronic – Why does AVR code use bit shifting

avrcmicrocontroller

In AVR programming, register bits are invariably set by left-shifting a 1 to the appropriate bit position – and they're cleared by a ones' complement of the same.

Example: for an ATtiny85, I might set PORTB, b4 like this:

PORTB |= (1<<PB4);

or clear it like this:

PORTB &= ~(1<<PB4);

My question is: Why is it done this way? The simplest code ends up being a mess of bit-shifts. Why are bits defined as bit positions instead of masks.

For instance, the IO header for the ATtiny85 includes this:

#define PORTB   _SFR_IO8(0x18)
#define PB5     5
#define PB4     4
#define PB3     3
#define PB2     2
#define PB1     1
#define PB0     0

To me, it would be much more logical to define the bits as masks instead (like this):

#define PORTB   _SFR_IO8(0x18)
#define PB5     0x20
#define PB4     0x10
#define PB3     0x08
#define PB2     0x04
#define PB1     0x02
#define PB0     0x01

So we could do something like this:

// as bitmasks
PORTB |=  PB5 |  PB3 |  PB0;
PORTB &= ~PB5 & ~PB3 & ~PB0;

to turn bits b5, b3, and b0 on and off, respectively. As opposed to:

// as bit-fields
PORTB |=  (1<<PB5) |  (1<<PB3) |  (1<<PB0);
PORTB &= ~(1<<PB5) & ~(1<<PB3) & ~(1<<PB0);

The bitmask code reads much more clearly: set bits PB5, PB3, and PB0. Furthermore, it would seem to save operations since the bits no longer need to be shifted.

I thought maybe it was done this way to preserve generality in order to allow porting code from an n-bit AVR to an m-bit (example 8-bit to 32-bit). But this doesn't appear to be the case, since #include <avr/io.h> resolves to definition files specific to the target microcontroller. Even changing targets from an 8-bit ATtiny to an 8-bit Atmega (where bit definitions change syntactically from PBx to PORTBx, for example), requires code changes.

Best Answer

The simplest code ends up being a mess of bit-shifts. Why are bits defined as bit positions instead of masks.

No. Not at all. The shifts are only in the C source code, not in the compiled machine code. All the examples you showed can and will be resolved by the compiler at compile time because they are simple constant expressions.

(1<<PB4) is just a way to say "bit PB4".

  • So it not only works, it does not create more code size.
  • It makes also sense for the human programmer to name the bits by their index (e.g. 5) and not by their bit mask (e.g. 32) because this way consecutive numbers 0..7 can be used to identify the bits instead of awkward power of two (1, 2, 4, 8, .. 128).

  • And there is another reason (maybe the main reason):
    The C-header files can not only be used for C-code but also for assembler source code (or assembler code inlined in C source code). In AVR assembler code you definitely not only want to use bit masks (which can be created from indices by bit shifting). For some AVR bit manipulation assembler instructions (e.g. SBI, CBI, BST, BLD) you have to use bit indices as immediate operator in their instruction op code.
    Only if you identify bits of SFRs by indices (not by bit mask) you can use such identifiers directly as the assembler instructions immediate operand. Otherwise you had to have two definitions for each SFR bit: one defining its bit index (which can be used e.g. as operand in aforementioned bit manipulating assembler instructions) and one defining its bit mask (which can only be used for instructions where the whole byte is manipulated).