Sending multi-byte data over I2C between different processors
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 &amp;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 &gt;&gt; 0;  // least significant byte
bytes[1] = data &gt;&gt; 8;  // most significant byte


Or you can instead do the assignments the other way around
(data &gt;&gt; 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 *) &amp;data)[0];  // first byte
bytes[1] = ((uint8_t *) &amp;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.