Problems about using SPI

Hi,

I am struggling making one hifive1 be the SPI master (MHF below), and the other one be the slave (SHF below).

After connecting both of them from pin 10 to 13 (SS, MOSI, MISO, SCK), I tried this SPI example. In the MHF part, no modification is required. The linking is automatically done by arduino IDE and the hifive1 package. In the SHF part, however, the scenario is very different from arduino family boards.

In practice, a SPI slave often works in a interrupt-driven model. This cause two difficulties when applying the example code to the SHF:

  • SPI registers
    Arduino uses things like SPDR (data reg.) and SPCR (control reg.). I wonder if there was similar design in hifive1, so I dig into the related files but found no trivial mappings for this. Even if I try to replace SPDR by SPI_REG_TXFIFO/SPI_REG_RXFIFO, I have no idea how to reassemble SPCR and SPSR from all the SPI_REG_XXXs.
  • ISR design
    The author use ISR (SPI_STC_vect) to declare the interrupt handler, and they don’t even need to do attachInterrupt() in setup(). But the ISR here is a AVR-specific macro. I did try to design a ISR for SHF, but the problem became: what interrupt number to attach? INT_SPI1_BASE? but it did not seem to work either.

Therefore for SHF, I currently have no idea how to utilize the hardware support part, so I implemented software-emulated SPI slave. (But of course I want to know the native way to do SPI on hifive1 later :sweat_smile: )

There are two ISRs,

  • on the CHANGING of SS
    After the FALLING of SS, set an ENABLE flag, and disable it once RISING is observed.
  • on the RISING of SCK
    The SPI.begin() shows that hifive1 boards do SPI under mode 0. So when SCK rises and SS is low (ENABLE flag is set), one bit of the data should be transmitted in MOSI. In theory I can read the bit and do the shift-and-accumulate on SHF.

I don’t know if I understood anything wrong, but surprisingly at least to me, this emulation did not work either. I always imagine the SPI communication process as the upper part of figure 76 in this website, so I expected when the MHF send a byte of data, there should be 8 rises on SCK pin. However, the ISR on SCK does not respond like that; rather, it activate only once as MHF do a SPI.transfer().

So I don’t know where to go from here now. :cry:

Quick summary: I am working on using hifive1 as an SPI slave. I try to find an easy mapping to work for an arduino example code but in vain. I then try to do it in a software-emulated way, but nothing works out either.

Sorry for the messy description above. If any of you need more information, please just ask me. Any help would be appreciated. Thanks!

The SPI controller on the HiFive1 FE310 supports master mode only. Chapter 13 of the Freedom E300 Platform Reference Manual describes the SPI registers.

Could you post your code for emulating the slave functionality?

Thanks for the information!

I’ve read the manual and tried some modification but still not worked.

With the master code:

#include <SPI.h>
#include "encoding.h"

void setup (void)
{
  Serial.begin (115200);
  Serial.println ("[MASTER0]");

  digitalWrite(SS, HIGH);
  digitalWrite(SCK, LOW);
  
  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SS, OUTPUT);
  pinMode(SCK, OUTPUT);
  SPI.begin ();

  // Just try to add more delay here, but seems no effect
  SPI_REG(SPI_REG_SCKDIV) = 1023;
  SPI_REG(SPI_REG_DSCKCS) = 255;
  SPI_REG(SPI_REG_DCSSCK) = 255;
  SPI_REG(SPI_REG_DINTERCS) = 255;
  SPI_REG(SPI_REG_DINTERXFR) = 255;

}  // end of setup

byte transferAndWait (const byte what)
{
  byte a = SPI.transfer (what);
  delayMicroseconds (20);
  return a;
} // end of transferAndWait

void loop (void)
{
  static int index = 0;
  digitalWrite(SS, LOW);
  delay (250);

  //send 1, 2, 4, 8, ... 128, 1, ... sequence, 
  //one number roughly per second
  byte ret = transferAndWait ((byte)(1 << index));  

  delay (250);
  digitalWrite(SS, HIGH);

  Serial.print ("Send: ");
  Serial.print (1<< index);
  Serial.print (", Recv: ");
  Serial.println (ret);

  index++;
  index %= 8;

  delay (500);  // totally 1 second delay
}  // end of loop

and the slave:

#include <SPI.h>
#include "encoding.h"

int pinmask[2];
bool state;
void setup (void)
{
  Serial.begin (115200);
  Serial.println ("[SLAVE6]");

  SPI.begin();
  //some tweaks here:  
  //  SS and SCK become input pins, and
  //  turn of the CSMODE setting
  pinMode(SS, INPUT);
  pinMode(SCK, INPUT);
  SPI_REG(SPI_REG_CSMODE) = SPI_CSMODE_OFF;

  SPI_REG(SPI_REG_SCKDIV) = 1023;
  SPI_REG(SPI_REG_DSCKCS) = 255;
  SPI_REG(SPI_REG_DCSSCK) = 255;
  SPI_REG(SPI_REG_DINTERCS) = 255;
  SPI_REG(SPI_REG_DINTERXFR) = 255;

  pinmask[0] = digitalPinToBitMask(SCK);
  attachInterrupt(digitalPinToInterrupt(SCK), ISR0, RISING);
  pinmask[1] = digitalPinToBitMask(SS);
  attachInterrupt(digitalPinToInterrupt(SS), ISR1, CHANGE);

  set_csr(mstatus, MSTATUS_MIE);
}  // end of setup

// SPI interrupt routine
bool newData = false;
bool enable = false;

volatile byte x;
void ISR0 (void)
{
  if (enable == false)
    return;
  newData = true;
  x = SPI.transfer (x);
  SPI_REG(SPI_REG_CSMODE) = SPI_CSMODE_OFF;

  GPIO_REG(GPIO_RISE_IP) = pinmask[0];
}
void ISR1 (void)
{
  enable = (enable == true) ? false : true;
  GPIO_REG(GPIO_RISE_IP) = pinmask[1];
  GPIO_REG(GPIO_FALL_IP) = pinmask[1];
}

void loop (void)
{
  clear_csr(mstatus, MSTATUS_MIE);
  if (newData == true ) {
    Serial.print(x);
    Serial.println(".  ");
    newData = false;
  }
  set_csr(mstatus, MSTATUS_MIE);
}

where the slave’s MISO connects to master’s MOSI and vice versa.

The output of master is something like:

...
Send: 1, Recv: 0
Send: 2, Recv: 0
Send: 4, Recv: 255
Send: 8, Recv: 0
Send: 16, Recv: 0
Send: 32, Recv: 0
Send: 64, Recv: 0
Send: 128, Recv: 0
Send: 1, Recv: 0
Send: 2, Recv: 0
Send: 4, Recv: 255
Send: 8, Recv: 0
Send: 16, Recv: 0
Send: 32, Recv: 0
Send: 64, Recv: 0
Send: 128, Recv: 0
Send: 1, Recv: 0
Send: 2, Recv: 0
Send: 4, Recv: 255
...

and the output of slave be:

...
0.
0.
0.
0.
255.
0.
0.
0.
0.
0.
0.
0.
255.
0.
0.
0.
0.
0.
0.
...

I guess the result indicates that after the master sends 0x0000 0001, the slave cannot read the result synchronously so just seeing the last bit 8 times, which is 255.

Any comments would be appreciated.

Regarding the slave code:

  • Don’t call SPI.begin(). SPI.begin() enables the hardware I/O function (IOF) for the SPI pins, meaning that the pins are directly controlled by the SPI1 peripheral and not by the software GPIO registers.
  • Replace SPI.transfer() with a bit-banged implementation. The SPI library cannot be used here since it only supports master functionality, so you would need to detect the SCK transitions and perform the sampling/shifting manually.
  • Remove all SPI_REG() assignments. These don’t do anything since the SPI1 hardware is not involved.

Regarding the master code:

  • The digitalWrite(SS, ...) calls are not needed. The SPI controller automatically asserts/deasserts the chip select before/after each SPI transaction.
  • DSCKCS, DCSSCK, DINTERCS, and DINTERXFR are 16-bit registers, while the SPI_REG macro assumes 32-bit memory-mapped registers. Interacting with them as individual fields requires some slightly different macros:
#define _REG16(p, i) (*(volatile uint16_t *) ((p) + (i)))
#define SPI_REG16(offset) _REG16(SPI1_BASE_ADDR, offset)

SPI_REG16(SPI_REG_DSCKCS) = 255;
SPI_REG16(SPI_REG_DCSSCK) = 255;
SPI_REG16(SPI_REG_DINTERCS) = 255;
SPI_REG16(SPI_REG_DINTERXFR) = 255;

Thanks for the quick response and tips!

The slave is re-writed to the following:

#include <SPI.h>
#include "encoding.h"

int pinmask[2];
bool isHigh = false;
byte newData;
void setup (void)
{
  Serial.begin (115200);
  Serial.println ("[SLAVE6]");

  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SS, INPUT);
  pinMode(SCK, INPUT);
  isHigh = (digitalRead(SCK) == HIGH) ? true : false;

  pinmask[0] = digitalPinToBitMask(SCK);
  attachInterrupt(digitalPinToInterrupt(SCK), ISR0, CHANGE);
  pinmask[1] = digitalPinToBitMask(SS);
  attachInterrupt(digitalPinToInterrupt(SS), ISR1, CHANGE);

  set_csr(mstatus, MSTATUS_MIE);
  newData = 0;
}  // end of setup

// SPI interrupt routine


bool dorw = false;
bool enable = false;

byte rxfifo;
byte txfifo = '0';
volatile byte x;
void ISR0 (void)
{
  if (enable == false)
    return;
  if (isHigh) {
    GPIO_REG(GPIO_RISE_IP) = pinmask[0];
    dorw = true;
  }
  else {
    GPIO_REG(GPIO_FALL_IP) = pinmask[0];
    rxfifo = (rxfifo << 1) | (x == HIGH);
    txfifo <<= 1;
  }
  isHigh = (isHigh == true) ? false : true;
}

void ISR1 (void)
{
  enable = (enable == true) ? false : true;
  GPIO_REG(GPIO_RISE_IP) = pinmask[1];
  GPIO_REG(GPIO_FALL_IP) = pinmask[1];
}

void loop (void)
{
  if (dorw) {
    clear_csr(mstatus, MSTATUS_MIE);
    x = digitalRead(MISO);
    digitalWrite(MOSI, txfifo & 0x80);
    dorw = false;
    newData++;
    set_csr(mstatus, MSTATUS_MIE);
  }

  if (newData < 8)
    return ;

  txfifo = rxfifo;
  Serial.print(rxfifo);
  Serial.println(".  ");
  newData = 0;
}

but now (with the same master) the output is like:

...
0.
1.
130.
4.
8.
16.
32.
64.
0.
1.
130.
4.
8.
16.
32.
64.
0.
1.
130.
4.
...

The 0 and 130 are somehow weird. Any ideas?

130 is 8'b1000_0010, which might indicate that the MSB is being sampled too early when MOSI still represents the LSB of the previous transfer. The SPI controller continues to drive MOSI with its last value even after SS is deasserted. This would also explain 8'b0100_0000 being followed by 8'b0000_0000 when it should be 8'b1000_0000. The other cases are unaffected since LSB = MSB = 0, so the MSB would be the correct value by coincidence.

This doesn’t quite explain why the intermediate bits seem to be unaffected. However, it is straightforward to test this hypothesis by transmitting different values from the master, not just powers of 2.

It is not clear to me exactly how the code would manifest such behavior, but I noticed that there are a couple potential race conditions. For example, enable being initialized to false assumes that the first execution of ISR1 is after a falling edge of SS, which would not be the case if the interrupt is enabled while a transfer is ongoing. The delays on the master side make this less likely, but it is still a possibility.

Also, suppose that the master is a quiescent state (SCK is low) when setup() is run, so isHigh = false. When ISR0 is first run, it would be on a rising edge of SCK, but the else clause would be executed first instead. The code just happens to work since the handler fails to clear the GPIO_RISE_IP flag, so the processor takes another interrupt immediately and executes the if clause as expected the second time.

To ensure that the slave is properly synchronized with the master, the ISRs should check whether or not it was a rising or falling edge that triggered the interrupt, either by examining GPIO_RISE_IP / GPIO_FALL_IP or the SS / SCK pins directly. You could also try moving the if (dorw) { ... } logic from loop() into ISR0 to reduce latency. (If you do so, the clear_csr / set_csr become unnecessary since interrupts are already disabled within the handlers.)

I change both the master and the slave according to your suggestion. The master now sends from 48(‘0’) to 122(‘z’). And the slave becomes:

#include <SPI.h>
#include "encoding.h"

int pinmask[2];
byte newData;
void setup (void)
{
  Serial.begin (115200);
  Serial.println ("[SLAVE6]");

  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SS, INPUT);
  pinMode(SCK, INPUT);

  pinmask[0] = digitalPinToBitMask(SCK);
  attachInterrupt(digitalPinToInterrupt(SCK), ISR0, CHANGE);
  pinmask[1] = digitalPinToBitMask(SS);
  attachInterrupt(digitalPinToInterrupt(SS), ISR1, FALLING);

  set_csr(mstatus, MSTATUS_MIE);
  newData = 0;
}  // end of setup

byte data[2]; // 0 for rx, 1 for tx;
bool boot = false;
volatile byte rxfifo;
volatile byte txfifo = '0';
volatile byte x;
void ISR0 (void)
{
  if (GPIO_REG(GPIO_RISE_IP) & pinmask[0]) {
    GPIO_REG(GPIO_RISE_IP) = pinmask[0];
    digitalWrite(MOSI, txfifo & 0x80);
    x = digitalRead(MISO);

  }
  else if (GPIO_REG(GPIO_FALL_IP) & pinmask[0]) {
    GPIO_REG(GPIO_FALL_IP) = pinmask[0];
    rxfifo = (rxfifo << 1) | (x == HIGH);
    txfifo <<= 1;
    if (boot == true) {
      newData++;
      if (newData >= 8) {
        txfifo = rxfifo;
        data[0] = rxfifo;
        data[1] = txfifo;
        rxfifo = 0;
        newData = 0;
      }
    }
  }
}

void ISR1 (void)
{
  GPIO_REG(GPIO_FALL_IP) = pinmask[1];
  boot = true;
}

void loop (void)
{
  if (boot != true)
    return;
  if (newData != 0)
    return;  
  Serial.print ("Recv: ");
  Serial.println (data[0]);
}

Various output patterns are observed. For instance, from the master’s point of view, it maybe something like (it is a real case!):

...
Send: 119, Recv: 187
Send: 120, Recv: 59
Send: 121, Recv: 188
Send: 122, Recv: 60
Send: 48, Recv: 189
Send: 49, Recv: 24
Send: 50, Recv: 24
Send: 51, Recv: 153
Send: 52, Recv: 25
Send: 53, Recv: 154
Send: 54, Recv: 26
Send: 55, Recv: 155
Send: 56, Recv: 27
Send: 57, Recv: 156
Send: 58, Recv: 28
Send: 59, Recv: 157
...

It is worth noting that, the values master receives may not be the values the slave sends.

However, consider three different sequences,

  • The sequence that the master sends: 48, 49, 50, …
  • The sequence that the slave receives/sends
  • The sequence that the master receives

they are actually identical bit strings. I think they have different interpretation of the whole bit string because of timing skew of value sampling. Is there any way to have more delay or larger period of the SCK signal?

BTW, once I remove the SS control commands from the master code, the SS becomes no signal at all. So I keep it for now.

Good news! I got the software-emulated SPI slave work on my hifive1 board.:tada:

The key modification is the logic inside the ISR handling SCK. The problem is on the slave side. It is just too late to let the master sample the right bit, if the slave write the bit right after it notice the rising of SCK. So I move tx-related codes to falling edge part to ensure that when the next rising edge comes, the right bit should have already been there.

The slave code now becomes:

#include <SPI.h>
#include "encoding.h"

int pinmask[2];
byte newData;
volatile byte txfifo = 128;
void setup (void)
{
  Serial.begin (115200);
  Serial.println ("[SLAVE6]");

  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SS, INPUT);
  pinMode(SCK, INPUT);

  pinmask[0] = digitalPinToBitMask(SCK);
  attachInterrupt(digitalPinToInterrupt(SCK), ISR0, CHANGE);
  pinmask[1] = digitalPinToBitMask(SS);
  attachInterrupt(digitalPinToInterrupt(SS), ISR1, FALLING);

  set_csr(mstatus, MSTATUS_MIE);
  newData = 0;
  digitalWrite(MOSI, txfifo & 0x80);
}  // end of setup

byte data[2]; // 0 for rx, 1 for tx;
bool boot = false;
volatile byte rxfifo;
volatile byte x;
void ISR0 (void)
{
  if (GPIO_REG(GPIO_RISE_IP) & pinmask[0]) {
    GPIO_REG(GPIO_RISE_IP) = pinmask[0];
    x = digitalRead(MISO);
    rxfifo = (rxfifo << 1) | (x == HIGH);
  }
  else if (GPIO_REG(GPIO_FALL_IP) & pinmask[0]) {
    txfifo <<= 1;
    digitalWrite(MOSI, txfifo & 0x80);
    if (boot == true) {
      newData++;
      if (newData >= 8) {
        txfifo = rxfifo;
        digitalWrite(MOSI, txfifo & 0x80);
        data[0] = rxfifo;
        data[1] = txfifo;
        rxfifo = 0;
        newData = 0;
      }
    }
    GPIO_REG(GPIO_FALL_IP) = pinmask[0];
  }
}

void ISR1 (void)
{
  GPIO_REG(GPIO_FALL_IP) = pinmask[1];
  boot = true;
}

void loop (void)
{
  if (boot != true)
    return;
  if (newData != 0)
    return;

  Serial.print ("Recv: ");
  Serial.println (data[0]);
}

But there are some glitches sometimes, for example:

...
Send: 55, Recv: 54
Send: 56, Recv: 119 
Send: 57, Recv: 24
Send: 58, Recv: 57
Send: 59, Recv: 58
Send: 60, Recv: 59
Send: 61, Recv: 60
Send: 62, Recv: 61
Send: 63, Recv: 126 
Send: 64, Recv: 31
Send: 65, Recv: 64
Send: 66, Recv: 65
Send: 67, Recv: 66
Send: 68, Recv: 67
Send: 69, Recv: 68
Send: 70, Recv: 69
Send: 71, Recv: 70
Send: 72, Recv: 7
Send: 73, Recv: 72
...

This implementation does not seem to be stable enough for serious usage, but is by far the best one. As a little exercise for understanding SPI, I think this is an interesting experience. My next step should be playing with some real SPI modules, can’t wait!

Anyway, thanks for all the kind helps!

You’re welcome! Glad to be of help.

It always seems to be single-bit corruption, so at least the slave is shifting the correct number of times. Perhaps there is some noise on MOSI, if the glitches are happening on the slave side? I wonder if implementing a low-pass filter would help, i.e., sampling three times and taking the majority.

You could also try increasing SPI_REG_SCKDIV, but a clock divisor of 2048 is already rather generous.

1 Like