Introdução a FPGA
em
Esta é a primeira parte do guia de programação para FPGAs! Este guia irá virar eventualmente um verilog4noobs
para qualquer pessoa que quiser iniciar na área de programação de hardware possa ter um jeito fácil de conseguir! Iremos começar a explicar o que é um FPGA e como ele funciona.
Para quem preferir, este artigo foi feito em base na Livestream sobre Verilog que fiz a um tempo atrás e está disponível no YouTube: https://www.youtube.com/watch?v=BcKwqju5gxA
O que é um FPGA
FPGA é uma abreviatura para Field Programmable Gate Array, ou Matriz de Portas Programáveis em Campo. O termo campo
usado aqui se refere ao fato de que o chip pode ser programado após sair da fábrica. O termo portas
se referem a portas lógicas.
Alguns exemplos de portas lógicas:
Portas Lógicas - Remix de https://commons.wikimedia.org/wiki/File:Circuit_elements.svg
Portas Lógicas
As portas lógicas efetuam operações lógicas entre bits, e podem ter N entradas e uma saída. O FPGA é uma matriz de portas lógicas as quais podem ser interligadas para gerar circuitos lógicos equivalentes a qualquer chip desejado. Portas lógicas são blocos bem simples, e geralmente podem ser representados com poucos transístores. Por exemplo, dado A
, B
entradas e Q
saída. Podemos implementar portas NOT, NAND e NOR destas maneiras:
Representação em transístores bipolares de uma Porta Inversora (NOT)
Representação em transístores bipolares de uma Porta Não-E (NAND)
Representação em transístores bipolares de uma Porta Não-OU (NOR)
As operações lógicas realizadas pelas portas lógicas descritas seguem a seguinte tabela da verdade:
Porta E | |||
A | B | A & B | |
---|---|---|---|
0 | 0 | 0 | |
0 | 1 | 0 | |
1 | 0 | 0 | |
1 | 1 | 1 |
Porta OU | |||
A | B | A | B | |
---|---|---|---|
0 | 0 | 0 | |
0 | 1 | 1 | |
1 | 0 | 1 | |
1 | 1 | 1 |
Porta Inversora | ||
A | ~A | |
---|---|---|
0 | 1 | |
1 | 0 |
Porém você nunca sabe especificamente qual porta irá usar em qual posição do FPGA. Por essa dúvida, seria nescessário cada bloco
do FPGA conter todos os tipos básicos de portas lógicas para que na hora de ligar, pudesse escolher a porta correta. Isso tornaria o chip ineficiente pois de N portas que um bloco teria, você apenas usaria uma.
Felizmente o FPGA não usa portas lógicas na sua maneira primitiva. Ao invés disso ele usa uma “porta programável” feita com um circuito lógico chamado multiplexador.
O que é um Multiplexador
Um multiplexador é uma unidade lógica de N entradas com apenas uma saída, e log2(N) entradas de controle. Abaixo segue um exemplo de um multiplexador de 8 entradas.
*
Neste multiplexador temos:
-
X0-X7
=> Entrada de dados -
A, B, C
=> Entrada de controle -
O
=> Saída
As entradas A, B e C formam um número de 3 bits que representam qual entrada X estará ligada a saída O.
MUX8 | ||||
A | B | C | O | |
---|---|---|---|---|
0 | 0 | 0 | X0 | |
0 | 0 | 1 | X1 | |
0 | 1 | 0 | X2 | |
0 | 1 | 1 | X3 | |
1 | 0 | 0 | X4 | |
1 | 0 | 1 | X5 | |
1 | 1 | 0 | X6 | |
1 | 1 | 1 | X7 |
Com a sua contra-parte (demultiplexador ou demux), é possível transferir vários canais de dados em apenas um canal, desde que ambos Mux e Demux estejam com os valores A,B,C corretamente configurados. Porém para FPGA’s MUX geralmente são usados de maneiras diferentes. Em lógica digital, você pode usar um MUX para implementar qualquer porta lógica associando as entradas do mux a valores pré-definidos.
Implementando portas lógicas com Multiplexador
A implementação de portas lógicas pode parecer complexa, mas para portas simples (NOT, OR, AND) é bem simples de entender o funcionamento. Vamos usar um MUX de 2 entradas (1 bit).
*
Neste caso a tabela da verdade é bem mais simples:
MUX2 | |||
A | O | ||
---|---|---|---|
0 | X0 | ||
1 | X1 |
De modo que se quisermos que o MUX2 vire uma porta inversora, podemos apenas pré-configurar as entradas X0 e X1 com os valores 1 e 0.
MUX2-NOT | |||||
X0 | X1 | A | O porta | O valor | |
---|---|---|---|---|---|
1 | 0 | 0 | X0 | 1 | |
1 | 0 | 1 | X1 | 0 |
Caso queiramos fazer uma porta AND, podemos configurar a entrada X0 como 0, e usar a entrada X1 e A como entradas da porta AND
MUX2-AND | |||||
X0 (FIXO) | X1 (AND0) | A (AND1) | O porta | O valor | |
---|---|---|---|---|---|
0 | 0 | 0 | X0 | 0 | |
0 | 0 | 1 | X1 | 0 | |
0 | 1 | 0 | X0 | 0 | |
0 | 1 | 1 | X1 | 1 |
Ou no caso de uma porta OR, fixamos o valor de X1 em 1 e usamos X0 e A como operadores OR.
MUX2-OR | |||||
X0 (OR0) | X1 (FIXO) | A (OR1) | O porta | O valor | |
---|---|---|---|---|---|
0 | 1 | 0 | X0 | 0 | |
0 | 1 | 1 | X1 | 1 | |
1 | 1 | 0 | X0 | 1 | |
1 | 1 | 1 | X1 | 1 |
Existem outras operações que podem ser implementadas usando multiplexadores, inclusive operações mais complexas caso o multiplexador tenha mais entradas. Este uso é frequentemente chamado de Lookup-Table (ou Tabela de Consulta em português). Os detalhes de como criar operações não serão discutidos aqui (porém se quiserem, posso fazer um artigo no futuro sobre :) ), mas com essas informações já conseguimos explicar como funciona as células do FPGA!
“LUT” Lookup-Table dos FPGAs
Cada célula do FPGA contém (em geral) uma LUT (Lookup-Table), um Flip-Flop tipo D e um mux 2:1.
Célula Lógica do FPGA
O flip-flop serve para sincronização dos dados quando a operação é feita sob um domínio de um clock (um sinal de sincronia). Já o MUX 2:1 serve para selecionar se aquela célula será sincronizada com algo ou não (ela alterna entre a saída do flip-flop e a saída direto da LUT).
As LUT’s seguem o princípio da reprogramabilidade do multiplexador como foi comentado na seção anterior, onde cada uma das entradas pode estar ligada a um nível lógico fixo ( 0 ou 1 ) ou a outras células / Pinos de entrada. Cada célula independente pode não fazer muita coisa por sí, porém interligadas a outras células podem fazer praticamente qualquer coisa!
O que é uma HDL
HDL significa Hardware Description Language (ou Linguagem de Descrição de Hardware). Uma HDL serve para abstrair os conceitos de portas lógicas e células do FPGA para um nível onde fique mais fácil de pensar na lógica do programa. Uma HDL é efetivamente traduzida a netlist (lista de nós) de pseudo-LUTs que será usada para preparar o conjunto de dados que irá efetivamente ligar as células dentro do FPGA.
As duas linguagens mais tradicionais são VHDL e Verilog. Ambas são suportadas pela grande maioria de ferramentas de FPGA especificas dos fabricantes e também das de código fonte aberto.
// Exemplo em Verilog
module contador ( // Definição de entradas e saídas do módulo
out , // Saída do contador
enable , // Sinal de ativação do contador
clk , // Sinal de clock
reset // Sinal de reset
);
// ------------- Portas de saída ----------
output [7:0] out; // Saída de 8 bits
// ------------ Portas de Entrada ---------
input enable, clk, reset;
// ------------ Variáveis Internas --------
reg [7:0] out; // Contador de 8 bits, associado a saída out
// ------------- Código começa aqui -------
always @(posedge clk)
if (reset) begin
out <= 8'b0 ;
end else if (enable) begin
out <= out + 1;
end
endmodule
-- Exemplo em VHDL
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
-- Definição de entradas e saídas do módulo
entity contador is
port (
cout :out std_logic_vector (7 downto 0); -- Saída de 8 bits do contador
enable :in std_logic; -- Sinal de ativação do contador
clk :in std_logic; -- Sinal de clock
reset :in std_logic -- Sinal de reset
);
end entity;
-- Definição do funcionamento do módulo
architecture rtl of contador is
signal count :std_logic_vector (7 downto 0);
begin
process (clk, reset) begin
if (reset = '1') then
count <= (others=>'0');
elsif (rising_edge(clk)) then
if (enable = '1') then
count <= count + 1;
end if;
end if;
end process;
cout <= count;
end architecture;
Existe outro processo de geração que usam linguagens de mais alto nível (por exemplo C++ ou Python), este processo se chama High Level Synthesis (Síntese de Alto Nível), onde o código do FPGA é construido programaticamente em uma linguagem de mais alto nível e no fim é gerado um código Verilog / VHDL para sintetização habitual. Alguns dos processos geram diretamente a netlist, porém muitos softwares de empresas de FPGA usam padrões próprios de netlist o que pode tornar um problema o suporte.
Processo de “compilação” (sintentização)
No processo de compilação, ou melhor dizendo, sintetização (que é o termo adequado pra esse processo) um código escrito em HDL se torna uma netlist que será usada para construção do que será o código que ficará na memória do FPGA. Neste processo as entradas e saídas da netlist tem nomes simbólicos que apontam para alguma entrada/saída do chip. A netlist é agnóstica a esses nomes e eles só representaram algo significativo para os processos finais da síntese.
Neste passo são feitas várias otimizações do circuito lógico para que haja o mínimo possível de nós na netlist. Algumas dessas otimizações podem ser feitas assumindo algum FPGA especifico (pelo tamanho de sua LUT). Após a netlist pronta, as ferramentas deverão “achar um jeito de encaixar” o netlist no FPGA alvo. Este passo é chamado Place & Route (Colocar e Rotear)
Place & Route
O processo de place & route é muito similar a quando um engenheiro desenha uma placa de circuito impresso. Você coloca os componentes na placa e liga os fios entre eles. Dependendo do circuito isto pode ser fácil ou difícil. Imagine que quanto mais espaço da placa seus componentes ocupar, mais difícil é de achar um caminho para todas suas trilhas. O mesmo ocorre com o FPGA.
Este passo é sempre especifico do FPGA de destino, onde parâmetro como linhas globais de clock, tamanho de LUT e posição de LUT são levadas em conta. Além disso, algumas ferramentas permitem colocar restrições nos parâmetro de delay, clock mínimo e tensão elétrica para as rotas. Com todas essas informações, o programa de Place & Route irá tentar achar uma configuração válida para aquele FPGA.
Dependendo da complexidade do projeto, o tempo pode ser desde alguns segundos até alguns dias. Além disso pode também não ser possível rotear o seu código no FPGA destino, mesmo que este não ocupe o FPGA inteiro!
Após o processo estar completo, a netlist será incrementada com a posição física dos nós dentro do FPGA, por exemplo:
// Antes do Place & Route
LUT4(0,1,0,1) -> LUT4(1,1,0,0)
// Depois do Place & Route
LUT4_0.0(0,1,0,1) -> LUT4_1.1(1,1,0,0) // one 0.0 e 1.1 são as coordenadas dentro do chip
Após isso seu programa está completo, e o único passo restante é gerar a sequência de bits que irão programar o FPGA para esta configuração!
Geração de Bitstream
A ultima etapa do processo é a geração do bitstream. Este gerador recebe como entrada a netlist complementada com as posições físicas das células do FPGA e como elas se interligam e converte para uma sequência de bits proprietária do FPGA. Cada marca e modelo de FPGA tem uma sequência especifica para programação e sua programação também pode variar do meio de origem.
Por exemplo, um FPGA pode ser programado via:
- “Porta Serial” (Na verdade pino de programação serial)
- JTAG
- Memória Flash SPI
Next steps…
Todos estes processos vão ficar mais claros nos próximos artigos! Esta é a primeira parte de uma série de posts sobre Verilog e programação para FPGA. Farei assim que possível o próximo post :D
Espero que tenham gostado!