Projeto completo de uma UART em MyHDL

UART em MyHDL
Este post faz parte da série MyHDL

A UART é um velho conhecido de todo engenheiro de sistemas embarcados, é provavelmente o primeiro protocolo de comunicação que aprendemos na universidade. Neste artigo vamos implementar o nosso próprio circuito serial em MyHDL. Assim podemos fazer nosso próprio hardware que se comunica com nosso código em Java, C#, ou com outro microcontrolador como um Arduino

Conforme vimos em um artigo anterior, MyHDL é uma linguagem de descrição de hardware em Python que possibilita gerar arquivos em VHDL e Verilog. Além é claro de prover todo o potencial do Python para o usuário, como por exemplo, bibliotecas para testes unitários.

UART

Como sabemos, tanto o transmissor quanto o receptor entram em um acordo quanto à taxa de transferência (Baud Rate), o número de bits de dados, o número de stop bits e outros detalhes mais. Neste exemplo vamos implementar uma UART com 8 bits de dados, 2 stop bits e 115200 bps de Baud Rate.

Nossa UART é composta por três módulos:

  • baudrate_gen: Circuito responsável por gerar nossos pulsos de baudrate;
  • serial_tx: Circuito responsável por enviar dados. Possui como entrada uma porta de 8 bits com o dado a ser enviado e uma porta de start. Quando start vai para ‘1’ o dado é enviado pela porta de saída TX;
  • serial_rx: Circuito responsável por receber dados. Possui como entrada a porta RX e como saída uma porta de 8 bits de dados e uma porta indicando quando os dados estão prontos.
arch
Arquitetura da nossa UART em MyHDL

baudrate_gen

Este é o nosso módulo mais simples, com base no clock de entrada é gerada uma saída na taxa especificada. A fórmula é bem simples: Frequência do clock / Frequência do baudrate. Tenho um cristal de 50MHz no meu FPGA e desejo uma taxa 115200 Hz. 50.000.000 / 115.200 da aproximadamente 434. Então, a cada 434 pulsos do meu clock de 50 MHz vou gerar um pulso de baud, descrito como “tick” na nossa arquitetura. 

Como entrada temos o clock (sysclk), um reset (reset_n), a taxa de baud desejada (baud_rate_i) . Como saída temos os ticks: half_baud_rate_o e baud_rate_o. A primeira vai para ‘1’ na metade do tempo, neste caso, a cada 217 pulsos de clock de 50 MHz.

O código fica bem similar a uma descrição em VHDL ou Verilog. O módulo é tratado como uma função em Python, os argumentos das funções são as nossas portas. O MyHDL identifica automaticamente o que é entrada e o que é saída no módulo. Um registrador é inferido  para guardar o número de pulsos, baud_gen_count_reg.

A keyword Signal indica que é um Sinal (equivalente ao VHDL). O tipo intbv se refere a INT BIT VECTOR, é algo semelhante a um STD_LOGIC_VECTOR . Com a diferença de que o MyHDL sabe na hora de gerar VHDL/Verilog, se vai ser um vetor lógico, um vetor com sinal ou vetor sem sinal.

from myhdl import *

def baudrate_gen(sysclk, reset_n, baud_rate_i, half_baud_rate_tick_o, baud_rate_tick_o):

    """ Serial
    This module implements a baudrate generator

    Ports:
    -----
    sysclk: sysclk input
    reset_n: reset input
    baud_rate_i: the baut rate to generate
    baud_rate_tick_o: the baud rate enable
    -----

    """
    baud_gen_count_reg = Signal(intbv(0, min = 0, max = 900))
    half_baud_const = baud_rate_i//2

    @always_seq(sysclk.posedge, reset = reset_n)
    def sequential_process():
        baud_gen_count_reg.next = baud_gen_count_reg + 1
        baud_rate_tick_o.next = 0
        half_baud_rate_tick_o.next = 0
        if baud_gen_count_reg == baud_rate_i:
            baud_gen_count_reg.next = 0
            baud_rate_tick_o.next = 1 
        if baud_gen_count_reg == half_baud_const:
            half_baud_rate_tick_o.next = 1 
    

    return sequential_process

O decorator @always_seq emite automaticamente o nosso PROCESS (VHDL) ou ALWAYS (VERILOG) de forma síncrona e reseta todos os registradores para zero. É sempre necessário ter ao final um return com todos os nossos process/always/decorators.

serial_tx

Este módulo é responsável por enviar os dados via serial. Tem-se uma máquina de estados que aguarda um sinal de (start_i) para iniciar o processo de enviar o byte  (data_i) pela nossa saída de TX (transmit_o).

O código é bem semelhante a uma implementação em VHDL ou Verilog, descrevemos tudo a nível RTL. Temos agora também o decorator @always_comb que é equivalente a um PROCESS/ALWAYS puramente combinacional. Enviamos um bit a cada “tick” do nosso baudrate. Como ficou um pouco mais complexo, optei pelo estilo de RTL dividido em duas máquinas de estados, aonde os registradores são inferidos no decorator sequencial e a lógica combinacional no decorator combinacional.

from myhdl import *

t_State = enum('ST_WAIT_START', 'ST_SEND_START_BIT', 'ST_SEND_DATA' , 'ST_SEND_STOP_BIT' )


def serial_tx(sysclk, reset_n, start_i, data_i, n_stop_bits_i, baud_rate_tick_i, transmit_o):

    """ Serial
    This module implements a transmiter serial interface

    Ports:
    -----
    sysclk: sysclk input
    reset_n: reset input
    baud_rate_tick_i: the baud rate
    start_i: start sending data
    data_i: the data to send
    transmit_o: data output
    -----

    """
    END_OF_BYTE = 7
    
    state_reg = Signal(t_State.ST_WAIT_START)
    state = Signal(t_State.ST_WAIT_START)

    transmit_reg = Signal(bool(0))
    transmit = Signal(bool(0))

    count_8_bits_reg = Signal(intbv(0, min = 0, max = 8))
    count_8_bits = Signal(intbv(0, min = 0, max = 8))

    count_stop_bits_reg = Signal(intbv(0, min = 0, max = 8))
    count_stop_bits = Signal(intbv(0, min = 0, max = 8))

    @always_comb
    def outputs():
        transmit_o.next = transmit_reg

    @always_seq(sysclk.posedge, reset = reset_n)
    def sequential_process():
        state_reg.next   = state
        transmit_reg.next  = transmit
        count_8_bits_reg.next = count_8_bits
        count_stop_bits_reg.next = count_stop_bits
    
    @always_comb
    def combinational_process():
        state.next  = state_reg
        transmit.next = transmit_reg
        count_8_bits.next = count_8_bits_reg
        count_stop_bits.next = count_stop_bits_reg

        if state_reg == t_State.ST_WAIT_START:
            transmit.next = True
            if start_i == True:
                state.next = t_State.ST_SEND_START_BIT

        elif state_reg == t_State.ST_SEND_START_BIT:
            transmit.next = False
            if baud_rate_tick_i == True:
                state.next = t_State.ST_SEND_DATA

        elif state_reg == t_State.ST_SEND_DATA:
            transmit.next = data_i[count_8_bits_reg]
            if baud_rate_tick_i == True:
                if count_8_bits_reg == END_OF_BYTE:
                    count_8_bits.next = 0
                    state.next = t_State.ST_SEND_STOP_BIT
                else:
                    count_8_bits.next = count_8_bits_reg + 1
                    state.next = t_State.ST_SEND_DATA
                

        elif state_reg == t_State.ST_SEND_STOP_BIT:
            transmit.next = True
            if baud_rate_tick_i == True:
                if count_stop_bits_reg == (n_stop_bits_i - 1):
                    count_stop_bits.next = 0
                    state.next = t_State.ST_WAIT_START
                else:
                    count_stop_bits.next = count_stop_bits_reg + 1
        else:
            raise ValueError("Undefined State")

            

    return outputs, sequential_process, combinational_process

serial_rx

Este módulo é responsável por transformar os dados que estão vindo via serial em um byte de informação. Aguarda-se o start bit no RX e armazena-se os próximos 8 bits de dados em um registrador de 1 byte. Assim que o dado (data_o) está pronto, o ready_o vai para nível lógico alto.

from myhdl import *

t_State = enum('ST_WAIT_START_BIT', 'ST_GET_DATA_BITS', 'ST_GET_STOP_BITS' )


def serial_rx(sysclk, reset_n, n_stop_bits_i, half_baud_rate_tick_i, baud_rate_tick_i, recieve_i, data_o, ready_o):

    """ Serial
    This module implements a reciever serial interface

    Ports:
    -----
    sysclk: sysclk input
    reset_n: reset input
    half_baud_rate_tick_i: half baud rate tick
    baud_rate_tick_i: the baud rate
    n_stop_bits_i: number of stop bits
    recieve_i: rx
    data_o: the data output in 1 byte
    ready_o: indicates data_o is valid
    -----

    """
    END_OF_BYTE = 7
    
    state_reg = Signal(t_State.ST_WAIT_START_BIT)
    state = Signal(t_State.ST_WAIT_START_BIT)

    data_reg = Signal(intbv(0, min = 0, max = 256))
    data = Signal(intbv(0, min = 0, max = 256))
    ready_reg = Signal(bool(0))
    ready = Signal(bool(0))
    
    count_8_bits_reg = Signal(intbv(0, min = 0, max = 8))
    count_8_bits = Signal(intbv(0, min = 0, max = 8))

    count_stop_bits_reg = Signal(intbv(0, min = 0, max = 8))
    count_stop_bits = Signal(intbv(0, min = 0, max = 8))

    @always_comb
    def outputs():
        data_o.next = data_reg
        ready_o.next = ready_reg


    @always_seq(sysclk.posedge, reset = reset_n)
    def sequential_process():
        state_reg.next   = state
        data_reg.next  = data
        ready_reg.next = ready
        count_8_bits_reg.next = count_8_bits
        count_stop_bits_reg.next = count_stop_bits
    
    @always_comb
    def combinational_process():
        state.next  = state_reg
        data.next = data_reg
        ready.next = ready_reg
        count_8_bits.next = count_8_bits_reg
        count_stop_bits.next = count_stop_bits_reg

        if state_reg == t_State.ST_WAIT_START_BIT:
            ready.next = False
            if baud_rate_tick_i == True:
                if recieve_i == False:
                    state.next = t_State.ST_GET_DATA_BITS

        elif state_reg == t_State.ST_GET_DATA_BITS:
            if baud_rate_tick_i == True:
                data.next[count_8_bits_reg] = recieve_i
                if count_8_bits_reg == END_OF_BYTE:
                    count_8_bits.next = 0
                    state.next = t_State.ST_GET_STOP_BITS
                else:
                    count_8_bits.next = count_8_bits_reg + 1
                    state.next = t_State.ST_GET_DATA_BITS
                

        elif state_reg == t_State.ST_GET_STOP_BITS:
            if baud_rate_tick_i == True:
                if count_stop_bits_reg == (n_stop_bits_i - 1):
                    count_stop_bits.next = 0
                    ready.next = True
                    state.next = t_State.ST_WAIT_START_BIT
                else:
                    count_stop_bits.next = count_stop_bits_reg + 1
        else:
            raise ValueError("Undefined State")

            

    return outputs, sequential_process, combinational_process

Testbenches

Vamos testar da forma tradicional o nosso MyHDL, ou seja, criando um testbench e verificando a waveform.

Importe os módulos que acabamos de criar (serial_tx.py, serial_rx,py e baudrate_gen.py) no arquivo tb_serial.py.

from math import *
from myhdl import *

from serial_tx import serial_tx
from serial_rx import serial_rx
from baudrate_gen import baudrate_gen

def bench():

    CLK_PERIOD = 20
    clk_freq = 50000000
    baud_const = int(floor(clk_freq / 115200))
    clock = Signal(bool(0))
    reset = ResetSignal(0, active=0, async=True)
    start = Signal(False)
    rx_rdy = Signal(False)
    tx_data = Signal(intbv(0, min = 0, max = 256))
    rx_data = Signal(intbv(0, min = 0, max = 256))
    n_stop = 2
    baudrate_tick = Signal(bool(0))
    half_baudrate_tick = Signal(bool(0))
    tx = Signal(bool(0))
    rx = Signal(bool(0))


    # design under test
    baud_gen_inst = baudrate_gen(clock, reset, baud_const, half_baudrate_tick, baudrate_tick)  
    serial_tx_inst   = serial_tx(clock, reset, start, tx_data, n_stop, baudrate_tick, tx)
    serial_rx_inst   = serial_rx(clock, reset, n_stop, half_baudrate_tick, baudrate_tick, tx, rx_data, rx_rdy)

    # clock generator
    @always(delay(CLK_PERIOD/2))
    def clockgen():
        clock.next = not clock

    @instance
    def stimulus(): 
        tx_data.next = 196
        reset.next = 0
        for i in range(1):
            yield clock.negedge
        reset.next = 1
        for i in range(10):
            yield clock.negedge
        start.next = 1
        yield clock.negedge
        start.next = 0
  
    return baud_gen_inst, serial_tx_inst, serial_rx_inst, clockgen, stimulus


def test_bench():
    tb = traceSignals(bench)
    sim =  Simulation(tb)
    sim.run(1000000)

test_bench()

Definimos a nossa função bench responsável por parametrizar nosso projeto e também gerar os estímulos. Colocamos como constantes o periodo do clock (1 / 50000000), a frequência do clock (50000000), e calculamos a constante da nossa taxa de baudrate (número de pulsos para tick).

Então declaramos os tipos de dados dos nossos módulos, ao utilizar um ResetSignal para o reset você pode especificar se ele é síncrono ou assincrono e se é ativo em nível lógico alto ou baixo. Isto é necessário para o decorator @always_seq gerar o reset da forma correta. Também instanciamos os nossos módulos e fazemos as conexões, bem semelhante a forma feita em VHDL/Verilog.

Então temos dois decorators, um para gerar o clock e outro para gerar os estímulos do circuito. Como podemos observar, estamos colocando o dado 196 no serial_tx e enviando. Esperamos que o serial_rx nos dê este dado de volta ao emitir o sinal rx_rdy. A keyword yield é equivalente ao wait for em VHDL, ele espera que a condição seja verdadeira e então continua a execução.

A função test_bench executa o bench em MyHDL. O traceSignals é responsável por gerar os nossos waveforms no formato .vcd, iremos rodar a simulação por 1 ms. Para isto basta executar na linha de comando:

>python tb_serial.py

O arquivo bench.vcd foi gerado, podemos abri-lo em qualquer visualizador de waveform. Para abrir no gtkwave (grátis e opensource) basta executar

>gtkwave bench.vcd

Vamos visualizar o nosso waveform:

E funcionou! O valor 196 foi transmitido via TX (transmit_o), recebido via RX (recieve_i) e quando o ready foi para nível lógico alto lá estava nosso dado de novo!

Teste automatizado

Uma vantagem do MyHDL é a possibilidade de automatizar o nosso teste e não verificar mais waveforms. Para isto vamos alterar o nosso tb_serial.py. Basta adicionar as duas linhas abaixo do start.next = 0:

yield rx_rdy.posedge
assert tx_data == rx_data

e rodar o py.test no terminal:

>py.test tb_serial.py

Sem título

Pronto! Automaticamente o MyHDL verifica se o dado de envio e recebimento são os mesmos após a borda de subida do rx_rdy acontecer.

Gerando o VHDL / Verilog

Tudo muito bonito e testado! Mas agora vamos gerar o VHDL/Verilog a partir do nosso MyHDL. Isto é muito simples, basta alterar estas linhas no testbench:

tb_serial.py:

# design under test
    #baud_gen_inst = baudrate_gen(clock, reset, baud_const, half_baudrate_tick, baudrate_tick)  
    #serial_tx_inst   = serial_tx(clock, reset, start, tx_data, n_stop, baudrate_tick, tx)
    #serial_rx_inst   = serial_rx(clock, reset, n_stop, half_baudrate_tick, baudrate_tick, tx, rx_data, rx_rdy)
    
    # generate VHDL
    baud_gen_inst = toVHDL(baudrate_gen, clock, reset, baud_const, half_baudrate_tick, baudrate_tick)  
    serial_tx_inst   = toVHDL(serial_tx, clock, reset, start, tx_data, n_stop, baudrate_tick, tx)
    serial_rx_inst   = toVHDL(serial_rx, clock, reset, n_stop, half_baudrate_tick, baudrate_tick, tx, rx_data, rx_rdy)

E mais estas:

    tb = bench()
    #tb = traceSignals(bench)
    sim =  Simulation(tb)
    sim.run(1000000)

Agora, além de testar o seu design, os arquivos VHDL também já são gerados automaticamente. A função toVerilog geraria Verilog ao invés de VHDL.

O VHDL/Verilog gerado é completamente legível e bem estruturado, na verdade é basicamente uma tradução do seu RTL em Python. Um RTL ruim em Python gera um arquivo VHDL/Verilog ruim. Como exemplo o nosso serial_tx.vhd gerado:

-- File: serial_tx.vhd
-- Generated by MyHDL 0.8
-- Date: Tue Aug 19 14:17:48 2014


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

use work.pck_myhdl_08.all;

entity serial_tx is
    port (
        sysclk: in std_logic;
        reset_n: in std_logic;
        start_i: in std_logic;
        data_i: in unsigned(7 downto 0);
        baud_rate_tick_i: in std_logic;
        transmit_o: out std_logic
    );
end entity serial_tx;
-- Serial
-- This module implements a transmiter serial interface
-- 
-- Ports:
-- -----
-- sysclk: sysclk input
-- reset_n: reset input
-- baud_rate_tick_i: the baud rate
-- start_i: start sending data
-- data_i: the data to send
-- transmit_o: data output
-- -----

architecture MyHDL of serial_tx is


constant n_stop_bits_i: integer := 2;
constant END_OF_BYTE: integer := 7;


type t_enum_t_State_1 is (
    ST_WAIT_START,
    ST_SEND_START_BIT,
    ST_SEND_DATA,
    ST_SEND_STOP_BIT
);

signal transmit_reg: std_logic;
signal count_8_bits: unsigned(2 downto 0);
signal count_8_bits_reg: unsigned(2 downto 0);
signal state: t_enum_t_State_1;
signal transmit: std_logic;
signal count_stop_bits_reg: unsigned(2 downto 0);
signal count_stop_bits: unsigned(2 downto 0);
signal state_reg: t_enum_t_State_1;

begin





transmit_o <= transmit_reg;


SERIAL_TX_SEQUENTIAL_PROCESS: process (sysclk, reset_n) is
begin
    if (reset_n = '0') then
        count_8_bits_reg <= to_unsigned(0, 3);
        count_stop_bits_reg <= to_unsigned(0, 3);
        transmit_reg <= '0';
        state_reg <= ST_WAIT_START;
    elsif rising_edge(sysclk) then
        state_reg <= state;
        transmit_reg <= transmit;
        count_8_bits_reg <= count_8_bits;
        count_stop_bits_reg <= count_stop_bits;
    end if;
end process SERIAL_TX_SEQUENTIAL_PROCESS;


SERIAL_TX_COMBINATIONAL_PROCESS: process (transmit_reg, start_i, count_8_bits_reg, data_i, baud_rate_tick_i, count_stop_bits_reg, state_reg) is
begin
    state <= state_reg;
    transmit <= transmit_reg;
    count_8_bits <= count_8_bits_reg;
    count_stop_bits <= count_stop_bits_reg;
    case state_reg is
        when ST_WAIT_START =>
            transmit <= '1';
            if (start_i = '1') then
                state <= ST_SEND_START_BIT;
            end if;
        when ST_SEND_START_BIT =>
            transmit <= '0';
            if (baud_rate_tick_i = '1') then
                state <= ST_SEND_DATA;
            end if;
        when ST_SEND_DATA =>
            transmit <= data_i(to_integer(count_8_bits_reg));
            if (baud_rate_tick_i = '1') then
                if (count_8_bits_reg = END_OF_BYTE) then
                    count_8_bits <= to_unsigned(0, 3);
                    state <= ST_SEND_STOP_BIT;
                else
                    count_8_bits <= (count_8_bits_reg + 1);
                    state <= ST_SEND_DATA;
                end if;
            end if;
        when ST_SEND_STOP_BIT =>
            transmit <= '1';
            if (baud_rate_tick_i = '1') then
                if (signed(resize(count_stop_bits_reg, 4)) = (n_stop_bits_i - 1)) then
                    count_stop_bits <= to_unsigned(0, 3);
                    state <= ST_WAIT_START;
                else
                    count_stop_bits <= (count_stop_bits_reg + 1);
                end if;
            end if;
        when others =>
            assert False report "End of Simulation" severity Failure;
    end case;
end process SERIAL_TX_COMBINATIONAL_PROCESS;

end architecture MyHDL;

Testando na placa

Agora é a hora da verdade! Estou usando o kit DE-2 da Terasic e um cabo USB->Serial na porta RS-232.

Layout
Kit DE2

Usei como base o GOLD REFERENCE DESIGN, que já possui todas as portas declaradas e os pinos do FPGA identificados.

Instanciei meus três componentes, conectei a condição de START do Serial_TX ao botão KEY[0], conectei o RX vindo direto do RS232 no meu recieve_i e o TX direto do cabo no meu transmit_o.

Também conectei como loopback o dado a enviar no TX e o dado recebido no RX. A conexão está exatamente igual à indicada na arquitetura lá no começo do arquivo.

instancia

E não é que funciona? Basta conectarmos o cabo no FPGA e no computador e configurar o seu software de preferência que se comunique via serial.

Eu utilizei o Hyper Terminal, configurei a taxa de 115200 Hz, 8 bits de dados, sem paridade e 2 bits de stop (115200-8-n-2) e voilá… Tudo que você digita da Eco enquanto o botão estiver pressionado.

(Salve seu trabalho antes!!! Como esta é uma serial simplificada, não foram observados sinais de controle como CLEAR TO SEND e READY TO SEND. Isto me ocasionou uma tela azul no Windows 7 durante os testes).

O start do tx está mapeado em um botão sem debounce e não registrado. Ao apertar o botão para enviar um dado o sinal de “start” vai para ‘1’ diversas vezes.

loopback_serial
Loopback na serial

Conclusão

Com este projeto podemos observar que o MyHDL funciona e muito bem, gerando um bom VHDL e funcionando na placa. O ciclo de desenvolvimento fica muito mais rápido em Python e você possui ferramentas a mais para testar seu módulo antes de gerar o VHDL, é possível realizar diversos asserts e testar o seu projeto inteiro com um simples py.test <tb_top.py>.

Esta implementação de UART é mais didática, a intenção não é desenvolver uma UART para ser utilizada em um projeto da vida real.

O código do projeto está disponível no github.

MyHDL

MyHDL – Descrevendo hardware em Python
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
8 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Marcelo Andriolli
Marcelo Andriolli
26/08/2014 23:20

Parabéns André! Muito bacana o artigo e o projeto! Que tal um SPI com MyHDL? hehehe hehehehe

André Castelan
Reply to  Marcelo Andriolli
03/09/2014 08:46

Valeu Marcelo! Ótima sugestão, vou fazer 🙂

abs

ronaldo
ronaldo
25/08/2014 20:54

Funcionou bem!
Apenas nao consegui rodar o código abaixo:

yield rx_rdy.posedge
assert tx_data == rx_data

André Castelan
Reply to  ronaldo
25/08/2014 22:14

Olá Ronaldo. Qual erro aconteceu? Você comentou a parte do test bench necessária ? (TraceSignals)

Abraço

ronaldo
ronaldo
Reply to  André Castelan
25/08/2014 22:26

Segui a instrução: Para isto vamos alterar o nosso tb_serial.py. Basta adicionar as duas linhas abaixo do start.next = 0:
yield rx_rdy.posedge
assert tx_data == rx_data

E apresentou o erro:

File “tb_serial.py”, line 41
yield rx_rdy.posedge
^
IndentationError: unexpected indent

As ações antes desta alteração funcionaram perfeitamente

André Castelan
Reply to  ronaldo
25/08/2014 22:58

Ola Ronaldo é um erro de identação, verifique se você está usando quatro espaços de identação. Abraço

ronaldo
ronaldo
Reply to  André Castelan
25/08/2014 23:06

Pode cre! valeu!

Os comentários estão desativados.

Home » Software » Python » Projeto completo de uma UART em MyHDL

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: