Nano in SPI slave mode not able to write SPDR during SPI ISR routine

Hi,

I'm attempting to modify an existing project that uses an arduino to emulate the buttons on a racing wheel controller (Thrustmaster T150) so that the wheel can be replaced with my own but still communicate with the stock wheel base.

The Arduino sketch simulates three 8 bit shift registers which are used on the wheel to read the button states. The base station then polls 8 bytes from the wheel at a rate of 100 Hz, only using the first three bytes for data. The first byte is static and identifies the wheel. The next two bytes are populated with the button states.

The sketch sets up the arduino as an SPI slave. It loads the first static byte into SPDR making it ready to be sent accross the SPI connection. Once that byte is sent, the SPI interrupt is then used load up the next byte to be sent.

The problem I'm having is that in the SPI interrupt routine, I attempt to write the SPDR but this seems to have no effect. It always loads up all ones (11111111) in every byte after the first. This is confirmed by connecting a second arduino as an SPI master and logging the results via serial connection. The first, static byte is always correctly received but everything after that is always all ones.

I have confirmed that the interrupt routine is being called by logging via serial though this throws off the timing of the SPI so it's difficult to debug in this way.

I've modified the slave sketch below to not try and read any button states but simply send, hard-coded bytes from an array. As you can see in the serial output from the master, the first byte is always correct but the second (and subsequent) bytes are always 11111111.

Any insight would be appreciated.

Slave code:

volatile byte wheelState [8];
volatile byte pos;

void setup (void) {
  DDRB  |= B00001011; // digital pins 8,9,11 used as inputs with a pull-up to +VCC
  PORTB |= B00001011;
  
  DDRC |= B00111111; // pins 14-19 (A0 - A5) also used as digital inputs
  PORTC |= B00111111; // pulled-up to +VCC via internal 100k resistors
  
  DDRD  |= B11111011; // digital pins 0,1,3,4,5,6,7 used as inputs
  PORTD |= B11111011; // pulled-up to +VCC via internal 100k resistors
  
  wheelState[0] = B11010001; // TX RW Ferrari 458 Italia Wheel first data byte
  wheelState[1] = B10000001; // second data byte - buttons
  wheelState[2] = B11111111; // third data byte - buttons
  wheelState[3] = B11111111; // this and below - not used, but wheelbase reads all 8 bytes...
  wheelState[4] = B11111111;
  wheelState[5] = B11111111;
  wheelState[6] = B11111111;
  wheelState[7] = B11111111;

  //Serial.begin(9600);    // Arduino debug console - occupies pins RX (0) and TX (1) on Uno
  pinMode(MISO, OUTPUT); // arduino is a slave device
  SPCR |= _BV(SPE);      // Enables the SPI when 1
  SPCR |= _BV(SPIE);     // Enables the SPI interrupt when 1
  
  // interrupt for SS rising edge. Arduino Uno Pin10 must be connected to Pin2!!!
  attachInterrupt (0, ss_rising, RISING);
}

// Interrupt0 (external, pin 2) - prepare to start the transfer
void ss_rising () {
  SPDR = wheelState[0]; // load first byte into SPI data register
  pos = 1;
}

// SPI interrupt routine
ISR (SPI_STC_vect) {
  byte c = SPDR;
  c = SPCR;
  SPDR = wheelState[pos++]; // load the next byte to SPI output register and return.
}

void loop() {
  // scan the button presses and save that to wheelState array. Data transfer to wheelbase is interrupt-driven above.
  //wheelState[1] = ((PINB & B00000010) << 1) | (PIND & B11111011); // take bit 1 from PORTB + the rest from PORTD B11111x11
  //wheelState[2] = ((PINB & B00001000) << 3) | (PINC & B00111111) | B10000000; // take bit 3 from PORTB + bits 0-5 from PORTC
}

Master Code:

/* This sketch provided "AS IS" under the BSD New license.
http://opensource.org/licenses/BSD-3-Clause
April 2015 © blog@rr-m.org

download tx_rw_wheel_reader.ino above - it has more comments */
#include <SPI.h>
#include <Arduino.h>

const int slaveSelectPin = 7;

void setup() {
 Serial.begin(9600);
 SPCR |= _BV(CPHA);
 SPCR |= _BV(CPOL);
 //SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));
 SPI.begin();
 pinMode(slaveSelectPin, OUTPUT);
}

void loop() {
 // tell the wheel, that we gonna read the data now
 digitalWrite(slaveSelectPin, LOW);

 // read 3 bytes and output them to Arduino Serial monitor
 // as binaries 11000001 11111111 11111111
 // last 17 bits are buttons. 1 - released, 0 - pressed.
 for(int i=1; i<=8; i++) {
   delayMicroseconds(1000);
   printBinary(SPI.transfer(0x00));
 }
 Serial.println();

 // release the wheel
 digitalWrite(slaveSelectPin, HIGH);

 // wait 1 second, then read data from wheel again
 delay(1000);
}

// print byte as binary, zero padded if needed
// "127" -> "01111111"
void printBinary(byte data) {
 for(int i=7; i>0; i--) {
   if (data >> i == 0) {
     Serial.print("0");
   } else {
     break;
   }
 }
 Serial.print(data,BIN);
 Serial.print(" ");
}

Serial output from master:

11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 
11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 
11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 
11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 
11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 
11010001 11111111 11111111 11111111 11111111 11111111 11111111 11111111

It' interesting that even that code works as far as you see in the output. The SPI hardware has a fixed SS pin (10 on the Nano) if operated in slave mode. You cannot use pin 2 for that but you may connect it additionally to use that interrupt. As you left pin 10 floating (a guess of mine because you didn't specify) the MISO pin might be active or inactive depending on the state of some pins around it.

Sorry, I should have specified, the SS line is tied to both pint 10 and 2 on the nano.

It's strange because assigning SPDR in the normal interrupt works just fine but not when done in the SPI interrupt.

It's strange because assigning SPDR in the normal interrupt works just fine but not when done in the SPI interrupt.

It's made to be set in the SPI interrupt. Read the SPSR to check if the WCOL bit is set.

I'll try that tonight. I'll slow down the polling from the master and dump the contents of that register on the slave using serial monitor.

I'll slow down the polling from the master and dump the contents of that register on the slave using serial monitor.

No, never call a method of the Serial object inside an interrupt handler. As the serial interface depends on interrupts to function correctly and interrupts are disabled during interrupt handlers you might end up in a dead lock.

Check the bit in the handler and set some flag to be handled in the loop() if the bit was set.

OK, so when I read SPSR in my SPI interrupt the WCOL flag is definitely set to 1. Not sure what is causing it. I have slowed the polling speed on my master to twice a second (2 Hz) and still see the same behavior. As per the datasheet by reading SPSR and then SPDR I can manually clear the WCOL bit.
However, doing this before setting SPDR in the SPI interrupt has no effect on the data received on the master side.

What's also strange is that when I check SPSR in the SPI interrupt, the SPIF bit is set to 0. Shouldn't this be set to 1 indicating the write is finished since I'm in the SPI interrupt and that should be triggered off of SPIF transitioning to high?

No matter what I do, setting SPDR in the interrupt is not taking effect. It always sends to the master, whatever it shifts into the slave from the MOSI pin. I have confirmed that by tying MOSI on the slave to high or low. When tied to high, the master gets the first correct byte and then all the rest are 11111111. When tied to low the master gets the first correct byte and the rest are 00000000;

I've been googling and looking at the datasheet but I can't find anything besides the WCOL that would prevent writing to the SPDR. Pretty stumped right now.

Disregard what I wrote previously about WCOL being set. I mixed up the WCOL and SPIF bits. So WCOL is in fact NOT set and SPIF IS set. Exactly as I would expect to see....

This still explains nothing however.... Why is the write to SPDR during the SPI interrupt not working?

In setup, the DDR registers are the complement of what they should be. 0 bit means input; 1 bit is output. See the data sheet chapter I/O Ports, section Switching Between Input and Output.
You don't need to declare the data direction for MOSI, SCK and SS on the slave. SPI hardware takes care of that when you enable it. You do need to declare for MISO, but the pinMode() takes care of that.

I don't know if you can assume any initial state of SPCR. I would prefer:

SPCR = _BV(SPE) | _BV(SPIE);

The interrupt is called ss_rising(), but don't you mean to detect when the SS line goes active low–falling?

If the master sends too many bytes, SPDR = wheelState[pos++]; will crash. How about:

if ( pos < 8 ) SPDR = wheelState[pos++];

@DannySwarzman
All valid criticisms of the code (which is not mine but code I found online for a similar steering wheel which others have reported as working)

However, I've removed all of the potential issues you've mentioned and currently my slave code looks like this:

void setup (void) {
  pinMode(MISO, OUTPUT); // arduino is a slave device
  pinMode(MOSI, INPUT_PULLUP);
  SPCR = (1<<SPIE) | (1<<SPE) | (0<<DORD) | (0<<MSTR) | (0<<CPOL) | (0<<CPHA) | (0<<SPR1) | (0<<SPR0);
  attachInterrupt (0, ss_rising, RISING);
}

// Interrupt0 (external, pin 2) - prepare to start the transfer
void ss_rising () {
  SPDR = B10101010; // load first byte into SPI data register
}

// SPI interrupt routine
ISR (SPI_STC_vect) {
  byte c = SPSR;
  c = SPDR;
  SPDR = B00001111; // load the next byte to SPI output register and return.
}

void loop() {
}

This should produce the following output on the master:
10101010 00001111 00001111 00001111 00001111 00001111 00001111 00001111

However, I'm still only getting the first byte correctly and the rest are all 0 or 1 depending on wether the MOSI pin on the slave is tied to 0 or 1. The assigment of SPDR in the SPI ISR simply isn't working. The point where I read SPSR in that routine aslo indicates that WCOL is not set so there does not seem to be any collisions...

As for the interrupt being on the rising edge, it is intended to trigger at that point as it preloads the static first byte which never changes before the master begins the cycle of reading the next 8 bytes. It's currently the only SPDR assignment which is working.

However, I'm still only getting the first byte correctly and the rest are all 0 or 1 depending on wether the MOSI pin on the slave is tied to 0 or 1.

Depending on the MOSI pin? I think it's time for you to post a wiring diagram.

So I just followed CBC_North to post #9. I have exactly the same issue and I noticed the thread just stopped abruptly. In my case I have a 328P that receives fine with an ISR. I was disappointed that all replies stopped shortly after I thought he posed the problem accurately.

For all intents, my ISR works like this:

rx_byte = SPDR;
Do a short something.....
SPDR = tx_byte;

The symptoms are just like CBC_North reports. The SPDR register acts like the tx_byte transfer never happened and therefore shifts out the previous contents of the SPDR shift register (the rx_byte) on the MISO line.

To avoid unrelated questions:

  1. Its the only ISR in the code.
  2. The problems are easily confirmed with a logic analyzer.
  3. Each paired transfer, read the rx_byte, write the next tx_byte is separated by 25ms.
  4. SPI operating Mode 3

I could probably load the SPDR outside the ISR, but I don't like to fix problems without understanding what is going on. Workarounds without understanding create problems in the future.

Al

Ok, I think I have found my very dumb mistake

My actual line was

SPDR - tx_buf;

instead of

SPDR = tx_buf;

This may be very obvious but I read past it and the font was much smaller.

I wonder if something like this happened to the earlier poster?

Al

Let us follow SSS (Start Small Strategy) Methodology to trouble shoot your project:

1. Make the following connections (Fig-1) between two Arduinos.

spiblk.png
Figure-1:

2. Upload the following sketch in SPI-Master-UNO.

#include<SPI.h>
byte myData[8] = {0x00};
void setup()
{
  Serial.begin(9600);
  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV16);//1 MBits/s
  digitalWrite(SS, LOW);  //Slave is selected
  //--------------------
}

void loop()
{
  for (int i = 0; i < 8; i++)
  {
    myData[i] = SPI.transfer(myData[i]);
    delayMicroseconds(100); //allows Slave to process received byte
    Serial.println(myData[i], HEX);  //shows: 0xAB
  }
  Serial.println("======================");
  delay(1000);  //test interval
}

3. Upload the following sketch in SPI-Slave-NANO.

#include<SPI.h> //must be included to get the meanings of SS, MOSI, MISO, SCK names
byte wheelState[] = {0xD1, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //fied values
int i = 0;

void setup()
{
  Serial.begin(9600);
  SPI.setClockDivider(SPI_CLOCK_DIV16);//1 MHz speed insted of default 4 MHz
  pinMode(SS, INPUT_PULLUP);  // ensure SS stays high for now
  pinMode(MISO, OUTPUT);       //always do
  pinMode(MOSI, INPUT);        //always do
  pinMode(SCK, INPUT);         //always do
  SPCR |= _BV(SPE);     //SPI logic is enabled
  SPCR |= !(_BV(MSTR)); //Arduino is Slave
  SPI.attachInterrupt();   //SPI interrupt logic is enabled
}

void loop()
{
}

ISR(SPI_STC_vect)
{
  SPDR = wheelState[i];
  i++;
  if (i == 8)     //8-byte data are sent
  {
    i = 0;          //array pointer is reset
  }
}

4. Open the Serial Monitor of SPI-Master and check that you are receiving the wheelState values correctly and in correct sequence (Fig-2). Notice that there is 1-byte offset (the last byte has appeared in the first position), and it is due to back-to-back connection of the SPDR Registers of Master and Slave. You have to consider this offset while processing the data.

smjk.png
Figure-2:

5. Now, add your other complexities one-by-one with the above codes and verify the functionality.

6. Any query will be highly appreciated.

spiblk.png

smjk.png