Como ya sabéis todos, un sistema digital trabaja internamente con números en binario. Pero para los que vayáis más rezagados haremos un pequeño repaso.

En un sistema como el que usamos habitualmente de base 10, cada dígito tiene un valor que depende de la posición que ocupa dentro del número. Por ejemplo el 745 sería 7*100+4*10+5*1. Si os dais cuenta el 2 ocupa la posición 2 y se multiplica por la base elevada a la posición es decir 10^2. El 4 se multiplica por la base elevada al valor que ocupa 10^1 y el 5 se multiplica por 10^0=1.

Si en vez de diez dígitos tenemos dos, estamos en un sistema binario, lo que tendríamos es únicamente el 1 y el 0 para representar números. La idea es la misma, multiplicar 1 o 0 por la base elevada a la posición que ocupa. De esta forma por ejemplo el número 11001 tendría el equivalente decimal:

1*2^4+1*2^3+0*2^2+0*2^1+1*2^0 = 16+8+1 = 25.

Ahora viene lo realmente interesante. Qué pasa si tenemos ese 11001 y queremos mostrar en una pantalla o un display 7 segmentos el 25. Tendríamos que convertir el valor binario a un 2 y un 5. Para esto se suele utilizar código BCD.

Código BCD

El código BCD representa números en base 10 utilizando solo 1 y 0. La idea es representar cada dígito decimal utilizando 4 bits. De esta forma el 25 anterior que en binario era 11001, quedaría como dos dígitos BCD de cuatro bits, el primero representando un dos y el segundo un 5, es decir 0010 0101.

Esto se puede hacer para cualquier número decimal, por ejemplo el 745 en decimal del que habíamos hablado antes quedaría 0111 0100 0101.

Convertir números binarios a decimal para poder ser mostrado es una tarea habitual y en VHDL hay diversas formas de hacerla. En este artículo vamos a mostrar tres formas diferentes, las tres producen el mismo resultado pero utilizando diferentes circuitos, lo cual nos servirá para aprender un par de cosas sobre la relación entre área y velocidad de un circuito digital.

Para ello vamos a trabajar con esta entidad. En ella podemos ver que tenemos una entrada de 8 bits llamada binary, es decir representaremos números binarios entre 0 y 255. Luego tenemos una señal de start que se utiliza para comenzar la traducción y tres salidas, una por cada dígito decimal.

El resultado del circuito será que en la salida hundred obtendremos el dígito correspondiente a las centenas, en ten el de las decenas y en one el de las unidades.

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

entity binary2bcd is
  port(
    clk    : in std_logic;
    reset  : in std_logic; 
    start  : in std_logic; 
    binary  : in std_logic_vector(7 downto 0);
    hundred : out integer range 0 to 9;
    ten     : out integer range 0 to 9;
    one     : out integer range 0 to 9);
end entity;

El test que vamos a utilizar para los tres casos es el siguiente:

library IEEE;
use IEEE.std_logic_1164.all;

entity test is
end entity;

architecture arch of test is
  signal clk     : std_logic:='0';
  signal start   : std_logic:='1';
  signal reset   : std_logic;
  signal binary  : std_logic_vector(7 downto 0);
  signal hundred : integer range 0 to 9;
  signal ten     : integer range 0 to 9;
  signal one     : integer range 0 to 9;

begin

clk <= not(clk) after 10 ns;
reset <= '1', '0' after 10 ns;
binary <= "00000001", "00011001" after 20 ns;

bin0 : entity work.binary2bcd(funcional) port map (clk, reset, start, binary, hundred, ten, one);

end architecture;

Convertir de binario a decimal usando una memoria ROM

La forma más sencilla de convertir un dato en binario a su equivalente decimal es utilizando una memoria ROM donde almacenamos los pares de valores.

En nuestro caso la memoria contendrá 256 posiciones. Lo mejor para generar esto es utilizar algún programa que genere el contenido, ya que si para 256 posiciones es tedioso, para valores de 16 o 32 bits es inviable.

El código de la ROM quedaría de la siguiente manera. Hemos omitido casi todos los valores por simplicidad.

architecture funcional of binary2bcd is
type rom_t is array (0 to 255) of std_logic_vector(11 downto 0);
signal rom : rom_t := (
  0 => B"0000_0000_0000",
  1 => B"0000_0000_0001",
  2 => B"0000_0000_0010",

  25 => B"0000_0010_0101",
  
  119 => B"0001_0001_1001",

  255 => B"0010_0101_0101",
  others => B"0000_0000_0000");

signal rom_out : std_logic_vector(11 downto 0);
begin
  process(binary)
  begin
    rom_out <= rom(to_integer(unsigned(binary)));
  end process;
  
  hundred <= to_integer(unsigned(rom_out(11 downto 8)));
  ten     <= to_integer(unsigned(rom_out(7 downto 4)));
  one     <= to_integer(unsigned(rom_out(3 downto 0)));
 
end architecture;

Conversor de binario a decimal con lógica combinacional

Hacer la conversión entre binario y los dígitos decimales mediante un algoritmo es otra alternativa que tenemos. Hay varias posibilidades, en stackoverflow varias formas de hacerlo. En este caso vamos a utilizar una muy sencilla y poco eficiente, pero que nos va a servir para explicar un concepto interesante, la reutilización de operadores.

El código es muy simple, consta de un solo proceso combinacional que toma el valor de 8 bits y va haciendo comparaciones. Si es mayor de 199 entonces las centenas son 2 y le resta 200, luego busca las decenas y lo último que queda son las unidades. Utiliza variables y se ejecuta en un solo ciclo.

architecture funcional of binary2bcd is
begin
  process(binary)
    variable binary_int : integer range 0 to 255;
    variable hundred_int : integer range 0 to 9;
    variable ten_int : integer range 0 to 9;
    variable one_int : integer range 0 to 9;
  begin
    hundred <= 0;
    ten     <= 0;
    one     <= 0;
  
    binary_int := to_integer(unsigned(binary));
  
    if binary_int > 199 then
      hundred_int := 2;
      binary_int := binary_int - 200;
    elsif binary_int > 99 then
      hundred_int := 1;
      binary_int := binary_int - 100;
    else 
      hundred_int := 0;
    end if;
  
    if binary_int > 89 then
      ten_int := 9;
      binary_int := binary_int - 90;
    elsif binary_int > 79 then
      ten_int := 8;
      binary_int := binary_int - 80; 
    elsif binary_int > 69 then
      ten_int := 7;
      binary_int := binary_int - 70;
    elsif binary_int > 59 then
      ten_int := 6;
      binary_int := binary_int - 60;
    elsif binary_int > 49 then
      ten_int := 5;
      binary_int := binary_int - 50;
    elsif binary_int > 39 then
      ten_int := 4;
      binary_int := binary_int - 40;
    elsif binary_int > 29 then
      ten_int := 3;
      binary_int := binary_int - 30;
    elsif binary_int > 19 then
      ten_int := 2;
      binary_int := binary_int - 20;
    elsif binary_int > 9 then
      ten_int := 1;
      binary_int := binary_int - 10;  
    else
      ten_int := 0;
    end if;
  
    hundred <= hundred_int;
    ten     <= ten_int;
    one     <= binary_int;
  end process;
end architecture;
traductor binario a decimal

El problema de esta implementación, que probablemente sería la que haría cualquiera que venga directamente de algún lenguaje de programación, es que para generar una conversión debe hacer en el peor de los casos 12 comparaciones y 10 restas encadenadas. Es decir necesitamos 22 módulos aritméticos, conectados todos uno detrás de otro, por lo que el circuito resultante va a ser muy grande y lento. Esto se puede ver muy bien en el resultado de Vivado.

Esto solo funcionará para aplicaciones muy sencillas y que no necesiten trabajar a una frecuencia elevada. Si necesitamos un conversor rápido y eficiente, lo recomendable es usar una memoria ROM o si no se dispone de ella, convertir esta implementación combinacional en una implementación multiciclo.

Convertidor de binario a decimal multiciclo

En este caso lo que pretendemos es separar las unidades funcionales (comparador y restador) que se necesitan para ejecutar el algoritmo, de la unidad de control, que le indica qué operaciones realizar en cada paso. En esta implementación el resultado tardará varios ciclos de reloj en generarse, pero reduciremos el consumo de área de 22 unidades a 2 y el camino critico en un factor de 10. Esto es así porque la señal eléctrica solo deberá propagarse por un restador y un comparador en lugar de los 22 del caso anterior.

Podríamos haber hecho la misma implementación incluso utilizando un solo restador, haciendo las comparaciones con él. El algoritmo habría tardado el doble de ciclos de reloj pero trabajaría al doble de frecuencia. Si te interesa como hacerlo y no sabes como, pregunta en comentarios.

Este es el código VHDL resultante. Como veis hay un proceso que compara y otro que resta. Esas son nuestras unidades funcionales que forman nuestro camino de datos o datapath. La máquina de estados de dos procesos genera las señales de control que hacen que cada ciclo de reloj esas unidades hagan las operaciones necesarias para ejecutar el algoritmo.

La implementación puede realizarse con menos estados, ya que llevando un contador todos los estados de las decenas pueden agruparse en uno solo, pero hemos preferido dejarlo así para que se vea con claridad el algoritmo original y su conversión a una implementación multiciclo.

architecture funcional of binary2bcd is
  signal a1,a2 : integer range 0 to 255;
  signal b1,b2 : integer range 0 to 255;
  signal s2 : integer range 0 to 255;
  signal comp  : std_logic;
  
  -- Registros utilizados
  type estado_t is (IDLE, DOSCIEN, CIEN, NOVENTA, OCHENTA, SETENTA, SESENTA,CINCUENTA,CUARENTA,TREINTA,VEINTE, DIEZ, SALIDA);
  signal estado_reg, estado_reg_siguiente : estado_t;
  
  signal binary_reg, binary_reg_siguiente   : integer range 0 to 255;
  signal hundred_reg, hundred_reg_siguiente : integer range 0 to 9;
  signal ten_reg, ten_reg_siguiente     : integer range 0 to 9;
  signal one_reg, one_reg_siguiente     : integer range 0 to 9;
   
begin

  --Comparar
  comp <= '1' when a1 > b1 else '0';
 
  --Restar
  process(a2,b2,s2)
  begin
    s2 <= a2-b2;
  end process;
  
  process(binary, comp, s2,estado_reg, binary_reg, start, hundred_reg, ten_reg, one_reg)
  begin
    --Registros conservan valor si no se asigna ota cosa
    binary_reg_siguiente  <= binary_reg;
    hundred_reg_siguiente <= hundred_reg;
    ten_reg_siguiente     <= ten_reg;
    one_reg_siguiente     <= one_reg;
    estado_reg_siguiente  <= estado_reg;
    --Salidas
    one     <= one_reg; 
    ten     <= ten_reg;
    hundred <= hundred_reg;
  
    --Entradas por defecto al comparador y restador
    a1 <= binary_reg;
    a2 <= binary_reg;
    b1 <= 0;
    b2 <= 0;
  
    case estado_reg is 
      when IDLE =>
        if(start='1') then 
          binary_reg_siguiente  <= to_integer(unsigned(binary));
          estado_reg_siguiente  <= DOSCIEN;
        end if;    
      when DOSCIEN =>
        --Comparamos con 199
        b1 <= 199;    
        --Si es mayor de 199, el primer digito es dos, restamos 200 al registro y vamos por las centenas
        if comp ='1' then
          hundred_reg_siguiente <= 2;
          b2 <= 200; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= NOVENTA;
        else
          estado_reg_siguiente <= CIEN;
        end if;
       
      when CIEN =>
        --Comparamos con 99
        b1 <= 99;     
        --Si es mayor de 99, el primer digito es uno, restamos 200 al registro y vamos por las decenas
        --si no el primer digito es cero y vamos por las decenas
        if comp ='1' then
          hundred_reg_siguiente <= 1;
          b2 <= 100; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= NOVENTA;
        else
          hundred_reg_siguiente <= 0;
          estado_reg_siguiente  <= NOVENTA;
        end if;      
      when NOVENTA =>       
        b1 <= 89;
        if comp ='1' then
          ten_reg_siguiente <= 9;
          b2 <= 90; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= OCHENTA;
        end if;
      when OCHENTA => 
        b1 <= 79;
        if comp ='1' then
          ten_reg_siguiente <= 8;
          b2 <= 80; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= SETENTA;
        end if;   
      when SETENTA => 
        b1 <= 69;
        if comp ='1' then
          ten_reg_siguiente <= 7;
          b2 <= 70; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= SESENTA;
        end if;    
      when SESENTA => 
        b1 <= 59;
        if comp ='1' then
          ten_reg_siguiente <= 6;
          b2 <= 60; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= CINCUENTA;
        end if;
      when CINCUENTA => 
        b1 <= 49;
        if comp ='1' then
          ten_reg_siguiente <= 5;
          b2 <= 50; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= CUARENTA;
        end if;
      when CUARENTA => 
        b1 <= 39;
        if comp ='1' then
          ten_reg_siguiente <= 4;
          b2 <= 40; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= TREINTA;
        end if;
      when TREINTA => 
        b1 <= 29;
        if comp ='1' then
          ten_reg_siguiente <= 3;
          b2 <= 30; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= VEINTE;
        end if;
      when VEINTE => 
        b1 <= 19;
        if comp ='1' then
          ten_reg_siguiente <= 2;
          b2 <= 20; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= DIEZ;
        else
          estado_reg_siguiente <= DIEZ;
        end if;
      when DIEZ => 
        b1 <= 9;
        if comp ='1' then
          ten_reg_siguiente <= 1;
          b2 <= 10; 
          binary_reg_siguiente  <= s2;
          estado_reg_siguiente  <= SALIDA;
        else
          estado_reg_siguiente  <= SALIDA;
        end if;  
      when SALIDA =>
        one_reg_siguiente   <= binary_reg;
   
        
      when others =>
        null;
    end case;
  end process;
 
  process(clk, reset)
  begin
    if(reset='1') then
      estado_reg  <= IDLE;
      binary_reg  <= 0; 
      hundred_reg <= 0;
      ten_reg     <= 0;
      one_reg     <= 0;
    
    elsif(rising_edge(clk)) then
      estado_reg  <= estado_reg_siguiente;
      binary_reg  <= binary_reg_siguiente; 
      hundred_reg <= hundred_reg_siguiente;
      ten_reg     <= ten_reg_siguiente;
      one_reg     <= one_reg_siguiente;
    end if; 
  
  end process;
end architecture;

Y esta es la implementación que nos da Vivado de esta versión. Donde podéis ver solo dos circulos, que son el restador y el comparador y registros que almacenan los valores intermedios. A partir de estas implementaciones se puede jugar más y realizar por ejemplo una versión segmentada, pero eso lo dejaremos para otro artículo.

traductor binario decimal multiciclo