Style question for sending 16bit values over UART.  - Page 1
Forum section: electronics / general

author: Obi_Kwiet
author_rank: Regular Contributor
timestamp: February 07, 2019, 03:59:26 am
content: I'm dusting off an old project, and I decided I wanted to address the few compiler warnings I had. I'm working in C.At one point I need to pass an array of 16bit values to another micro-controller over uart. The driver from the TX micro takes a pointer to an array of uint8_t. My solution was to simply set the uint8_t array pointer equal to the first element pointer of the 16bit array, and feed the driver that, like so.Code:uint16_t LEDArray;char *ptrLEDArray;ptrLEDArray = LEDArray;This works fine, except I get a compiler warning about assignment from incompatible pointer type. Should this warning be ignored, or is there a better way to do this? I can think of some ways, but they all involve writing a loop that runs through the array, which is pretty lame. Is my approach bad practice for reasons I don't understand?

author: ataradov
author_rank: Super Contributor
timestamp: February 07, 2019, 04:09:28 am
content: Your approach is fine, don't overthink it, just do a cast: "ptrLEDArray = (char *)LEDArray;".

author: newbrain
author_rank: Super Contributor
timestamp: February 07, 2019, 12:12:44 pm
content: As ataradov said.But, let's overthink it a bit!Converting a pointer to whatever to a pointer to char is one of the few pointer conversions explicitly permitted in the C standard, in chapter 6.3.2.3 Pointers, clause 7 ofC99 draft:QuoteSpecifically, the typeuint8_tis usually defined asunsigned char, so the above should hold.If the array address is cast to a(char *), though, a conforming compiler might still complain ascharandunsigned charare not the same type (clause 15 and footnote 35 in chapter 6.2.5 Types), even where char is unsigned by default or due to target architecture.I would then suggest to useuint8_t*orunsigned char*in the cast.One reasonunsigned charis commonly used as a way of passing byte sequences is that it is the only type for which the standard explicitly guarantee that all its bits concur to forming the value (so one won't lose information when accessing it), see 6.2.6.1 cl. 3 and 6.2.6.2 cl. 1.

author: Doctorandus_P
author_rank: Super Contributor
timestamp: February 07, 2019, 01:52:05 pm
content: My prefered method is to mae a union in which the data can either be accessed as an array of integers or an array of bytes.It is bad practice to ignore warnings. Once you have too many warnings you want to ignore, you do not see the single other / important warning anymore.Be carefull though that methods like this can cause problems with endianness. It is safer to write some small support functions to explicitly serialise your data and extract high and low byte of the integer in a well defined order, for example:uint8_t high = big >>8;uint8_t low = big & 0xff; // This may need a cast to suppres "assignment to smaller datatype"

author: Cicero
author_rank: Contributor
timestamp: February 07, 2019, 01:57:50 pm
content: Quote from: Doctorandus_P on February 07, 2019, 01:52:05 pmCame here to say this...

author: Obi_Kwiet
author_rank: Regular Contributor
timestamp: February 07, 2019, 02:14:55 pm
content: Good call on the cast. I don't do a ton of software, so I didn't think about casting the pointer. I figured that I was setting one memory address type equal to another, but I guess the cast should just tell the compiler that I meant to do that.ataradov, if I am understanding you correctly, it sounds like uint8_t is the only data that C guarantees the storage format for? (Other than endianness.) I guess you just need to be aware of your architecture before using this method.I don't see any good reason to worry about byte order from the perspective of the other micro-controller. If the other micro is opposite endian, you'll still have to write a function to convert the bit order as well, so you may as well figure that out and write it all in one place. (And I think I did.) This way you do a single operation instead of executing multiple instructions on every array element and creating a duplicate array.

author: newbrain
author_rank: Super Contributor
timestamp: February 07, 2019, 02:27:21 pm
content: Quote from: Doctorandus_P on February 07, 2019, 01:52:05 pmI, for one, welcome our over-overthinkers.But this is one of the few cases where a cast is benign and also sanctioned by the standard.Quote from: Doctorandus_P on February 07, 2019, 01:52:05 pmQFT.And silencing them with casts is often not a good thing.Quote from: Obi_Kwiet on February 07, 2019, 02:14:55 pmYes, that's the purpose in this case.Quote from: Obi_Kwiet on February 07, 2019, 02:14:55 pmI think you mean me, not ataradov, who gave a perfectly good answer and did not indulge in going into the details rabbit hole..And yes, that's my reading of the standard.In practice, I'd be hard pressed to find a counter example in modern, reasonable architectures, where (signed or unsigned) integers have padding bits (I'm sure someone will come up with one!).

author: ataradov
author_rank: Super Contributor
timestamp: February 07, 2019, 03:25:25 pm
content: Quote from: Obi_Kwiet on February 07, 2019, 02:14:55 pmYes, but I typically design all my protocols to be little-endian. This way all my stuff is compatible with sane architectures. For others you would have to do support functions and all that jazz. But there is really no need to think about until you actually do a port to BE architecture. And it may be never.So you are not violating anything. Your protocol is rigidly defined. It just happens to be defined in a way that works out of the box on LE architectures.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 07, 2019, 03:59:10 pm
content: Mind that when you do something like,Code:low = (*big_p) & 0xff;high = (*big_p) >> 8;big_p += sizeof(array);assuming sizeof(array) is 2 (so you aren't skipping bytes), the compiler will emit code to access the first byte, then the second byte, then increment the pointer. In subsequent optimization steps (assuming more than -O0), it should realize that these steps are redundant (e.g., loading a 16-bit value into a pair of 8-bit registers, saving 'low'; loading them over again, shifting them over by a whole register width, then saving 'high'..), and optimize it down to sensible code. In particular, for something like AVR, it has to increment the pointer and load memory, which should reduce to a pair of lds reg, [reg+] instructions. Which includes the increment right there, so it's done.This is why it's always a good idea to inspect the assembled output, to make sure you're doing what you think you're doing.Instead of explicitly incrementing the pointer, indexing the array in a loop should reduce to very similar results.Such code is always portable to other platforms, at some cost to memory or efficiency. For example, a 32-bit machine will have an int array with a natural width of 4 bytes, but as long as you're using the same portable logic, you're only writing and reading the lower 16. Memory is wasted, but access is not inefficient, in that it stays word-aligned. (You usually pay a price for misaligned access: x86 memory fetch requires an entire bus cycle whether you're reading a single byte or the full bus; ARM doesn't allow it at all and so the compiler must generate fetch, mask and bitshift instructions. In both cases, the faster code will load a word into a register, and chop it up from there.) Likewise for big-endian systems, the pointer might have to stutter-step (e.g., 6809), costing execution time and code space, but remaining correct.There can be no such thing as truly portable code; you can optimize your code structure to suit some platforms, and you can use #defines to target multiple platforms, but only as many as you have tested (and have driver support for -- in an embedded context, this is the probably the bigger challenge).Tim

author: Siwastaja
author_rank: Super Contributor
timestamp: February 07, 2019, 04:21:51 pm
content: Quote from: ataradov on February 07, 2019, 03:25:25 pmAnd, happens to be easier to write, easier to read and understand, less error prone, more ecologically sustainable, and so on.Indeed - I do the same.

author: newbrain
author_rank: Super Contributor
timestamp: February 07, 2019, 04:27:13 pm
content: Quote from: T3sl4co1l on February 07, 2019, 03:59:10 pmSomething is off with that code:Eitherbig_pis a pointer to anuint8_t, thenhighwill always be 0, or to something larger than anuint8_ttype, and the += is skipping at least one array element.Maybe you meant something as:Code:low = (*big_p++) & 0xff;high = (*big_p++) >> 8;

author: legacy
author_rank: Super Contributor
timestamp: February 07, 2019, 04:58:14 pm
content: OT:for the Power9 Blackbird mainboardwe (at DTB)have recently decided to rewrite the whole firmware in order to force the CPU into BE since the factory default is LE.This seems a silly detail but it caused *a lot* of troubles with the software.So BE vs LE is not a thing that you have to underestimate when you write software.

author: SiliconWizard
author_rank: Super Contributor
timestamp: February 07, 2019, 05:45:12 pm
content: If the underlying send function uses byte transfers (meaning alignment is no issue), my preferred way of doing this is to declare a pointer to void as the function parameter. (void *)Then you can pass it any pointer you want without the need of an explicit cast.

author: Obi_Kwiet
author_rank: Regular Contributor
timestamp: February 07, 2019, 07:00:42 pm
content: Quote from: newbrain on February 07, 2019, 02:27:21 pmYeah, I meant you, my bad! Thanks for the help. I'm happy with this solution, but I'll keep the various pitfalls in mind for the future.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 07, 2019, 07:09:40 pm
content: Quote from: newbrain on February 07, 2019, 04:27:13 pmYes, let me clarify that:Code:int array;uint8_t low, high;int* big_p = array;...low = (*big_p) & 0xff;high = (*big_p) >> 8;big_p += sizeof(array);QuoteThat's not what I meant, because on the next access, that won't align to the next element of "array", assuming a variable width array. The alternative is to simply use array in the expressions, and let the compiler figure it out -- which if your loop is straightforward and optimization is on (I forget which levels will yield this result), will end up using the same load-and-pointer-increment instructions as intended.Tim

author: nctnico
author_rank: Super Contributor
timestamp: February 07, 2019, 07:56:44 pm
content: Quote from: ataradov on February 07, 2019, 04:09:28 amVery bad advice! The pointer to the character array may not be word aligned so the CPU may throw an exception or just read the wrong value (and if not now then it may happen later when someone . The C compiler throws a warning for a good reason! If you want to do this at least use memcpy() to copy the data into the memory. If you are sure endianess is never a problem then this is sufficient.

author: ataradov
author_rank: Super Contributor
timestamp: February 07, 2019, 07:59:00 pm
content: Quote from: nctnico on February 07, 2019, 07:56:44 pmWe are casting down to bytes and use it as a byte array. There are no alignment issues here.

author: nctnico
author_rank: Super Contributor
timestamp: February 07, 2019, 08:43:44 pm
content: Quote from: ataradov on February 07, 2019, 07:59:00 pmYou are right. I misread that part. It is just that you often see people casting arrays or structures over byte arrays and run into trouble.

author: newbrain
author_rank: Super Contributor
timestamp: February 07, 2019, 09:57:01 pm
content: Quote from: T3sl4co1l on February 07, 2019, 07:09:40 pmI still think you meant a different thing, i.e., seeing the declarations:Code:// ...big_p++;Incrementing big_p, a pointer toint, ofsizeof(int), probably 4 in a 32 bit arch, means that 3 elements of the array are skipped, after processing half of the first one...not that it's wrong in itself, but, humor me, it looks very odd in the context.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 08, 2019, 12:23:13 am
content: Right, the pointer math is done for you when you use a non-byte-typed pointer...Call that big_p++ then!Tim