Hardware SPI setup

Hi,
So I’m trying to get the hardware SPI up and running on my Particle Photon. (frirmware 4.9) And the instructions explains this fairly well. I do lack some good examples.
At some places on the net it says the SPI bus goes max on 10Mhz, the manual says 60 and so on.

I have the following setup:

//Initializes the SPI bus by setting SCK, MOSI, and a user-specified slave-select pin to outputs, MISO to input. SCK and MOSI are pulled low, and slave-select high.
SPI.begin(A2);
SPI.setClockSpeed(60, MHZ);
SPI.setClockDivider(SPI_CLOCK_DIV64);
SPI.setBitOrder(MSBFIRST); // Data is read and written MSb first.
// Data is captured on rising edge of clock (CPHA = 0)
// Base value of the clock is HIGH (CPOL = 1)
SPI.setDataMode(SPI_MODE0);

When measured with my low-end oscilloscope 1V/div 20us/div 86- 11kHz I get this kind ow saw shape instead of a nice square pattern. Any how, it seems to fail, the rest of my setup. Just want to know if my start is correct.

BR
Chris

@bytheway I do see the documentation is lacking a full example of an SPI implementation. If you want to change the clock divider, you don’t need to set the speed, and vice versa. You can alternatively set the SPI.setClockDividerReference(SPI_CLK_ARDUINO); and then set the clock divider to what you might in ARDUINO to get close to a comparable speed. This is handy for porting code over from Arduino.

Basic Example

void setup() {
    SPI.setClockSpeed(4, MHZ);
    SPI.begin();  // defaults to set A2 as an OUTPUT
}

void loop() {
    digitalWrite(A2, LOW);
    SPI.transfer(0x55); //  A3 CLK, A5 MOSI, A4 MISO, A2 SS
    digitalWrite(A2, HIGH);
    delay(500);
}

Your Example

void setup() {
    SPI.setClockDivider(SPI_CLOCK_DIV64); // 60/64 = 937.5kHz
    SPI.setBitOrder(MSBFIRST); // Data is read and written MSB first.
    SPI.setDataMode(SPI_MODE0);
    SPI.begin();  // defaults to set A2 as an OUTPUT
}

void loop() {
    digitalWrite(A2, LOW);
    SPI.transfer(0x55); //  A3 CLK, A5 MOSI, A4 MISO, A2 SS
    digitalWrite(A2, HIGH);
    delay(500);
}

Let us know how it goes :wink:

2 Likes

Thanks for the information!
I will not port, so DividerReference is not needed. On the other hand, if I don’t specify Clock or divider the SPI will run in 60Mhz? I have no clue if slave circuit can handle that. The only thing I know right now, is that my clock reading (oscilloscope) where funky.

The transfer command will not put SS low in the beginning nor high in the end, so this is manually?
I also notice that you have a delay of 500ms after each read/write. Is that good practice?

SPI clock is 60MHz and SPI1 is 30MHz, however the divider is set to DIV2 by default so they will run at 30MHz and 15Mhz respectively. Now thinking about this, it might be a good idea to default these to something slower, around 4MHz.

Correct, this is up to the user to control. Typically when you create a library or routine that has lower level READ and WRITE functions, you would put these into those functions. It is done this way so that you can add delays before and after the SS control, or if you needed to invert the state of SS it would be trivial.

These routines are just some test routines I had in the Build IDE to test SPI write functionality with an oscilloscope. 500ms delay is just there so I can be sure the output it occurring at a consistent time.

###SPI Docs for reference
https://docs.particle.io/reference/firmware/photon/#spi

1 Like

So, here is what’s works for me as test (I played around with Clock speed):

void setup() {
    SPI.begin(); 
    SPI.setClockSpeed(10, MHZ);
    SPI.setBitOrder(MSBFIRST); // Data is read and written MSB first.
    SPI.setDataMode(SPI_MODE0);
}

void loop() {
    Serial.println(registerRead());
    delay(1000);
}
uint8_t registerRead(void)
{
     uint8_t result;
      digitalWrite(A2, LOW); // take the chip select low to select the device
      SPI.transfer(0XD0 | 0x80); // send the device the register you want to read and set the read bit
      result = SPI.transfer(0x00); // send a value of 0 to read the first byte returned
      digitalWrite(A2, HIGH); // take the chip select high to de-select
      return result;
}

But what I don’t get is how the transfer function could be used as in the doc’s: SPI.transfer(tx_buffer, rx_buffer, length, myFunction);

With that in mind I would like the above function to look something like:

uint8_t registerRead(uint8_t value)
{
     uint8_t result;
     uint8_t tx_buffer, rx_buffer;
     tx_buffer = value | 0x80;
 
      digitalWrite(A2, LOW);

      SPI.transfer(tx_buffer, rx_buffer, 1, ?);
     
      digitalWrite(A2, HIGH);
      return rx_buffer;
}

I do not comprehend SPI.transfer(tx_buffer, rx_buffer, length, myFunction).

BR

This overload of SPI.transfer() is meant for transfering a fair number of bytes, and uses DMA for speed. If you only want to transfer single bytes you'd better use result = SPI.transfer(cmdByte);.

The docs do tell about the function

meaning

  SPI.transfer(tx_buffer, rx_buffer, 1, NULL); // synchronous, but as said only useful for multi byte transfer

If you pass a callback function, the call returns immediately, and the callback will be executed when the async DMA transfer is finished.

like this

uint8_t rx[255], tx[255];

void someFn()
{
  ...
  SPI.transfer(tx, rx, 255, callback); // async returns immediately
  Serial.printlnf("Waiting for transfer %d", millis());
  ...
}

void callback(void)
{
  Serial.printlnf("transfer finished %d", millis());
  for(int i=0; i<255; i++)
    Serial.printf("%02x ", rx[i]);
  Serial.println();
}
2 Likes

Thanks for the above answer. Just want to add some code that works for me, enjoy:

/* ========= Application.cpp ============= */
uint8_t _latchPin = A2;
uint8_t _sclkPin = A3;
uint8_t _sdoPin = A4;
uint8_t _sdiPin = A5;

void setup()
{
  //Initializes the SPI bus by setting SCK, MOSI, and a user-specified slave-select pin to outputs, MISO to input. SCK and MOSI are pulled low, and slave-select high.
  SPI.begin();
  SPI.setClockSpeed(10, MHZ);
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE3);
}

void loop()
{
  delay(1000);
  //Serial.println(readTemp());
  Serial.print((float)readTemp()/100);
  Serial.print(" °C");
  Serial.print("\n");
}

// Read an 8 bit value over SPI
uint8_t registerRead8(uint8_t value)
{
  digitalWrite(_latchPin, LOW); // take the chip select low to select the device
  SPI.transfer(value | 0x80); // send the device the register you want to read
  value = SPI.transfer(0x00); // send a value of 0 to read the first byte returned
  digitalWrite(_latchPin, HIGH); // take the chip select high to de-select
  return value;
}

int32_t readTemp(void)
{
  int32_t T;
  int32_t adc_T = ((uint32_t)registerRead8(BME280_TEMPERATURE_MSB_REG);
  T = (adc_T * 5 + 128) >> 8;
  return T;

}

is the above code for master or slave? and is it not important to specify like this SPI2.begin(SPI_MODE_SLAVE, DAC1); to make the hardware slave or master?

Master is default

i assume the above code is for master and it is receiving data. Can you tell me the code for slave, that is sending data?

I tried to communication in electron(master ) and P1(slave) but slave is receiving nothing.

An SPI slave will not be able to send data on its own. It needs the master to provide the clock and can only send when the master wants to receive data from that device (SS/CS pulled LOW).

The sample code for SPI.onSelect() should provide some clues.

In a bidirectional setup, you’d usually see the master issuing a read request and then entering a (read) transmission cycle that provides the clock signal for the slave to send back the rrquested data.

1 Like

Thanks for the response, but that’s exactly what i am trying to do. Electron is master and it has to send data to the slave wifi.

Electron code (master):

/******************* electron  ****************************/

#include "application.h"

static uint8_t rx_buffer[64];
static uint8_t tx_buffer[64];

/*******************************************************************************
 *  Macros
 ******************************************************************************/
SYSTEM_THREAD(ENABLED);

STARTUP(System.enableFeature(FEATURE_RETAINED_MEMORY));
	
void setup(){

	pinMode(DAC1, OUTPUT);
	/** SPI initialize **/
	SPI2.begin(SPI_MODE_MASTER, DAC1);
	
	SPI2.setClockSpeed(32768); //10 khz
	SPI2.setBitOrder(MSBFIRST);  
	SPI2.setDataMode(SPI_MODE0); 
	
	delay(100);
}

void loop(){
	/** Send data over SPI2 **/
	uint8_t value = 0x5f; 
	
	digitalWrite(DAC1, LOW); 
	delay(10);

       SPI2.transfer(value); // send the device the register you want to read and set the read bit
	
	delay(10);
	digitalWrite(DAC1, HIGH);
	
	delay(500);	
}

and the P1 slave ccde is exactly same as the link you have provided.

Am I doing anything wrong in programming?

And how is the wiring?

So the wiring is for Electron MISO: C2, MOSI: C1, SCK: D4, SS: DAC
P1 MISO:A4, MOSI: A5, SCK: A3 and SS: DAC

According to reference api of electron SCK should be C3 but i have done mistakenly on D4, so in the lower HAL layer of particle, in file soi_hal.c , I changed the pin map from D4 to C3. I think that should not be the cause of problem.

this chunk of code is from spi_hal.c where i made the change, i commented in this code below, where the change is done:

const STM32_SPI_Info spiMap[TOTAL_SPI] =
{
        { SPI1, &RCC->APB2ENR, RCC_APB2Periph_SPI1, &RCC->AHB1ENR, RCC_AHB1Periph_DMA2, DMA_Channel_3,
          DMA2_Stream5, DMA2_Stream2, DMA2_Stream5_IRQn, DMA2_Stream2_IRQn, DMA_IT_TCIF5, DMA_IT_TCIF2, SCK, MISO, MOSI, SS, GPIO_AF_SPI1 },
        { SPI3, &RCC->APB1ENR, RCC_APB1Periph_SPI3, &RCC->AHB1ENR, RCC_AHB1Periph_DMA1, DMA_Channel_0,
          DMA1_Stream7, DMA1_Stream2, DMA1_Stream7_IRQn, DMA1_Stream2_IRQn, DMA_IT_TCIF7, DMA_IT_TCIF2, D4, D3, D2, D5, GPIO_AF_SPI3  }
#if PLATFORM_ID == 10 // Electron
        ,{ SPI3, &RCC->APB1ENR, RCC_APB1Periph_SPI3, &RCC->AHB1ENR, RCC_AHB1Periph_DMA1, DMA_Channel_0,
          DMA1_Stream7, DMA1_Stream2, DMA1_Stream7_IRQn, DMA1_Stream2_IRQn, DMA_IT_TCIF7, DMA_IT_TCIF2, D4, C2, C1, C0, GPIO_AF_SPI3  }    // ORIGINALLY WAS C3 BUT CHANGED TO D4
#endif
};

So if this is true ...

... then this might contribute to the issue

due to this statement in the code

    SPI.begin(SPI_MODE_SLAVE, A2);

Have you actually checked the STM32F2 datasheet that this change is even valid?
The hardware inderfaces can't just be remapped any way you like. GPIOs have dedicated alternative functions and these need to be activated and mapped with care.
The pin names are not just interchangable, since they are already abstracted (hence the A in HAL) from the hardware GPIO port and pin wiring.

2 Likes

I am really sorry, from exactly same i meant code functions and in general is same, but yes i did make changes for the slave pin.
this is the P1 code:

/******************* P1  ****************************/
#include "application.h"

SYSTEM_THREAD(ENABLED);
//PRODUCT_ID(HUB_PRODUCT_ID);
STARTUP(System.enableFeature(FEATURE_RETAINED_MEMORY));

char strAvail[50];
// SPI slave example
static uint8_t rx_buffer[64];
static uint8_t tx_buffer[64];
static uint32_t select_state = 0x00;
static uint32_t transfer_state = 0x00;

void onTransferFinished() {
    transfer_state = 1;
}

void onSelect(uint8_t state) {
	Particle.publish("HEY");
    if (state)
        select_state = state;
}

/* executes once at startup */
void setup() {
    Serial.begin(9600);
    for (int i = 0; i < sizeof(tx_buffer); i++)
      tx_buffer[i] = (uint8_t)i;
    SPI.onSelect(onSelect);
    SPI.begin(SPI_MODE_SLAVE, DAC);
	SPI.setClockSpeed(32768); //10 khz
	SPI.setBitOrder(MSBFIRST);  // use default instead
	SPI.setDataMode(SPI_MODE0); 
	delay(100);
}

/* executes continuously after setup() runs */
void loop() {
		Particle.publish("In while 1");
        while(select_state == 0);
        select_state = 0;

        transfer_state = 0;
		Particle.publish("After select state");
        SPI.transfer(tx_buffer, rx_buffer, sizeof(rx_buffer), onTransferFinished);
        while(transfer_state == 0);
		Particle.publish("After transfer state");
        if (SPI.available() > 0) {
			snprintf(strAvail, 16, "bytes recvd: %d" , SPI.available());
			Particle.publish(strAvail);		
        }
}

And sorry no I didnt go through datasheet, but I am going to dig into that right now. thanks

And yes, I checked in datasheet and on this pin diagram as well of electron:


So C3 that is by default SCK for SPI2 of particle, it is AF6 (alternate function 6th) according to STM32F2 data sheet

similarly the pin i am using D4 is AF6 for SPI2 clock, both according to datasheet and pin out

So Atleast it is confirmed D4 can be used instead of C3 for SCK. The only doubt i have is did making that one single change in HAL layer could be enough to activate D4 as SCK, or do I have to make other changes in HAL layer as well.

Please let me know what should I do from here, thanks

We can ping @rickkas7 on that.

@rickkas7 any suggestions pleasee ?

While you could change the HAL pin map to have a weird mix of pins that combine some of the D pins and some on the C pins, it really would be a maintenance nightmare. You’d always have to build your own system firmware versions using the local toolchain and never use the standard release system firmware. That being said, I think all you need to do is change that pin definition in spiMap and it should work.

By changing your board to use only the C pins (and object SPI2) or only the D pins (and object SPI1) is really a better way to solve the problem if possible, however.

1 Like