Buscar..


Introducción

En este tema, proponemos un método simple para diseñar correctamente circuitos digitales simples con VHDL. El método se basa en diagramas de bloques gráficos y en un principio fácil de recordar:

Piense en el hardware primero, codifique VHDL a continuación

Está dirigido a los principiantes en el diseño de hardware digital utilizando VHDL, con un conocimiento limitado de la semántica de síntesis del lenguaje.

Observaciones

El diseño de hardware digital con VHDL es simple, incluso para principiantes, pero hay algunas cosas importantes que debe saber y un pequeño conjunto de reglas que debe obedecer. La herramienta utilizada para transformar una descripción de VHDL en hardware digital es un sintetizador lógico. La semántica del lenguaje VHDL utilizado por los sintetizadores lógicos es bastante diferente de la semántica de simulación descrita en el Manual de referencia del lenguaje (LRM). Peor aún: no está estandarizado y varía entre las herramientas de síntesis.

El método propuesto introduce varias limitaciones importantes en aras de la simplicidad:

  • Sin pestillos disparados por nivel.
  • Los circuitos están sincronizados en el borde ascendente de un solo reloj.
  • No hay reinicio ni ajuste asíncrono.
  • No hay unidad múltiple en señales resueltas.

El ejemplo del diagrama de bloques , primero de una serie de 3, presenta brevemente los conceptos básicos del hardware digital y propone una breve lista de reglas para diseñar un diagrama de bloques de un circuito digital. Las reglas ayudan a garantizar una traducción directa al código VHDL que simula y sintetiza como se espera.

El ejemplo de codificación explica la traducción de un diagrama de bloques al código VHDL y lo ilustra en un circuito digital simple.

Finalmente, el ejemplo del concurso de diseño de John Cooley muestra cómo aplicar el método propuesto en un ejemplo más complejo de circuito digital. También elabora las limitaciones introducidas y relaja algunas de ellas.

Diagrama de bloques

El hardware digital se construye a partir de dos tipos de primitivas de hardware:

  • Puertas combinatorias (inversores, y, o xor, sumadores completos de 1 bit, multiplexores de 1 bit ...) Estas puertas lógicas realizan un cálculo booleano simple en sus entradas y producen una salida. Cada vez que cambia una de sus entradas, comienzan a propagar señales eléctricas y, después de un breve retraso, la salida se estabiliza al valor resultante. El retardo de propagación es importante porque está fuertemente relacionado con la velocidad a la que puede funcionar el circuito digital, es decir, su frecuencia de reloj máxima.
  • Elementos de memoria (latches, D-flip-flops, RAMs ...). Contrariamente a las puertas lógicas combinatorias, los elementos de memoria no reaccionan inmediatamente al cambio de cualquiera de sus entradas. Tienen entradas de datos, entradas de control y salidas de datos. Reaccionan ante una combinación particular de entradas de control, no ante ningún cambio de sus entradas de datos. El D-flip-flop disparado por el flanco ascendente (DFF), por ejemplo, tiene una entrada de reloj y una entrada de datos. En cada flanco ascendente del reloj, la entrada de datos se muestrea y se copia en la salida de datos que permanece estable hasta el siguiente flanco ascendente del reloj, incluso si la entrada de datos cambia entre ellos.

Un circuito de hardware digital es una combinación de lógica combinatoria y elementos de memoria. Los elementos de memoria tienen varios roles. Uno de ellos es permitir la reutilización de la misma lógica combinatoria para varias operaciones consecutivas en diferentes datos. Los circuitos que usan esto a menudo se denominan circuitos secuenciales . La siguiente figura muestra un ejemplo de un circuito secuencial que acumula valores enteros utilizando el mismo sumador combinatorio, gracias a un registro activado de flanco ascendente. También es nuestro primer ejemplo de diagrama de bloques.

Un circuito secuencial

El revestimiento de tuberías es otro uso común de los elementos de memoria y la base de muchas arquitecturas de microprocesadores. Su objetivo es aumentar la frecuencia de reloj de un circuito dividiendo un procesamiento complejo en una sucesión de operaciones más simples y paralelizando la ejecución de varios procesamientos consecutivos:

Tubería de revestimiento de un complejo procesamiento combinatorio.

El diagrama de bloques es una representación gráfica del circuito digital. Ayuda a tomar las decisiones correctas y obtener una buena comprensión de la estructura general antes de la codificación. Es el equivalente de las fases de análisis preliminares recomendadas en muchos métodos de diseño de software. Los diseñadores experimentados frecuentemente saltan esta fase de diseño, al menos para circuitos simples. Sin embargo, si usted es un principiante en diseño de hardware digital, y si desea codificar un circuito digital en VHDL, adoptar las 10 reglas simples a continuación para dibujar su diagrama de bloques debería ayudarlo a hacerlo bien:

  1. Rodea tu dibujo con un gran rectángulo. Este es el límite de tu circuito. Todo lo que cruza este límite es un puerto de entrada o salida. La entidad VHDL describirá este límite.
  2. Claramente, los registros activados por flanco (por ejemplo, bloques cuadrados) de la lógica combinatoria (por ejemplo, bloques redondos). En VHDL se traducirán en procesos pero de dos tipos muy diferentes: sincrónicos y combinatorios.
  3. No utilice pestillos activados por nivel, use solo registros activados por flanco ascendente. Esta restricción no proviene de VHDL, que es perfectamente utilizable para modelar pestillos. Es solo un consejo razonable para principiantes. Los cierres son menos necesarios y su uso plantea muchos problemas que probablemente deberíamos evitar, al menos para nuestros primeros diseños.
  4. Utilice el mismo reloj único para todos sus registros activados de flanco ascendente. Una vez más, esta restricción está aquí por simplicidad. No proviene de VHDL, que es perfectamente utilizable para modelar sistemas de reloj múltiple. Nombra el reloj del clock . Viene del exterior y es una entrada de todos los bloques cuadrados y solo de ellos. Si lo desea, ni siquiera represente el reloj, es el mismo para todos los bloques cuadrados y puede dejarlo implícito en su diagrama.
  5. Representa las comunicaciones entre bloques con flechas con nombre y orientadas. Para el bloque del que proviene una flecha, la flecha es una salida. Para el bloque al que va una flecha, la flecha es una entrada. Todas estas flechas se convertirán en puertos de la entidad VHDL, si están cruzando el rectángulo grande, o señales de la arquitectura VHDL.
  6. Las flechas tienen un solo origen, pero pueden tener varios destinos. De hecho, si una flecha tuviera varios orígenes, crearíamos una señal VHDL con varios controladores. Esto no es completamente imposible, pero requiere un cuidado especial para evitar cortocircuitos. Así evitaremos esto por ahora. Si una flecha tiene varios destinos, bifurque la flecha tantas veces como sea necesario. Use puntos para distinguir los cruces conectados y no conectados.
  7. Algunas flechas vienen de fuera del rectángulo grande. Estos son los puertos de entrada de la entidad. Una flecha de entrada no puede ser también la salida de ninguno de sus bloques. Esto se aplica mediante el lenguaje VHDL: los puertos de entrada de una entidad se pueden leer pero no se pueden escribir. Esto es de nuevo para evitar cortocircuitos.
  8. Algunas flechas salen afuera. Estos son los puertos de salida. En las versiones VHDL anteriores a 2008, los puertos de salida de una entidad se pueden escribir pero no leer. Por lo tanto, una flecha de salida debe tener un solo origen y un único destino: el exterior. Sin bifurcaciones en las flechas de salida, una flecha de salida no puede ser también la entrada de uno de sus bloques. Si desea usar una flecha de salida como entrada para algunos de sus bloques, inserte un nuevo bloque redondo para dividirlo en dos partes: la interna, con tantas horquillas como desee, y la flecha de salida que proviene de la nueva Bloquea y sale a la calle. El nuevo bloque se convertirá en una simple asignación continua en VHDL. Una especie de cambio de nombre transparente. Desde VHDL 2008 también se pueden leer los puertos de salida.
  9. Todas las flechas que no vienen o van desde / hacia el exterior son señales internas. Los declararás todos en la arquitectura VHDL.
  10. Cada ciclo en el diagrama debe comprender al menos un bloque cuadrado. Esto no se debe a VHDL. Viene de los principios básicos del diseño de hardware digital. Los bucles combinatorios deben ser absolutamente evitados. Excepto en casos muy raros, no producen ningún resultado útil. Y un ciclo del diagrama de bloques que comprendería solo bloques redondos sería un bucle combinatorio.

No olvide revisar cuidadosamente la última regla, es tan esencial como las otras, pero puede ser un poco más difícil de verificar.

A menos que necesite absolutamente características que excluimos por ahora, como pestillos, relojes múltiples o señales con múltiples controladores, debe dibujar fácilmente un diagrama de bloques de su circuito que cumpla con las 10 reglas. De lo contrario, es probable que el problema sea con el circuito que desea, no con VHDL o el sintetizador lógico. Y probablemente significa que el circuito que desea no es hardware digital.

La aplicación de las 10 reglas a nuestro ejemplo de un circuito secuencial llevaría a un diagrama de bloques como:

Diagrama de bloques retrabajado del circuito secuencial.

  1. El rectángulo grande alrededor del diagrama está cruzado por 3 flechas, que representan los puertos de entrada y salida de la entidad VHDL.
  2. El diagrama de bloques tiene dos bloques redondos (combinatorios), el sumador y el bloque de cambio de nombre de salida, y un bloque cuadrado (síncrono), el registro.
  3. Utiliza solo registros activados por flanco.
  4. Solo hay un reloj, llamado clock y solo usamos su flanco ascendente.
  5. El diagrama de bloques tiene cinco flechas, una con un tenedor. Corresponden a dos señales internas, dos puertos de entrada y un puerto de salida.
  6. Todas las flechas tienen un origen y un destino, excepto la flecha denominada Sum que tiene dos destinos.
  7. Las flechas Data_in y Clock son nuestros dos puertos de entrada. No son salida de nuestros propios bloques.
  8. La flecha Data_out es nuestro puerto de salida. Para ser compatibles con las versiones VHDL anteriores a 2008, agregamos un bloque de redenominación (redondeo) adicional entre Sum y Data_out . Por lo tanto, Data_out tiene exactamente una fuente y un destino.
  9. Sum y Next_sum son nuestras dos señales internas.
  10. Hay exactamente un ciclo en la gráfica y comprende un bloque cuadrado.

Nuestro diagrama de bloques cumple con las 10 reglas. El ejemplo de codificación detallará cómo traducir este tipo de diagramas de bloques en VHDL.

Codificación

Este ejemplo es el segundo de una serie de 3. Si aún no lo hizo, primero lea el ejemplo del diagrama de bloques .

Con un diagrama de bloques que cumple con las 10 reglas (consulte el ejemplo del diagrama de bloques ), la codificación VHDL se vuelve sencilla:

  • el gran rectángulo circundante se convierte en la entidad VHDL,
  • Las flechas internas se convierten en señales VHDL y se declaran en la arquitectura.
  • Cada bloque cuadrado se convierte en un proceso síncrono en el cuerpo de la arquitectura.
  • Cada bloque redondo se convierte en un proceso combinatorio en el cuerpo de la arquitectura.

Ilustrémoslo en el diagrama de bloques de un circuito secuencial:

Un circuito secuencial

El modelo VHDL de un circuito comprende dos unidades de compilación:

  • La entidad que describe el nombre del circuito y su interfaz (nombres de puertos, direcciones y tipos). Es una traducción directa del gran rectángulo circundante del diagrama de bloques. Suponiendo que los datos son enteros y que el clock usa el bit tipo VHDL (solo dos valores: '0' y '1' ), la entidad de nuestro circuito secuencial podría ser:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • La arquitectura que describe las partes internas del circuito (lo que hace). Aquí es donde se declaran las señales internas y donde se instancian todos los procesos. El esqueleto de la arquitectura de nuestro circuito secuencial podría ser:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Tenemos tres procesos para agregar al cuerpo de la arquitectura, uno síncrono (bloque cuadrado) y dos combinatorios (bloques redondos).

Un proceso síncrono se ve así:

process(clock)
begin
  if rising_edge(clock) then
    o1 <= i1;
    ...
    ox <= ix;
  end if;
end process;

donde i1, i2,..., ix son todas las flechas que entran en el bloque cuadrado correspondiente del diagrama y o1, ..., ox son todas las flechas que o1, ..., ox el bloque cuadrado correspondiente del diagrama. Absolutamente nada será cambiado, excepto los nombres de las señales, por supuesto. Nada. Ni siquiera un solo personaje.

El proceso síncrono de nuestro ejemplo es así:

  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

Que se puede traducir de manera informal a: si el clock cambia, y solo entonces, si el cambio es un flanco ascendente ( '0' a '1' ), asigne el valor de la señal Next_sum a la señal Sum .

Un proceso combinatorio se ve así:

process(i1, i2,... , ix)
  variable v1: <type_of_v1>;
  ...
  variable vy: <type_of_vy>;
begin
  v1 := <default_value_for_v1>;
  ...
  vy := <default_value_for_vy>;
  o1 <= <default_value_for_o1>;
  ...
  oz <= <default_value_for_oz>;
  <statements>
end process;

donde i1, i2,..., in son todas las flechas que entran en el bloque redondo correspondiente del diagrama. todo y no mas No olvidaremos ninguna flecha y no agregaremos nada más a la lista.

v1, ..., vy son variables que podemos necesitar para simplificar el código del proceso. Tienen exactamente el mismo rol que en cualquier otro lenguaje de programación imperativo: mantener valores temporales. Deben ser absolutamente asignados todos antes de ser leído. Si no garantizamos esto, el proceso ya no será combinatorio, ya que modelará el tipo de elementos de memoria para retener el valor de algunas variables de una ejecución de proceso a la siguiente. Este es el motivo de las declaraciones vi := <default_value_for_vi> al comienzo del proceso. Tenga en cuenta que <default_value_for_vi> deben ser constantes. Si no, si son expresiones, podríamos usar accidentalmente variables en las expresiones y leer una variable antes de asignarla.

o1, ..., om son todas las flechas que o1, ..., om el bloque redondo correspondiente de su diagrama. todo y no mas Absolutamente deben ser asignados al menos una vez durante la ejecución del proceso. Como las estructuras de control VHDL ( if case ...) pueden evitar fácilmente que se asigne una señal de salida, le recomendamos encarecidamente que asigne a cada una de ellas, incondicionalmente, con un valor constante <default_value_for_oi> al comienzo del proceso. De esta manera, incluso si una instrucción if enmascara una asignación de señal, de todos modos habrá recibido un valor.

Absolutamente nada se cambiará a este esqueleto VHDL, excepto los nombres de las variables, si las hay, los nombres de las entradas, los nombres de las salidas, los valores de <default_value_for_..> constants y <statements> . No se olvide de una única asignación de valores por defecto, si lo hace la síntesis inferirá elementos de memoria no deseadas (lo más probable pestillos) y el resultado no será lo que inicialmente quería.

En nuestro ejemplo de circuito secuencial, el proceso de sumador combinatorio es:

  process(Sum, Data_in)
  begin
    Next_sum <= 0;
    Next_sum <= Sum + Data_in;
  end process;

Lo que se puede traducir de manera informal a: si Sum o Data_in (o ambos) cambian, asigne el valor 0 para señalar a Next_sum y luego asigne nuevamente el valor Sum + Data_in .

Como la primera asignación (con el valor predeterminado constante 0 ) es seguida inmediatamente por otra asignación que la sobrescribe, podemos simplificar:

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

El segundo proceso combinatorio corresponde al bloque redondo que agregamos en una flecha de salida con más de un destino para cumplir con las versiones VHDL anteriores a 2008. Su código es simplemente:

  process(Sum)
  begin
    Data_out <= 0;
    Data_out <= Sum;
  end process;

Por la misma razón que con el otro proceso combinatorio, podemos simplificarlo como:

  process(Sum)
  begin
    Data_out <= Sum;
  end process;

El código completo para el circuito secuencial es:

-- File sequential_circuit.vhd
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;

architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

  process(Sum)
  begin
    Data_out <= Sum;
  end process;
end architecture ten_rules;

Nota: podríamos escribir los tres procesos en cualquier orden, no cambiaríamos nada al resultado final en la simulación o en la síntesis. Esto se debe a que los tres procesos son declaraciones simultáneas y VHDL los trata como si fueran realmente paralelos.

Concurso de diseño de John Cooley.

Este ejemplo se deriva directamente del concurso de diseño de John Cooley en SNUG'95 (reunión del grupo de usuarios de Synopsys). El concurso estaba destinado a oponerse a los diseñadores de VHDL y Verilog en el mismo problema de diseño. Lo que John tenía en mente era probablemente determinar qué idioma era el más eficiente. Los resultados fueron que 8 de los 9 diseñadores de Verilog lograron completar el concurso de diseño, pero ninguno de los 5 diseñadores de VHDL pudo. Con suerte, usando el método propuesto, haremos un trabajo mucho mejor.

Presupuesto

Nuestro objetivo es diseñar en VHDL (entidad y arquitectura) sintetizables, un contador síncrono up-by-down, down-by-5, cargable, módulo 512, con salida de acarreo, salida de préstamo y salida de paridad. El contador es un contador sin signo de 9 bits, por lo que oscila entre 0 y 511. La especificación de la interfaz del contador se muestra en la siguiente tabla:

Nombre Ancho de bits Dirección Descripción
RELOJ 1 Entrada Reloj maestro; El contador está sincronizado en el flanco ascendente de CLOCK.
DI 9 Entrada Bus de entrada de datos; el contador está cargado con DI cuando UP y DOWN son bajos
ARRIBA 1 Entrada Comando de conteo hasta por 3; cuando UP es alto y DOWN es bajo, el contador se incrementa en 3, envolviendo su valor máximo (511)
ABAJO 1 Entrada Comando de conteo de por 5; cuando ABAJO es alto y ARRIBA es bajo, el contador disminuye en 5, envolviendo su valor mínimo (0)
CO 1 Salida Llevar a cabo la señal; alto solo cuando se cuenta más allá del valor máximo (511) y, por lo tanto, se ajusta
BO 1 Salida Pedir prestado la señal; alto solo cuando se cuenta por debajo del valor mínimo (0) y, por lo tanto, se ajusta
HACER 9 Salida Bus de salida; el valor actual del contador; cuando ARRIBA y ABAJO son altos, el contador conserva su valor
correos 1 Salida Paridad de señal de salida; alto cuando el valor actual del contador contiene un número par de 1

Cuando se cuenta más allá de su valor máximo o cuando se cuenta por debajo de su valor mínimo, el contador se ajusta:

Contador de valor actual ARRIBA ABAJO Contador siguiente valor Siguiente CO Siguiente BO Siguiente PO
X 00 DI 0 0 paridad (DI)
X 11 X 0 0 paridad (x)
0 ≤ x ≤ 508 10 x + 3 0 0 paridad (x + 3)
509 10 0 1 0 1
510 10 1 1 0 0
511 10 2 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 paridad (x − 5)
4 01 511 0 1 0
3 01 510 0 1 1
2 01 509 0 1 1
1 01 508 0 1 0
0 01 507 0 1 1

Diagrama de bloques

Basándonos en estas especificaciones podemos comenzar a diseñar un diagrama de bloques. Primero representemos la interfaz:

La interfaz externa

Nuestro circuito tiene 4 entradas (incluido el reloj) y 4 salidas. El siguiente paso consiste en decidir cuántos registros y bloques combinatorios usaremos y cuáles serán sus funciones. Para este ejemplo simple, dedicaremos un bloque combinatorio al cálculo del siguiente valor del contador, la ejecución y el préstamo. Se usará otro bloque combinatorio para calcular el siguiente valor de la paridad hacia afuera. Los valores actuales del contador, la ejecución y el préstamo se almacenarán en un registro, mientras que el valor actual de la paridad se almacenará en un registro separado. El resultado se muestra en la siguiente figura:

Dos bloques combinatorios y dos registros.

La comprobación del cumplimiento del diagrama de bloques con nuestras 10 reglas de diseño se realiza rápidamente:

  1. Nuestra interfaz externa está representada adecuadamente por el gran rectángulo circundante.
  2. Nuestros 2 bloques combinatorios (redondos) y nuestros 2 registros (cuadrados) están claramente separados.
  3. Usamos solo registros activados por flanco ascendente.
  4. Utilizamos un solo reloj.
  5. Tenemos 4 flechas internas (señales), 4 flechas de entrada (puertos de entrada) y 4 flechas de salida (puertos de salida).
  6. Ninguna de nuestras flechas tiene varios orígenes. Tres tienen varios destinos ( clock , ncnt y do ).
  7. Ninguna de nuestras 4 flechas de entrada es una salida de nuestros bloques internos.
  8. Tres de nuestras flechas de salida tienen exactamente un origen y un destino. Pero do con 2 destinos: el exterior y uno de nuestros bloques combinatorios. Esto infringe la regla número 8 y debe solucionarse insertando un nuevo bloque combinatorio si queremos cumplir con las versiones VHDL anteriores a 2008:

Un bloque combinatorio extra.

  1. Tenemos ahora exactamente 5 señales internas ( cnt , nco , nbo , ncnt y npo ).
  2. Solo hay un ciclo en el diagrama, formado por cnt y ncnt . Hay un bloque cuadrado en el ciclo.

Codificación en versiones VHDL anteriores a 2008

Traducir nuestro diagrama de bloques en VHDL es sencillo. El valor actual del contador varía de 0 a 511, por lo que utilizaremos una señal de bit_vector 9 bits para representarlo. La única sutileza proviene de la necesidad de realizar operaciones a nivel de bits (como calcular la paridad) y aritméticas en los mismos datos. El estándar numeric_bit paquete de biblioteca ieee resuelve esto: se declara un unsigned tipo con exactamente la misma declaración que bit_vector y sobrecarga los operadores aritméticos de tal manera que ellos toman cualquier mezcla de unsigned y números enteros. Para calcular la ejecución y el préstamo usaremos un valor temporal unsigned 10 bits.

Las declaraciones de la biblioteca y la entidad:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

El esqueleto de la arquitectura es:

architecture arc1 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
    <...processes...>
end architecture arc1;

Cada uno de nuestros 5 bloques está modelado como un proceso. Los procesos síncronos correspondientes a nuestros dos registros son muy fáciles de codificar. Simplemente utilizamos el patrón propuesto en el ejemplo de codificación . El registro que almacena la bandera de paridad de salida, por ejemplo, está codificado:

  poreg: process(clock)
  begin
    if rising_edge(clock) then
      po <= npo;
    end if;
  end process poreg;

y el otro registro que almacena co , bo y cnt :

  cobocntreg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
    end if;
  end process cobocntreg;

El proceso combinatorio de cambio de nombre también es muy simple:

  rename: process(cnt)
  begin
    do <= (others => '0');
    do <= bit_vector(cnt);
  end process rename;

El cálculo de paridad puede usar una variable y un bucle simple:

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

El último proceso combinatorio es el más complejo de todos, pero aplicar estrictamente el método de traducción propuesto también lo hace fácil:

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;

Tenga en cuenta que los dos procesos síncronos también podrían fusionarse y que uno de nuestros procesos combinatorios se puede simplificar en una simple asignación de señal concurrente. El código completo, con la biblioteca y las declaraciones de paquetes, y con las simplificaciones propuestas es el siguiente:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc2 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
  reg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      po  <= npo;
      cnt <= ncnt;
    end if;
  end process reg;

  do <= bit_vector(cnt);

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;
end architecture arc2;

Yendo un poco más lejos.

El método propuesto es simple y seguro, pero se basa en varias restricciones que se pueden relajar.

Salta el dibujo del diagrama de bloques

Los diseñadores experimentados pueden omitir el dibujo de un diagrama de bloques para diseños simples. Pero aún piensan primero en el hardware. Dibujan en su cabeza en lugar de en una hoja de papel, pero de alguna manera continúan dibujando.

Utilizar reinicios asíncronos.

Hay circunstancias en las que los reinicios asíncronos (o conjuntos) pueden mejorar la calidad de un diseño. El método propuesto admite solo restablecimientos sincrónicos (es decir, los restablecimientos que se tienen en cuenta en los flancos ascendentes del reloj):

  process(clock)
  begin
    if rising_edge(clock) then
      if reset = '1' then
        o <= reset_value_for_o;
      else
        o <= i;
      end if;
    end if;
  end process;

La versión con reinicio asíncrono modifica nuestra plantilla agregando la señal de reinicio en la lista de sensibilidad y otorgándole la más alta prioridad:

  process(clock, reset)
  begin
    if reset = '1' then
      o <= reset_value_for_o;
    elsif rising_edge(clock) then
      o <= i;
    end if;
  end process;

Fusionar varios procesos simples.

Ya lo usamos en la versión final de nuestro ejemplo. La fusión de varios procesos síncronos, si todos tienen el mismo reloj, es trivial. La fusión de varios procesos combinatorios en uno también es trivial y es solo una simple reorganización del diagrama de bloques.

También podemos fusionar algunos procesos combinatorios con procesos síncronos. Pero para hacer esto debemos volver a nuestro diagrama de bloques y agregar una regla undécima:

  1. Agrupe varios bloques redondos y al menos un bloque cuadrado dibujando un recinto alrededor de ellos. También encierra las flechas que pueden ser. No permita que una flecha cruce el límite del envolvente si no sale o va desde / hacia fuera del envolvente. Una vez hecho esto, mire todas las flechas de salida del gabinete. Si cualquiera de ellos proviene de un bloque redondo del gabinete o es también una entrada del gabinete, no podemos combinar estos procesos en un proceso síncrono. Si no podemos.

En nuestro ejemplo de contador, por ejemplo, no podríamos agrupar los dos procesos en el gabinete rojo de la siguiente figura:

Procesos que no pueden ser fusionados.

Porque ncnt es una salida del gabinete y su origen es un bloque redondo (combinatorio). Pero podríamos agrupar:

Procesos que pueden fusionarse.

La señal interna npo se volvería inútil y el proceso resultante sería:

  poreg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process poreg;

que también podría fusionarse con el otro proceso síncrono:

  reg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process reg;

La agrupación podría incluso ser:

Más agrupación

Conduciendo a la arquitectura mucho más simple:

architecture arc5 of cooley is
  signal cnt: unsigned(8 downto 0);
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
    variable tmp:  bit;
  begin
    if rising_edge(clock) then
      ncnt := '0' & cnt;
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po  <= not tmp;
      cnt <= ncnt(8 downto 0);
    end if;
  end process;

  do <= bit_vector(cnt);
end architecture arc5;

con dos procesos (la asignación de señal concurrente de do es una abreviatura para el proceso equivalente). La solución con un solo proceso se deja como ejercicio. Cuidado, plantea preguntas interesantes y sutiles.

Yendo aún más lejos

Los pestillos activados por nivel, la caída de los bordes del reloj, los relojes múltiples (y los resincronizadores entre dominios de reloj), los controladores múltiples para la misma señal, etc. no son malos. A veces son útiles. Pero aprender a usarlos y cómo evitar las dificultades asociadas va mucho más allá de esta breve introducción al diseño de hardware digital con VHDL.

Codificación en VHDL 2008

VHDL 2008 introdujo varias modificaciones que podemos usar para simplificar aún más nuestro código. En este ejemplo podemos beneficiarnos de 2 modificaciones:

  • Los puertos de salida se pueden leer, ya no necesitamos la señal cnt ,
  • El operador xor unario se puede usar para calcular la paridad.

El código VHDL 2008 podría ser:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc6 of cooley is
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
  begin
    if rising_edge(clock) then
      ncnt := unsigned('0' & do);
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      po <= not (xor ncnt(8 downto 0));
      do <= bit_vector(ncnt(8 downto 0));
    end if;
  end process;
end architecture arc6;


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow