USB ISR + SPI

Hi all,
My setup: 10x 64x32 LCD screens which communicate via SPI with a Leonardo. I have my Leo set up as a joystick with a custom output report and I've made the necessary modifications to HID.cpp et. al. to call back into my code when receiving that report from my host-side driver. Receiving a USB report happens within an ISR; I'm aware that I should therefore avoid using anything that takes a particularly long time to execute. I have to do initial setup of the screens but to keep the report size small for incremental updates when running it just consists of just the id of the screen, the id of the content to display and some screen background info (4 bytes in total.)

My question: The initial setup loop on the host could happen quickly enough that I end up receiving another USB ISR before I've had a chance to loop around in my main loop and completely process a screen. So storing the info in volatile globals seems like a bad idea because they might be overwritten with new data before (or during!) processing it. What's the best approach to dealing with this? Should I implement my own queue Leo-side? Should I just delay a little in the setup loop in the host side driver?

Thanks very much!

Should I just delay a little in the setup loop in the host side driver?

Yes. Two weeks seems like a good amount.

Meanwhile, you might consider reading the sticky threads at the top of the forum that you were supposed to read BEFORE posting here. I'm sure something will occur to you by the time you are done.

Seriously? I'm aware that you probably see your share of stupid questions on a daily basis but being a cheeky arse really changes very little and only serves to drive people away. I've posted here before and have read the stickies. It was either post in programming questions or in project guidance and I opted to post here because it was a general programming question that others doing USB communications work with peripherals might have as well. I am attempting to ask a question to prevent more nefarious errors later on. I don't think anything I've done here is unreasonable.

Some people come here to get help, some to give help, and a few to throw rocks.

I'm not too familiar with USB but it sounds like you need some kind of flow control so that the PC does not send work to the Arduino faster than it can be performed.

I don't think anything I've done here is unreasonable.

Except for the requirement that, if you want to post here, you need to post your code, no, you haven't.

If the overload is simply that you might start to receive a following message before you have finished processing the current one then one possible approach is to use swinging buffers between the interrupt that receives messages and the main context that handles them, so that the interrupt can continue to receive messages while the main context is busy. However, there is very limited memory available in the Arduino and you can't simply rely on the Arduino being able to buffer all received USB traffic until it gets round to handling it. If the USB host is liable to send messages faster than the Arduino can handle them then you need to decide at an architectural level what flow control is needed and how you are going to achieve that. That could be just rate-limiting the sender, or requiring a positive handshake, or any other scheme that you prefer.

I understand that is generally a good idea but in this case I wasn't convinced that it was necessary; a wall of code can turn people off to answering; no one likes reading a novel in a forum post. I wasn't aware it was considered an absolute requirement; that said, I'm happy to provide it.

HID.cpp:

bool WEAK HID_Setup(Setup& setup)
{
    //...
    if (REQUEST_HOSTTODEVICE_CLASS_INTERFACE == requestType)
    {
        //...
        if (HID_SET_REPORT == r)
        {
            uint8_t *data = new uint8_t[setup.wLength];
            if (setup.wLength == USB_RecvControl(data, setup.wLength))
            {
                Joystick.recvState(data);//TODO - refactor; global callback might be better
            }
            delete data;
            return true;
        }
    }
    return false;
}

void Joystick_::sendState(JoyState_t *joySt)
{
    //HID_SendReport(Report number, array of values in same order as HID descriptor, length)
    HID_SendReport(1, joySt->getHIDData(), JoyState::BYTES);
    // The joystick is specified as using report 1 in the descriptor. That's where the "1" comes from
}

void Joystick_::recvState(const uint8_t *data)
{
    if (recvStateCallback)
    {
        (*recvStateCallback)(data);
    }
}

Dsky.h + Dsky.cpp:

    MCP23017 _buttonDetector;
    MCP23017 _slaveSelector;
    uint8_t _currentSlaveSelectPin;

void Dsky::begin(void)
{
    _slaveSelector.begin(B001);
    for(uint8_t i = 0; i < BUTTON_COUNT; i++)//BUTTON_COUNT == 10
    {
        _slaveSelector.pinMode(i, OUTPUT);
    }
    
    _slaveSelector.writeGPIOAB(65535);//All HIGH; Slave Select starts with LOW

    _buttonDetector.begin(B010);
    _buttonDetector.config(LOW, HIGH, LOW, LOW, LOW, LOW, HIGH);
    _buttonDetector.enablePinInterruptOnChange(0);

    readState();//Clear any latent interrupts
}

void Dsky::reset(void)
{
    for (uint8_t button = 0; button < BUTTON_COUNT; button++)
    {
        beginCommand(button);
        SPI.transfer(Command_Reset);
        SPI.transfer(CommandData_Reset);
        endCommand();
    }
}

uint16_t Dsky::readState(void)
{
    return _buttonDetector.readIntCapAB();
}

void Dsky::setDisplay(uint8_t button, const uint8_t data[])
{
    beginCommand(button);
    SPI.transfer(Command_SetDisplayData);
    for (int i = 0; i < 256; i++)
    {
        SPI.transfer(data[i]);
    }
    endCommand();
}

void Dsky::setBrightness(uint8_t button, Brightness brightness)
{
    beginCommand(button);
    SPI.transfer(Command_SetBrightness);
    SPI.transfer(brightness);
    endCommand();
}

void Dsky::setColor(uint8_t button, Color red, Color green, Color blue)
{
    byte colorData = B00000011;
    colorData |= (red << 6);
    colorData |= (green << 4);
    colorData |= (blue << 2);

    beginCommand(button);
    SPI.transfer(Command_SetColor);
    SPI.transfer(colorData);
    endCommand();
}

void Dsky::beginCommand(uint8_t button)
{
    _slaveSelector.digitalWrite(_currentSlaveSelectPin = button, LOW);
}

void Dsky::endCommand(void)
{
    _slaveSelector.digitalWrite(_currentSlaveSelectPin, HIGH);
}

My ino:

JoyState_t joyState;
Dsky dsky;

//INT0 and INT1 are SDA and SCL - can't use them
#define DSKY_INTERRUPT INT2

volatile byte btnId = 0xFF;
volatile byte brightness = 0xFF;
volatile byte colorR = 0xFF;
volatile byte colorG = 0xFF;
volatile byte colorB = 0xFF;
volatile byte contentId = 0xFF;

void setup()
{
    SPI.begin();
    SPI.setBitOrder(MSBFIRST);
    SPI.setDataMode(SPI_MODE2);
    SPI.setClockDivider(SPI_CLOCK_DIV4);

    dsky.begin();

    attachInterrupt(DSKY_INTERRUPT, onDskyButtonChanged, FALLING);

    Joystick.recvStateCallback = &onJoystickReceiveState;
}

void loop()
{
    delay(10); //Allow programmer to break in

    //As per: http://www.microchip.com/forums/m659620.aspx and http://www.raspberrypi.org/forums/viewtopic.php?t=58185&p=457459
    //The MCP23017 will not actually clear INTFx until the state that caused the interrupt resets so basically you're forced to read INTCAPx or GPIOx every time so the interrupt clears and can be re-triggered
    dskyButtonState = dsky.readState();

    if (dskyButtonChanged)
    {
        delay(25); //debounce
        joyState.dskyButtons = dskyButtonState;
        dskyButtonChanged = false;
    }

    if(buttonId != 0xFF)
    {
        dsky.setBrightness(btnId, (Dsky::Brightness)convertBrightness(brightness));
        dsky.setColor(btnId, (Dsky::Color)convertColor(colorR), (Dsky::Color)convertColor(colorG), (Dsky::Color)convertColor(colorB));
        dsky.setDisplay(btnId, convertContentId(contentId));
        btnId = 0xFF;
        brightness = 0xFF;
        colorR = 0xFF;
        colorG = 0xFF;
        colorB = 0xFF;
        contentId = 0xFF;
    }

    Joystick.sendState(&joyState);// Refactor - only send on joystate changed
}

void onDskyButtonChanged(void)
{
    dskyButtonChanged = true;
}

void onJoystickReceiveState(const uint8_t *data)
{
    uint8_t reportId = data[0];
    switch (reportId)
    {
    //...other reports omitted
    case 4://Dsky Button Brightness and Color, 2 bytes
    {
        btnId = data[1];
        brightness = data[2] >> 4;
        colorR = data[2] & 0x0F;
        colorG = data[3] >> 4;
        colorB = data[3] & 0x0F;
        contentId = data[4];
    }
        break;
    }
}

int convertBrightness(byte rawBrightness)
{
    switch (rawBrightness)
    {
    case 1:
        return Dsky::Brightness_OneTwentieth;
    case 2:
        return Dsky::Brightness_OneTenth;
    case 3:
        return Dsky::Brightness_OneSeventh;
    case 4:
        return Dsky::Brightness_OneFifth;
    case 5:
        return Dsky::Brightness_OneThird;
    case 6:
        return Dsky::Brightness_OneHalf;
    case 7:
        return Dsky::Brightness_Full;
    }
}

int convertColor(byte rawColor)
{
    switch (rawColor)
    {
    case 1:
        return Dsky::Color_Off;
    case 2:
        return Dsky::Color_Quarter;
    case 4:
        return Dsky::Color_Half;
    case 8:
        return Dsky::Color_Full;
    }
}

const uint8_t* convertContentId(byte contentId)
{
    switch(contentId)
    {
        case 0:
            return imageData_Stage;//Returns a 256 byte array of screen data
        //...
    }
}

Thanks Peter; I had not considered handshake. I'll try using 2 reports, one input and one output as a handshake.