Verificación de Hardware con Python: UVM y cocotb

8 minute read

Published:

¿Qué es la Verificación de Hardware?

La verificación de hardware es un proceso crítico en el diseño de circuitos digitales que asegura que un diseño funcione correctamente antes de su fabricación. Este proceso tradicionalmente se realizaba con lenguajes como SystemVerilog y metodologías como UVM (Universal Verification Methodology). Sin embargo, con la llegada de herramientas como cocotb, ahora es posible realizar verificación de hardware utilizando Python, lo que hace el proceso más accesible y productivo.

¿Qué es cocotb?

cocotb (Coroutine-based Cosimulation Testbench) es un framework de verificación de hardware que permite escribir testbenches en Python. Funciona como una interfaz entre Python y simuladores de Verilog/SystemVerilog como Icarus Verilog, Verilator, o ModelSim.

Ventajas de cocotb:

  • Python: Aprovecha todo el ecosistema de Python (pandas, numpy, matplotlib, etc.)
  • Productividad: Escribir tests en Python es más rápido que en SystemVerilog
  • Accesibilidad: Python es más fácil de aprender que SystemVerilog
  • Reutilización: Puedes usar bibliotecas existentes de Python para análisis y visualización
  • Coroutines: Soporte nativo para operaciones asíncronas

Proyecto: verification_uvm_cocotb

El repositorio verification_uvm_cocotb es una colección educativa de ejercicios y proyectos para aprender verificación de hardware utilizando Python con cocotb y UVM (pyuvm).

Dependencias del Proyecto

El proyecto requiere las siguientes bibliotecas de Python:

  • cocotb (>=2.0.1): Framework principal para simulación y verificación
  • pandas (>=2.3.3): Manejo y análisis de datos
  • pytest (>=9.0.2): Framework para pruebas unitarias
  • pyuvm (>=4.0.1): Implementación de UVM en Python

Puedes instalarlas usando:

uv sync
# o
pip install -e .

Herramientas del Sistema

Además de las bibliotecas de Python, necesitas:

  • Icarus Verilog (iverilog): Simulador de Verilog open-source
  • GTKWave: Visor de waveforms para analizar señales

En Ubuntu/Debian:

sudo apt-get install iverilog gtkwave

Estructura del Proyecto

main.py

Script principal que verifica la instalación de todas las dependencias necesarias:

def check_essential_libraries():
    libraries = ["cocotb", "pandas", "pytest", "pyuvm"]
    all_present = True
    for lib in libraries:
        try:
            __import__(lib)
            print(f"{lib} is installed.")
        except ImportError:
            print(f"{lib} is not installed.")
            all_present = False
    return all_present

Course_1: Fundamentos Python

Contiene módulos básicos para operaciones fundamentales:

  • v_binaries.py: Operaciones binarias básicas (AND, OR, XOR, NOT)
  • v_binary_types.py: Utilidades para tipos de datos binarios
  • v_decorator.py: Decoradores para funciones
  • v_loggin.py: Utilidades personalizadas de logging
  • v_random.py: Generación de números aleatorios
  • v_random_choice.py: Selección aleatoria de elementos

Course_2: Proyectos Prácticos con cocotb

Esta es la sección más importante, con proyectos completos de verificación:

1. Sumador de 4 bits (4bit_adder)

Verificación de un sumador básico:

@cocotb.test()
async def test(dut):
    logging.getLogger().setLevel(logging.INFO)
    err = 0
    for i in range(10):
        a = random.randint(0,15)
        b = random.randint(0,15)
        dut.a.value = a
        dut.b.value = b
        await Timer(10, unit='ns')
        y = dut.y.value.to_unsigned()
        
        if(y == (a + b)):
            logging.info('Test Passed: a:%0d, b:%0d and y:%0d @ %0s',
                         a, b, y, str(get_sim_time(unit='ns')))
        else:
            logging.error('Test Failed: a:%0d, b:%0d and y:%0d @ %0s',
                          a, b, y, str(get_sim_time(unit='ns')))
            err += 1

2. Multiplexor 8:1 (8_1_mux)

Verificación de un multiplexor con selección de 3 bits:

@cocotb.test()
async def test(dut):
    error_count = 0
    logging.getLogger().setLevel(logging.INFO)
    
    din_bin   = LogicArray(0, 8)
    sel_bin   = LogicArray(0, 3)
    dout_bin  = LogicArray(0, 1)

    for _ in range(30):
        din = random.randint(0,255)
        sel = random.randint(0, 7)
        
        dut.din.value = din
        dut.sel.value = sel
        
        await Timer(10, 'ns')
        
        dout = dut.dout.value
        
        if str(din_bin)[7 - sel] != str(dout_bin):
            error_count += 1

3. Flip-Flop D (d_flip_flop)

Verificación de un flip-flop tipo D con reset:

@cocotb.test()
async def test(dut):
    logging.getLogger().setLevel(logging.INFO)

    cocotb.start_soon(rst_stimuli(dut))
    cocotb.start_soon(Clock(dut.clk, 20, "ns").start())

    await Timer(100, "ns")
    err = 0
    for i in range(10):
        din = random.randint(0, 1)
        dut.din.value = din
        await RisingEdge(dut.clk)
        await RisingEdge(dut.clk)
        
        if dut.dout.value != din:
            err += 1

4. Memoria (memory)

Verificación de un módulo de memoria con operaciones de lectura y escritura:

mem_arr = {}

async def write_data(dut):
    await RisingEdge(dut.rst)
    await FallingEdge(dut.rst)
    logging.info('Writing Data to Memory')
    
    for i in range(15):
        addr = random.randint(0, 15)
        data = random.randint(0, 255)
        mem_arr[addr] = data
        dut.addr.value = addr
        dut.din.value = data
        dut.wr.value = 1
        await ClockCycles(dut.clk, 1)

async def read_data(dut):
    await FallingEdge(dut.wr)
    logging.info('Reading Data from Memory')
    
    for i in range(15):
        addr = random.randint(0, 15)
        dut.addr.value = addr
        dut.wr.value = 0
        await ClockCycles(dut.clk, 2)
        dout = dut.dout.value
        
        if mem_arr.get(addr) != dout:
            err += 1

5. Operaciones Binarias (binary)

Demostración de operaciones con tipos binarios en cocotb:

@cocotb.test()
async def test(dut):
    logging.getLogger().setLevel(logging.INFO)
    
    # Operaciones bitwise
    a = LogicArray("1011")
    b = LogicArray("1100")
    
    logging.info(f"a & b = {a & b}")
    logging.info(f"a | b = {a | b}")
    logging.info(f"a ^ b = {a ^ b}")
    logging.info(f"~a = {~a}")
    
    # Operaciones de shift
    a = LogicArray("10110011")
    n = len(a)
    val = a.to_unsigned() << 2
    val &= (1 << n) - 1
    a = LogicArray.from_unsigned(val, n)
    logging.info(f"a << 2 = {a}")

6. Codificador de Prioridad (pri_encoder)

Verificación con modelo de referencia:

def pri_model(input_bin):
    # Modelo de referencia para verificar el codificador
    for i in range(len(input_bin)-1, -1, -1):
        if input_bin[i] == 1:
            return LogicArray.from_unsigned(7-i, 3)
    return LogicArray(0, 3)

@cocotb.test()
async def test(dut):
    error_count = 0
    
    for _ in range(30):
        input_rand = random.randint(0, 255)
        input_bin[:] = input_rand
       
        dut.en.value = 1
        dut.i.value = input_rand
        await Timer(10, 'ns')
        
        output = dut.y.value.to_unsigned()
        
        if pri_model(input_bin) != output_bin:
            error_count += 1

7. Generación de Reloj (clock)

Diferentes métodos para generar señales de reloj:

# Método manual
async def clk1(dut):
    ton = 10
    toff = 10
    while True:
        dut.clk1.value = 1
        await Timer(ton, 'ns')
        dut.clk1.value = 0
        await Timer(toff, 'ns')

# Usando clase Clock de cocotb
@cocotb.test() 
async def top_tb(dut):
    cocotb.start_soon(Clock(dut.clk, 10, unit='ns').start())
    await Timer(100, unit='ns')

Conceptos Clave de cocotb

1. Decoradores y Coroutines

@cocotb.test()
async def test(dut):
    # dut = Device Under Test
    await Timer(10, 'ns')

2. Triggers (Disparadores)

  • Timer: Espera un tiempo específico
  • RisingEdge: Espera flanco de subida
  • FallingEdge: Espera flanco de bajada
  • Edge: Cualquier cambio
  • ClockCycles: Espera N ciclos de reloj

3. Acceso a señales DUT

# Escribir valores
dut.a.value = 5
dut.b.value = 10

# Leer valores
result = dut.y.value.to_unsigned()

4. Tareas concurrentes

cocotb.start_soon(write_data(dut))
cocotb.start_soon(read_data(dut))
cocotb.start_soon(Clock(dut.clk, 10, 'ns').start())

5. Tipos de datos LogicArray

from cocotb.types import LogicArray

# Crear arrays binarios
a = LogicArray(0, 8)  # 8 bits en 0
b = LogicArray("1011")  # Desde string binario
c = LogicArray.from_signed(-5, 8)  # Número con signo

# Conversiones
unsigned_val = a.to_unsigned()
signed_val = a.to_signed()

Estructura de un Proyecto cocotb

Cada proyecto en Course_2 sigue esta estructura:

project_name/
├── module.sv           # Diseño en Verilog
├── module_tb.py        # Testbench en Python
├── makefile            # Configuración de simulación
├── results.xml         # Resultados de las pruebas
├── dump.vcd            # Waveforms generados
└── sim_build/          # Archivos temporales

Ejemplo de Makefile

TOPLEVEL_LANG ?= verilog
SIM ?= icarus
VERILOG_SOURCES = $(shell pwd)/adder.sv
TOPLEVEL := adder
COCOTB_TEST_MODULES := adder_tb

include $(shell cocotb-config --makefiles)/Makefile.sim

Cómo Usar el Proyecto

  1. Verificar Python: Asegúrate de tener Python >= 3.12

  2. Instalar dependencias:
    uv sync
    # o
    pip install -e .
    
  3. Verificar instalación:
    uv run main.py
    
  4. Ejecutar simulaciones: Navega a cualquier proyecto en Course_2/:
    cd Course_2/4bit_adder
    make
    
  5. Ver resultados: Abre el archivo .vcd con GTKWave:
    gtkwave adder.vcd
    

Ventajas de Python para Verificación

  1. Ecosistema rico: Puedes usar pandas para análisis, matplotlib para gráficas, numpy para operaciones matemáticas

  2. Debugging más fácil: Puedes usar cualquier IDE de Python con breakpoints

  3. Tests más legibles: El código Python es generalmente más claro que SystemVerilog

  4. Automatización: Fácil integración con CI/CD y frameworks de testing

  5. Comunidad: Gran comunidad de Python para soporte

Casos de Uso Reales

cocotb se usa en:

  • Chips RISC-V: Verificación de procesadores open-source
  • Interfaces de comunicación: I2C, SPI, UART, PCIe
  • Aceleradores ML: Verificación de hardware para machine learning
  • FPGAs: Desarrollo y testing de diseños para FPGA
  • ASICs: Verificación de diseños para fabricación

Recursos Adicionales

Conclusión

La verificación de hardware con Python y cocotb representa un cambio paradigmático en la metodología tradicional de verificación. Combina la potencia de Python con la necesidad crítica de verificar diseños digitales, haciéndolo más accesible tanto para ingenieros de hardware que quieren aprovechar Python, como para desarrolladores de software que quieren incursionar en hardware.

El proyecto verification_uvm_cocotb proporciona una ruta de aprendizaje estructurada, desde conceptos básicos de Python aplicados a verificación (Course_1) hasta implementaciones completas de testbenches para diseños reales (Course_2). Es un recurso invaluable para cualquiera interesado en aprender verificación de hardware de manera moderna y práctica.

Ya sea que estés diseñando un simple sumador o un complejo sistema on chip (SoC), cocotb y Python te proporcionan las herramientas necesarias para verificarlo de manera eficiente y profesional.