UART Autobaud em VHDL

Olá leitor! Neste artigo (publicado originalmente no portal Embarcados) eu demonstro como implementar uma UART autobaud em VHDL. A sigla UART significa Universal Asynchronous Receiver Transmitter (ou Transmissor Receptor Assíncrono Universal) e nada mais é do que a interface de comunicação serial assíncrona comumente utilizada em microcontroladores e equipamentos externos (como GPS). Por sua natureza assíncrona, a comunicação serial utilizando UARTs necessita que tanto o transmissor quanto o receptor estejam configurados para trabalhar na mesma velocidade de comunicação (a chamada baud rate). Uma UART autobaud é um tipo especial de UART que pode se sincronizar automaticamente com o transmissor remoto, adaptando a sua velocidade de comunicação à velocidade de comunicação do transmissor. O código apresentado neste artigo foi escrito especificamente para utilização no meu softcore FPz8, mas poderá ser facilmente adaptado para outras aplicações.

Princípios da comunicação assíncrona

Antes de nos aprofundarmos na construção da nossa UART, é importante entender alguns conceitos básicos sobre a comunicação assíncrona. Primeiramente, vale destacar que a denominação “assíncrona” vem da ausência de sincronismo real entre os dispositivos em comunicação, isto vem se opor à comunicação síncrona utilizada em protocolos como I2C, SPI, etc onde há um sinal de sincronismo (uma linha de relógio ou clock) que garante a sincronização entre os dispositivos durante a comunicação.

Pois bem, em não havendo um sinal de sincronismo, a comunicação assíncrona utiliza um padrão especial para sincronizar os dispositivos e delimitar um novo dado. Este padrão caracteriza-se por uma transição de marca (linha de comunicação em estado de repouso, normalmente nível lógico “1”) para espaço (linha de comunicação em estado ativo, normalmente nível lógico “0”). Os bit são enviados em sequência sendo que cada um possui uma duração fixa e que usualmente segue uma das velocidades padronizadas (300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600 ou 115200 bps).

O primeiro bit é chamado de bit de partida (start bit) e consiste sempre num espaço (nível lógico “0”), seguido por bits de dados (normalmente 8 e iniciando-se pelo bit menos significativo) e por um bit de parada (stop bit) que deve ser sempre uma marca (nível lógico “1”). Note que a linha, quando em estado inativo, deve permanecer sempre em marca (nível lógico alto). A figura 1 mostra um típico quadro assíncrono comumente conhecido como 8N1 (8 bits de dados, nenhum bit de paridade e 1 bit de parada).

Figura 1 – Um típico quadro assíncrono

Observação: é possível inverter os níveis de comunicação serial, fazendo com que marca seja um nível lógico “0” e espaço um nível lógico “1”, neste caso, a partida é sinalizada pela transição de subida da linha!

Implementação em VHDL

A implementação de uma UART é relativamente simples, sendo necessário basicamente dois registradores de deslocamento além de timers para a transmissão e para a recepção de dados. De maneira geral, a transmissão é mais simples de implementar, pois tudo o que é necessário é (em termos de eletrônica digital) um registrador de deslocamento com entrada paralela e saída serial e um sinal de clock na velocidade de comunicação desejada.

No caso da recepção, o processo é um pouco mais complicado, pois é necessário amostrar o sinal preferencialmente na metade do período de cada bit. Por esta razão, nossa implementação utiliza dois tempos: um bit e meio bit. Após a detecção da borda de descida (de marca para espaço), o receptor aguarda meio tempo de bit e então verifica se a linha está em “0”, caracterizando um bit de partida, caso positivo, a cada tempo de bit completo a entrada é amostrada e o sinal é armazenado num registrador (em eletrônica digital seria um registrador de deslocamento com entrada serial e saída paralela). Após a amostragem dos oito bits de dados, o receptor verifica se o próximo bit está em nível “1”, caracterizando um bit de parada. Caso positivo o dado lido é salvo num registrador, caso contrário, ocorreu um erro de recepção, o qual pode ter sido ocasionado por diversos fatores como: velocidades diferentes entre o transmissor e o receptor, ruído, falha de sincronização ou a recepção de um caractere break (caracterizado por manter a linha em nível “0” por um tempo superior a 10 tempos de bit. A figura 2 ilustra a sequencia de amostragens do sinal.

Figura 2 – Amostras do sinal serial tomadas pelo receptor

A UART do FPz8 utiliza dois tipos de dados definidos pelo usuário para especificar o estado do transmissor (Tdbg_uarttxstate) e receptor (Tdbg_uartrxstate) da UART. Além disso, uma estrutura chamada Tdbg_uart contém todas as variáveis (registradores) utilizados pela UART.

Os estados possíveis para o receptor da UART são:

type Tdbg_uartrxstate is (
	DBGST_NOSYNC,			-- UART não está sincronizada com o transmissor
	DBGST_WAITSTART,		-- UART está aguardando o caractere 0x80 para sincronização
	DBGST_MEASURING,		-- UART está medindo a duração do caractere (0x80)
	DBGST_IDLE,			-- UART está sincronizada e aguardando dados
	DBGST_START,			-- UART recebeu um bit de partida
	DBGST_RECEIVING,		-- UART recebendo dados
	DBGST_ERROR			-- UART está em erro de recepção
);

Os estados possíveis para o transmissor da UART são:

type Tdbg_uarttxstate is (
	DBGTX_INIT,			-- UART inicializando
	DBGTX_IDLE,			-- UART aguardando dados para transmissão
	DBGTX_START,			-- UART enviando bit de partida
	DBGTX_TRASMITTING,		-- UART enviando dados
	DBGTX_BREAK, 			-- UART preparando-se para enviar break
	DBGTX_BREAK2			-- UART aguardando break completar
);

A seguir temos a estrutura com as variáveis (registradores) utilizados pela UART. Estão presentes duas variáveis para controlar o estado do transmissor e do receptor, registradores utilizados para o deslocamento dos dados, contadores para controlar o número de bits enviados/recebidos, contadores para o controle da velocidade de transmissão e de recepção, além de flags para sinalização de estado da UART.

type Tdbg_uart is record
	RX_STATE	: Tdbg_uartrxstate;
	TX_STATE	: Tdbg_uarttxstate;
	RX_DONE		: std_logic;				-- new data is available (1 bit)
	TX_EMPTY	: std_logic;				-- tx buffer is empty (1 bit)
	DBG_SYNC	: std_logic;				-- debugger is synchronized to host (1 bit)
	WRT		: std_logic;				-- write/read command flag (1 bit)
	LAST_SMP	: std_logic;				-- last sample read from DBG_RX pin (1 bit)
	SIZE		: std_logic_vector(15 downto 0);	-- 16-bit size of command (16 bits)
	TXSHIFTREG	: std_logic_vector(8 downto 0);		-- transmitter shift register (9 bits)
	RXSHIFTREG	: std_logic_vector(8 downto 0);		-- receiver shift register (9 bits)
	TX_DATA		: std_logic_vector(7 downto 0);		-- TX buffer (8 bits)
	RX_DATA		: std_logic_vector(7 downto 0);		-- RX buffer (8 bits)
	RXCNT		: integer range 0 to 15;		-- received bit counter (4 bits)
	TXCNT		: integer range 0 to 15;		-- transmitted bit counter (4 bits)
	BAUDPRE		: integer range 0 to 2;			-- baud prescaler (2 bits)
	BAUDCNTRX	: std_logic_vector(11 downto 0);	-- RX baud divider (12 bits)
	BAUDCNTTX	: std_logic_vector(11 downto 0);	-- TX baud divider (12 bits)
	BITTIMERX	: std_logic_vector(11 downto 0);	-- RX bit-time register (1/2 bit-time) (12 bits)
	BITTIMETX	: std_logic_vector(11 downto 0);	-- TX bit-time register (12 bits)
end record;

Observe que o bit WRT e a variável SIZE são específicos para a utilização no sistema de depuração do FPz8 e não seriam implementados no uso geral da UART. Note também que o flag RX_DONE é utilizado para sinalizar que um novo dado foi recebido e está disponível no registrador RX_DATA. O bit TX_EMPTY, por sua vez, sinaliza que o registrador de deslocamento de transmissão está vazio e que a aplicação pode escrever um novo dado a ser transmitido utilizando o registrador TX_DATA. Não há proteção contra buffer overrun, ou seja, se um novo dado for recebido sem que o anterior tenha sido lido, o dado anterior é descartado. O mesmo ocorre com o buffer de transmissão. Caso o leitor deseje, a implementação destes controles é simples e pode ser feita sem muito esforço.

Os registradores BITTIMETX e BITTIMERX controlam a duração dos bits (BITTIMETX armazena a duração de um bit completo ao passo que BITTIMERX armazena a duração de meio tempo de bit).

O código do transmissor é bastante simples e está apresentado na listagem abaixo.

case DBG_UART.TX_STATE is
	when DBGTX_INIT =>
		DBG_UART.TX_EMPTY := '1';
		DBG_UART.TX_STATE:=DBGTX_IDLE;
	when DBGTX_IDLE =>	-- UART is idle and not transmitting
		DBG_TX <= '1';
		if (DBG_UART.TX_EMPTY='0' and DBG_UART.DBG_SYNC='1') then	-- there is new data in TX_DATA register
			DBG_UART.BAUDCNTTX:=x"000";
			DBG_UART.TX_STATE := DBGTX_START;
		end if;
	when DBGTX_START =>
		if (DBG_UART.BAUDCNTTX=DBG_UART.BITTIMETX) then
			DBG_UART.BAUDCNTTX:=x"000";
			DBG_UART.TXSHIFTREG := '1'&DBG_UART.TX_DATA;
			DBG_UART.TXCNT := 10;
			DBG_UART.TX_STATE := DBGTX_TRASMITTING;
			DBG_TX <= '0';
		end if;
	when DBGTX_TRASMITTING =>	-- UART is shifting data
		if (DBG_UART.BAUDCNTTX=DBG_UART.BITTIMETX) then
			DBG_UART.BAUDCNTTX:=x"000";
			DBG_TX <= DBG_UART.TXSHIFTREG(0);
			DBG_UART.TXSHIFTREG := '1'&DBG_UART.TXSHIFTREG(8 downto 1);
			DBG_UART.TXCNT :=DBG_UART.TXCNT - 1;
			if (DBG_UART.TXCNT=0) then 
				DBG_UART.TX_STATE:=DBGTX_IDLE;
				DBG_UART.TX_EMPTY := '1';
			end if;
		end if;
	when DBGTX_BREAK =>					
		DBG_UART.BAUDCNTTX:=x"000";
		DBG_UART.TX_STATE:=DBGTX_BREAK2;
	when DBGTX_BREAK2 =>
		DBG_TX <= '0';
		DBG_UART.RX_STATE := DBGST_NOSYNC;
		if (DBG_UART.BAUDCNTTX=x"FFF") then	
			DBG_UART.TX_STATE:=DBGTX_INIT;
		end if;
end case;

No caso do receptor, existem alguns detalhes que valem a pena comentar:

  1. A entrada DBG_RX por sua natureza assíncrona (relativamente ao FPGA) deve obrigatoriamente ser amostrada através de um ou dois flip-flops (nós utilizamos dois) de forma a garantir que a sua amostragem seja feita em perfeita sincronia com o clock do sistema. Coisas absolutamente estranhas podem acontecer se você não sincronizar entradas externas com o clock interno! A razão disso não é óbvia mas também não é muito difícil de entender: FPGAs são como blocos de montar lógicos, mas não implementam necessariamente um circuito lógico digital da forma como conhecemos. De fato, como FPGAs são constituídos de blocos lógicos (que normalmente incluem uma tabela de pesquisa ou look up table – LUT e um registrador) e de uma matriz de interconexão entre estes blocos, o resultado final é que mesmo circuitos lógicos mais simples podem utilizar diversos caminhos e blocos, resultando em diferentes atrasos e fazendo com que um sinal assíncrono possa ser interpretado de diversas formas conforme o arranjo lógico interno no FPGA. Um sinal assíncrono injetado num arranjo lógico complexo de um FPGA pode resultar nos mais diversos efeitos, inclusive levando o circuito a estados “impossíveis”! Por esta razão, é muito importante sincronizar os sinais externos antes de aplicá-los ao circuito lógico da sua aplicação. No caso em tela, a entrada DBG_RX é amostrada em RXSYNC2 que por sua vez é amostrada em RXSYNC1 que é o sinal lido pela UART.
  2. A detecção do bit de partida consiste essencialmente na detecção de uma borda de descida na entrada de recepção da UART. Para isso, utilizamos um flip-flop adicional responsável por armazenar o estado anterior do sinal recebido. Assim, a comparação do sinal atual e do sinal anterior permite detectar facilmente uma borda de descida (ou subida se fosse o caso)! O “if” responsável por isso está dentro do estado DBGST_IDLE no código a seguir.
DBG_UART.BAUDPRE := DBG_UART.BAUDPRE+1;	-- baudrate prescaler
if (DBG_UART.BAUDPRE=0) then 
	DBG_UART.BAUDCNTRX := DBG_UART.BAUDCNTRX+1;
	DBG_UART.BAUDCNTTX := DBG_UART.BAUDCNTTX+1;
end if;
RXSYNC2 <= DBG_RX;		-- DBG_RX input synchronization
RXSYNC1 <= RXSYNC2;		-- RXSYNC1 is a synchronized DBG_RX signal
case DBG_UART.RX_STATE is
	when DBGST_NOSYNC =>
		DBG_UART.DBG_SYNC := '0';
		DBG_UART.RX_DONE := '0';
		DBG_CMD := DBG_WAIT_CMD;
		DBG_UART.RX_STATE := DBGST_WAITSTART;
	when DBGST_WAITSTART =>
		if (RXSYNC1='0' and DBG_UART.LAST_SMP='1') then
			DBG_UART.RX_STATE := DBGST_MEASURING;
			DBG_UART.BAUDCNTRX := x"000";
		end if;
	when DBGST_MEASURING =>
		if (DBG_UART.BAUDCNTRX/=x"FFF") then 
			if (RXSYNC1='1') then
				DBG_UART.DBG_SYNC := '1';
				DBG_UART.RX_STATE := DBGST_IDLE;
				DBG_UART.BITTIMERX := "0000"&DBG_UART.BAUDCNTRX(11 downto 4);
				DBG_UART.BITTIMETX := "000"&DBG_UART.BAUDCNTRX(11 downto 3);
			end if;
		else
			DBG_UART.RX_STATE := DBGST_NOSYNC;
		end if;
	when DBGST_IDLE =>
		DBG_UART.BAUDCNTRX:=x"000";
		DBG_UART.RXCNT:=0;
		if (RXSYNC1='0' and DBG_UART.LAST_SMP='1') then	-- it is a start bit
			DBG_UART.RX_STATE := DBGST_START;				
		end if;
	when DBGST_START =>
		if (DBG_UART.BAUDCNTRX=DBG_UART.BITTIMERX) then
			DBG_UART.BAUDCNTRX:=x"000";
			if (RXSYNC1='0') then
				DBG_UART.RX_STATE := DBGST_RECEIVING;
			else
				DBG_UART.RX_STATE := DBGST_ERROR;
				DBG_UART.TX_STATE := DBGTX_BREAK;
			end if;
		end if;
	when DBGST_RECEIVING =>
		if (DBG_UART.BAUDCNTRX=DBG_UART.BITTIMETX) then
			DBG_UART.BAUDCNTRX:=x"000";
			-- one bit time elapsed, sample RX input
			DBG_UART.RXSHIFTREG := RXSYNC1 & DBG_UART.RXSHIFTREG(8 downto 1);
			DBG_UART.RXCNT := DBG_UART.RXCNT + 1;
			if (DBG_UART.RXCNT=9) then
				if (RXSYNC1='1') then
					-- if the stop bit is 1, rx is completed ok
					DBG_UART.RX_DATA := DBG_UART.RXSHIFTREG(7 downto 0);
					DBG_UART.RX_DONE := '1';
					DBG_UART.RX_STATE := DBGST_IDLE;
				else
					-- if the stop bit is 0, it is a break char, reset receiver
					DBG_UART.RX_STATE := DBGST_ERROR;
					DBG_UART.TX_STATE := DBGTX_BREAK;
				end if;
			end if;
		end if;
	when others =>
end case;
DBG_UART.LAST_SMP := RXSYNC1;

Detecção automática da velocidade

Por fim, antes de encerrarmos, ainda falta explicar como ocorre a detecção automática da velocidade! Pois bem, a UART aqui demonstrada utiliza o caractere 0x80 para a sincronização. Isto significa que após um reset ou após receber um BREAK, o primeiro dado que o transmissor remoto (aquele que está enviando dados para esta UART) deverá enviar será um caractere 0x80. A nossa UART irá medir o período do sinal e calcular a velocidade de comunicação a partir do mesmo! Mas porquê 0x80? Muitas UARTs dotadas da funcionalidade de autobaud utilizam caracteres como 0x55 (pois o mesmo consiste numa sequencia alternada de “0s” e “1s”), mas a Zilog (no caso do Z8 encore que foi a base do FPz8) escolheu o caractere 0x80, uma vez que ele consiste numa grande sequencia de “0s” (de fato, o caractere 0x80 consiste numa sequencia de oito zeros, se incluirmos o start), o que inclusive facilita a medição do período e aumenta a precisão final!

Sendo assim, após um reset a UART vai automaticamente para o estado DBGST_NOSYNC e em seguida para DBGST_WAITSTART onde ela aguarda por uma borda de descida (o início do suposto caractere de sincronização). Ao detectar esta borda, o contador BAUDCNTRX é zerado e a UART passa para o estado de medição, aguardando que a linha de comunicação retorne ao nível lógico alto. Feito isso, o registrador BITTIMETX recebe a contagem dividida por 8, ou seja, o tempo de cada bit e o registrador BITTIMERX recebe a contagem dividida por 16, ou seja, o tempo de meio bit! Após isso a UART está configurada para a velocidade correta e somente retornará ao modo de medição através de um reset ou se receber um caractere BREAK (ou seja, se a linha de comunicação permanecer em nível lógico “0” por mais do que 10 vezes o tempo de bit)!

Por hora é isso, o código completo desta UART está dentro do projeto do FPz8, espero que ela possa servir de inspiração para qualquer um que esteja precisando ou estudando UARTs!

Referências

Leave a Reply