Trabajo práctico con FPGAs en el conjunto de Redd. Dominar DMA para el bus Avalon-ST y cambiar entre buses Avalon-MM

Continuamos avanzando hacia la creación de dispositivos reales basados ​​en el complejo Redd FPGA. Para otro proyecto de todo el hardware, necesito un analizador lógico simple, por lo que avanzaremos en esta dirección. Lucky - y ve al analizador de bus USB (pero esto aún es a largo plazo). El corazón de cualquier analizador es la RAM y una unidad que primero carga datos y luego los recupera. Hoy lo diseñaremos.

Para hacer esto, dominaremos el bloque DMA. En general, DMA es mi tema favorito. Incluso hice un gran artículo sobre DMA en algunos controladores ARM . De ese artículo queda claro que DMA toma ciclos de reloj del bus. En el artículo actual, consideraremos cómo van las cosas con el sistema de procesador basado en FPGA.




Creación de hardware


Comenzamos a crear el hardware. Para comprender cuánto entra en conflicto el bloque DMA con los ciclos de reloj, tendremos que tomar medidas precisas a alta carga en el bus Avalon-MM (Avalon Memory-Mapped). Ya hemos descubierto que el puente Altera JTAG-to-Avalon-MM no puede proporcionar grandes cargas de autobús. Por lo tanto, hoy tenemos que agregar un núcleo de procesador al sistema para que acceda al bus a alta velocidad. Cómo se hace esto se ha descrito aquí . En aras de la optimización, desactivemos ambas memorias caché para el núcleo del procesador, pero creemos un bus fuertemente conectado, como lo hicimos aquí .



Agregue 8 kilobytes de memoria de programa y memoria de datos. Recuerde que la memoria debe ser de doble puerto y tener una dirección en un rango especial (para que no salte, bloquéela, discutimos las razones de todo esto aquí ).



Ya hemos creado el proyecto miles de veces, por lo que no hay nada particularmente interesante en el proceso de creación en sí (si acaso, todos los pasos para crearlo se describen aquí ).

La base está lista. Ahora necesitamos una fuente de datos que guardaremos en la memoria. Lo ideal es un temporizador constantemente. Si durante alguna medida el bloque DMA no pudo procesar los datos, lo veremos inmediatamente por el valor faltante. Bueno, es decir, si en la memoria hay valores 1234 y 1236, significa que en el reloj, cuando el temporizador emitió 1235, el bloque DMA no transfirió datos. Crear un archivoTimer_ST.sv con un contador tan simple:

module Timer_ST (
  input              clk,
  input              reset,
	
  input  logic       source_ready,
  output logic       source_valid,
  output logic[31:0] source_data
	
);
    logic [31:0] counter;
    always @ (posedge clk, posedge reset)
    if (reset == 1)
    begin
        counter <= 0;
    end else
    begin
        counter <= counter + 1;
    end

    assign source_valid = 1;
    assign source_data [31:24] = counter [7:0];
    assign source_data [23:16] = counter [15:8];
    assign source_data [15:8] = counter [23:16];
    assign source_data [7:0] = counter [31:24];

endmodule

Este contador es como un pionero: siempre está listo (en la salida source_valid siempre es uno) y siempre cuenta (excepto en los momentos del estado de reinicio). ¿Por qué el módulo tiene exactamente estas señales? Lo discutimos en este artículo .

Ahora creamos nuestro propio componente ( aquí se describe cómo se hace esto ). Automatización eligió por error el bus Avalon_MM para nosotros. Reemplácelo con avalon_streaming_source y mapee las señales como se muestra a continuación:



Genial. Agregue nuestro componente al sistema. Ahora estamos buscando el bloque DMA ... Y encontramos no uno, sino tres. Todos ellos se describen en el documento Embedded Peripheral IP User Guide de Altera (como siempre, doy nombres, pero no enlaces, ya que los enlaces siempre cambian).



¿Cuál usar? No puedo resistir la nostalgia. En 2012, hice un sistema basado en el bus PCIe. Todos los manuales de Altera contenían un ejemplo basado en el primero de estos bloques. Pero él con el componente PCIe dio una velocidad de no más de 4 megabytes por segundo. En esos días, escupí y escribí mi bloque DMA. Ahora no recuerdo su velocidad, pero llevó los datos de las unidades SATA al límite de las capacidades de las unidades y los SSD de aquellos tiempos. Es decir, he afilado un diente en este bloque. Pero no me deslizaré en una comparación de los tres bloques. El hecho es que hoy tenemos que trabajar con una fuente basada en Avalon-ST (Avalon Streaming Interface), y solo el bloque Modular Scatter-Gather DMA admite tales fuentes . Aquí lo ponemos en el diagrama.

En la configuración de bloque, seleccione el modoTransmisión a memoria asignada . Además: quiero conducir los datos desde el lanzamiento hasta el llenado de SDRAM, así que reemplacé la unidad de transferencia de datos máxima de 1 kilobyte a 4 megabytes. Es cierto, me advirtieron que al final, el parámetro FMax no estará tan caliente (incluso si reemplaza el bloque máximo con 2 kilobytes). Pero por hoy, FMax es aceptable (104 MHz), y luego lo resolveremos. Dejé los parámetros restantes sin cambios. También puede establecer el modo de transmisión en Acceso completo a palabras solamente, esto aumentará FMax a 109 MHz. Pero hoy no lucharemos por el rendimiento.



Entonces. La fuente es, DMA es. Receptor ... SDRAM? En futuras condiciones de combate, sí. Pero hoy necesitamos un recuerdo con características conocidas. Desafortunadamente, SDRAM necesita enviar periódicamente comandos que toman varios ciclos de reloj, además esta memoria puede ser ocupada por la regeneración. Por lo tanto, en lugar de eso, ahora utilizaremos la memoria FPGA incorporada. Todo está funcionando para ella en un solo paso, sin demoras impredecibles.

Dado que el controlador SDRAM es de puerto único, la memoria integrada también se puede usar exclusivamente en modo de puerto único. Es importante. El hecho es que queremos escribir en la memoria usando el maestro de bloque DMA, pero por otro lado, queremos leer desde esta memoria usando el núcleo del procesador o el bloque Altera JTAG-to-Avalon-MM. La mano se extiende y conecta los bloques de escritura y lectura a dos puertos diferentes ... ¡Pero no puedes! Más bien, está prohibido por las condiciones del problema. Porque hoy es posible, pero mañana reemplazaremos la memoria por una de un solo puerto. En general, obtenemos un bloque de tres componentes (temporizador, DMA y memoria):



Bueno, y solo para pro forma, agregaré UT y sysid al sistema JTAG (aunque el segundo no ayudó, aún así tuve que conjurar con un adaptador JTAG). Ya hemos estudiado qué es y cómo su adición resuelve pequeños problemas. No teñiré los neumáticos, todo está claro con ellos. Solo muestra cómo se ve todo en mi proyecto:



eso es todo. El sistema esta listo. Asignamos direcciones, asignamos vectores de procesador, generamos un sistema (no olvide que debe guardar con el mismo nombre que el proyecto en sí, luego irá al nivel superior de la jerarquía), agregarlo al proyecto. Haga que el tramo de restablecimiento sea virtual, conecte clk al pin_25 tramo. Estamos armando el proyecto, vertiéndolo en el equipo ... ¿Cómo está, pobrecito, en la oficina vacía debido a la ubicación remota total? ... Es solitario y aterrador para ella, probablemente, solo ... Pero estaba distraído.

Crear una parte de software


Formación


En el Editor BSP con el movimiento habitual de la mano, enciendo el soporte de C ++. A menudo inserto una captura de pantalla de este caso que dejo de hacerlo. Pero otra captura de pantalla, aunque ya se ha visto, sigue siendo tan común. Así que hablemos una vez más. Recordamos que el sistema está tratando de poner datos en la memoria más grande. Y así es Buffer . Por lo tanto, forzamos todo a los datos :



Programa experimento


Creamos un código que simplemente llena la memoria con el contenido de la fuente (en el papel del contador).

Ver código
#include "sys/alt_stdio.h"
#include <altera_msgdma.h>
#include <altera_msgdma_descriptor_regs.h>
#include <system.h>
#include <string.h>

int main()
{ 
  alt_putstr("Hello from Nios II!\n");

  memset (BUFFER_BASE,0,BUFFER_SIZE_VALUE);

  //  ,   
  IOWR_ALTERA_MSGDMA_CSR_CONTROL(MSGDMA_0_CSR_BASE,
      ALTERA_MSGDMA_CSR_STOP_DESCRIPTORS_MASK);

  //   ,      ,
  //    ,   .  .

  //    FIFO
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_READ_ADDRESS(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      (alt_u32)0);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_WRITE_ADDRESS(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      (alt_u32)BUFFER_BASE);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_LENGTH(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      BUFFER_SIZE_VALUE);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_CONTROL_STANDARD(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      ALTERA_MSGDMA_DESCRIPTOR_CONTROL_GO_MASK);

   //  ,    
   IOWR_ALTERA_MSGDMA_CSR_CONTROL(MSGDMA_0_CSR_BASE,
       ALTERA_MSGDMA_CSR_STOP_ON_ERROR_MASK
       & (~ALTERA_MSGDMA_CSR_STOP_DESCRIPTORS_MASK)
       &(~ALTERA_MSGDMA_CSR_GLOBAL_INTERRUPT_MASK)) ;


   //   
   static const alt_u32 errMask = ALTERA_MSGDMA_CSR_STOPPED_ON_ERROR_MASK |
           ALTERA_MSGDMA_CSR_STOPPED_ON_EARLY_TERMINATION_MASK |
           ALTERA_MSGDMA_CSR_STOP_STATE_MASK |
           ALTERA_MSGDMA_CSR_RESET_STATE_MASK;

  volatile alt_u32 status;
  do
  {
     status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));     

  alt_putstr("You can play with memory!\n");

  /* Event loop never exits. */
  while (1);

  return 0;
}


Comenzamos, esperamos el mensaje "¡Puedes jugar con la memoria!" , puse el programa en pausa y mire la memoria, comenzando desde la dirección 0. Al principio tenía mucho miedo:



desde la dirección 0x80, el contador cambia su valor bruscamente. Por otra parte, una cantidad muy grande. Pero resultó que todo está bien. En nuestro lugar, el contador nunca se detiene y siempre está listo, y DMA tiene su propia cola de lectura anticipada. Permíteme recordarte la configuración del bloque DMA:



0x80 bytes son 0x20 palabras de treinta y dos bits. Solo 32 decimales. Todo encaja. En condiciones de depuración, esto no da miedo. En condiciones de combate, la fuente funcionará más correctamente (se restablecerá su disponibilidad). Por lo tanto, simplemente ignoramos esta sección. En otras áreas, el medidor cuenta secuencialmente. Mostraré solo el fragmento de volcado en ancho. Tome una palabra que lo examiné en su totalidad.



Sin confiar en mis ojos, escribí un código que verifica automáticamente los datos:

  volatile alt_u32* pData = (alt_u32*)BUFFER_BASE;
  volatile alt_u32 cur = pData[0x10];
  int nLine = 0;
  for (volatile int i=0x11;i<BUFFER_SIZE_VALUE/4;i++)
  {
	  if (pData[i]!=cur+1)
	  {
		  alt_printf("Problem at 0x%x\n",i*4);
		  if (nLine++ > 10)
		  {
			  break;
		  }
	  }
	  cur = pData[i];
  }

Tampoco revela ningún problema.

Tratando de encontrar al menos algunos problemas


De hecho, la ausencia de problemas no siempre es buena. Como parte del artículo, necesitaba encontrar los problemas y luego mostrar cómo se solucionan. Después de todo, los problemas son obvios. ¡Un autobús ocupado no puede pasar datos sin demoras! ¡Debería haber retrasos! Pero verifiquemos por qué todo sucede tan bien. En primer lugar, puede resultar que todo esté en el FIFO del bloque DMA. Reduzca su tamaño al mínimo: ¡



Todo sigue funcionando! Bueno. Asegúrese de provocar el número de accesos al bus más que la dimensión FIFO. Añadir un contador de visitas:



Mismo texto:
  volatile alt_u32 status;
  volatile int n = 0;
  do
  {
	  status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
	  n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));  


Al final del trabajo, son 29. Esto es más de 16. Es decir, el FIFO debería desbordarse. Por si acaso, agreguemos más lecturas de registro de estado. No ayuda.

Con pena, me desconecté del complejo remoto de Redd, rehice el proyecto a mi placa de pruebas existente, a la que puedo conectarme con un osciloscopio en este momento (en la oficina, nadie está a distancia, no puedo alcanzar el osciloscopio). Se agregaron dos puertos al temporizador:

   output    clk_copy,
   output    ready_copy

Y los nombró:

    assign clk_copy = clk;
    assign ready_copy = source_ready;

Como resultado, el módulo comenzó a verse así:

module Timer_ST (
   input           clk,
   input           reset,
	
   input logic     source_ready,
   output logic    source_valid,
   output logic[31:0] source_data,

   output    clk_copy,
   output    ready_copy
	
);
    logic [31:0] counter;
    always @ (posedge clk, posedge reset)
    if (reset == 1)
    begin
        counter <= 0;
    end else
    begin
        counter <= counter + 1;
    end

    assign source_valid = 1;
    assign source_data [31:24] = counter [7:0];
    assign source_data [23:16] = counter [15:8];
    assign source_data [15:8] = counter [23:16];
    assign source_data [7:0] = counter [31:24];

    assign clk_copy = clk;
    assign ready_copy = source_ready;

endmodule

En casa tengo un modelo más pequeño con un cristal, así que tuve que reducir el apetito de la memoria. Y resultó que mi programa primitivo no encajaría en una sección de 4 kilobytes. Entonces, el tema planteado en el último artículo es, oh, cuán relevante. Memoria en el sistema: ¡apenas suficiente!

Cuando se inicia el programa, nos dan una lista oleada de 16 o 17 medidas. Esto se llena con el FIFO del bloque DMA. El mismo efecto que me asustó al principio. Son estos datos los que formarán el relleno de búfer muy falso.



A continuación, tenemos una hermosa imagen a 40960 nanosegundos, es decir, 2048 ciclos (con un cristal doméstico, el búfer tuvo que reducirse a 8 kilobytes, es decir, 2048 palabras de treinta y dos bits). Aquí está su comienzo:



Aquí está el final:



Bueno, y en todo momento, ni una sola falla. No, estaba claro que esto sucedería, pero había algo de esperanza ... ¿

Tal vez deberíamos intentar escribir en el autobús, y no solo leerlo? Agregué un bloque GPIO al sistema: agregué una



entrada mientras esperaba la preparación:



Mismo texto
  volatile alt_u32 status;
  volatile int n = 0;
  do
  {
	status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
       IOWR_ALTERA_AVALON_PIO_DATA (PIO_0_BASE,0x01);
       IOWR_ALTERA_AVALON_PIO_DATA (PIO_0_BASE,0x00);

       n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));  


No hay problemas y eso es todo! ¿A quién culpar?

No hay milagros, pero hay cosas inexploradas.


¿A quién culpar? Con pena, comencé a estudiar todos los menús de la herramienta Platform Designer y, al parecer, encontré una pista. ¿Cómo se ve un neumático por lo general? El conjunto de cables a los que están conectados los clientes. ¿Entonces? Así parece. Esto lo vemos en las figuras del editor. Simplemente, el segundo objetivo del artículo era mostrar cómo se puede dividir el autobús en dos segmentos independientes, cada uno de los cuales funciona sin interferir con el otro.

Pero veamos los mensajes que aparecen cuando se genera el sistema. Resaltar palabras clave:



Y hay muchos mensajes similares: se agrega, se agrega. Resulta que después de editar con bolígrafos, se agregan muchas cosas adicionales al sistema. ¿Cómo verías un esquema en el que todo esto ya está disponible? Todavía estoy nadando en esto, pero podemos obtener la respuesta más probable dentro del marco del artículo eligiendo este elemento del menú: la



imagen abierta es impresionante en sí misma, pero no la daré. E inmediatamente seleccionaré esta pestaña:



Y allí veremos lo siguiente:



Mostraré más grande lo más importante:



¡Los neumáticos no están combinados! ¡Están segmentados! No puedo justificarlo (quizás los expertos me corregirán en los comentarios), ¡pero parece que el sistema insertó los interruptores por nosotros! Son estos interruptores los que crean los segmentos de bus aislados, y el sistema principal puede funcionar en paralelo con la unidad DMA, que en este momento puede acceder a la memoria sin conflictos.

Provocamos problemas reales


Habiendo recibido todo este conocimiento, concluimos que podemos muy bien provocar problemas. Esto es necesario para asegurarse de que el sistema de prueba pueda crearlos, lo que significa que el entorno de desarrollo realmente los resuelve de forma independiente. No nos referiremos a dispositivos abstractos en el bus, sino a la misma memoria Buffer para que el bloque cmd_mux_005 distribuya el bus entre el núcleo del procesador y el bloque DMA. Reescribimos la función de espera sufrida así:



Mismo texto
  volatile alt_u32 status;
  volatile int n = 0;
  volatile alt_u32* pBuf = (alt_u32*)BUFFER_BASE;
  volatile alt_u32 sum = 0;
  do
  {
	  status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
	  sum += pBuf[n];

	  n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));


Y finalmente, ¡aparecieron caídas en la forma de onda!



La función de verificación de memoria también encontró muchas omisiones:



sí, y vemos muy bien que los datos se cambian de fila a fila:



y aquí hay un ejemplo de un punto negativo específico (falta 6CCE488F):



ahora vemos que el experimento se realizó correctamente, solo el entorno de desarrollo llevado a cabo Optimización para nosotros. Este es el caso cuando pronuncio la frase "Todos lastimaron inteligentemente todo el acero" no con burla, sino con gratitud. ¡Gracias a los desarrolladores de Quartus por este asunto!

Conclusión


Aprendimos cómo insertar un bloque DMA en el sistema para transferir datos de transmisión a la memoria. También nos aseguramos de que el proceso de descarga de otros dispositivos en el bus no interfiera con el proceso de descarga. El entorno de desarrollo creará automáticamente un segmento aislado que se ejecutará en paralelo con otras secciones del bus. Por supuesto, si alguien recurre al mismo segmento, los conflictos y el tiempo dedicado a resolverlos son inevitables, pero el programador puede prever tales cosas.

En el próximo artículo, reemplazaremos la RAM con un controlador SDRAM y el temporizador con una "cabeza" real y crearemos el primer analizador lógico. ¿Funcionará? No lo sé todavía. Espero que los problemas no aparezcan.

All Articles