Los datos SPI llegan cada segundo envío y en la secuencia incorrecta

Estoy simulando un proyecto donde dos STM32 se comunican entre sí a través de SPI (controladores HAL), donde el SPI Master es un STM32F4 y el SPI Slave es un STM32F3. Básicamente, el maestro enviará 5 bytes de datos (1 byte de comando seguido de 4 bytes ficticios) y el esclavo responderá con 4 bytes de datos al maestro según el primer byte de comando recibido.

Cómo debe operar el sistema : al presionar el botón de usuario en el maestro, se enviarán datos SPI (en modo sin interrupción) al esclavo (y el esclavo recibirá los datos en modo de interrupción). El esclavo luego envía datos de vuelta al maestro dependiendo del primer byte recibido. Hay un indicador LED azul en el dispositivo esclavo si se han recibido datos SPI y un indicador LED verde en el maestro que indica si recibe la secuencia correcta de datos del esclavo. A continuación se muestra cómo el maestro debe enviar/recibir los datos sincronizados:

Intercambio de datos SPI sincronizado

Problema del dispositivo esclavo : durante las primeras pulsaciones de botón, el esclavo recibe correctamente los datos del maestro. Después de algunos ciclos, al observar los LED y establecer puntos de interrupción en el modo de depuración, noté que el esclavo comienza a recibir datos después de cada 2 pulsaciones de botón en el maestro. En el dispositivo esclavo, los datos recibidos son correctos. También debo tener en cuenta que la rutina de servicio de interrupción en el maestro se ejecuta cada vez que se presiona un botón, por lo que los datos SPI deben enviarse cada vez que se presiona un botón.

Problema del dispositivo maestro : el maestro nunca recibe la secuencia correcta de datos. Si, por ejemplo, el esclavo envía los datos: 0x01, 0x02, 0x03, 0x04 en ese orden, el maestro recibiría los datos en este orden: 0x02, 0x03, 0x04, 0x01. Además, esto también sucede cada 2 pulsaciones de botón.

Así es como se ve el evento:

  1. presione el botón n:
    • Esclavo: los datos son recibidos correctamente por el esclavo (y envía datos de vuelta al maestro)
    • Maestro: Valor basura recibido
  2. (n+1) pulsación de botón:
    • Esclavo: el esclavo no recibe los datos (y no los devuelve porque el esclavo envía datos en función de los datos recibidos)
    • Maestro: valores relevantes recibidos pero en orden incorrecto
  3. (n+2) pulsación del botón (lo mismo que sucedió en la pulsación del botón n-ésimo):
    • Esclavo: los datos son recibidos correctamente por el esclavo (y envía datos de vuelta al maestro)
    • Maestro: Valor basura recibido y así sucesivamente.

Aquí está el código de configuración SPI para el Maestro:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F4 (Master) */
   /* APB2 Clock set to 8MHz to match APB2 clock of Slave*/

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_MASTER;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
}

La configuración SPI para el Esclavo:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F3 (Slave) */
   /* APB2 Clock is 8MHz */

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_SLAVE;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;
   hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
   hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
   
   /* Set interrupt priority and enable interrupts for SPI1 */
   HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
   HAL_NVIC_EnableIRQ(SPI1_IRQn);
}

Aquí está el código relevante para el maestro (el segmento de interrupción del botón pulsador):

void HAL_GPIO_EXTI_Callback (uint16_t GPIO_Pin) {

   /* Clear wake-up power flag */
   __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);

   if (GPIO_Pin == PUSH_BUTTON_PIN) {
      /* Transmit one command byte located in pTxBuff array of length cTxLen */
      HAL_SPI_Transmit(&hspi1, (uint8_t*)pTxBuff, cTxLen, spi_timeout);

      /* Wait for end of SPI transmission */
      while(HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);

      /* Transmit dummy values located in pTempTxBuff and store retrieved relevant data in pRxBuff */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTempTxBuff, (uint8_t*)pRxBuff, cRxLen, spi_timeout);

      /* Flash LED if correct sequence stored in pRxBuff was received - cRxLen = 4 */
      if((pRxBuff[0] == DATA0) && (pRxBuff[1] == DATA1) && (pRxBuff[2] == DATA2) && (pRxBuff[3] == DATA3)) {
         /* Toggle dedicated green LED */
         ToggleLED();
      }

}

Y el código relevante para el Esclavo:

void HAL_SPI_RxCpltCallback (SPI_HandleTypeDef *hspi) {
   /* Toggle blue LED when SPI receives data through interrupt */
   ToggleLED();

   /* Check the data received through SPI interrupt stored in pRxBuff[0] (array is of size 1) */
   if (pRxBuff[0] == COMMAND_DATA) {

      /**
       * pTxBuff := contains appropriate data to be sent to Master device based on this specific command
       * pTempRxBuff := array to store dummy variables, this array is of size 4
       */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTxBuff, (uint8_t*)pTempRxBuff, cTxLen, spi_timeout);
   }

   /* Set device in SPI Receive interrupt mode again and store received data in pRxBuff of size 1 */
   HAL_SPI_Receive_IT(&hspi1, (uint8_t*)pRxBuff, cRxCmdLen);
}

No entiendo dónde está el error aquí. Si solo el maestro envía los datos al dispositivo esclavo, todo funciona bien. También he hecho coincidir la configuración SPI y las velocidades de reloj del bus. También intenté cambiar la línea de dirección SPI_DIRECTION_1LINEcomo se sugiere en otro lugar, pero no funcionó. También intenté usar HAL_SPI_Transmit()y HAL_SPI_Receive()por separado en lugar de HAL_SPI_TransmitReceive(), pero siguen apareciendo los mismos problemas. Además, me gustaría usar HAL_SPI_TransmitReceive()ambos para verificar en el Maestro y el Esclavo que los dispositivos se comunican correctamente. Cualquier ayuda sería muy apreciada.

¡Gracias!

Parece que puede tener un problema de sincronización entre pruebas o entre la expectativa y la realidad. Considere usar un alcance u obtener un analizador lógico USB económico para ejecutar con sigrok.
No deberías estar haciendo todas esas llamadas SPI adentro HAL_GPIO_EXTI_Callback. Esa devolución de llamada se llama directamente desde el controlador de interrupciones EXTI, por lo que luego está bloqueando las llamadas SPI desde dentro de la interrupción EXTI. En lugar de eso, simplemente configure una volatilebandera desde EXTI y luego, en su ciclo principal, realice las operaciones SPI cuando vea que la bandera se configura.
Lo mismo ocurre con HAL_SPI_RxCpltCallbackel esclavo. No debería estar haciendo el bloqueo HAL_SPI_TransmitReceivedesde dentro de una devolución de llamada (que en realidad todavía está dentro de la interrupción). O use otra volatilebandera allí, o haga HAL_SPI_TransmitReceive_ITsu lugar y haga su HAL_SPI_RxCpltCallbackestado para que recuerde si lo último fue HAL_SPI_TransmitReceive_ITpor 4 bytes o HAL_SPI_Receive_ITpor 1 byte.
También asegúrese de que su interruptor de disparo tenga un rebote adecuado para que no inicie accidentalmente las secuencias de disparo SPI una encima de la otra. Esto es lo primero que pensé al leer tu pregunta.
@ChrisStratton ¿Cuál sería la solución si hay problemas de sincronización? Uno de los problemas de sincronización que podría pensar es que la segunda transmisión del Maestro probablemente comenzaría antes porque el dispositivo Esclavo tardará tiempo en verificar el primer byte de comando enviado por el Maestro. ¿También tenemos que hacer que los relojes del sistema en ambos dispositivos sean iguales? El Maestro está operando actualmente a 16MHz pero el Esclavo está operando a 8MHz. Mi suposición es que debería estar bien porque la transacción SPI sería registrada por el Reloj SPI.
@brhans ¡Gracias por la sugerencia! Intentaré usar banderas volátiles y manejar las tareas en el ciclo while principal. Con respecto al código en sí, ¿la lógica/secuencia es correcta para devolver los datos sincronizados según el comando recibido por el dispositivo maestro? Mi otra preocupación es que la forma en que lo estoy abordando podría no ser correcta.
@MichaelKaras Agregaré el mecanismo de rebote. Creo que tuve un retraso allí, pero lo eliminé mientras realizaba cambios en el código.
@brhans Implementé el segundo enfoque que sugirió en el dispositivo esclavo donde lo hace HAL_SPI_TransmitReceive_ITy agregué estados en la HAL_SPI_RxCpltCallbackfunción. Sin embargo, HAL_SPI_TransmitReceive_ITno parece llamar HAL_SPI_RxCpltCallbackcuando solicita los 4 bytes ficticios del maestro (aunque la interrupción funcionó, ya que recibió los 4 bytes ficticios correctos del maestro y los almacenó en una matriz). ¿Alguna razón por la que esto podría ser el caso?
@brhans También probé HAL_SPI_TransmitReceive_ITcon HAL_SPI_TxRxCpltCallbacky HAL_SPI_TxCpltCallbackpero ninguna de las funciones se activó después de HAL_SPI_TransmitReceive_ITprocesarse con éxito.

Respuestas (2)

En SPI, el MAESTRO y el Esclavo están realmente conectados en un bucle a los búfer FIFO. Como se muestra en la imagen.
Intente vaciar estos búferes de datos y registros de desplazamiento antes de cada transferencia de tramas de datos. En los controladores maestro y esclavo.

Diagrama de comunicación SPI

(Fuente de la imagen: Electronics Hub - Conceptos básicos de la interfaz periférica en serie (SPI) )

Chico incrustado: Hola, según lo exige la regla de este sitio , cuando incluimos algo en una respuesta (por ejemplo, una foto, una imagen o un texto) que no es nuestro trabajo original, debemos mencionarlo (citarlo) correctamente. Esa imagen proviene de otro lugar, por lo que para cumplir con esa regla, debe estar referenciada. Encontré (lo que creo que es) la fuente original y agregué esa referencia para ti esta vez. Por favor, ¿puedes hacer eso en el futuro? ¡Gracias! (Le recomiendo que también lea el recorrido y el centro de ayuda para ver más reglas del sitio).
Embedded Guy: exacto, STM32 tiene búferes SPI fifo que hacen que sea muy fácil tener errores de alineación de datos.

En general, recomiendo enfáticamente no usar HAL para el código de comunicación. Para SPI, UART e I2C, solo funciona realmente en el escenario de 'camino feliz' y, por lo general, no falla con elegancia (problemas permanentes de alineación de datos, etc.). También es muy inflado y lento, lo que puede crear problemas si intenta hacer cosas que requieren mucha velocidad (como analizar un marco SPI mientras lo recibe activamente, como es el caso aquí). El código generado por STM32Cube configura las funciones del controlador de interrupciones en el archivo stm32xx_it.c; puede comentar los controladores HAL generados y poner los suyos propios. ¡Recuerde borrar los indicadores de interrupción antes de que la función regrese!

Algunas ideas sobre lo que está fallando y algunos consejos a medida que continúa trabajando en este proyecto:

  1. No estás usando la HAL correctamente. En realidad, es imposible hacer lo que quieres hacer correctamente usando HAL. Para que el esclavo responda con nuevos datos en función del primer byte recibido del maestro, debe tener un código para analizar el primer byte desde el propio controlador de interrupciones SPI, no desde la devolución de llamada. Las funciones de devolución de llamada HAL se llaman dentro del controlador de interrupciones SPI, después de que se haya recibido un marco completo (es decir, un marco del tamaño esperado con una transición nSS ascendente). Para implementar el sistema como se describe, deberá escribir su propio controlador de interrupciones SPI.

  2. Como señaló Embedded Guy, los periféricos STM32 SPI tienen un búfer FIFO que se puede llenar con basura debido a errores de software (en el esclavo olado maestro), bordes de reloj espurios causados ​​por ruido, etc. Desafortunadamente, no hay un comando explícito de 'borrar fifo' que se pueda emitir desde dentro del propio periférico. La siguiente idea obvia, usar los registros de datos RX/TX para vaciar el FIFO, tampoco es 100 % efectiva (aunque puede ser lo suficientemente buena en la mayoría de los casos). Por ejemplo, en una situación en la que ha recibido 5/8 bits de una palabra, la lectura del registro de datos RX no eliminará este byte incompleto del FIFO. La única forma consistentemente confiable que he encontrado para borrar los búferes SPI FIFO es usar el periférico RCC (controlador de reloj de reinicio) para restablecer completamente el periférico SPI y luego reiniciarlo rápidamente. Afortunadamente esto no es tan terrible como parece,

Consejo final: puede que no sea un buen diseño de sistema intentar que su esclavo analice el primer byte recibido mientras recibe activamente una trama SPI. Esto se debe simplemente a que SPI puede ejecutarse tan rápido que es posible que no haya terminado de analizar el primer byte y de preparar los bytes de respuesta para cuando el maestro haya terminado de cronometrar el primero (tiene un tiempo muymarco de tiempo limitado para hacerlo, ¡solo el ancho de una transición de reloj!). Si su reloj SPI es lento y tiene un analizador muy simple y rápido que se ejecuta dentro del controlador de interrupciones, probablemente pueda salirse con la suya con esta especificación. Sin embargo, podría ser mejor enviar el byte de "comando" en un marco SPI completo y luego emitir un marco de lectura de varios bytes por separado. En este escenario, el esclavo tendría mucho más tiempo para analizar el comando maestro antes de tener que preparar un mensaje de respuesta.