ATTiny2313 SPI slave data receive issues (probably SPI mode)

Hi,

I’m trying to get an ATTiny2313 to run as SPI slave, fed from an Arduino UNO as master. The SPI interrupt on the slave is in fact being triggered whenever data arrives but somehow the data seems to be mangled. I suspect an issue with the SPI modes but I can’t figure it out so it might be something different after all.

The test setup consists of the UNO reading a button input on pin 3, toggling its own LED on pin 2 with each button press and sending ON/OFF commands (byte 0xFF / 0x00, respectively) to the SPI slave which in turn is supposed to toggle its LED in sync.

The example is rather dull but I figured I’d better start with something simple before I send more complex command structures through SPI.

I’ve spend quite a lot of time googling for examples but they mostly deal with Attinys as SPI masters, not slaves.

Code for the master:

#include <SPI.h>

#define PIN_SS_ATTINY2313 10
#define PIN_LED 2
#define PIN_BTN 3

#define CMD_LEDON 0xFF
#define CMD_LEDOFF 0x00

byte bitOrder = MSBFIRST;
int btnState; int lastBtnState; int ledState;

void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_BTN, INPUT);
  
  Serial.begin(9600);
  
  SPI.begin();
  SPI.setDataMode(SPI_MODE0);
  SPI.setBitOrder(bitOrder);
  SPI.setClockDivider(SPI_CLOCK_DIV128); // 128kKz
  
  ledOff();
}

void loop() {
  lastBtnState = btnState;
  btnState = digitalRead(PIN_BTN);
  
  if (lastBtnState == 0 && btnState == 1) {
    if (ledState == LOW) {
      ledOn();
    } else {
      ledOff();
    }
  }
  
  Serial.println(btnState);
}

void ledOn() {
  digitalWrite(PIN_LED, HIGH);
  ledState = HIGH;
  Serial.println("switching LED on");
  digitalWrite(PIN_SS_ATTINY2313, LOW);
  SPI.transfer(CMD_LEDON);
  digitalWrite(PIN_SS_ATTINY2313, HIGH);
}

void ledOff() {
  digitalWrite(PIN_LED, LOW);
  ledState = LOW;
  Serial.println("switching LED off");
  digitalWrite(PIN_SS_ATTINY2313, LOW);
  SPI.transfer(CMD_LEDOFF);
  digitalWrite(PIN_SS_ATTINY2313, HIGH);
}

Code for the slave:

/**
 * Slave part of the SPI-LED-blink experiment
 *
 * To be run on an ATTiny2313.
 *
 * The ATtiny2313 will receive commands via SPI and is supposed to
 * switch its LED on/off accordingly.
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
 
#define CTRL_PORT	DDRB
#define DATA_PORT	PORTB
#define SS_PIN		PB4
#define CLK_PIN		PB7
#define	DI_PIN		PB5
#define DO_PIN		PB6

#define PIN_LED PD5
#define CMD_LEDON 0xFF
#define CMD_LEDOFF 0x00

// Some macros that make the code more readable
#define output_low(port,pin) port &= ~(1<<pin)
#define output_high(port,pin) port |= (1<<pin)
#define set_input(portdir,pin) portdir &= ~(1<<pin) 
#define set_output(portdir,pin) portdir |= (1<<pin)

char cData;
static char res = 1;
byte ledState = 0;

int main(void) {
  init();
  while (1) {}
  return 0;
}

void init() {
  // initialize the direction of PORTD #5 to be an output
  set_output(DDRD, PIN_LED);  
  
  output_high(PORTD, PIN_LED);
  ledState = 1;

  // DO pin is configured for output
  CTRL_PORT |= _BV(DO_PIN);
  // other pins as input
  CTRL_PORT &= ~_BV(DI_PIN) | ~_BV(CLK_PIN) | ~_BV(SS_PIN);
  // set pull ups.
  DATA_PORT |= _BV(DI_PIN) | _BV(CLK_PIN);
  // enable USI overflow interrupt, set three wire mode and set
  //	clock to External, positive edge.
  USICR = _BV(USIOIE) | _BV(USIWM0) | _BV(USICS0) | _BV(USICS1);
  // clear overflow flag
  USISR = _BV(USIOIF);	  
  // interrupt Enable 
  sei();
}

/**
 * USI overflow intterupt - triggered when transfer complete
 */
ISR(USI_OVERFLOW_vect) {
  res = USIDR;
  USISR = _BV(USIOIF);
  
  if (res == CMD_LEDOFF) {
    output_low(PORTD, PIN_LED);
    ledState = 0;
  } else if (res == CMD_LEDON) {
    output_high(PORTD, PIN_LED);
    ledState = 1;
  }
  
  USIDR = ~res;	
}

Any hints would be greatly appreciated.

Oh by the way, does anyone know a page that explains the code syntax for the 2313 properly? By now I kind of figured out things like _BV() and stuff but the lack of understanding of the registers and commands makes debugging much more tedious than it should be. I’ve read mostly through the 2313s datasheet but naturally it contains a reference of commands and registers, but not a longer explanation.

Thanks a ton!

Hi MrOnak

I've not used ATtiny as SPI slave but usually slaves need Slave Select or Chip Select taken low before they recognise the master.

I don't see you doing that in your master code.

Usually you need a digitalWrite low before SPI.transfer and high after the transfer.

Regards

Ray

Thanks for that hint, hackscribble. Code for the master is updated (new version in OP) but the effect is the same, the Attiny doesn't seem to get the data.

Edit:
Well the slave doesn't actually care about the slave-select pin so I believe that is not an issue at the moment but you're right, to do proper SPI the slave-select was missing. thanks again for that.

Ok for further testing I changed the code of the slave such that I added a global bool “cmdAvailable”, set that inside the interrupt function and handle the LED logic in the while(1) loop:

unsigned char cmd;
bool cmdAvailable;

int main(void) {
  init();
  
  while (1) {
    if (cmdAvailable == true) {
      // testing  
      output_high(PORTD, PIN_LED);      
      _delay_ms(1000);
      output_low(PORTD, PIN_LED);
      
      // SPI command handling will go here

      cmdAvailable = false;
    }
  }
  
  return 0;
}

ISR(USI_OVERFLOW_vect) {
  cmdAvailable  = true;
  cmd           = USIDR;
  USISR         = (1<<USIOIF); // clear overflow flag
}

The weird thing is, the check for cmdAvailable == true inside main() never passes, the boolean cmdAvailable is always false.
BUT when I toggle the LED inside the interrupt it does indeed toggle.

Any ideas what’s going on? From outside it seems like the interrupt isn’t called but code inside the interrupt funciton is being executed. It is possible to set global variables from within an interrrupt, right?

Right… turns out I forgot to declare the interrupt-accessed variables as volatile. f…

The code still isn’t real SPI (i.e. the slave ignores the SS pin) but for what its worth, here’s working 1-byte command transmission from master to slave:

master

/**
 * Attempting to blink an LED on an ATTiny2313 acting as SPI slave
 *
 * The LED is toggled by pressing a button. Note the the code turns the LED on/off in a latching fashion
 *
 * For verification purposes the defined LED of the Arduino will toggle in sync with the LED on the Attiny2313;
 */
#include <SPI.h>

#define PIN_SS_ATTINY2313 10
#define PIN_LED 2
#define PIN_BTN 3

#define CMD_LEDON 0b10011001
#define CMD_LEDOFF 0x00

byte bitOrder = MSBFIRST;
int btnState; int lastBtnState; int ledState;

void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_BTN, INPUT);
  pinMode(PIN_SS_ATTINY2313, OUTPUT);
  
  digitalWrite(PIN_SS_ATTINY2313, HIGH);
  
  SPI.begin();
  SPI.setDataMode(SPI_MODE0);
  SPI.setBitOrder(bitOrder);
  SPI.setClockDivider(SPI_CLOCK_DIV128); // 128kKz
  
  ledOff();
}

void loop() {
  lastBtnState = btnState;
  btnState = digitalRead(PIN_BTN);
  
  if (lastBtnState == 0 && btnState == 1) {
    if (ledState == LOW) {
      ledOn();
    } else {
      ledOff();
    }
  }
}

void ledOn() {
  digitalWrite(PIN_LED, HIGH);
  ledState = HIGH;

  digitalWrite(PIN_SS_ATTINY2313, LOW);
  SPI.transfer(CMD_LEDON);
  digitalWrite(PIN_SS_ATTINY2313, HIGH);
}

void ledOff() {
  digitalWrite(PIN_LED, LOW);
  ledState = LOW;

  digitalWrite(PIN_SS_ATTINY2313, LOW);
  SPI.transfer(CMD_LEDOFF);
  digitalWrite(PIN_SS_ATTINY2313, HIGH);
}

slave

/**
 * Slave part of the SPI-LED-blink experiment
 *
 * To be run on an ATTiny2313.
 *
 * The ATtiny2313 will receive commands via SPI and is supposed to
 * switch its LED on/off accordingly.
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
 
#define CTRL_PORT	DDRB
#define DATA_PORT	PORTB
#define SS_PIN		PB4
#define CLK_PIN		PB7
#define	DI_PIN		PB5
#define DO_PIN		PB6

// SPI mode 0 or 1 - modes 2 and 3 are NOT supported
#define SPI_MODE 0

#define PIN_LED PD5
#define CMD_LEDON 0b10011001
#define CMD_LEDOFF 0x00

// Some macros that make the code more readable
#define output_low(port,pin) port &= ~(1<<pin)
#define output_high(port,pin) port |= (1<<pin)
#define set_input(portdir,pin) portdir &= ~(1<<pin) 
#define set_output(portdir,pin) portdir |= (1<<pin)

boolean ledState;
volatile unsigned char cmd;
volatile boolean cmdAvailable;

int main(void) {
  init();
  
  while (1) {
    if (cmdAvailable == true) {
      // SPI command handling
      switch (cmd) {
        case CMD_LEDOFF:
          output_low(PORTD, PIN_LED);
          ledState = false;
          break;
        case CMD_LEDON:
          output_high(PORTD, PIN_LED);
          ledState = true;
          break;
      }

      cmdAvailable = false;
    }
  }
  
  return 0;
}

void init() {
  // initialize the direction of PORTD #5 to be an output
  set_output(DDRD, PIN_LED);  
  
  output_high(PORTD, PIN_LED);
  ledState     = true;
  cmdAvailable = false;

  // DO pin is configured for output
  CTRL_PORT |= _BV(DO_PIN);
  // other pins as input
  CTRL_PORT &= ~_BV(DI_PIN) | ~_BV(CLK_PIN) | ~_BV(SS_PIN);
  // set pull ups
  DATA_PORT |= _BV(DI_PIN) | _BV(CLK_PIN);
  // enable USI overflow interrupt, set three wire mode and set
  //	clock to external, positive edge
  USICR = 0;
  USICR = (1<<USIOIE) | (1<<USIWM0) | (1<<USICS1) | (SPI_MODE<<USICS0);
  
  USISR = (1<<USIOIF); // clear overflow flag
  
  // enable interrupts
  sei();
}

/**
 * USI overflow intterupt - triggered when transfer complete
 */
ISR(USI_OVERFLOW_vect) {
  cmd           = USIDR;
  USISR         = (1<<USIOIF); // clear overflow flag
  cmdAvailable  = true;
}

expect a github somewhere once I’m done.