Mostrando entradas con la etiqueta md5. Mostrar todas las entradas
Mostrando entradas con la etiqueta md5. Mostrar todas las entradas

2023/07/09

MD5 como IOC apesta

Qué es un IOC

Cuando hay ataque, en sus distintas etapas, se van generando acciones y artefactos, que deberían quedar registradas en algún lado y que son detectables.

Por ejemplo, te envían un link malicioso, para que te descargues un archivo, que al ejecutarse accede a una url para bajar componentes adicionales, exfiltrar información o recibir órdenes. Además, se escribe en el disco como persistencia y agrega una clave en el registro para reiniciarse cuando el sistema lo haga, ¿qué tenemos?

  • Una url maliciosa
  • Un archivo malicioso
  • Más archivos maliciosos
  • Más urls
  • Claves en el registro

Si vos ves este ataque, podés tomar nota de identificadores de cada una de estas piezas y luego, ante cada intento de acceder a urls comparar con las maliciosas, al bajar, escribir o leer un archivo hacerlo con los maliciosos y revisar el registro buscando claves.... maliciosas.


El caso particular del archivo no es muy práctico tomarlo como referencia pues puede ser grande, lo mejor es calcularle un hash y guardar ese Indicator Of Compromise.

 

Pequeña pieza de recomendación

 

Lamentablemente, incluso entre la gente técnica, la falta de conocimiento detallado hace que se cometan errores de concepto, por ejemplo el que paso a reportar, pues lo he visto varias veces con varias personas.

Ponele que tenés dos herramientas que entienden IOCs, el DNS y el proxy saliente.

Te llega este IOC, ¿dónde lo ponés?

https://3984274.aws.com

Muy bien, en cualquiera o ambos.

¿Y este?

https://3984274.aws.com/api/service/34

Si tuvieras la tentación de decir lo mismo, antes mirá este:

https://www.afip.gob.ar/api/service/34

Hay personas que han dicho:

"Pongamos en ambos"

Hay dos problemas, el de seguridad y el conceptual técnico.

El de seguridad es que si para bloquear  

https://www.afip.gob.ar/api/service/34

ponés  "www.afip.gob.ar" en el DNS, generás una denegación de servicio a todo afip sólo por una ruta comprometida. Podría ser válido, razonando "si tiene esa ruta comprometida, puede estar todo el resto, no interactuemos en absoluto con afip hasta que corrijan", ok, es una decisión.

En la conversación de esa decisión es cuando detecté el problema conceptual técnico:

El DNS no entiende ni "https://" ni "/api/service/34"

La única manera de bloquear sin "daños colaterales" es en el proxy.

Sigamos....

 

Qué es MD5


Hashing es una operación que se hace sobre un dato y devuelve un hash, que es un número largo y en teoría cambia ante cualquier modificación sobre el dato. Si calculás un hash de un dato, modificás un bit, recalculás, te da otro hash.

Por alguna falta de visión, pues esto de los IOCs es relativamente reciente, se utilizan como IOCs de archivos hashes MD5, que se diseñó en 1991 y ya desde 1993 se le han hallado problemas criptográficos que en el mejor de los casos, parecería que ya desde 2010 se considera deprecado.

Digo falta de visión pues si buscamos en wikipedia IOC, el artículo nace en 2013, como que ya existían varios SHA para ese entonces, pero seguro que lo de los IOC con MD5 entraron de la mano de gente de seguridad no técnica y programadores sin conocimientos de seguridad que buscaban ahorrar espacio y tiempo de ejecución.

Ojo, siempre es fácil señalar los errores del pasado desde el futuro, pero bueno.

 

La colisión "accidental"

 

Una colisión de hash es cuando dos datos distintos dan el mismo hash. Las colisiones son esperables pues la variedad de datos es infinita y los hashes son números, en el caso de MD5 entre 0 y 2 a la 128, que aunque es un número grande, claramente es muy muy muy inferior a infinito.

 

Se podría argumentar que dada la baja probabilidad de colisión y la ventaja del ahorro de espacio y tiempo de ejecución no importa, aunque ya tenemos al menos un caso conocido.

 

Dice este reporte que no sé de dónde saqué, pero se parece a la situación habitual de malware, en este caso Trojan.Win64.CoinMiner, un minero:

https://itsafety.net/report/20200515-65ade21dc82c01972891285581d85866-servermanager-exe_process-partofthreat

Trojan.Win64.CoinMiner
Trojan.Win64.CoinMiner


Fijate que dice que la mayor parte de los antivirus no lo detecta, ¿por qué será?

Dice Microsoft, que respecto a windows debe tener una cierta autoridad

https://learn.microsoft.com/en-us/windows-server/administration/server-manager/server-manager

Microsoft
Microsoft


Puede ser que estemos hablando de distintas piezas, pero si buscás en virustotal ese hash (65ade21dc82c01972891285581d85866), te trae

 

VirusTotal
VirusTotal

De paso observá que el hash que te muestra es un SHA256, correcto.

No tengo las ganas de buscar cada versión legítima del archivo hasta encontrar el que coincide con esa firma, para demostrar fehacientemente el punto pues su demostración pasa por otro lado.

 

La colisión a propósito


Desde hace años en mis charlas muestro este ejemplo:


angel devil
angel devil

Vemos que dos programas generan distinto output, miden lo mismo, tienen la misma longitud pero son distintos, con sha256 eso no pasa.

 

sha256
sha256


La verdad verdad, no sé bien de dónde lo saqué, tampoco importa muchísimo pues se trata de criptoarqueología y no hay nadie que discuta que MD5 no sirve.

Es un programa en C con un buffer. En ese buffer hay un valor, se compila y al ejecutar se comprueba que sea ese valor. En este caso se ejecuta angel().


Luego, con fastcoll y longEgg se manipula ese buffer en la copia de angel llamada devil hasta que da el mismo hash MD5. Al ejecutar y fallar la comprobación, se ejecuta devil().

 

int main() {
  if (strcmp(dummya, dummyb) != 0) {
    return angel();
  } else {
    return devil();
  }
}



Lo que importa, es que probablemente se puedan tomar dos programas de la misma longitud, manipular una parte y hacerles concidir los hashes, con lo cual para cada ejecutable legítimo del sistema

Digo probablemente pues como script kiddie todo esto parece fácil, pero no lo he comprobado.

Me pregunto por qué no se hace y apuesto a que no vale la pena, es demasiado fácil contrarestar, leyendo el archivo de atrás para adelante para el MD5 o tomando otro SHA cualquiera basta para detectar correctamente, salvo en las herramientas más precarias, para decirles de alguna manera....

lectura invertida
lectura invertida


Para qué puede servir MD5

 

MD5 es útil en situaciones sin adversarios. Por ejemplo, tenés dos archivos del mismo tamaño y no sabés si son iguales. Si están en la misma máquina corrés cmp y listo, te lo dice. Pero si están en distintas máquinas, tendrías que copiar uno a la otra o montar en una una carpeta de otra.... mucho trabajo. El MD5 de cada archivo te puede confirmar si son distintos, no tanto que sean iguales, pero ya dije, sin adversarios.

Me ha servido para hacer unas pruebas con FPGA, tal cual relato en un montón de entradas....

Y por supuesto sirve para demostrar lo inseguro que es.

2021/12/21

Forzador MD5 en PYNQ, el software


Siguiendo los pasos detallados anteriormente, no bien creás el IP, te aparecen los drivers en:

./ip_repo/MD5_Brute_Forcer_1.0/drivers/MD5_Brute_Forcer_v1_0/src/MD5_Brute_Forcer.h

 

Los valores de desplazamiento de cada registro son:

#define MD5_BRUTE_FORCER_S_AXI_SLV_REG0_OFFSET 0
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG1_OFFSET 4
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG2_OFFSET 8
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG3_OFFSET 12
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG4_OFFSET 16
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG5_OFFSET 20
#define MD5_BRUTE_FORCER_S_AXI_SLV_REG6_OFFSET 24

 y algunas funciones

#define MD5_BRUTE_FORCER_mWriteReg( \
  BaseAddress, RegOffset, Data) \
    Xil_Out32((BaseAddress) + \
  (RegOffset), (u32)(Data))

#define MD5_BRUTE_FORCER_mReadReg(BaseAddress, RegOffset) \
    Xil_In32((BaseAddress) + (RegOffset))



XStatus MD5_BRUTE_FORCER_Reg_SelfTest(void * baseaddr_p);

 

Tras generar el bitstream, exportar el hardware, abrís la SDK

 

Comprobás que exista lo tuyo, en mi caso MD5_Brute_Forcer:

 

En system.hdf vemos MD5_Brute_Forcer
En system.hdf vemos MD5_Brute_Forcer


Creás entonces una nueva "Application Project" con template de "empty application", que te va a crear una carpeta con el nombre que le pongas con sufijo _bsp. En mi caso, en:

./md5_brute_forcer/md5_brute_forcer.sdk/brute_forcer_bsp/ps7_cortexa9_0/include/

en xparameters.h  te define:


/* Definitions for peripheral MD5_BRUTE_FORCER_0 */
#define XPAR_MD5_BRUTE_FORCER_0_DEVICE_ID 0
#define XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR 0x43C00000
#define XPAR_MD5_BRUTE_FORCER_0_S_AXI_HIGHADDR 0x43C0FFFF

tambien verás que MD5_Brute_Forcer.h ahora está en 

brute_forcer_bsp/ps7_cortexa9_0/include/MD5_Brute_Forcer.h

Creás un main.c

Un problema que no puede resolver es que los archivos xgpio.h  xgpio_l.h que proveen la resolución del nombre de las funciones más básicas de lectura y escritura en la memoria, no están siendo generados o incluidos. Lo que he hecho es tomarlos de uno de los ejercicios y copiarlos en 

brute_forcer_bsp/ps7_cortexa9_0/include/


Llegado este punto, hay que grabar el bitstream, "Xilinx" -> "Program FPGA" por un lado y ejecutar el programa por el otro con botón derecho sobre la aplicación, "Run as" -> "launch on hardware (GDB)" y usando por ejemplo 

miniterm.py  /dev/ttyUSB1  115200 


y a disfrutar de los resultados.

 

Un pipeline
Un pipeline


Dos pipelines
Dos pipelines

Cuatro pipelines
Cuatro pipelines


Como notarás:

En waited dice cuando charlyseconds demoró, va decreciendo.

En found plin plin plin at pipeline tal de tantas ves como evoluciona de 1 a 4 pipelines. El pipeline que lo halló es 1,2,4,8, si mirás el registro, entenderás.

Con los valores 200 a 203, como al valor hay que sumarle el pipeline donde fué hallado.

Y lo más notorio, hay un desplazamiento entre el valor esperado y el obtenido, es algo que siempre ha ocurrido, en las simulaciones tambien, me falta diagnosticar, por ahora se va a tener que ajustar desde el software, al igual que los primeros y últimos valores que no los puede hallar, motivo de otra entrada futura.

Y más notorio aún, para cuatro pipelines falla para un número relativamente bajo, tengo que revisar bien, pero me puede llevar mucho tiempo y como no hace a la implementación de software, por hoy no me preocupo.


Estos resultados no salen de un repollo, veamos el programa main.c, antes recordemos, recordemos, no, no recordemos, es de una entrada futura pero que estoy haciendo a la vez, en la del futuro digo que "de paso agrego más valores a los registros", pero bueno, aceptemos este efecto sobrenatural.


Registros
Registros


Lo primero es adaptar las primitiva provistas por el driver de bajo nivel a algo más usable.

Para habilitar y deshabilitar hay que escribir 1 y 0 en el registro 4, eso se obtiene de esta base y desplazamiento:


XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR

MD5_BRUTE_FORCER_S_AXI_SLV_REG4_OFFSET

Las funciones son:

void enable() {
     MD5_BRUTE_FORCER_mWriteReg(
         XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
         MD5_BRUTE_FORCER_S_AXI_SLV_REG4_OFFSET, 1
         );
}

void disable() {
    MD5_BRUTE_FORCER_mWriteReg(
      XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
      MD5_BRUTE_FORCER_S_AXI_SLV_REG4_OFFSET, 0
    );
}


Para escribir el hash hay que escribir 4 registros de 32 bits en los registro 0 a 3:

void setHash(unsigned int r3,
             unsigned int r2,
             unsigned int r1,
             unsigned int r0) {
    MD5_BRUTE_FORCER_mWriteReg(
       XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
       MD5_BRUTE_FORCER_S_AXI_SLV_REG3_OFFSET, r3
    );
    MD5_BRUTE_FORCER_mWriteReg(
       XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
       MD5_BRUTE_FORCER_S_AXI_SLV_REG2_OFFSET, r2
    );
    MD5_BRUTE_FORCER_mWriteReg(
       XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
       MD5_BRUTE_FORCER_S_AXI_SLV_REG1_OFFSET, r1
    );
    MD5_BRUTE_FORCER_mWriteReg(
       XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
       MD5_BRUTE_FORCER_S_AXI_SLV_REG0_OFFSET, r0);
}



Para leer el resultado, está en el registro 5:


unsigned int readTarget() {
        return MD5_BRUTE_FORCER_mReadReg(
         XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
         MD5_BRUTE_FORCER_S_AXI_SLV_REG5_OFFSET );
}


Para leer cuantos pipelines hay y cuál tuvo el hit, hay que leer el registro 6 y extraer los bits necesarios:


unsigned int readStatus() {
     return MD5_BRUTE_FORCER_mReadReg(
          XPAR_MD5_BRUTE_FORCER_0_S_AXI_BASEADDR,
          MD5_BRUTE_FORCER_S_AXI_SLV_REG6_OFFSET
     );
}


Lo mismo para la cantidad de pipelines y cuá halló el resultado:

unsigned int readPipelineCount() {
    unsigned int status = readStatus();
    return ( status & 0x0000f000 ) >>12;
}

unsigned int readPipelineHit() {
    unsigned int status = readStatus();
    return status >> 16;
}


Para mostrar los bits de estado:

void printStatus(const char * tab) {
    unsigned int status = readStatus();
    printf("%s",tab);
    if (status & 0x1)  printf("paused ");
    if (status & 0x2)  printf("running ");
    if (status & 0x4)  printf("warming ");
    if (status & 0x8)  printf("found ");
    if (status & 0x10) printf("done ");
    if (status & 0x20) printf("enabled ");
    if (status == 0) printf("errorNoStatus ");

    printf("\r\n");
}


Y eso es todo, otra vez anticipando el futuro, cuando pueda prescindir de los printf probablemente pueda usar un microblaze en la Nexys4DDR, quizás a la vez hacer un IP sin AXI pues no creo que entren los ocho pipelines y el microblaze a la vez.






2021/12/20

Forzador MD5 en PYNQ, el hardware

Este es el detalle del trabajo práctico de la materia Micro Arquitecturas y Soft Cores, ya comentada previamente.

El objetivo es instanciar el Forzador Brutal de MD5 en un chip ZYNQ de una placa PYNQ-Z2 proveniente de una Nexys4DDR que es más grande, ya habiendo fracasado durante la cursada de Basic Digital Design agregando VIO e ILA como interfaz. En esta oportunidad la interfaz será AXI hacia los cores donde se ejecutará un programita escrito en C.

 

Pasos

Necesito saber previamente algunos datos:

Número de registros para el periférico AXI

  • para determinar el hash (input, 128 bits, 16 bytes, 4 registers )
  • para obtener el valor (output, 32 bits, 4 bytes, 1 register)
  • control para iniciar proceso (input, 1 bit, 1 register)
  • status (output 3 bits, 1 register)
    • running
    • found
    • done

Son 7 registros.

 

Respecto a estos pasos, tengo un fuerte Déjà vu, es que ya lo he hecho antes pero entendiendo menos al hacer los ejercicios de dos libros.

 

Crear el IP

  • Tasks -> manage IP -> New IP Location
    • Part: xc7z020clg400-1
    • Target Language: Verilog
    • Crear y seleccionar una carpeta, en esta aparecerá todo lo que cree Vivado
  •  Tools -> Create and Package New IP
    • Create a new AXI4 peripheral
    • Name: ip_md5
    • Number of registers:7
    • Interfaces: quizás quitar el "00"
    • Next Step: Edit IP
  • Copiar los archivos del proyecto original a  ip_repo/ip_md5_1.0/hdl
    • Agregarlos con Add Design Sources
      • mejor NO copy sources into IP Directory
    • en este momento
      • ya se ha creado el C con los drivers de bajo nivel
      • es buena idea hacer un commit
  • Editar el ip de S_AXI, ip_md5_v1_0_S_AXI_inst
    • Acá va la magia, ver luego
  • Flow -> Run Synthesis
  • Flow -> Package IP 
    • identification
      • add category
    • file groups
      • merge changes from file groups wizard
    • customization parameters
      • merge changes from customization parameters wizard
    • ports and interfaces
      • hay que ver que esté lo que hayas puesto, en este caso status
    •  review and package
      • re-package IP
  • Otro buen momento para un commit 
  • close project 

 

 Crear el proyecto

  • File -> project -> new
    • location lo puse a la par que ip_repo
    • varios next
    • choose board -> pynq-z2
    • varios next
  • Settings 
    • Project Settings
      • IP
        • Repository -> ADD
          • lo del paso Crear IP
  • IP Integrator -> Create block design
  • Add IP -> zynq
  • run block automation
  • re-customize IP ( doble click)
    • ps-pl configuration 
      • general
        • enable clock resets
          • fclk_reset0_n
      • Axi non secure enablement
        • gp master axi interface
          • m axi gp0 interface ON
    • MIO Configuration
      • I/O Peripherals
        • UART 0 -> MIO 14::15
    • clock configuration
      • pl fabric clocks
        • fclk_clk0 -> 100
  • Add  IP ->
  • run connection automation
  • si tenías ports
    • make external
    • agregar el xdc y ajustarlo
  • validate design
  • source ->design_1.bd -> create hdl wrapper
  • source -> design_1.bd -> generate output products (paciencia)
  • Program and debug
    • generate bitstream (más paciencia)
  • file
    • export
      • export hardware -> include bitstream
  • file
    • launch sdk

SDK

  • comprobar en system.hdf que esté la IP y la uart
  • comprobar que en wrapper platform esten los drivers 
  • file
    • new
      • application project
        • empty application template
        • comprobar bsp-> ps7_cortexa9_0-> include -> ip_md5.h
  • el programa en C que quieras, otro día vemos más detalles.
  • botón derecho sobre el proyecto -> run as -> launch on hardware (GDB)
  • un segundo antes abriste la terminal:
    • miniterm.py  /dev/ttyUSB1  115200
    • y para un programa muy sencillo:

printf("MD5 Brute Forcer SelfTest\r\n");
   MD5_ACCELERATOR_Reg_SelfTest(
   XPAR_MD5_ACCELERATOR_0_S_AXI_BASEADDR
);

    • una salida muy sencilla:

Salida seft test
Salida seft test


Detalles

 

En el comienzo me equivoqué con el conteo de registros, pensé que eran 6, sin embargo fue intutitiva la corrección del hdl generado. Como por otros motivos tuve que arrancar de cero, al comparar la nueva generación con lo corregido comprobé que la corrección fué correcta.


Respecto a la paciencia en "Generate Output Products", nunca avisa que termina y arriba a la derecha el iconito de trabajando queda activo y dice "Synthesis out-of-date_". La manera de saber cuando ha terminado es mirando en la ventanita de Design Runs.


output products corriendo
output products corriendo

output products corriendo
output products finalizado


La magia


Primer nivel del wrapper


Si tu IP no usa puertos, no hay nada que tocar. En mi caso, para diagnosticar puse unos puertitos y se los conecté.

va en la interfaz:

      output wire [5:0] status,

en la instanciación de S_AXI:

      .status(status),

 

Segundo nivel del wrapper


Si tu IP no usa puertos, no hay nada que tocar en la interfaz, en este caso si, entonces:

        output wire [5:0] status,

Luego van los registros de salida:

    // Add extra user logic register here
    
    wire [C_S_AXI_DATA_WIDTH-1:0]    target_o;
    wire [C_S_AXI_DATA_WIDTH-1:0]    status_o;

    // I/O Connections assignments

En donde se leen, hay que reemplzar los registros slaves por estos nuevos:

// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
    3'h0   : reg_data_out <= slv_reg0;
    3'h1   : reg_data_out <= slv_reg1;
    3'h2   : reg_data_out <= slv_reg2;
    3'h3   : reg_data_out <= slv_reg3;
    3'h4   : reg_data_out <= slv_reg4;
    3'h5   : reg_data_out <= target_o;
    3'h6   : reg_data_out <= status_o;
    default : reg_data_out <= 0;
endcase


Y por último, la conexión de cada bit de status al registro de status y la instanciación de nuestro IP:

// Add user logic here
    assign status = {status_o[0],status_o[1],
    status_o[2],status_o[3],
    status_o[4],status_o[5]};
       
    driver driver (
       .CLK(S_AXI_ACLK),
       .CPU_RESETN(S_AXI_ARESETN),
       .target_selected({slv_reg3,slv_reg2,slv_reg1,slv_reg0}),
       .enable_switch(slv_reg4[0]),
       .target(target_o),
       .status_paused(status_o[0]),
       .status_running(status_o[1]),
       .status_warming(status_o[2]),
       .status_found(status_o[3]),
       .status_done(status_o[4]),
       .enabled(status_o[5])
    );
    // User logic ends

 

Cuando metés la pata en el IP


La recomendación que he recibido y que sufrí no respetar, es si algo está mal, empezar otra vez de cero. Me parece que no hace falta tanto como de cero, estoy seguro que cuando incorporé los puertos de diagnóstico no lo hice, pero si tirar la sdk.

 

Edición del IP


  • Tasks -> manage IP -> Open IP Location
  • elegir el ip -> edit in IP Packager
  • Corregir el HDL
    • Acá va la magia, corregida
  • Flow -> Run Synthesis
  • Flow -> Package IP 
    • quizás File Groups
    •  review and package
      • re-package IP

 

Editar el proyecto


  • File -> project -> recent
  • IP Integrator -> open block design
    • aparece un mensaje diciendo que hay que hacer upgrade
    • report ip status
      • upgrade selected
    • te ofrece generate output products (paciencia)
    •  
  • Program and debug
    • generate bitstream (más paciencia)
  • [opcional] borrar el .sdk para evitar conflictos
  • file
    • export
      • export hardware -> include bitstream
  • file
    • launch sdk


Respecto a borrar el sdk antes de reexportar, no te olvides de tener copia o todo versionado para recuperar lo que ya hayas hecho. Debido a que mi IP usa Xil_In  y Xil_out, provistos por xgpio.h, cada vez tuve que restaurarlos, obtenidos originalmente de lo generador por uno de los labs.

 

Simulación


Agregué un test bench conectado al componente y a ese testbench lo puse como top. Si ejecutas la simulación con el wrapper como top, estás muerto, tendrías que generar los estímulos para AXI, olvidate...



Notas relacionadas por el lado de EAMTA 2021:

 

2021/03/24

Refactorizaciones de MD5 en FPGA: 4 algunos arreglitos

Venís de poder ingresar hashes arbitrarios.


Ajustes de los probes

Había dicho que el haber partido el probe de 128 bits en dos de 64 no me quitaba el sueño y habrás pensado que había algo más.

Pues si, tengo en mi desván mental la idea de AXI <=> 32 bits, así que escarbé superficialmente y en algún manual encontre que:

 ARM expects that:

    the majority of components use a 32-bit interface

    only components requiring 64-bit atomic accesses use a 64-bit interface.
 


y no veo motivo para contrariar a ARM, así que partir los 128 en cuatro de 32 medio que estaba en mis planes, sólo me quedó el resentimiento de no haber podido comprender si VIO/Logic Analyzer no se llevan bien con 128 bits o fue a raiz de mi error que no pude usar un probe de 128 bits, eso está en la entrada anterior, olvidémoslo.

Sintetizando, voy a partir en cuatro los 128 bits porque parece ser lo más sencillo para el futuro, que no sé si llegará.

De paso, para poder automatizar toda la interacción, pase el botón de reset a VIO, una pena, me gustaba usar el botón del reset de la CPU...

Partido en cuatro:

// virtual inputs
   wire           enable_switch;
   wire           reset_switch;  
   wire [  4 : 0] probe_in0;
   wire [ 31 : 0] probe_in1;
   wire [  1 : 0] probe_out0;
   wire [ 31 : 0] probe_out1;
   wire [ 31 : 0] probe_out2;
   wire [ 31 : 0] probe_out3;
   wire [ 31 : 0] probe_out4;
  
// VIO patching
   assign probe_in0  = { status_paused,
                         status_running,
                         status_warming,
                         status_found,
                         status_done
                       };
   assign probe_out0 = { reset_switch,
                         enable_switch
                       };
   assign probe_in1  = target;
  

vio_0 vio_driver (
  .clk(CLK),                // input wire clk
  .probe_in0(probe_in0),    // input wire [4 : 0] probe_in0
  .probe_in1(probe_in1),    // input wire [31 : 0] probe_in1
  .probe_out0(probe_out0),  // output wire [0 : 0] probe_out0
  .probe_out1(probe_out1),  // output wire [31 : 0] probe_out1
  .probe_out2(probe_out2),  // output wire [31 : 0] probe_out2
  .probe_out3(probe_out3),  // output wire [31 : 0] probe_out3
  .probe_out4(probe_out4)   // output wire [31 : 0] probe_out4 
);


Testing con Tcl

Dejando de lado la simulación, para testear sobre todo al comienzo utilicé métodos muy cavernícolas, directamente sobre la placa, usando la opción de avanzar el contador de a un paso con un botón y me parece que tambien inspeccionando con los 8 dígitos de 7 segmentos estados intermedios.

Ahora, con VIO, puedo tanto con la GUI interactuar de modo más sencillo y el valor resultante copiarlo y pegarlo en lugar de transcribirlo a mano, un cierto progreso.

Pero seguiría siendo un cavernícola... mejor digo, siendo un cavernícola, voy a pasar de la Edad de Piedra a la Edad de Bronce e interactuar con VIO usando la consola Tcl, desde la cual puedo ejecutar scripts tanto con comandos como con lógica.

Lo primero es descifrar cómo se lee y escribe, que es muy sencillo, en Vivado Design Suite User Guide: Programming and Debugging (UG 908) desde la página 205 hay algunas explicaciones y además mirando lo que Vivado va ejecutando en la consola no podés dejar de entender algo, sólo que lleva un tiempito.

Por ahora y quizás por siempre, no me interesan los detalles y el protocolo subyacente, lo usaré tipo caja negra. Mi POC consiste en poder ejecutar un ciclo completo de reset, cargar hash, habilitar, deshabilitar, evaluar si fué hallado y mostrarlo.

El formato de este blog no es el mejor para mostrar las sucesivas refactorizaciones, agradeceré tu mejor esfuerzo.

Si es tu primer contacto con Tcl:

set variable valor  equivale a variable = valor 

set variable1 $variable2  equivale a variable1 = variable2

funcion arg1 arg2 equivale a funcion(arg1, arg2)

set var [ funcion arg1 arg2 ] equivale a var = funcion(arg1, arg2)

En general, la clave de entender Tcl es que son todas funciones, incluido el set, if, while:

while condicion acción : la función while recibe dos argumentos.

 

Con respecto a {}, yo sabía programar bastante bien en Tcl, pero eso fue hace 20 años y para esto no necesito recuperar tanto, así que no estoy en condiciones de explicarlo responsablemente. Podés RTFM y ver la previous y next lesson. Si más o menos sabés programar en dos o tres lenguajes, tu sistema de pattern recognition seguramente te permitirá entender todo lo que sigue.

Versión base

En esta versión, pegué todo el código sin mayor pensamiento en escalabilidad:


# Deshabilitar el contador

set_property OUTPUT_VALUE 0 [get_hw_probes enable_switch]
commit_hw_vio [get_hw_probes {enable_switch}]
 

# Apretar y soltar el botón de reset


startgroup
set_property OUTPUT_VALUE 0 [get_hw_probes reset_switch]
commit_hw_vio [get_hw_probes {reset_switch}]
endgroup

startgroup
set_property OUTPUT_VALUE 1 [get_hw_probes reset_switch]
commit_hw_vio [get_hw_probes {reset_switch}]
endgroup


# Cargar las cuatro partes del hash

set_property OUTPUT_VALUE 2d1bbde2 [get_hw_probes probe_out1]

commit_hw_vio [get_hw_probes {probe_out1}]

set_property OUTPUT_VALUE acac0afd [get_hw_probes probe_out2]
commit_hw_vio [get_hw_probes {probe_out2}]

set_property OUTPUT_VALUE 07646d98 [get_hw_probes probe_out3]
commit_hw_vio [get_hw_probes {probe_out3}]

set_property OUTPUT_VALUE 154f402e [get_hw_probes probe_out4]
commit_hw_vio [get_hw_probes {probe_out4}]

# Habilitar el contador

set_property OUTPUT_VALUE 1 [get_hw_probes enable_switch]
commit_hw_vio [get_hw_probes {enable_switch}]

set status_done 0

while {$status_done == 0} {

      # Leer el valor de status_done

    refresh_hw_vio [get_hw_vios {hw_vio_1}]
    set status_done [ get_property INPUT_VALUE [get_hw_probes status_done] ]
    after 1000
}

      # Leer el valor de status_found

set status_found [ get_property INPUT_VALUE [get_hw_probes status_found] ]


if {$status_found == 1} {

      # Leer el valor hallado

    refresh_hw_vio [get_hw_vios {hw_vio_1}]
    set result [ get_property INPUT_VALUE [get_hw_probes target] ]
    puts $result
} else {
    puts "Not found"
}

# Deshabilitar el contador


set_property OUTPUT_VALUE 0 [get_hw_probes enable_switch]
commit_hw_vio [get_hw_probes {enable_switch}]


Primera refactorización: procedures


Tenemos un nuevo elemento, proc, que recibe tres argumentos: nombre del procedimiento, argumentos y código a ejecutar.

Con respecto a startgroup/endgroup, me desasné en UG 835 Vivado Design Suite TclCommand Reference Guide, del cual no tengo link, pues para variar usé "Documentation Navigator", una simpática aplicación que te permite buscar documentación y en el caso de los pdf te muestra la última versión y opcionalmente anteriores y te los va bajando a ~/Documents/XilinxDocs/Vivado/documentation/sw_manuals/xilinx2019_2, en este caso. Me imagino que depende de lo que bajes en lugar de 2019_2 dirá otra cosa.

Me resulta medio raro no tener bajados los archivos junto al resto de los miles de manuales, pero es bastante cómodo, creo que voy a tirar un symlink para tener lo mejor de los dos mundos.

Volviendo a xxxgroup, viene a ser parece para hacer transacciones reversibles, por ahora las voy a dejar, pero en la próxima iteración probaré quitarlas.

 

proc reset {} {
    startgroup
    set_property OUTPUT_VALUE 0 [get_hw_probes reset_switch]
    commit_hw_vio [get_hw_probes {reset_switch}]
    endgroup

    startgroup
    set_property OUTPUT_VALUE 1 [get_hw_probes reset_switch]
    commit_hw_vio [get_hw_probes {reset_switch}]
    endgroup
}

proc disable {} {
  set_property OUTPUT_VALUE 0 [get_hw_probes enable_switch]
  commit_hw_vio [get_hw_probes {enable_switch}]
}

proc enable {} {
  set_property OUTPUT_VALUE 1 [get_hw_probes enable_switch]
  commit_hw_vio [get_hw_probes {enable_switch}]
}

proc writeHash {hash1 hash2 hash3 hash4} {
  set_property OUTPUT_VALUE $hash1 [get_hw_probes probe_out1]
  commit_hw_vio [get_hw_probes {probe_out1}]

  set_property OUTPUT_VALUE $hash2 [get_hw_probes probe_out2]
  commit_hw_vio [get_hw_probes {probe_out2}]

  set_property OUTPUT_VALUE $hash3 [get_hw_probes probe_out3]
  commit_hw_vio [get_hw_probes {probe_out3}]

  set_property OUTPUT_VALUE $hash4 [get_hw_probes probe_out4]
  commit_hw_vio [get_hw_probes {probe_out4}]
}

proc readStatus {} {
  refresh_hw_vio [get_hw_vios {hw_vio_1}]
  return [ get_property INPUT_VALUE [get_hw_probes status_done] ]
}

proc readFound {} {
  refresh_hw_vio [get_hw_vios {hw_vio_1}]
  return [ get_property INPUT_VALUE [get_hw_probes status_found] ]
}

proc readResult {} {
  refresh_hw_vio [get_hw_vios {hw_vio_1}]
  return [ get_property INPUT_VALUE [get_hw_probes target] ]
}

#inicio

reset

disable

writeHash 2d1bbde2 acac0afd 07646d98 154f402e

enable

set status_done 0

while {$status_done == 0} {

    set status_done [ readStatus ]
    after 1000
}

set status_found [ readFound ]


if {$status_found == 1} {
    set result [ readResult ]
    puts [ readResult ]
} else {
    puts "Not found"
}


disable

 

Segunda refactorizacion: más usable

Evité repeticiones y toma el hash en una sola pieza:


proc readProbe {pin} {
  refresh_hw_vio [get_hw_vios {hw_vio_1}]
  return [ get_property INPUT_VALUE [get_hw_probes $pin] ]
}

proc writeProbe {value pin } {
    set_property OUTPUT_VALUE $value [get_hw_probes $pin]
    commit_hw_vio [get_hw_probes ${pin}]
}

proc reset {} {
  writeProbe 0 reset_switch
  writeProbe 1 reset_switch
}

proc disable {} {
  writeProbe 0 enable_switch
}

proc enable {} {
  writeProbe 1 enable_switch
}

proc writeHash {hash} {
  writeProbe [ string range $hash  0  7 ] probe_out1
  writeProbe [ string range $hash  8 15 ] probe_out2
  writeProbe [ string range $hash 16 23 ] probe_out3
  writeProbe [ string range $hash 24 31 ] probe_out4
}

proc readResult {} {
  return [ readProbe target]
}

proc isDone {} {
  return [ readProbe status_done ]
}

proc isFound {} {
  return [ readProbe status_found ]
}

reset

disable

writeHash 2d1bbde2acac0afd07646d98154f402e

enable

while { ! ( [ isDone ] ) } {
    after 1000
}

if { [ isFound ] } {
    puts [ readResult ]
} else {
    puts "Not found"
}

disable

 

Lo próximo será utilizar el código anterior para ejectuar muchas búsquedas para valores interesantes y así ver si hay errores (los hay en los extremos) y ajustar correctamente el valor obtenido:


El código en github

Notas relacionadas


 

Refactorizaciones de MD5 en FPGA: 3 VIO total

Veníamos de reemplazar algunos switches y leds por VIO.

Llegó el momento de hacer que este juguete sea más productivo, para eso hace falta poder ingresar hashes arbitrarios en pocos segundos o menos en lugar de regenerar el bitstream en decenas de minutos.

La idea es eliminar el selector de hashes hardcodeados e ingresar el hash a buscar de alguna manera. Sería bastante complicado hacerlo físicamente, habría que conectar 128 switches o ir cargando por tandas, demasiado trabajo y muy fácil equivocarse al cargar.

Pero, ¿qué son 128 switches virtuales, que además entienden hexadecimal? Gracias a VIO puedo ahorrarme la interfaz de usuario.

Aunque mucho mejor que la opción física, para probar muchos valores es un tanto tedioso, luego exploraré interactuar programáticamente con el VIO y así poder testear no una simulación sino la realidad misma.

Primero lo primero, el VIO manual.

 

VIO total
VIO total


Hay que quitar los selectores y enchufarle el VIO a la entrada de los hashes.

 

Para modificar el VIO hay que hacerle doble click al xci:

Seleccionar el xci
Seleccionar el xci


Y ajustar los nuevos valores:


Nueva probe
Nueva probe, fijate como me equivoqué con ese 127


 

También hay que cambiar la interfaz del driver tambien, antes entraban cuatro bits, ahora son 128 en su lugar.

Los assign para que VIO muestre nombres útiles:


   wire [   4 : 0] probe_in0;
   wire [  31 : 0] probe_in1;
   wire [   4 : 0] probe_out0;
   wire [ 127 : 0] probe_out1;

   assign probe_in0  = {
            status_paused,
            status_running,
            status_warming,
            status_found,
            status_done
   };
   assign probe_out0 = enable_switch;
   assign probe_in1  = target;
   assign probe_out1 = target_selected;

 

La nueva interfaz del VIO:


vio_0 vio_driver (
 .clk(CLK),               // input wire clk
 .probe_in0(probe_in0),  // input wire [4 : 0] probe_in0
 .probe_in1(probe_in1),  // input wire [31 : 0] probe_in1
 .probe_out0(probe_out0),// output wire [0 : 0] probe_out0
 .probe_out1(probe_out1) // output wire [127 : 0] probe_out0
);

Recordá que es output respecto al VIO, va a un input del diseño.

La nueva interfaz del driver:


driver u_driver(
  .CLK            (CLK),      
  .CPU_RESETN     (CPU_RESETN),     
  .enable_switch  (enable_switch),     
  .target_selected(target_selected),
  .target         (target),
  .status_paused  (status_paused),
  .status_running (status_running),
  .status_warming (status_warming),
  .status_found   (status_found),
  .status_done    (status_done)    
  );

Hasta acá parecía fácil, pero cuando estaba corrigiendo el ancho de VIO se cerró Vivado y aunque luego lo corregí y parecía todo ok, de modo efectivo quedó de 127 bits en lugar de 128. O mejor dicho, aceptó el assign probe_out1 = target_selected pero lo tomó de 127 bits y me dejó uno aparte, si hubiera sido el más significativo pude haber lidiado pero al ser el menos significativo rompe toda la representación hexadecimal.


Tuve que pedirle "Regenerate Output Products" en Design Runs pero los tomó de la cache:

 

Cached
Cached

Tiré la IP y volvíendolo a incorporar nuevamente me ignoró hasta que encontré como limpiar la cache, en particular vía GUI:

  • Project manager
  • Settings
  • IP
  • IP Cache
  • Clear Cache

Y...

no, falló.

Pensé que era por el assign, lo quité.

Tiré todo el proyecto salvo los .v y el xpr

Partí el probe en dos de 64 bits...

Ok, anda, hora de ver el manual. Virtual Input/Output v3.0 LogiCORE IP Product Guide (PG 159) no es, sólo explica el IP, obvio, el problema está en Vivado logic analyzer, cuya documentación en un framentito de Vivado Design Suite User Guide: Programming and Debugging (UG 908) desde la página 205, no veo nada que indique esa limitación.

Como sea, debido a ese error del comienzo, no sé si Vivado quedó "contaminado" o justo accidentalmente hay una limitación o bug que impide manipular un probe de 128 bits.

Quedó todo muy parecido a lo expuesto anteriormente, salvo que en lugar de un probe de 128 bits tengo dos de 64, no me quita el sueño.

   wire [ 4 : 0] probe_in0;
   wire [31 : 0] probe_in1;
   wire [ 4 : 0] probe_out0;
   wire [ 63 : 0] probe_out1;
   wire [ 63 : 0] probe_out2;
  

vio_0 vio_driver (
  .clk(CLK),                // input wire clk
  .probe_in0(probe_in0),    // input wire [4 : 0] probe_in0
  .probe_in1(probe_in1),    // input wire [31 : 0] probe_in1
  .probe_out0(probe_out0),  // output wire [0 : 0] probe_out0
  .probe_out1(probe_out1),  // output wire [63 : 0] probe_out1
  .probe_out2(probe_out2)   // output wire [63 : 0] probe_out2
);

driver u_driver(
  .CLK            (CLK),      
  .CPU_RESETN     (CPU_RESETN),     
  .enable_switch  (enable_switch),     
  .target_selected({probe_out1,probe_out2}),
  .target         (target),
  .status_paused  (status_paused),
  .status_running (status_running),
  .status_warming (status_warming),
  .status_found   (status_found),
  .status_done    (status_done)    
  );


Veamos la secuencia completa de un reset por placa, cargar el hash en el VIO, ejecución y hallazgo.

Reset, el circuito está en modo "paused", target en cero.

reset
reset


Tras ingresar en la consola tcl los comandos:

set_property OUTPUT_VALUE d96ff7938f84d310 [get_hw_probes probe_out1 -of_objects [get_hw_vios -of_objects [get_hw_devices xc7a100t_0] -filter {CELL_NAME=~"vio_driver"}]]
commit_hw_vio [get_hw_probes {probe_out1} -of_objects [get_hw_vios -of_objects [get_hw_devices xc7a100t_0] -filter {CELL_NAME=~"vio_driver"}]]
set_property OUTPUT_VALUE c9d25805bfdbabb2 [get_hw_probes probe_out2 -of_objects [get_hw_vios -of_objects [get_hw_devices xc7a100t_0] -filter {CELL_NAME=~"vio_driver"}]]
commit_hw_vio [get_hw_probes {probe_out2} -of_objects [get_hw_vios -of_objects [get_hw_devices xc7a100t_0] -filter {CELL_NAME=~"vio_driver"}]]


queda el valor del hash en probe_out1 y probe_out2:


hash cargado
hash cargado

Al oprimir en VIO enable_switch, pasa al estado "running" tras un efímero "warming":


buscando
buscando

Como halla el valor, queda en "done" y "found":


hallado
hallado

Fijate que el valor es el esperado "BEBA_CAFE" + 0x220, pronto lidiaremos con ello.

El código en github


Notas relacionadas



2021/03/21

Refactorizaciones de MD5 en FPGA: 2 VIO

El contexto de esta nota está en la anterior.

Habiendo recuperado el proyecto y migrado a Vivado 2019.2 (cosa que me olvidé de mencionar antes) y hecho algunas correcciones y la simulación, la siguiente tarea es agregar VIO.

 

VIO es Virtual Input Output, un IP que te permite interactuar desde Vivado con la placa conectándole entradas y salidas virtuales, esto es muy útil para interactuar con una placa que no tiene leds, botones y switches, por ejemplo las que están en AWS. Si a esto le sumanos el programa hw_server, que permite "compartir" una placa en red, podemos tener la placa en casa y con un tunel ssh programarla y verla en acción desde afuera. Si le sumanos un par de dólares la hora, podemos probarlo en AWS, me han dicho que se hace así o similar, pero, otro día.

El escenario que más me habilita ahora es migrar de la Nexys4DDR que tiene 8 dígitos de siete segmentos, 16 leds, 16 switches y 5 botones a la PYNQ que tiene 4 leds, 2 switches y 4 botones o incluso a la Parallella, que tiene... nada.

De no utilizar VIO para poder interactuar con el circuito debería hacerlo vía AXI desde las CPUs de las ZYNQ (PYNQ y Parallella son ZYNQ), cosa que sé a un nivel muy elemental según he practicado en PYNQ en el 2020, pero es el objetivo final de esta seguidilla de pruebas, no nos adelantemos.

Pasamos de un una interfaz muy sencilla para ver en el resultado gracias a los 8 dígitos de 7 segmentos pero muy díficil de cambiar el hash a buscar, por ahora tengo unos precargados  que se seleccionan con 4 switches.

 

Para aplicar VIO hay que agregarlo con IP Catalog y definir sus características.

Recordá que los pasos que anoto acá son más un machete para mí que un tutorial para vos, así que no capturo paso a paso, blogger no es una buena herramienta para hacer esto, quizás junte ganas y haga un video en algún momento.


IP Catalog ->

Buscar VIO

Solapa General options
   Input probe count (en mi caso dos pues quiero separar el contador del estado)
   Output probe count

Solapa PROBE_IN ports
   ajustar cantidad de bits
Solapa PROBE_OUT ports
   ajustar cantidad de bits y valores iniciales


Observá que el input y output es respecto al VIO, es complementario a tus inputs y outputs.


Al igual que en el caso de la simulación, he optado por crear un nuevo top que instancie al existente en lugar de incorporar el VIO en el top original. De un modo u otro, en el template obtenés lo que hay que pegar en tu código:

 

Project manager
  sources
    IP
      vio_0
        instantation template
          vio_0.vho (VHDL)
          vio_0.veo (Verilog) 

 

vio_0 vio_driver (
 .clk(CLK),              // input  wire clk
 .probe_in0(probe_in0),  // input  wire [ 4 : 0] probe_in0
 .probe_in1(probe_in1),  // input  wire [31 : 0] probe_in1
 .probe_out0(probe_out0) // output wire [ 4 : 0] probe_out0
);

Customize IP
Customize IP

 

 

 

Salvo el clock y las entradas y salidas que quieras conservar de la placa, hay que eliminarlas del xdc y pasarlas al nuevo top. Dejé el botón de reset:


## 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 }];

##Buttons
set_property -dict { PACKAGE_PIN C12   IOSTANDARD LVCMOS33 }
      [get_ports { CPU_RESETN }];



Podés conectar directamente probe_in y probe_out, pero si usás unos wires en el medio, luego la vista de VIO se dá cuenta y usa esos nombres, que es mucho más cómodo.

 

 

   wire [ 4 : 0] probe_in0;
   wire [31 : 0] probe_in1;
   wire [ 4 : 0] probe_out0;
  
   assign probe_in0  = { status_paused,
                         status_running,
                         status_warming,
                         status_found,
                         status_done };
   assign probe_out0 = { enable_switch, target_switch };
   assign probe_in1  = target;

 


Se puede apreciar que el VIO ocupa poco lugar:

 

VIO footprint
VIO footprint
 

 

Lo que cambia con respecto a la grabación normal es que en el diálogo ahora hay un archivo extra, que es el VIO:

 

 

Archivo extra
Archivo extra



 Así quedan las conexiones entre el top original y VIO:


VIO conectado
VIO conectado
 


Esa diferencia entre [29:0] y [31:0] en counter es por que, recordá, los tres bits menos significativos no son parte del contador, dependen de a que pipeline nos estemos refiriendo, así que le ponemos 0:

 

assign target = {counter_out,3'b000};

 

Esa característica me hizo perder varias horas pues la adaptación la estaba haciendo en el VIO en lugar de lo más cerca posible de la diferencia: lección, hay que ser lo menos inteligente posible, si hay algo raro, debe ser lo más reducido posible.

Para comprender y corregir, tuve que volver un paso atrás a simular, pero sin el VIO, sólo con el cambio que éste había producido. Queda pendiente simular con VIO si es que es posible.

En algún momento haré que se pongan los tres bits del pipeline que lo halló.

Al descargar el bitstream se abre un dashboard que desaparece si te vas al Project Manager, pero regresa si volvés a Program & Debug.

VIO en acción
VIO en acción

 


Si no hubiera puesto los wires que mencioné antes, diría probe_xxx, menos legible.


El mensaje original es 0xBEBACAFE, pero tal como he mencionado, este diseño trae un valor cercano, me falta investigar bien esa diferencia para compensarla, pero para elllo, necesito pasar al siguiente nivel, sacarle realmente el jugo al VIO, voy a prescindir del selector de hashes e ingresar hashes arbitrarios... en la próxima entrada.

El código en github

Notas relacionadas





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