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.
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
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.
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.
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.
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.














Parabéns André! Muito bacana o artigo e o projeto! Que tal um SPI com MyHDL? hehehe hehehehe
Valeu Marcelo! Ótima sugestão, vou fazer 🙂
abs
Funcionou bem!
Apenas nao consegui rodar o código abaixo:
yield rx_rdy.posedge
assert tx_data == rx_data
Olá Ronaldo. Qual erro aconteceu? Você comentou a parte do test bench necessária ? (TraceSignals)
Abraço
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
Ola Ronaldo é um erro de identação, verifique se você está usando quatro espaços de identação. Abraço
Pode cre! valeu!