2019/08/21

Para estudiar Programming FPGAs: Getting Started with Verilog

Esta es la adaptación a Vivado/Nexys 4 DDR de los ejemplos del libro, escrito por Simon Monk que adquirí básicamente como introducción a Verilog tal que pudiera leer en el transporte público, pues necesito aún aprender Verilog para implementar Forzando Brutalmente MD5 Sin Computadoras en una Parallella y el ejemplo que quiero extender está en Verilog.



Como introducción a Verilog es muy liviana, termina y me digo ¿qué puedo hacer que no sea portar los ejemplos?

Son muy prácticos e interesantes, aparte de los típicos contadores y decodificadores de siete segmentos, tiene PWM, audio y vga, muy copado.

Comparando con [Free Range VHDL], se queda muy corto en los contenidos y la profundidad, pero le pasa por encima con la práctica, pues FR es agnóstico a la plataforma y no explica como sintetizar ni simular.

Los ejemplos son para tres placas que no tengo ni voy a tener, así que me dí la tarea de adaptarlos y ejecutarlos en la que sí tengo, una Nexys 4 DDR (Artix 7) y en lugar de usar ISE, usar Vivado 2018.2.

En teoría lo único que tendría que hacer es adaptar los clocks y las UCF, o sea, los archivitos que dicen a que patitas se conectan lo que se haya programado. En la práctica, hay un poco más. Y al interactuar con hardware externo un poco de debug no viene mal.


Lo bueno de este proceso es que me sirvió para:
  • fijar el flujo de trabajo en Vivado.
  • comprender los constraints files.
  • asimilar Verilog, del cual no sé casi nada.

Si te interesa reproducirlo, quizás te convenga dejar de leer y tampoco mirar el código en github.

Apliqué esta práctica tambien a otros cursos online que iré publicando.

Manos a la obra


git clone https://github.com/simonmonk/prog_fpgas.git


Comencé con el contador del capítulo 3. Me generó un reporte donde dice que el part name no es soportado y que los UCF (el archivo de constraints) no le sirve y que no entiende Schematics y como convertirlo a algo compatible usando ISE , que es la versión previa a Vivado.

Como me desvía mucho de mis intereses actuales, a saltear ejemplos hasta hallar Verilog.

 

Contador del capítulo 4



Según lo esperado,  hay que adaptar el target, ajustar el XDC y darle para adelante.

El target:
  • Project Manager->
  • Settings->
  • Project settings->
  • General->
  • Project Device->
  • Boards->
  • Nexys-4-DDR
El XDC:

  • Project Manager->
  • Add Sources->
  • Add or create constraints->
  • github/digilent/digilent-xdc/Nexys-4-DDR-Master.xdc

Adaptar los get_ports a la interfaz:

module counter(
    input BTNC,              // Clock en el original
    output reg [3:0] Q
    );


Habilitar los pines en XDC:



## LEDs

set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { Q[0] }];

                                                #IO_L18P_T2_A24_15 Sch=led[0]

set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { Q[1] }];

                                                #IO_L24P_T3_RS1_15 Sch=led[1]

set_property -dict { PACKAGE_PIN J13 IOSTANDARD LVCMOS33 } [get_ports { Q[2] }];

                                                #IO_L17N_T2_A25_15 Sch=led[2]

set_property -dict { PACKAGE_PIN N14 IOSTANDARD LVCMOS33 } [get_ports { Q[3] }];

                                                #IO_L8P_T1_D11_14 Sch=led[3]


##Buttons
set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { BTNC }];
                                                #IO_L9P_T1_DQS_14 Sch=btnc

Al hacer la implementación, me dijo:

[Place 30-574] Poor placement for routing between an IO pin and BUFG. If this sub optimal condition is acceptable for this design, you may use the CLOCK_DEDICATED_ROUTE constraint in the .xdc file to demote this message to a WARNING. However, the use of this override is highly discouraged. These examples can be used directly in the .xdc file to override this clock rule.
    < set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets BTNC_IBUF] >

    BTNC_IBUF_inst (IBUF.O) is locked to IOB_X0Y82
     and BTNC_IBUF_BUFG_inst (BUFG.I) is provisionally placed by clockplacer on BUFGCTRL_X0Y0


Parece que no le gusta que le pongan en el reloj cosas que no son del reloj. Seguí las instrucciones (el set property...) y todo ok.

 set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets BTNC_IBUF];


Nota de color



Durante un momento estuve confundido y en lugar de conectar el botón conecté el clock,

## Clock signal
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { Clock }];

                                               #IO_L12P_T1_MRCC_35 Sch=clk100mhz

create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {Clock}];




Podía tomar dos caminos:

Aprender a manejarme con todo lo de los clocks, agregado a la deuda, o hacer más ancho el contador y fué más ancho, pero aún con


output reg [15:0] Q

y en el XDC:

descomentar y renombrar Q[4] a Q[7],

el problema es que con el reloj a 100 MHz, aun con un contador de 16 bits (65536), la frecuencia más baja es 1525 Hz, obviamente inapreciable sin instrumental. Luego entendí que el "Clock" estaba provisto por apretar un botoncito.

 

Siete segmentos del capítulo 5

 

Hay un prescaler que no ajusté al número apropiado tal que haga refresco a 1kHz, deber estar haciéndolo a algo bastante más alto, en otra oportunidad diagnosticaré y corregiré.

Como mi placa tiene 8 digitos contra los 3 o 4 de las del libro, para mejorar la estética, habilité todos los dígitos propagando las centenas a la izquierda, no tenía ganas de apretar mil veces el botoncito ni modificar mucho el ejemplo tal que otro botón sumara de a diez y otro de a cien. Estuve varias horas dando vueltas por que no funcionaba hasta que me di cuenta que unos wires no estaban bien por el ancho y fundamentalmente por unos warnings que decían que habían patitas sin usar.


Cuenta regresiva del capítulo 6


Como usa componentes ya modificados en el ejemplo del contador, primero hice la adaptación tal como viene y luego le apliqué el cambio, que es usar los ocho dígitos.




kdiff3 papilio/ch06_countdown_timer/src/timer.v \
       elbert2/ch06_countdown_timer/src/timer.v \
       mojo/ch06_countdown_timer/src/timer.v


PWM del capítulo 7


No pude postergar más el asunto de la frecuencia, ya que el próximo ejemplo usa este para interactuar con un componente externo.

Adelantándome un poco pero sin hacer trampa, tras adaptar el PWM a mi placa, para el servo hay que meter un pulso así:

Stefan Tauner [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)]

Y en el ejemplo hay cualquier cosa, ya que es de PWM en general, más considerando la disparidad de clocks de las placas involucradas:
  • Elbert2      12 MHz
  • Papillo One  32 MHz
  • Mojo         50 MHz
  • Nexys4 DDR  100 MHz

Como siempre, te recomiendo te detengas, hagas tus pruebas y regreses.

Tablita útil para los que no estamos todo el día con esto


Hay tres caminos, uno es pensar y calcular, el otro medir y el otro mirar fijo los ejemplos y sus diferencias, tambien llamado ingeniería inversa.


Voy a tomar los tres caminos y los voy a mezclar.

Pensar y calcular


Primero hay que comprender el código:

  reg [7:0] duty = 0;
  reg [6:0] prescaler = 0; // CLK freq / 128 / 256 = 1.5

  always @(posedge CLK)
  begin

    prescaler <= prescaler + 1;
    // esto indica el ancho del pulso
    if (s_up)
      duty <= duty + 5;
    if (s_dn)
      duty <= duty - 5;

  end


  pwm p(.pwm_clk (prescaler[6]), .duty (duty),...);

 y el módulo pwm

  reg [7:0] count = 0; 

  always @(posedge pwm_clk) ; // pwm_clk <= prescaler
  begin
    count <= count + 1;
    PWM_PIN <= (count < duty);

  end


No lo termino de entender, así que a...

Medir



Para medir repliqué la salida del PWM del led a uno pin de un pmod y tambien el pwm_clk y de ahí al osciloscopio.

Volver a pensar y calcular


La frecuencia es 391 kHz, que es 100MHz dividido 256, si lo divido otra vez, tengo 1.527 kHz, con lo cual llego al ejemplo.

Para avanzar hacia el servo, lo que tengo no sirve pues no estoy respetando los límites y lo modifico sin mirar el siguiente ejemplo o paso al siguiente ejemplo... ok, versiono el ejemplo con valores equivalentes e intento hacer el servo y si fallo me copio.

Hay unos dibujitos muy lindos de pwm en el manual de referencia.

 

Mi intento de servo del capítulo 7


Pensé, primero tengo que lograr un período de 20 ms, tras algunas pruebas, quedé cerca y las aproveché para aprender como parametrizar un módulo, muy útil y sencillo:

En la interfaz del módulo se define un parameter con un valor default,

  module pwm #( 
    parameter DUTYWIDTH  = 8
  ) (
    input pwm_clk,
    input [DUTYWIDTH - 1:0] duty,
    output reg PWM_led,
    output reg PWM_pin,
    output reg PWM_CLK_pin
  );


que puede ser reemplazado en la instanciación desde el parent:

  parameter DUTYWIDTH = 8;
  pwm #(.DUTYWIDTH(DUTYWIDTH)) p(.pwm_clk


Luego pensé que si debe variar entre 1 y 2 ms el pulso, hacen falta algunos && en los ifs de variación:

  if (s_up && duty < DUTYHIGH )
  begin
    duty <= duty + DUTYSTEP;
  end
  if (s_dn && duty >= DUTYLOW)
  begin
    duty <= duty - DUTYSTEP;
  end 


Finalmente, como no estaba en 20ms de período si no en 21ms, en lugar de dejar que el contador del prescaler diera toda la vuelta, le puse un tope:

  if (prescaler > PRESCALER_MAX )
    prescaler <= 0;


El código completo acá y listo, a comparar con el libro.

 

Servo del capítulo 7

 

Un poco decepcionante, no hay ningún control del ancho del pulso.

Tomé la de Mojo por el clock múltiplo (50MHz), así que de una me arrancó con un periodo del doble, 10ms.


Pensé que con multiplicar por dos el prescaler se arreglaría pero no, así que miré las implementaciones de las otras placas:

 find . -iname servo_tester.v | xargs kdiff3
 find . -iname tester.v | xargs kdiff3



Interesante es que usa parámetros posicionales:

  servo #(100) (...

preferí nombrarlos

  servo #(.CLK_F(100)) (...

y aunque el libro menciona el parameter, no explica como reemplazarlo en synthesis time.

Además puse el valor inicial de pulse_len en 1000 en lugar de 500 y está ok, salvo la falta de límites.




Si mirás bien los kdiff3s anteriores, vas a ver que cada placa no tiene parámetros distintos si no código distinto, mmh, me parece que me quedo con mi implementación, tengo dudas de si pasarle el prescaler al módulo o conservarlo afuera.



Abajo a la izquierda el servo se mueve mientras arriba al centro cambia el ancho del pulso.

Capítulo 8 WAV Player


A diferencia del ejemplo de la Nexys4DDR, el libro no usa la memoria RAM sino la memoria de la FPGA, que ahorita no recuerdo el nombre, pero ya aparecerá... block memory. Tampoco usa el audio output sino un mero GPIO.

Lo adapté tal como está, el objetivo de esta experiencia es sólo hacerlo andar, no entrar en cambios drásticos.

Usé el ejemplo de Mojo, que es la más power.

La lectura en "memoria" del audio le agrega a la síntesis cinco minutos.

Asombrosamente, anda.

Capítulo 9 VGA básico


Esta es la interfaz del módulo:

  input CLK,
  output HS,
  output VS,
  output [2:0] RED,
  output [2:0] GREEN,
  output [1:0] BLUE


Este es el .UCF del Papillo One:

  NET "CLK" LOC = P89; #32MHz
  # Outputs
  NET "HS" LOC = P83;
  NET "VS" LOC = P85;
  NET "RED[0]" LOC = P61;
  NET "RED[1]" LOC = P58;
  NET "RED[2]" LOC = P54;
  NET "GREEN[0]" LOC = P68;
  NET "GREEN[1]" LOC = P66;
  NET "GREEN[2]" LOC = P63;
  NET "BLUE[0]" LOC = P78;
  NET "BLUE[1]" LOC = P71;



Y esto es lo que hay en el XDC:


#... [get_ports { VGA_R[0] }]; #IO_L8N_T1_AD14N_35 Sch=vga_r[0]
#... [get_ports { VGA_R[1] }]; #IO_L7N_T1_AD6N_35 Sch=vga_r[1]
#... [get_ports { VGA_R[2] }]; #IO_L1N_T0_AD4N_35 Sch=vga_r[2]
#... [get_ports { VGA_R[3] }]; #IO_L8P_T1_AD14P_35 Sch=vga_r[3]
#... [get_ports { VGA_G[0] }]; #IO_L1P_T0_AD4P_35 Sch=vga_g[0]
#... [get_ports { VGA_G[1] }]; #IO_L3N_T0_DQS_AD5N_35 Sch=vga_g[1]
#... [get_ports { VGA_G[2] }]; #IO_L2N_T0_AD12N_35 Sch=vga_g[2]
#... [get_ports { VGA_G[3] }]; #IO_L3P_T0_DQS_AD5P_35 Sch=vga_g[3]
#... [get_ports { VGA_B[0] }]; #IO_L2P_T0_AD12P_35 Sch=vga_b[0]
#... [get_ports { VGA_B[1] }]; #IO_L4N_T0_35 Sch=vga_b[1]
#... [get_ports { VGA_B[2] }]; #IO_L6N_T0_VREF_35 Sch=vga_b[2]
#... [get_ports { VGA_B[3] }]; #IO_L4P_T0_35 Sch=vga_b[3]
#... [get_ports { VGA_HS }]; #IO_L4P_T0_15 Sch=vga_hs
#... [get_ports { VGA_VS }]; #IO_L3N_T0_DQS_AD1N_15 Sch=vga_vs


Mete miedo, es la primera vez en mi vida que voy a conectar a un monitor algo que no sea una placa de video. No hay que dejarse acobardar.

...[get_ports { RED[0] }]; #IO_L8N_T1_AD14N_35 Sch=vga_r[0]
...[get_ports { RED[1] }]; #IO_L7N_T1_AD6N_35 Sch=vga_r[1]

...[get_ports { RED[2] }]; #IO_L1N_T0_AD4N_35 Sch=vga_r[2]

...[get_ports { GREEN[0] }]; #IO_L1P_T0_AD4P_35 Sch=vga_g[0]
...[get_ports { GREEN[1] }]; #IO_L3N_T0_DQS_AD5N_35 Sch=vga_g[1]
...[get_ports { GREEN[2] }]; #IO_L2N_T0_AD12N_35 Sch=vga_g[2]

...[get_ports { BLUE[0] }]; #IO_L2P_T0_AD12P_35 Sch=vga_b[0]
...[get_ports { BLUE[1] }]; #IO_L4N_T0_35 Sch=vga_b[1]

...[get_ports { HS }]; #IO_L4P_T0_15 Sch=vga_hs
...[get_ports { VS }]; #IO_L3N_T0_DQS_AD1N_15 Sch=vga_vs


Fiel a lo que había aprendido con respecto a los nombres, adapté el XDC. Obviamente sintetizó, implementó y generó de una pero está el asunto del reloj, recuerdo haber escuchado hace muchos años a monitores VGA sometidos a resoluciones extremas y luego romperse. Tengo dos caminos:

  • Conectar a un monitor descartable, difícil, el que tengo no sé que tan mal anda y sería una pena romperlo.
  • Desde una placa de video generar la misma resolucion, medirlo y comparar.

Lo segundo sin ninguna duda es más educativo, pero tambien es más trabajo, voy a reventar primero el monitor que ya está dañado.

De un modo u otro es útil releer en el libro lo que dice de como ajustar los timings según el clock de cada placa, lo que me lleva al tercer camino:

  • Comprender los timings y medir el HS y VS a ver si son apropiados sin conectar el monitor.

Para ello dupliqué las señales en el PMOD, tengo un VS de 230 Hz y HS de 120KHz:


En amarillo el HS 119 kHz antes del ajuste



En cyan VS 232 Hz antes del ajuste



Empíricamente dividí el clock por cuatro a ver si me queda en 57.5 e intuyo que HS debería ser eso por 500, un poco menos de 30 kHz, perfecto!


En amarillo HS 30 kHz




En cyan VS 57.8 Hz



Funcionando. Si, son alfileres con cabeza los que uso para las puntas.

Lo que sigue es cuesta abajo... pero en algún momento debería sentarme a hacer bien las cuentas y usar otras resoluciones correctamente, quedará para otra entrada.

Capítulo 9 VGA memoria

 

Este, al igual que el del wav levanta la info de block memory, pero en lugar de tirarla al audio, va a VGA

El módulo vga.v es el mismo que el anterior, vga_mem.v tiene agregado los probes y declaré explícitamente un wire blank que no estaba.

Ufa, es una bandera inglesa, me guardo la foto.

Capítulo 9 VGA juego

Nada, más de lo mismo, sólo hay que conectar los pines y anda, más o menos, menos que más. Habría que toquetear los tiempos y centrar bien la pantalla, va a la deuda.



Resumiendo


El procedimiento para migrar los ejemplos del libro es:
  • Crear un nuevo proyecto
  • Elegir board/device 
  • Copiar los verilogs y agregarlos al proyecto
  • Agregar XDC y adaptarlo mirando fijo la interfaz del top module y los .ucf
    • Pines
    • Reloj 
  • Pelearse con lo que haya que pelear
    • la adaptación al clock
  • Run Synthesis
  • Volver a pelearse con lo que haya que pelear
  • Run Implementation
  • Volver a pelearse con lo que haya que pelear
  • Generate Bitstream
  • Conectar los accesorios de hardware necesarios
  • Hardware Manager
  • Auto Connect
  • Program Device
Es horrible el tiempo que tarda, no bien pueda voy a aprender a distribuir la carga vía red.

Reitero que el código está en github.

Deudas  

Estos son algunos de los temas que no resolví para no salirme del rumbo, que era sólo portar los ejemplos, en algún momento saldaré mi deuda y se reflejará en futuras entradas.
  • Usar distintos clockings.
  • Comprender y usar el audio jack. 
  • Calibrar las medidas de VGA.
  • Comprender los timings de VGA para usar otras resoluciones.
  • Adaptación eléctrica del servo, no termina de girar los 90°.
  • Distribuir la carga del proceso de build.




No hay comentarios:

Publicar un comentario