Como continuación del
primer artículo , quiero mostrar un ejemplo de trabajo con FPGA (FPGA) en python como ejemplo. Este artículo cubrirá el aspecto de la prueba con más detalle. Si el marco
MyHDL permite a las personas que trabajan en Python, utilizando la sintaxis y el ecosistema familiares, mirar el mundo de FPGA, entonces los desarrolladores de FPGA experimentados no entienden el significado del uso de Python. Los paradigmas de descripción de hardware para MyHDL y Verilog son similares, y elegir un idioma específico es una cuestión de hábito y gusto. Verilog / VHDL representa el hecho de que el firmware se ha escrito en estos idiomas durante mucho tiempo y, de hecho, son estándar para la descripción de equipos digitales. Python, como novato en este campo, puede competir en entornos de prueba de escritura. Una parte importante del tiempo del desarrollador de FPGA se dedica a probar sus diseños. A continuación, quiero demostrar con un ejemplo cómo se hace esto en python con MyHDL.
Supongamos que hay una tarea para describir un dispositivo que funciona con memoria en el FPGA. Para simplificar, tomaré la memoria que se comunica con otros dispositivos a través de una interfaz paralela (y no a través de una serie, por ejemplo, I2C). Dichos microcircuitos no siempre son prácticos en vista del hecho de que se requieren muchos pines para trabajar con ellos; por otro lado, se proporciona un intercambio de información más rápido y fácil. Por ejemplo, doméstico 1645RU1U y sus análogos.

Descripción del módulo
El registro se ve así: FPGA proporciona una dirección de celda de 16 bits, datos de 8 bits, genera una señal de escritura WE (habilitación de escritura). Dado que OE (habilitación de salida) y CE (habilitación de chip) siempre están habilitados, la lectura se produce cuando se cambia la dirección de la celda. La escritura y la lectura se pueden llevar a cabo de forma secuencial en varias celdas seguidas, comenzando desde una dirección adr_start específica, registrada en el borde delantero de la señal adr_write, y una celda en una dirección arbitraria (acceso aleatorio).
En MyHDL, el código se ve así (las señales de escritura y lectura vienen en lógica inversa):
from myhdl import * @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we):
Si se convierte a Verilog con la función:
def convert(hdl): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(0)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] inst = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) inst.convert(hdl=hdl) convert(hdl='Verilog')
entonces obtenemos lo siguiente:
`timescale 1ns/10ps module ram_driver ( data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we ); input [7:0] data_in; output [7:0] data_out; reg [7:0] data_out; output [15:0] adr; reg [15:0] adr; input [15:0] adr_start; input adr_write; inout [7:0] data_memory; wire [7:0] data_memory; input read; input write; output we; reg we; reg [7:0] mem_z; assign data_memory = mem_z; always @(write) begin: RAM_DRIVER_WRITE_DATA if ((!write)) begin mem_z <= data_in; we <= 0; end else begin mem_z <= 'bz; data_out <= data_memory; we <= 1; end end always @(posedge adr_write, posedge write, negedge read) begin: RAM_DRIVER_WRITE_START_ADR if (adr_write) begin adr <= adr_start; end else begin adr <= (adr + 1); end end endmodule
No es necesario convertir un proyecto a Verilog para simulación, este paso será necesario para flashear el FPGA.
Modelado
Después de la descripción de la lógica, el proyecto debe ser verificado. Puede restringirse, por ejemplo, para simular influencias de entrada y ver la respuesta del módulo en el diagrama de tiempo. Pero con esta opción, es más difícil predecir la interacción de su módulo con un chip de memoria. Por lo tanto, para verificar completamente el funcionamiento del dispositivo creado, debe crear un modelo de memoria y probar la interacción entre estos dos dispositivos.
Dado que el trabajo se lleva a cabo en python, el tipo de diccionario dado (diccionario) se sugiere para el modelo de memoria. Los datos en los que se almacenan como {clave: valor}, y para este caso {dirección: datos}.
memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456
Para el mismo propósito, el tipo de datos de la lista es adecuado, donde cada elemento tiene sus propias coordenadas, lo que indica la ubicación del elemento en la lista:
memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456
El uso de diccionarios para simular la memoria parece preferible en vista de una mayor visibilidad.
La descripción del shell de prueba (en el archivo test_seq_access.py) comienza con la declaración de señales, la inicialización de los estados iniciales y su lanzamiento a la función del controlador de memoria descrita anteriormente:
@block def testbench(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we)
A continuación se describe el modelo de memoria. Los estados iniciales se inicializan, por defecto la memoria se llena con valores cero. Limite el modelo de memoria a 128 celdas:
memory = {i: intbv(0) for i in range(128)}
y describa el comportamiento de la memoria: cuando WE en el estado bajo, escriba el valor en la línea en la dirección de memoria correspondiente; de lo contrario, el modelo da el valor en la dirección dada:
mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None
Luego, en la misma función, puede describir el comportamiento de las señales de entrada (para el caso de escritura / lectura secuencial): se registra la dirección de inicio → se registran 8 celdas de información → se registra la dirección de inicio → se leen 8 celdas de información grabadas.
@instance def stimul(): init_adr = random.randint(0, 50) # yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr # yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # 8 write.next = 0 data_in.next = random.randint(0, 100) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr # adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # read.next = 0 yield delay(100) read.next = 1 yield delay(100) raise StopSimulation return stimul, ram, access
Ejecutar simulación:
tb = testbench() tb.config_sim(trace=True) tb.run_sim()
Después de iniciar el programa, el archivo testbench_seq_access.vcd se genera en la carpeta de trabajo, ábralo en gtkwave:
gtkwave testbench_seq_access.vcd
Y vemos la imagen:

La información registrada fue leída con éxito.
Puede ver el contenido de la memoria agregando el siguiente código a testbench:
for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value))
Lo siguiente aparece en la consola:

Prueba
Después de eso, puede realizar varias pruebas automatizadas con un mayor número de celdas grabables / legibles. Para hacer esto, se agregan varios ciclos de prueba y diccionarios ficticios a testbench, donde se agrega la información escrita y legible y la construcción de aserción, lo que provoca un error si dos diccionarios no son iguales:
@instance def stimul(): for time in range(100): temp_mem_write = {} temp_mem_read = {} init_adr = random.randint(0, 50) yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[i] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): read.next = 0 temp_mem_read[i] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, " " for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) raise StopSimulation return stimul, ram, access
A continuación, puede crear un segundo banco de pruebas para probar la operación en modo de acceso aleatorio: test_random_access.py.
La idea de la segunda prueba es similar: escribimos información aleatoria en una dirección aleatoria y agregamos un par {dirección: datos} al diccionario temp_mem_write. Luego damos la vuelta a las direcciones en este diccionario y leemos la información de la memoria, ingresándola en el diccionario temp_mem_read. Y al final con la construcción de aserción, verificamos el contenido de dos diccionarios.
import random from myhdl import * from ram_driver import ram_driver @block def testbench_random_access(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) memory ={i:intbv(0) for i in range(128)} mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None @instance def stimul(): for time in range(10): temp_mem_write = {} temp_mem_read = {} yield delay(100) for i in range(64): write.next = 1 adr_write.next = 1 adr_start.next = random.randint(0, 126) yield delay(100) adr_write.next = 0 yield delay(100) write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[int(adr_start.val)] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) for key in temp_mem_write.keys(): adr_start.next = key adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) read.next = 0 temp_mem_read[key] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, ' random access' raise StopSimulation return stimul, ram, access tb = testbench_random_access() tb.config_sim(trace=True) tb.run_sim()
Python tiene varios marcos para automatizar la ejecución de pruebas. Tomaré pytest por simplicidad, debe instalarse desde pip:
pip3 install pytest
Cuando se inicia el comando "pysest" desde la consola, el marco buscará y ejecutará todos los archivos en la carpeta de trabajo con "test_ *" en sus nombres.

Pruebas completadas con éxito. Cometeré un error deliberadamente en la descripción del dispositivo:
@block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): mem_z = data_memory.driver() @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: adr.next = adr_start else: adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 1 # , else: mem_z.next = None data_out.next = data_memory we.next = 1
Corro pruebas:

Como se esperaba, en ambas pruebas, se consideró la información inicial (ceros), es decir, no se registró nueva información.
Conclusión
El uso de python junto con myHDL le permite automatizar las pruebas de firmware desarrollado para FPGA y crear casi cualquier entorno de prueba usando las capacidades ricas del lenguaje de programación python.
El artículo considera:
- creando un módulo que funciona con memoria;
- creando un modelo de memoria;
- creación de casos de prueba;
- prueba de automatización con el marco pytest.