1

Learning C and having some issues with pointers/arrays. Using MPLAB X and a PIC24FJ128GB204 as target device, but I don't really think it matters for this question. The answer might be obvious, but without much background in C (yet), it is difficult to find a similar question that I understand enough to draw conclusions.

I have written an I2C library with the following function:

int I2C1_Write(char DeviceAddress, unsigned char SubAddress, unsigned char *Payload, char ByteCnt){
    int PayloadByte = 0;
    int ReturnValue = 0;
    char SlaveWriteAddr;

    // send start
    if(I2C1_Start() < 0){
        I2C1_Bus_SetDirty;
        return I2C1_Err_CommunicationFail;
    }

    // address slave
    SlaveWriteAddr = (DeviceAddress << 1) & ~0x1;       // shift left, AND with 0xFE to keep bit0 clear
    ReturnValue = I2C1_WriteSingleByte(SlaveWriteAddr);
    switch (ReturnValue){
        case I2C1_OK:
            break;
        case I2C1_Err_NAK:
            I2C1_Stop();
            I2C1_Bus_SetDirty;
            return I2C1_Err_BadAddress;
        default:
            I2C1_Stop();
            I2C1_Bus_SetDirty;
            return I2C1_Err_CommunicationFail;

    }

    // part deleted for brevity

    // and finally payload
    for(PayloadByte = 0; PayloadByte < ByteCnt; PayloadByte++){
        // send byte one by one
        if(I2C1_WriteSingleByte(Payload[PayloadByte]) != I2C1_ACK){
            I2C1_Stop();
            I2C1_Bus_SetDirty;
            return I2C1_Err_CommunicationFail;            
        }
    }
    return I2C1_OK;
}   

I want to call this function from another one, using a predefined const:

const unsigned char CMD_SingleShot[3] = {2, 0x2C, 0x06};

This has the length of the command as first byte, then the command bytes.

The calling function is:

int SHT31_GetData(unsigned char MeasurementData[]){
    // address device and start measurement
    if(I2C1_Write(SHT31_Address,
                0xFF,
                CMD_SingleShot[1],       // this is where the error message is given
                CMD_SingleShot[0])
                < 1){
        return -1;
    }
    //code omitted for brevity

    return 1;
}

The error message:

../Sensirion_SHT31.c:40:17: warning: passing argument 3 of 'I2C1_Write' makes pointer from integer without a cast

../I2C1.h:66:5: note: expected 'unsigned char *' but argument is of type 'unsigned char'

The problem is clearly (unsigned char)CMD_SingleShot[1] where I try to give a pointer to the second byte of the unsigned char array.

I have tried:

  • reading up on pointers and arrays and trying to understand
  • searching for similar functions
  • given up on understanding and trying random thing hoping the error messages would lead me to the correct way of doing this. Things like:
CMD_SingleShot[1]
&CMD_SingleShot[1]
(unsigned char)CMD_SingleShot + 1

this just gave other error messages.

My questions:

  • given the I2C1_Write function as is (expecting a unsigned char *) (for instance, if that was not my code and I couldn't change that), how would I pass the pointer to the second byte in the cont unsigned char array? My understanding is that an array is a pointer, so
  • since this is my code, is there a better way to do this altogether?
  • 1
    What happened when you tried `&CMD_SingleShot[1]`? That should at least compile. – kaylum Feb 11 '20 at 10:26
  • I can get it to compile, but with lots of errors and I don't yet have the knowledge to judge which ones are important, so try to solve the issues. Error message when using &CMD_SingleShot[1]: Sensirion_SHT31.c:40:17: warning: passing argument 3 of 'I2C1_Write' discards qualifiers from pointer target type I2C1.h:66:5: note: expected 'unsigned char *' but argument is of type 'const unsigned char *' – Dieter Vansteenwegen ON4DD Feb 11 '20 at 10:38
  • That's not an error, it is a warning. Because you declared `CMD_SingleShot` to contain `const unsigned char` values. But the function expects a pointer to `unsigned char` values. Looks like the function doesn't change the `Payload` value so you can declare that parameter as `const unsigned char *Payload`. – kaylum Feb 11 '20 at 10:41
  • `const unsigned char *MeasurementData[]`, `const unsigned char *Payload` - but the library might be broken anyhow, so you can forcibly cast away `const` there: `(unsigned char *)&CMD_SingleShot[1]` – Antti Haapala -- Слава Україні Feb 11 '20 at 10:42
  • @kaylum it is as much an error as the other one. – Antti Haapala -- Слава Україні Feb 11 '20 at 10:42
  • @AnttiHaapala True. I filtered out the fact that the first one was not flagged as an error as it is clearly wrong. Whereas the second case could be ok (but not good practice). – kaylum Feb 11 '20 at 10:47
  • @kaylum they're both constraint violations in C, meaning that the program is an invalid one. – Antti Haapala -- Слава Україні Feb 11 '20 at 10:48
  • @kaylum Forget about "it's not an error it's a warning". Every message your compiler spits out is to be regarded as severe bug until proven otherwise. In 99% of the cases, the compiler is correct and warning equals bug. Furthermore, the C language doesn't have the concept of errors and warnings, that's a compiler invention. C only has diagnostic messages and a compiler is expected to give such a message whenever the code violates the C standard. It may do so with a "warning", "error" or by delivering a personal hand-written letter to your door, long as you are notified. – Lundin Feb 11 '20 at 12:57

3 Answers3

2

First, don't do casting unless you know better than the compiler what is going on. Which, in your case, you don't. This is nothing to be ashamed of.

Doing &CMD_SingleShot[1] is a step in the right direction. The problem is that CMD_SingleShot[1] is of type const unsigned char and therefore taking the address of that gives you a pointer of type const unsigned char *. This cannot be passed to the Payload parameter since it expects a pointer of unsigned char *. Luckily you don't modify whatever Payload points to, so there is no reason for that to be non-const. Just change the definition of Payload to const unsigned char * and the compiler should be happy.

And by the way, in c, &Foo[n] is the same as Foo + n. Whatever you write is a matter of taste.

Edit:

Sometimes you don't have access to the library source code, and because of bad library design you are forced to cast. In that case, it is up to you to get things right. The compiler will happily shoot you in the foot if you ask it to.

In your case, the correct cast would be (unsigned char *)&CMD_SingleShot[1] and NOT (unsigned char *)CMD_SingleShot[1]. The first case interprets a pointer of one type as a pointer of different type. The second case interprets an unsigned character as a pointer, which is very bad.

HAL9000
  • 2,138
  • 1
  • 9
  • 20
  • That makes sense. And indeed, there's no reason not to use a const unsigned char * for the Payload. But say I hadn't access to the code of the I2C library and was stuck with a function expecting an unsigned char *, how would I call it then? (unsigned char *)CMD_SingleShot[1] ? – Dieter Vansteenwegen ON4DD Feb 11 '20 at 10:55
  • And while I will take your word for it, why not cast? In the book I used to learn C, it is taught as good practice (and it also results in far less error messages). – Dieter Vansteenwegen ON4DD Feb 11 '20 at 10:56
  • 1
    @DieterVansteenwegenON4DD Please read again the first paragraph of HAL9000's answer. Even though your book claims differently, let our experience of several decades of C programming guide you: Casts are mostly signs of bad design, and sometimes pure errors. Some books are simply worse than others. – the busybee Feb 11 '20 at 11:12
  • @thebusybee So: in theory casting is not bad as such, but should be avoided? – Dieter Vansteenwegen ON4DD Feb 11 '20 at 15:39
  • @HAL9000 " The second case interprets an unsigned character as a pointer, which is very bad. " Because that would give you a pointer to the memory address that corresponds to the value in memory address `CMD_SingleShot[1]`, correct? – Dieter Vansteenwegen ON4DD Feb 11 '20 at 15:40
  • @DieterVansteenwegenON4DD The c-standard doesn't specify what `(unsigned char *)CMD_SingleShot[1]` means. But on a typical computer, yes, that would very likely be a pointer to memory location `0x2C` and *not* a pointer to the memory location that *holds* the value `0x2C` – HAL9000 Feb 11 '20 at 17:32
  • @DieterVansteenwegenON4DD Well, it's like Aspirin. It is not bad as such, and it helps in certain cases. But these applications tend to hide the real reason of the headache, and it's the same with casts. And if you abuse or over-use it, well, you can imaging the result. You write the source code *not* for the compiler, you write it for *the next developer reading* it. That might be your future self. It happened to me more than once. So make your source code as clean as possible. – the busybee Feb 11 '20 at 17:41
  • @DieterVansteenwegenON4DD, Casts lets you do things that wouldn't be possible otherwise. But they come with a big cost, you may lose error messages and warnings from the compiler. Avoid them as much as possible. – HAL9000 Feb 11 '20 at 17:47
1

Passing the address of the second byte of your command is done with either

&CMD_SingleShot[1]

or

CMD_SingleShot+1

But then you will run into an invalid conversion error since your command is defined as const unsigned char and then &CMD_SingleShot[1] is of type const unsigned char* but your function expects unsigned char*.

What you can do is either change the argument of your function:

int I2C1_Write(char DeviceAddress, unsigned char SubAddress, const unsigned char *Payload, char ByteCnt)

or cast your passing argument:

I2C1_Write(SHT31_Address, 0xFF, (unsigned char*)&CMD_SingleShot[1], CMD_SingleShot[0])

In the latter case be aware that casting away const'ness might result in undefined behaviour when changing it afterwards.

Odysseus
  • 1,213
  • 4
  • 12
  • "In the latter case be aware that casting away const'ness might result in undefined behaviour when changing it afterwards." Because the called function can then change the variable? – Dieter Vansteenwegen ON4DD Feb 11 '20 at 10:58
  • Additionally: "&CMD_SingleShot[1]". Why the & (address of)? As I understood, arrays are pointers, so I would expect CMD_SingleShot[1] to work as well... – Dieter Vansteenwegen ON4DD Feb 11 '20 at 11:03
  • Yes - the I2C_Write function is able to change it because it gets a pointer to a non-const unsigned char although the source data was meant not to change when declaring it const. So I recommend to avoid that – Odysseus Feb 11 '20 at 11:05
  • 1
    @DieterVansteenwegenON4DD `CMD_SingleShot[1]` is *not* a pointer, it is of type of *the element*, that is `const unsigned char`. – the busybee Feb 11 '20 at 11:07
  • `CMD_SingleShot` is an array and therefore passed as a pointer (`const unsigned char*`) but `CMD_SingleShot[1]` is not the array but a single element of it (`const unsigned char`). So you have to explicitly use `&` to get the address of this particular element. The first element is a special case, because the array pointer is the address of the first element – Odysseus Feb 11 '20 at 11:11
  • @Odysseus ok, that makes sense. While learning python, I could do type(variable) to get the type, which I cannot do now making it much more difficult to get a grip on what happens... – Dieter Vansteenwegen ON4DD Feb 11 '20 at 11:15
1

The function call is mostly correct, but since the 3rd parameter of the function is a pointer, you must pass an address to an array accordingly, not a single character. Thus &CMD_SingleShot[1] rather than CMD_SingleShot[1].

if(I2C1_Write(SHT31_Address,
                0xFF,
                &CMD_SingleShot[1], 
                CMD_SingleShot[0])
                < 1)

However when you do this, you claim that you get "discards qualifiers from pointer target" which is a remark about const correctness - apparently CMD_SingleShot is const (since it's a flash variable or some such?).

That compiler error in turn simply means that the function is incorrectly designed - an I2C write function should clearly not modify the data, just send it. So the most correct fix is to change the function to take const unsigned char *Payload. Study const correctness - if a function does not modify data passed to it by a pointer, then that pointer should be declared as const type*, "pointer to read-only data of type".

If it wouldn't be possible to change the function because you are stuck with an API written by someone else, then you'd have to copy the data into a read/write buffer before passing it to the function. "Casting away" const is almost never correct (though most often works in practice, but without guarantees).


Other concerns:

  • When programming C in general, and embedded C in particular, you should use stdint.h instead of the default types of C, that are problematic since they have variable sizes.

  • Never use char (without unsigned) for anything else but actual strings. It has implementation-defined signedness and is generally dangerous - never use it for storing raw data.

  • When programming an 8 bit MCU, always use uint8_t/int8_t when you know in advance that the variable won't hold any larger values than 8 bits. There are plenty of cases where the compiler simply can't optimize 16 bit values down to 8 bit.

  • Never use signed or potentially signed operands to the bitwise operators. Code such as (DeviceAddress << 1) & ~0x1 is wildly dangerous. Not only is DeviceAddress potentially signed and could end up negative, it gets implicitly promoted to int. Similarly, 0x1 is of type int and so on 2's complement PIC ~0x1 actually boils down to -2 which is not what you want.

    Instead try to u suffix all integer constants and study Implicit type promotion rules.

Community
  • 1
  • 1
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • Lots of information there, thank you. 2 questions: 1. So `unsigned char` is ok to use for 8bit raw data? 2. stdint.h would then be dependent on the architecture? 3. I do not completely understand (but don't mean to doubt) your last point: even if `DeviceAddress` would be signed, would it not "simply" be a bunch of 8 bits that get sent to the I2C address register bit for bit? In that case, does it really matter? I Will read up on "signed operands". The last sentence and link are without doubt important, but will take a long while to digest and understand. – Dieter Vansteenwegen ON4DD Feb 11 '20 at 15:37
  • @DieterVansteenwegenON4DD Yes unsigned char is ok, but int is not ok because signed types are almost always dangerous in embedded. These types are dependent on architecture, whereas `uint8_t` is always 8 bits 2's complement no matter if you run the code on a PIC or x86 64. The problem with signed arithmetic is that you can easily get unexpected behavior. Take for example `#define ALL_ONE 0xFF`... `unsigned char whatever=0;` ... `if(~whatever == ALL_ONE)`, which has a fat bug because of integer promotion. – Lundin Feb 11 '20 at 16:34
  • Hm, why does `~0x1` boil down to `-1` on 2's complement? – the busybee Feb 11 '20 at 17:44