Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial programming >> VHDL

Como fazer um AXI FIFO na RAM do bloco usando o handshake pronto/válido


Fiquei um pouco incomodado com as peculiaridades da interface AXI na primeira vez que tive que criar lógica para interfacear um módulo AXI. Em vez dos sinais de controle regulares ocupado/válido, cheio/válido ou vazio/válido, a interface AXI usa dois sinais de controle denominados “pronto” e “válido”. Minha frustração logo se transformou em admiração.

A interface AXI possui controle de fluxo integrado sem usar sinais de controle adicionais. As regras são fáceis de entender, mas existem algumas armadilhas que devem ser consideradas ao implementar a interface AXI em um FPGA. Este artigo mostra como criar um AXI FIFO em VHDL.

AXI resolve o problema do atraso de um ciclo


Prevenir a leitura e a sobregravação é um problema comum ao criar interfaces de fluxo de dados. O problema é que quando dois módulos lógicos com clock se comunicam, cada módulo só poderá ler as saídas de sua contraparte com um atraso de um ciclo de clock.



A imagem acima mostra o diagrama de temporização de um módulo sequencial gravando em um FIFO que usa o write enable/full esquema de sinalização. Um módulo de interface grava dados no FIFO afirmando o wr_en sinal. O FIFO irá afirmar o full sinaliza quando não há espaço para outro elemento de dados, solicitando que a fonte de dados pare de gravar.

Infelizmente, o módulo de interface não tem como parar no tempo enquanto estiver usando apenas lógica com clock. O FIFO aumenta o full bandeira exatamente na borda ascendente do relógio. Simultaneamente, o módulo de interface tenta escrever o próximo elemento de dados. Ele não pode amostrar e reagir ao full sinal antes que seja tarde demais.

Uma solução é incluir um almost_empty extra sinal, fizemos isso no tutorial Como criar um buffer de anel FIFO em VHDL. O sinal adicional precede o empty sinal, dando ao módulo de interface tempo para reagir.

O handshake pronto/válido


O protocolo AXI implementa o controle de fluxo usando apenas dois sinais de controle em cada direção, um chamado ready e o outro valid . O ready sinal é controlado pelo receptor, um '1' lógico valor neste sinal significa que o receptor está pronto para aceitar um novo item de dados. O valid sinal, por outro lado, é controlado pelo emissor. O remetente deve definir valid para '1' quando os dados apresentados no barramento de dados são válidos para amostragem.

Aqui vem a parte importante: a transferência de dados só acontece quando ambos ready e valid são '1' no mesmo ciclo de clock. O receptor informa quando está pronto para aceitar os dados, e o remetente simplesmente coloca os dados lá fora quando tem algo para transmitir. A transferência ocorre quando ambos concordam, quando o remetente está pronto para enviar e o destinatário está pronto para receber.



A forma de onda acima mostra um exemplo de transação de um item de dados. A amostragem ocorre na borda ascendente do clock, como geralmente é o caso da lógica com clock.

Implementação


Há muitas maneiras de implementar um AXI FIFO em VHDL. Pode ser um registrador de deslocamento, mas usaremos uma estrutura de buffer em anel porque é a maneira mais direta de criar um FIFO na RAM do bloco. Você pode criar tudo em um processo gigante usando variáveis ​​e sinais, ou pode dividir a funcionalidade em vários processos.

Essa implementação usa processos separados para a maioria dos sinais que precisam ser atualizados. Apenas os processos que precisam ser síncronos são sensíveis ao clock, os demais usam lógica combinacional.




A entidade


A declaração de entidade inclui uma porta genérica que é usada para definir a largura das palavras de entrada e saída, bem como o número de slots para reservar espaço na RAM. A capacidade do FIFO é igual à profundidade da RAM menos um. Um slot é sempre mantido vazio para distinguir entre um FIFO cheio e um vazio.
entity axi_fifo is
  generic (
    ram_width : natural;
    ram_depth : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- AXI input interface
    in_ready : out std_logic;
    in_valid : in std_logic;
    in_data : in std_logic_vector(ram_width - 1 downto 0);

    -- AXI output interface
    out_ready : in std_logic;
    out_valid : out std_logic;
    out_data : out std_logic_vector(ram_width - 1 downto 0)
  );
end axi_fifo; 

Os dois primeiros sinais na declaração de porta são as entradas de clock e reset. Esta implementação usa reset síncrono e é sensível à borda de subida do relógio.

Há uma interface de entrada estilo AXI usando os sinais de controle prontos/válidos e um sinal de dados de entrada de largura genérica. Por fim, vem a interface de saída AXI com sinais semelhantes aos da entrada, apenas com direções invertidas. Os sinais pertencentes à interface de entrada e saída são prefixados com in_ ou out_ .

A saída de um AXI FIFO pode ser conectada diretamente à entrada de outro, as interfaces se encaixam perfeitamente. Embora, uma solução melhor do que empilhá-los seria aumentar o ram_depth genérico se você quiser um FIFO maior.

Declarações de sinal


As duas primeiras instruções na região declarativa do arquivo VHDL declaram o tipo de RAM e seu sinal. A RAM é dimensionada dinamicamente a partir das entradas genéricas.
-- The FIFO is full when the RAM contains ram_depth - 1 elements
type ram_type is array (0 to ram_depth - 1)
  of std_logic_vector(in_data'range);
signal ram : ram_type;

O segundo bloco de código declara um novo subtipo inteiro e quatro sinais dele. O index_type é dimensionado para representar exatamente a profundidade da RAM. O head sinal sempre indica o slot de RAM que será usado na próxima operação de gravação. O tail O sinal aponta para o slot que será acessado na próxima operação de leitura. O valor do count sinal é sempre igual ao número de elementos atualmente armazenados no FIFO, e count_p1 é uma cópia do mesmo sinal atrasada em um ciclo de clock.
-- Newest element at head, oldest element at tail
subtype index_type is natural range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal count : index_type;
signal count_p1 : index_type;

Em seguida, vêm dois sinais chamados in_ready_i e out_valid_i . Estas são apenas cópias das saídas da entidade in_ready e out_valid . O _i postfix significa apenas interno , faz parte do meu estilo de codificação.
-- Internal versions of entity signals with mode "out"
signal in_ready_i : std_logic;
signal out_valid_i : std_logic;

Por fim, declaramos um sinal que será usado para indicar uma leitura e uma escrita simultâneas. Vou explicar o seu propósito mais tarde neste artigo.
-- True the clock cycle after a simultaneous read and write
signal read_while_write_p1 : std_logic;

Subprogramas


Após os sinais, declaramos uma função para incrementar nosso index_type personalizado . O next_index função olha para o read e valid parâmetros para determinar se há uma transação de leitura ou leitura/gravação em andamento. Se for esse o caso, o índice será incrementado ou encapsulado. Caso contrário, o valor do índice inalterado é retornado.
function next_index(
  index : index_type;
  ready : std_logic;
  valid : std_logic) return index_type is
begin
  if ready = '1' and valid = '1' then
    if index = index_type'high then
      return index_type'low;
    else
      return index + 1;
    end if;
  end if;

  return index;
end function;

Para nos poupar de digitação repetitiva, criamos a lógica para atualizar o head e tail sinais em um procedimento, em vez de dois processos idênticos. O update_index procedimento pega o relógio e os sinais de reset, um sinal de index_type , um ready sinal e um valid sinal como entradas.
procedure index_proc(
  signal clk : in std_logic;
  signal rst : in std_logic;
  signal index : inout index_type;
  signal ready : in std_logic;
  signal valid : in std_logic) is
begin
    if rising_edge(clk) then
      if rst = '1' then
        index <= index_type'low;
      else
        index <= next_index(index, ready, valid);
      end if;
    end if;
end procedure;

Este processo totalmente síncrono usa o next_index função para atualizar o index sinal quando o módulo está fora de reset. Quando em redefinição, o index sinal será definido para o valor mais baixo que pode representar, que é sempre 0 devido a como index_type e ram_type é declarado. Poderíamos ter usado 0 como o valor de redefinição, mas tento o máximo possível evitar a codificação.

Copiar sinais internos para a saída


Essas duas instruções simultâneas copiam as versões internas dos sinais de saída para as saídas reais. Precisamos operar em cópias internas porque o VHDL não nos permite ler sinais de entidade com o modo out dentro do módulo. Uma alternativa seria declarar in_ready e out_valid com modo inout , mas a maioria dos padrões de codificação da empresa restringe o uso de inout sinais de entidade.
in_ready <= in_ready_i;
out_valid <= out_valid_i;

Atualize a cabeça e a cauda


Já discutimos o index_proc procedimento que é usado para atualizar o head e tail sinais. Ao mapear os sinais apropriados para os parâmetros deste subprograma, obtemos o equivalente a dois processos idênticos, um para controlar a entrada FIFO e outro para a saída.
-- Update head index on write
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid);

-- Update tail index on read
PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);

Como o head e o tail forem definidos para o mesmo valor pela lógica de reset, o FIFO estará vazio inicialmente. É assim que esse ring buffer funciona, quando ambos estão apontando para o mesmo índice significa que o FIFO está vazio.

Inferir RAM do bloco


Na maioria das arquiteturas FPGA, as primitivas de bloco de RAM são componentes totalmente síncronos. Isso significa que, se quisermos que a ferramenta de síntese infira a RAM do bloco de nosso código VHDL, precisamos colocar as portas de leitura e gravação dentro de um processo com clock. Além disso, não pode haver valores de reinicialização associados à RAM do bloco.
PROC_RAM : process(clk)
begin
  if rising_edge(clk) then
    ram(head) <= in_data;
    out_data <= ram(next_index(tail, out_ready, out_valid_i));
  end if;
end process;

Não há habilitação de leitura ou habilitar gravação aqui, isso seria muito lento para o AXI. Em vez disso, estamos continuamente gravando no slot de RAM apontado pelo head índice. Então, quando determinamos que uma transação de gravação ocorreu, simplesmente avançamos o head para bloquear o valor escrito.

Da mesma forma, out_data é atualizado a cada ciclo de clock. O tail ponteiro simplesmente se move para o próximo slot quando ocorre uma leitura. Observe que o next_index função é usada para calcular o endereço para a porta de leitura. Temos que fazer isso para garantir que a RAM reaja rápido o suficiente após uma leitura e comece a gerar o próximo valor.

Conte o número de elementos no FIFO


Contar o número de elementos na RAM é simplesmente uma questão de subtrair o head do tail . Se o head foi empacotado, temos que compensá-lo pelo número total de slots na RAM. Temos acesso a essas informações através do ram_depth constante da entrada genérica.
PROC_COUNT : process(head, tail)
begin
  if head < tail then
    count <= head - tail + ram_depth;
  else
    count <= head - tail;
  end if;
end process;

Também precisamos acompanhar o valor anterior do count sinal. O processo abaixo cria uma versão dele que está atrasada em um ciclo de clock. O _p1 postfix é uma convenção de nomenclatura para indicar isso.
PROC_COUNT_P1 : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      count_p1 <= 0;
    else
      count_p1 <= count;
    end if;
  end if;
end process;

Atualize o pronto saída


O in_ready sinal deve ser '1' quando este módulo estiver pronto para aceitar outro item de dados. Este deve ser o caso enquanto o FIFO não estiver cheio, e é exatamente isso que diz a lógica desse processo.
PROC_IN_READY : process(count)
begin
  if count < ram_depth - 1 then
    in_ready_i <= '1';
  else
    in_ready_i <= '0';
  end if;
end process;

Detectar leitura e gravação simultâneas


Por causa de um caso de canto que explicarei na próxima seção, precisamos ser capazes de identificar operações simultâneas de leitura e gravação. Sempre que houver transações de leitura e gravação válidas durante o mesmo ciclo de clock, esse processo definirá o read_while_write_p1 sinal para '1' no ciclo de clock seguinte.
PROC_READ_WHILE_WRITE_P1: process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      read_while_write_p1 <= '0';

    else
      read_while_write_p1 <= '0';
      if in_ready_i = '1' and in_valid = '1' and
        out_ready = '1' and out_valid_i = '1' then
        read_while_write_p1 <= '1';
      end if;
    end if;
  end if;
end process;

Atualize o válido saída


O out_valid sinal indica aos módulos downstream que os dados apresentados em out_data é válido e pode ser amostrado a qualquer momento. O out_data sinal vem diretamente da saída RAM. Implementando o out_valid sinal é um pouco complicado por causa do atraso do ciclo de clock extra entre a entrada e a saída da RAM do bloco.

A lógica é implementada em um processo combinacional para que possa reagir sem atraso à mudança do sinal de entrada. A primeira linha do processo é um valor padrão que define o out_valid sinal para '1' . Este será o valor predominante se nenhuma das duas instruções If subsequentes for acionada.
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1)
begin
  out_valid_i <= '1';

  -- If the RAM is empty or was empty in the prev cycle
  if count = 0 or count_p1 = 0 then
    out_valid_i <= '0';
  end if;

  -- If simultaneous read and write when almost empty
  if count = 1 and read_while_write_p1 = '1' then
    out_valid_i <= '0';
  end if;

end process;

A primeira instrução If verifica se o FIFO está vazio ou estava vazio no ciclo de clock anterior. Obviamente, o FIFO está vazio quando há 0 elementos nele, mas também precisamos examinar o nível de preenchimento do FIFO no ciclo de clock anterior.

Considere a forma de onda abaixo. Inicialmente, o FIFO está vazio, conforme indicado pelo count sinal sendo 0 . Em seguida, ocorre uma gravação no terceiro ciclo de clock. O slot 0 da RAM é atualizado no próximo ciclo de clock, mas leva um ciclo adicional antes que os dados apareçam no out_data resultado. O objetivo do or count_p1 = 0 é garantir que out_valid permanece '0' (circulado em vermelho) enquanto o valor se propaga pela RAM.



A última instrução If protege contra outro caso de canto. Acabamos de falar sobre como lidar com o caso especial de write-on-vazio verificando os níveis de preenchimento FIFO atuais e anteriores. Mas o que acontece se fizermos uma leitura e gravação simultâneas quando count já é 1 ?

A forma de onda abaixo mostra tal situação. Inicialmente, há um item de dados D0 presente no FIFO. Está lá há algum tempo, então tanto count e count_p1 são 0 . Em seguida, uma leitura e gravação simultâneas ocorrem no terceiro ciclo de clock. Um item sai do FIFO e um novo entra nele, tornando os contadores inalterados.



No momento da leitura e escrita, não há próximo valor na RAM pronto para ser emitido, como teria ocorrido se o nível de preenchimento fosse maior que um. Temos que esperar dois ciclos de clock antes que o valor de entrada apareça na saída. Sem qualquer informação adicional, seria impossível detectar este caso de canto e o valor de out_valid no ciclo de clock seguinte (marcado como vermelho sólido) seria erroneamente definido como '1' .

É por isso que precisamos do read_while_write_p1 sinal. Ele detecta que houve uma leitura e gravação simultâneas, e podemos levar isso em consideração definindo out_valid para '0' nesse ciclo de clock.

Sintetizando no Vivado


Para implementar o design como um módulo autônomo no Xilinx Vivado, primeiro temos que dar valores às entradas genéricas. Isso pode ser feito no Vivado usando as ConfiguraçõesGeralGenéricos/Parâmetros menu, como mostra a imagem abaixo.



Os valores genéricos foram escolhidos para corresponder ao primitivo RAMB36E1 na arquitetura Xilinx Zynq, que é o dispositivo de destino. O uso de recursos pós-implementação é mostrado na imagem abaixo. O AXI FIFO usa um bloco de RAM e um pequeno número de LUTs e flip-flops.


AXI está mais do que pronto/válido


AXI significa Advanced eXtensible Interface, é parte do padrão Advanced Microcontroller Bus Architecture (AMBA) da ARM. O padrão AXI é muito mais do que o handshake de leitura/válido. Se você quiser saber mais sobre o AXI, recomendo estes recursos para leitura adicional:





VHDL

  1. Nuvem e como ela está mudando o mundo da TI
  2. Como aproveitar ao máximo seus dados
  3. Como inicializar a RAM do arquivo usando TEXTIO
  4. Como você se prepara para IA usando IoT
  5. Como a Internet Industrial está mudando o gerenciamento de ativos
  6. Práticas recomendadas de rastreamento de ativos:como aproveitar ao máximo seus dados de ativos arduamente conquistados
  7. Como podemos obter uma imagem melhor da IoT?
  8. Como aproveitar ao máximo a IoT no ramo de restaurantes
  9. Como os dados estão habilitando a cadeia de suprimentos do futuro
  10. Como tornar os dados da cadeia de suprimentos confiáveis ​​