2
\$\begingroup\$

I'm trying to setup a STM32F303RE SPI2 slave that must continuously and repeatedly send contents of a 2-byte buffer using DMA.

More specifically, if my buffer is:

#define ALIGN(x)    __attribute__((aligned(x)))
ALIGN(4) uint8_t TxBuffer[2] = { 'A', 'B' };

then I want my STM board to behave in the following way:

  1. if master sends it 2 bytes, it should always send back 'AB'
  2. if master sends it 1 byte, it should always send back 'A'
  3. if master sends it N>2 bytes, it should always send 'AB' N/2 times + a trailing 'A' if N is odd

Since I'm a beginner with this, I decided to start with a simple implementation and build on that along the way. Which is why I'm using DMA without interrupts. Here's how the (relevant) code currently looks:

/* TX & RX buffers for SPI. */
ALIGN(4) uint8_t        TxBuffer[2];
ALIGN(4) uint8_t        RxBuffer[2]; /* Dummy, not actually used. */

int main(void)
{
    SPI_Config();
    SysTickConfig();

    RxBuffer[0] = (RxBuffer[1] = 0);
    TxBuffer[0] = 'A';
    TxBuffer[1] = 'B';

    while (1)
    {
        /* Clear DMA1 global flags */
        DMA_ClearFlag(DMA1_FLAG_GL4);
        DMA_ClearFlag(DMA1_FLAG_GL5);
        /* Disable the DMA channels */
        DMA_Cmd(DMA1_Channel4, DISABLE);
        DMA_Cmd(DMA1_Channel5, DISABLE);
        /* Disable the SPI peripheral */
        SPI_Cmd(SPI2, DISABLE);
        /* Disable the SPI Rx and Tx DMA requests */
        SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Rx | SPI_I2S_DMAReq_Tx, DISABLE);

        DMA1_Channel4->CNDTR = (DMA1_Channel5->CNDTR = 2);
        DMA1_Channel4->CPAR = (uint32_t) &SPI2->DR;
        DMA1_Channel5->CPAR = (uint32_t) &SPI2->DR;
        DMA1_Channel4->CMAR = (uint32_t) &RxBuffer[0];
        DMA1_Channel5->CMAR = (uint32_t) &TxBuffer[0];

        /* Enable the SPI Rx and Tx DMA requests */
        SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Rx | SPI_I2S_DMAReq_Tx, ENABLE);
        /* Enable the SPI peripheral */
        SPI_Cmd(SPI2, ENABLE);
        DMA_Cmd(DMA1_Channel4, ENABLE);
        DMA_Cmd(DMA1_Channel5, ENABLE);

        /* Wait the SPI DMA transfers complete */
        while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET) {}
        while (DMA_GetFlagStatus(DMA1_FLAG_TC5) == RESET) {}
        while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) {}
        while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET) {}

        // Here RxBuffer data can be inspected
    }
}

static void SPI_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* Enable SCK, MOSI, MISO and NSS GPIO clocks */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB , ENABLE);

    /* SPI pin mappings */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource12, GPIO_AF_5); // SPI2_NSS
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_5); // SPI2_SCK
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_5); // SPI2_MISO
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_5); // SPI2_MOSI

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_DOWN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    /* SPI SCK pin configuration */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* SPI  MOSI pin configuration */
    GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_15;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* SPI MISO pin configuration */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* SPI NSS pin configuration */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* Enable the SPI peripheral */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);

    /* SPI configuration -------------------------------------------------------*/
    SPI_I2S_DeInit(SPI2);
    SPI_StructInit(&SPI_InitStructure);
    SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;

    SPI_Init(SPI2, &SPI_InitStructure);
    SPI_CalculateCRC(SPI2, DISABLE);
    SPI_TIModeCmd(SPI2, DISABLE);
    SPI_NSSPulseModeCmd(SPI2, DISABLE);

    /*
     * SPI_I2S_FLAG_RXNE flag should be set as soon as 1 byte (quarter buffer)
     * is shifted into receiving FIFO.
     */
    SPI_RxFIFOThresholdConfig(SPI2, SPI_RxFIFOThreshold_QF);

    /* Enable the DMA peripheral */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    /* DMA Configuration -------------------------------------------------------*/
    DMA_DeInit(DMA1_Channel4);
    DMA_DeInit(DMA1_Channel5);
    DMA_StructInit(&DMA_InitStructure);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &SPI2->DR;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize =  DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_InitStructure.DMA_BufferSize = 0;
    DMA_InitStructure.DMA_MemoryBaseAddr = 0;

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
}

This works ok if master always sends an even number of bytes per chip-select (NSS pin). If the master only sends one byte @ some point (within a single chip-select) things start to get messy.

Here's a concrete scenario:

  • STM32 board starts
  • Master sends 2 bytes in a single chip-select and reads back the 2 bytes it received from slave's end. As expected, these are 'AB'.
  • Master sends 1 byte in a single chip-select and reads the byte it received from the slave. As expected, this is 'A'.
  • Master sends 2 bytes in a single chip-select and reads the 2 bytes it received from slave's end. This time those 2 bytes are 'BA'. As per the above stated conditions (1-3), I want them to be 'AB' instead.

What must I do to achieve this? I noticed "while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET)" never finishes when the master sends just the single byte, so I'm guessing that behind the scenes the DMA simply always waits for 2 bytes per-transfer (i.e. before setting TC), regardless of the state of NSS (chip-select).

Somehow I want to force DMA completion when NSS is high again (i.e. when the SPI slave is not chip-selected anymore).

\$\endgroup\$
3
\$\begingroup\$

After reading the SPI and DMA chapters thouroughly, the clear conclusion is that the SPI peripheral (or the DMA) does not provide flags/behavior targeting changes of chip-selection (NSS pin). But that makes sense since the NSS pin is also one of the GPIOs and we can retrieve its state by that interface instead.

So, I achieved this some days ago by simply...

  1. Setting up an interrupt on rising NSS (PB12) - NSS rises after a transaction, i.e. when the slave is no longer chip-selected

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
    
    SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOB, EXTI_PinSource12);
    EXTI_InitStruct.EXTI_Line = EXTI_Line12;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_Init(&EXTI_InitStruct);
    
    /* 4 bits Preemptive priority, 4 bits Sub-priority. */
    NVIC_SetPriorityGrouping(3);
    NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 15;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 15;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
    
  2. Resetting SPI2 (no other way to clear TXFIFO...) and rewinding DMA channel to the start of buffer when the interrupt triggers

    void EXTI15_10_IRQHandler(void)
    {   
        EXTI_ClearITPendingBit(EXTI_Line12);
    
        /* Clear DMA1 global flags */
        DMA_ClearFlag(DMA1_FLAG_GL4);
        DMA_ClearFlag(DMA1_FLAG_GL5);
        /* Disable the DMA channels */
        DMA_Cmd(DMA1_Channel4, DISABLE);
        DMA_Cmd(DMA1_Channel5, DISABLE);
    
        /*
         * Bring back SPI2 DMAs to start of Rx & Tx buffers -
         * CPAR/CMAR stay the same after disable, no need to
         * `restore` those.
         */
        DMA1_Channel4->CNDTR = (DMA1_Channel5->CNDTR = 2);
    
        /* Reset SPI2 (clears TXFIFO). */
        RCC->APB1RSTR |= RCC_APB1RSTR_SPI2RST;
        RCC->APB1RSTR &= ~RCC_APB1RSTR_SPI2RST;
    
        /* Reconfigure SPI2. */
        SPI_Init(SPI2, &SPI_InitStructure);
        SPI_CalculateCRC(SPI2, DISABLE);
        SPI_TIModeCmd(SPI2, DISABLE);
        SPI_NSSPulseModeCmd(SPI2, DISABLE);
    
        /* Re-enable SPI2 and DMA channels. */
        SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Rx, ENABLE);
        DMA_Cmd(DMA1_Channel4, ENABLE);
        DMA_Cmd(DMA1_Channel5, ENABLE);
        SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);
        SPI_Cmd(SPI2, ENABLE);
    }   
    
  3. Change

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    

    to

    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    

    for the N>2 cases.

At first I was concerned that entirely disabling and reconfiguring SPI2 would take too long in EXTI15_10_IRQHandler, but I managed to get that to execute in 2.6us (from initially 16us with -O3!) by making a simple change in the StdPeriph library:

made all functions called from the handler "static inline" (as they should have been in the first place). With that change -O3 becomes a lot more useful.

\$\endgroup\$
2
\$\begingroup\$

Don't fully understand, but I think there is a flaw in your conception. A DMA transfer is used to stream large block of data, and not to wait a certain command and then to respond - this is done using interrupts. A DMA receive is ment to continously listen to a stream of data from a device, surely not to wait a single byte. Therefore you can't receive a data packet with different lenght, since the DMA buffer has to be full enough to trigger an interrupt.

EDIT: I took a look on your code and it's a no go. Waiting in endless loop the DMA is a no go. When the receive buffer is full, then it has to trigger an interrupt - a callback function, this is the place where you should evaluate the receive buffer. But as said, forget the receive DMA. The benefit of DMA is to do other things when the DMA takes care of sending/receiving, surely it is not made to wait in endless loop to acomplish.

\$\endgroup\$
  • \$\begingroup\$ Hi Marko. What if instead of those 2 bytes was e.g. a 2MB buffer? Wouldn't DMA be useful then? To be more specific, what I'm actually trying to do is to have the slave connected between a device which has a finite number of states - in this case 16 0/1 states (similar to a state machine) - and the master. The master wants to continuously monitor the state of the state-machine (and the SPI slave acts as the intermediary) and it does so e.g. by requesting those 16-bits in a loop, e.g. every 1ms. My question is how do I keep that slave in this normal functioning mode when the master misbehaves? \$\endgroup\$ – Corneliu Zuzu Oct 1 '16 at 8:25
  • \$\begingroup\$ Regarding your later edit: yes, I'm intending use DMA with interrupts later, but as I said since I'm just beginning with STM32 programming, currently I've used the StdPeriph library-provided example (see SPI\SPI_TwoBoards\SPI_DataExchangeDMA - which was without interrupts as well) and build on that on the way. Are you saying that with interrupts I could make the DMA trigger completion when the NSS is up again even if the master didn't send 2 bytes? That would solve my problem. \$\endgroup\$ – Corneliu Zuzu Oct 1 '16 at 8:35
  • \$\begingroup\$ DMA is really needed in SPI slave mode. Interrupts to load the next data into the Tx register may have too much latency at high data rates. Also, DMA really doesn't care about the length of data to transfer. \$\endgroup\$ – Ber Nov 7 '17 at 11:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.