nuttx icon indicating copy to clipboard operation
nuttx copied to clipboard

[BUG] ESP32C6 Devkit SPI Slave initialization error

Open FelipeMdeO opened this issue 1 year ago • 7 comments

Description / Steps to reproduce the issue

Issue present in Master branch.

1 - Connect 2 devices, one as master and other as slave - I followed comments in this PR. 2 - Run simple example just for open spi slave file (cal bind function, it is responsible to initialize spi slave). 3 - Send data from master 2 times, in the second send you will see that the slave answer same data received. Look image below: Master: image Slave: image

First Message: image Second Message: image

@eren-terzioglu and @fdcavalcanti Can you help me please? I am stucked in this point for some days ...

On which OS does this issue occur?

[OS: Linux]

What is the version of your OS?

Ubuntu 24

NuttX Version

NuttX version 12.7.0-RC0 eae57cb0e6-dirty Oct 6 2024 16:01:24 esp32c6-devkitm:spi-dirty

Issue Architecture

[Arch: risc-v]

Issue Area

[Area: Specific Peripheral]

Verification

  • [X] I have verified before submitting the report. Oi

FelipeMdeO avatar Oct 06 '24 20:10 FelipeMdeO

Hi, We will look into that, thanks for reporting

eren-terzioglu avatar Oct 07 '24 10:10 eren-terzioglu

Hi, Could you send the example code from spi slave driver side?

eren-terzioglu avatar Oct 14 '24 13:10 eren-terzioglu

Hello @eren-terzioglu

below you can see an example:

#include <nuttx/config.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

/****************************************************************************
 * Public Functions
 ****************************************************************************/

#define MASTER 0

#ifdef MASTER
#define SOURCE_FILE "/dev/spi2"
#else
#define SOURCE_FILE "/dev/spislv2"
#endif
#define BUFFER_SIZE 256

/****************************************************************************
 * hello_slave_main
 ****************************************************************************/

int main(int argc, FAR char *argv[])
{
  int fd;
  char buffer[BUFFER_SIZE];
  ssize_t bytes_read;

  printf("Slave started!!\n");
  fd = open(SOURCE_FILE, O_RDWR);

  if (fd < 0)
  {
      printf("Failed to open %s: %s\n", SOURCE_FILE, strerror(errno));
      return 0;
  }

  while(1)
  {
      // Read the number from the source file
      printf("Slave: Reading from %s\n", SOURCE_FILE);
      bytes_read = read(fd, buffer, BUFFER_SIZE - 1);
      
      if (bytes_read < 0)
      {
          printf("Failed to read from %s: %s\n", SOURCE_FILE, strerror(errno));
          close(fd);
          return 0;
      }
      else if (bytes_read > 0)
      {
          buffer[bytes_read] = '\0';
          printf("Slave: Read value '%s' from %s\n", buffer, SOURCE_FILE);

          // Write the same value back
          printf("Slave: Writing value '%s' back to %s\n", buffer, SOURCE_FILE);
          if (write(fd, buffer, strlen(buffer)) < 0)
          {
              printf("Failed to write to %s: %s\n", SOURCE_FILE, strerror(errno));
              close(fd);
              return 0;
          }
      }

      sleep(1);
  }

  close(fd);
  return 0;
}

In this example I am always answer empty buffer to master, but you will see that the master will print data. If you have one available, please use a logic analyzer.

FelipeMdeO avatar Oct 14 '24 15:10 FelipeMdeO

Hello @eren-terzioglu

below you can see an example:

#include <nuttx/config.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

/****************************************************************************
 * Public Functions
 ****************************************************************************/

#define MASTER 0

#ifdef MASTER
#define SOURCE_FILE "/dev/spi2"
#else
#define SOURCE_FILE "/dev/spislv2"
#endif
#define BUFFER_SIZE 256

/****************************************************************************
 * hello_slave_main
 ****************************************************************************/

int main(int argc, FAR char *argv[])
{
  int fd;
  char buffer[BUFFER_SIZE];
  ssize_t bytes_read;

  printf("Slave started!!\n");
  fd = open(SOURCE_FILE, O_RDWR);

  if (fd < 0)
  {
      printf("Failed to open %s: %s\n", SOURCE_FILE, strerror(errno));
      return 0;
  }

  while(1)
  {
      // Read the number from the source file
      printf("Slave: Reading from %s\n", SOURCE_FILE);
      bytes_read = read(fd, buffer, BUFFER_SIZE - 1);
      
      if (bytes_read < 0)
      {
          printf("Failed to read from %s: %s\n", SOURCE_FILE, strerror(errno));
          close(fd);
          return 0;
      }
      else if (bytes_read > 0)
      {
          buffer[bytes_read] = '\0';
          printf("Slave: Read value '%s' from %s\n", buffer, SOURCE_FILE);

          // Write the same value back
          printf("Slave: Writing value '%s' back to %s\n", buffer, SOURCE_FILE);
          if (write(fd, buffer, strlen(buffer)) < 0)
          {
              printf("Failed to write to %s: %s\n", SOURCE_FILE, strerror(errno));
              close(fd);
              return 0;
          }
      }

      sleep(1);
  }

  close(fd);
  return 0;
}

In this example I am always answer empty buffer to master, but you will see that the master will print data. If you have one available, please use a logic analyzer.

Thanks, I am checking with a logic analyzer too.

eren-terzioglu avatar Oct 14 '24 15:10 eren-terzioglu

Hi @FelipeMdeO ,

I checked the issue and seems it is some kind of feature. Code flow works like that:

this is the reason why first returned value is all zeros. Because first return value is not data that we recieved, what we have in tx buffer to send which is all zeros in this case

eren-terzioglu avatar Oct 21 '24 11:10 eren-terzioglu

Hi @FelipeMdeO !

Adding to what @eren-terzioglu said: SPI is a full-duplex bus, so it's expected that the first exchange receives all zeros from the slave device (even running the tx/rx copy example). The slave device is just transmitting the content of the its TX buffer. Running the example on master for the second time, slave will transmit the data it received in the first execution. This is the expected behavior.

I will close this issue for now. Please let me know if you find anything else different from the expected behavior.

tmedicci avatar Oct 22 '24 13:10 tmedicci

@tmedicci please talk with @FelipeMdeO he said you closed it without understanding the issue he is facing, please don't close any issue until the user said it was fixed ;-)

acassis avatar Oct 22 '24 14:10 acassis

@tmedicci please talk with @FelipeMdeO he said you closed it without understanding the issue he is facing, please don't close any issue until the user said it was fixed ;-)

Alright, I'm waiting for his response regarding @eren-terzioglu's message and mine. This is the official channel to keep any conversation and we had no response for a while.

tmedicci avatar Oct 22 '24 15:10 tmedicci

Hello @tmedicci, @eren-terzioglu, sorry for the delay.

Using the example available in the master branch, yes, the first transmission will be zeroed, and the second transmission will repeat everything that is being received, which is indeed expected.

However, I would like you to make a change to the available example: Please comment out the write instruction as I did below: (spislv) image

Run the test again, and you will see that the slave will send data even though the application is not requesting it. image

In summary:

The lower half is "contaminating" the TX with what is coming from the RX. Feel free to request more information if needed to reproduce the issue.

FelipeMdeO avatar Oct 22 '24 17:10 FelipeMdeO

@eren-terzioglu and @tmedicci, Did you understand the issue? Are you able to reproduce it?

FelipeMdeO avatar Oct 23 '24 16:10 FelipeMdeO

@eren-terzioglu and @tmedicci, Did you understand the issue? Are you able to reproduce it?

Yes I reproduced and working on it. Thanks for the report

eren-terzioglu avatar Oct 24 '24 07:10 eren-terzioglu

Hi @FelipeMdeO Behavior seems explained in idf documentation. So this is sort of expected behaviour of SPI Slave driver for esp devices. You can get over it with filling TX buffer with data like how it was done in idf example

eren-terzioglu avatar Oct 29 '24 15:10 eren-terzioglu

Hello @xiaoxiang781216 , don't you think it would be better for us to discuss this behavior a little more?

Perhaps the driver should be changed, as it is expected that the same application behaves consistently across different drivers. What I mean is that an application running on an ST [likely referring to STMicroelectronics or a specific ST platform] won't need to handle receiving "garbage" from the lower half due to a specific behavior of Espressif. Therefore, if it becomes necessary to migrate an app running on ST to Espressif, it would be necessary to modify the application.

@eren-terzioglu , looking at the following excerpt from the documentation: "Every clock pulse, a data bit is shifted from the Host to the Device on the MOSI line and back on the MISO line at the same time." I understand that a bit is sent from the host to the device and at the same instant a bit from the device is sent to the host, but not exactly the same bit. In fact, we do not see exactly the same bit being sent; we see the corresponding bit from the previous transaction. But it can be a misunderstand from my side.

I believe @Donny9 could give us his opinion on this. If we come to the understanding that the driver's behavior should not send anything when nothing is requested by the app, we should enforce this in the lower half (which is exactly what I have been doing currently in the solution I am working on).

FelipeMdeO avatar Oct 29 '24 17:10 FelipeMdeO

@eren-terzioglu @xiaoxiang781216 "Every clock pulse, a data bit is shifted from the Host to the Device on the MOSI line and back on the MISO line at the same time. " It doesn't mean that it is the same byte. If it is the same value, I think there is a BUG in the internal SPI controller

acassis avatar Oct 29 '24 18:10 acassis

@FelipeMdeO and @acassis.

MOSI and MISO lines exchange data on every pulse clock. The "feature" @eren-terzioglu told you about can be better described as:

"On every clock pulse, the slave device will receive data on MOSI line and transmit the content of the transmit buffer on MISO. After a byte is transmitted, its content will be overwritten with the byte just received in the receiver buffer". The consequence is: for the 1st transaction, if the transmitter buffer is not written, it would transmit its initial value (all zeros). For the 2nd transaction, if not written, it'd transmit the content received in the 1st transmission (that was copied to the transmit buffer during the 1st transmission).

That being said, we can summarize it as: if not written between transmissions, the slave device will transmit the content received in the previous transmission.

I don't know how other drivers behaves when the transmit buffer is not set between transmissions in the slave device: one could set to a pre-defined value between transmissions on upper-half driver, but the "problem" is about setting it between transmissions (which is done by the app).

tmedicci avatar Oct 30 '24 18:10 tmedicci

Hello @tmedicci, @eren-terzioglu, good evening.

I set up two ESP32-C6 Devkit M, one as a slave and the other as a master, using the example available in the master of the IDF. I made a small change in the slave so that it does not send any data on TX (slave doesn't touch in the buffer), and what I observed is that there is no loopback as you mentioned. In other words, the behavior we have in the IDF is different from what we have in NuttX.

Please take a look at the screenshots below: image image

Could you set up the same configuration and perform the same test I did, please?

FelipeMdeO avatar Oct 30 '24 21:10 FelipeMdeO

Please, I commented t.tx_buffer = sendbuf; also, I have same behavior, no loopback:

image image

FelipeMdeO avatar Oct 30 '24 21:10 FelipeMdeO

Hi @FelipeMdeO , thanks for reporting. I will forward to the team.

But, just to make sure, do you have any problems on slave device setting the the transmit buffer each transmission? (this is the usage case).

Can you test it, please?

tmedicci avatar Oct 31 '24 12:10 tmedicci

Hello @tmedicci

In the file esp_spi_slave.c, there is the following function: static void spislave_prepare_next_tx(struct spislave_priv_s *priv) { if (priv->tx_length != 0) { ... } else { ... } } Currently (in my fork), in the else condition, I am "setting" the TX buffer to 0x00. The behavior is correct this way. So, to answer your question: no, I do not encounter any issues if I set up the buffer for each transmission.

In my experience, I haven’t come across any MCU manufacturers that have loopback behavior in SPI slave mode. Therefore, I believe we need to align this behavior to ensure that applications developed for different devices behave consistently. This adjustment needs to be made either through a workaround like mine in the lower-half or by replicating the IDF behavior in our 3rd-party setup.

[EDIT] @tmedicci , About the usage case... Our behavior is different than IDF, In IDF, slave prepare 10 bytes to sent in next transmission (an example), when master assert CS, 10 bytes will be sent and master will receive 10 bytes and next bytes will be zero. In our case, master will receive 10 bytes and "trash" from last transaction ( if it has more than 10 bytes).

FelipeMdeO avatar Oct 31 '24 13:10 FelipeMdeO

Hello @tmedicci

In the file esp_spi_slave.c, there is the following function: static void spislave_prepare_next_tx(struct spislave_priv_s *priv) { if (priv->tx_length != 0) { ... } else { ... } } Currently (in my fork), in the else condition, I am "setting" the TX buffer to 0x00. The behavior is correct this way. So, to answer your question: no, I do not encounter any issues if I set up the buffer for each transmission.

In my experience, I haven’t come across any MCU manufacturers that have loopback behavior in SPI slave mode. Therefore, I believe we need to align this behavior to ensure that applications developed for different devices behave consistently. This adjustment needs to be made either through a workaround like mine in the lower-half or by replicating the IDF behavior in our 3rd-party setup.

[EDIT] @tmedicci , About the usage case... Our behavior is different than IDF, In IDF, slave prepare 10 bytes to sent in next transmission (an example), when master assert CS, 10 bytes will be sent and master will receive 10 bytes and next bytes will be zero. In our case, master will receive 10 bytes and "trash" from last transaction ( if it has more than 10 bytes).

@FelipeMdeO , I still couldn't get it: what is the behavior of the slave device if you call write on the SPI slave peripheral? (without any workarounds in the lower-half).

For instance, this example writes back the read content. If you modify it to write any other data, does the tx_buffer behave as expected?

tmedicci avatar Oct 31 '24 17:10 tmedicci

Hello @tmedicci , I still couldn't get it: what is the behavior of the slave device if you call write on the SPI slave peripheral? (without any workarounds in the lower-half). When I call "write", slave will send nbytes requested in write command and fill other bytes with trash (consider master sending 10 bytes and nbytes = 5 ).

For instance, example writes back the read content. If you modify it to write any other data, does the tx_buffer behave as expected?

The result only is the expected if slave always write exactly number of bytes than master. With master write 10 bytes and slave write 5, master will receive 5 "true" bytes and 5 "echoed" bytes.

FelipeMdeO avatar Oct 31 '24 17:10 FelipeMdeO

Hello @tmedicci , I still couldn't get it: what is the behavior of the slave device if you call write on the SPI slave peripheral? (without any workarounds in the lower-half). When I call "write", slave will send nbytes requested in write command and fill other bytes with trash (consider master sending 10 bytes and nbytes = 5 ).

For instance, example writes back the read content. If you modify it to write any other data, does the tx_buffer behave as expected?

The result only is the expected if slave always write exactly number of bytes than master. With master write 10 bytes and slave write 5, master will receive 5 "true" bytes and 5 "echoed" bytes.

Yes, this is the expected behavior. Check section 28.5.5 of https://www.espressif.com/sites/default/files/documentation/esp32-c6_technical_reference_manual_en.pdf

GP-SPI2 provides 16 x 32-bit data buffers, i.e., SPI_W0_REG ~ SPI_W15_REG, as shown in Figure 28-2. CPU-controlled transfer indicates the transfer in which the data to send is from GP-SPI2 data buffer and the received data is stored to GP-SPI2 data buffer. In such transfer, every single transaction needs to be triggered by the CPU after its related registers are configured.

This is the case if CONFIG_ESPRESSIF_SPI2_DMA isn't enabled (and it isn't by default). For master device, the data from this buffer is sent to MOSI and received data on MISO overwrites this same buffer. For slave devices, it's the opposite: if you just write 5 bytes to the transmit buffer and master sends 10 bytes, the first 5 written bytes in the transmit buffer will be sent and the next 5 bytes represents any content in that buffer. That content is just the received content of the last transmission.

tmedicci avatar Oct 31 '24 19:10 tmedicci

Hello @tmedicci,

Now the behavior make sense. We are working with only one buffer to tx and rx. If we hope different behavior we need handle it by software. Thank you for clarification and patience.

FelipeMdeO avatar Oct 31 '24 20:10 FelipeMdeO