Two Basic Questions About Arduino's "shiftOut()"

Howdy Y'all...

I have a two (2) questions about the code that makes up the Arduino predefined shiftOut() function. First, please allow me to explain where I think the shiftOut() code is and what it does, then I will ask my questions. Please go easy on me, I'm no expert on this C/C++ stuff:

In my stand-alone copy of Arduino v1.8.5 running on a Windows 10 laptop I found the code for shiftOut() inside this file:

C:...\arduino-1.8.5\hardware\arduino\avr\cores\arduino\wiring_shift.c

Here is an exact copy of the shiftOut() code extracted from the file "wiring_shift.c":

void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val)
{
  uint8_t i;

  for (i = 0; i < 8; i++)  {
    if (bitOrder == LSBFIRST)
      digitalWrite(dataPin, !!(val & (1 << i)));
    else	
      digitalWrite(dataPin, !!(val & (1 << (7 - i))));
			
    digitalWrite(clockPin, HIGH);
    digitalWrite(clockPin, LOW);		
  }
}

I think I understand what is going on with this code. In the LSBFIRST case, (1 << i) steps a 1 from LSB to MSB one bit position per step. At each step the moving bit is bitwise ANDed with the byte to be shifted out. This allows the digitalWrite() statement to toggle the result to the output pin once per bit as the bit position steps from LSB to MSB. In the MSBFIRST case (1 << (7 - i)) steps the 1 in the opposite direction (from MSB to LSB) and just like the LSBFIRST case each bit is sent to the ouput pin, just in the reverse order starting with the MSB.

Now - for my two questions:

Question-1: What are the two bitwise NOT operators !! doing in: !!(val & (1 << i)) and !!(val & (1 << (7 - i)))? I duplicated the shiftOut() function on my Arduino Uno while probing the bytes during each step and as far as I can tell the !! operators do nothing except slow down and complicate the code. So why are the back-to-back NOTs there?

Question-2: Where are the { } brackets in the if(){}-else{} statement? I know the code works without the brackets, I tested it stand-alone. If the brackets are not functionally needed then why do they exist? If the brackets exist just to add clarity to the code (a very good reason IMO), then why are they missing from Arduino's shiftOut() built-in code?

By the way, just for fun (and a bit of clarity) here is the same shiftOut() function written with the { } brackets included and uint8_t replaced by Arduino's byte. I tested this code stand-alone as well and it works exactly like Arduino's shiftOut() built-in function (even without the !!'s). Note how I renamed shiftOut() to shOut() here to avoid a collision when testing:

// Functional duplicate of the Arduino built-in shiftOut(): 
void shOut(byte dataPin, byte clockPin, byte bitOrder, byte val) {
  byte i;
  for (i = 0; i < 8; i++) {
    if (bitOrder == LSBFIRST) {
      digitalWrite(dataPin, !!(val & (1 << i)));
    }
    else {
      digitalWrite(dataPin, !!(val & (1 << (7 - i))));
    }
    digitalWrite(clockPin, HIGH);
    digitalWrite(clockPin, LOW);    
  }
}

In case anyone is wondering why I'm digging into the Arduino code; I was thinking about how to reorder the endianness of an unsigned byte array and I remembered shiftOut(). I figured shiftOut() with its MSBFIRST/LSBFIRST options must do this (it does). And yes, bit-reordering algorithms are almost as common as hen's-teeth. For example have a look here:

http://graphics.stanford.edu/~seander/bithacks.html

Thanks for any replies... David in Sunny Florida

!! Ensures that a simple non zero is turned into a guaranteed 1.

{} Group statements. Here there is no need.

AWOL:
!! Ensures that a simple non zero is turned into a guaranteed 1.

{} Group statements. Here there is no need.

How does two back-to-back NOTs turn a non-zero like B01010101 into a guaranteed 1?

B01010101 is true. (It is non zero)
!true is zero.
!zero is true (aka 1)
(In the time it took you to reply and me to write my answer, you could have written an illustrative sketch)

I don't have my Arduino with me now, so no sketches. Not everyone carries an Arduino with them - I guess I should :wink:

BTW I was mistaken. Actually B01010101 will never happen because in this case the bitwise AND results in one bit only, a 0 or a 1.

Sorry, I still don't get it...

Either a 0 or a 1 gets sent to digitalWrite() every single time. There can be no other result. So how does using !! change this inevitable result?

No, me neither. I'm on a train.

B01010101 & 4 (for example , aka 1 << 2) == 4

4 is non zero, aka true.
!true is zero.
!zero . . . well, this has already been covered.

I think I see where you are coming from. But B01010101 can never occur in this function. That's why I corrected my earlier reply.

The result of the bitwise AND will always be a 0 or a 1 because the byte to be shifted out is always ANDed with a single bit, so a single bit always gets sent to digitalWrite(). AFAIK, once you have a single bit, double NOT'ing it does nothing. Am I wrong?

Thanks for your patience...

Am I wrong?

Yes.
Of course.

B01010101 can never occur in this function

is a ridiculous assertion.

void setup() {
  Serial.begin(250000);
  byte val = 255;
  for (byte i = 0; i < 8; i++) {
    Serial.print(F("("));
    Serial.print(val);
    Serial.print(F(" & (1 << "));
    Serial.print(i);
    Serial.print(F(")) = 0x"));
    hexByte(val & (1 << i));
    Serial.print(F(", !!("));
    Serial.print(val);
    Serial.print(F(" & (1 << "));
    Serial.print(i);
    Serial.print(F(")) = 0x"));
    hexByte(!!(val & (1 << i)));
    Serial.println();
  }
}
void hexByte(byte val) {
  if (val < 16) {
    Serial.write('0');
  }
  Serial.print(val, HEX);
}
void loop() {}
(255 & (1 << 0)) = 0x01, !!(255 & (1 << 0)) = 0x01
(255 & (1 << 1)) = 0x02, !!(255 & (1 << 1)) = 0x01
(255 & (1 << 2)) = 0x04, !!(255 & (1 << 2)) = 0x01
(255 & (1 << 3)) = 0x08, !!(255 & (1 << 3)) = 0x01
(255 & (1 << 4)) = 0x10, !!(255 & (1 << 4)) = 0x01
(255 & (1 << 5)) = 0x20, !!(255 & (1 << 5)) = 0x01
(255 & (1 << 6)) = 0x40, !!(255 & (1 << 6)) = 0x01
(255 & (1 << 7)) = 0x80, !!(255 & (1 << 7)) = 0x01

AWOL:
B01010101 can never occur in this function
is a ridiculous assertion.

I beg to differ. At each iteration this: !!(val & (1 << i)); or this !!(val & (1 << (7 - i))) always results one non-zero bit position asserted, never more than one. A byte with only one bit position asserted ANDed with any other byte will always result in a byte with only one bit asserted. Look at it this way:

  B11111111 <- byte to be shifted-out
 & B00010000 <- !!(val & (1 << i)) at 4th iteration
 -----------
 = B00010000 <- this gets sent to digitalWrite()

CORRECTION: B00010000 does not get sent to digitalWrite(), B00000001 does.
That's what the !! is for. I now understand. Thanks for the help everyone...

I hope this illustrates how B01010101 can never be the result of this shiftOut() function.

Beg away.
Work through the numbers.
Work through the code.

I'm done.

As long as the value you pass is 8 bits or fewer you don't need to constrain it to the range 0..1.
The digitalWrite() function already treats a zero value as LOW and a non-zero value as HIGH.

Inside digitalWrite(uint8_t pin, uint8_t val) in wiring.digital.cpp:

	if (val == LOW) {
		*out &= ~bit;
	} else {
		*out |= bit;
	}

If you are writing a function to shift out 16-bit or 32-bit values it would be better to shift the data and not the mask:

      digitalWrite(dataPin, (val >> i) & 1);
1 Like

@Whandall,

Thank You! I went and got an Arduino Nano (I was outside). Below is what I did a minute ago. I think it gets the same result as you - but in a comparatively ugly way...

/*
Result the Pin-13 LED blinks two times
(I padded the missing 0's manually):
00100000
00000001
done
 */

void setup() {
 Serial.begin(9600);

 pinMode(13, OUTPUT);

 byte a = B0100000;
 Serial.println(a, BIN);
 digitalWrite(13, a);
 delay(1000);
 digitalWrite(13, LOW);
 
 delay(1000);

 a = !!a;
 Serial.println(a, BIN);
 digitalWrite(13, a);
 delay(1000);
 digitalWrite(13, LOW);
}

void loop() {
 Serial.println("done");
 while(1){}
}

johnwasser:
As long as the value you pass is 8 bits or fewer you don't need to constrain it to the range 0..1.
The digitalWrite() function already treats a zero value as LOW and a non-zero value as HIGH.

Yes, good point. Since digitalWrite() will take a byte with any number of bit positions asserted as a HIGH argument, this seems to mean that the !! operators in the shiftOut() function really don't do anything useful! The sloppy sketch I posted above proves this - doesn't it? The Pin-13 LED went on the first time with the value B0100000.

And to beat a dead horse...

/*
Result, I padded the missing zeros manually... 
----------
00001111 <= Argument in MSB > LSB format 
----------
00000111
10000000
00000000
00000001
00000000 <= This goes to digitalWrite()
----------
00000110
01000000
00000000
00000001
00000000
----------
00000101
00100000
00000000
00000001
00000000
----------
00000100
00010000
00000000
00000001
00000000
----------
00000011
00001000
00001000
00000000
00000001
----------
00000010
00000100
00000100
00000000
00000001
----------
00000001
00000010
00000010
00000000
00000001
----------
00000000
00000001
00000001
00000000
00000001
----------
done
 */

void setup() {
  Serial.begin(9600);

  byte p = 0; // "byte" is the same as "uint8_t"
  byte a = 0;
  byte b = 0;
  byte c = 0;
  byte d = 0;
  byte e = 0;

  //MSBFIRST Example:
  //p = B01010101;
  p = B00001111;
  
  Serial.println("----------");
  Serial.println(p, BIN);
  Serial.println("----------");
  a = (7 - 0);
  Serial.println(a, BIN);
  b = (1 << (7 - 0));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 0)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 0)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 0)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 1);
  Serial.println(a, BIN);
  b = (1 << (7 - 1));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 1)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 1)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 1)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 2);
  Serial.println(a, BIN);
  b = (1 << (7 - 2));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 2)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 2)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 2)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 3);
  Serial.println(a, BIN);
  b = (1 << (7 - 3));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 3)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 3)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 3)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 4);
  Serial.println(a, BIN);
  b = (1 << (7 - 4));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 4)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 4)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 4)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 5);
  Serial.println(a, BIN);
  b = (1 << (7 - 5));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 5)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 5)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 5)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 6);
  Serial.println(a, BIN);
  b = (1 << (7 - 6));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 6)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 6)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 6)));
  Serial.println(e, BIN);
  Serial.println("----------");
  a = (7 - 7);
  Serial.println(a, BIN);
  b = (1 << (7 - 7));
  Serial.println(b, BIN);
  c = (p & (1 << (7 - 7)));
  Serial.println(c, BIN);
  d = !(p & (1 << (7 - 7)));
  Serial.println(d, BIN);
  e = !!(p & (1 << (7 - 7)));
  Serial.println(e, BIN);
  Serial.println("----------");
}

void loop() {
  Serial.println("done");
  while(1){}
}

digitalWrite() is documented to accept HIGH or LOW, which equate to 1 and 0. Any other values may work, but you would have to look at the implementation (as johnwasser did) to know. Even then, you would be using the function in an undocumented way, which could fail in the future if the implementation of digitalWrite() changes.