2021/09/20

H4CK3D 2021: un desvío necesario

Si venís leyendo los primeros pasos, entiendo que te va a resultar más o menos natural que siga tomando el control de la placa en lugar de ir directo hacia la POC.

Para esta necesito examinar el flujo de ejecución de las instrucciones del programa a atacar, ante mi se abren tres caminos, no necesariamente excluyentes:


Analizar el código fuente generado

 

O tomar el ejecutable y decompilarlo a assembly o pedirle al compilador que genere o conserve el mismo código. Esto último se logra con la opción -save-temps agregada a la siguiente línea del Makefile:


CFLAGS = -march=rv32i -mabi=ilp32 -Wall -Wextra -pedantic -DFREQ=$(FREQ_PLL)000000 -Os -ffreestanding -nostartfiles -g -Iprograms/$(PROGRAM)

Me parece conveniente además quitár ese -Os, que es optimización de tamaño.

 

Generar una simulación

 

Por lo que entendí de la charla FOSS para el desarrollo con FPGA de Rodrigo Melo, verilator es mi amigo, pero fuí a la documentación y está pensada para quien se va a dedicar, no para un turista como yo, aprender a usarlo es un subproyecto tan grande como el resto del proyecto.

 

Instrumentación

 

Puedo volcar los buses a los puertos de salida y capturarlos desde otro sistema, puede ser buen momento para usar la PYNQ, que tiene la cantidad de entradas apropiadas y al correr linux me facilita muchas cosas, pero, también es un subproyecto muy grande.


Tanto en la simulación como en la monitorización, necesito lidiar con:

static inline uint32_t rdcycle(void) {
    uint32_t cycle;
    asm volatile ("rdcycle %0" : "=r"(cycle));
    return cycle;
}

        uint32_t start = rdcycle();
        while ((rdcycle() - start) <= FREQ);

 

No sé si es un mero delay o cumple otra función indispensable, recordemos que cualquier problema de concurrencia se resuelve con un delay().

Mientras barajo todas estas posibilidades, lo mejor es terminar de implementar el acceso al conector arduino y el pmod restante, a modo de práctica y profundización del conocimiento del sistema y de paso es trabajo que se puede aportar al icicle de Pablo, por si alguien luego lo quiere aprovechar.


entonces...

 

De tanto repetir, ya tengo un procedimiento, funciona pero los razonamientos que agrego son muy personales, tomalos con suma precaución:

Identificar los pines y agregarlos al archivo.pcf

Agregar esos pines ya sea como input u ouput al top.

Pasarlos al módulo icicle

Dentro del modulo icicle, tomar los pines

Para cada módulo definir

    logic XXX_sel; 

Este sirve para que luego el mapeador, el casez (mem_address) identique a donde está apuntando la dirección en el bus 

    logic [31:0] XXX_read_value;

Es donde va a quedar escrito el valor leído

    logic XXX_ready;

Avisa que el valor leído puede ser leído. Fijate que casi todos los _ready están conectados a _sel, esto es por que se pueden leer inmediatamente. En los casos de  ram.sv y flash.sv evidentemente hay demoras.

Si notaste que los dispositivos están enmarcado en unos `ifdef asociados a defines que van en edufpga-defines.sv:

 

// Defines for EDU-FPGA
`define PMOD0
`define PMOD1
`define ARDUINO

 

Notarás tambien que en el caso de no estar definido se le asigna cero a la lectura y siempre listo:

 

`ifdef PMOD1
    assign pmod1_read_value = {24'b0, pmod1_sel ? pmod1 : 8'b0};
    assign pmod1_ready = pmod1_sel;
`else
    assign pmod1_read_value = 0;
    assign pmod1_ready = pmod1_sel;
`endif

Como programador me asusta la línea repetida, pero me parece más clara, así que ahí queda por ahora.

Cuando si está definido, como son los 8 bits menos significativos, se le ponen ceros. Luego, si ha sido seleccionado el valor de los pines, si no, ceros.


Fijate que ese XXX_read_value va a parar a un bruto OR, por eso la asignación debe estar condicionada:

assign mem_read_value = ram_read_value | leds_read_value | buttons_read_value | pmod0_read_value | pmod1_read_value | arduino_read_value | uart_read_value | timer_read_value | flash_read_value;
 

Parecido los XXX_ready:

assign mem_ready = ram_ready | leds_ready | buttons_ready | pmod0_ready | pmod1_ready | arduino_ready | uart_ready | timer_ready | flash_ready | mem_fault;

Luego, hay que copiar/pegar/editar leds o buttons según quieras leer o escribir.

En el caso de arduino con más de 8 bits, hay que implementar los cuatro ifs, es por que... mmh, luego voy a investigarlo mejor, me parece que se puede simplificar, va como tarea al backlog que tengo conectado a /dev/null.

Creo que finalmente, hay que mapear:

La idea sería algo como (quité algunos ceros para que no salte la línea):

32'b..._00000001_00000000_000011??: pmod1_sel   = 1; // 0x0001000c
32'b..._00000001_00000000_000100??: arduino_sel = 1; // 0x00010010

Esos dos ?? son dos bits, cuatro posiciones. Para los pmods sobra, para el arduino está ok.

Para 0x0001000c no hay nada en:

0x0001000d
0x0001000e
0x0001000f

En cambio para arduino, se usan los cuatro bytes (32 bits):

0x00010010
0x00010011
0x00010012
0x00010013

 

Luego en el programa:

#define PMOD1       *((volatile uint32_t *) 0x0001000c)
#define ARDUINO     *((volatile uint32_t *) 0x00010010)

En la carpeta programs hay... programas, ¿qué esperabas? El ejemplo más completo es buttons2ledsArduinoShiftPmodToPmod que no hace falta que te explique que los botones que aprietes se van a manifestar en los leds y que lo que pongas en un pmod saldrá por el otro y que en el conector arduino verás unos bits moviéndose.

 

El shift en el conector arduino
El shift en el conector arduino

El shift es medio raro por como armé el pinout, va en zig-zag. Y el pin 89 estába mal etiquetado, es 82, ya avisé.

 


 

Los switches que uso para el pmod tienen décadas, no andan del todo bien...

 

Si mirás el código en icicle.sv, verás que para leds y buttons estoy usando los parámetros LEDCOUNT y BUTTONCOUNT respectivamente, no así parámetros para los pmods ni arduino. Esto es por que no espero que tengan valores variables. De hecho, esos parámetros son una gentileza para quien quiera adaptar a las otras placas, la edu-ciaa-fpga tiene 4 y 4, listo.

Pude haber hecho bloques de 8 bits o 4 y así combinar inputs con outputs con facilidad y mejor aún llevar el concepto a dos módulos, ponele GPInput y GPOutput, o mucho mejor implementar el módulo GPIO, pero eso me desvía de mi proyecto, yo voy a usar los pines directamente, el archivo de defines lo voy a tener vacío. No quiero ni necesito que haya nada mapeado a la memoria ni que sea accesible desde el programa en ejecución.


El módulo GPIO

 

Este debería tener al menos una máscara de selección de sentido, quizás otra de enable/disable, lo mejor que se me ocurre es agrandar la distancia que hay en el casez (mem_address) entre direcciones y que luego el módulo termine de determinar si la interacción es con las máscaras o con los registros de datos. 

Desde el momento que no hay interrupciones, no vale la pena preocuparse por ellas.

Ya tengo un branch local con el código para pines inout, algún dia...

 

Conclusiones

 

He usado variables desde programas en C, ¿quién no? He usado funciones o macros provistas por el fabricante para configurar y acceder los puertos, magia. He visto que Vivado cuando le dás un dispositivo para usar desde la PYNQ te define direcciones de memoria donde va a estar. (dos seriales, pynq con acelerador, acelerador con serial, acelerador con leak).

Ahora he mapeado dispositivos a posiciones de memoria y lo he hecho de modo incremental.

Para ello, no me he tenido que preocupar mucho por el legacy, son cuatro programas.

Tampoco por unos bytes más o menos ni por el silicio que consuman.

Este proyecto es muy recomendable para poder comprender realmente estos conceptos y porqué a veces los registros están con un orden aparentemente razonable y otra veces no. Lo que te propongo es que te bajes el repo:

git clone https://github.com/ciaa/icicle.git

y te pares donde tomé yo:

git checkout f63f218

y te diviertas reconstruyendo mi camino o tomando el tuyo propio.


Uno de estos días cuando Pablo acepte mi segundo pull request, verás todo. O podés hacer trampa.


 





No hay comentarios:

Publicar un comentario