I am sending two signed 16bit numbers via LoRa, where all the variables are declared as "int". The x and y variables are degrees in hundredths, and are between +/- 9,000:
but when they are back together as a 16bit integer, the sign is lost. If I don't do the subtraction step, the positive integers are fine, but the negative ones show positive values up near 65,300-ish, as you would expect if they were unsigned. Why doesn't the sign bit work when the 16bit integer is "reassembled"? There are no "unsigned" declarations in the code.
I know the sign bit is present in the 16bit reassembled integer because the subtraction step results in the correct original negative angle.
There is an issue with how you are declaring and/or displaying these results. If you have xavl and yavl declared as 16-bit integers (signed or unsigned) then add or subtract 65536 (0xffff) you will end up with the SAME value as you started with! Run the simple program below to convince yourself and substitute positive and negative values for x:
int16_t x;
void setup() {
Serial.begin(9600);
x = -1;
Serial.print("x before subtracting 65536: ");
Serial.println(x);
x = x - 65536;
Serial.print("x after subtracting 65536: ");
Serial.println(x);
}
void loop() {
}
Adding/subtracting 65536 causes an overflow and the carry bit is discarded leaving you with the same value.
if you are sending multiple data (e.g. mixtures of floats, ints, etc) over Lora put it in a structure and transmit/receive the complete structure at a time (assuming not too large!) see decimals-strings-and-lora
saves packing/unpacking ints, floats, etc - be careful if source/target use different word sizes
How can a signed 16 bit number "show" a value near 65,300-ish?
Without the code it is hard to see what is going wrong.
Can you create a small complete it compiles we can run it ourselves sketch that demonstrates the failure, similar to @red_car's in #4 above that shows basically that there is no problem
taking apart a two byte entity and
putting it back together without
damaging it in the least
There has to be something you are doing differently.
Nope. I put your code in and, just as in the first post example, I get the two's complement. Here's the printout of your serial messages (I set x = -150 instead of -1). But I also get a double (bigger than 65536) and a printout as if it's a float. :
x before subtracting 65536: -150.00
x after subtracting 65536: -65686.00
I am using "int" to declare x. I assume that makes x a 16-bit signed integer. Is there something more special about "int16_t"?
Just to be clear about what the ESP32 is doing--I send the integer -150 as two bytes and reassemble it. It prints as positive 65,386 in the slave unit. So I subtract 65536 and it then prints as -150, the correct original integer. I am inferring from this that there is another register associated with the variable xavI that "defines" it, either signed or unsigned. When I concatenate the two transmitted bytes, I get all the correct bits in that new integer, but it is seen now as unsigned. Why? What does it take to make it back into a signed integer? My subtraction trick works, but it bugs the you know what out of me because I don't understand why the sign bit is ignored.
Oops, was just reading the reference about "int" and there is a mention that some machines store a variable declared as "int" in 4 bytes. That would explain the unexpected behavior. I think that must be the case for the ESP32.
Even better would be to use a struct as has already been suggested. If you don't like that for some reason, you could use an array since the variables are all the same type.
An int is 16 bits on an UNO but 32 bits on an ESP32.
We could also assume that the endianness of the sender and receiver are the same but nothing tells us that
I recall from those discussions that you can legally cast an object's pointer (&xavI in this case) into a uint8_t * and then dereference it to access individual bytes. That's what memcpy() does, right? Take a look at the source code for the Stream class's readBytes() function. You'll see it does exactly the same thing.
if you have a uint32_t it's OK to use it's address and cast to access its individual bytes indeed
here you want to do the opposite. you have a flow of bytes coming from Lora and you want to force them into a variable of a specific type.
I think the "OK approach" is to get the bytes from the flow into a byte buffer (as you get individual bytes from Lora.read() or LoRa.readBytes) and then memcpy() this buffer into the target variable storage space.
I'm unconvinced that it's not the same thing. Take a look at readBytes(). It takes bytes one-by-one from the LoRa stream and puts them individually into the object via a char *. That's also what memcpy() does.
It again accesses the object's bytes one at a time and puts them in a transfer buffer. That's what memcpy() does. I've never heard an objection to write().
it's OK to store chars into a char buffer as readBytes does and it's OK to cast to char* and access individual bytes of a multi-bytes variables. The spec says so
The fact that memcpy() does the same thing is irrelevant here, it's implementation detail and disregards what the compiler will do in between.
The issue is that the casting is invalid because of C++’s strict aliasing rules. Basically, you mustn’t cast a pointer to a different type and then dereference it unless you cast to char*.
The purpose of strict aliasing and related rules is to enable type-based alias analysis, which would be decimated if a program can validly create a situation where two pointers to unrelated types (e.g., an int* and a float*) could simultaneously exist and both can be used to load or store the same memory (see this email on SG12 reflector). Thus, any technique that is seemingly capable of creating such a situation necessarily invokes undefined behavior. When it is needed to interpret the bytes of an object as a value of a different type, std::memcpy or std::bit_cast (since C++20) can be used
so if you want to respect the specification, you get the byte buffer and memcpy() it into the target variable's address and the compiler knows about (and most of the time will recognize and optimize) this type of usage of memcpy(), it's likely that no copy will occur but the type-based alias analysis will be fine.
You transformed a pointer of type int16_t into a char * buffer. that's fine for reading the bytes but that's not fine for writing those bytes and then later on reuse xavI as an int16_t (or whatever xavI was)
the spec states
AliasedType is std::byte, (since C++17) char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.
you need a two steps process
char buf[sizeof xavI];
Serial.readBytes(buf, sizeof buf); // get the bytes in a byte array
memcpy(&xavI, buf, sizeof xavI); // move the byte array content into the variable