Comenzando con vhdl

Señales frente a variables, una breve descripción de la semántica de simulación de VHDL

Este ejemplo trata uno de los aspectos más fundamentales del lenguaje VHDL: la semántica de simulación. Está destinado a principiantes de VHDL y presenta una vista simplificada donde se han omitido muchos detalles (procesos pospuestos, interfaz de procedimiento de VHDL, variables compartidas…) Los lectores interesados ​​en la semántica completa real deben consultar el Manual de referencia del lenguaje (LRM).

Señales y variables

La mayoría de los lenguajes de programación imperativos clásicos usan variables. Son contenedores de valor. Un operador de asignación se utiliza para almacenar un valor en una variable:

a = 15;

y el valor almacenado actualmente en una variable se puede leer y usar en otras declaraciones:

if(a == 15) { print "Fifteen" }

VHDL también usa variables y tienen exactamente el mismo rol que en la mayoría de los lenguajes imperativos. Pero VHDL también ofrece otro tipo de contenedor de valor: la señal. Las señales también almacenan valores, también se pueden asignar y leer. El tipo de valores que se pueden almacenar en las señales es (casi) el mismo que en las variables.

Entonces, ¿por qué tener dos tipos de contenedores de valor? La respuesta a esta pregunta es esencial y está en el corazón del lenguaje. Comprender la diferencia entre variables y señales es lo primero que debe hacer antes de intentar programar algo en VHDL.

Ilustremos esta diferencia con un ejemplo concreto: el intercambio.

Nota: todos los siguientes fragmentos de código son partes de procesos. Más adelante veremos qué son los procesos.

    tmp := a;
    a   := b;
    b   := tmp;

intercambia las variables a y b. Después de ejecutar estas 3 instrucciones, el nuevo contenido de a es el antiguo contenido de b y viceversa. Como en la mayoría de los lenguajes de programación, se necesita una tercera variable temporal (tmp). Si, en lugar de variables, quisiéramos intercambiar señales, escribiríamos:

    r <= s;
    s <= r;

o:

    s <= r;
    r <= s;

¡con el mismo resultado y sin necesidad de una tercera señal temporal!

Nota: el operador de asignación de señales VHDL <= es diferente del operador de asignación de variables :=.

Veamos un segundo ejemplo en el que asumimos que el subprograma print imprime la representación decimal de su parámetro. Si a es una variable entera y su valor actual es 15, ejecutando:

    a := 2 * a;
    a := a - 5;
    a := a / 5;
    print(a);

imprimirá:

5

Si ejecutamos esto paso a paso en un depurador podemos ver el valor de a cambiando del 15 inicial a 30, 25 y finalmente 5.

Pero si s es una señal entera y su valor actual es 15, ejecutando:

    s <= 2 * s;
    s <= s - 5;
    s <= s / 5;
    print(s);
    wait on s;
    print(s);

imprimirá:

15
3

Si ejecutamos esto paso a paso en un depurador, no veremos ningún cambio de valor de s hasta después de la instrucción esperar. Además, el valor final de s no será 15, 30, 25 o 5, ¡sino 3!

Este comportamiento aparentemente extraño se debe a la naturaleza fundamentalmente paralela del hardware digital, como veremos en las siguientes secciones.

Paralelismo

VHDL es un lenguaje de descripción de hardware (HDL), es paralelo por naturaleza. Un programa VHDL es una colección de programas secuenciales que se ejecutan en paralelo. Estos programas secuenciales se denominan procesos:

P1: process
begin
  instruction1;
  instruction2;
  ...
  instructionN;
end process P1;

P2: process
begin
  ...
end process P2;

Los procesos, al igual que el hardware que están modelando, nunca terminan: son bucles infinitos. Después de ejecutar la última instrucción, la ejecución continúa con la primera.

Al igual que con cualquier lenguaje de programación que admita una forma u otra de paralelismo, un programador es responsable de decidir qué proceso ejecutar (y cuándo) durante una simulación VHDL. Además, el lenguaje ofrece construcciones específicas para la comunicación y sincronización entre procesos.

Planificación

El programador mantiene una lista de todos los procesos y, para cada uno de ellos, registra su estado actual, que puede ser “en ejecución”, “ejecutable” o “suspendido”. Hay como máximo un proceso en estado en ejecución: el que se está ejecutando actualmente. Siempre que el proceso que se está ejecutando actualmente no ejecute una instrucción esperar, continúa ejecutándose y evita que se ejecute cualquier otro proceso. El planificador VHDL no es preventivo: es responsabilidad de cada proceso suspenderse y dejar que se ejecuten otros procesos. Este es uno de los problemas que los principiantes de VHDL encuentran con frecuencia: el proceso de ejecución libre.

  P3: process
    variable a: integer;
  begin
    a := s;
    a := 2 * a;
    r <= a;
  end process P3;

Nota: la variable a se declara localmente mientras que las señales s y r se declaran en otro lugar, en un nivel superior. Las variables VHDL son locales para el proceso que las declara y no pueden ser vistas por otros procesos. Otro proceso también podría declarar una variable llamada a, no sería la misma variable que la del proceso P3.

Tan pronto como el planificador reanude el proceso ‘P3’, la simulación se atascará, la hora actual de la simulación ya no progresará y la única forma de detener esto será matar o interrumpir la simulación. La razón es que ‘P3’ no tiene una declaración de ’espera’ y, por lo tanto, permanecerá en estado ’en ejecución’ para siempre, recorriendo sus 3 instrucciones. Ningún otro proceso tendrá la oportunidad de ejecutarse, incluso si es “ejecutable”.

Incluso los procesos que contienen una declaración esperar pueden causar el mismo problema:

  P4: process
    variable a: integer;
  begin
    a := s;
    a := 2 * a;
    if a = 16 then
      wait on s;
    end if;
    r <= a;
  end process P4;

Nota: el operador de igualdad VHDL es =.

Si el proceso P4 se reanuda mientras el valor de la señal s es 3, se ejecutará para siempre porque la condición a = 16 nunca será verdadera.

Supongamos que nuestro programa VHDL no contiene tales procesos patológicos. Cuando el proceso en ejecución ejecuta una instrucción esperar, se suspende inmediatamente y el programador lo pone en el estado suspendido. La instrucción esperar también incluye la condición para que el proceso vuelva a ser ejecutable. Ejemplo:

    wait on s;

significa suspenderme hasta que cambie el valor de la señal s. Esta condición es registrada por el programador. El planificador luego selecciona otro proceso entre los ejecutables, lo pone en estado en ejecución y lo ejecuta. Y lo mismo se repite hasta que todos los procesos “ejecutables” se hayan ejecutado y suspendido.

Nota importante: cuando varios procesos son ejecutables, el estándar VHDL no especifica cómo el programador seleccionará cuál ejecutar. Una consecuencia es que, según el simulador, la versión del simulador, el sistema operativo o cualquier otra cosa, dos simulaciones del mismo modelo VHDL podrían, en un punto, tomar decisiones diferentes y seleccionar un proceso diferente para ejecutar. Si esta elección tuviera un impacto en los resultados de la simulación, podríamos decir que VHDL no es determinista. Como el no determinismo suele ser indeseable, sería responsabilidad de los programadores evitar situaciones no deterministas. Afortunadamente, VHDL se encarga de esto y aquí es donde las señales entran en escena.

Señales y comunicación entre procesos

VHDL evita el no determinismo utilizando dos características específicas:

  1. Los procesos pueden intercambiar información solo a través de señales
  signal r, s: integer;  -- Common to all processes
...
  P5: process
    variable a: integer; -- Different from variable a of process P6
  begin
    a := s + 1;
    r <= a;
    a := r + 1;
    wait on s;
  end process P5;

  P6: process
    variable a: integer; -- Different from variable a of process P5
  begin
    a := r + 1;
    s <= a;
    wait on r;
  end process P6;

Nota: los comentarios VHDL se extienden desde -- hasta el final de la línea.

  1. El valor de una señal VHDL no cambia durante la ejecución de los procesos

Cada vez que se asigna una señal, el programador registra el valor asignado, pero el valor actual de la señal permanece sin cambios. Esta es otra gran diferencia con las variables que toman su nuevo valor inmediatamente después de ser asignadas.

Veamos una ejecución del proceso ‘P5’ anterior y supongamos que ‘a=5’, ’s=1’ y ‘r=0’ cuando el programador lo reanuda. Después de ejecutar la instrucción a := s + 1;, el valor de la variable a cambia y se convierte en 2 (1+1). Al ejecutar la siguiente instrucción r <= a; es el nuevo valor de a (2) el que se asigna a r. Pero siendo r una señal, el valor actual de r sigue siendo 0. Entonces, al ejecutar a := r + 1;, la variable a toma (inmediatamente) el valor 1 (0+1), no 3 (2+1) como diría la intuición.

¿Cuándo tomará realmente la señal ‘r’ su nuevo valor? Cuando el programador haya ejecutado todos los procesos ejecutables y todos se suspenderán. Esto también se conoce como: después de un ciclo delta. Solo entonces el programador observará todos los valores que se han asignado a las señales y actualizará los valores de las señales. Una simulación VHDL es una alternancia de fases de ejecución y fases de actualización de señal. Durante las fases de ejecución, el valor de las señales se congela. Simbólicamente, decimos que entre una fase de ejecución y la siguiente fase de actualización de la señal transcurrió un delta de tiempo. Esto no es en tiempo real. Un ciclo delta no tiene duración física.

Gracias a este mecanismo de actualización de señal retrasada, VHDL es determinista. Los procesos pueden comunicarse solo con señales y las señales no cambian durante la ejecución de los procesos. Entonces, el orden de ejecución de los procesos no importa: su entorno externo (las señales) no cambia durante la ejecución. Mostremos esto en el ejemplo anterior con los procesos P5 y P6, donde el estado inicial es P5.a=5, P6.a=10, s=17, r=0 y donde el programador decide ejecutar ‘P5’ primero y ‘P6’ a continuación. La siguiente tabla muestra el valor de las dos variables, el valor actual y el siguiente de las señales después de ejecutar cada instrucción de cada proceso:

proceso / instrucción P5.a P6.a s.actual s.siguiente r.actual r.siguiente
Estado inicial 5 10 17 0
P5/a: = s + 1 18 10 17 0
P5 / r <= un 18 10 17 0 18
P5/a: = r + 1 1 10 17 0 18
P5 / espera en s 1 10 17 0 18
P6/a:= r + 1 1 1 17 0 18
P6 / s <= a 1 1 17 1 0 18
P6 / espera en r 1 1 17 1 0 18
Después de la actualización de la señal 1 1 1 18

Con las mismas condiciones iniciales, si el programador decide ejecutar ‘P6’ primero y ‘P5’ después:

proceso / instrucción P5.a P6.a s.actual s.siguiente r.actual r.siguiente
Estado inicial 5 10 17 0
P6/a:= r + 1 5 1 17 0
P6 / s <= a 5 1 17 1 0
P6 / espera en r 5 1 17 1 0
P5/a: = s + 1 18 1 17 1 0
P5 / r <= un 18 1 17 1 0 18
P5/a: = r + 1 1 1 17 1 0 18
P5 / espera en s 1 1 17 1 0 18
Después de la actualización de la señal 1 1 1 18

Como vemos, tras la ejecución de nuestros dos procesos, el resultado es el mismo sea cual sea el orden de ejecución.

Esta semántica de asignación de señales contraria a la intuición es la razón de un segundo tipo de problemas que los principiantes de VHDL encuentran con frecuencia: la asignación que aparentemente no funciona porque se retrasa un ciclo delta. Cuando se ejecuta el proceso P5 paso a paso en un depurador, después de que r haya sido asignado 18 y a haya sido asignado r + 1, uno podría esperar que el valor de a sea 19 pero el el depurador dice obstinadamente que r=0 y a=1

Nota: la misma señal se puede asignar varias veces durante la misma fase de ejecución. En este caso, es la última asignación la que decide el siguiente valor de la señal. Las demás asignaciones no tienen ningún efecto, como si nunca se hubieran ejecutado.

Es hora de verificar nuestra comprensión: vuelva a nuestro primer ejemplo de intercambio e intente comprender por qué:

  process
  begin
    ---
    s <= r;
    r <= s;
    ---
  end process;

en realidad intercambia las señales r y s sin la necesidad de una tercera señal temporal y por qué:

  process
  begin
    ---
    r <= s;
    s <= r;
    ---
  end process;

sería estrictamente equivalente. Trate de entender también por qué, si s es una señal entera y su valor actual es 15, y ejecutamos:

  process
  begin
    ---
    s <= 2 * s;
    s <= s - 5;
    s <= s / 5;
    print(s);
    wait on s;
    print(s);
    ---
  end process;

las dos primeras asignaciones de la señal s no tienen efecto, por eso se le asigna finalmente 3 a s y por qué los dos valores impresos son 15 y 3.

Tiempo físico

Para modelar hardware es muy útil poder modelar el tiempo físico que tarda alguna operación. Aquí hay un ejemplo de cómo se puede hacer esto en VHDL. El ejemplo modela un contador síncrono y es un código VHDL completo e independiente que podría compilarse y simularse:

-- File counter.vhd
entity counter is
end entity counter;

architecture arc of counter is
  signal clk: bit; -- Type bit has two values: '0' and '1'
  signal c, nc: natural; -- Natural (non-negative) integers
begin
  P1: process
  begin
    clk <= '0';
    wait for 10 ns; -- Ten nano-seconds delay
    clk <= '1';
    wait for 10 ns; -- Ten nano-seconds delay
  end process P1;

  P2: process
  begin
    if clk = '1' and clk'event then
      c <= nc;
    end if;
    wait on clk;
  end process P2;

  P3: process
  begin
    nc <= c + 1 after 5 ns; -- Five nano-seconds delay
    wait on c;
  end process P3;
end architecture arc;

En el proceso ‘P1’ la instrucción ’esperar’ no se usa para esperar hasta que cambie el valor de una señal, como vimos hasta ahora, sino para esperar un tiempo determinado. Este proceso modela un generador de reloj. La señal clk es el reloj de nuestro sistema, es periódica con periodo de 20 ns (50 MHz) y tiene ciclo de trabajo.

El proceso P2 modela un registro que, si acaba de ocurrir un flanco ascendente de clk, asigna el valor de su entrada nc a su salida c y luego espera el siguiente cambio de valor de clk.

El proceso P3 modela un incrementador que asigna el valor de su entrada c, incrementado en uno, a su salida nc… con un retardo físico de 5 ns. Luego espera hasta que cambie el valor de su entrada c. Esto también es nuevo. Hasta ahora siempre asignábamos señales con:

  s <= value;

que, por las razones explicadas en los apartados anteriores, podemos traducir implícitamente en:

  s <= value; -- after delta

Este pequeño sistema de hardware digital podría representarse mediante la siguiente figura:

Un contador síncrono

Con la introducción del tiempo físico, y sabiendo que también tenemos un tiempo simbólico medido en delta, ahora tenemos un tiempo bidimensional que denotaremos T+D donde T es un tiempo físico medido en nanosegundos y D un número de deltas (sin duración física).

La imagen completa

Hay un aspecto importante de la simulación VHDL que no discutimos todavía: después de una fase de ejecución, todos los procesos están en estado “suspendido”. Decimos informalmente que el programador luego actualiza los valores de las señales que se han asignado. Pero, en nuestro ejemplo de un contador síncrono, ¿debe actualizar las señales clk, c y nc al mismo tiempo? ¿Qué pasa con los retrasos físicos? ¿Y qué sucede a continuación con todos los procesos en estado “suspendido” y ninguno en estado “ejecutable”?

El algoritmo de simulación completo (pero simplificado) es el siguiente:

  1. Inicialización
    • Set current time Tc to 0+0 (0 ns, 0 delta-cycle)
    • Initialize all signals.
    • Execute each process until it suspends on a wait statement.
      • Record the values and delays of signal assignments.
      • Record the conditions for the process to resume (delay or signal change).
    • Compute the next time Tn as the earliest of:
      • The resume time of processes suspended by a wait for <delay>.
      • The next time at which a signal value shall change.
  1. Ciclo de simulación
    • Tc=Tn.
    • Update signals that need to be.
    • Put in run-able state all processes that were waiting for a value change of one of the signals that has been updated.
    • Put in run-able state all processes that were suspended by a wait for <delay> statement and for which the resume time is Tc.
    • Execute all run-able processes until they suspend.
      • Record the values and delays of signal assignments.
      • Record the conditions for the process to resume (delay or signal change).
    • Compute the next time Tn as the earliest of:
      • The resume time of processes suspended by a wait for <delay>.
      • The next time at which a signal value shall change.
    • If Tn is infinity, stop simulation. Else, start a new simulation cycle.

Simulación manual

Para concluir, ejerzamos ahora manualmente el algoritmo de simulación simplificado en el contador síncrono presentado anteriormente. Decidimos arbitrariamente que, cuando varios procesos son ejecutables, el orden será P3>P2>P1. Las siguientes tablas representan la evolución del estado del sistema durante la inicialización y los primeros ciclos de simulación. Cada señal tiene su propia columna en la que se indica el valor actual. Cuando se ejecuta una asignación de señal, el valor programado se agrega al valor actual, p. a/[email protected]+D si el valor actual es a y el siguiente valor será b en el tiempo T+D (tiempo físico más ciclos delta). Las 3 últimas columnas indican la condición para reanudar los procesos suspendidos (nombre de las señales que deben cambiar o hora en que se reanudará el proceso).

Fase de inicialización:

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 0+0
Inicializar todas las señales 0+0 ‘0’ 0 0
P3/nc<=c+1 después de 5 ns 0+0 ‘0’ 0 0/[email protected]+0
P3/espera en c 0+0 ‘0’ 0 0/[email protected]+0 c
P2/if clk='1'... 0+0 ‘0’ 0 0/[email protected]+0 c
P2/fin si 0+0 ‘0’ 0 0/[email protected]+0 c
P2/esperar en clk 0+0 ‘0’ 0 0/[email protected]+0 Clk c
P1/clk<='0' 0+0 ‘0’/‘0’@0+1 0 0/[email protected]+0 Clk c
P1/esperar 10 ns 0+0 ‘0’/‘0’@0+1 0 0/[email protected]+0 10+0 Clk c
Calcular la próxima vez 0+0 0+1 ‘0’/‘0’@0+1 0 0/[email protected]+0 10+0 Clk c

Ciclo de simulación #1

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 0+1 ‘0’/‘0’@0+1 0 0/[email protected]+0 10+0 Clk c
Actualizar señales 0+1 ‘0’ 0 0/[email protected]+0 10+0 Clk c
Calcular la próxima vez 0+1 5+0 ‘0’ 0 0/[email protected]+0 10+0 Clk c

Nota: durante el primer ciclo de simulación no hay fase de ejecución porque ninguno de nuestros 3 procesos tiene su condición de reanudación satisfecha. P2 está esperando un cambio de valor de clk y ha habido una transacción en clk, pero como los valores antiguo y nuevo son los mismos, esto no es un cambio de valor.

Ciclo de simulación #2

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 5+0 ‘0’ 0 0/[email protected]+0 10+0 Clk c
Actualizar señales 5+0 ‘0’ 0 1 10+0 Clk c
Calcular la próxima vez 5+0 10+0 ‘0’ 0 1 10+0 Clk c

Nota: de nuevo, no hay fase de ejecución. nc cambió pero ningún proceso está esperando en nc.

Ciclo de simulación #3

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 10+0 ‘0’ 0 1 10+0 Clk c
Actualizar señales 10+0 ‘0’ 0 1 10+0 Clk c
P1/clk<='1' 10+0 ‘0’/‘1’@10+1 0 1 Clk c
P1/esperar 10 ns 10+0 ‘0’/‘1’@10+1 0 1 20+0 Clk c
Calcular la próxima vez 10+0 10+1 ‘0’/‘1’@10+1 0 1 20+0 Clk c

Ciclo de simulación #4

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 10+1 ‘0’/‘1’@10+1 0 1 20+0 Clk c
Actualizar señales 10+1 ‘1’ 0 1 20+0 Clk c
P2/if clk='1'... 10+1 ‘1’ 0 1 20+0 c
P2/c<=nc 10+1 ‘1’ 0/[email protected]+2 1 20+0 c
P2/fin si 10+1 ‘1’ 0/[email protected]+2 1 20+0 c
P2/esperar en clk 10+1 ‘1’ 0/[email protected]+2 1 20+0 Clk c
Calcular la próxima vez 10+1 10+2 ‘1’ 0/[email protected]+2 1 20+0 Clk c

Ciclo de simulación #5

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 10+2 ‘1’ 0/[email protected]+2 1 20+0 Clk c
Actualizar señales 10+2 ‘1’ 1 1 20+0 Clk c
P3/nc<=c+1 después de 5 ns 10+2 ‘1’ 1 1/[email protected]+0 20+0 Clk
P3/espera en c 10+2 ‘1’ 1 1/[email protected]+0 20+0 Clk c
Calcular la próxima vez 10+2 15+0 ‘1’ 1 1/[email protected]+0 20+0 Clk c

Nota: se podría pensar que la actualización nc estaría programada en 15+2, mientras que nosotros la programamos en 15+0. Al agregar un retraso físico distinto de cero (aquí 5 ns) a un tiempo actual (10+2), los ciclos delta desaparecen. De hecho, los ciclos delta son útiles solo para distinguir diferentes tiempos de simulación T+0, T+1… con el mismo tiempo físico T. Tan pronto como cambie el tiempo físico, los ciclos delta se pueden restablecer.

Ciclo de simulación #6

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 15+0 ‘1’ 1 1/[email protected]+0 20+0 Clk c
Actualizar señales 15+0 ‘1’ 1 2 20+0 Clk c
Calcular la próxima vez 15+0 20+0 ‘1’ 1 2 20+0 Clk c

Nota: de nuevo, no hay fase de ejecución. nc cambió pero ningún proceso está esperando en nc.

Ciclo de simulación #7

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 20+0 ‘1’ 1 2 20+0 Clk c
Actualizar señales 20+0 ‘1’ 1 2 20+0 Clk c
P1/clk<='0' 20+0 ‘1’/‘0’@20+1 1 2 Clk c
P1/esperar 10 ns 20+0 ‘1’/‘0’@20+1 1 2 30+0 Clk c
Calcular la próxima vez 20+0 20+1 ‘1’/‘0’@20+1 1 2 30+0 Clk c

Ciclo de simulación #8

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 20+1 ‘1’/‘0’@20+1 1 2 30+0 Clk c
Actualizar señales 20+1 ‘0’ 1 2 30+0 Clk c
P2/if clk='1'... 20+1 ‘0’ 1 2 30+0 c
P2/fin si 20+1 ‘0’ 1 2 30+0 c
P2/esperar en clk 20+1 ‘0’ 1 2 30+0 Clk c
Calcular la próxima vez 20+1 30+0 ‘0’ 1 2 30+0 Clk c

Ciclo de simulación #9

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 30+0 ‘0’ 1 2 30+0 Clk c
Actualizar señales 30+0 ‘0’ 1 2 30+0 Clk c
P1/clk<='1' 30+0 ‘0’/‘1’@30+1 1 2 Clk c
P1/esperar 10 ns 30+0 ‘0’/‘1’@30+1 1 2 40+0 Clk c
Calcular la próxima vez 30+0 30+1 ‘0’/‘1’@30+1 1 2 40+0 Clk c

Ciclo de simulación #10

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 30+1 ‘0’/‘1’@30+1 1 2 40+0 Clk c
Actualizar señales 30+1 ‘1’ 1 2 40+0 Clk c
P2/if clk='1'... 30+1 ‘1’ 1 2 40+0 c
P2/c<=nc 30+1 ‘1’ 1/[email protected]+2 2 40+0 c
P2/fin si 30+1 ‘1’ 1/[email protected]+2 2 40+0 c
P2/esperar en clk 30+1 ‘1’ 1/[email protected]+2 2 40+0 Clk c
Calcular la próxima vez 30+1 30+2 ‘1’ 1/[email protected]+2 2 40+0 Clk c

Ciclo de simulación #11

Operaciones Tc Tn Clk c nc P1 P2 P3
Establecer hora actual 30+2 ‘1’ 1/[email protected]+2 2 40+0 Clk c
Actualizar señales 30+2 ‘1’ 2 2 40+0 Clk c
P3/nc<=c+1 después de 5 ns 30+2 ‘1’ 2 2/[email protected]+0 40+0 Clk
P3/espera en c 30+2 ‘1’ 2 2/[email protected]+0 40+0 Clk c
Calcular la próxima vez 30+2 35+0 ‘1’ 2 2/[email protected]+0 40+0 Clk c

Contador síncrono

-- File counter.vhd
-- The entity is the interface part. It has a name and a set of input / output
-- ports. Ports have a name, a direction and a type. The bit type has only two
-- values: '0' and '1'. It is one of the standard types.
entity counter is
  port(
    clock: in  bit;    -- We are using the rising edge of CLOCK
    reset: in  bit;    -- Synchronous and active HIGH
    data:  out natural -- The current value of the counter
  );
end entity counter;

-- The architecture describes the internals. It is always associated
-- to an entity.
architecture sync of counter is
  -- The internal signals we use to count. Natural is another standard
  -- type. VHDL is not case sensitive.
  signal current_value: natural;
  signal NEXT_VALUE:    natural;
begin
  -- A process is a concurrent statement. It is an infinite loop.
  process
  begin
    -- The wait statement is a synchronization instruction. We wait
    -- until clock changes and its new value is '1' (a rising edge).
    wait until clock = '1';
    -- Our reset is synchronous: we consider it only at rising edges
    -- of our clock.
    if reset = '1' then
      -- <= is the signal assignment operator.
      current_value <= 0;
    else
      current_value <= next_value;
    end if;
  end process;

  -- Another process. The sensitivity list is another way to express
  -- synchronization constraints. It (approximately) means: wait until
  -- one of the signals in the list changes and then execute the process
  -- body. Sensitivity list and wait statements cannot be used together 
  -- in the same process.
  process(current_value)
  begin
    next_value <= current_value + 1;
  end process;

  -- A concurrent signal assignment, which is just a shorthand for the
  -- (trivial) equivalent process.
  data <= current_value;
end architecture sync;

Instalación o configuración

Un programa VHDL se puede simular o sintetizar. La simulación es lo que más se parece a la ejecución en otros lenguajes de programación. La síntesis traduce un programa VHDL en una red de puertas lógicas. Muchas herramientas de simulación y síntesis de VHDL son parte de las suites comerciales de automatización de diseño electrónico (EDA). Con frecuencia también manejan otros lenguajes de descripción de hardware (HDL), como Verilog, SystemVerilog o SystemC. Existen algunas aplicaciones gratuitas y de código abierto.

simulación VHDL

GHDL es probablemente el simulador VHDL gratuito y de código abierto más maduro. Viene en tres sabores diferentes según el backend utilizado: gcc, llvm o mcode. Los siguientes ejemplos muestran cómo usar GHDL (versión mcode) y Modelsim, el simulador comercial HDL de Mentor Graphics, bajo un sistema operativo GNU/Linux. Las cosas serían muy similares con otras herramientas y otros sistemas operativos.

Hola Mundo

Cree un archivo hello_world.vhd que contenga:

-- File hello_world.vhd
entity hello_world is
end entity hello_world;

architecture arc of hello_world is
begin
  assert false report "Hello world!" severity note;
end architecture arc;

Una unidad de compilación VHDL es un programa VHDL completo que se puede compilar solo. Entidades son unidades de compilación VHDL que se utilizan para describir la interfaz externa de un circuito digital, es decir, sus puertos de entrada y salida. En nuestro ejemplo, la entidad se llama hola_mundo y está vacía. El circuito que estamos modelando es una caja negra, no tiene entradas ni salidas. Arquitecturas son otro tipo de unidad de compilación. Siempre están asociados a una entidad y se utilizan para describir el comportamiento del circuito digital. Una entidad puede tener una o más arquitecturas para describir el comportamiento de la entidad. En nuestro ejemplo, la entidad está asociada a solo una arquitectura llamada arc que contiene solo una instrucción VHDL:

  assert false report "Hello world!" severity note;

La declaración se ejecutará al comienzo de la simulación e imprimirá el mensaje ¡Hola mundo! en la salida estándar. La simulación terminará entonces porque no hay nada más que hacer. El archivo fuente VHDL que escribimos contiene dos unidades de compilación. Podríamos haberlos separado en dos archivos diferentes, pero no podríamos haberlos dividido en archivos diferentes: una unidad de compilación debe estar contenida por completo en un archivo fuente. Tenga en cuenta que esta arquitectura no se puede sintetizar porque no describe una función que se pueda traducir directamente a puertas lógicas.

Analiza y ejecuta el programa con GHDL:

$ mkdir gh_work
$ ghdl -a --workdir=gh_work hello_world.vhd
$ ghdl -r --workdir=gh_work hello_world
hello_world.vhd:6:8:@0ms:(assertion note): Hello world!

El directorio gh_work es donde GHDL almacena los archivos que genera. Esto es lo que dice la opción --workdir=gh_work. La fase de análisis verifica la corrección de la sintaxis y produce un archivo de texto que describe las unidades de compilación encontradas en el archivo fuente. La fase de ejecución en realidad compila, vincula y ejecuta el programa. Tenga en cuenta que, en la versión mcode de GHDL, no se generan archivos binarios. El programa se vuelve a compilar cada vez que lo simulamos. Las versiones gcc o llvm se comportan de manera diferente. Tenga en cuenta también que ghdl -r no toma el nombre de un archivo fuente VHDL, como lo hace ghdl -a, sino el nombre de una unidad de compilación. En nuestro caso le pasamos el nombre de la entidad. Como solo tiene una arquitectura asociada, no es necesario especificar cuál simular.

Con Modelsim:

$ vlib ms_work
$ vmap work ms_work
$ vcom hello_world.vhd
$ vsim -c hello_world -do 'run -all; quit'
...
# ** Note: Hello world!
#    Time: 0 ns  Iteration: 0  Instance: /hello_world
...

vlib, vmap, vcom y vsim son cuatro comandos que proporciona Modelsim. vlib crea un directorio (ms_work) donde se almacenarán los archivos generados. vmap asocia un directorio creado por vlib con un nombre lógico (work). vcom compila un archivo fuente VHDL y, por defecto, almacena el resultado en el directorio asociado al nombre lógico work. Finalmente, vsim simula el programa y produce el mismo tipo de salida que GHDL. Tenga en cuenta de nuevo que lo que pide vsim no es un archivo fuente sino el nombre de una unidad de compilación ya compilada. La opción -c le dice al simulador que se ejecute en modo de línea de comandos en lugar del modo predeterminado de interfaz gráfica de usuario (GUI). La opción -do se usa para pasar un script TCL para que se ejecute después de cargar el diseño. TCL es un lenguaje de secuencias de comandos muy utilizado en las herramientas EDA. El valor de la opción -do puede ser el nombre de un archivo o, como en nuestro ejemplo, una cadena de comandos TCL. ejecutar -todos; quit instruye al simulador para que ejecute la simulación hasta que finalice naturalmente, o para siempre si dura para siempre, y luego para salir.

Hola Mundo

Hay muchas formas de imprimir el clásico “¡Hola mundo!” mensaje en VHDL. El más simple de todos es probablemente algo como:

-- File hello_world.vhd
entity hello_world is
end entity hello_world;

architecture arc of hello_world is
begin
  assert false report "Hello world!" severity note;
end architecture arc;

Un entorno de simulación para el contador síncrono

Entornos de simulación

Un entorno de simulación para un diseño VHDL (el Diseño bajo prueba o DUT) es otro diseño VHDL que, como mínimo:

  • Declara las señales correspondientes a los puertos de entrada y salida del DUT.
  • Instancia el DUT y conecta sus puertos a las señales declaradas.
  • Instancia los procesos que impulsan las señales conectadas a los puertos de entrada del DUT.

Opcionalmente, un entorno de simulación puede instanciar otros diseños además del DUT, como, por ejemplo, generadores de tráfico en las interfaces, monitores para comprobar los protocolos de comunicación, verificadores automáticos de las salidas del DUT…

El entorno de simulación es analizado, elaborado y ejecutado. La mayoría de los simuladores ofrecen la posibilidad de seleccionar un conjunto de señales para observar, trazar sus formas de onda gráficas, poner puntos de interrupción en el código fuente, introducir el código fuente…

Idealmente, un entorno de simulación debería poder utilizarse como una prueba robusta de no regresión, es decir, debería detectar automáticamente violaciones de las especificaciones del DUT, informar mensajes de error útiles y garantizar una cobertura razonable de las funcionalidades del DUT. Cuando tales entornos de simulación están disponibles, se pueden volver a ejecutar en cada cambio del dispositivo bajo prueba para comprobar que sigue siendo funcionalmente correcto, sin necesidad de inspecciones visuales tediosas y propensas a errores de las trazas de simulación.

En la práctica, diseñar entornos de simulación ideales o incluso buenos es un desafío. Con frecuencia es tan difícil, o incluso más, que diseñar el propio DUT.

En este ejemplo, presentamos un entorno de simulación para el ejemplo [Synchronous counter](https://www.wikiod.com/es/vhdl/comenzando-con-vhdl#Contador síncrono). Mostramos cómo ejecutarlo usando GHDL y Modelsim y cómo observar formas de onda gráficas usando GTKWave con GHDL y el visor de forma de onda incorporado con Modelsim. Luego discutimos un aspecto interesante de las simulaciones: ¿cómo detenerlas?

Un primer entorno de simulación para el contador síncrono

El contador síncrono tiene dos puertos de entrada y uno de salida. Un entorno de simulación muy simple podría ser:

-- File counter_sim.vhd
-- Entities of simulation environments are frequently black boxes without
-- ports.
entity counter_sim is
end entity counter_sim;

architecture sim of counter_sim is

  -- One signal per port of the DUT. Signals can have the same name as
  -- the corresponding port but they do not need to.
  signal clk:  bit;
  signal rst:  bit;
  signal data: natural;

begin

  -- Instantiation of the DUT
  u0: entity work.counter(sync)
  port map(
    clock => clk,
    reset => rst,
    data  => data
  );

  -- A clock generating process with a 2ns clock period. The process
  -- being an infinite loop, the clock will never stop toggling.
  process
  begin
    clk <= '0';
    wait for 1 ns;
    clk <= '1';
    wait for 1 ns;
  end process;

  -- The process that handles the reset: active from beginning of
  -- simulation until the 5th rising edge of the clock.
  process
  begin
    rst  <= '1';
    for i in 1 to 5 loop
      wait until rising_edge(clk);
    end loop;
    rst  <= '0';
    wait; -- Eternal wait. Stops the process forever.
  end process;

end architecture sim;

Simulando con GHDL

Vamos a compilar y simular esto con GHDL:

$ mkdir gh_work
$ ghdl -a --workdir=gh_work counter_sim.vhd
counter_sim.vhd:27:24: unit "counter" not found in 'library "work"'
counter_sim.vhd:50:35: no declaration for "rising_edge"

Entonces los mensajes de error nos dicen dos cosas importantes:

  • El analizador GHDL descubrió que nuestro diseño instancia una entidad llamada “contador”, pero esta entidad no se encontró en la biblioteca “trabajo”. Esto se debe a que no compilamos counter antes de counter_sim. Al compilar diseños VHDL que instancian entidades, los niveles inferiores siempre deben compilarse antes que los niveles superiores (los diseños jerárquicos también se pueden compilar de arriba hacia abajo, pero solo si instancian “componentes”, no entidades).
  • La función rising_edge utilizada por nuestro diseño no está definida. Esto se debe al hecho de que esta función se introdujo en VHDL 2008 y no le dijimos a GHDL que usara esta versión del lenguaje (de manera predeterminada, usa VHDL 1993 con tolerancia a la sintaxis de VHDL 1987).

Arreglemos los dos errores y lancemos la simulación:

$ ghdl -a --workdir=gh_work --std=08 counter.vhd counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
^C

Tenga en cuenta que la opción --std=08 es necesaria para el análisis y la simulación. Tenga en cuenta también que lanzamos la simulación en la entidad counter_sim, arquitectura sim, no en un archivo fuente.

Como nuestro entorno de simulación tiene un proceso interminable (el proceso que genera el reloj), la simulación no se detiene y debemos interrumpirla manualmente. En su lugar, podemos especificar un tiempo de parada con la opción --stop-time:

$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns
ghdl:info: simulation stopped by --stop-time

Tal como está, la simulación no nos dice mucho sobre el comportamiento de nuestro DUT. Volquemos los cambios de valor de las señales en un archivo:

$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns --vcd=counter_sim.vcd
Vcd.Avhpi_Error!
ghdl:info: simulation stopped by --stop-time

(ignore el mensaje de error, esto es algo que debe corregirse en GHDL y eso no tiene ninguna consecuencia). Se ha creado un archivo counter_sim.vcd. Contiene en formato VCD (ASCII) todos los cambios de señal durante la simulación. GTKWave puede mostrarnos las formas de onda gráficas correspondientes:

$ gtkwave counter_sim.vcd

donde podemos ver que el contador funciona como se esperaba.

Una forma de onda GTKWave

Simulando con Modelsim

El principio es exactamente el mismo con Modelsim:

$ vlib ms_work
...
$ vmap work ms_work
...
$ vcom -nologo -quiet -2008 counter.vhd counter_sim.vhd
$ vsim -voptargs="+acc" 'counter_sim(sim)' -do 'add wave /*; run 60ns'

ingrese la descripción de la imagen aquí

Tenga en cuenta la opción -voptargs="+acc" pasada a vsim: evita que el simulador optimice la señal data y nos permite verla en las formas de onda.

Simulaciones que terminan con gracia

Con ambos simuladores teníamos que interrumpir la simulación interminable o especificar un tiempo de parada con una opción dedicada. Esto no es muy conveniente. En muchos casos, la hora de finalización de una simulación es difícil de anticipar. Sería mucho mejor detener la simulación desde dentro del código VHDL del entorno de simulación, cuando se alcanza una condición particular, como, por ejemplo, cuando el valor actual del contador llega a 20. Esto se puede lograr con una afirmación en el proceso que maneja el reinicio:

  process
  begin
    rst <= '1';
    for i in 1 to 5 loop
      wait until rising_edge(clk);
    end loop;
    rst <= '0';
    loop
      wait until rising_edge(clk);
      assert data /= 20 report "End of simulation" severity failure;
    end loop;
  end process;

Siempre que data sea diferente de 20, la simulación continúa. Cuando data llega a 20, la simulación falla con un mensaje de error:

$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:90:24:@51ns:(assertion failure): End of simulation
ghdl:error: assertion failed
  from: process work.counter_sim(sim2).P1 at counter_sim.vhd:90
ghdl:error: simulation failed

Tenga en cuenta que volvimos a compilar solo el entorno de simulación: es el único diseño que cambió y es el nivel superior. Si hubiéramos modificado solo counter.vhd, habríamos tenido que volver a compilar ambos: counter.vhd porque cambió y counter_sim.vhd porque depende de counter.vhd.

Bloquear la simulación con un mensaje de error no es muy elegante. Incluso puede ser un problema cuando se analizan automáticamente los mensajes de simulación para decidir si se pasa o no una prueba automática de no regresión. Una solución mejor y mucho más elegante es detener todos los procesos cuando se alcanza una condición. Esto se puede hacer, por ejemplo, agregando una señal booleana de fin de simulación (eof). Por defecto se inicializa a falso al comienzo de la simulación. Uno de nuestros procesos lo establecerá en “verdadero” cuando llegue el momento de finalizar la simulación. Todos los demás procesos monitorearán esta señal y se detendrán con una eterna ’espera’ cuando se vuelva ‘verdadera’:

  signal eos:  boolean;
...
  process
  begin
    clk <= '0';
    wait for 1 ns;
    clk <= '1';
    wait for 1 ns;
    if eos then
      report "End of simulation";
      wait;
    end if;
  end process;

  process
  begin
    rst <= '1';
    for i in 1 to 5 loop
      wait until rising_edge(clk);
    end loop;
    rst <= '0';
    for i in 1 to 20 loop
      wait until rising_edge(clk);
    end loop;
    eos <= true;
    wait;
  end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:120:24:@50ns:(report note): End of simulation

Por último, pero no menos importante, hay una solución aún mejor introducida en VHDL 2008 con el paquete estándar env y los procedimientos stop y finish que declara:

use std.env.all;
...
  process
  begin
    rst    <= '1';
    for i in 1 to 5 loop
      wait until rising_edge(clk);
    end loop;
    rst    <= '0';
    for i in 1 to 20 loop
      wait until rising_edge(clk);
    end loop;
    finish;
  end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
simulation finished @49ns