6
\$\begingroup\$

I'm learning microcontrollers, and have picked AVRs to start with. In many AVR tutorials I find the registers are initialised by performing some bitwise operations on the assignment, for example something like this:

PORTD = PORTD | (1<<3);

There is another way to initialize that gave me the same results:

PORTD = 0b00001000;

I find the latter easier to think about. If the latter is easier to think about, why do programmers use the former? Is it because programmers, unlike in the example, usually deal with assigning very long or complex binary data to registers, and hence they find it difficult to assign a binary string and resort to assigning data via bitwise operations that may be easier to think about?

\$\endgroup\$
3
  • 5
    \$\begingroup\$ In many cases the compiler will translate the |= to a single bit operation on the AVR as opposed to a read/modify/write using PORTD that the code implies. Also worth noting is the (1<<3) is evaluated by the compiler - no actual code to do the bit shift is generated. edit I see justme said much the same thing! \$\endgroup\$
    – Kartman
    Oct 30, 2022 at 18:42
  • 4
    \$\begingroup\$ As the most upvoted answer doesn't address this: your second example defines all bits in PORTD at the same time, setting bit 3 and clearing all the others. Using 1<<3 or 0b000010000 is a matter of style, but omitting the | changes the functionality! \$\endgroup\$ Oct 31, 2022 at 13:16
  • \$\begingroup\$ @GuntramBlohm as the author of that answer: Um, yes, I do, second large heading! Your two lines of code do different things; |= only sets the additional bits in the right hand side of the statement, = overwrites the content. \$\endgroup\$ Nov 2, 2022 at 8:33

7 Answers 7

35
\$\begingroup\$

0b00001000 vs 1<<3

The binary notation is not standards-compliant C. That might just rule it out.

If it doesn't rule it out for a project: Personal preference. I do not enjoy counting twelve zeros to put something in the fourth-highest bit in a 16 bit register, for example. I'd at the very least would use four-bit-grouped binary, or hexadecimal if I wanted to write this. At that point, actually writing 1<<12 seems clearer, in any imaginable way.

(There's older compilers that might not support 0b binary literals in C, as well.)

PORTB = vs PORTB = PORTB|

Your two lines of code do different things; |= only sets the additional bits in the right hand side of the statement, = overwrites the content.

Why it's bad to choose (1<<3) or 0b00001000

Generally, writing

PORTD = PORTD | (1<<4);

or

PORTD |= (1<<4);

or

PORTD |= 0b00010000;
// or |= 0x10;
// or |= 020; 
// or however you represent that constant

is bad style. What does the 4 or the 0b00010000; mean? Have a header somewhere that declares a constant, e.g.

// myproject.h
#define BLUE_LED_PIN (1<<4)
…


// main.c
#include "myproject.h"
…
//turn on blue LED
PORTD |= BLUE_LED_PIN;

See how that actually tells you what you're doing?

\$\endgroup\$
17
  • 6
    \$\begingroup\$ The last paragraph is the important one! \$\endgroup\$ Oct 30, 2022 at 15:48
  • 2
    \$\begingroup\$ Or indeed to isolate your port literals as well as your pin literals, you can #define BLUE_LED_PORT PORTD and BLUE_LED_PORT |= BLUE_LED_PIN; \$\endgroup\$
    – jonathanjo
    Oct 30, 2022 at 16:15
  • 4
    \$\begingroup\$ Another reason to prefer (1<<3) over 0b00001000 is that the documentation for microcontrollers will often specify that the bit you're setting is "bit 3", so the "3" matches the documentation. (This is not universal - some manufacturers like to number their bits from 1, so (1<<3) is "bit 4", and occasionally they'll be numbered in reverse shudder.) \$\endgroup\$
    – psmears
    Oct 31, 2022 at 10:28
  • 3
    \$\begingroup\$ @Lundin: I do agree that numbering with anything other than "bit n has value (1<<n)" is sub-optimal, to put it mildly. But I don't think we can say that people who use that don't know what they're doing - sometimes great engineers are constrained by external factors (e.g. having to keep the documentation consistent with previous products that were produced by less competent people). Also note that, although the question's tagged C, a lot of "how to program AVR" tutorials are based on the Arduino IDE - which is C++, which does support the 0b.... notation (as of ISO 14882:2014). \$\endgroup\$
    – psmears
    Oct 31, 2022 at 10:57
  • 3
    \$\begingroup\$ @psmears As for Arduino and C++, I wouldn't assume that's what used unless the OP says so. Though I posted an answer with a note about the upcoming C23 which seems to bring in binary notation, and more importantly decimal separators, into C just like in C++14. \$\endgroup\$
    – Lundin
    Oct 31, 2022 at 11:38
13
\$\begingroup\$

Well, the example is very poor, as for initializating the whole port after reset, either one can be used.

The point is that later on, you might want to control one bit only, and keep the state of the other pins.

There may be some registers that don't initialize to zero on reset so it depends on many things and which makes more sense in which case.

For example you can't clear bits by ORing.

And why it is so often used on AVRs is that the CPU has specific instructions for e.g. ORing and ANDing a single bit in IO register space, opcodes SBI and CBI, so controlling one bit only encodes into a single fast hardware instruction that is atomic, instead of being a non-atomic sequence of multiple instructions performing a read-modify-write sequence.

And because of this ability to set/clear single bits by the bit number instead of mask, the AVR MCU register bit names are defined with their respective bit numbers (0..7), compared to some other MCUs which define the bit with the bit mask directly.

Therefore, on an AVR platform, you tend to see these bit shifts a lot.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Yes, this is the important reason why we use the OR assignment. \$\endgroup\$ Oct 30, 2022 at 15:50
  • \$\begingroup\$ Another trick on AVR MCUs is that when you have a port as a output you can flip the pin by writing a 1 onto the linked bit of the PIN register of the port (reading that register reads the port state). no knowledge of the other port pins needed for that and its one atomic instruction. \$\endgroup\$
    – masterX244
    Nov 2, 2022 at 8:53
  • \$\begingroup\$ @masterX244 That is AVR specific. It is not a generic feature of all AVRs. \$\endgroup\$
    – Justme
    Nov 2, 2022 at 9:04
8
\$\begingroup\$

I don't know about the shifting, but the ORing is most useful is when the same register shares multiple unrelated functions. Then the second initialization doesn't disturb the previous initialization.

// Initialize function A
Reg = Reg | some_bits_to_set; 
... 
... 
// Initialize function B 
Reg = Reg | some_other_bits_to_set;
Reg = Reg & ~some_bits_to_clear 
...
\$\endgroup\$
1
  • \$\begingroup\$ The shifting is because in AVRs, all register bit names are defined as per their respective bit number instead of their respective bit mask. And even without that detail, if you know you want to set bit 4, then set bit 4 by shifting a 1 by 4 bits which enables compiler to use cbi/sbi instructions instead of doing a read/modify/write with the mask 0x10. \$\endgroup\$
    – Justme
    Oct 30, 2022 at 20:09
5
\$\begingroup\$

Check out How to access a hardware register from firmware? It answers most of the question and also explains why 1u << should always be used and never 1 <<.


Regarding 0b notation

0b numbers should be avoided for several reasons:

  • It isn't valid C but a non-standard extensions. 0b might not be available on specific compilers, or might behave differently between different compilers.

  • It has the same signed or unsigned problem as hex constants - depending on what value you use, you get different types. Example:

    if(~0b1 > 1)
    {
      puts("This is what I expected");
    }
    else
    {
      puts("This is what I got instead");
    }
    

    This is all because u/U suffix wasn't used. Check out this example:

    #include <stdio.h>
    
    #define WHAT(x)                             \
      _Generic((x),                             \
      int:           puts("int"),               \
      unsigned int:  puts("unsigned int"),      \
      long:          puts("long"),              \
      unsigned char: puts("aint gonna happen"), \
      char:          puts("aint gonna happen")) \
    
    int main (void)
    {
      WHAT(0b1);
      WHAT(0b00000000000000000000000000000001);
      WHAT(0b10000000000000000000000000000000);
      WHAT(0b00000000000000000000000000000001U);
      WHAT(0b100000000000000000000000000000000);
    }
    

    Output from one particular gcc/GNU17 compiler:

    int
    int
    unsigned int
    unsigned int
    long
    

    Apart from this (hopefully) behaving like hex constants (which might already give you surprising results as seen above), it isn't well-defined either. Because standard C (C17 C17 6.4.4.1/5) explicitly specifies the behavior of integer constants when typed in decimal, octal or hex format but it does not specify the same for binary format. So what type you will end up with on some specific compiler is anyone's guess.

  • Binary is less readable than hex. This is the very reason why we use hex in computer science in the first place - it is a more conventient way to express binary numbers.

    Sure as long as you have 8 bit registers that's not a problem, but what if you have 32 bit registers? Does REG = 0b10010010110011001010010111110000 look like sensible code? Of course not, it's an unreadable mess and the reason why C never bothered to include a 0b format in the first place.

    (That being said, the upcoming "C23" version of the C language looks like it will be adding 0b notation, since it with also add decimal separators to make large numbers more readable, such as 0b1001'0010. But neither binary constants nor decimal separators are standard C.)


Regarding common bit masking coding styles

As explained in my link and in other answers, "magic numbers" is unacceptable and we should use meaningful names such as SPICR = SPICR_SPIE | SPICR_CPHA ;

As for how to declare the actual bit masks, there's two acceptable forms commonly used:

#define SPICR_SPIE (1u << 7)  // bit number notation

or

#define SPICR_SPIE 0x80u      // raw hex notation

The bit number notation is more common overall, but especially in the "Atmel world" like AVR or SAM series, Atmel/Microchip Studio and corresponding "bloat libs" like ASF. So I would recommend using it.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ Whether binary is more or less readable than hex depends upon what is being represented. If a 16-bit register uses five bits to control each of three functions, a value written as 110101101011010 may be easier to read than 0x6B5A, , especially if the previous line contains a suitably-aligned comment -----=====-----. \$\endgroup\$
    – supercat
    Nov 1, 2022 at 18:05
  • \$\begingroup\$ @supercat You can assume that the person reading your code is a programmer and therefore has no problem reading hex. Grouping of bits is best represented as bit masks, so your code could have been written as #define MASK0(x) ( ((x) << 0) & 0x1Fu ) etc ... MASK2(0x1A) | MASK1(0x1A) | MASK0(0x1A). No "ASCII art" needed. And whoops, with the binary goo dispelled we find out it's the same value written 3 times. And then maybe we realize it should have written as MASK2(val) | MASK1(val) | MASK0(val). Yet another illustration of why binary is problematic. \$\endgroup\$
    – Lundin
    Nov 2, 2022 at 7:23
  • \$\begingroup\$ I'm probably better than most people at decoding hex "on the fly", but if asked whether a number contains five or more consecutive 1 bits, I'd have an easier time answering the question if the number was written in binary than if it's written in hex. If one is looking at code while referring to a data sheet or reference manual that includes positional diagrams of register bits, having the layout of bits in code match the layout of bits in the diagram may make things easier to read than having things written other ways. Personally, I would have like to have seen the language spec allow... \$\endgroup\$
    – supercat
    Nov 2, 2022 at 17:27
  • \$\begingroup\$ ...implicit concatenation of numeric tokens, with the proviso that a programmer must avoid having any tokens that would match any defined macro names unless one wanted them to be expanded. Even when using hex numbers, it's easier to proofread 0x 3000 0000 0000 0007 than 0x3000000000000007, and such concatenation rules would eliminate the need for the PP-number concept. \$\endgroup\$
    – supercat
    Nov 2, 2022 at 17:33
  • \$\begingroup\$ @supercat As already mentioned in the answer, I consider implementing decimal separators a prerequisite to allowing binary constants, and it looks like C23 will have both. As for how many digits a number are, well luckily 99.999% of all computers ever made use a word size which is a multiple of 4 bits, so there's really no reason not to use hex in any given hardware-related scenario. \$\endgroup\$
    – Lundin
    Nov 3, 2022 at 7:33
3
\$\begingroup\$

basically it's because it's a simpler operation (!)

PORTD = PORTD | (1<<3);

(here (1<<3) can be replaced with any other constant valued expression that's available at compile time and comes out to the same value)

Compiles to a single word opcode

sbi PORTD,3

(or in the general case all instances of 3 above can be replaced by a constant between 0 and 7 inclusive)

\$\endgroup\$
1
\$\begingroup\$

Generally the bitwise or the constant assignment operations will be identical. If one would be more efficient than the other (but give the same ultimate result), the compiler will change the method to the optimal one.

Therefore the main reason for one over the other is to provide some amount of self-documentation or clarity to the code. Of course this is a matter of preference and style.

\$\endgroup\$
1
  • 9
    \$\begingroup\$ They are only equivalent if the destination is known to be exactly zero. Otherwise they have different behavior. \$\endgroup\$ Oct 30, 2022 at 21:21
0
\$\begingroup\$

To add to Marcus Müller's excellent answer:

In the question you say:

There is another way to initialize that gave me the same results:

That's only true if the current value of PORTD is zero. But if PORTD is currently, say, 1, doing PORTD = 0b00001000; will give a different value compared to PORTD = PORTD | (1<<3);.

The point of writing this operation in this way to set one bit and leave the other bits alone. That's why we can't just assign a fixed value.

Writing the number in the form 1 << x is a really convenient way of saying "bit x", without having to count zeroes when we type, or convert from binary to hex in our heads. Though as Marcus says in his answer, even better is not to use magic numbers in our code at all.

And for completeness, if you want to clear this bit (while leaving the other bits alone), you could write PORTD &= ~(1 << 3);.

\$\endgroup\$
1
  • \$\begingroup\$ Indeed, it's misleading to speak of these as 2 ways to "initialize" since only the plain assignment to a constant value is initializing at all. The | version is used because we want to set specific bits without initializing the entire value of the register. \$\endgroup\$
    – Wooble
    Nov 2, 2022 at 13:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.