Esclavo STM32 SPI: restablecer el estado DMA en NSS alto

Estoy tratando de configurar un esclavo STM32F303RE SPI2 que debe enviar contenido de forma continua y repetida de un búfer de 2 bytes mediante DMA.

Más específicamente, si mi búfer es:

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

entonces quiero que mi placa STM se comporte de la siguiente manera:

  1. si el maestro lo envía 2 bytes , siempre debe devolver 'AB'
  2. si el maestro lo envía 1 byte , siempre debe devolver 'A'
  3. si el maestro lo envía N> 2 bytes , siempre debe enviar 'AB' N/2 veces + una 'A' final si N es impar

Como soy un principiante en esto, decidí comenzar con una implementación simple y desarrollarla en el camino. Por eso estoy usando DMA sin interrupciones. Así es como se ve actualmente el código (relevante):

/* 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);
}

Esto funciona bien si el maestro siempre envía un número par de bytes por selección de chip (pin NSS). Si el maestro solo envía un byte @ en algún punto (dentro de una sola selección de chip), las cosas comienzan a complicarse.

He aquí un escenario concreto:

  • Comienza la placa STM32
  • El maestro envía 2 bytes en una sola selección de chip y lee los 2 bytes que recibió del extremo del esclavo. Como era de esperar, estos son 'AB' .
  • El maestro envía 1 byte en una sola selección de chip y lee el byte que recibió del esclavo. Como era de esperar, esto es 'A' .
  • El maestro envía 2 bytes en una sola selección de chip y lee los 2 bytes que recibió del extremo del esclavo. Esta vez esos 2 bytes son 'BA' . De acuerdo con las condiciones establecidas anteriormente (1-3), quiero que sean 'AB' en su lugar.

¿Qué debo hacer para lograr esto? Noté que " while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET)" nunca termina cuando el maestro envía solo un byte, por lo que supongo que detrás de escena, el DMA simplemente siempre espera 2 bytes por transferencia (es decir, antes de configurar TC), independientemente del estado de NSS (chip -seleccionar).

De alguna manera, quiero forzar la finalización de DMA cuando NSS esté alto nuevamente (es decir, cuando el esclavo SPI ya no esté seleccionado por chip).

Respuestas (3)

Después de leer detenidamente los capítulos de SPI y DMA, la conclusión clara es que el periférico SPI (o el DMA) no proporciona indicadores/cambios de orientación de comportamiento de selección de chip (pin NSS). Pero eso tiene sentido ya que el pin NSS también es uno de los GPIO y podemos recuperar su estado mediante esa interfaz.

Entonces, lo logré hace unos días simplemente...

  1. Configuración de una interrupción al aumentar NSS (PB12): NSS aumenta después de una transacción, es decir, cuando el esclavo ya no está seleccionado por chip

    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. Restablecer SPI2 (no hay otra forma de borrar TXFIFO...) y rebobinar el canal DMA al inicio del búfer cuando se activa la interrupción

    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. Cambiar

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    

    a

    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    

    para los N>2 casos.

Al principio, me preocupaba que deshabilitar y reconfigurar por completo SPI2 llevaría demasiado tiempo en EXTI15_10_IRQHandler, pero logré que se ejecutara en 2.6us (desde inicialmente 16us con -O3 ) haciendo un simple cambio en la biblioteca StdPeriph:

hizo todas las funciones llamadas desde el controlador " static inline " (como deberían haber sido en primer lugar). Con ese cambio -O3 se vuelve mucho más útil.

No entiendo completamente, pero creo que hay un defecto en su concepción. Una transferencia DMA se usa para transmitir un gran bloque de datos, y no para esperar un comando determinado y luego responder; esto se hace mediante interrupciones. Una recepción DMA está diseñada para escuchar continuamente un flujo de datos desde un dispositivo, seguramente sin esperar un solo byte. Por lo tanto, no puede recibir un paquete de datos con una longitud diferente, ya que el búfer DMA debe estar lo suficientemente lleno para activar una interrupción.

EDITAR: Eché un vistazo a tu código y no funciona. Esperar en bucle sin fin el DMA es imposible. Cuando el búfer de recepción está lleno, debe activar una interrupción: una función de devolución de llamada, este es el lugar donde debe evaluar el búfer de recepción. Pero como se dijo, olvídese de recibir DMA. El beneficio de DMA es hacer otras cosas cuando DMA se encarga de enviar/recibir, seguramente no está hecho para esperar en un ciclo sin fin para lograrlo.

Hola Marko. ¿Qué pasaría si en lugar de esos 2 bytes fuera, por ejemplo, un búfer de 2 MB? ¿No sería útil DMA entonces? Para ser más específico, lo que realmente estoy tratando de hacer es tener el esclavo conectado entre un dispositivo que tiene un número finito de estados, en este caso 16 0/1 estados (similar a una máquina de estado), y el maestro. El maestro quiere monitorear continuamente el estado de la máquina de estado (y el esclavo SPI actúa como intermediario) y lo hace, por ejemplo, solicitando esos 16 bits en un bucle, por ejemplo, cada 1 ms. Mi pregunta es ¿cómo mantengo a ese esclavo en este modo de funcionamiento normal cuando el maestro se porta mal?
Con respecto a su edición posterior: sí, tengo la intención de usar DMA con interrupciones más adelante, pero como dije, dado que recién estoy comenzando con la programación STM32, actualmente he usado el ejemplo proporcionado por la biblioteca StdPeriph (ver SPI\SPI_TwoBoards\SPI_DataExchangeDMA - que también fue sin interrupciones) y construir sobre eso en el camino. ¿Está diciendo que con las interrupciones podría hacer que el DMA se active cuando el NSS vuelva a funcionar, incluso si el maestro no envió 2 bytes? Eso solucionaría mi problema.
DMA es realmente necesario en modo esclavo SPI. Las interrupciones para cargar los siguientes datos en el registro Tx pueden tener demasiada latencia a velocidades de datos altas. Además, DMA realmente no se preocupa por la longitud de los datos a transferir.

Supongo que estaba usando una operación sin CS/NSS para poder usar el pin NSS como interrupción (EXTI).

En mi caso necesitaba un hardware NSS. Envié la señal NSS tanto al pin NSS como a un EXTI GPIO. Luego usé esta interrupción para reiniciar el DMA SPI de esta manera:

HAL_SPI_Abort(psHandleSlave->psPeripheral);
__HAL_RCC_SPI2_FORCE_RESET();
__HAL_RCC_SPI2_RELEASE_RESET();

Las líneas 2 y 3 hacen exactamente lo mismo que:

RCC->APB1RSTR |= RCC_APB1RSTR_SPI2RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_SPI2RST;

... pero son proporcionados por HAL.