Why not use: x = !x;?

This is true, however my point is that this abstraction is too strong. At the end of the day, there are no processors in the core set that do not present a digital "0" or a digital "1" on a pin, or read one there. It's true that some very green beginners might not know that 0V (low) corresponds to a binary value of 0, and a Vcc (high) correspond to a binary value of 1. However I think that this gap should narrow very quickly even for a beginner to do useful things.

The fact that "there is no assurances as to what HIGH and LOW really are" is an unnecessary and arbitrary condition. That is because it doesn't reflect the reality of the hardware environment in which it is used. If there are no assurances, there should at least be some method that does offer assurance. In fact there is, for example 'digitalWrite(pin, 0)' but then we should stop beating up people for failing to utilize the abstraction.

If, some day, someone decided to make HIGH==0 and LOW==1, and of course modified the GPIO API to reflect that change, it would force the use of the abstraction and exclude a lot of possibilities for the logic that are routinely suggested on this forum, for example using it with bit manipulation of unsigned integer values (classic case is displaying a binary number on some LEDs). In the case of the API which is modified in the way I've described, it would not work, and it would break 10 or more years of code.

To revisit the original problem, we are describing a LED state. LEDs are not HIGH or LOW, they are ON or OFF (when connected to a digital output, anyway). This abstraction, also, can not be utilized with boolean logic, e.g. 'ledState = not ledState' because such logic is not defined by the abstraction. Perhaps, to blue sky a bit, the '=' and the 'not' operator could be overloaded for that purpose. But it isn't.

I hope that you can see the sources of my distaste for the whole thing, and although I admit that my suggestion of a purely boolean approach might not fit the problem, I do not believe that the existing abstraction is working very well for what it is supposed to do.

The outcome of fully accepting boolean values instead for the API would result in statements like 'digitalWrite(pin, false)'. Also statements like, 'digitalWrite(pin, not digitalRead(pin) );' would be legitimized. I don't believe yet that it would be such a terrible thing.

Why? ledState is simply a user variable and not defined by Arduino IDE. The compiler wouldn't care if ledState is a bool, int long or float.

(I just tried it).

But only one of those is semantically correct... remember the warnings are mostly disabled...

I always use "compile warnings: on" in setup. No warnings.

You are correct that semantically correct means that you use datatypes for what they are meant to be used for. NOT is a binary operation, I.E. boolean.

But a bool is actually a byte in Arduino and NOT 1 is zero for all of the types I tested.

That's part of the definition of a bool - it should use the most compact available data type, which is byte, AKA unsigned char AFAIK, at least on most Arduino systems. Processors have parallel data lines, so a single bit access while not throwing away the extra bits of a word is not possible in a machine cycle on all systems. But the goal is also minimum storage requirement.

Yes, many types are derived from other types. The use of 'not' does imply bool, I suppose LEDs could have more than one state, such as RGB combinations. Then the assignment would mean something else.

Not with input pullup:
switch

1 Like

I see what you’ve done :upside_down_face:

I would agree with you but the fact is that the Arduino.cc developers have, over the years and to this day, chosen to not ever nail down the value parameter for digital i/o functions other than state that the value parameter must be HIGH or LOW for the digital i/o functions.
As such, there is no assurances as to what HIGH or LOW are or that they will not change from their current declaration/definition in the future or be different in different core implementations.
And because of this, a user of the API functions that are documented to only use HIGH and LOW should ALWAYS be using those symbols and ONLY those symbols (HIGH and LOW) for the value parameter since that is how they are documented to work.
The official documentation for the digital functions like digitalWrite(pin, value)
here: digitalWrite() - Arduino Reference
defines the function to take HIGH or LOW for the value parameter:

value: HIGH or LOW.

It never defines what these symbols are or that the use of anything other than HIGH or LOW symbols can be used.
So in order to be completely compliant with the API as defined in the official documentation, any code that uses the digital i/o functions must always use the HIGH or LOW symbols for the value parameter.
I wouldn't define API functions this way.
IMO, it is way too ambiguous and limits certain use cases, but this is what they have done.
It is irrelevant if we would like it to work differently, it simply isn't documented to work differently so there is no guarantee how it would work or if it would work at all if you step out side of the way it is documented and abuse the API by using knowledge of a particular implementation and use the functions in a way that is not specified in the official documentation.

It doesn't matter how the h/w works.
This is about how the API is documented to work. And the API as documented by the Arduino.cc developers currently requires using the symbols HIGH or LOW for the value parameter.
The behavior when using anything other than those symbols is undefined.

I agree, but that is how it is defined in the official documentation.
Code that uses an API should conform to the official API documentation to ensure portability.

NOPE.
digitalWrite(pin, 0) is undefined.
The documentation for digitalWrite(pin, value) only defines the behavior when LOW or HIGH is used, using anything else is undefined.
That it may work on particular implementations does not change the fact that using 0 instead of LOW is abusing the API and according to the official documentation is not guaranteed to work.

This type of discussion comes up every few years.
The Arduino documentation has defined how the the digital functions work.
The value parameter is defined to be HIGH or LOW and using anything else is not guaranteed work since the documentation does not specify the behavior when using something other than HIGH or LOW.

--- bill

1 less "new thing" to confuse beginners.

Teach em bit logic.

You actually really describe the pin’s state. Wether HIGH will turn the led ON depends on your board/circuit wiring. you can’t know just from the code, you need extra documentation.

Yeah, I realized that later on but decided not to muddy things by trying to retract it. I think you probably mean, from the point of view of the intended format of the parameters. In this case it is also ambiguous in that '0' does not imply any specific data type.

I also realized that using boolean types would limit the number of available operations, as the logical operators are only "and, or, not", while the bitwise boolean operators include "^,>>,<<" as well. The shift operators can only operate on an integer type, IMO C++ gets fuzzy on type casting here. It's allowing two concepts to overlap, integer and bit array. It's convenient, that's why no attempt has been made to change it AFAIK.

It might be helpful to decouple the requirements of the specification of a value to pass to the API, vs. the possibilities of representation of those values in a user program, in which there is conceivably more flexibility. In this code:

// if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }

ledState is not actually passed to digitalWrite(). It's sort of implied that maybe it will in subsequent code. We also don't really know if it was properly declared as a 'pinState' type... I don't know whether this is checked in the current environment, but it should be, to fully conform to the API requirements if later on you did:

digitalWrite(pin, ledState);

In theory, there are two important properties of a pinState type, the value, and the underlying type (because pinState is not a native type). To respect and uphold the portability of code utilizing it, it is true, one can not make assumptions about those because someday they might change. However, I think that what has happened, is that the definition of HIGH and LOW, and the type definition of pinState, are so congruent with pragmatic I/O and the low level behaviour of the pins, that a false identity has been allowed to take root. That is why sometimes you will actually see something like

digitalWrite(pin, i);

Where 'i' is an integer. I wonder, how much code actually exists, that does not actually bypass the strict definitions of the API in this way. Now the question, does that legacy code deserve the same continuing support as the 100% compliant code? If so, in a way, you are supporting both an API, and it's nemesis, on an ongoing basis.

I would argue, this is the true reason why the actual values of HIGH and LOW will never change.

I think my issue is that I want to manipulate pinStates as if they are typical digital states, and a strict imposition of the pinType paradigm prohibits that (at least in order to be future proof).

The only way I can think of to circumvent that, is to perform bit manipulations in some other type, like integer, and then restrict the values actually passed to digitalWrite() like so:

byte ledState
if (digitalRead(pin) == HIGH) ledState = 1; else ledState = 0;
...
ledState = ! ledState;
ledState = ledState ^ someOtherValue; // to illustrate additional possible boolean operations
...
if (ledState !=0) digitalWrite(pin, HIGH); else digitalWrite(pin, LOW);

The part of the API contract that wasn't discussed, and has some bearing on this, is the value returned by the digitalRead(). It's little confusing to try to grasp what it means if it returns only HIGH or LOW, then to maintain symmetry with digitalWrite, those values and datatype should be the only ones used as well (as I did above).

I can't say that I have offered any improvement on the status quo. But if the subject keeps coming back, it's for a reason - the API definition is so strict, as to rule out some convenient operations at least directly. I'm trying to see a way that this could be improved.

One way is to extend the API specification of a pinState, to dictate that it can only have the values 0 or 1, and that it be stored in the least significant bit of an integer. This might seem like breaking an abstraction, but I think in this case, the compelling uniformity of the underlying hardware suggests that it be subsumed into the definition. Then I could legitimately

pinState newState = not ledState;

and so on. It's common to choose either logical or bitwise operations on an integer value, according to the need. It's allowed, where something like bitwise operations on a bool variable might be prohibited (I never tried, though...).

I think it is already performed that way, and tacitly accepted in the rule breaking legacy code that I described earlier, that passes integer values to digitalWrite().

I would say it keeps coming back up because like so many other API definitions in Arduino, it is sloppy and vague particularly values/types and return values.
I believe that this originally happened due to the inexperience of the original Arduino developers and now many of these types of issues are impossible to correct unless there is a "break the entire world" event similar to what happened when Arduino 1.0 was released when the Ardino.cc team knowingly decided to break 100% of all the existing 3rd party code. We are now coming up on the 10 year anniversary of the 1.0 release in just a few weeks and there are still issues that show up every now and then from that "break the world" decision back then.
And now the ecosystem is much larger and the pain would be even worse to try something like this again.

(But some of these types of things are still happening, so it wasn't just a thing of the past)

And yes the type is never specified as well and that makes things even more difficult as there is technically no way to ever use a variable for the value parameter.

I have an LCD library that I want to be portable across all platforms.
In that code I never use variables and never use anything but HIGH and LOW for all the digital i/o functions.

Most Arduino code is not so careful.

If you go look at what the Arduino.cc devs did in the core where they changed HIGH / LOW from defines to a specific type, (They did this on the magaavr core) they found out that it broke a considerable amount of existing code.
(lots of discussion about this on the developers list)
The breakage was so severe that they went back and added some C++ overloads to support the pre-existing code and created Compat.h

#ifndef __COMPAT_H__
#define __COMPAT_H__

namespace arduino {

inline void pinMode(pin_size_t pinNumber, int mode) {
	pinMode(pinNumber, (PinMode)mode);
};

inline void digitalWrite(pin_size_t pinNumber, int status) {
	digitalWrite(pinNumber, (PinStatus)status);
};

}

#endif

So at this point, it is pretty much a given that value parameters for the digital i/o parameters are int compatible.

That said, they didn't fully solve the issue, since they only "fixed" it for C++ code and there can still be issues with existing C code that uses the digital i/o functions.

IMO, they should go back and update the documentation to specify this and just state that value is an int compatible type and make it that in the actual implementation.
But that still doesn't specify what value LOW and HIGH need to be.
In fact things seem to be moving in a direction that could create some new incompatibility issues.

In terms of changing the API to now specify that a zero value means low and non-zero value means high, you can't do that anymore since they now have defined, at least in the megaavr platform, a new parameter of CHANGE for digitalWrite() that flips/toggles the pin state.

		/* Set output to value */
		if (val == LOW) { /* If LOW */
			port->OUTCLR = bit_mask;

		} else if (val == CHANGE) { /* If TOGGLE */
			port->OUTTGL = bit_mask;
									/* If HIGH OR  > TOGGLE  */
		} else {
			port->OUTSET = bit_mask;
		}

With the addition of CHANGE, a core can no longer use non-zero to mean HIGH.
Even worse, there is currently no way for a sketch to compile time detect that CHANGE is supported so as currently implemented, this new pin change feature is not portable and since is no way to automatically work around this by compile time detecting its existence and providing a compatible, albeit slower, work around for when CHANGE is not supported.

IMO, lots of rookie mistakes are often made by the arduino.cc devs as they attempt to migrate in new capabilities.

--- bill

Yes, I can live with an 'int' interface. Just trying to keep my answer short. :slight_smile: Thanks for the detailed feedback.
Ken

If everyone will allow me one brief question though, could digitalWrite() not be overloaded to accept different type parameters, e.g.

void digitalWrite( int pinNum, int outputVal);
void digitalWrite( int pinNum, pinType outputVal);

and then negotiate the results of different levels of abstraction, inside the definitions? Then, 'digitalWrite(pin, 1)' goes to the first, and 'digitalWrite(pin, HIGH)' to the second.

I long ago gave up on adhering exactly to the official API in my cores out of frustration with things like this. I used to just say "Well that's what the official core does" - but the official behavior is ill-defined and occasionally just bad enough that I gave up -
What made me rip the enums out of megaTinyCore in fury was actually a long confusing debug process before discovering that I had accidentally capitalized the P in a pinMode call and wasn't getting a compile error. At which point I discovered that the code I'd taken from the official "megaavr" core used enums in that way and was horrified that I hadn't noticed for so long), The main advantage that the enums offered WAS that they wouldn't let you write things that weren't valid values to the pins, which was defeated by the compatibility code.

Aside from not using the enums on modern AVRs ("megaavr" is a totally wrong name, as Microchip uses "megaAVR" to mean something else) I try to maximize compatibility of the digitalRead()/etc functions even though I rarely use them because they are so mindbogglingly slow, bulky and awful.

I should probably revise the documentation of my cores to be more explicit about documenting return types and accepted input values for Arduino API functions as implemented in my cores, since yeah, they are not well defined in the official API documentation, and my cores do differ from the official implementation in a few subtle ways that are still consistent with the official documentation).

My general impression has been of the Arduino team being very good at outreach and making an IDE that doesn't have a tough learning curve and is generally good enough (I still don't us alternative IDEs, I know many people use VSCode or PIO with Arduino cores and libraries) particularly for educational applications) - who would have imagined that EMBEDDED C++ would be a effective language for students' first exposure to programming? But it seems to work better than a lot of introductory programming curriculums that use "easier" languages! But that they're not the best api designers. embedded developers. or hardware designers.

Oh well, it will suffice for me that according to the code given in the reference, pinState is an integer.

So if x is an integer and x is 0, then !x is 1, right?

Of course, and you'll see loads of code that takes advantage of that. The chance of the values of HIGH and LOW ever changing is vanishingly small, but it's still bad practice to take advantage of what you know about the implementation details.

Is that right, then, Paul? I didn't know you could do that with integers, because I assumed !x could logically be any non-zero integer (in fact, all non-zero integers), rather than specifically 1. Presumably this must be addressed in the formal definition of C++.

Yes, that is how the abominable '!!x' came to be - converting any non-zero value to a 1.

1 Like

Technically no in c++.

You apply a Boolean operator to an integer so the integer 0 is first promoted into a bool and becomes false and then the not operator is applied and you get true. So the type of !x is a bool, not an integer, and the value is true. (Which is its own thing in C++, unlike C)

If you use it where an int is expected then the rule will apply an true will be promoted into 1.

Interesting, suppose x is a long integer, large value but the lower bits are zero. Suppose bool are represented by a byte. When it's promoted to a bool, are the upper bits truncated and resulting in 'false', giving the wrong answer?