STM32 SPI: comportamiento extraño en TXFIFO vacío (¿historial de bytes anterior?)

El siguiente código configura y habilita SPI2 como esclavo en mi placa STM32F303RE , escribe 0xAA, 0xBB, 0xCC, 0xDD bytes en el registro DR y hace un bucle en un tiempo (1) :

/* Enable clocks for GPIOB (SPI2 pins) and SPI2 peripheral. */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, 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_InitTypeDef gpio_init_struct =
{
    .GPIO_Mode = GPIO_Mode_AF,
    .GPIO_OType = GPIO_OType_PP,
    .GPIO_PuPd = GPIO_PuPd_DOWN,
    .GPIO_Speed = GPIO_Speed_50MHz
};

/* SPI NSS pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_12;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI SCK pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_13;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI MISO pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_14;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI  MOSI pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_15;
GPIO_Init(GPIOB, &gpio_init_struct);

SPI_InitTypeDef spi_init_struct =
{
    .SPI_Direction          = SPI_Direction_2Lines_FullDuplex,
    .SPI_Mode               = SPI_Mode_Slave,
    .SPI_DataSize           = SPI_DataSize_8b,
    .SPI_CPOL               = SPI_CPOL_Low,
    .SPI_CPHA               = SPI_CPHA_1Edge,
    .SPI_NSS                = SPI_NSS_Hard,
    .SPI_BaudRatePrescaler  = SPI_BaudRatePrescaler_2,
    .SPI_FirstBit           = SPI_FirstBit_MSB,
    .SPI_CRCPolynomial      = 7
};

SPI_I2S_DeInit(SPI2);
SPI_Init(SPI2, &spi_init_struct);
SPI_CalculateCRC(SPI2, DISABLE);
SPI_TIModeCmd(SPI2, DISABLE);
SPI_NSSPulseModeCmd(SPI2, DISABLE);

SPI_Cmd(SPI2, ENABLE);
SPI_SendData8(SPI2, (uint8_t) 0xAA);
SPI_SendData8(SPI2, (uint8_t) 0xBB);
SPI_SendData8(SPI2, (uint8_t) 0xCC);
SPI_SendData8(SPI2, (uint8_t) 0xDD);

while(1) { }

Con un maestro que solicita 2 bytes por selección de chip , el maestro recibe:

0xAA 0xBB
0xCC 0xDD
0xAA 0xAA -----> TXFIFO should be empty here, why not "0x00 0x00"?
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
......... (0xAA 0xAA infinite times)

Habría esperado que el maestro recibiera "0x00 0x00" después de que TXFIFO se vacíe . ¿ Por qué obtengo " 0xAA 0xAA " continuamente en su lugar? No pude encontrar algo que apuntara a tal comportamiento en el manual.

ACTUALIZAR 1

Esperar a que las transacciones terminen justo antes del tiempo (1) y luego escribir ceros en el SPI , así:

while(SPI_GetTransmissionFIFOStatus(SPI2) != SPI_TransmissionFIFOStatus_Empty) { }
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) != RESET) { }

#define ZEROS_CNT   (1)
for(int i = 0; i < ZEROS_CNT; i++)
    SPI_SendData8(SPI2, 0);

while(1) { }

representa el siguiente comportamiento maestro para diferentes valores de ZEROS_CNT:

ZEROS_CNT = 0 => master receives after TXFIFO is empty: 0xAA infinitely
ZEROS_CNT = 1 => master receives after TXFIFO is empty: 0x00 1 times, followed by 0xBB infinitely
ZEROS_CNT = 2 => master receives after TXFIFO is empty: 0x00 2 times, followed by 0xCC infinitely
ZEROS_CNT = 3 => master receives after TXFIFO is empty: 0x00 3 times, followed by 0xDD infinitely
ZEROS_CNT >= 4 => master receives after TXFIFO is empty: 0x00 infinitely

Parece como si el periférico SPI tuviera algún tipo de historial de lo que se escribió en el TXFIFO y cuando se vacía, envía bytes de ese historial.

ACTUALIZAR 2

Se comporta igual independientemente de cuántos bytes solicite el maestro en una sola selección de chip. He intentado pedir 1, 2, 4 y 5 a la vez.

Esto parece bastante extraño. ¿Intentó deshabilitar el SPI después de enviar 0xdd?
Hola Vladimir, sí, otra cosa extraña es que una vez deshabilitado, enviará "0x00 0x00" - pero si se vuelve a habilitar, comenzará a enviar "0xAA 0xAA" nuevamente... dos soluciones para esto serían deshabilitarlo hasta que tenga algo nuevo para enviar -O- reiniciando el periférico por completo (SPI_I2S_DeInit(SPI2)). Sin embargo, me gustaría saber por qué se comporta de esta manera (y con qué lógica) y si puedo evitar hacer un reinicio completo (que también requiere reconfigurar el periférico, algo superfluo).
¿Le importaría agregar el enlace a la nota/hoja de datos/lo que sea relevante? No puedo investigarlo ahora, pero podría estar de vuelta en mi mente esta noche.
¿Te refieres al manual de referencia STM32? Para mi placa STM32F303RE, aquí está: st.com/content/ccc/resource/technical/document/reference_manual/…
¿Has probado a esperar a que lleguen los datos recibidos?
@domen Consulte la ACTUALIZACIÓN
¿Qué? No. Espere en RX de datos SPI. Jugar con GPIO mientras se depura el problema de SPI es buscar problemas (y soluciones pirateadas que podrían funcionar).
Tengo un analizador lógico, lo anterior funciona correctamente. Cámbielo esperando en SPI RX (por cierto, ¿por qué esperar en RX y no hasta que TXFIFO esté vacío y BSY no esté configurado en su lugar?) y verá que se comporta de la misma manera. :)
Hola @domen, lo siento, acababa de apagar mi Raspberry Pi 2 (maestro) antes y no tuve tiempo de cambiar el código. Actualizada la pregunta. Como esperaba, esperar a que TXFIFO se vacíe y BSY se desactive genera el mismo comportamiento.
DE ACUERDO. Creo que debe esperar a que TXFIFO esté vacío antes de cada SPI_SendData8. Aunque para ser justos, no estoy seguro de cómo se relaciona esto. 0xaa (10101010) parece que el reloj podría inducirse en la línea de datos. ¿Cuánto tiempo son sus cables? ¿Qué hay en los registros de estado? Supongo que también es posible que tome un byte aleatorio cuando no hay nada nuevo en el registro TX.
el reloj es dos veces más rápido que los datos, por lo que si ocurriera un acoplamiento (muy malo), esperaría 0x00 o 0xff, dependiendo de la polaridad del canal.
@domen No son para nada aleatorios. El experimento descrito debería dejar eso claro. Esperar a que TXFIFO esté vacío después de cada byte no es una opción (anula el propósito de FIFO) y de todos modos se comporta igual.

Respuestas (2)

Me di cuenta de esto y decidí hacer esta respuesta más completa creando algunas animaciones. En primer lugar, hay 2 hechos a tener en cuenta que determinan la lógica detrás del comportamiento de TXFIFO:

  • Como dice el manual, TXFIFO tiene un tamaño de 32 bits = 4 bytes ; deje que esos bytes sean B1 | B2 | B3 | B4 en ese orden.
  • HECHO #1 : Cuando el maestro solicita un byte del esclavo, el byte extraído y devuelto es B1 , pero en realidad no se elimina de TXFIFO ---> los contenidos de TXFIFO se giran a la izquierda y no se desplazan a la izquierda . Esto da como resultado que TXFIFO se vea como B2|B3|B4|B1 después de este pop, en lugar de B2|B3|B4|00 (que creo que es lo que @ogrenci quiso decir con su respuesta).
  • HECHO #2 : Cuando TXFIFO se vacía, por alguna razón una solicitud maestra recibe el primer byte que está en TXFIFO en ese momento en lugar de 0x00 como hubiera esperado.

RXFIFO probablemente se comporte de la misma manera.

El algoritmo para push/pop , escrito en C# es el siguiente:

public class TXFIFO
{
    public byte[] data;
    byte push_position = 1;
    byte occupied = 0;

    public TXFIFO()
    {
        data = new byte[4];
    }

    public byte Push(byte v)
    {
        // write
        data[push_position - 1] = v;
        // push_position
        if (push_position < 4) push_position++;
        else push_position = 1;
        // occupied
        if (occupied < 4) occupied++;
        return v;
    }

    public byte Pop()
    {
        // read
        if (occupied == 0) return data[0];
        byte v = data[0];
        // rotate left once
        for (int i = 1; i < 4; i++)
            data[i - 1] = data[i];
        data[3] = v;
        //push_position
        if (push_position > 1) push_position--;
        else push_position = 4;
        //occupied
        if (occupied > 0) occupied--;
        return v;
    }

    public byte GetOccupied()
    {
        return occupied;
    }
}

Y aquí hay 5 animaciones que ilustran los escenarios ZEROS_CNT descritos originalmente (ver la ACTUALIZACIÓN 1 de la pregunta ). Tenga en cuenta que, para aclarar el punto, en lugar de insertar ceros , inserté valores 0x01-0x02-..to..-ZEROS_CNT aquí.

CEROS_CNT = 0:

ingrese la descripción de la imagen aquí

CEROS_CNT = 1:

ingrese la descripción de la imagen aquí

CEROS_CNT = 2:

ingrese la descripción de la imagen aquí

CEROS_CNT = 3:

ingrese la descripción de la imagen aquí

CEROS_CNT = 4:

ingrese la descripción de la imagen aquí

CEROS_CNT = 5:

ingrese la descripción de la imagen aquí

...etcétera...

Como se mencionó anteriormente en la pregunta, una solución para enviar 0x00 cuando TXFIFO está vacío sería mantener el periférico SPI deshabilitado cuando TXFIFO está vacío hasta que se escriban nuevos datos en DR , que es lo que terminé haciendo, después de entender lo que está pasando. .

De hecho, es probable que los datos no se copien realmente en el búfer, pero se mantienen dos índices para el inicio y el final de los datos almacenados en el búfer. Cuando el inicio y el final apuntan al mismo elemento del búfer, eso puede significar una de dos cosas: el búfer está completamente lleno o completamente vacío. No hay forma de determinar cuál es sin mantener otra bandera en alguna parte. Consulte Zona de influencia circular .
@m.Alin gracias! Espero que aclaren las cosas. JimmyB no está seguro de lo que está tratando de decir: el SPI retiene información que le indica cuál es el nivel de ocupación actual de los FIFO.
@JimmyB ooh, entiendo. Sí, lo más probable es que tengas razón. Sería demasiado ineficiente seguir desplazando elementos hacia la izquierda al hacer un pop. De todos modos, creo que los resultados de las operaciones pop/push anteriores generarían los mismos valores. Supongo que uno podría tratar de entender cómo ese esquema de cambio en el diagrama de bloques SPI realmente funciona a nivel electrónico para descubrir realmente cómo se realizan estas operaciones internamente (la estructura de datos utilizada).
Solo quería proporcionar una explicación simple de por qué un determinado elemento parece permanecer en el búfer. De hecho, si el dispositivo sabe cuándo el búfer está vacío, el circuito SPI podría/debería usar ese bit de información para proporcionar una salida consistente (0x00) en lugar de basura "aleatoria".
@JimmyB sí, gracias por el aporte y lo siento, no entendí lo que intentabas sugerir la primera vez;)
Pregunta tonta: ¿qué usaste para las animaciones?
Hola @pgvoorhees. En realidad, nada especial, solo un poco de C# (mi clase TXFIFO tiene un método Draw que devuelve una instancia de mapa de bits en la que dibujó su estado (consulte Clases de mapa de bits y gráficos). Construiría un escenario (secuencia de pulsaciones/pops) y llamaría Dibujar después de cada operación. Guardé los mapas de bits en archivos de imagen y usé gifcreator.me para crear los GIF. Pero lo más probable es que haya mejores herramientas para hacer esto en lugar de reinventar la rueda, simplemente no tenía la disposición para buscarlos, ya que tenía otro trabajo que hacer.

Los valores después del 4º byte no provienen del búfer TX del esclavo. Examine el diagrama de bloques SPI. El maestro los envía originalmente como datos de volcado cuando se cicla el SCI y regresan del registro de desplazamiento del esclavo y aparecen como datos válidos. Cuando el maestro extrae datos, eso es lo que se esperaba que se anulara, pero no lo hace en este caso porque el esclavo no inserta datos en el búfer de TX y carga el registro de desplazamiento.

Hola, ogrenci, ¿quisiste decir "enviado como datos de volcado cuando el SPI es ciclado por Raspberry PI"?
Hola @CorneliuZuzu. Me refiero a Raspberry PI. Edité la respuesta.