Close

Oh My... TRUE = NON-ZERO pitfalls

A project log for PotentiallyUseful/Frustrating/Obscure C/GCC/Make

Notes about unexpected findings in C/GCC... (and Make, and plausibly Bash, etc.)

eric-hertzEric Hertz 07/03/2016 at 20:246 Comments

Hah. somehow it'd never occurred to me... Hopefully because I never needed it like this... otherwise there may be bugs all over the place completely unknown.

---------------

So, "TRUE" in the sense of

if(something){<do something else>}
... is whenever something is non-zero. NON-ZERO.

Right, that's easy... and quite handy.

Checking whether a pin is high on a port...?

if( PORTA_INPUT_REGISTER & (1<<pinNum) ) { ... }
does the trick quite nicely...

And, with optimization, that's nothing more than a <read-register> and an <and> (the left-shift is turned into a constant, assuming pinNum is a constant).

Okie Dokie!

So now, let's use that somewhere a bit more complex... right? Not sure *why* exactly, but say you want to for some reason...

e.g.

uint8_t portInputs = PORT_INPUT_REGISTER;

if ( portInputs & (1<<pinNum) ) { ... }
Awesome.

Now say you're using that as an output to a function... why-not-eh?

//Returns NON-ZERO when the pin is high
//ZERO when it's low
uint8_t IsPinHigh(uint8_t pinNum)
{
   return ( PORT_INPUT_REGISTER & (1<<pinNum) ); 
}
...
if(IsPinHigh(3)) { ... }

Woot, it's all good...

So, doing-so this way saves quite a few instructions... Another way of doing something like this would be to have IsPinHigh() return ONE (the value, 0x01) if the pin is high, rather than NON-ZERO. But, doing-so requires an additional test, a non-zero test. which I guess would probably only be one or two instructions, but they're instructions, nonetheless. Another way, still, (and more obvious to me, for some reason) would be to >> the result back, e.g.:

uint8_t IsPinHigh(uint8_t pinNum)
{
   return ( ( PORT_INPUT_REGISTER & (1<<pinNum) ) >> pinNum ); 
}
So, now you've got a ton of instructions... but your output is either 0 or 1, rather'n 0 or (1<<pinNum).

Anyways, we *know* it's going to be used *only* in cases where non-zero or zero is what matters... e.g. in if(IsPinHigh()) statements, so why waste all those extra instructions?

Now, who knows, maybe you want it to be a little more complex, still... I dunno why... (back to the Zero v. Non-Zero example):

Or, heck, don't even bother making it more complex... We've got a pretty good setup, here. It works great!

Awesome. Now move that same bit of code to another processor, maybe a 32-bitter, where the Port-Registers are 16bits wide...

//Returns NON-ZERO when the pin is high
//ZERO when it's low
uint8_t IsPinHigh(uint8_t pinNum)
{
   return ( PORT_INPUT_REGISTER & (1<<pinNum) ); 
}
...
if(IsPinHigh(13)) { ... }

Whoops.

Duh.

See it?

I mean, all we care about is a boolean value, yahknow, it's either true or not true... That *easily* fits in a uint8_t. But... Yeah. Probably not worth all this explanation, really, because yahknow it's so blatantly obvious, here... Even more fun when you're porting code that's already been written and long-functional and it's not all spelled-out for you like this.

And worse, still, when the return-value of IsPinHigh() is stored in *yet another* variable, which may well be uint8_t as well...

Oy.

So, here's a nastier example where it might be *slightly* less-obvious.

//Returns NON-ZERO when the pin is high
//ZERO when it's low
uint8_t IsPinHigh(uint8_t pinNum)
{
   return ( PORT_INPUT_REGISTER & (1<<pinNum) ); 
}

//If isTrue is NON-ZERO, then doSomething
//If isTrue is ZERO, then don't doSomething
void doSomethingWhenTrue(uint8_t isTrue)
{
    if(isTrue)    { ... }
}
...

int pinIsHigh = IsPinHigh(3);
doSomethingWhenTrue(pinIsHigh);
Or, imagine you saw the glaring fact of IsPinHigh()'s return-value, and fixed it *right away*, but instead of pinIsHigh having been an int, instead it was a uint8_t... Or, yahknow, there might be any number of combinations wherein it might even get missed by -Wconversion (see below).

FYI: -Wconversion is a handy argument to throw at gcc... but you'll also get to see just how many conversions happen behind the scenes that you don't usually think about... E.G. did you know that ~(0xff) becomes an *int* (rather than the uint8_t it started as)? So, typedef or #define a new type for a case like this. Run it once as the uint8_t it was, capture the output from gcc (with all those gazillions of new warnings) and run it again with the new uint16_t (or 32) and *diff* the outputs from the two runs... helpful, but probably not guaranteed to find *every* case.

Discussions

Yann Guidon / YGDES wrote 07/04/2016 at 01:09 point

You started the wrong way anyway :-)

This all is easily prevented with:

whatever_t IsPinHigh(whatever_t pinNum) {
   return ( 1 & (PORT_INPUT_REGISTER >> pinNum));
}

This way you can even CHAIN the calls withough writing even more shifts, though if you have more pins to test, it's better to use a whole mask right away to prevent race conditions and save instructions.

Yeah, it's all a matter of how you look at things, and the bad habits we have learned...

  Are you sure? yes | no

Eric Hertz wrote 07/04/2016 at 08:54 point

"easily prevented with"... I knew there'd be a "Hindsight is 20/20" comment, if any :)

I didn't write the particular code that brought this up. But I have written code like it, so this new insight is one I'mma keep in mind for now on...

However, again, you *have* to use shifts in your code, whereas in the other code it could e.g. be inlined (or a macro) and there would be *no* shifts... shifts being relatively expensive (0xff>>7) takes seven shift instructions on an AVR (or maybe the optimizer would be smart enough to recognize that it could do a nibble-swap and three shifts?).

It's not so much "bad habit" as opposed to using a screwdriver as a hammer, or something. Or maybe more like using a nail for a shear-force load, never expecting someone to hang the thing from the ceiling. 

Again, if it was inlined or a macro (with constant pin-number, etc.), the entire comparison would be two instructions, and repeatably-so in all use-cases (on the same processor), regardless of the bit-number.

But, I agree, for general-purpose code or code that may be ported to another architecture (which is what I'm doing), it's probably significantly safer to always use 1=TRUE and 0=FALSE as return-values. (Who uses nails anyways, when screws are so widely-available?)

(Interesting side-note... I did the << >> method, once, long ago, and avr-gcc's optimizer, with -Os, was smart enough to recognize and minimize it down to only a couple instructions, I can't recall the exact operation it performed, but it was *way* optimized, and I was in awe).

And, you bring up another good point... I simplified this example for the write-up, making it more along the lines of the sorts of "mistakes" (maybe better: "mistaken presumptions") I've probably written in my code... But the actual case that brought this to my attention was something more like

uint8_t isAnyInputPinHigh() { 

  return ( PORT_INPUT_REGISTER & INPUT_MASK );

}

which, of course, fails when the PORT_INPUT_REGISTER is 16-bit (and inputs are on bits higher than bit7).

As it stands, I'm not trying to change the functionality of the original author's code, as it's known to work quite well... so I'm replacing the uint8_t return-type with a new portWidth_t and following the chains (weeee!). 

On the plus-side, it means I can test whether my code changes the original functionality quite easily, without even having access to the original architecture: build it for the original architecture, then diff the .elf files... so far, hundreds of lines of changes/abstraction, and the thing still builds identically to the original :)

  Are you sure? yes | no

Yann Guidon / YGDES wrote 07/04/2016 at 14:53 point

Yay !

  Are you sure? yes | no

Eric Hertz wrote 07/04/2016 at 22:58 point

Why didn't I see it before...? Here's an example from the code I'm working with:

#define bit_istrue(x,mask) ((x & mask) != 0)

I guess I don't know my C standard well enough to know whether that would be expected to return 0 or 1 vs 0 or non-zero, but 0 or 1 seems logical... and back to two or three instructions :)

  Are you sure? yes | no

Eric Hertz wrote 07/04/2016 at 23:05 point

Search-fu fail. K&R to the rescue!

"By definition, the numeric value of a relational or logical expression is 1 if the relation is true, and 0 if the relation is false" 

Man, it's been a good year or so since I've picked up this book from 1988.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 07/04/2016 at 23:34 point

It's never too late...

  Are you sure? yes | no