2021/03/21

Refactorizaciones de MD5 en FPGA: 1 simulación

Este tema se parece a una cucaracha: la pisás y la dás por muerta, pero cuando volvés con la palita y la escoba a juntarla se cambió de lugar o incluso se escapó.


Tras todo lo aprendido en Basic Digital Design, propuse como trabajo práctico tomar este proyecto y aplicarle algunas mejoras.


Para ponerte al tanto podés ponerte a leer todo lo anterior o mejor contentarte con este resumen. De paso, me parece que es la primera vez que hago un diagrama decente del diseño.

Se trata de calcular el hash MD5 para una serie de valores con la intención de recuperar el valor original. Tal como está hecho alcanza para cuatro o cinco letras.

 

Esta no es exactamente la implementación original, es la que quedó tras lo que hice ahora, pero a grandes rasgos es la misma.

Con data_selector se elige uno de los hashes hardcodeados, con enable se inicia el proceso, cuando coincide el hash generado con el seleccionado, se detiene todo y en counter tenés el número aproximado al que generó el hash.

Diagrama conceptual
Diagrama conceptual



En realidad son ocho pipelines, así que este diagrama es más real. Fijate que el contador tiene tres bits menos, cada pipeline se instancia así:


pipeline pipeline0(.CLK(CLK),
  .counter_in({counter_out,3'b000}),
  .target_hash(target_selected),
  .reset(reset),
  .found(found0)
);

Los tres bits menos significativos están fijos para cada pipeline y corresponden a su índice.

Implementación 8 lanes
Implementación 8 lanes



La primera gran modificación fue simular. ¿Me podés creer que cuando hice esto no sabía simular y tuve que implementar la funcionalidades de avanzar el contador paso a paso y la de bajar la velocidad para poder ver que estaba ocurriendo?

La ventaja de este método "incorrecto" es que me obligó a pensar bastante e ingeniármelas para hacerlo funcionar.

Supuse que simular sería sencillo, pero no, aunque el circuito "funcionaba", al simular fallaba, no sé bien por qué ni me maté para comprenderlo, aunque estoy seguro de que el motivo era que durante los ciclos que los pipelines estaban inválidos hallaban algo espúreo y se ahí se detenía antes de tiempo. Tomé mi lista de tareas y las apliqué:

  • No aceptar como hallado mientras el pipeline estuviera "calentándose". Para esto agregué un nuevo contador que habilita la evaluación de hallado recién desde el momento en que llega a final del pipeline el primer valor válido.
  • Reemplazar la lógica de control pegada con moco por una FSM bien pensada.

 

Con warm up
Con warm up

 

No es exactamente como está en el diagrama, en lugar del and el warm_up_counter afecta el comportamiento de la FSM pero así tambien pudo haber funcionado.

No sólo anduvo la simulación como seda, sino que también la implementada.

Para simular lo que se puede hacer es crear un uno módulo por encima del top, que al crearlo le avisás a Vivado que va a ser un módulo de simulación.

 

En este, declarás regs y wires tal que reproduzcan las entradas y salidas respectivamente de la placa y le conectás el top que tenías,


`timescale 1ns/100ps

module tb_driver();
   reg  CLK;
   reg  reset_button;
   reg  enable_switch;
   reg  [3:0]target_switch;
   wire [7:0] SEG;
   wire [7:0] DIGIT;
   wire status_paused;
   wire status_running;
   wire status_warming;
   wire status_found;
   wire status_done;

always
  #5 CLK = ~CLK;
 
initial begin
  CLK  = 1'b0;
  reset_button  = 1'b1;
  enable_switch = 1'b0;
//target_switch = 4'b0000;
//target_switch = 4'b0001;  // 0000 0001
//target_switch = 4'b0010;  // 0000 0002
//target_switch = 4'b0011;  // 0000 0010
  target_switch = 4'b0100;  // 0000 0100
//target_switch = 4'b0101;  // 0000 1000
//target_switch = 4'b0110;  // 0001 0000


  # 200  reset_button  = 1'b0;
  # 200  enable_switch = 1'b1;
  # 1200 reset_button  = 1'b1;
  # 200  reset_button  = 1'b0;
  # 2000 $finish;
end

driver u_driver(
  .CLK            (CLK),      
  .CPU_RESETN     (reset_button),     
  .enable_switch  (enable_switch),     
  .target_switch  (target_switch),
  .SEG            (SEG),
  .DIGIT          (DIGIT),
  .status_paused  (status_paused),
  .status_running (status_running),
  .status_warming (status_warming),
  .status_found   (status_found),
  .status_done    (status_done)    
  );

endmodule

 

 

Con esta línea le estás diciendo que # de los que se ven más abajo equivalen a 1n y las fracciones 100ps.


`timescale 1ns/100ps


Con esto declarás las entradas y salidas, fijate que las entradas son registros, pues les vas a dar valores luego en el initial:


   reg  CLK;
   reg  reset_button;
   reg  enable_switch;
   reg  [3:0]target_switch;
   wire [7:0] SEG;
   wire [7:0] DIGIT;
   wire status_paused;
   wire status_running;
   wire status_warming;
   wire status_found;
   wire status_done;

 

Mirá la correspondencia con lo que hay en el xdc:

 
## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports {
    CLK }];
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {
    CLK }];



## Switches

set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports {
    enable_switch }];

set_property -dict {PACKAGE_PIN M13 IOSTANDARD LVCMOS33} [get_ports {
    target_switch[0] }];
set_property -dict {PACKAGE_PIN R15 IOSTANDARD LVCMOS33} [get_ports {
    target_switch[1] }];
set_property -dict {PACKAGE_PIN R17 IOSTANDARD LVCMOS33} [get_ports {
    target_switch[2] }];
set_property -dict {PACKAGE_PIN T18 IOSTANDARD LVCMOS33} [get_ports {
    target_switch[3] }];


## Leds

set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {
    running_led }];
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {
    done_led }];
set_property -dict {PACKAGE_PIN J13 IOSTANDARD LVCMOS33} [get_ports {
    found_led }];


##7 segment display

set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports {
    SEG[7] }];
set_property -dict {PACKAGE_PIN R10 IOSTANDARD LVCMOS33} [get_ports {
    SEG[6] }];
set_property -dict {PACKAGE_PIN K16 IOSTANDARD LVCMOS33} [get_ports {
    SEG[5] }];
set_property -dict {PACKAGE_PIN K13 IOSTANDARD LVCMOS33} [get_ports {
    SEG[4] }];
set_property -dict {PACKAGE_PIN P15 IOSTANDARD LVCMOS33} [get_ports {
    SEG[3] }];
set_property -dict {PACKAGE_PIN T11 IOSTANDARD LVCMOS33} [get_ports {
    SEG[2] }];
set_property -dict {PACKAGE_PIN L18 IOSTANDARD LVCMOS33} [get_ports {
    SEG[1] }];
set_property -dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {
    SEG[0] }];

set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[0] }];
set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[1] }];
set_property -dict {PACKAGE_PIN T9  IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[2] }];
set_property -dict {PACKAGE_PIN J14 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[3] }];
set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[4] }];
set_property -dict {PACKAGE_PIN T14 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[5] }];
set_property -dict {PACKAGE_PIN K2  IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[6] }];
set_property -dict {PACKAGE_PIN U13 IOSTANDARD LVCMOS33} [get_ports {
    DIGIT[7] }];


##Buttons


set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports {
    step_button }];
set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports {
    reset_button }];

 

 

 

Esto siginifica que cada 5 ns va a transicionar el clock, período de 10 ns, 100 Mhz:


always
  #5 CLK = ~CLK;

 

Estos son los estímulos que va a recibir la simulación, todo el primer bloque es simultáneo al cominezo, luego, va dejando pasar tantos ns como # diga:

initial begin
  CLK  = 1'b0;
  reset_button  = 1'b1;
  enable_switch = 1'b0;
//target_switch = 4'b0000;
//target_switch = 4'b0001;  // 0000 0001
//target_switch = 4'b0010;  // 0000 0002
//target_switch = 4'b0011;  // 0000 0010
  target_switch = 4'b0100;  // 0000 0100
//target_switch = 4'b0101;  // 0000 1000
//target_switch = 4'b0110;  // 0001 0000


  # 200  reset_button  = 1'b0;
  # 200  enable_switch = 1'b1;
  # 1200 reset_button  = 1'b1;
  # 200  reset_button  = 1'b0;
  # 2000 $finish;
end

 

 

Y finalmente, la instanciación del circuito, conectando a cada entrada o salida lo declarado antes:


 driver u_driver(
  .CLK            (CLK),      
  .CPU_RESETN     (reset_button),     
  .enable_switch  (enable_switch),     
  .target_switch  (target_switch),
  .SEG            (SEG),
  .DIGIT          (DIGIT),
  .status_paused  (status_paused),
  .status_running (status_running),
  .status_warming (status_warming),
  .status_found   (status_found),
  .status_done    (status_done)    
  );

endmodule


La simulación resultante:

 

 

Simulación
Simulación


Primero se desactiva el reset, luego se activa el enable, inmediatamente se pone en running y warming y empieza el contador, tras algunos ciclos finaliza warming y varios despues uno de los pipelines encuentra una coincidencia, se activan found y done, fin.


Me quedan unos defectos que no sé si resolveré pues tengo un tiempo limitado para presentar el trabajo práctico y además puedo mitigarlos "por software" más adelante:

  • Comprobar bien si los ciclos de warming son correctos
  • Puede estar relacionado, ver si encuentra los números bajos.
  • Ver que encuentre los números altos, es que el contador avisa que terminó antes de que se vacíe el pipeline, necesitaría un "cooling".
  • Se produce un offset entre lo hallado y el real y no hay información de cuál pipeline lo halló.

La solución por software para todos menos el último sería calcular los números bajos y los altos. Si los encuentra listo, no le pide nada al circuito pues ya los tiene.

Para el último sería tomar el valor y explorar los ocho posibles tras corregir el offset.

 

 

El código fuente de base y la simulación están en los tags base y simul respectivamente.

 

Notas relacionadas



No hay comentarios:

Publicar un comentario