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.

Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.