blocking spi full duplex

Tip / Sign in to post questions, reply, level up, and achieve exciting badges. Know more

cross mob
NiLe_4796031
Level 2
Level 2
10 replies posted 5 replies posted 5 questions asked

If I understand correctly, there are two interfaces to Cy_SCB_SPI, the high-level interface which permits concurrent reads and writes but is asynchronous / interrupt driven, and the low-level interface which permits only reads or writes but allows you to choose asynchronous or blocking.

I'm wrapping the Cypress SPI interface for calling code that expects a blocking SPI read+write function that starts communication immediately (the talk of FIFOs in the documentation makes me worry that sometimes the transfer is simply queued for later), and power usage I'd like to put the core to sleep until the interrupt indicating the transfer is complete (or other SPI state change) and for performance I'd like to avoid timeouts (either sleeping longer than needed or waking up too soon).

I thought it was possible to build a blocking interface on top of Cy_SCB_SPI_Transfer where my interrupt handler sets a flag indicating that the transfer is done, but I'm encountering a race condition where the interrupt handler runs between the point where I check the flag and where I sleep for the next interrupt, leading to my core sleeping forever as no further interrupt arrives.

I've also looked into building it on the low-level interface with Cy_SCB_SPI_Read and Cy_SCB_SPI_Write but these appear to just block until data is in the FIFO not until the communication is actually complete over the wire. See Re: Cy_SCB_SPI_ReadArray documentation  and Re: Use Low Level SPI read .

Is there any way to do build a blocking full-duplex SPI function? Have I overlooked something in the API?

My device is SPI master.

0 Likes
1 Solution
lock attach
Attachments are accessible only for community members.

Oh, I see the problem.

Even if the FIFO is Empty, it doesn't mean the SPI is done transmitting. So you need to keep checking if the Master Status is done.

Attached you find a solution for this, but still spends around 50% of time in a while loop. You could drop the spi width to be 8 bits, so you spend only 25% of time in a while loop.

I think a way to remove the while loop is by using the Smart I/O to generate the SS signal. You can setup the SPI width to be 12, then write your entire frame to the FIFO at once. You need to add 4 dummy bits on every 32 bits. For example:

If the frame is 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, write to the SPI FIFO:

0xAAA, 0xAAA, 0xAAF, 0xBBB, 0xBBB, 0xBBF, 0xCCC, 0xCCC, 0xCCF

Then configure the Smart I/O to assert on 1st bit, de-assert on 32nd bit and assert on 36th. Do this all over again.

View solution in original post

0 Likes
16 Replies
AlanH_86
Employee
Employee
100 replies posted 50 replies posted 25 solutions authored

Which PSoC?  6 or 4?

0 Likes

PSoC 6.

0 Likes

The answer is yes.  You can block by doing while(Cy_SCB_GetNumInTxFifo(CySCB_Type const *base)); which will block until the Tx Fifo is empty

Alan

Cy_SCB_GetNumInTxFifo(CySCB_Type const *base)
0 Likes

That looks like a while loop which will keep CPU 100% active until the tx fifo is empty? I'm looking for a way to sleep the CPU until the tx fifo is empty.

0 Likes

I attempted to prototype something like this by putting all my logic inside the interrupt handler and using a state machine to make progress. It appears that even on the M4, the time it takes to exit the interrupt handler, go back to the while(1) loop with a single `wfi` instruction ("wait for interrupt") and back into the interrupt handler is too slow for my needs. I'm going to end up using the busy loop. Thanks!

0 Likes
AlanH_86
Employee
Employee
100 replies posted 50 replies posted 25 solutions authored

Why do you want to block until the fifo is empty?  Why not just put it in the fifo and get on with life.

Then check that the fifo is empty before you deep sleep?  Meaning change the place where the busy wait exists in your code.

0 Likes

Because the SPI block still doesn't support 32-bit transfers! I have to manually wiggle the CS line myself every 32 bits, and I must wait until exactly after the transfer is done.

I'm doing 18 32-bit RX/TX SPI messages on a 10kHz timer, with SPI at the highest speed the Cypress block supports. This means I spend approximately 50% of the time communicating SPI and 50% of the time sleeping for next timer interrupt.

What I wanted to do was save power by sleeping (not deep sleep, merely wait-for-interrupt) after putting 2x16-bit data in the TX and receive an interrupt when the actual TX is done so I can raise the CS line and move on to the next 32-bit command. My device is running on battery and is very power constrained.

0 Likes

I think the easiest way would be to write twice to the SPI FIFO and setup an interrupt to trigger when the TX FIFO is empty. You can sleep during this time. Here is the flow:

1) Write twice to the SPI FIFO

2) Enable the SPI interrupt (to trigger on TX FIFO)

3) Go to sleep

4) On wake-up, check if the FIFO is empty (maybe some other interrupt woke it up)

5) If not empty, go to sleep

6) If empty, move on to the next transfer (step 1)

In the interrupt handler, disable the interrupt, so it doesn't keep triggering.

0 Likes

You made a statement that I think might be key: "In the interrupt handler, disable the interrupt, so it doesn't keep triggering." I've done this before with TCPWM blocks, but I thought it was specific to them. If I knew for sure that the interrupt would continue to fire until I did something to clear it, I could use that to solve my problem. Does SPI do this? If I set Cy_SCB_SetTxFifoLevel(base, 1); will it continue to interrupt until I add bytes to the TX FIFO?

The flow you suggested is roughly what I already had. The problem I have with that flow is step #5. If I write:

> while (Cy_SCB_GetNumInTxFifo(SPI_1_HW) != 0) {

>   Cy_SysPm_Sleep(CY_SYSPM_WAIT_FOR_INTERRUPT);

> }

then I might end up with testing Cy_SCB_GetNumInTxFifo while it's equal to 1, then the FIFO goes to zero triggering the interrupt, then we WFI for an interrupt that will never come (because it already came).

I tried a second flow which was to always fill the TX FIFO in the interrupt handler then immediately return, while the main loop was while (1) { Cy_SysPm_Sleep(CY_SYSPM_WAIT_FOR_INTERRUPT); }. The first thing the interrupt handler would do is clear the CS line. What I found with my logic analyzer is a 0.33ms gap from the end of the SPI transmission to the change of the CS line. Is it possible that Cortex M0 takes 0.33ms to go to sleep and wake back up? If so then this is a non-starter, I have a 0.1ms window to make all 18 TX/RX comms.

0 Likes
lock attach
Attachments are accessible only for community members.

Oh, I see the problem.

Even if the FIFO is Empty, it doesn't mean the SPI is done transmitting. So you need to keep checking if the Master Status is done.

Attached you find a solution for this, but still spends around 50% of time in a while loop. You could drop the spi width to be 8 bits, so you spend only 25% of time in a while loop.

I think a way to remove the while loop is by using the Smart I/O to generate the SS signal. You can setup the SPI width to be 12, then write your entire frame to the FIFO at once. You need to add 4 dummy bits on every 32 bits. For example:

If the frame is 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, write to the SPI FIFO:

0xAAA, 0xAAA, 0xAAF, 0xBBB, 0xBBB, 0xBBF, 0xCCC, 0xCCC, 0xCCF

Then configure the Smart I/O to assert on 1st bit, de-assert on 32nd bit and assert on 36th. Do this all over again.

0 Likes

I didnt realize that things werent done when the FIFO was empty...  that is good to know.

Using the Smart I/O how much faster can you handle the SS than using the hard block?

0 Likes

The Smart I/O can be clocked by the same clock that goes to the SPI block. So it might add one cycle delay.

0 Likes
lock attach
Attachments are accessible only for community members.

Thank for you clever suggestions!

You made me realize that I didn't quite understand how the interrupts work and I spent a while digging into them. It seems that I can get an interrupt at end of an SPI transmission by setting the RxFifoLevel. The RX FIFO can't contain the data until the whole thing has been read, and because RX and TX happen at the same time, this means that it bumps the RX FIFO size by one at the end of transmission.

I started an empty project and implemented that idea, see the Design02 zip attached. I've also checked the waveform with the logic analyzer:

spi-rx.png

and a whole row of 18:

spi-rx-18.png

My only concern at this stage is whether there's a potential for race condition between the first time blocking_send checks spi_done and the first time it waits for an interrupt.

0 Likes

My only concern at this stage is whether there's a potential for race condition between the first time blocking_send checks spi_done and the first time it waits for an interrupt.

In the example project I posted there's nothing to change the timing so a race condition can't occur, but in real usage we have other interrupts which can happen while blocking_send is processing. In code,

static uint32_t blocking_send(uint32_t tx) {

    spi_done = false;

    Cy_GPIO_Write(CS_0_PORT, CS_0_NUM, 0);

    Cy_SCB_SPI_Write(SPI_1_HW, tx >> 16);

    Cy_SCB_SPI_Write(SPI_1_HW, tx & 0xffff);

// An unrelated interrupt fires here, execution returns when the SPI transmission is almost done.

    while (!spi_done) {

// The SPI interrupt fires, SPI_1_Isr runs, spi_done is set to true.

        Cy_SysPm_Sleep(CY_SYSPM_WAIT_FOR_INTERRUPT); // Missed the SPI ISR. Either sleeps forever, or too long.

    }

    return rx[0] << 16 | rx[1];

}

I'm convinced there's still a race condition here.

0 Likes

Nick, using the RX Interrupt Level matches perfectly what you need! It generates an interrupt exactly on the end of the SPI frame.

A few ways to solve the race condition:

1) If your firmware architecture allows, you could disable other interrupts in the firmware before writing to the SPI FIFO and only re-enable them after waking-up.

2) You could enable a timeout interrupt before going to sleep and disable it after waking up.

3) Use an RTOS.

0 Likes

RTOS is the answer

0 Likes