How to use the SPI (master) correctly

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

cross mob
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

(By the way, please don’t say to look at the examples – I have they I cannot see how they cover my situations as described below.)     

 

   

 I am trying to communicate with an MAX3421E (USB Host controller chip) using an SPI (master) component. The bit rate is 3MHz with 8-bit values. At this stage, I'm writing this as a polled interface - at some time in the future (as the project grows) I'll probably need to move to an interrupt driven approach.     

 

   

There are two situations that I need to handle:     

 

   

1) I need to read/write individual registers which means that I need to write a command byte and then either a value or dummy for the register value to be written or read.     

 

   

2) I need to read/write a FIFO with up to 64 values (plus a leading command byte)     

 

   

In the first case, I receive a 'status' value back as I send the command and, when reading a register, I will receive the register value with the 2nd byte.     

 

   

In the second case I need to send the command (I can ignore the status value in this situation) and then send or receive up to 64 values from/to memory. When writing my values to the slave, I don’t need anything back so I’m OK with overflowing the FIFO. However I do call “xxx_ClearRxBuffer” afterwards to make sure it is clear before anything else uses the SPI component.     

 

   

For both cases, I'm using the "xxx_PutArray" function to write the values to the SPI component and the only functions that seem to apply to read the values are "xxx_GetRxBufferSize" and "xxx_ReadRxData".     

 

   

From what I understand, I need to ensure that there are always values to send to ensure that the \SS\ line stays low for the entire transaction (however many values long)     

 

   

I have tried using the SPI component both with and without the software buffer and I'm getting different issues with both approaches.     

 

   

Without the software buffer (using the 4 value FIFO for both Tx and Rx), I can send the 2 values for case #1 and I can receive the status and 2nd value OK. However, for case #2, I *think* it is OK for writing multiple values to the chip's FIFO but I have no idea how to read back the values.     

 

   

So Question #1: What is the correct way to read back the received values when there are more than will fit into the FIFO.     

 

   

My trials show that I seem to be able to use this configuration to send my 65 values and the slave gets these OK. However I will certainly overflow the 4-value FIFO. In the situation where I’m trying to read the values back from the I will need to somehow get the values form the FIFO *while* the other values are being sent.     

 

   

Therefore Question #2: how can I ensure that the \SS\ line is kept low while sending multiple values to the slave AND reading back the received values?     

 

   

To try to get around the above limitation, I tried using a 65 value software buffer for both Tx and Rx. In case #1 (i.e. 2 values exchanged)  I seem to need to look at both the “xxx_GetRxBufferSize” function value (which always seems to be 0) and the “xxx_ReadRxStatus” (and the xxx_STS_RX_FIFO_NOT_EMPTY bit) but that tells me a value is available – but fails to say there is a 2nd value received. It is as though the software buffer is not used for receiving as both values fit into the FIFO but I only seem to be able to read the first!     

 

   

So, Question #3: how should you read fewer values (i.e. that will fit into the FIFO and not need the software buffer) when the software buffer is enabled?     

 

   

Which also leads to Question #4: Is the only option to not use the \SS\ output of the device and manually handle this output pin in my code? Alternatively are there APIs that let me manipulate the \SS\ manually?     

 

   

Thanks     

 

   

Susan     

 

0 Likes
17 Replies
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

A 3MHz transfer speed and a 60MHz CPU does not leave very much room for executing code. So you will have to use a fairly large buffer handled by the (already optimized) generated sources and use the appropiate APIs. When using buffers (128 to 256 is a good size for you to be on the right size) for both Rx and Tx you will have to forget about the FIFOs and just inspect as you already suspected the data using "xxx_GetRxBufferSize" (which is actually not a buffer size but a byte count)  and "xxx_ReadRxData".

   

You have a means to control the successful transmission by comparing _GetRxBufferSize() to the number of bytes you transmitted (which you normally know), so when you start to loose bytes something must be fishy.

   

The ss-line works well as long as a single transfer does not empty the FIFO, so when transmitting the register number and then the register value there might be a short ss-line glitch from which some devices tend to get hickups. So transfer both with a single call to _PutArray() and check for a complete transfer before continuing. Even the requred dummy bytes for reading results can / should be sent with the same _PutArray() when the device needs a single ss-low phase.

   

Do not touch (read) any SPI-status registers in the buffered mode! Some bits are "sticky" (reset when red) and this could infer with the internal interrupt handler.

   

 

   

Greetings to Down Under! (My daughter lives there)

   

Bob

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

 Hello Bob

   

(I hope your daughter lives up north somewhere in Australia as it is getting a bit cold down here in Melbourne at the moment!)

   

Thank you for the information. It would seem that the buffered option is the way to go. (Actually I found that part of the problem I had with that approach was that I had NOT turned on the global interrupts - but even doing that still leads to some problems.)

   

And yes, I am using the xxx_PutArray function for all of the transfers and the slave chip seems to be responding correctly so the \SS\ line appears to be controlled as it should.

   

However, I'm a bit puzzled by two of your comments: the first is that I should wait until the transfer is complete before continuing and the second is not to read the status registers in buffered mode beause reading the sticky bits will interfere with the buffering ISR (at least that is the way I read what you said).

   

Therefore, how can I tell that the values sent via the xxx_PutArray function have actually been sent if I don't use the xxx_ReadTxStatus and look for (say) the xxx_STS_SPI_IDLE? Is it reliable to look at the xxx_GetRxBufferSize and wait until that gets to the byte count or is there a "better" way?

   

Susan

0 Likes
himc_284346
Level 3
Level 3
Welcome! 10 replies posted 5 replies posted

  Hi,

   

 

   

As Bob pointed out it is not recommended to directly read the sticky register. You can read the number of bytes left to understand that transmission is done or not.But having said that, I have also tested using the condition while (0u == (SPIM_ReadTxStatus() & SPIM_STS_SPI_DONE)) in case of SPI-DMA also and it had worked fine. Seems it will depend on the project.

   

Cheers!!!

0 Likes
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

Sorry, Susan

   

I really overlooked your reply and HiZ triggered me to look at this thread again.

   

The resetting of the status flags will interfere with the interrupt driven circular buffer provided by the component.

   

So the Idea of using GetRxBufferSize() will help. You may always use constructs like

   

while(GetRxBufferSize()) ProcessData(); // Empty the recieve buffer

   

Moreover you may use a ClearRxBuffer() before you are expecting relevant data to empty the buffer and start anew.

   

 

   

Bob

   

Here: Trees are in blossom, we have got spring!

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

Thanks for the assistance guys - it is much appreciated.

   

However I have moved things along a bit and I've moved to a DMA approach which gives me a bit more flexibility. (I won't go into why now but this seems to be the most flexible approach that does what I need.) But I'm having problems getting an ISR triggered when the Rx DMA completes.

   

I am sure that the interrupt is correctly set up as I can use an API call to trigger it and the ISR is called. I am sure that the Tx DMA is correctly set up as I can send a variable number of bytes to the SPI and I can see the values being sent to the values being sent to the MOSI line.

   

The Rx DMA is set up almost exactly the same but, despite the fact  can see values being passed back on the MISO line (and the SPI worked previously without the DMA) there is never any data put into the Rx buffer.

   

The code is:

   

 

   

uint8 rxChannel;

   

uint8 rxTD;

   

uint8 rxBuffer[65];

   

uint8 txChannel;

   

uint8 txTD;

   

uint8 txBuffer[65];

   

uint8 dmaFlag;

   

 

   

CY_ISR( DMA_ISR)

   

{

   

dmaFlag = 1;

   

}

   

 

   

static void MAX3421_SetupTransfer( uint8 count, uint8* src, uint8* dst)

   

{

   

cystatus result;

   

uint8 state;

   

   

uint16 xferCount;

   

uint8 nextTD;

   

uint8 config;

   

 

   

result = CyDmaChSetExtendedAddress( txChannel, HI16( (uint32)src), HI16( (uint32)MAX3421SPI_TXDATA_PTR));

   

result = CyDmaTdSetAddress( txTD, LO16((uint32)src), LO16((uint32)MAX3421SPI_TXDATA_PTR));

   

result = CyDmaTdSetConfiguration( txTD, count, DMA_DISABLE_TD, TD_INC_SRC_ADR);

   

   

result = CyDmaChSetExtendedAddress( rxChannel, HI16((uint32)MAX3421SPI_RXDATA_PTR), HI16((uint32)rxBuffer));

   

result = CyDmaTdSetAddress( rxTD, LO16((uint32)MAX3421SPI_RXDATA_PTR), LO16((uint32)rxBuffer));

   

result = CyDmaTdSetConfiguration( rxTD, count, DMA_DISABLE_TD, TD_INC_DST_ADR);

   

   

dmaFlag = 0;

   

   

result = CyDmaChEnable( rxChannel, 1u);

   

result = CyDmaChEnable( txChannel, 1u);

   

   

// result = CyDmaChSetRequest( txChannel, CPU_REQ);

   

   

for(;;)

   

{

   

if( dmaFlag)

   

{ break; }

   

   

result = CyDmacError();

   

result = CyDmaTdGetConfiguration( rxTD, &xferCount, &nextTD, &config);

   

result = CyDmaTdGetConfiguration( txTD, &xferCount, &nextTD, &config);

   

result = CyDmaChGetRequest( txChannel);

   

result = CyDmaChGetRequest( rxChannel);

   

}

   

}

   

[\code]

   

(All of the junk at the end of the function is me verifying that the DMA transfers have completed and there are no errors reported.)

   

Previously I have allocated the DMA channels and added the TDs etc.

   

I can't see my mistake in the Rx DMA setup but I can't see why I don't get values into the RxBuffer when the Tx side works perfectly.

   

Susan

   
        
0 Likes
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

There is a nasty hidden bug in your code-snippet that (presumably) had not occured yet.

   

As a general rule:

   

Every global variable that gets altered in an interrupt handler must be declared as "volatile"

   

The reason is: "volatile" is an information for the C-optimization step telling that the variable might be changed from "outsides" of the current context. An example:

   

while(MyFlag);

   

The above line is obviously a loop waiting for MyFlag becoming FALSE (0). The optimizer "sees" that within the loop the variable "MyFlag" is not altered, so the code can be changed by extracting the variable out of the loop and testing it only once leaving an infinite loop as the TRUE (!=0) case for "MyFlag".

   

The default optimization settings for the compile step in Creator are very weak, but this may change unannounced. At least when one of your project runs against flash size limits or is too time-consuming a compilation with "Release" settings will start the desaster: Project does not run, but cannot be debugged to find the cause.

   

Some more explanations here

   

 

   

So in your case, use

   

volatile uint8 dmaFlag;

   

 

   

Happy coding

   

Bob

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

 Is there a 'smiley' for a 'facepalm'?! That is something I often pull others up for in other forums and thanks for pointing it out.

   

I have tried a couple of other things: by not preserving the TD I can see that the byte count for both the rx and tx sides go to zero and the address pointers (to memory) increment as they should. 

   

Therefore it would appear that the DMA operations are occurring correctly, but I'm still not getting the ISR called and stil not getting values put into the destination memory.

   

Given my previous 'silly mistake' it is probably something very obvious but I must admit I can't see it right now.

   

(By the way, I should explain that the function I have shown will ultimately use the passed parameters. However when I was having the issue with receiving the data, I switched over to referencing the global buffers directly. That seemed to work for the Tx side and also when I switched back to using the parameter. However I've left the direct reference to the receive buffer in there while I get things sorted out.)

   

Susan

0 Likes
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

Susan,

   

I cannot see from your code-snippet what the cause of the not-receiving-from-DMA-error might be. Furthermore I miss the help of the IDE telling me the underlying definitions for variables etc just by hoovering with the mos over an identifier.

   

I even cannot see whether you enabled global interrupts. Can you please post your complete project, so that we all can have a look at all of your settings? To do so, use
Creator->File->Create Workspace Bundle (minimal)
and attach the resulting file.



Bob
 

0 Likes
ETRO_SSN583
Level 9
Level 9
250 likes received 100 sign-ins 5 likes given

Did you issue the isr_StartEX for the ISR ?

   

 

   

    

   

          

   

CY_ISR_PROTO(MyIntFunc);      // Prototype declaration

   

then

   

CY_ISR(MyIntFunc)                         // Interrupt function definition

   

{

   

// Place code here

   

}

   

 

   

In  initialization part of the program

   

 

   

isr_StartEX(MyIntFunc);               // Start Interrupt with my handler

   

 

   

CY_ISR-macro have a look into the "System Reference Guide" under Help -> Documentation..

   

 

   

Regards, Dana.

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

Hi Dana - yes I have and, as I've said above (somewhere in all of the words) I have called the xxx_SetPending function and the ISR is called. My understanding is that this is a software way of doing the same thing as the hardware would do to set the pending bit in the appropriate register and so would show that the interrupt was correctly set up. Please correct me if I'm wrong.     

   

Hi Bob - I actually have a copy of the project with me but I really need to do what I ask others to do i other forums and that is to create a minimal project that exhibits the problem. Right now there is a lot of other code that is noise with respect to this situation and is possibly going to make fault finding a bit harder in someone else’s code. When I get home tonight, I'll create the minimal project and post that.     

   

I have turned on the global interrupts (at least I have now - as mentioned before I had missed out that step at the start) but I agree that I have probably not shown everything that I need to.     

   

(Edit: Some hours later - just seen another post that mentions the DMA__TD_TERMOUT_EN option bit for the TD. I have NOT set this bit and that could be the reason why the interrupt was not triggered. I'll try this option tonight but I missed the requirement for this option bit entirely. The way the description for the 'nrq' line reads implies that the nrq is pulsed regardless. However the need is mentioned in the CyDmaTdSetConfiguration but I missed it before now.)

   

Susan     

0 Likes
lock attach
Attachments are accessible only for community members.
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

I have attached a "minimal" project that exhibits the problem.

   

As I mentioned before, I have now solved the problem with the ISR not being called; all that remains is the fact that the DMA on the Rx side does not fill the memory buffer.

   

You should be able to simulate my external hardware by simply linking the MOSI to the MISO pins and you will get back what is sent - which is "0x8A 0x14".

   

the 'main' calls "MAX3421USB_Init' which sets up the DMA, the Interrupt control and then calls MAX3421_SetRegisterBits. This function is designed to take the register ID (1st parameter) and pass it (shifted left by 3 bits) with the "write" bit set (bit 1) to be a control value; this is followed by the bits to be sent. (The MAX3421 has some registers with 'sticky' bits but the job of the function is to isolate the caller from having to know which registers have which sticky bits set and only set/clear the bits of interest at that point).

   

Once the TxBuffer is set up, the MAX3421_SetupTransfer function is called to perform the operation via DMA. In this case I have the function set as a blocking routine for testing purposes.

   

Thanks for the on-going support and assistance.

   

Susan

   

Edit: I forgot to mention that this is running on a CY8CKIT-050 with a fully patched/updated IDE.

0 Likes
lock attach
Attachments are accessible only for community members.
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

Susan,

   

I made the attached project ready for Cypress to show a different issue with the SPI in 16-bit mode. This is a modified Cypress example project, so I deeply regret the coding style which is not mine at all.

   

The project transfers data over SPI (Keep it to 8 bits width !!!) and stores received data all done by DMA.

   

The obvious difference is in line 101

   

    CyDmaChEnable(rxChannel, STORE_TD_CFG_ONCMPLT);
where the channel is advised to keep (the incremented) address values for the next use.

   

Since in this program the DMA completes only once, I presume you will have to set up the TD anew to restart a transmission.

   

 

   

Bob

0 Likes

Bob,

I am trying to use DMA for SPI transfer to external device and need to send and receive like getting data from external spi ram.

The spidma project you posted looks very close I think to what I need.

I want to place the DMA transfer code into a sub routine for my app that can send variable number of bytes (number of bytes is passed to the routine).

What would I need to do in the DMA code to change the number of bytes ech time I use the routine? It would be called, do the transactions and then need to stop until called again.

To start a transaction, I suspect I write to the DMA config to change number of bytes to transfer and then pop the data into the TX buffer and then fill my rx buffer using a loop like the example code?

My attempt at rearranging the code is below: Would this work? If not, could you please give me some help.

#include <project.h>

void DmaTxConfiguration(uint8 bufLength);

void DmaRxConfiguration(uint8 bufLength);

/* DMA Configuration for DMA_TX */

#define DMA_TX_BYTES_PER_BURST      (sizeof(DataItem))

#define DMA_TX_REQUEST_PER_BURST    (1u)

#define DMA_TX_SRC_BASE             (CYDEV_SRAM_BASE)

#define DMA_TX_DST_BASE             (CYDEV_PERIPH_BASE)

/* DMA Configuration for DMA_RX */

#define DMA_RX_BYTES_PER_BURST      (sizeof(DataItem))

#define DMA_RX_REQUEST_PER_BURST    (1u)

#define DMA_RX_SRC_BASE             (CYDEV_PERIPH_BASE)

#define DMA_RX_DST_BASE             (CYDEV_SRAM_BASE)

#define BUFFER_SIZE                 (8u)

#define STORE_TD_CFG_ONCMPLT        (1u)

#if(SPIM_USE_SECOND_DATAPATH)

typedef uint16  DataItem; // Set this to uint8 or uint16

#else

typedef uint8 DataItem;

#endif

/* Variable declarations for DMA_TX*/

uint8 txChannel;

uint8 txTD;

/* Variable declarations for DMA_RX */

uint8 rxChannel;

uint8 rxTD;

DataItem txBuffer [BUFFER_SIZE] = {0x0u};

DataItem rxBuffer[BUFFER_SIZE] = {0};

void SendSpi(uint8 numBytes){

    uint8 i;

   

    DmaTxConfiguration(numBytes);

    DmaRxConfiguration(numBytes);

    txBuffer[0] = 0x01;

    txBuffer[1] = 0x02;

    txBuffer[2] = 0x03;

    while (0u == (SPIM_ReadTxStatus() & SPIM_STS_SPI_DONE))

    {

    }

    //At this point rxBuffer holds all receive data??   

}

int main()

{  

uint8 i;

    SPIM_Start();

    CyDmaChEnable(rxChannel, STORE_TD_CFG_ONCMPLT);

    CyDmaChEnable(txChannel, STORE_TD_CFG_ONCMPLT);

    for(;;)

    {

      SendSpi(3);

      CyDelay(100);

     

    }

}

void DmaTxConfiguration(uint8 bufLength)

{

    txChannel = DMA_TX_DmaInitialize(DMA_TX_BYTES_PER_BURST, DMA_TX_REQUEST_PER_BURST,

                                        HI16(DMA_TX_SRC_BASE), HI16(DMA_TX_DST_BASE));

    txTD = CyDmaTdAllocate();

    CyDmaTdSetConfiguration(txTD, bufLength*sizeof(DataItem), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR);

    CyDmaTdSetAddress(txTD, LO16((uint32)txBuffer), LO16((uint32) SPIM_TXDATA_PTR));

    CyDmaChSetInitialTd(txChannel, txTD);

}   

void DmaRxConfiguration(uint8 bufLength)

{

    rxChannel = DMA_RX_DmaInitialize(DMA_RX_BYTES_PER_BURST, DMA_RX_REQUEST_PER_BURST,

                                     HI16(DMA_RX_SRC_BASE), HI16(DMA_RX_DST_BASE));

    rxTD = CyDmaTdAllocate();

    CyDmaTdSetConfiguration(rxTD, bufLength*sizeof(DataItem), CY_DMA_DISABLE_TD, TD_INC_DST_ADR);

    CyDmaTdSetAddress(rxTD, LO16((uint32)SPIM_RXDATA_PTR), LO16((uint32)rxBuffer));

    CyDmaChSetInitialTd(rxChannel, rxTD);

}

/* [] END OF FILE */

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

Hello Bob,

   

In relation to the difference you have pointed out, I actually started with the setting of '1u' (which is the same as the STORE_TD_CFG_ONCMPLT constant) and the TDs were certainly returned to the initial configuration. 

   

However I changed this to '0' (as in my sample project) so that I could check that the address and count values were being updated correctly (which they were).

   

The other difference is that I'm using the 'HI16' macro on the variable name whereas you (and the example code) generally uses CYDEV_SRAM_BASE. However I found no difference when I tried using this symbol.

   

Susan

0 Likes
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

Well, isn't there another difference between my code and yours? I think my code works  and yours doesn't

   

The use of CYDEV_SRAM_BASE has only to do with compatibilities to PSoC3 on which a Hi16() would deliver wrong values.

   

I would suggest to use my working example and start modifying it step-by-step to suit your needs until you break it

   

Please keep me informed!

   

 

   

Bob

0 Likes
SuMa_296631
Level 5
Level 5
50 replies posted 25 replies posted 10 replies posted

There are times in life when you simply have to shake your head in wonder!

   

I created a new project from scratch, placed all of the components (admittedly with different names for some - and if that turns out to be the problem then there are bigger issues here!) and coded it up.....and it all works!

   

At this stage it is all in 1 code file but at least I have a base to grow from.

   

I have no idea what is different (I even had to go back to the old source file to see what I'd done in a couple of places and virtually copied the code) so I'm sorry that I cannot either log a fault or provide any warnings or guidance to others.

   

Thanks to Bob and others for your help.

   

Susan

0 Likes
Bob_Marlowe
Level 10
Level 10
First like given 50 questions asked 10 questions asked

Glad to have been of some help!

   

I have seen issues (two times, very rare) with the pin-component which were not reproducable, the component plainly did not work. When deleted and re-created all was fine, as expected. Maybe you met such a ghostly component.

   

 

   

Bob

0 Likes