Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PIO SPI slave example #115

Open
jakecrowley opened this issue May 19, 2021 · 11 comments
Open

Add PIO SPI slave example #115

jakecrowley opened this issue May 19, 2021 · 11 comments
Assignees

Comments

@jakecrowley
Copy link

I currently have an Arduino Mega in SPI master mode, and a RPi Pico in slave mode, communicating over SPI. I was using the mega to troubleshoot some issues I was having.

My pinout is:

Arduino MOSI -> Pico GP16 (SPI0 RX)
Arduino SS   -> Pico GP17 (SPI0 CSn)
Arduino GND  -> Pico GND 
Arduino SCLK -> Pico GP18 (SPI0 SCK)

Here is the Pico's code:

#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/spi.h"

int main() {
    stdio_init_all();

    printf("Initializing SPI...\n");

    // SPI initialisation. This example will use SPI at 1MHz.
    spi_init(spi_default, 1000*1000);

    spi_set_slave(spi_default, true);
    gpio_set_function(PICO_DEFAULT_SPI_RX_PIN, GPIO_FUNC_SPI);
    gpio_set_function(PICO_DEFAULT_SPI_SCK_PIN, GPIO_FUNC_SPI);
    gpio_set_function(PICO_DEFAULT_SPI_TX_PIN, GPIO_FUNC_SPI);
    gpio_set_function(PICO_DEFAULT_SPI_CSN_PIN, GPIO_FUNC_SPI);

    int bytesread = 0;
    uint8_t buffer[14];

    while(true)
    {
        bytesread = spi_read_blocking(spi_default, 0, buffer, 14);
        for(int i = 0; i < bytesread; i++)
        {
            printf("%02x ", buffer[i]);
        }
        printf("\n");
    }
}

and here is the Arduino's code:

#include <SPI.h>                            

void setup (void)
{
  SPI.begin();                            //Begins the SPI commnuication
  SPI.setClockDivider(SPI_CLOCK_DIV8);    //Sets clock for SPI communication at 1 MHz
  pinMode(SS, OUTPUT);
  digitalWrite(SS, HIGH);                 
}

//Hello, World! in hex
byte buf[14] = {0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x00};

void loop(void)
{
  digitalWrite(SS, LOW);
  for(int i = 0; i < 14; i++){
    SPI.transfer(buf[i]);
  }
  digitalWrite(SS, HIGH);
  delay(100);
}

This code sends the byte array buffer over SPI, setting CS to LOW, completing the transfer, then setting CS back to HIGH, as this is how the SPI protocol is supposed to function. However, in doing so, the Pico only receives the first byte of each transfer, waiting until it recieves 14 bytes and then displaying all the first byte.

Pico serial output:
image

After doing some fiddling around with the code, I realized that if I change the loop to toggle CS on each byte sent, the Pico successfully receives all of the data.

void loop(void)
{
  for(int i = 0; i < 14; i++){
    digitalWrite(SS, LOW);
    SPI.transfer(buf[i]);
    digitalWrite(SS, HIGH);
  }
  delay(100);
}

New Pico serial output:
image

However, this is not how the SPI protocol should operate and makes the Pico incompatible with pretty much every SPI master device, and seems to be an issue in the pico's hardware_spi library, unless this is user error and if that is the case then I will gladly be corrected.

@lurch
Copy link
Contributor

lurch commented May 19, 2021

See raspberrypi/pico-sdk#88 (comment) ?

@jakecrowley
Copy link
Author

See raspberrypi/pico-sdk#88 (comment) ?

Yeah, I have looked at that. The problem is that solution only works for master mode, and I need this functionality for slave mode. As far as I can tell the only fix would be to reimplement SPI as a PIO state machine, however I'm pretty new to this stuff so I could be wrong.

@lurch
Copy link
Contributor

lurch commented May 19, 2021

I'm not very familiar with SPI code (and so there may be a better workaround than what I'm suggesting), but would changing your spi_read_blocking call to only read a single byte at a time be a viable workaround?
...or maybe using just a single SPI.transfer(buf, 14) on the Arduino side would also work? https://www.arduino.cc/en/Reference/SPITransfer 🤷

@jakecrowley
Copy link
Author

I'm not very familiar with SPI code (and so there may be a better workaround than what I'm suggesting), but would changing your spi_read_blocking call to only read a single byte at a time be a viable workaround?
...or maybe using just a single SPI.transfer(buf, 14) on the Arduino side would also work? https://www.arduino.cc/en/Reference/SPITransfer 🤷

I have tried something similar to what you are suggesting. I looked into how the spi.c file and saw that the spi_read_blocking function uses spi_get_hw(spi)->dr to pull the byte from the SPI bus directly, so I just ran that through a while loop but unfortunately got the same result. Looking at the clock line through my logic analyzer each pulse is only around 250 nanoseconds which is probably way too fast to try something like that.
Like I said, I am new to the pico and state machine stuff but maybe there would be a way to just reimplement the protocol and pull the bits from the data line on the rising edge of the clock signal while the SS pin is low?
Also, changing the way the code on the Arduino side works wouldn't help since I was only using that for troubleshooting, I would like to be able to interface with existing hardware in the end.

@jakecrowley
Copy link
Author

It actually seems like something similar to what I was thinking of is actually already implemented in the clocked_input.pio example. I'll give it a try and see if this works as a solution to my issue.

@jakecrowley
Copy link
Author

Good news, the clocked input pio example actually works as-is, as it completely ignores the CS line all together. However, I would like to request an actual SPI implementation rather than just reading in data on a clock signal.

@lurch
Copy link
Contributor

lurch commented May 19, 2021

See also #104

@Wren6991
Copy link
Contributor

Wren6991 commented May 20, 2021

This code sends the byte array buffer over SPI, setting CS to LOW, completing the transfer, then setting CS back to HIGH, as this is how the SPI protocol is supposed to function. However, in doing so, the Pico only receives the first byte of each transfer, waiting until it recieves 14 bytes and then displaying all the first byte.

This is how one variant of SPI functions. Unfortunately the PL022 doesn't support this variant in slave mode.

To do this with PIO, you could try something like the following (just a sketch, not promising there are no typos):

.program clocked_input_chip_select

; Sample bits using an external clock, and push groups of bits into the RX FIFO.
; - IN pin 0 is the data pin
; - IN pin 1 is the clock pin
; - JMP pin is the chip select
; - Autopush is enabled, threshold 8
;
; This program waits for chip select to be asserted (low) before it begins
; clocking in data. Whilst chip select is low, data is clocked continuously. If
; chip select is deasserted part way through a data byte, the partial data is
; discarded. This makes use of the fact a mov to isr clears the input shift
; counter.

flush:
    mov isr, null         ; Clear ISR and input shift counter
    jmp check_chip_select ; Poll chip select again

.wrap_target
do_bit:
    wait 0 pin 1          ; Detect rising edge and sample input data
    wait 1 pin 1          ; (autopush takes care of moving each complete
    in pins, 1            ; data byte to the FIFO)
check_chip_select:
    jmp pin, flush        ; Bail out if we see chip select high
.wrap

This uses the PINCTRL_JMP_PIN config to select the GPIO used for chip select, so this can be any GPIO, independent of the other pin mappings.

@Wren6991 Wren6991 changed the title Pico in SPI slave mode does not read data correctly Add PIO SPI slave example Jun 1, 2021
@Wren6991 Wren6991 transferred this issue from raspberrypi/pico-sdk Jun 1, 2021
@MikeLoh
Copy link

MikeLoh commented Feb 9, 2022

Hello, I know issue was indicated closed - but I was curious and did a deeper dive into the reason for SPI issue receiving only first byte in slave mode. The reason can be found on page 535 of current RP2040 Datasheet.

However, in the case of continuous back-to-back transmissions, the SSPFSSOUT signal must be pulsed HIGH between each data word transfer. This is because the slave select pin freezes the data in its serial peripheral register and does not permit it to be altered if the SPH bit is logic zero. Therefore, the master device must raise the SSPFSSIN pin of the slave device between each data transfer to enable the serial peripheral data write. On completion of the continuous transfer, the SSPFSSOUT pin is returned to its idle state one SSPCLKOUT period after the last bit has been captured.

Solution for now would be to use interrupt on chip select (which was suggested before) to enable and disable internal SPI accordingly. The RP2040 datasheet details how this is done - not positive of support in Pico SDK. Plan to look further in coming week.

Wonder though, is there a PIO state machine that can do same with internal SPI hardware making the chip select operate more like we are all expecting where chip select is held? I still have lots to learn about PIO.

@suicidaleggroll
Copy link

suicidaleggroll commented Feb 25, 2022

Another comment for this closed thread, MikeLoh is absolutely right that this is mentioned in the datasheet, but one quick note on that, the RP2040 behavior is different depending on the SPI phase. His quote above is for SPH=0, while below is the datasheet comment for SPH=1.

In the case of a single word transfer, after all bits have been transferred, the SSPFSSOUT line is returned to its idle HIGH
state one SSPCLKOUT period after the last bit has been captured. For continuous back-to-back transfers, the
SSPFSSOUT pin is held LOW between successive data words and termination is the same as that of the single word
transfer.

I've tested this myself, and indeed if you operate the SPI with CPHA=1 (CPOL doesn't matter for this), the SPI slave will operate correctly in the "standard" way and will receive all bytes with CS staying low for the duration of the transfer. It's very odd to me that changing the SPI phase would have this effect, but it might be a usable workaround for some people.

@MikeLoh
Copy link

MikeLoh commented Feb 25, 2022

@suicidaleggroll I had discovered this same workaround and also have successfully tested. Fortunately it will work with our implementation to have SPH=0, giving us the operation we expect.

Sorry, had not gotten around to posting this update to here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants