Desarrollo de su propio núcleo para incrustar en un sistema de procesador basado en FPGA



Entonces, en el primer artículo del ciclo , se dijo que es mejor usar un sistema de procesador para controlar nuestro equipo implementado usando FPGAs para el complejo Redd, después de lo cual durante el primer y segundo artículo se mostró cómo hacer este sistema. Bueno, está hecho, incluso podemos elegir algunos núcleos listos de la lista para incluirlos en él, pero el objetivo final es administrar nuestros propios núcleos personalizados. Ha llegado el momento de considerar cómo incluir un núcleo arbitrario en el sistema del procesador.

Todos los artículos del ciclo:
Desarrollo del "firmware" más simple para FPGAs instalados en Redd, y depuración utilizando la prueba de memoria como ejemplo
Desarrollo del "firmware" más simple para FPGAs instalados en Redd. Parte 2. Código del programa

Para comprender la teoría de hoy, debe encontrar y descargar el documento de especificaciones de la interfaz de Avalon , ya que el bus Avalon es el bus base para el sistema NIOS II. Me referiré a secciones, tablas y figuras para la revisión del documento del 26 de septiembre de 2018.

Abrimos la sección 3 dedicada a las interfaces mapeadas de memoria, o más bien - 3.2. La Tabla 9 enumera las señales del bus. Tenga en cuenta que todas estas señales son opcionales. No encontré una sola señal que tuviera "Sí" en la columna Requerido. Es posible que no enviemos esta o aquella señal a nuestro dispositivo. Por lo tanto, en el caso más simple, el bus es extremadamente simple de implementar. El comienzo de la tabla se ve así:



Como puede ver, todas las señales están muy bien descritas (excepto que esto se hace en inglés). A continuación se muestran los cuadros de tiempo para varios casos. El caso más simple no plantea ninguna pregunta. Ahora tomaré el diagrama de tiempo del documento y cubriré algunas de las líneas con un relleno translúcido (todas son opcionales, tenemos el derecho de excluir cualquiera de las consideraciones).



Miedo Pero todo es simple: se nos da la dirección y la luz estroboscópica de lectura , debemos configurar los datos en el bus readdata. Y viceversa: se nos da la dirección, los datos en el bus de escritura de datos y la luz estroboscópica de escritura, y tenemos que ajustar los datos. No es para nada aterrador, un bus sincrónico típico.

Se necesitan líneas ocultas byteenable para el caso cuando el acceso a la memoria no es palabras de 32 bits. Esto es extremadamente importante cuando diseñamos núcleos universales. Pero cuando diseñamos un núcleo de un día, simplemente escribimos en el documento sobre este núcleo (soy un oponente de la marca en mi cabeza, pero alguien puede limitarlo a esto) que necesitamos usar palabras de 32 bits y eso es todo. Bueno, y la señal de respuesta , es muy especial, y no nos interesa en principio.

Algunas veces es importante que cuando el equipo no esté listo, sea posible retrasar la operación del bus por varios ciclos de reloj. En este caso, se debe agregar la señal WaitRequest . La tabla de tiempos cambiará de la siguiente manera:



Mientras WaitRequest está activado , el asistente sabe que nuestro dispositivo está ocupado. Tenga cuidado si esta señal no se restablece, todo el sistema se "congelará" al manipularlo, por lo que solo un reinicio del FPGA puede restablecerlo. JTAG se cuelga con el sistema. La última vez que observé este fenómeno fue en la preparación de este artículo, por lo que los recuerdos aún son vívidos.

Además, en el documento de la compañía se consideran casos más productivos de canalización de datos y transacciones por lotes, pero la tarea del artículo no es considerar todas las opciones posibles, sino mostrarle al lector la forma de trabajar, enfatizando que todo esto no da miedo, por lo que nos limitaremos a estas dos opciones simples.

Diseñemos un dispositivo simple que periódicamente no estará disponible en el bus. Lo primero que viene a la mente es la interfaz en serie. Mientras la transmisión esté en progreso, haremos que el sistema espere. Y en la vida, aconsejo encarecidamente que no haga esto: el procesador se detendrá hasta el final de una transacción ocupada, pero este es un caso ideal para un artículo, ya que el código de implementación será comprensible y no muy engorroso. En general, haremos un transmisor en serie que pueda enviar datos y señales de selección de chips a dos dispositivos.



Comencemos con la opción de llanta más simple. Hagamos un puerto de salida paralelo, que forma las señales de la elección de los cristales.



Para esto, tomaré el proyecto obtenido en el artículo anterior, pero para evitar confusiones, lo pondré en el directorio AVALON_DEMO. No cambiaré los nombres de otros archivos. En este directorio, cree el directorio my_cores . El nombre del directorio puede ser cualquier cosa. Almacenaremos nuestros núcleos en él. Es cierto, hoy será uno. Cree un archivo CrazySerial.sv con el siguiente contenido:
module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output reg [1:0] cs ); always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; end else begin if (write) case (address) 2'h00: cs <= writedata [1:0]; default:; endcase end end endmodule 

Vamos a hacerlo bien. En primer lugar, las líneas de interfaz. clk y reset son las líneas de reloj y reset. Los nombres de las direcciones , escribir y escribir líneas de datos se toman de la tabla con la lista de señales del documento de Interfaces Mapeadas de Memoria .





De hecho, podría dar cualquier nombre. Vincular las líneas lógicas con las físicas se realizará más adelante. Pero si da los nombres, como en la tabla, el entorno de desarrollo los conectará por sí mismo. Por lo tanto, es mejor tomar los nombres de la tabla.

Bueno, cs son las líneas de selección de cristal que saldrán del chip.

La implementación en sí es trivial. Cuando se reinicia, las salidas se ponen a cero. Y así, en cada medida verificamos si hay una señal de escritura . Si hay una dirección igual a cero, haga clic en los datos. Por supuesto, sería posible agregar un decodificador aquí, lo que evitará la elección de dos dispositivos a la vez, pero lo que es bueno en la vida sobrecargará el artículo. El artículo proporciona solo los pasos más necesarios, sin embargo, se observa que en la vida todo se puede hacer más complicado.

Genial Estamos listos para introducir este código en el sistema del procesador. Vamos a Platform Designer , seleccionamos como archivo de entrada el sistema que creamos en experimentos anteriores:



Llamamos la atención sobre el elemento Nuevo componente en la esquina superior izquierda:



Para agregar su componente, haga clic en este elemento. En el cuadro de diálogo que se abre, complete los campos. Y para el artículo, complete solo el nombre del componente:



Ahora vaya a la pestaña Archivos y haga clic en Agregar archivo :



Agregue el archivo creado anteriormente, selecciónelo en la lista y haga clic en Analizar archivo de síntesis :



No hay errores al analizar SystemVerilog , pero hay varios errores conceptuales. Son causados ​​por el hecho de que algunas líneas estaban conectadas incorrectamente por el entorno de desarrollo. Vamos a la pestaña Señales e interfaces y prestamos atención aquí:



Las líneas cs se asignaron incorrectamente a la interfaz avalon_slave0 , la señal readdata . Pero luego todas las otras líneas se reconocieron correctamente, gracias al hecho de que les dimos nombres de la tabla de documentos. ¿Pero qué hacer con las líneas problemáticas? Deben asignarse a una interfaz como conducto . Para hacer esto, haga clic en el elemento "agregar interfaz"



En el menú desplegable, seleccione conducto :



Obtenemos una nueva interfaz:



Si lo desea, puede cambiarle el nombre. Es cierto que esto sin duda será necesario si queremos hacer varias interfaces externas. Como parte del artículo, le dejaremos el nombre conduit_end . Ahora conectamos la línea cs con el mouse y la arrastramos a esta interfaz. Debemos lograr lanzar una señal debajo de la línea conduit_end , luego se nos permitirá hacer esto. En otros lugares, el cursor aparecerá como un círculo tachado. Al final, deberíamos tener esto:



Reemplace el tipo de señal con readdata con, digamos, chipselect . Imagen final:



Pero los errores permanecieron. El bus avalon no tiene asignada una señal de reinicio. Seleccionamos avalon_slave_0 de la lista y miramos sus propiedades.



Reemplace ninguno con reinicio . Al mismo tiempo, examinaremos las otras propiedades de la interfaz.



Se puede ver que el direccionamiento es en palabras. Bueno, aquí se configuran otras cosas de la documentación. Los diagramas de tiempo que se obtienen en este caso se dibujarán en la parte inferior de las propiedades:



En realidad, no hay más errores. Puedes hacer clic en Finalizar . Nuestro módulo creado apareció en el árbol de dispositivos:



Agréguelo al sistema del procesador, conecte las señales del reloj y reinicie. Conectamos el bus de datos al procesador Data Master . Haga doble clic en Conduit_end y asigne a la señal externa un nombre, por ejemplo, líneas . Resulta de alguna manera así:



Es importante no olvidar que, dado que agregamos un bloque al sistema, debemos asegurarnos de que no entre en conflicto con nadie en el espacio de direcciones. En este caso particular, no hay conflictos en la figura, pero de todos modos, seleccionaré el elemento del menú Sistema-> Asignar direcciones base .

Eso es todo. El bloque se crea, configura y agrega al sistema. Haga clic en el botón Generar HDL , luego en Finalizar .

Hacemos un borrador del proyecto, luego de lo cual vamos al Pin Planner y asignamos las piernas. Resultó así:



Que corresponde a los contactos B22 y C22 del conector de interfaz.

Hacemos el ensamblaje final, cargamos el sistema del procesador en el FPGA. Ahora necesitamos refinar el código del programa. Lanzamiento de Eclipse.

Permítame recordarle que actualmente estoy trabajando con un proyecto que se encuentra en un directorio diferente en relación con mi último trabajo con Redd. Para no confundirme, eliminaré proyectos antiguos del árbol (pero solo del árbol, sin borrar los archivos mismos).



A continuación, hago clic con el botón derecho del mouse en un árbol vacío y selecciono Importar en el menú:



Siguiente - General-> Proyecto existente en el espacio de trabajo :



Y simplemente seleccione el directorio en el que se almacenan los archivos del proyecto:





Ambos proyectos heredados de experimentos anteriores se conectarán al entorno de desarrollo.



Destacaré el siguiente elemento en un marco:
Cada vez que cambie la configuración del hardware, seleccione el elemento Nios II -> Generar menú BSP para el proyecto BSP nuevamente.




En realidad, después de esta operación, apareció un nuevo bloque en el archivo \ AVALON_DEMO \ software \ SDRAMtest_bsp \ system.h :
 /* * CrazySerial_0 configuration * */ #define ALT_MODULE_CLASS_CrazySerial_0 CrazySerial #define CRAZYSERIAL_0_BASE 0x4011020 #define CRAZYSERIAL_0_IRQ -1 #define CRAZYSERIAL_0_IRQ_INTERRUPT_CONTROLLER_ID -1 #define CRAZYSERIAL_0_NAME "/dev/CrazySerial_0" #define CRAZYSERIAL_0_SPAN 16 #define CRAZYSERIAL_0_TYPE "CrazySerial" 

En primer lugar, estamos interesados ​​en la constante CRAZYSERIAL_0_BASE .

Agregue el siguiente código a la función main () :
  while (true) { IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x03); } 

Comenzamos a depurar y miramos el contenido de las líneas con un osciloscopio. Debe haber un código binario incremental. El esta ahi.



Además, la frecuencia de acceso a los puertos es simplemente maravillosa:



Aproximadamente 25 MHz es la mitad de la frecuencia del bus (2 ciclos de reloj). A veces el tiempo de acceso no es de 2 ciclos, sino más largo. Esto se debe a la ejecución de operaciones de ramificación en el programa. En general, el acceso más simple al autobús funciona.

Es hora de agregar, por ejemplo, la funcionalidad del puerto serie. Para hacer esto, agregue la señal de interfaz de solicitud de espera relacionada con el bus y un par de señales de puerto serie: sck y sdo . Total, obtenemos el siguiente fragmento de código en systemverilog :



Mismo texto:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); 


De acuerdo con las reglas de buena forma, debe hacer una máquina simple que transmita datos. Desafortunadamente, la máquina más sencilla en el artículo se verá muy difícil. Pero, de hecho, si no aumento la funcionalidad de la máquina (y no voy a hacerlo dentro del marco del artículo), entonces solo tendrá dos estados: la transmisión está en progreso y la transmisión no está en progreso. Por lo tanto, puedo codificar el estado con una sola señal:
envío de registros = 0;

Durante la transmisión, necesito un contador de bits, un divisor de reloj (estoy haciendo un dispositivo deliberadamente lento) y un registro de desplazamiento para los datos transmitidos. Agregue los registros apropiados:
  reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; 

Dividiré la frecuencia por 10 (guiado por el principio de "¿por qué no?"). En consecuencia, en el quinto paso, llamaré a SCK, y en el décimo, dejar esta línea, después de lo cual, pasaré al siguiente bit de datos. En todas las demás medidas, simplemente aumente el contador divisor. Es importante no olvidar que en la cuarta medida también necesita aumentar el contador, y en la novena - cero. Si omitimos la transición al siguiente bit, la lógica especificada se ve así:
  if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin sck <= 0; clk_div <= 0; // <   > end default: clk_div <= clk_div + 1; endcase end else 

Ir al siguiente bit es fácil. Cambiaron el registro de desplazamiento, luego, si el bit actual es el séptimo, dejaron de funcionar cambiando el estado de la máquina, de lo contrario aumentaron el contador de bits.
  shifter <= {shifter[6:0],1'b0}; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end 

En realidad, eso es todo. El bit de salida siempre se toma del bit alto del registro de desplazamiento:
  assign sdo = shifter [7]; 

Y la línea más importante para la revisión actual. La señal de solicitud de espera se activa hasta la unidad siempre que se transmiten datos en serie. Es decir, es una copia de la señal de envío que establece el estado de la máquina:
  assign waitrequest = sending; 

Bueno, y al escribir en la dirección 1 (recuerde, aquí tenemos el direccionamiento en palabras de 32 bits), ajustamos los datos en el registro de desplazamiento, ponemos a cero los contadores e iniciamos el proceso de transferencia:
  if (write) //... 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end 

Ahora daré todos los fragmentos descritos como un solo texto:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); reg sending = 0; reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; sck <= 0; sending <= 0; end else begin if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin clk_div <= 0; shifter <= {shifter[6:0],1'b0}; sck <= 0; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end end default: clk_div <= clk_div + 1; endcase end else if (write) case (address) 2'h00: cs <= writedata [1:0]; 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end end assign sdo = shifter [7]; assign waitrequest = sending; endmodule 


Comenzamos a introducir nuevo código en el sistema. En realidad, la ruta es la misma que cuando se crea el componente, pero algunos de los pasos ya pueden omitirse. Ahora nos familiarizaremos con el proceso de refinamiento. Vaya a Diseñador de plataforma . Si solo cambiamos el código verilog, sería bastante simple realizar la operación Generar HDL para el sistema terminado. Pero dado que el módulo tiene nuevas líneas (es decir, la interfaz ha cambiado), necesita ser rehecho. Para hacer esto, selecciónelo en el árbol, presione el botón derecho del mouse y seleccione Editar .



Estamos editando un sistema existente. Tan solo vaya a la pestaña Archivos y haga clic en Analizar archivos de síntesis :



Predeciblemente se produjeron errores. Pero ya sabemos que las líneas equivocadas tienen la culpa. Por lo tanto, vamos a la pestaña Señales e interfaces , arrastramos sck y sdo a lo largo de la misma línea desde la interfaz avalon_slave_0 a la interfaz conduit_end :



También cambie el nombre de los campos de Tipo de señal para ellos. El resultado debe ser el siguiente:



En realidad, eso es todo. Haga clic en Finalizar , llame a Generar archivo HDL para el sistema del procesador, redacte el proyecto en Quartus, asigne nuevos tramos:



Estos son los contactos A21 y A22 del conector de interfaz, hacemos el ensamblaje final, completamos el "firmware" en el FPGA.

Plancha actualizada. Ahora el programa. Vamos a Eclipse. ¿Qué recordamos hacer allí? Así es, no te olvides de elegir Generate BSP .

En realidad, eso es todo. Queda por agregar funcionalidad al programa. Transfieramos un par de bytes al puerto serie, pero enviaremos el primer byte al dispositivo seleccionado por la línea cs [0] y el segundo - cs [1] .
  IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x12); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x34); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); 

Tenga en cuenta que no hay controles de disponibilidad allí. Las parcelas van una tras otra. Sin embargo, en el osciloscopio todo resultó bastante consistente



El rayo amarillo es cs [0] , el rayo verde es sdo , el rayo violeta es sck y el rayo azul es cs [1] . Se puede ver que el código 0x12 fue al primer dispositivo, 0x34 al segundo.

La lectura se realiza de manera similar, pero no puedo encontrar ningún ejemplo hermoso, excepto la lectura banal del contenido del pie del conector. Pero ese ejemplo es tan degenerado que ni siquiera es interesante hacerlo. Pero aquí vale la pena señalar que al leer esta configuración de bus puede ser extremadamente importante:



Si hay una línea de Lectura , aparecerá un cuadro de tiempo de lectura en el cuadro de diálogo de configuración. Y mostrará la influencia de este parámetro. Al leer las patas del conector, aún no se notará, pero al leer desde el mismo FIFO o RAM, completamente. La RAM se puede configurar para emitir datos inmediatamente después de que se envíe la dirección, o se puede emitir sincrónicamente. En el segundo caso, se agrega latencia. Después de todo, el bus estableció la dirección, configuró la luz estroboscópica ... Pero no hay datos en el borde más cercano de la señal del reloj. Aparecerán después de este frente ... Es decir, el sistema tiene una latencia de latencia única. Y solo debe tenerse en cuenta al configurar este parámetro. En resumen, si no está leyendo lo que se esperaba, primero verifique si necesita configurar la latencia. El resto: leer no es diferente de escribir.

Bien, permítame recordarle una vez más que es mejor no eliminar la preparación del bus para operaciones a largo plazo, de lo contrario es muy posible reducir drásticamente el rendimiento del sistema. La señal de listo es buena para mantener la transacción durante un par de ciclos de reloj, y no hasta 80 ciclos de reloj, como en mi ejemplo. Pero, en primer lugar, cualquier otro ejemplo sería inconveniente para el artículo y, en segundo lugar, para los núcleos de un día, esto es bastante aceptable. Estará completamente consciente de sus acciones y evitará situaciones en las que el autobús esté bloqueado. Es cierto que si el núcleo sobrevive el tiempo que se le asigna, tal suposición puede arruinar la vida en el futuro, cuando todos se olviden de ello, y ralentizará todo. Pero será más tarde.

Sin embargo, hemos aprendido a hacer que el núcleo del procesador controle nuestros núcleos. Todo está claro con el mundo direccionable, ahora es el momento de lidiar con el mundo de la transmisión. Pero haremos esto en el próximo artículo, y posiblemente incluso en varios artículos.

Conclusión


El artículo muestra cómo se puede conectar un kernel arbitrario de Verilog para controlar el sistema de procesador Nios II. Se muestran las opciones para la conexión más simple al bus Avalon, así como la conexión en la que el bus puede estar ocupado. Se proporcionan enlaces a la literatura, desde la cual puede encontrar otros modos de funcionamiento del bus Avalon en el modo Asignación de memoria.

El proyecto resultante se puede descargar aquí .

Source: https://habr.com/ru/post/454938/


All Articles