2021/03/28

Agujeros analógicos para preservación de webinares.

Los webinars existian desde antes que el covid, pero no les prestaba mucha atención por que prefiero las actividades presenciales. Como el covid prefiere los webinars, en este último año tuve que desarrollar algunos métodos y programas, no sólo para webinars sino para remoto en general, como xzoom y usar dos máquinas a la vez.

Una comodidad que presentan, cuando quedan grabados es poder "asistir" aunque tengas un conflicto horario. A veces se pueden bajar y otras no y el problema es que no me gusta mucho quedar sujeto a la voluntad de quien pueda dejar de publicar la grabación.

Dado que no nos importa mucho la calidad pues lo que nos interesa son los conceptos, vamos a explotar lo que se llama el agujero analógico que básicamente dice algo así como:

No importa cuánto ni cómo intentes proteger un contenido multimedia digital, siempre alguien puede rompértelo con una cámara y un micrófono.

Con respecto a la grabación en vivo es muy sencillo se hace con recordmydesktop, tengo unos scripts facilitadores en github, los que dicen record pero no timer, podés elegir grabar la ventana entera o de un modo un tanto indirecto un área.

El que graba por id de ventana te pide que le señales cuál y lo hace.

El que es por área, determina un área de una ventana de referencia que despues quitás. Es útil para grabar parte de una ventana, como suele ser un proyector.

Uso de máscara para recordmydesktop
Uso de máscara para recordmydesktop


En la imagen podés ver que estoy usando una terminal que tiene transparencia total para poder ver que hay debajo, hace de máscara.

Si los probás un poquito vas a llegar a la conclusión de que en realidad el que graba por área es prescindible, pero sirve para cuando querés tener el comando listo.


Hasta donde yo sé usar recordmydesktop y configurar audio, no he podido que este grabe sólo el audio del video que está grabando y esto es por que no tiene relación con la aplicación a la que está grabando, a tal punto que si le ponés otra ventana encima grabará a ésta última, recordmydesktop interactúa con xwindows y graba todo el audio. Para ello con Linux Mint Mate en Sound Preferences pongo como Input a "Monitor of ...." según a donde esté yendo el audio.

Tendría que explorar pulseaudio o lo que sea que tengas de audio para mejorar este specot o mejor me quedo con mantener la disciplina de no activar ningún otro audio mientras y además recordar no cambiar de desktop ni abrir nada nuevo que pueda aparecer encima de la región grabada.

Queda entonces muy limitada la grabación en vivo haciendo otra cosa, aunque no es imposible.

Ponele que ya tenés tu copia para futura referencia, perfecto, pero había dicho antes que quizás tenías un conflicto horario. Supongamos que estás presente con una compu pero no podés asistir, podrías grabarlo.

Nos queda grabar la grabación, que nos trae otro problema, el de cuándo terminar. Si estamos viendo a la vez o cuidadosamente haciendo otra cosa, vemos que terminó y listo, pero si nos fuimos a hacer otra cosa, va a seguir grabando luego de haber terminado la sesión.

Para esto agrego un nuevo script. Lo que hace es iniciar la grabación y la manda al background. Espera un tiempo determinado y le envía un control-C para que deje de grabar, notá que ahora sabemos cuál es la duración del video pues es una grabación.

Para enviarle el control-C, necesitamos conocer el pid.

La primera tentación se usar 

ps ax | grep recordmydesktop | cut -d" " -f1
3412

pero me ofende, es hora de progresar, usemos 

pidof recordmydesktop
3419

que es lo mismo pero más elegante.

Ambas soluciones tienen un problema, improbable pero posible, que es que haya más de una instancia de recordmydesktop en ejecución y no tenemos manera de saber cuál es la correcta.

Lo podemos arreglar con:

ps | grep recordmydesktop | cut -d" " -f2


  PID TTY          TIME CMD
 3473 pts/22   00:00:00 bash
 3486 pts/22   00:00:00 recordmydesktop
 3507 pts/22   00:00:00 ps
 4993 pts/22   00:00:00 bash

 

Pero ya estamos viendo que ese espacio adelante de ps que no está en ps ax nos va a traer problemas.

Necesitamos obtener el pid del que acabamos de ejecutar.

Como lo ejecutamos con & para mandarlo al background, podemos consultarlo con jobs -p, pero otra vez tenemos el conflicto de que pueden haber más jobs ejecutándose en este mismo proceso, aunque tambien improblable. Vamos a usar éste método quedándonos con el último job:

./script.sh &
jobs -p | tail -1
3491


¿Cómo viene el script?

  • iniciar grabación en background
  • obtener el pid
  • esperar nn segundos

Esto es con sleep segundos, por ejemplo para un video de 44:32:

sleep 2672

y sólo falta mandarle el control-C


 /bin/kill --table
 1 HUP      2 INT     3 QUIT   4 ILL     5 TRAP
 6 ABRT     7 BUS     8 FPE    9 KILL   10 USR1
11 SEGV    12 USR2   13 PIPE  14 ALRM   15 TERM
16 STKFLT  17 CHLD   18 CONT  19 STOP   20 TSTP
21 TTIN    22 TTOU   23 URG   24 XCPU   25 XFSZ
26 VTALRM  27 PROF   28 WINCH 29 POLL   30 PWR
31 SYS
    


Fijate que uso /bin/kill, esta es la versión por id:

TIME=$(( $1 * 60 + $2 ))
echo "Select which window to record"
echo "Press CRTL-C to cancel"
echo
ID=$( xwininfo | grep "Window id" \
               | cut -d : -f 3 | cut -d" " -f 2 )
echo "Recording $1:$2 ($TIME seconds)"
echo "Press CRTL-C to stop recording"
echo -n "Starts in 5 "
sleep 5

recordmydesktop --windowid=$ID &
RMDPID=$( jobs -p | tail -1 )

sleep ${TIME}
/bin/kill -2 ${RMDPID}


Esta por área:


TIME=$(( $1 * 60 + $2 ))
echo "Put a window over the area that you want to record and select it"
echo "Press CRTL-C to cancel"
echo

INFO=$( xwininfo  )

WIDTH=$( echo "$INFO" | grep Width: | cut -d " " -f 4 )
HEIGHT=$( echo "$INFO" | grep Height: \
                       | cut -d " " -f 4 )
X=$( echo "$INFO" | grep "Absolute upper-left X"
                  | cut  -d " " -f 7 )
Y=$( echo "$INFO" | grep "Absolute upper-left Y"
                  | cut  -d " " -f 7 )
echo "Recording $1:$2 ($TIME seconds)"
echo "Press CRTL-C to stop recording"
echo -n "Starts in 5 "
sleep 5

recordmydesktop -x "$X" -y "$Y" --width "$WIDTH" \
                --height "$HEIGHT" &
RMDPID=$( jobs -p | tail -1 )

sleep ${TIME}
/bin/kill -2 ${RMDPID}

 

No te olvides de desactivar el screensaver o al menos darle un tiempo tal que no se active antes de tiempo.


Todo  en github.

Esto no termina...


 

 




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



2021/03/11

Basic Digital Design EAMTA 2021

Gracias a la cuarentena tenía algunos días de vacaciones sin tomar, aproveché para tomar un sorpresivo curso, bastante extenso, cinco días de seis horas y tres de cinco, 45 horas en total, de diseño digital básico con Verilog, en el marco del evento EAMTA 2021, a cargo de Ariel Pola y Federico Zacchigna.

Pese a lo "básico" del curso, se vislumbran "situaciones extremas", donde se muestra el eterno conflico entre La Potencia, La Velocidad y El Área. No es algo a lo que alguien como yo pueda estar expuesto siquiera con baja frecuencia. Pero para mí que ya sabía algo de antes por haber cursado CLP, estudiado unos libros, hecho algunos moocs y practicado en mis experimentos, me resultó excelente. No sé para alguien que viene de cero FPGA o a alguien con conocimientos previos como yo pero que le moleste reforzar, cómo le habrá resultado.

Es como si estuvieras viendo en un curso introductorio de C como usar assembly, escenario que podría corresponder al de sistemas embebidos de muy bajos recursos.

A diferencia de los otras actividades similares que he transitado, esta implica un conocimiento mayor de técnicas digitales, pues estamos hablando de por ejemplo comprender que en el caso de implementar un multiplicador de una variable por una constante, se puede eliminar el hardware necesario para los ceros de la constante.

Los detalles finos de estos conceptos vistos superficialmente se ven exhaustivamente en otro curso o materia que se debe llamar algo así como "Digital Advanced Digital Design", que parece ser mucho más difícil, ya veremos si me quedan días de vacaciones y neuronas para cuando llegue.

Aprendí, recordé, reforcé y completé cosas que vengo viendo de algún modo desde hace más de 30 años.

El último día lo sobreviví comiendo exclusivamente chocolate blanco...

Si la medida objetiva de mi satisfacción con un curso está dada por mi grado de participación, en este caso ha sido tan grande que debo incluso haber molestado a los docentes y compañeros de cursada.

 

Trabajo Práctico


El trabajo práctico es una extensión de los laboratorios desarrollados como tarea durante la cursada, en el momento que escribo esto estoy considerando proponer mi propio trabajo práctico para optimizar mi esfuerzo, pero tengo cierta reticencia en parte para no generarles a los docentes trabajo extra, en parte por que es más difícil y por que el tema que resuelve no coincide con el tema protagonista del curso, que es procesamiento de señales. Ese mismo tema es que me induce a proponer mi propio TP, pues mi ingnorancia del mismo produce que no termine de entender lo que estoy haciendo aunque si cómo lo estoy haciendo.

 

Logística 


Algunos tips de cómo lidiar con algunas fallas.

 

Para el uso de classroom

 

  • Los archivos que tienen preview como txt y pdf, hay que pedirle abrir en otra ventana y ahí recién ofrece download.
  • Hubiese sido más cómodo que un zip con todos los archivos para bajar de un solo tiro

 

Vivado

 

Falla al inicio

Tras invocar vivado, este se cierra diciendo que:

Caused by: java.lang.IllegalArgumentException: Window must not be zero

Primero pensé que era por haber estado jugando al Dune2 desde unas semanas antes, no como una especie de castigo divino, es que estaba en un brote procrastinador nulo y en algún momento puse en full screen a dosbox me rompió el multihead y lo restauré.

Luego, encontré referencias a las versiones de java, independientes de la versión de Xilinx, relacionadas a multihead. Hay que desactivar las pantallas adicionales, arrancar vivado, reactivar las pantallas y listo. Igual muy raro porque no me había ocurrido antes con las otras versiones y ahora si me ocurre hasta donde probé.

ILA

 

Al incorporar el ILA (luego veremos que es eso) fallaba diciendo que ....

WARNING: [Vivado 12-818] No files matched '/home/projects/ej01_ila_vio_v2/ej01_ila_vio_v2.srcs/sources_1/ip/ila_0_1/ila_v6_2/constraints/ila_impl.xdc'
ERROR: [Common 17-55] 'set_property' expects at least one object.


El problema es que ese archivo si existe. Supuse un problema de concurrencia, pero con un solo core también falló.

No se arregló:

  •    haciendo source de settings64.sh
  •    iniciando del iconito
  •    usando un solo core
  •    primero VIO, luego ILA
  •    ILA solita


Viendo con más detalle, dice en la consola que ejecutó:

source ila_0.tcl -notrace

tuve que hacer cd hasta el proyecto y ejecutar:

source ./ej01_ila_vio_v3.runs/ila_0_synth_1/ila_0.tcl -notrace

identifiqué dos menciones a ila_impl.xdc, puse un

puts "antes del primer get files"


y obtuve:

source ./ej01_ila_vio_v3.runs/ila_0_synth_1/ila_0.tcl -notrace
INFO: [IP_Flow 19-234] Refreshing IP repositories
INFO: [IP_Flow 19-1704] No user IP repositories specified
INFO: [IP_Flow 19-2313] Loaded Vivado IP repository '/home/user/Xilinx/Vivado/2019.2/data/ip'.
antes del primer get files
WARNING: [Vivado 12-818] No files matched '/home/carlos/Desktop/digital_design/projects/ej01_ila_vio_v3/ej01_ila_vio_v3.srcs/sources_1/ip/ila_0_1/ila_v6_2/constraints/ila_impl.xdc'
ERROR: [Common 17-55] 'set_property' expects at least one object.
Resolution: If [get_<value>] was used to populate the object, check to make sure this command returns at least one valid object.
current_project ej01_ila_vio_v3


Si era un camino muerto, no llegué a enterarme, Federico me pasó esta receta, parece ser que por el locale debe estar cambiando la interpretación de algún caracter, quién sabe:



cd ~/Xilinx/Vivado/2019.2/bin
export LANG=en_US.utf8
export LC_ALL=en_US.utf8
./vivado


Para corregir el launcher, lo editás y ponés en command:


env LANG="en_US.utf8" LC_ALL="en_US.utf8" /opt/Xilinx/Vivado/2019.2/bin/vivado


Quizás alcance sólo con LC_ALL, pero ya se me acabó el tiempo para esa prueba.

 

Resumen

 

  • Configure display settings
  • Disable extra monitors
  • Start vivado
  • Reenable extra monitors


Algunos aprendizajes

 

No deseo spoilear el curso, tomalo, pero algunas cositas voy a comentar.

 

VIO e ILA

 

Son unos IP Cores que permiten hacer control y monitoréo desde la PC. En lugar de conectarles botones y leds, le conectás VIO y podés prender y apagar y ver los resultados. Con ILA podés obtener muestras de salidas, quizás no logra capturar todo pero medio que alcanza para saber si más o menos está haciendo lo que querés.

Si no tenés la placa y alguien te la presta de modo remoto, como ocurrió en este curso que nos habilitaron varias Arty-A7-35, es obvia la utilidad de este recuros. 

¿Para qué querrías hacer esto si tenés la placa?Esto lo exploraré mejor uno de estos días, es parte de mi TP.

No son gratis, ocupan un montón de espacio. Si mal no recuerdo ese manchón no estaba antes de desplegarlos.


VIO-ILA footprint
VIO-ILA footprint

Vivado tiene la opción de marcar con distintos colores los distintos componentes pero estoy muy cansado, ¿ya lo dije? Cuándo haga el TP probablemente lo muestre mejor.

 

 

Z


Otra cosa que aprendí es que internamente Z es mala palabra, me hubiese venido muy bien saberlo cuando quise hacer la cpu para el bit supervisor.

Notas relacionadas