Question

Confused about casting and order of operations

This is a very specific problem and I haven't heard back from the author, so I thought I would throw this out and see if my sanity is intact or not.

This is in regard to this video regarding DMA on a particular ARM processor STM32F407 and casting 16 bit words to 32 bit. I used to consider myself an expert in C but something is wrong here or my thinking is wrong.

Now around 16:46, you will see this setup for the DMA:

uint16_t rxBuf[8];
uint16_t txBuf[8];
...
HAL_I2SEx_TransmitReceive_DMA(&hi2s2, txBuf, rxBuf, 4);

And around 19:05 you will see this in the callback routine:

int lsample = (int) (rxBuf[0]<<16)|rxBuf[1];
int rsample = (int) (rxBuf[2]<<16)|rxBuf[3];

Now, what's going on is that the STM32F407 can only do DMA using 16-bit words, even if the data on the serial bus (I2S) is 24-bit or 32-bit data. I am sure (or assuming) that type int is the same as int32_t. The value in rxBuf[0] contains the upper 16 bits of a sample and rxBuf[1] has the lower 16 bits of the 32-bit left-channel sample.

My problem is the parenths surrounding rxBuf[0]<<16 do not include the cast (int) to its left.

How can that possibly work? The cast to 32 bits applies to the result (rxBuf[0]<<16) but what is inside the pareths is only a 16-bit unsigned value being shifted left by 16 bits. There should be nothing but zeros left in that 16-bit value when it is cast to 32 bits.

I think the two lines of code should be

int32_t lsample = (int32_t) ( ( (uint32_t)rxBuf[0]<<16 ) | (uint32_t)rxBuf[1] );
int32_t rsample = (int32_t) ( ( (uint32_t)rxBuf[2]<<16 ) | (uint32_t)rxBuf[3] );

First you must cast the value rxBuf[0] into (uint32_t), then shift it left 16 bits. C will naturally cast the 16-bit value rxBuf[1] to 32 bits since the left operand is now 32 bits, but it's good to be explicit with the cast anyway.

But that is not the code shown and the guy's project obviously works. But I cannot see how you can get anything other than zeros in the upper 16 bits of the word.

Is it that the compiler isn't working correctly and serendipitously treating these 16-bit values as a 32-bit word? Or am I (after 4 decades of coding in C) just not understanding the correct syntax for casting in C? The C reference manual seems to support my understanding of the correct syntax.

 5  154  5
1 Jan 1970

Solution

 8

What's happening here is a result of integer promotion. Generally speaking, any expression using a type smaller than int will promote that type to int.

The exact verbiage of this is spelled out in section 6.3.1.1p2 of the C standard:

The following may be used in an expression wherever an int or unsigned int may be used:

  • An object or expression with an integer type (other than int or unsigned int) whose integer conversion rank is less than or equal to the rank of int and unsigned int.
  • A bit-field of type _Bool, int, signed int, or unsigned int.

If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions

And the bitwise shifts are one such operator where this applies, as specified in section 6.5.7p3 of the C standard:

The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined.

So in this expression:

int lsample = (int) (rxBuf[0]<<16)|rxBuf[1];

The value of rxBuf[0] is first promoted to int before the left shift operator is applied. The value of rxBuf[1] also gets promoted to int before the bitwise OR operator is applied. So the cast actually has no effect in this case.

There is however a bug here. Assuming an int is 32 bits, if the high bit of rxBuf[0] happens to be set, then the shift will result in a bit value of 1 being shifted into the sign bit of the result. This will trigger undefined behavior as per section 6.5.7p4 of the C standard regarding the bitwise shift operators:

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1 × 2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

The proper way to handle this is to cast the value of rxBuf[0] to uint32_t to allow the shift to work properly:

int32_t lsample = ((uint32_t)rxBuf[0]<<16)|rxBuf[1];
int32_t rsample = ((uint32_t)rxBuf[2]<<16)|rxBuf[3];
2024-07-01
dbush

Solution

 4

C implicitly promotes integer arguments of the shift operators with rank less than int to int. The (int) doesn't do anything there.

2024-07-01
Mark Adler