Showing posts with label SPI slave. Show all posts
Showing posts with label SPI slave. Show all posts

Tuesday, July 26, 2022

Itsy bitsy Teensy trouble - with Slave SPI

This is another one of those posts mostly meant for myself.

I've been hitting my head against the wall for four days, trying to get Slave SPI working on a Teensy 4.0, with a Teensy 4.1 as the master.

I started out with the https://github.com/tonton81/SPISlave_T4 library (and https://forum.pjrc.com/threads/66389-SPISlave_T4) , but could not make it work, so I stripped it down to the minimum. After a while (after removing a while-loop from the ISR) I got some response. However, it was not stable and it seemed like it would only receive every second byte.

After a lot more troubleshooting I got it working on every transfer, but only if writing 0 to the TX buffer. Any other values would give different troubles.

In the end, it turned out to most likely be a combination of things - mixing up MOSI and MISO pins as well as wrong impedance settings on the pins. Removing and attaching probes to the pins also made stuff fail.

Anyway, next time I would check the following:

- Make sure CFG1 is set to full duplex (00 or 11) 

- Check crossed vs straight connections between MISO and MOSI, matching CFG1

- Connect/disconnect logic probes on MISO/MOSI (works when bytes go missing)

- Make sure master deasserts PCS between frames, I wrote 16 bits without rising the PCS (or rather, rising for a too short time) between blocks of 8 bits when frame size was 8 bit.

- Make sure master speed is > 1MHz

I have yet to find a completely stable solution and I am worried that connecting multiple slaves to the same master may not work as intended. 

Learned

Anyway, here are some of the things I learned:

- The minimum SPI speed on the Teensy is 1MHz (without doing special stuff)

- To set SPI in slave mode: set bit 1 in CFG1 to 0 and update Clock gating register for the appropriate LPSPI, for example CCM_CCGR1 |= (3UL << 6); (lpspi4_clk_enable)

- When CFG1 bits 25-24 are 00, MOSI on the master connects to MISO on the slave and vice versa.

- When CFG1 bits 25-24 are 11, MOSI on the master connects to MOSI on the slave (and MISO to MISO).

- When communicating between two Teensy 4.x, the pin impedance must be changed on both slave and master. For example: 

IOMUXC_SW_PAD_CTL_PAD_GPIO_B0_01 = IOMUXC_PAD_DSE(3) | IOMUXC_PAD_SPEED(3) | IOMUXC_PAD_PKE;

- Even when changing the impedance, I needed to experiment with connecting/disconnecting the logic probes.

- TX buffer empty flag is set immediately after the last TXFIFO byte is shifted out, at the same time an RX interrupt is triggered (if RXWATER is 0).

- The SPI documentation talks about different size descriptions:

    - Transfer - when sending data - Transfer complete IR is triggered when all data in TXFIFO has been sent (FIFO is empty)

    - Word - Up to 32bits, 0-padded if less than 32 bits received. FIFOs are 16 words long.

    - Frame - Frame complete IR is triggered when PCS (chip select) goes high (deasserts). A Frame may be from 8bits long, length must be divisible by 2 and may be longer than 32bits (span multiple words). If word length is reached, the frame is split across multiple words in the RXFIFO.

- TDF and RDF - Transmit and Receive Data flags are set by the system and reset by reading from/writing to the TX and RXFIFOs, not by clearing interrupt flags.

- The SPI slave example sets IER bit 1 which is Transmit Data Interrupt. It will be triggered when the number of bytes in the TXFIFO is less than TXWATER. With TXWATER 0 this will be triggered at the same time a byte is received. I don't understand why this is chosen instead of for example FCF.

- The SPI slave library, and also the more elaborate example at https://github.com/tonton81/SPI_MSTransfer_T4, uses a lot of looping and waiting inside the ISR, which cannot be a good way of doing things...

- With an 8bit frame and a single byte in the TX buffer, the following are set on RX: Frame completed, Word completed, RX data ready, TX data ready. RXFIFO in FSR is incremented. If another byte is received before a new byte is written to TX, we get a TX underrun.

Symptoms

Here are some of the symptoms I experienced:

- Missing or erroneous slave sends when anything but 0 was written to TDR - i suspect that we tried writing and reading from the same pins and that messed up stuff, though not entirely sure WHAT was going on.

- TX buffer underruns even if I wrote to the output buffer every time a byte was received

- Elements in the TX buffer disappeared (even if length was correct) as verified by logic probes. 

- When writing the same bytes over and over to TX, the same bytes disappeared every time.

- Swapping master and slave made DIFFERENT bytes disappear.

- Missing RX, some of the received bytes never ended up in the RXFIFO

- All receives worked when I only wrote 0 to TX buffer.

- It looked like I needed to receive 16bits and/or only every second byte was received.

- When unplugging either the MISO or MOSI cable, sends started working perfectly

- Removing probes sometimes made stuff work - but only after powercycling the Teensys.



Test code

My slave test code can be found at

https://github.com/xonik/teensy-spislave-test

Master code is

#include <SPI.h>

SPISettings settings(10000000, MSBFIRST, SPI_MODE0);


void setup() {

  IOMUXC_SW_PAD_CTL_PAD_GPIO_B0_01 = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(3) | IOMUXC_PAD_PKE; 

  IOMUXC_SW_PAD_CTL_PAD_GPIO_B0_02 = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(3) | IOMUXC_PAD_PKE; 

  IOMUXC_SW_PAD_CTL_PAD_GPIO_B0_03 = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(3) | IOMUXC_PAD_PKE; 

  IOMUXC_SW_PAD_CTL_PAD_GPIO_B0_00 = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(3) | IOMUXC_PAD_PKE; 


  Serial.begin(1000000);

  pinMode (10, OUTPUT);

  digitalWriteFast(10,HIGH);

  Serial.println("SPI Starting");


  SPI.begin();   

  SPI.beginTransaction(settings);  

}


void loop() {

  digitalWriteFast(1,HIGH);

  digitalWriteFast(1,LOW); 

  delay(500);


  digitalWriteFast(10,LOW);

  Serial.println(SPI.transfer(0xAA));

  digitalWriteFast(10,HIGH);  

}


Friday, January 22, 2021

SPI error handling on the PIC16F

Various stuff happens in the SPI module that indicate errors. Here is a short description of what is going on.

Double buffering

SPI on the PIC16F uses a double buffer consisting of SSPxSR and SSPxBUF. 

SSPxSR is the shift register directly connected to the input, it receives bits when the clock runs. Once 8 bytes have been received, it transfers its content to SSPxBUF, sets the SSPxSTAT.BF (buffer full) bit and raises the SSPxIF interrupt. The BF bit is cleared automatically when reading SSPxBUF.

Data to be transferred is written to SSPxBUF and shifted out either immediately (for master) or the next time the other device runs the clock (for slave).

The following two bits indicate errors in transmission:


SSPxCON1.WCOL

Write Collision. Is set if the user tries to write a new byte to SSPxBUF before the current one has been shifted out/transferred. The data written to SSPxBUF is ignored, and all future writes are also ignored until the WCOL is cleared by the user.


SSPxCON1.SSPOV

Receive overflow. Is set if a new byte is received before the old one is read. Only used when device is an SPI slave. Slave must read SPPxBUF even before transfers to prevent the setting of this bit. On overflow, the current byte in SSPxSR is lost. 

In my DCO I check this when SS is driven high, before copying received data to where the user program expects to find it.


SSPxCON3.BOEN

Buffer overwrite enable. If set to high, data is written to the SSPxBUF even if the current byte has not been read. The datasheet does not say explicitly that it does or does not set the SSPOV bit, but it can safely be ignored. BOEN is used if we want to chain SPI slaves as one long shift register - data written to the slave will automatically be transferred to the SSPxBUF and shifted out once the next byte is received, without being touched by the user code. This way, one can keep the slave select pin low, transfer any number of bytes corresponding to the number of slaves connected, and then raise the slave select pin once all data has been transferred.

This is NOT used for my DCO as it will receive three bytes for each slave before the SS is driven high and thus has its own logic for shifting data internally.


SS pin 

The slave select pin synchronises reception. When in use it will be held high until the master is ready to communicate. The master will lower the pin, then transfer data and finally rise it. Using the SS pin makes sure data does not become out of sync and allows multiple slaves on the same SPI bus.

When the SS pin is driven high, the SPI bit counter is reset to 0, even if less than 8 bits have been received, meaning we will never get out of sync as long as SS is used.


SPI Initialisation

I can't find it right now, but I read that SPI must be initialised only when the clock is in the idle state. It may be that this is just because if not we risk getting incomplete/corrupt data, but it still seems like a good practice.

The way I've achieved this in the DCO is to initialise the SPI when the SS pin goes high the first time. That means that the master can either drive the SS pin low and high again, or it will happen automatically if data is transferred (but the data transferred will be lost) if the slave has restarted for some reason, or starts after the master has sent its first byte. This may not be the solution for everyone but it is sufficient for me. A different option would be to use some other scheme where an additional pin on the slave signals its status as "ready to receive" or similar.

To be able to do this, I had to both set use of SS to true and map a pin as SS, and add an edge trigger interrupt to the same pin. The edge bit should be set to 1 (rising edge).

Another PS for the use of SPI: The TRIS bits for the pins in use must be set manually upon initialisation;

SDI (TRIS set = input)

SDO (TRIS cleared = output)

SS (TRIS set = input)

SCK (TRIS cleared = output for master and TRIS set = input for slave)

Any function not desired may be disabled by setting the opposite data direction/TRIS value.