Interfaz SPI en Xilinx FPGA, dominios de reloj y restricciones de tiempo

Estoy conectando una placa Raspberry Pi a una placa de desarrollo con un Spartan 6. Quiero hacer esto usando SPI. Debido a la forma en que está diseñada la placa de desarrollo, necesito conectar SPI CLK y DATA a los pines IO estándar.

Soy consciente de la necesidad de cruzar dominios de reloj con doble búfer para protegerse contra la metaestabilidad. El RPi y el SPI CLK están obviamente en un dominio separado del tejido FPGA interno. No veo demasiado problema: solo un registro de 8 bits y la señal que dice cuando un byte está listo deben sincronizarse con el reloj de estructura interno. No estoy tratando de obtener altas tasas de datos. Solo se escribirá un byte cada 25 us (esto se debe a que el RPi es lento para leer un GPIO, pero no hay problema para este proyecto). Estoy pensando en sincronizar el SPI a 15 MHz, e incluso podría reducirlo si fuera necesario.

Este es mi verilog. Simula y prueba bien en banco.

module my_spi_in (
  // RPI clock domain
  input i_RPI_spi_data,
  input i_RPI_spi_clk,
  input i_RPI_reset,
  // internal 64MHz domain
  input i_sys_clk,
  output [7:0] o_data,
  output o_fifo_write
);

  // registers in RPI clock domain
  reg [7:0] r_RPI_shift_in = 8'b0;
  reg [2:0] r_RPI_ctr = 3'b0;
  reg r_RPI_word_done = 1'b0;

  // synchronisation registers
  reg [7:0]r_data_sync_1 = 8'b0;
  reg [7:0]r_data_sync_2 = 8'b0;
  reg [2:0] r_word_done_sync = 3'b0;

  // RPI clock domain : input shift register logic
  always @ (posedge i_RPI_spi_clk, posedge i_RPI_reset) begin
    if (i_RPI_reset == 1'b1) begin
      r_RPI_shift_in <= 8'b0;
      r_RPI_ctr <= 3'b0;
    end else begin
      r_RPI_ctr <= r_RPI_ctr + 1'b1;
      r_RPI_shift_in <= {i_RPI_spi_data, r_RPI_shift_in[7:1]};
    end
  end

  // RPI clock domain : word done
  always @ (negedge i_RPI_spi_clk) begin
    if (~i_RPI_reset && r_RPI_ctr == 3'b000) r_RPI_word_done <= 1'b1;
    else r_RPI_word_done <= 1'b0;
  end

  // sync registers
  always @ (posedge i_sys_clk) begin
    r_data_sync_1 <= r_RPI_shift_in;
    r_data_sync_2 <= r_data_sync_1;
    r_word_done_sync[0] <= r_RPI_word_done;
    r_word_done_sync[1] <= r_word_done_sync[0];
    r_word_done_sync[2] <= r_word_done_sync[1];
  end

  assign o_data = r_data_sync_2;
  assign o_fifo_write = r_word_done_sync[1] && ~r_word_done_sync[2];
endmodule

En mi archivo .ucf solo tengo lo siguiente, para decirle a ISE que este no es un reloj "real" (no se construirá sin esto):

NET "i_RPI_spi_clk" CLOCK_DEDICATED_ROUTE = FALSE;
NET "i_RPI_reset" CLOCK_DEDICATED_ROUTE = FALSE;

Mi pregunta: ¿es este el mejor enfoque? ¿Necesito hacer algo más? (Idealmente, sería bueno establecer también algunas restricciones de tiempo para el reloj y los datos SPI, para que las herramientas sean conscientes de la velocidad de la interfaz SPI).

Gracias de antemano por tu consejo.

EDITAR: Debo dejar en claro que el RPi solo está transfiriendo un solo byte antes de verificar un pin GPIO. Esto resulta ser lento (toma alrededor de 25 us), por lo que nunca hay dos bytes consecutivos en el bus SPI. Hay actividad SPI durante unos 0,5 us (un byte a 15 MHz), luego no pasa nada durante unos 24 us hasta que el RPi ha leído el GPIO. Obviamente, esto es mucho más lento de lo que SPI es capaz de hacer: el tiempo de lectura de RPi está ralentizando bastante la transferencia, pero esto es bastante aceptable para este sistema.

Respuestas (2)

El enfoque habitual es cruzar MOSI, CS y SCLK al dominio de reloj de estructura FPGA interno (funcionando a una velocidad mucho más alta que el bus SPI) y hacer todo el trabajo allí.

Cruzar un dominio de reloj con una salida de registro paralelo tiene al menos la posibilidad inherente de un estado inválido donde hacerlo con un bus serie realmente no lo hace. Esto se debe a que es posible que su filtro de metaestabilidad registre diferentes niveles en diferentes bits si varios bits cambian de estado dentro de la ventana de configuración o retención. Además, llevar la transmisión en serie al dominio del reloj central le permite hacer cosas como implementar fácilmente filtros de falla, lo que puede valer la pena tener.

Este también es un buen consejo. Mi respuesta aborda una solución más directa al código existente.
Gracias por esto. Mi reloj interno es de solo 64 MHz y no quiero aumentarlo particularmente. ¿Supongo que quiero que sea al menos 8 o 10 veces más alta que la tasa SPI? De hecho, la salida de la palanca de cambios tendrá algunos valores no válidos si se lee en el momento equivocado, pero hay una señal para evitar que eso suceda. También podría agregar un registro más para evitar eso. Pero "es posible que su filtro de metaestabilidad registre diferentes niveles en diferentes bits si varios bits cambian de estado dentro de la ventana de configuración o retención", esto no lo entiendo del todo. ¿Podría explicar por favor? muchas gracias.
Podría reducir la velocidad SPI a aproximadamente 4 MHz y probar CLK/MOSI con un reloj de tela de 64 MHz. La tasa de datos apenas se ve afectada y el aumento en la simplicidad es bastante atractivo. Gracias.
Bien, veo la idea. De hecho, así puedo evitar el cronometraje con el SPI CLk por completo, al usar dos de las señales en los registros para detectar el borde. ¡Gracias! gran idea.
@dmb Recuerde siempre que solo puede sincronizar de manera confiable un solo bit a través de un límite de reloj. Esta es la misma razón por la que se usa el código gris para punteros en FIFO asíncronos. En su caso, sincroniza un/el bit de control con el dominio del reloj receptor y luego lo usa como una habilitación para capturar las otras señales en el nuevo dominio.
para aclarar (y asegurarme de que entiendo): si ejecutamos un bus paralelo a través de registros de sincronización, no podemos garantizar que todos se sincronizarán en el mismo ciclo del reloj de sincronización, ¿verdad? Sin embargo, si están calificados por alguna otra señal (syncd) que ocurre (digamos) 3 ciclos de reloj (del reloj de sincronización) después de que cambian, todos deberían estar sincronizados para entonces. (Tal vez esto no sea una buena práctica, pero es bueno entender lo que está pasando y por qué). Muchas gracias a todos. Excelente ayuda y consejo.

No, tiene una idea completamente equivocada cuando se trata de transferir un bus de bits múltiples a través de un límite de dominio de reloj.

Aquí, el problema no es la metaestabilidad, sino más bien muestrear los bits en el bus en un momento en que se sabe que no están cambiando, de modo que siempre obtenga un valor autoconsistente.

Por lo tanto, es correcto sincronizar y retrasar la r_RPI_word_doneseñal antes de realizar la detección de bordes, pero NO es correcto pasar los datos a través de múltiples registros.

Su reloj interno es varias veces más rápido que el reloj SPI (¿verdad?), por lo que cuando se o_fifo_writeproduce el pulso, SABES que los bits de datos son estables y se pueden muestrear de manera segura. No necesita los registros r_data_sync_1y r_data_sync_2, y debe directamente

assign o_data = r_RPI_shift_in;

De hecho, retrasar los datos es muy contraproducente, porque prácticamente garantiza que está muestreando los datos en un momento en que está cambiando, lo que resulta en la captura de algunos bits de una palabra y algunos bits de la siguiente palabra.

Está bien, gracias. De hecho, debido a que cada byte está separado por un largo tiempo inactivo (la transferencia de un byte toma alrededor de 0.5us, hay alrededor de 24us antes del siguiente), pero de hecho no tiene sentido registrar los datos como usted indica.