Luces de Navidad controladas por FPGA 
Como cada año por estas fechas hago una revisita al concepto de las luces para el belén usando diferentes tecnologías. En este caso y como no podía ser de otro modo, intentaremos usar una FPGA para implementar este efecto.

Aspectos funcionales

Partimos de 5 leds de alta luminosidad (los mismos de años anteriores) y necesitamos que parpadeen de forma aleatoria, como si simularan el aspecto de una porción del cielo nocturno. La secuencia de parpadeo debería ser lo más aleatoria posible y lo ideal es que la probabilidad de parpadeo sea controlable para simular un ciclo día-noche.

Diseño

Para generar una secuencia de números pseudoaleatorios la forma más sencilla es utilizar un LFSR con la cantidad suficiente de bits como para dar la percepción de que se trata de un generador de números realmente aleatorios. Si partimos de un LFSR de 10 bits, para que sea maximal (que su secuencia numérica sea lo más larga posible antes de “dar la vuelta”) debemos implementar el siguiente polinomio de realimentación:
$$x^{10} + x^7 + 1$$
Este polinomio de realimentación garantiza una secuencia maximal de $2^{n} - 1$ valores, siendo en este caso $n=10$. La secuencia no es de $2^{n}$ valores debido a que el valor 0 (todos los bits a cero) no está incluido en la secuencia.

("=1" denota la operación XOR en notación IEC) La ruta de datos que se va a usar es la siguiente:

El funcionamiento interno sería el siguiente.

1. Se inicializa el LFSR (se le mete un valor que incluya, al menos un bit a 1).
2. Hacer 5 veces (una vez por cada uno de los 5 leds).
2.1. Se itera el LFSR para que genere el siguiente numero pseudoaleatorio.
2.2. Se empuja el bit resultante de la comparación entre el valor del LFSR (valor A) y una constante (valor B) en el registro de desplazamiento.
3. Se carga en el latch de salida el valor que hay en el registro de desplazamiento.
4. Se espera 1 segundo.
5. Saltar al paso 2.

Tanto para el conteo de la carga de los 5 bits en el registro de desplazamiento como para el conteo del tiempo de espera de 1 segundo se utiliza un contador de 32 bits de dos límites: uno de los límites se fija a 5 (para contar los bits) y otro de los límites se fija en 32000000 para contar 1 segundo (el reloj de la FPGA va a 32MHz).

A partir de este algoritmo se puede diseñar la siguiente máquina de estados:



Salidas de la FSM:
LFSR.RST = Reset del LFSR.
LFSR.ENA = Enable del LFSR.
SR.ENA = Enable del registro de desplazamiento.
LATCH.ENA = Enable del latch de salida.
CNT.RST = Reset del contador.
CNT.ENA = Enable del contador.

Entradas de la FSM:
CNT.T1 = a 1 cuando el contador llega a 5.
CNT.T2 = a 1 cuando el contador llega a 32000000.

Como se puede ver, se trata de un diseño totalmente síncrono, basado en enables y en el que se evita el uso de “gated clocks”, por lo tanto, perfectamente sintetizable en cualquier FPGA.

Por ahora la probabilidad de parpadeo está fijada por hardware como una constante (el valor de B en el diagrama, que no es modificable), sin embargo el diseño queda preparado para que en una siguiente versión se pueda obtener dicha constante de algún parámetro físico (ADC, reloj de tiempo real, etc.)

Implementación

La implementacion de todos los módulos se ha realizado siguiendo siempre un modelo RTL. A continuación se lista el codigo fuente de la unidad de más alto nivel (que se ha denominado “ChristmasLights”) y que engloba todos los submódulos (LFSR, comparador, registro de desplazamiento, latch, contador y FSM).

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ChristmasLights is
	generic (
		NLeds : integer := 8;
		NWaitClocks : integer := 20;    -- for the simulation 20 clocks between lights change, but in real hardware change this value according FPGA clock
		Probability : integer := 512    -- 0 = all lights on, 1023 = all lights off
	);
	port (
		Clk   : in std_logic;
		Reset : in std_logic;
		Led   : out std_logic_vector((NLeds - 1) downto 0)
	);
end ChristmasLights;

architecture Architecture1 of ChristmasLights is
	component LFSR10 is
		port (
			Reset  : in std_logic;
			Enable : in std_logic;
			Clk    : in std_logic;
			Data   : out std_logic_vector(9 downto 0)
		);
	end component;
	component Comparator is
		generic (
			NBits : integer := 4
		);
		port (
			A           : in std_logic_vector((NBits - 1) downto 0);
			B           : in std_logic_vector((NBits - 1) downto 0);
			AGreatThanB : out std_logic;
			ALessThanB  : out std_logic;
			AEqualB     : out std_logic
		);
	end component;
	component ShiftRegister is
		generic (
			NBits : integer := 8
		);
		port (
			Enable         : in std_logic;
			Clk            : in std_logic;
			SerialInput    : in std_logic;
			ParallelOutput : out std_logic_vector((NBits - 1) downto 0)
		);
	end component;
	component Latch is
		generic (
			NBits : integer := 8
		);
		port (
			Enable  : in std_logic;
			Clk     : in std_logic;
			DataIn  : in std_logic_vector((NBits - 1) downto 0);
			DataOut : out std_logic_vector((NBits - 1) downto 0)
		);
	end component;
	component TwoLimitCounter is
		generic (
			NBits : integer := 4;
			Limit1 : integer := 3;
			Limit2 : integer := 2
		);
		port (
			Reset       : in std_logic;
			Enable      : in std_logic;
			Clock       : in std_logic;
			Terminated1 : out std_logic;
			Terminated2 : out std_logic
		);
	end component;
	signal LfsrEnable : std_logic;
	signal LfsrReset : std_logic;
	signal LfsrData : std_logic_vector(9 downto 0);
	signal CompOutput : std_logic;
	signal SREnable : std_logic;
	signal SRData : std_logic_vector((NLeds - 1) downto 0);
	signal LatEnable : std_logic;
	signal CntReset : std_logic;
	signal CntEnable : std_logic;
	signal CntBitsOut : std_logic;
	signal CntTimeOut : std_logic;
	signal FSMDBus : std_logic_vector(2 downto 0);
	signal FSMQBus : std_logic_vector(2 downto 0);
begin
	-- LFSR
	Lfsr : LFSR10 port map (
		Clk => Clk,
		Enable => LfsrEnable,
		Reset => LfsrReset,
		Data => LfsrData
	);
	-- comparator
	Comp : Comparator generic map (
		NBits => 10
	)
	port map (
		A => LfsrData,
		B => std_logic_vector(to_unsigned(Probability, 10)),
		AGreatThanB => CompOutput
	);
	-- shift register
	SR : ShiftRegister generic map (
		NBits => NLeds
	)
	port map (
		Enable => SREnable,
		Clk => Clk,
		SerialInput => CompOutput,
		ParallelOutput => SRData
	);
	-- output latch
	Lat : Latch generic map (
		NBits => NLeds
	)
	port map (
		Enable => LatEnable,
		Clk => Clk,
		DataIn => SRData,
		DataOut => Led
	);
	-- two limit counter
	Cnt : TwoLimitCounter generic map (
		NBits => 32,
		Limit1 => NLeds,
		Limit2 => NWaitClocks
	)
	port map (
		Reset => CntReset,
		Enable => CntEnable,
		Clock => Clk,
		Terminated1 => CntBitsOut,
		Terminated2 => CntTimeOut
	);

	-- FSM D FFs
	process (Clk, Reset)
	begin
		if (Clk'event and (Clk = '1')) then
			if (Reset = '1') then
				FSMQBus <= (others => '0');
			else
				FSMQBus <= FSMDBus;
			end if;
		end if;
	end process;
	-- FSM next state logic
	FSMDBus <= "000" when (Reset = '1') else
	           "001" when (FSMQBus = "000") else
	           "010" when (FSMQBus = "001") or (FSMQBus = "011") else
	           "011" when (FSMQBus = "010") and (CntBitsOut = '0') else
	           "100" when (FSMQBus = "010") and (CntBitsOut = '1') else
	           "101" when (FSMQBus = "100") or ((FSMQBus = "101") and (CntTimeOut = '0')) else
	           "001" when (FSMQBus = "101") and (CntTimeOut = '1') else
	           "000";
	-- FSM output logic
	LfsrReset <= '1' when (FSMQBus = "000") else
	             '0';
	CntReset <= '1' when (FSMQBus = "001") or (FSMQBus = "100") else
	            '0';
	CntEnable <= '1' when (FSMQBus = "010") or (FSMQBus = "101") else
	             '0';
	LfsrEnable <= '1' when (FSMQBus = "010") else
	              '0';
	SREnable <= '1' when (FSMQBus = "011") else
	            '0';
	LatEnable <= '1' when (FSMQBus = "100") else
	             '0';
end Architecture1;

Vídeo con el código VHDL implementado sobre la FPGA Spartan3E de Xilinx.



Todo el codigo puede descargarse de la sección soft. Feliz programación y feliz Navidad :-).

[ añadir comentario ] ( 1555 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2354 )
Implementación de un receptor serie asíncrono sobre FPGA 
Un receptor serie asíncrono es un módulo de hardware que recibe datos serie de forma asíncrona: es el elemento receptor de una UART. A lo largo de este post se aborda paso a paso el diseño digital y la implementación de un módulo receptor serie asíncrono muy sencillo en VHDL, con un bit de start, un bit de stop y 8 bits de datos, así como su posterior implementación en una FPGA.

Especificaciones del receptor

La idea es crear un módulo muy sencillo que sea capaz de recibir datos en formato 8N1, es decir, 1 bit de start, 8 bits de datos, sin paridad y 1 bit de stop. Se asume el orden de envío estándar LSB --> MSB (primero el bit 0 y por último el bit 7) y una velocidad de 9600 bits por segundo (bps). Además de estas especificaciones "funcionales" se va a intentar que el circuito resultante sea totalmente síncrono (sin gated clocks, que el reloj sea el mismo para todos los sub módulos secuenciales del receptor). Este último requisito facilitará la implementación del módulo sobre cualquier FPGA sin limitación en la cantidad de líneas de reloj y de paso servirá para entender las alternativas al uso de gated clocks en el diseño de circuitos digitales.

Bloques del receptor

Los diferentes bloques que componen el receptor asíncrono son los siguientes:
- Un registro de desplazamiento: donde se irán empujando los bits a medida que lleguen.
- Un latch o registro de salida: donde se realizará una carga paralela desde el registro de desplazamiento del dato recibido una vez se compruebe que la recepción ha sido correcta.
- Dos contadores independientes para realizar la división de frecuencia y el conteo de los bits que van llegando, respectivamente.
- Una máquina de estados (FSM, Finite-State Machine) encargada del control de los contadores, del registro de desplazamiento y del registro de salida.



Algoritmo

De forma resumida el funcionamiento es el siguiente:

1. En el estado inicial, la FSM espera a que el pin RX valga 0.

2. En el instante en que RX pase a valor 0 la FSM inicializa un contador que tarda el equivalente en tiempo a 1.5 bits a 9600 bps en alcanzar el límite de cuenta, en el momento que este contador alcanza su límite se pasa al siguiente estado.

3. Se inicializa un contador que va a contar la cantidad de bits (8 + 1 bit de stop = 9).

4. Se empuja el valor de RX en el registro de desplazamiento, se reinicia otro contador que tiene como límite el equivalente en tiempo a 1 bit a 9600 bps y se incrementa el contador del número de bits

5. Si el contador de bits vale 9, saltamos al paso 8.

6. Esperamos a que el contador de tiempo para 1 bit llegue al límite

7. Saltamos al paso 4.

8. Si el bit de stop vale 1 cargamos el buffer de salida y hacemos DATAOUT = 1 para indicar que en el buffer de salida hay datos válidos, en caso contrario no se carga de buffer de salida.

9. Saltamos al paso 1.



Evitar el uso de “gated clocks”

En el anterior proyecto en el que se implementó un multiplicador en VHDL usando el algoritmo de Booth, el entorno de desarrollo ISE Design Suite de Xilinx mostraba un warning en el que se indicaba que había que evitar el uso de “gated clocks”.

Un “gated clock” es una línea de reloj que no se corresponde con la salida de un oscilador o un PLL sino que es la salida de una función combinacional o secuencial en un circuito. En el caso del multiplicador implementado en el anterior post, sí se utilizan gated clocks: Por ejemplo, cuando se quiere cargar un registro, la salida del FSM ataca directamente a la entrada de reloj de los biestables de ese registro. Esta forma de trabajar, a priori inocua, tiene varias implicaciones que en aquel proyecto no se tuvieron en cuenta:

1. Como bien me comentó mi colega Armando Sánchez Peña, las líneas de reloj son bienes muy preciados dentro de las FPGAs: su enrutamiento está muy cuidado para garantizar retardos equivalentes independientemente de la parte del chip donde lleguen y debido a ello no podemos disponer de todas las que queramos (aunque tengamos una FPGA con miles de unidades lógicas igual sólo disponemos de unas pocas decenas de líneas de reloj).

2. Los cambios de estado en los biestables de un FSM a veces no son todo lo limpios que uno desearía: Imaginemos que tenemos un FSM con tres estados (“00”, “01” y “11”), para el estado “01” tenemos una lógica de salida que genera un “1” en una entrada de reloj de un registro A y para el estado “11” tenemos una lógica de salida que genera un “1” en una entrada de reloj de otro registro B. Si el FSM está en el estado “00” y tiene que cambiar al estado “11”, es posible que los biestables basculen a velocidades ligeramente diferentes por lo que durante un breve intervalo de tiempo (picosegundos) se podría producir el estado “01” (si el biestable menos significativo es más rápido basculando que el más significativo) ¿Que sucederá durante este picosegundo? Pues que probablemente se produzca una carga espúrea y no deseada del registro A. Estos problemas pueden minimizarse utilizando codificación gray (estados adyacentes se codifican de tal manera que solo cambia de valor un bit) o codificación one-hot (un biestable por estado: gastamos más biestables pero la lógica de salida y de estado siguiente se simplifica por lo que a veces compensa). En posts futuros trataré de profundizará más en estos temas.

En multitud de foros sobre FPGAs y ASICs se comenta lo malo que es el uso de “gated clocks” sin embargo este post y otros ayudan mucho a aclarar este asunto. No todo es blanco o negro:

1. Para FPGAs hay que evitar el uso de gated clocks debido a la cantidad limitada de líneas de reloj de las que disponemos dentro del chip.

2. Para ASICs el uso de gated clocks mientras sea con cabeza (código gray, one-hot, etc.) no sólo es perfectamente válido, sino hasta aconsejable. Hay que tener en cuenta que una señal de reloj es una enorme fuente de consumo de corriente ya que cada vez que bascula la señal de reloj se producen micropicos de corriente debidos a las capacidades presentes en las entradas de reloj de los biestables a los que ataca. Un circuito con gated clocks consumirá menos corriente que su equivalente sin gated clocks.

Como obviamente, salvo casos excepcionales, lo normal es que dispongamos de una FPGA, no de un ASIC, lo lógico es intentar evitar el uso de gated clocks en nuestros diseños digitales. En este caso, como se puede ver en el diagrama de bloques anterior, esa ha sido la consigna que se ha seguido:

1. La misma señal de reloj para todos los módulos.

2. Sustituir los antiguos “gated clocks” por “enables” que permitan habilitar o deshabilitar módulos en un instante dado sin necesidad de enmascarar o tocar la señal de reloj.



Los enables en circuitos secuenciales se pueden implementar mediante lógica combinacional en los biestables o mediante señales CE (“Chip Enable”) que implementan muchos de los biestables presentes en las FPGAs que hay en el mercado. Para garantizar portabilidad en el código VHDL no se puede presuponer que los biestables de la FPGA vayan a tener entradas CE y dado que en este caso siempre se están usando biestables de tipo D, la opcion más lógica es la indicada en el documento FPGA Design Tips de Xilinx:



Esta es una forma sencilla de implementar un CE (“Chip Enable”) “a mano”. Cuando el multiplexor selecciona la entrada conectada a la salida Q, el biestable no cambia de estado por muchos ciclos de reloj que le lleguen. Además implementando un CE “a mano” de esta manera, nos aseguramos que el circuito resultante es sintetizable en cualquier FPGA independientemente de si ésta implementa entradas CE en sus biestables o no.

Máquina de estados

La máquina de estados resultante para nuestro módulo de recepción de la UART quedaría, utilizando la técnica de los “enables”, como sigue:



La máquina de estados es una versión “formal” del algoritmo descrito en párrafos anteriores. Como se puede apreciar el contador 1 se utiliza para controlar los tiempos entre los principales cambios de estado (cuando se detecta el bit de start y entre bit y bit de datos).

Contadores

El módulo receptor utiliza dos contadores, uno (contador 1) para contar el tiempo equivalente a 1.5 bits a 9600 bps y el tiempo equivalente a 1 bit a 9600 bps y otro contador (contador 2) para contar los bits que se van empujando en el registro de desplazamiento (9, los 8 de datos más el bit de stop). El segundo contador (contador 2) es trivial ya que cuenta hasta 9 mientras que para el contador 1 sí que es necesario realizar algunos cálculos previos. Consideremos una velocidad de 9600 bps:

$$9600\ \ bits/segundo = {1 \over 9600}\ \ segundos/bit$$

Teniendo en cuenta que, en el caso particular de la placa FPGA Papilio One, el reloj del sistema va a 32MHz tenemos que:

$$(32000000\ \ pulsos/segundo) \times \left({1 \over 9600}\ \ segundos/bit\right) = 3333.33\ \ pulsos/bit$$

Que, redondeando, nos da: 3333 pulsos a 32MHz por bit a 9600 bps. Multiplicando por 1.5 nos dará la cantidad de pulsos a 32MHz necesarios para contar 1.5 bits de tiempo:

$$3333.33 \times 1.5 = 5000\ pulsos\ a\ 32MHz\ por\ 1.5\ bit\ a\ 9600 bps$$

El Contador 1 tendrá, por tanto como límite de cuenta 1 el valor 3333 y como límite de cuenta 2 el valor 5000. En otras palabras, tras un reset en el contador 1, la salida TERM1 de dicho contador 1 se pondrá a “1” cuando pasen 3333 pulsos de reloj del sistema mientras que la salida TERM2 de ese mismo contador 1 se pondrá a “1” cuando pasen 5000 pulsos de reloj del sistema.

VHDL

Ambos contadores (1 y 2) son instancias separadas de un mismo módulo contador (en el caso del contador 2 se ignora la salida TERM2). Al ser tanto el registro de desplazamiento como el registro de salida simplemente arrays de biestables, se ha optado por implementar ambos submódulos dentro de la misma FSM.

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

entity UartReceiver is
    port (
        Rx        : in std_logic;
        Clock     : in std_logic;
        DataOut   : out std_logic_vector(7 downto 0);
        DataOutOk : out std_logic
    );
end UartReceiver;

architecture Architecture1 of UartReceiver is
    component TwoLimitCounter is
        generic (
            NBits : integer := 4;
            Limit1 : integer := 3;
            Limit2 : integer := 11
        );
        port (
            Reset       : in std_logic;
            Clock       : in std_logic;
            Enable      : in std_logic;
            Terminated1 : out std_logic;
            Terminated2 : out std_logic
        );
    end component;
    signal ShiftRegisterDBus : std_logic_vector(8 downto 0);  -- 8 bits + 1 bit de stop
    signal ShiftRegisterQBus : std_logic_vector(8 downto 0);
    signal ShiftRegisterEnable : std_logic;
    signal BufferDBus : std_logic_vector(7 downto 0);
    signal BufferQBus : std_logic_vector(7 downto 0);
    signal BufferEnable : std_logic;
    signal Counter1Reset : std_logic;
    signal Counter1Terminated1 : std_logic;
    signal Counter1Terminated2 : std_logic;
    signal Counter2Reset : std_logic;
    signal Counter2Enable : std_logic;
    signal Counter2Terminated : std_logic;
    signal FSMQBus : std_logic_vector(3 downto 0);
    signal FSMDBus : std_logic_vector(3 downto 0);
begin
    -- registro de desplazamiento
    process (Clock)
    begin
        if (Clock'event and (Clock = '1')) then
            ShiftRegisterQBus <= ShiftRegisterDBus;
        end if;
    end process;
    -- MSB first (apenas usado)
    -- ShiftRegisterDBus <= (ShiftRegisterQBus(7 downto 0) & Rx) when (ShiftRegisterEnable = '1') else ShiftRegisterQBus;
    -- LSB first: los valores se van metiendo por el bit más significativo
    ShiftRegisterDBus <= (Rx & ShiftRegisterQBus(8 downto 1)) when (ShiftRegisterEnable = '1') else ShiftRegisterQBus;
    
    -- buffer de salida
    process (Clock)
    begin
        if (Clock'event and (Clock = '1')) then
            BufferQBus <= BufferDBus;
        end if;
    end process;
    -- MSB first (apenas usado)
    -- BufferDBus <= ShiftRegisterQBus(8 downto 1) when (BufferEnable = '1') else BufferQBus;
    -- LSB first: El bit de stop está en el bit más significativo, el dato en el resto de bits
    BufferDBus <= ShiftRegisterQBus(7 downto 0) when (BufferEnable = '1') else BufferQBus;

    -- contador fino para medir 1 y 1,5 bits a 32MHz
    Counter1: TwoLimitcounter generic map (
        NBits => 13,
        --Limit1 => 50,   -- 1 bit a 1MHz
        --Limit2 => 75    -- 1.5 bits a 1MHz
        Limit1 => 3333,   -- 1 bit a 32MHz
        Limit2 => 5000    -- 1.5 bits a 32MHz
    )
    port map (
        Reset => Counter1Reset,
        Clock => Clock,
        Enable => '1',
        Terminated1 => Counter1Terminated1,
        Terminated2 => Counter1Terminated2
    );
    
    -- contador grueso de bits
    Counter2: TwoLimitcounter generic map (
        NBits => 4,
        Limit1 => 8,  -- poniendo el límite a 8 metemos 9 valores en el registro de desplaz.
        Limit2 => 0
    )
    port map (
        Reset => Counter2Reset,
        Clock => Clock,
        Enable => Counter2Enable,
        Terminated1 => Counter2Terminated
    );
   
    -- FSM: Biestables
    process (Clock)
    begin
        if (Clock'event and (Clock = '1')) then
            FSMQBus <= FSMDBus;
        end if;
    end process;
    
    -- FSM: Lógica del estado siguiente
    FSMDBus <= "0001" when (FSMQBus = "0000") and (Rx = '0') else
               "0010" when (FSMQBus = "0001") or ((FSMQBus = "0010") and (Counter1Terminated2 = '0')) else
               "0011" when (FSMQBus = "0010") and (Counter1Terminated2 = '1') else
               "0100" when (FSMQBus = "0011") or ((FSMQBus = "0101") and (Counter2Terminated = '0')) or ((FSMQBus = "0100") and (Counter1Terminated1 = '0')) else
               "0101" when (FSMQBus = "0100") and (Counter1Terminated1 = '1') else
               "0110" when (FSMQBus = "0101") and (Counter2Terminated = '1') else
               "0111" when (FSMQBus = "0110") and (ShiftRegisterQBus(8) = '1') else  -- bit de stop ok
               "1000" when (FSMQBus = "0111") else
               "1001" when (FSMQBus = "1000") else
               "1010" when (FSMQBus = "0110") and (ShiftRegisterQBus(8) = '0') else  -- bit de stop mal
               "0000";
    -- FSM: Lógica de salida
    ShiftRegisterEnable <= '1' when (FSMQBus = "0011") or (FSMQBus = "0101") else
                           '0';
    BufferEnable <= '1' when (FSMQBus = "0111") else
                    '0';
    Counter1Reset <= '1' when (FSMQBus = "0001") or (FSMQBus = "0011") or (FSMQBus = "0101") else
                     '0';
    Counter2Reset <= '1' when (FSMQBus = "0001") else
                     '0';
    Counter2Enable <= '1' when (FSMQBus = "0011") or (FSMQBus = "0101") else
                      '0';
    DataOutOk <= '1' when (FSMQBus = "1001") else
                 '0';

    -- salida paralelo
    DataOut <= BufferQBus;
end Architecture1;


Se trata de un diseño RTL, por lo que la implementación en VHDL es trivial, directa y siempre sintetizable.

A continuación puede verse una simulación en la que se recibe el valor serie 0x53:



La señal COUNTER1TERMINATED2 indica que han pasado 1.5 bits de datos desde el reset del contador 1 mientras que la señal COUNTER1TERMINATED1 indica que ha pasado 1 bit de datos desde el reset del contador 1.

Implementación física

A la placa Papilio One (Xilinx Spartan3E) se le conectó por un lado un array de 8 leds (conectado internamente al registro de salida del módulo receptor de la UART) y, por otro lado un módulo USBSerial basado en el chip FT232R, dicho módulo permite mediante un jumper seleccionar una operación a 3.3V (la FPGA incluida en la placa Papilio One no es tolerante a 5V): Se conectó la salida TX del módulo a un pin de la FPGA conectado internamente a la señal RX del módulo receptor.





En la foto se puede ver al receptor cargando un carácter 'i' (hexadecimal 69) enviado por el puerto serie desde el ordenador.

Todo el código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 1373 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2271 )
Implementación del algoritmo de multiplicación de Booth en VHDL sobre una FPGA 
El algoritmo de multiplicación de Booth permite multiplicar enteros con signo en complemento a dos utilizando una técnica muy sencilla basada en desplazamientos y sumas. A lo largo de este post se abordará el diseño y la codificación en VHDL de dicho algoritmo así como su implementación final en una FPGA.

El algoritmo

En la Wikipedia hay una explicación muy clara y detallada del algoritmo de Booth (https://es.wikipedia.org/wiki/Algoritmo_de_Booth). En este caso se ha asumido, por simplicidad, que ambos términos (multiplicando y multiplicador) tienen la misma cantidad de bits.

Partimos de dos números enteros X e Y, ambos de N bits:

1. Construimos una matriz de 3 filas y N+N+1 columnas. La primera fila la llamaremos A, la segunda S y la tercera P.

  1.1. En los N bits más significativos de A metemos X, el resto de bits de A los ponemos a 0.

  1.2. En los N bits más significativos de S metemos -X (complemento a 2 de X), el resto de bits de S los ponemos a 0.

  1.3. En los N bits más significativos de P metemos 0s, a continuación metemos los N bits de Y y en el bit que queda (el menos significativo) metemos un 0.

2. Hacer N veces:

  2.1. Si los dos bits menos significativos de P son 01, hacer P <- P + A, en caso de que sean 10, hacer P <- P + S, en caso de que sean 00 o 11, no hacer nada

  2.2. Hacer un desplazamiento aritmético (incluyendo el signo) de P hacia la derecha.

3. El resultado de la multiplicación serán los bits N a 1 de P (ojo, el bit 0 de P no forma parte de la solución).

Se trata de un algoritmo muy sencillo y que debe ser implementado de forma secuencial.

El flujo de datos

A continuación puede verse de forma esquemática cómo sería el flujo de datos en el multiplicador.



El multiplexor MUXa permite seleccionar entre la operación “P + A” o “P + S”, mientras que el multiplexor MUXp permite seleccionar entre al desplazamiento aritmético hacia la derecha de P, la entrada (para cargar el valor inicial de P a partir del operando Y) y la salida del sumador.

La unidad de control del multiplicador

Para gobernar las señales de carga de los registros y las señales de selección de los multiplexores es necesario implementar una unidad de control. La unidad de control se implementará mediante una máquina de estados finita (FSM) formada por biestables D, lógica de estado siguiente y lógica de salida de tipo Moore.



En este caso la máquina de estados que implementaría el algoritmo de Booth sería la siguiente:



Supongamos que se quiere multiplicar -3 por 2 utilizando una mantisa de 5 bits. En este caso:

-3 dec = 11101 bin
2 dec = 00010 bin

- Estado 0.
- Estado 1: MUXp=Y, Resetear el contador.
- Estado 2: Cargar A, Cargar S, Cargar P (se carga Y), Avanzar el contador.
- Estado 3.
Estando en el estado 3 los dos bits menos significativos de P valen en este momento “00” (P1=P0)y el contador no ha terminado (Ct=0), por lo que se va a estado 8.
- Estado 8: MUXp = SRA(P) (desplazamiento aritmético a la derecha de P un bit).
- Estado 9: MUXp = SRA(P), Cargar P (P <- SRA(P)).
- Estado 10: Avanzar el contador.
- Estado 3.
Estando en el estado 3 los dos bits menos significativos de P valen en este momento “10” (P1=1 y P0=0) y el contador no ha terminado (Ct=0), por lo que se va de nuevo al estado 6.
- Estado 6: MUXp = Sumador, MUXa = A.
- Estado 7: MUXp = Sumador, MUXa = A, Cargar P (P <- P + A)
- ...

Y así sucesivamente. Como se puede ver en el grafo de la FSM la multiplicación termina cuando, estando en el estado 3, el contador llega al final:

- ...
- Estado 3: Si el contador ha terminado pasamos al estado 11.
- Estado 11: Cargar Out (Out <- P).
- Estado 0 (se vuelve a empezar).
- ...

El el siguiente diagrama puede verse cómo quedaría todo el conjunto (registros, multiplexores, sumador y unidad de control) con lo que serían las entradas y salidas finales del multiplicador.



Implementación en VHDL

Para implementar en VHDL el FSM de la unidad de control basta con traducir el FSM a un modelo RTL: se traducen los arcos del grafo a lógica de estado siguiente y las salidas indicadas en los nodos del grafo a lógica de salida.

library ieee;
use ieee.std_logic_1164.all;

entity MultiplierControlUnit is
    generic (
        NBits : integer := 4
    );
    port (
        Clock             : in std_logic;
        Reset             : in std_logic;
        P1                : in std_logic;
        P0                : in std_logic;
        LoadA             : out std_logic;
        LoadS             : out std_logic;
        LoadP             : out std_logic;
        LoadOut           : out std_logic;
        AdderMuxSel       : out std_logic;
        PMuxSel           : out std_logic_vector(1 downto 0)
    );
end MultiplierControlUnit;

architecture Architecture1 of MultiplierControlUnit is
    component Counter
        generic (
            NBits : integer := 4;
            Limit : integer := 3
        );
        port (
            Reset      : in std_logic;
       	    Clock      : in std_logic;
            Terminated : out std_logic
        );
    end component;
    signal DBus : std_logic_vector(3 downto 0);
    signal QBus : std_logic_vector(3 downto 0);
    signal CounterReset : std_logic;
    signal CounterClock : std_logic;
    signal CounterTerminated : std_logic;
begin
    -- counter for shift loop
    C : Counter generic map (
    	NBits => 8,
        Limit => NBits
    )
    port map (
    	Reset => CounterReset,
        Clock => CounterClock,
        Terminated => CounterTerminated
    );
    
    -- D flip-flop with synchronous reset for FSM
    process (Clock, Reset)
    begin
        if (Clock'event and (Clock = '1')) then
	    if (Reset = '1') then
                QBus <= (others => '0');
            else
            	QBus <= DBus;
            end if;
        end if;
    end process;
    
    -- next state logic
    DBus <= "0001" when (QBus = "0000") else
            "0010" when (QBus = "0001") else
            "0011" when ((QBus = "0010") or (QBus = "1010")) else
            "0100" when ((QBus = "0011") and (P1 = '1') and (P0 = '0') and (CounterTerminated = '0')) else
            "0101" when (QBus = "0100") else
            "0110" when ((QBus = "0011") and (P1 = '0') and (P0 = '1') and (CounterTerminated = '0')) else
            "0111" when (QBus = "0110") else
            "1000" when ((QBus = "0101") or (QBus = "0111") or ((QBus = "0011") and (P1 = P0) and (CounterTerminated = '0'))) else
            "1001" when (QBus = "1000") else
            "1010" when (QBus = "1001") else
            "1011" when ((QBus = "0011") and (CounterTerminated = '1')) else
            "0000";

    -- output logic
    LoadA <= '1' when (QBus = "0010") else
             '0';
    LoadS <= '1' when (QBus = "0010") else
             '0';
    LoadP <= '1' when ((QBus = "0010") or (QBus = "0101") or (QBus = "0111") or (QBus = "1001")) else
             '0';
    LoadOut <= '1' when (QBus = "1011") else
               '0';
    PMuxSel <= "01" when ((QBus = "0001") or (QBus = "0010")) else  -- Y
               "10" when ((QBus = "0100") or (QBus = "0101") or (QBus = "0110") or (QBus = "0111")) else  -- +
               "00" when ((QBus = "1000") or (QBus = "1001")) else
               "11";
    AdderMuxSel <= '0' when ((QBus = "0110") or (QBus = "0111")) else  -- A
                   '1' when ((QBus = "0100") or (QBus = "0101")) else  -- S
                   '0';
    CounterReset <= '1' when ((QBus = "0001") or (QBus = "0010")) else
                    '0';
    CounterClock <= '1' when ((QBus = "0010") or (QBus = "1010")) else
                    '0';
end Architecture1;

La unidad de control incluye un contador interno (el componente instanciado como C) encargado de controlar la cantidad de veces que itera el bucle del algoritmo. En el caso del algoritmo de Booth el bucle itera tantas veces como bits tiene la mantisa (al instanciar el contador C hacemos Limit => NBits).

Como puede apreciarse, se trata de un diseño totalmente basado en modelos RTL (https://en.wikipedia.org/wiki/Register-transfer_level) por lo que su implementación es relativamente sencilla y el código generado siempre es sintetizable.

Todo el código fuente se puede descargar de la sección soft.



[ añadir comentario ] ( 1450 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2489 )
Display de 7 segmentos con interface serie en VHDL 
Las FPGAs y los CPLDs son circuitos integrados digitales programables a nivel hardware mediante algún tipo de lenguaje de descripción de hardware (VHDL, Verilog, SystemC, etc.). A lo largo de este post se desarrolla una primera toma de contacto con este tipo de integrados.

FPGA

La FPGA que se ha usado es una Xilinx Spartan 3E, que se puede encontrar en la placa Papilio One (http://papilio.cc). Esta placa es open hardware, con interface USB, memoria flash SPI para almacenar la configuración del hardware y con una buena relación calidad/precio (unos 38 dólares aproximadamente).

El entorno de desarrollo de Xilinx es un poco complejo (muchas opciones) pero se le coge el truco rápido. Es gratuito (aunque no es software libre) y muy fácil de instalar tanto en Windows como en Linux (no está para Mac). El entorno de desarrollo permite gestionar proyectos en VHDL o Verilog y generar al final los ficheros ".bit" que son los que se mandan a la FPGA.

De http://papilio.cc se descarga el Papilio Loader, un software open source que permite "tostar" los ficheros ".bit" en la placa FPGA y probar los diseños rápidamente. En la propia página del proyecto vienen varios tutoriales.

Prueba de concepto

Como prueba inicial se plantea un circuito conversor de binario (4 bits, del 0 al 9) a 7 segmentos mediante interfaz serie:

Se trata de un esquema muy sencillo: un registro de desplazamiento de 4 bits (que permite leer la entrada serie), un latch de 4 bits que carga el contenido del registro de desplazamiento y a la salida del latch una lógica combinatoria que convierte el número de 4 bits en una salida de 7 bits (para el display de 7 segmentos).

VHDL de la lógica combinatoria

VHDL es un lenguaje de descripción de hardware, no un lenguaje imperativo al uso. Cada línea de código describe un comportamiento y la única forma de realizar procesamiento secuencial es mediante la cláusula "process" ya que por defecto se ejecuta "todo a la vez".

Una lógica combinatoria está basada en puertas lógicas y las puertas lógicas se "ejecutan" siempre, no son como los biestables u otros elementos secuenciales. Para la lógica combinatoria de conversión binario a 7 segmentos se puede utilizar la sentencia WHEN...ELSE:

B(0) <= '1' when ((x = "0000") or (x = "0001") or (x = "0011") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
B(1) <= '1' when ((x = "0000") or (x = "0010") or (x = "0011") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';
B(2) <= '1' when ((x = "0000") or (x = "0010") or (x = "0110") or (x = "1000")) else '0';
B(3) <= '1' when ((x = "0000") or (x = "0010") or (x = "0011") or (x = "0101") or (x = "0110") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
B(4) <= '1' when ((x = "0000") or (x = "0001") or (x = "0010") or (x = "0011") or (x = "0100") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
B(5) <= '1' when ((x = "0000") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';
B(6) <= '1' when ((x = "0010") or (x = "0011") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';

Como se puede apreciar en este caso, las 7 líneas de código debe "ejecutarse" de forma concurrente. Dicho de otra forma: se describen 7 circuitos combinacionales que deben implementarse en paralelo.

En este caso x es la salida del latch mientras que B es la salida de la FPGA que está conectada al display de 7 segmentos. En este caso se ha usado un display de cátodo común por lo que para encender un segmento del display hay que emitir un ‘1’ en la salida correspondiente.

VHDL de la lógica secuencial

La lógica secuencial se divide en la lógica del registro de desplazamiento que se ha implementado utilizando el clásico modelo RTL:

d_reg <= data_in & q_reg(3 downto 1);   --d_reg es q_reg desplazado concatenado con el bit que hay en data_in

process(clock_in)  --el proceso se activa cuando clock_in cambia
begin
    if (rising_edge(clock_in)) then  --cuando hay un flanco de subida
        q_reg <= d_reg;     --se carga d_reg en q_reg
    end if;
end process;

Y la lógica del latch de 4 bits, que se encarga de cargar el registro de desplazamiento (q_reg) en la señal de entrada de la lógica combinatoria para la salida de 7 segmentos (x), y que también se ha implementado utilizando el modelo RTL:

process(latch_in)   --el proceso se activa cuando latch_in cambia
begin
    if (rising_edge(latch_in)) then    --cuando hay un flanco de subida
        x <= q_reg;    --se carga q_reg en x
    end if;
end process;

En este caso las acciones a realizar no se hacen "siempre" sino que dependen de otras señales (clock_in y latch_in) y debe hacerse una evaluación secuencial (si ocurre esto entonces aquello), por eso se utilizan bloques "process". Nótese que ambos bloques "process" se "ejecutan" en paralelo.

VHDL completo

El código VHDL completo, incluyendo la arquitectura y el port queda como sigue:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity SimpleShiftRegister is
	port (
		clock_in : in std_logic;
		data_in : in std_logic;
		latch_in : in std_logic;
		B : out std_logic_vector(6 downto 0)
	);
end SimpleShiftRegister;

architecture behavioral of SimpleShiftRegister is
signal d_reg : std_logic_vector(3 downto 0);
signal q_reg : std_logic_vector(3 downto 0);
signal x     : std_logic_vector(3 downto 0);
begin

	d_reg <= data_in & q_reg(3 downto 1);

	B(0) <= '1' when ((x = "0000") or (x = "0001") or (x = "0011") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
	B(1) <= '1' when ((x = "0000") or (x = "0010") or (x = "0011") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';
	B(2) <= '1' when ((x = "0000") or (x = "0010") or (x = "0110") or (x = "1000")) else '0';
	B(3) <= '1' when ((x = "0000") or (x = "0010") or (x = "0011") or (x = "0101") or (x = "0110") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
	B(4) <= '1' when ((x = "0000") or (x = "0001") or (x = "0010") or (x = "0011") or (x = "0100") or (x = "0111") or (x = "1000") or (x = "1001")) else '0';
	B(5) <= '1' when ((x = "0000") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';
	B(6) <= '1' when ((x = "0010") or (x = "0011") or (x = "0100") or (x = "0101") or (x = "0110") or (x = "1000") or (x = "1001")) else '0';
	
	process(clock_in)
	begin
		if (rising_edge(clock_in)) then
			q_reg <= d_reg;
		end if;
	end process;
	
	process(latch_in)
	begin
		if (rising_edge(latch_in)) then
			x <= q_reg;
		end if;
	end process;
end behavioral;

Tras compilar y sintetizar este código, la implementación eléctrica generada es la siguiente:



Como se puede ver, tanto el registro de desplazamiento como el latch se implementa mediante biestables D mientras que la lógica combinatoria de conversión de binario a 7 segmentos se implementa mediante LUTs (Look Up Tables), en lugar de mediante puertas lógicas. Esta forma de implementar lógica combinatoria es muy habitual en las FPGAs y los CPLDs.

Conexión con el Arduino

La FPGA funciona a 3.3 voltios mientras que el Arduino funciona a 5 voltios. Es necesario, por tanto adaptar los voltajes. En este caso concreto todas las señales salen del Arduino y entran en la FPGA por lo que se ha optado por hacer una adaptación de voltaje sencilla basada en transistores.

En el Arduino la función encargada de enviar un valor al display de 7 segmentos deberá colocar los datos de forma serie a través del pin data_in usando como reloj clock_in. Se activará latch_in cuando se desee mostrar en el display el valor cargado en el registro de desplazamiento. Obsérvese que al hacer la conversión de voltajes utilizando transistores NPN en configuración de emisor común, la lógica debe ser negada, es decir, para emitir un 1 de 3.3 voltios, emitimos un 0 de 5 voltios y para emitir un 0 de 3.3 voltios, emitimos un 1 de 5 voltios:

const int CLK_PIN = 0;
const int DATA_PIN = 1;
const int LATCH_PIN = 2;

#define  ONE   LOW
#define  ZERO  HIGH

void byteOut(unsigned char v) {
  for (unsigned char i = 0; i < 4; i++) {
    if ((v & 1) != 0)
      digitalWrite(DATA_PIN, ONE);
    else
      digitalWrite(DATA_PIN, ZERO);
    delay(1);
    digitalWrite(CLK_PIN, ONE);
    delay(1);
    digitalWrite(CLK_PIN, ZERO);
    delay(1);
    v = v >> 1;
  }
  digitalWrite(LATCH_PIN, ONE);
  delay(1);
  digitalWrite(LATCH_PIN, ZERO);
  delay(1);
}

A continuación puede verse un vídeo con el invento en funcionamiento:



Algunos enlaces para empezar con VHDL (en español)

Lista de reproducción de YouTube del profesor Carlos Fajardo sobre VHDL - Vídeos muy amenos y fáciles de seguir.
Libro online "Programación en VHDL" - Muy buen libro, aunque le echo en falta más ejemplos y ejercicios.

[ añadir comentario ] ( 1734 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2883 )

<< <Anterior | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |