Search code examples
c++arduinoglobal-variables

Why does a global buffer variable produce different results than a local buffer variable when using SPI?


Ok, long-time developer but with higher-order languages like C# and Swift. Working in native C++ for a hardware project I'm working on, and using Arduino for prototyping it. However, this has me stumped.

I have a buffer that I'm sending out over SPI that I currently have defined as a local variable, but when I move it out to global scope, I'm getting different results!

This works as expected...

void writeRow(uint8_t row){

    uint8_t buffer[] = {
        B01111111 ^ 0xFF,
        B00111111 ^ 0xFF,
        B00111111 ^ 0xFF,
        B00011111 ^ 0xFF,
        B00001111 ^ 0xFF,
        B00000111 ^ 0xFF,
        B00000011 ^ 0xFF,
        B00000001 ^ 0xFF
    };

    uint16_t rowMask = 1 << row;

    digitalWrite(PIN_LATCH, LOW);

    SPI.transfer16(rowMask); SPI.transfer(buffer,     2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 2, 2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 4, 2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 6, 2);

    digitalWrite(PIN_LATCH, HIGH);
}

But this one does not. Moving the buffer to global level here now sends all zeroes to the SPI bus.

uint8_t buffer[] = {
    B01111111 ^ 0xFF,
    B00111111 ^ 0xFF,
    B00111111 ^ 0xFF,
    B00011111 ^ 0xFF,
    B00001111 ^ 0xFF,
    B00000111 ^ 0xFF,
    B00000011 ^ 0xFF,
    B00000001 ^ 0xFF
};

void writeRow(uint8_t row){

    uint16_t rowMask = 1 << row;

    digitalWrite(PIN_LATCH, LOW);

    SPI.transfer16(rowMask); SPI.transfer(buffer,     2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 2, 2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 4, 2);
    SPI.transfer16(rowMask); SPI.transfer(buffer + 6, 2);

    digitalWrite(PIN_LATCH, HIGH);
}

So what am I missing? That buffer has to be reachable/updateable by several functions, hence wanting to move it out globally.

If this is wrong, what's the correct way to do this?


Update (and op-ed!)

I misunderstood the Arduino API. The culprit was I made the incorrect assumption that this version of 'transfer' worked exactly like all the other versions, just this used a buffer vs sending uint8_ts individually.

However--and this was in the docs so this is technically on me--unlike all the other overloads of the same function which send things out one or two bytes at a time, then returns the incoming data, the version that takes a buffer *overwrites that buffer with the incoming data*, thus it was trashing my global buffer the first time it went out.

I personally think this is an incredibly poor decision on those who designed the SPI library for Arduino because other overloads of the same function do not modify your outgoing data, and nothing in the name indicates that this version would work any differently. To the contrary, it looks like if you already have your data in a buffer, it's a much simpler way to send it out.

Because it has this unexpected side-effect behavior, in my opinion, it should have been named something different to be more clear that it is modifying your input data, something fundamentally different than the other overloads. That or at least adding an extra parameter saying you also wanted the incoming data. In my case, I don't want or need it. It's outbound-only.

Anyway, sorry for the distraction. Hopefully this will still be helpful to the SO community and be a good lesson to always read the docs, even when you think something is obvious.


Solution

  • As discussed in various comments and the question update, this is not an issue with C++, but with a misunderstanding of the SPI API.

    The issue is that SPI.transfer's overload taking a buffer pointer and size as argument does still do bidirectional transfer as other overloads do, but instead of returning transferred data in the way other overloads do, it overwrites the out-going buffer with the incoming data.

    Therefore, if buffer is global, the second and later calls to writeRow will send the previously received data rather than the data from the initialization. With a local buffer this doesn't happen because each call to writeRow initializes a new buffer with the content of the hard-coded initializer.

    See the Arduino documentation for SPI.transfer.