Hard time to make SPI work in slave mode

Working libraries, libraries being ported and related hardware
stevestrong
Posts: 1814
Joined: Mon Oct 19, 2015 12:06 am
Location: Munich, Germany

Re: Hard time to make SPI work in slave mode

Post by stevestrong » Thu Nov 16, 2017 1:44 pm

I am not sure, but you can try to see if it works.
If not, then connect same wire to another pin, too.

castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Thu Nov 16, 2017 2:35 pm

Yea, connecting to other pin might work too, since my problem was that the ESP8266 runs out of pins easily, however still have a few left on the STM.

I'm almost there, I fixed the code on Master and Slave and it works like a dream master sending the frames and slave consuming them in real time, however I can' t get more than 35Mhz.

Right now is very unoptimized on the slave. It creates a buffer for the DMA then when is completed does a memcopy to the video buffer.

Currently I'm running the RGB with double buffer, so I don't really need another buffer, I want the DMA channel to write on the RGB backbuffer, once the frame is received I just want to swap the back buffer for the front buffer on the display, so the operation is immediate and the "new" back buffer is ready to be filled by DMA.

I haven't try yet, however on the IRQ function I need to do something like:

Code: Select all

void DMA_Rx_irq(void)
{
	uint32_t dma_isr = dma_get_isr_bits(DMA1, DMA_CH4);
	if (dma_isr & DMA_ISR_TCIF1)
	{
            matrix.swapBuffer();
            byte* backBuffer = matrix.backBuffer();
            dma_setup_transfer(DMA1, dma_ch,
            	&((spi2.dev())->regs->DR), DMA_SIZE_8BITS,
            	backBuffer, DMA_SIZE_8BITS,
            	(DMA_MINC_MODE | DMA_CIRC_MODE | DMA_HALF_TRNS | DMA_TRNS_CMPLT)); // flags
        }
      	dma_clear_isr_bits(DMA1, DMA_CH4);
}
Will this work?
basically I need to change the DMA buffer where the data is written on the fly

stevestrong
Posts: 1814
Joined: Mon Oct 19, 2015 12:06 am
Location: Munich, Germany

Re: Hard time to make SPI work in slave mode

Post by stevestrong » Thu Nov 16, 2017 8:10 pm

Do you have to do some processing on the received data?
If not, then you can use the same double buffer to receive the data directly into the RGB buffer.
Make the DMA buffer circular (which is actually the case in my example) and set it to the RGB buffer and forget about everything.
There is no need to re-init the DMA if nothing changes, it will continuously write the double buffers from the beginning when it finishes (that is why "circular").

castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Thu Nov 16, 2017 9:46 pm

I don't have any processing on the slave, however I need to call matrix.SwapBuffer() every time a full frame has been received to tell the display to make the switch to use the backbuffer as frontbuffer and the frontbuffer as backbuffer to update the screen without tearing.

I think I got what you are saying.

New steps:
Allocate a single consecutive frame buffer * 2 to hold two frames 6144 * 2 (currently I have two distinct 6144 buffers)

On the matrix the buffers will be
byte videoBuffer[12288];
*frontBuffer = videoBuffer;
*backBuffer = videoBuffer + 6144

On DMA setup function I'll have to provide the pointer to the videobuffer and I have to tell that I have 2 records of 6144 instead only 1.
then I call matrix.swapbuffer() when the IRQ triggers for both Half And Full, that way DMA will fill two frames before automatically resets since is circular.

I think will work great, I was thinking to DMA fill always the backbuffer that is not show on the screen and for every Full completed buffer call the swapbuffer() however that would require to move the ptr where the DMA needs to write after every frame, the solution that you propose sounds much simpler, only caveat is that I need to allocate the 2 video buffer as a single consecutive buffer, the STM32 has 20K and I need to take 12K so probably I'll have to allocate this buffer as soon the code start to avoid hit memory fragmentation and run out of memory.

Also intentionally for testing in the middle of the master streaming I omitted a single byte from a frame and it produced a massive chain reaction problem in the slave and the display gets badly offset over time, so definitely I'll have to implement recovery solution attaching an IRQ when the SS go low to verify the health of the DMA buffer or a similar solution else some noise in the bus would trigger a pretty bad behavior.

castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Sat Nov 18, 2017 1:54 am

The steps above works great and everything is working as expected, I have double buffer enabled, low memory footprint, and the RGB display is refreshing at 250Hz 128w32h12bpp, had to do a few tricks on the RGB matrix to swap the buffers depending if dma filled the first or second half of the videobuffer (the default swapBuffer() in the RGBMatrix class is not deterministic), the Master is sending the frames and the slave is processing them, basically I'm done with it.

Now the next step is to get some answers from the slave.

For example STM has RTC, I want to get the current time/data every time the master resets.

They way I understand master always generate the clock cycles, so I have added 2 extra bytes at the beginning of every frame sent by the master to provide a signature about what the Master is sending.

if the slave gets a signature like 'DF' (Display Frame) then the slave take the dmabuffer and display the data on the RGB Display.

Also now I'm sending when needed a full frame with dummy data but with the signatue 'TR' (Time Request) however I can't figure out how to send the answer back to the master.

Also after 'TR' I'm generating another bunch of clock signals to give a chance to the slave to put in there the answer back.

What I'm trying
SPIClass spi2(2);
I tried spi2.send, spi.dmaSend, spi.write however nothing seems to put the answer on MISO, obviously I'm missing steps or the whole picture.

What are the steps for the slave to send an answer back to the master?

stevestrong
Posts: 1814
Joined: Mon Oct 19, 2015 12:06 am
Location: Munich, Germany

Re: Hard time to make SPI work in slave mode

Post by stevestrong » Sat Nov 18, 2017 10:59 am

If you really need to transmit from slave only a couple of bytes, then u could use USART (serial), the blue pill has 3 USARTs.

Otherwise, to transmit over SPI slave, you need to do the following:
- setup the DMA Tx channel for the number of bytes u want to transmit:

Code: Select all

static const dma_channel dma_tx_ch = DMA_CH5; // SPI2 Tx DMA channel is channel 5, see reference manual 13.3.7.
uint8_t dma_tx_buffer[8]; // set the correct size
/*****************************************************************************/
void DMA_Tx_Setup(void)
{
	dma_init(DMA1);	// init DMA clock domain
// DMA channel configuration for SPI2 Tx
	dma_disable(DMA1, dma_tx_ch);	// Disable the DMA channel.
	dma_setup_transfer(DMA1, dma_tx_ch,
					&((SPI_2.dev())->regs->DR), DMA_SIZE_8BITS,
					data_tx_buffer, DMA_SIZE_8BITS,
					(DMA_MINC_MODE)); // flags
	dma_set_num_transfers(DMA1, dma_tx_ch, sizeof(data_tx_buffer));
	dma_set_priority(DMA1, dma_tx_ch, DMA_PRIORITY_HIGH);
	dma_enable(DMA1, dma_tx_ch);	// Enable the DMA channel. It will now begin serving requests.
}
- enable the SPI Tx DMA.
- remove the RXONLY bit from SPI configuration register (which is currently set by the SPI lib)

Code: Select all

/*****************************************************************************/
void SPI_setup(void)
{
	// use SPI 2 for recording
	SPI_2.beginTransactionSlave(SPISettings(18000000, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
	spi_rx_dma_enable(SPI_2.dev());
	spi_tx_dma_enable(SPI_2.dev()); // this is needed for Tx DMA channel
	bb_peri_set_bit(&((SPI_2.dev())->regs->CR1), SPI_CR1_RXONLY_BIT, 0); // make it full duplex
}
Of course, you should fill up the Tx buffer with useful data before the master begins to transmit.

castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Sun Nov 19, 2017 8:17 am

Great, I'll give a try today.

I'm running overclocked at 128Mhz since I needed to get good refresh rate for the RGB matrix and programming with a ST-LINK V2, so I'm using USART1 for debugging, USART2 is used by the RGB Matrix, also PB10 (USART3) is used by the OE of the matrix however I can move that pin if need to.

I'll give a shot with SPI else it makes sense to use USART3, it is just a few bytes that the slave has to send over.

castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Sat Dec 02, 2017 8:18 am

I didn't reply for a while since I could not make the slave to send anything on the bus, so I moved to something else, however i finished the other modules and now I'm back where I left off to give a new try.

I setup the DMA channel as the sample below however still the only thing I get on the bus is MOSI activity and no MISO at all.

Is there something else I can check?

I ran out ideas, I'm posting the class in here to see if you can spot something:

Code: Select all


#include "EspSlave.h"
#include "RGBMultiMatrix.h"
#include "Buzzer.h"
#include <WString.h>

static EspSlave *activeSlave = NULL;

EspSlave::EspSlave()
	: ss_pin(SPI2_NSS_PIN), spi2(2)
{
	activeSlave = this;
}

void EspSlave::init(uint8* videoBuffer, uint16 videoBufferSize)
{
	Srl.println("Buffer Size");
	dmaBuffer = videoBuffer;
	dmaBufferSize = videoBufferSize;

	pinMode(ss_pin, OUTPUT);
	digitalWrite(ss_pin, HIGH);

	spi2.beginTransactionSlave(SPISettings(18000000, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
	spi_rx_dma_enable(spi2.dev());
	spi_tx_dma_enable(spi2.dev());
	bb_peri_set_bit(&((spi2.dev())->regs->CR1), SPI_CR1_RXONLY_BIT, 0); // make it full duplex

	dmaInit();
	dmaSetup();
}

void EspSlave::dmaInit(void)
{
	dma_irq_full_complete = 0;
	dma_irq_half_complete = 0;
	dma_clear_isr_bits(DMA1, DMA_CH4);
	dma_clear_isr_bits(DMA1, DMA_CH5);

	Srl.println("Dma init");
}

void EspSlave::dmaBufferReady(bool fullBuffer)
{
	if (fullBuffer)
	{
		dma_irq_full_complete = 1;
	}
	else
	{
		dma_irq_half_complete = 1;
	}
}

void DMA_Tx_irq(void)
{
	//uint32_t dma_isr = dma_get_isr_bits(DMA1, DMA_CH5);
	Srl.print("t");
	//dma_clear_isr_bits(DMA1, DMA_CH5);
}

void DMA_Rx_irq(void)
{
	uint32 dma_isr = dma_get_isr_bits(DMA1, DMA_CH4);
	if (dma_isr & DMA_ISR_HTIF1)
	{
		activeSlave->dmaBufferReady(false);
	}
	if (dma_isr & DMA_ISR_TCIF1)
	{
		activeSlave->dmaBufferReady(true);
	}
	dma_clear_isr_bits(DMA1, DMA_CH4);
}

static const dma_channel dma_tx_ch = DMA_CH5; // SPI2 Tx DMA channel is channel 5, see reference manual 13.3.7.
uint8 data_tx_buffer[8]; // set the correct size

void EspSlave::dmaSetup(void)
{
	data_tx_buffer[0] = 255;
	data_tx_buffer[1] = 200;
	data_tx_buffer[2] = 170;
	data_tx_buffer[3] = 130;
	data_tx_buffer[4] = 100;
	data_tx_buffer[5] = 70;
	data_tx_buffer[6] = 30;
	data_tx_buffer[7] = 1;

	Srl.println("Dma setup starts");
	dma_channel dma_ch = DMA_CH4; // SPI2 Rx DMA channel is channel 4, see reference manual 13.3.7.

	dma_init(DMA1);	// init DMA clock domain, DMA setup transfer for SPI2 Rx

	dma_disable(DMA1, dma_ch);	// Disable the DMA tube.
	dma_disable(DMA1, dma_tx_ch);

	dma_setup_transfer(DMA1, dma_ch, &((spi2.dev())->regs->DR), DMA_SIZE_8BITS, dmaBuffer, DMA_SIZE_8BITS, (DMA_MINC_MODE | DMA_CIRC_MODE | DMA_HALF_TRNS | DMA_TRNS_CMPLT)); // flags
	dma_setup_transfer(DMA1, dma_tx_ch, &((spi2.dev())->regs->DR), DMA_SIZE_8BITS, data_tx_buffer, DMA_SIZE_8BITS, (DMA_MINC_MODE)); // flags

	dma_set_num_transfers(DMA1, dma_ch, dmaBufferSize);
	dma_set_num_transfers(DMA1, dma_tx_ch, sizeof(data_tx_buffer));

	dma_set_priority(DMA1, dma_ch, DMA_PRIORITY_VERY_HIGH);
	dma_set_priority(DMA1, dma_tx_ch, DMA_PRIORITY_HIGH);

	dma_attach_interrupt(DMA1, dma_ch, DMA_Rx_irq);	// attach an interrupt handler.
	dma_attach_interrupt(DMA1, dma_tx_ch, DMA_Tx_irq);	// attach an interrupt handler.

	dma_enable(DMA1, dma_ch);	// Enable the DMA tube. It will now begin serving requests.
	dma_enable(DMA1, dma_tx_ch);	// Enable the DMA tube. It will now begin serving requests.

	Srl.println("Dma setup ends");
}

void EspSlave::processDmaBuffer(uint8* buffer, bool isCompletedBuffer)
{
	// First two bytes are master instruction
	switch (buffer[0])
	{
		case MASTER_INSTRUCTION_DISPLAY:
		{
			swapDmaBuffer(buffer, isCompletedBuffer, false);
			return;
		}
		case MASTER_INSTRUCTION_BUZZER:
		{
			//Srl.println("Instruction Buzzer");
			byte command = buffer[1];
			switch (buffer[1])
			{
				case MASTER_INSTRUCTION_BUZZER_VOLUME:
				{
					buzzer.setVolume(buffer[2]);
					break;
				}
				case MASTER_INSTRUCTION_BUZZER_FRECUENCY:
				{
					uint16 frecuency = buffer[2] << 8 | buffer[3];
					buzzer.setFrecuency(frecuency);
					break;
				}
				default:
				{
					Srl.println("Buzzer Unknown");
					break;
				}
			}
			break;
		}
		case MASTER_INSTRUCTION_GET_TIME:
		{
			Srl.print("TIME REQUESTED:");
			//byte d1[2];
			//d1[0] = 'A';
			//d1[1] = 'A';

			//if (buffer[1] == 0)
			//{
			//	spi2.write(d1, 2);

			//	Srl.println("INSTRUCTION");
			//}

			//if (buffer[1] == 1)
			//{
			//	spi2.send(d1, 2);
			//	Srl.println("RESPONSE");
			//}

			break;
		}
		default:
		{
			Srl.println(buffer[0]);
			Srl.println(buffer[1]);
			Srl.println("Unknown Frame");
		}
	}
	swapDmaBuffer(buffer, isCompletedBuffer, true);
}

void EspSlave::swapDmaBuffer(uint8* buffer, bool isCompletedBuffer, bool isInstruction)
{
	if (isCompletedBuffer == false)
	{
		if (isInstruction)
		{
			memcpy(buffer, dmaBuffer + VIDEO_FRAME_INSTRUCTION_SIZE, 12);
		}
		matrix.activateBuffer(0);
	}
	else
	{
		if (isInstruction)
		{
			memcpy(buffer, dmaBuffer, 12);
		}
		matrix.activateBuffer(1);
	}
}

void EspSlave::loop()
{
	if (dma_irq_half_complete)
	{
		dma_irq_half_complete = 0;
		processDmaBuffer(dmaBuffer, false);
	}

	if (dma_irq_full_complete)
	{
		dma_irq_full_complete = 0;
		processDmaBuffer(dmaBuffer + VIDEO_FRAME_INSTRUCTION_SIZE, true);
	}
}

EspSlave spiSlave;

stevestrong
Posts: 1814
Joined: Mon Oct 19, 2015 12:06 am
Location: Munich, Germany

Re: Hard time to make SPI work in slave mode

Post by stevestrong » Sat Dec 02, 2017 10:19 am


castortiu
Posts: 39
Joined: Tue Nov 07, 2017 8:34 am
Location: Seattle, WA

Re: Hard time to make SPI work in slave mode

Post by castortiu » Tue Dec 05, 2017 11:59 am

I tried for 8hs to make the code in the link above (non-dma) to work without success. Send back data on the MISO was easy however was painful, and totally unreliable, I used every combination of while( !(regs->SR & SPI_SR_TXE) ); while( (regs->SR & SPI_SR_BSY) );, enable/disable interrupts, etc, however the data send back was out of sync, sometimes the same character was sent multiple times, also I got a big performance hit, where the DMA version was working at 42MHZ, the non-dma with only RX was stable at 24Mhz and only 14MHZ on RX/TX mode.

So 99.999% of the time the master was using a clock of 24Mhz and reduce to 14Mhz when needed data from the slave, however even slowing down to a 1Mhz the data that got back was unreliable.

Tried virtually almost everything.

Rolled back all changes and started over with DMA again and finally I made the DMA SPI full-duplex setup working at a 42Mhz, also I'm removing the circular buffer and changing to synchronize the DMA pointer to the SS LOW (at least is what I want to do), this way my RGBMatrix will be immune to noise on the clock, a single clock cycle offset can produce a unrecoverable cascade data corruption on the display since the DMA buffer won't be writing the data in the buffer as the master is expecting it, If I sync the DMA pointer to the SS LOW the problem should go away.

There is almost no info about make the STM32 work with DMA at full duplex, I'll post my setup once I finish cleaning the code.

One of the big mistakes why there was no activity on the MISO was because of the flags to setup spi_slave_enable(), beyond remove SPI_CR1_RXONLY_BIT also I needed to add SPI_BIDIOE only, adding SPI_BIDIMODE as well stopped full duplex from working.

Working flags for DMA Full duplex
uint32 flags = ((_currentSetting->bitOrder == MSBFIRST ? SPI_FRAME_MSB : SPI_FRAME_LSB) | SPI_DFF_8_BIT | SPI_BIDIOE);

Post Reply