SPI slave by register configuration

JohnGerwick
Posts: 3
Joined: Sat Aug 10, 2019 12:11 pm

SPI slave by register configuration

Postby JohnGerwick » Mon Aug 12, 2019 8:44 am

Hello ESP32 friends,

I need to read data from a master SPI device with the esp32 as SPI slave.
The specs of the master SPI are:

8 MHz clock frequency
8 bit samples (if it works, then upgrade to 32 bit later)
100 kHz sample rate

1. attempt.
I used the IDF slave SPI receiver example and stripped off everything unnecessary. Code below:

Code: Select all

/* SPI Slave example, receiver MODIFIED
*/
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"

#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "lwip/netdb.h"
#include "lwip/igmp.h"

#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"
#include "soc/rtc_cntl_reg.h"
#include "rom/cache.h"
#include "driver/spi_slave.h"
#include "esp_log.h"
#include "esp_spi_flash.h"



#define GPIO_HANDSHAKE 2
#define GPIO_MOSI 13
#define GPIO_MISO 12
#define GPIO_SCLK 14
#define GPIO_CS 15

//Called after a transaction is queued and ready for pickup by master. We use this to set the handshake line high.
void my_post_setup_cb(spi_slave_transaction_t *trans) {
    WRITE_PERI_REG(GPIO_OUT_W1TS_REG, (1<<GPIO_HANDSHAKE));
}

//Called after transaction is sent/received. We use this to set the handshake line low.
void my_post_trans_cb(spi_slave_transaction_t *trans) {
    WRITE_PERI_REG(GPIO_OUT_W1TC_REG, (1<<GPIO_HANDSHAKE));
}

//Main application
void app_main()
{
    esp_err_t ret;

    //Configuration for the SPI bus
    spi_bus_config_t buscfg={
        .mosi_io_num=GPIO_MOSI,
        .miso_io_num=-1,
        .sclk_io_num=GPIO_SCLK
    };

    //Configuration for the SPI slave interface
    spi_slave_interface_config_t slvcfg={
        .mode=0,
        .spics_io_num=GPIO_CS,
        .queue_size=1,
        .flags=0,
        .post_setup_cb=my_post_setup_cb,
        .post_trans_cb=my_post_trans_cb
    };
	
    //Configuration for the handshake line
    gpio_config_t io_conf={
        .intr_type=GPIO_INTR_DISABLE,
        .mode=GPIO_MODE_OUTPUT,
        .pin_bit_mask=(1<<GPIO_HANDSHAKE)
    };

    //Configure handshake line as output
    gpio_config(&io_conf);
    //Enable pull-ups on SPI lines so we don't detect rogue pulses when no master is connected.
    gpio_set_pull_mode(GPIO_MOSI, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(GPIO_SCLK, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(GPIO_CS, GPIO_PULLUP_ONLY);

    //Initialize SPI slave interface
    ret=spi_slave_initialize(HSPI_HOST, &buscfg, &slvcfg, 1);
    assert(ret==ESP_OK);

    uint8_t recvbuf;
	
    spi_slave_transaction_t t;
    memset(&t, 0, sizeof(t));
	
    //Set up a transaction of 1 byte to send/receive
    t.length=1*8;
    t.tx_buffer=NULL;
    t.rx_buffer=&recvbuf;

	
	
    while(1) {

        spi_slave_transmit(HSPI_HOST, &t, portMAX_DELAY);
		
		//printf("received: %u\n", recvbuf); //debugging
    }

}
Problem: It worked with the limitation that only every 4th to 5th sample was read. I checked this by hooking the handshake pin from esp32 to an oscilloscope. The measure frequency was around 25 kHz, as can be seen here:
ESP32 25 kHz beschriftet.png
ESP32 25 kHz beschriftet.png (138.2 KiB) Viewed 14276 times

When uncommenting the printf line it showed that the read data was correct at least (even though the frequency was further reduced due to additional communication, of course).



2. attempt.
Because there is nothing left to strip from the while(1) loop in order to increased speed, I concluded that the IDF slave SPI driver does not fit my needs. From what I understand of SPI theory, there is actually no need that the esp32 is any longer busy than until chip select turns high again (mode 0).

So I tried to configure the registers to enable SPI slave communication manually:

Code: Select all

/* 
	SPI Test
*/

#include "soc/soc.h"
#include "soc/spi_reg.h"
#include "soc/io_mux_reg.h"
#include "soc/gpio_reg.h"
#include "soc/gpio_periph.h"

#include "esp_event.h"		// für vTaskDelay
#include "driver/periph_ctrl.h" // für periph_module_enable(...)

#include <stdio.h>

	
// SPI_TRANS_INTEN seems undefined, define here
#define SPI_TRANS_INTEN 	0b00000000000000000000001000000000

// debugging;
#define GPIO_HANDSHAKE 2


// Pinout for Olimex ESP32 EVB
// CLK		GPIO 14		EVB ext 15
// CS		GPIO 15		EVB ext 16
// MOSI		GPIO 13		EVB ext 14

	

void app_main()
{
	// activate HSPI (clear reset bit of peripheral, set clk_en bit)
	periph_module_enable(PERIPH_HSPI_MODULE); 
	
    //debugging; Configuration for the handshake line
    gpio_config_t io_conf={.intr_type=GPIO_INTR_DISABLE, .mode=GPIO_MODE_OUTPUT, .pin_bit_mask=(1<<GPIO_HANDSHAKE) };
	//Configure handshake line as output
    gpio_config(&io_conf);
	
	// use HSPI (HSPI = SPI2)
	uint8_t unit = 2;
	
	// Clean SPI registers
    WRITE_PERI_REG(SPI_USER_REG(unit), 0);	
    WRITE_PERI_REG(SPI_USER1_REG(unit), 0);
    WRITE_PERI_REG(SPI_USER2_REG(unit), 0);
    WRITE_PERI_REG(SPI_CTRL_REG(unit), 0);
    WRITE_PERI_REG(SPI_CTRL2_REG(unit), 0);
    WRITE_PERI_REG(SPI_SLAVE_REG(unit), 0);
    WRITE_PERI_REG(SPI_PIN_REG(unit), 0);
    WRITE_PERI_REG(SPI_CLOCK_REG(unit), 0);	
	WRITE_PERI_REG(SPI_W0_REG(unit), 0);				
	
	
	
	// debugging; MISO data
	WRITE_PERI_REG(SPI_W8_REG(unit), 0b0101010101);		
	
	
	
 	// Pin configuration via IO_MUX
	// Select function of IO_MUX pad
	SET_PERI_REG_BITS(IO_MUX_GPIO12_REG, MCU_SEL, 1, MCU_SEL_S);		// GPIO12 -> PadName MTCK -> Function 2: HSPID -> MOSI
	SET_PERI_REG_BITS(IO_MUX_GPIO13_REG, MCU_SEL, 1, MCU_SEL_S);		// GPIO13 -> PadName MTCK -> Function 2: HSPID -> MOSI
	SET_PERI_REG_BITS(IO_MUX_GPIO14_REG, MCU_SEL, 1, MCU_SEL_S);		// GPIO14 -> PadName MTMS -> Function 2: HSPICLK -> CLK
	SET_PERI_REG_BITS(IO_MUX_GPIO15_REG, MCU_SEL, 1, MCU_SEL_S);		// GPIO15 -> PadName MTDO -> Function 2: HSPICS0 -> CS
	
	// Enable input
	SET_PERI_REG_MASK(IO_MUX_GPIO12_REG, FUN_IE);		// GPIO12 -> PadName MTDI -> Function 2: HSPIQ -> MISO
	SET_PERI_REG_MASK(IO_MUX_GPIO13_REG, FUN_IE);		// GPIO13 -> PadName MTCK -> Function 2: HSPID -> MOSI
	SET_PERI_REG_MASK(IO_MUX_GPIO14_REG, FUN_IE);		// GPIO14 -> PadName MTMS -> Function 2: HSPICLK -> CLK
	SET_PERI_REG_MASK(IO_MUX_GPIO15_REG, FUN_IE);		// GPIO15 -> PadName MTDO -> Function 2: HSPICS0 -> CS
	
	// Bypass GPIO Matrix
	CLEAR_PERI_REG_MASK(GPIO_FUNC9_OUT_SEL_CFG_REG, GPIO_FUNC9_OEN_SEL);	// HSPIQ_out (page 51)
	CLEAR_PERI_REG_MASK(GPIO_FUNC10_IN_SEL_CFG_REG, GPIO_SIG10_IN_SEL);		// HSPID_in 
	CLEAR_PERI_REG_MASK(GPIO_FUNC8_IN_SEL_CFG_REG, GPIO_SIG8_IN_SEL);		// HSPICLK_in
	CLEAR_PERI_REG_MASK(GPIO_FUNC11_IN_SEL_CFG_REG, GPIO_SIG11_IN_SEL);		// HPSICS0_in 
	
	
	
	

	// SPI Mode 0 Non-DMA, technical reference p. 124 Table 28
	SET_PERI_REG_MASK(SPI_PIN_REG(unit),  SPI_CK_IDLE_EDGE); 									// set to 1
	CLEAR_PERI_REG_MASK(SPI_USER_REG(unit), SPI_CK_I_EDGE | SPI_USR_COMMAND);					// cleared to 0
	SET_PERI_REG_BITS(SPI_CTRL2_REG(unit), SPI_MISO_DELAY_MODE, 0, SPI_MISO_DELAY_MODE_S);		// set to 0
	SET_PERI_REG_BITS(SPI_CTRL2_REG(unit), SPI_MISO_DELAY_NUM, 0, SPI_MISO_DELAY_NUM_S);		// set to 0
	SET_PERI_REG_BITS(SPI_CTRL2_REG(unit), SPI_MOSI_DELAY_MODE, 2, SPI_MOSI_DELAY_MODE_S);		// set to 2
	SET_PERI_REG_BITS(SPI_CTRL2_REG(unit), SPI_MOSI_DELAY_NUM, 2, SPI_MOSI_DELAY_NUM_S);		// set to 2
	
	// Set bit order to MSB
    CLEAR_PERI_REG_MASK(SPI_CTRL_REG(unit), SPI_WR_BIT_ORDER | SPI_RD_BIT_ORDER);	

    // Enable full-duplex communication
    SET_PERI_REG_MASK(SPI_USER_REG(unit), SPI_DOUTDIN);
	
	// debugging; Take MISO data from SPI_W8_REG
	SET_PERI_REG_MASK(SPI_USER_REG(unit), SPI_USR_MISO_HIGHPART);

	// activate MOSI MISO
    SET_PERI_REG_MASK(SPI_USER_REG(unit), SPI_USR_MOSI | SPI_USR_MISO);
	
	// buffer lenght to 8 bit
	SET_PERI_REG_BITS(SPI_SLV_WRBUF_DLEN_REG(unit), SPI_SLV_WRBUF_DBITLEN, 7, SPI_SLV_WRBUF_DBITLEN_S);
	SET_PERI_REG_BITS(SPI_SLV_RDBUF_DLEN_REG(unit), SPI_SLV_RDBUF_DBITLEN, 7, SPI_SLV_RDBUF_DBITLEN_S);
	
	
	
	// enable SPI slave transmission done interrupt
	SET_PERI_REG_MASK(SPI_SLAVE_REG(unit), SPI_TRANS_INTEN);
	
	
	// Set to SPI slave mode
	SET_PERI_REG_MASK(SPI_SLAVE_REG(unit), SPI_SLAVE_MODE);
	
	// 32 bit buffer for MOSI data (actually 8 bit)
    uint32_t recvbuf;
	recvbuf = 0;
	
	while(1)
	{		
		// reset interrupt
		CLEAR_PERI_REG_MASK(SPI_SLAVE_REG(unit), SPI_TRANS_DONE);
		
		
		// wait for interrupt bit
		while (~(READ_PERI_REG(SPI_SLAVE_REG(unit)))&SPI_TRANS_DONE)
		{
		};
		
		// copy MOSI data to recvbuf
		recvbuf = READ_PERI_REG(SPI_W0_REG(unit));
		
		// debugging; 
		WRITE_PERI_REG(GPIO_OUT_W1TS_REG, (1<<GPIO_HANDSHAKE));		
		WRITE_PERI_REG(GPIO_OUT_W1TC_REG, (1<<GPIO_HANDSHAKE));
		
		
		// debugging; print content of buffer
		//printf("received: %u\n", recvbuf);
	} 
}

It is to the point now where the interrupt triggers successfully after every sample:
CS MOSI HANDSHAKE beschriftet.png
CS MOSI HANDSHAKE beschriftet.png (131.9 KiB) Viewed 14276 times

The esp32 also puts out correct data at the MISO pin (I only implemented this for debugging).
Problem: It reads 0 from SPI_W0_REG all the time. I checked this by uncommenting printf line in while loop again.


Question: What is missing/wrong in the code above so that it reads 0 instead of the expected non-zero data?


Looking forward to some enlightenment, I know you guys are awesome.
Thanks

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Tue Aug 13, 2019 4:29 pm

I believe I have the exact same problem: I don't read '0', but I see the contents of data_buf register is never updated. I tried to initialize with a constant value (xAAAAAAAA), and I always read this init value.
It seems that the input shift register is not transferred into the data_buf register, even if slv_rdata_bit is properly incremented. In my case I expect to receive 6 bytes and even the first 32-bit word (4 bytes) is not transferred. But slv_rdata_bit is correctly set to x2F...
I would be happy if you find a solution...(I spent my whole day on this issue).
Francis

JohnGerwick
Posts: 3
Joined: Sat Aug 10, 2019 12:11 pm

Re: SPI slave by register configuration

Postby JohnGerwick » Wed Aug 14, 2019 7:16 am

You are absolutely right.
I'll keep you up to date on any progress I make. Propably the next step is to go back to the working SPI driver example and check out all the register configuration done there.

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Wed Aug 14, 2019 11:43 am

Good news:
I just got my very first exchange:

[*] I gave up the 'data_buf'... I tried will possible combinations with flags, but it never works. I never saw any of the data_buf register modified by reception. I guess the issue could be with the timing configuration, but without a decent documentation, it is quite difficult to go further...
[*] I decided to keep the original management of the DMA (without FreeRTOS in my case), and it works fine.

In my case, I have a single data signal (MOSI = MISO), but I selected the two signals mode and I switch between MOSI and MISO routing when I want to receive/send. I discarded 'Half Duplex' and deal with Full Duplex only. Half Duplex looks so complicated...

I thought that the direct register mode would be simpler, but either it does not work, or I missed some information to enable reception.
If you find another solution, let me know..
Francis

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Wed Aug 14, 2019 12:52 pm

I was too optimistic and I still have some problems:
[*] I expect to receive 6 bytes. The 5 first bytes are correct, but the 6th is always 0x00.
[*] I initialized the reception buffer (bigger than the expected amount of bytes) to x5A. and I find that the first 8 bytes have been overwritten (instead of 6 bytes). The first 5 bytes with the correct value, and the last three one with x00.

In my case, the 6th is a CRC, and it would be better to get it to validate the exchange... I don't care about the last two bytes even if it could be dangerous (I would understand that the dma manages only 32 bit words)

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Wed Aug 14, 2019 1:58 pm

For my last lost byte, I just found the explanation in the driver manualhttps://docs.espressif.com/projects/esp ... slave.html:
Warning: Due to a design peculiarity in the ESP32, if the amount of bytes sent by the master or the length of the transmission queues in the slave driver, in bytes, is not both larger than eight and dividable by four, the SPI hardware can fail to write the last one to seven bytes to the receive buffer.
It seems that there is no way to get back my CRC...

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Fri Aug 16, 2019 6:36 am

I found a workaround for the 'last byte issue: I generate internally dummy clock pulses to push the last 32 bit word to the DMA:

Code: Select all

    ret = spi_slave_simple_transmit_dma ( host, trans_desc);

    for ( i = 0; i < ticks_to_wait; i++)
    {
        vTaskDelay(1);
        if ( spihost[host]->hw->slave.trans_done )
            break;
        //Manage ESP32 SPI bug (last byte is lost!)
        if ( trans_desc->rx_buffer )
        {
            if ( (trans_desc->length % 32) != 0)   
            {
                if ( spihost[host]->hw->slv_rd_bit.slv_rdata_bit >= trans_desc->length-1 )                
                {
                    //Generate dummy clock pulses
                    int NClock = 24;
            #   if DEBUG_SLAVE
                    ESP_LOGI(SPI_TAG, "Generate %d clock pulses", NClock);
            #   endif
                    for (int i = 0; i < NClock ; i++ )
                    {
                        gpio_matrix_in(GPIO_FUNC_IN_HIGH, spi_periph_signal[host].spiclk_in, false);
                        gpio_matrix_in(GPIO_FUNC_IN_LOW, spi_periph_signal[host].spiclk_in, false);
                    }
                    //then restore pin
                    gpio_matrix_in(S3P_SCK_PIN, spi_periph_signal[host].spiclk_in, false);
                    PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[S3P_SCK_PIN], FUNC_GPIO);
                    break;
                }
            }
        }
I tried to apply the same workaround to the 'register mode' (no DMA), but it does not work. It seems that SPI-slave without DMA should be banned...

JohnGerwick
Posts: 3
Joined: Sat Aug 10, 2019 12:11 pm

Re: SPI slave by register configuration

Postby JohnGerwick » Tue Aug 27, 2019 6:44 am

So Francis, you have a working code now?
Could you please provide a working example code here or inthe 'sample code' section?

It will be highly appreciated. Seems like I'm totally stuck. I haven't made any progress.
Thanks

FrancisL
Posts: 21
Joined: Wed Jul 25, 2018 3:34 pm

Re: SPI slave by register configuration

Postby FrancisL » Sun Apr 12, 2020 5:36 pm

Sorry for the very long delay. Yes, the code is working fine. What you need to do:
1. Assign for the length a multiple of 64 bit. For example, if you are supposed to receive 80 clocks, write 128 into the registers.
2. After get the N clocks from your 'master', complement virtuatually up to 128 (see my code above). In this example, you will have to generate 128-80=48 virtual clock pulses.
Francis

Who is online

Users browsing this forum: No registered users and 51 guests