0

I have been attempting to read data from an ATTINY84 via an ESP8266. Both are programmed via the Arduino IDE, though as PlatformIO is a proxy for that, I'm not certain what version of the ATTINY core is in use.

I have both devices connected to the I2C bus, with the Arduino as a slave. The ESP sends a code requesting a particular type of data, then calls Wire.requestFrom() to get 8 bytes of data. I am concerned over both my implementation and about potential endian-ness issues (even if it should turn out not to be an issue on these two particular devices, I'd like to know my future options).

The ATTINY code is a complex function that is called when there is a request (and the right option code, but that's probably not very relevant). It writes eight bytes using Wire.write after uncompressing them from the relevant arrays. (I have a lot of ADC data that technically should not fit in RAM.) Since this is is a callback function or possibly an interrupt, do I need to be concerned about data corruption if the code is too slow to write the values onto the bus? I have to imagine it's trying to read them even as it writes. I can also provide code snippets if it would help.


I am also unsure if I am running into Endian-ness issues by using bitshift to grab the 2 and 8 bits from the ADC return value. Would this always give me the correct values, or does the C++ compiler (as I assume it does not) simply refuse to handle differences between the data order? Specifically, if I shift a uint16_t (or any non-trivial datatype) right by 8 bits, and mask out the bottom 8 with &0xFF, do I always get the second-least-significant (in this particular case, high) byte, or can the compiler produce alternate outputs based on byte order?

Does Arduino have an equivalent of network byte order (or does Wire automatically do so for its overrides of the functions) that I can get around this potential issue with?

1 Answer 1

1

There seems to be multiple questions in what you are asking. Here I try to answer specifically this one:

if I shift a uint16_t (or any non-trivial datatype) right by 8 bits, and mask out the bottom 8 with &0xFF, do I always get the second-least-significant (in this particular case, high) byte, or can the compiler produce alternate outputs based on byte order?

Short answer: yes, you get the second-least-significant byte, irrespective of the platform's native endianness.

Now, I'll expand on this answer by showing the different ways you can convert an uint16_t to a sequence of bytes, and the implications on the byte order.

If you want to control the endianness, and you want your code to work independently of the native endianness of the platform, then bit shifting is the way to go. Let's assume you want to convert this:

uint16_t data;

into this:

uint8_t bytes[2];

You can do it this way in order to get the least significant byte first:

bytes[0] = data >> 0;  // least significant byte
bytes[1] = data >> 8;  // most significant byte

Or you can instead do the assignments the other way around (data >> 8 goes to bytes[0]...) if you want MSB first. Note that there is no point in masking, as the assignment does it implicitly.

In contrast, if you wanted to put the bytes in native endianness (the endianness of the platform), the traditional way to do it is to cast the address of the number to a pointer to bytes, then read from this pointer:

bytes[0] = ((uint8_t *) &data)[0];  // first byte
bytes[1] = ((uint8_t *) &data)[1];  // last byte

You can swap the indices on one side of the assignments if you want to instead reverse the byte order. But note that this kind of casting is now frowned upon, as it violates aliasing rules. A common alternative is to have the number and the byte array share the memory within an union:

union {
    uint16_t data;
    uint8_t bytes[2];
} x = { .data = some_16_bit_value };
do_something_with(x.bytes[0]);  // use first byte
do_something_with(x.bytes[1]);  // use second byte

These are the main byte-splitting idioms you may come across. If your purpose is to have an endianness that you control and is independent of the platform, then stick with the first option: the old good bit shifting.

3
  • That answers most of what I wanted, although the part about pointer-casting doesn't have the results I was hoping to hear. What happens if I send a uint32_t, for example, through Wire.write() (or an equivalent function) directly, though? Does that send in native byte order, or does Wire handle that automatically? I'd like to avoid bit-shifting for some of my more complex messages if possible. Oct 3, 2019 at 16:43
  • I kinda had three sub-questions, which I'll admit kinda doesn't help matters, but they are also somewhat related. One was what the bit-shifting actually does, the second is if the Wire library handles endian-ness discrepancies on its own, and the third (which is a bit less related) is exactly how/when data is written or requested when the master requests data -- is there a chance of the slave not having it ready in time or writing too slowly (that would cause some later section of the message to be wrong while the first was correct). Oct 3, 2019 at 16:46
  • 1
    @RDragonrydr: Wire.write() doesn't handle endianness. It sends either a single byte (Wire.write(value) sends only the LSB of value) or a byte array when called as Wire.write(const uint8_t *buffer, size_t length). Oct 3, 2019 at 20:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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