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.
rxBuf[0]
is set, then a right shift by 16 will shift it into the sign bit, which is signed integer overflow causing undefined behavior. So what you probably want is(uint32_t)rxBuf[0] << 16 | rxBuf[1]
, and assign it to a variable of typeuint32_t
. – Conniventunsigned
here, and being explicit about the width of the type becauseint
is implementation-dependent. – Gintzuint32_t
should be used instead ofint
and then the result be cast toint32_t
. It should beint32_t lsample = (int32_t)(((uint32_t)rxBuf[0]<<16 )|rxBuf[1]);
if we're gonna be super strict and proper and explicit with casting. But that(int32_t)
cast could be left out since it's implicit in the assignment. I disagree about undefined behavior. If there was sign extension (and that would happen if the 16-bit value wasint16_t
) then the 16-bit value is sign-extended in the cast and the 16 sign bits fall off the edge instead of 16 zeros. – Vicissitudeuint16_t
which is not sign-extended. I don't think most compilers enforce the rule against overflow for shifts, because there's probably lots of bad code relying on it, but for instance UBSAN traps it: godbolt.org/z/nzxdxj6Wq – Connivent40000 * 2^16
is 2621440000, which is greater than the maximum value ofint32_t
(2147483647), hence overflow and UB. You don't have to shift out of the sign bit to cause overflow; shifting in is bad enough, because you've overflowed the 31 value bits. – Conniventuint16_t
. But that value inside the pareths is still only a 16-bit value and, if the compiler is working according to spec,(rxBuf[0]<<16)
should simply be all zeros whether it'suint16_t
orint16_t
. It's left shifting so only zeros go in from the left and whatever bits that are shifted to the right fall off the edge. It should be all zeros. – Vicissitudeint
is 32 bits, whenuint32_t
is promoted toint
it will always be positive even if bit 15 was 1. You then shift that bit into the sign position, essentially creating a negative from an originally positive value which is undefined behavior according to the language. – GintzrxBuf[0] << 16
is shifting anint
, despite there being no explicit cast. So you really are shifting within a signed 32-bit number, not an unsigned 16-bit number. C doesn't actually have any way to do the latter. – Conniventuint16_t
being promoted to 32 bits? I don't see it. That word is a 16-bit word and it got all of its bits shifted out. It should only be 16 zeros remaining before it's cast to 32-bits. – Vicissitudeint
is implicit!!!! There's nothing to see. It happens automatically whether you ask for it or not, and can't be prevented. – Conniventuint16_t
value shifted by anint
value. Theuint16_t
is promoted toint
and then the shift happens on thatint
value. The result is alsoint
. – Gintzint
!!!! Shit! Crappy code and serendipitous luck. When it's shifting by a constant, I don't think of that constant as any more than 6 bits. It's not even anint
. – VicissituderxBuf[0] << (uint16_t)16
would be just the same. EVERY expression of typeuint16_t
gets promoted toint
.rxBuf[0]
gets promoted no matter what context it appears in, BEFORE the shift ever takes place. – Conniventchar
s promoted toint
like that? – Vicissitudeint
, which is to say, every type whose range fits within that ofint
. – Conniventuint16_t x = 5000; uint16_t y = x << 7;
, or eveny = x << (uint16_t)7;
, you may think you're shifting within a 16-bit value, but in the C abstract machine, you're shifting within a 32-bit value (or whatever sizeint
happens to be), then truncating the result to 16 bits. A compiler can of course optimize this to a shift of a 16-bit object if the target machine has such an instruction, since the end result is the same, but it isn't what is being done conceptually. – Conniventldrh r0, [x]
zero-extends into the 32-bit register.lsl r0, r0, #7
shifts the entire 32-bit register.strh r0, [y]
stores the low 16 bits only. You actually computed the high 16 bits of the 32-bit result of the shift, but then ignored them. Unlike x86, which can doshr ax, 7
on a 16-bit partial register, ARM has no 16-bit arithmetic or logic instructions per se. – Conniventint/unsigned
may be as narrow as 16-bits. Better to useuint32_t
(or wider) here.uint32_t lsample = ((uint32_t) rxBuf[0]<<16)) | rxBuf[1];
. – Postobitfloat
and scaled by 2^(-31) so that their range is between -1.0 and +0.99999999 . – Vicissitudeuint16_t
values to afloat
. – Postobitint32_t lsample = (int32_t)( ( (uint32_t)rxBuf[0]<<16 ) | (uint32_t)rxBuf[1] );
– Vicissitudeint
definitely 32-bit on this platform? – Desalinateint
was 32 bits because the STM32F407 has a 32-bit wide data bus. I just believed that, without explicit casting, a 16-bit number stays 16 bits until it's in an operation mixed with a 32-bit or longer word. I did not know that, implicitly, a 16-bitshort
would be cast (or "promoted") toint
. Not until an operation with anint
. – Vicissitude