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

Verificação aleatória restrita


A verificação aleatória restrita é uma estratégia de testbench que se baseia na geração de transações pseudo-aleatórias para o dispositivo em teste (DUT). O objetivo é alcançar a cobertura funcional de uma série de eventos predefinidos por meio de interação aleatória com o DUT.

A Open Source VHDL Verification Methodology (OSVVM) é uma biblioteca VHDL gratuita que inclui vários pacotes convenientes para criar testbenches aleatórios restritos. Estamos particularmente interessados ​​no RandomPkg e CoveragePck, que usaremos neste artigo. Recomendo visitar a página do GitHub do OSVVM para saber mais sobre os recursos desta biblioteca.

O dispositivo em teste


Vou mergulhar direto em um exemplo para explicar melhor como um testbench aleatório restrito difere do testbench clássico, que usa testes direcionados. Criamos um FIFO de buffer de anel no artigo anterior deste blog, mas não criamos um testbench de autoverificação para verificar a correção do módulo.

Vamos criar um testbench adequado para o FIFO de buffer de anel que usa verificação aleatória restrita.
entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

A entidade do módulo ring buffer é mostrada no código acima. Vamos tratar o DUT como uma caixa preta, o que significa que não assumiremos nenhum conhecimento de como o DUT é implementado. Afinal, este artigo é sobre o testbench, não o FIFO de buffer de anel.

Vamos instanciar o DUT no testbench usando o método de instanciação de entidade. A instanciação é trivial, então omitirei o código por enquanto, mas ele pode ser baixado posteriormente neste artigo.

Os genéricos do DUT serão mapeados para os seguintes valores:

Estratégia do Testbench


Vamos passar pela estratégia de teste antes de começarmos a implementar qualquer coisa. A imagem abaixo mostra o conceito principal do testbench que estamos prestes a criar.



Vamos realizar transações de gravação aleatórias no lado de entrada do DUT. Os dados de entrada serão definidos para um valor aleatório em cada ciclo de clock, e os strobes na entrada de habilitação de gravação terão duração aleatória.

Da mesma forma, realizaremos leituras aleatoriamente. Ativaremos o sinal de habilitação de leitura em rajadas que duram um número aleatório de ciclos de clock.

Haverá um modelo comportamental em paralelo com o DUT. Este é um FIFO que é implementado de forma diferente do buffer de anel que é usado no DUT, mas ainda tem a mesma interface. Ao contrário do DUT, o modelo comportamental não precisa ser sintetizável. Isso nos dá a liberdade de usar recursos avançados de programação VHDL para criá-lo.

Compararemos a saída do DUT com a saída do modelo comportamental em um processo separado. Este processo será o único responsável por fazer esta comparação em cada ciclo de clock usando declarações assert. Se as duas implementações FIFO se comportarem de forma diferente a qualquer momento, uma falha de asserção fará com que a simulação termine com um erro.

Por fim, coletaremos dados de cobertura funcional observando as transações que estão indo e vindo do DUT. Um ponto de cobertura funcional pode significar, por exemplo, uma leitura e gravação simultâneas ou que o FIFO seja preenchido pelo menos uma vez. Vamos monitorar esses eventos em nosso processo principal do sequenciador do testbench. A simulação será interrompida quando ocorrerem todos os eventos de cobertura funcional que estamos monitorando.

Importando a biblioteca OSVVM


A biblioteca OSVVM pode ser usada com qualquer simulador que suporte VHDL-2008. Ele já pode estar incluído nas bibliotecas padrão que acompanham seu simulador. Está incluído no ModelSim PE Student Edition, que pode ser baixado gratuitamente da Mentor Graphics.

O ModelSim vem com uma versão mais antiga do OSVVM, mas tudo bem, ele tem tudo o que precisamos. Podemos simplesmente ir em frente e importar os pacotes aleatórios e de cobertura assim:
library osvvm;
use osvvm.RandomPkg.all;
use osvvm.CoveragePkg.all;

A versão mais recente da biblioteca OSVVM sempre pode ser baixada na página do GitHub. Faça isso se o seu simulador não o tiver incluído ou se você quiser usar os recursos mais recentes da biblioteca.

Declarando as variáveis ​​OSSVM


A biblioteca OSVVM contém pacotes com tipos protegidos. As variáveis ​​criadas a partir delas seriam limitadas em escopo ao processo em que foram definidas. Portanto, vamos declará-las como variáveis ​​compartilhadas na região declarativa da arquitetura testbench, conforme mostrado no código abaixo.
 -- OSVVM variables
  shared variable rv : RandomPType;
  shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType;

O rv variável do tipo RandomPType é para gerar valores aleatórios. Precisamos apenas de um desses, pois podemos usar o mesmo objeto em todos os processos em que precisamos gerar valores aleatórios. A última linha de código declara seis variáveis ​​do tipo CovPType .

Declaramos seis variáveis ​​de cobertura porque teremos seis objetivos de cobertura, vamos nos referir a esses objetos como «bins». As variáveis ​​compartilhadas devem ser inicializadas antes que possam ser usadas para coletar dados de cobertura. Fazemos isso chamando o método AddBins procedimento em cada um dos CovPType caixas.
    -- Set up coverage bins
    bin1.AddBins("Write while empty", ONE_BIN);
    bin2.AddBins("Read while full", ONE_BIN);
    bin3.AddBins("Read and write while almost empty", ONE_BIN);
    bin4.AddBins("Read and write while almost full", ONE_BIN);
    bin5.AddBins("Read without write when almost empty", ONE_BIN);
    bin6.AddBins("Write without read when almost full", ONE_BIN);

Fornecemos uma descrição de string do compartimento de cobertura como o primeiro parâmetro para o AddBins procedimento. Essa string reaparecerá no final da simulação quando imprimirmos as estatísticas para cada um dos compartimentos de cobertura. Como você pode ver nas descrições do texto, vamos usar as caixas para verificar se ocorreram ou não alguns casos de canto muito específicos.

AddBins é um procedimento sobrecarregado que pode ser usado para criar vários scoreboards dentro das variáveis ​​bin. No entanto, teremos apenas um placar associado a cada bin. Portanto, forneceremos a constante de conveniência ONE_BIN como um parâmetro para o AddBins procedimento. Isso inicializará o CovPType variáveis ​​com um bin cada. Os placares representados pelos bins são considerados cobertos quando os eventos que monitoram ocorreram pelo menos uma vez.

Gerando entrada aleatória


Vamos começar criando o processo que gera dados de entrada para o DUT. O buffer de anel FIFO foi projetado para ignorar tentativas de sobregravação e sobreleitura. Portanto, podemos simplesmente escrever dados aleatórios em rajadas de duração aleatória. Não precisamos pensar se o DUT está realmente pronto para absorver os dados ou não.
  PROC_WRITE : process
  begin
    wr_en <= rv.RandSlv(1)(1) and not rst;

    for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop
      wr_data <= rv.RandSlv(RAM_WIDTH);
      wait until rising_edge(clk);
    end loop;
  end process;

A única consideração que este processo leva é que o DUT não está em reset. Habilitamos ou desabilitamos aleatoriamente o sinal de habilitação de gravação na primeira linha deste processo, mas ele só será habilitado se rst é '0' .

O loop for subsequente gravará dados aleatórios no DUT por um número aleatório de ciclos de clock, mesmo que o sinal de habilitação não esteja ativo. Podemos fazer isso porque o DUT deve ignorar o wr_data porta, a menos que o wr_en sinal é '1' . Após o loop for, o programa retornará ao início do processo, acionando outra transação de gravação aleatória.

Realizando leituras aleatórias


O processo que lê os dados do DUT é semelhante ao processo de gravação. Podemos ativar aleatoriamente o rd_en sinal a qualquer momento porque o DUT foi projetado para ignorar tentativas de leitura quando vazio. Os dados que aparecem no rd_data a porta não está realmente verificada. Este processo controla apenas o sinal de habilitação de leitura.
  PROC_READ : process
  begin
    rd_en <= rv.RandSlv(1)(1) and not rst;

    for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop
      wait until rising_edge(clk);
    end loop;
  end process;

Verificação comportamental


Construiremos um modelo comportamental do DUT em nosso testbench para verificar seu comportamento. Esta é uma estratégia de teste bem conhecida. Primeiro, alimentamos o modelo comportamental simultaneamente com a mesma entrada do DUT. Então, podemos comparar a saída dos dois para verificar se o DUT tem o comportamento correto.



A imagem acima mostra o layout básico de tal testbench. O modelo comportamental funciona em paralelo com o DUT. Nós o usamos como um modelo para verificar as saídas do DUT.

O FIFO do banco de teste


Usaremos uma lista encadeada para implementar o modelo comportamental. Listas vinculadas não podem ser sintetizadas, mas são perfeitas para testbenches. Você pode se lembrar do Como criar uma lista vinculada em VHDL artigo se você é um leitor regular deste blog. Usaremos o código dele para implementar o modelo comportamental para o buffer de anel FIFO.
package DataStructures is
   type LinkedList is protected
 
      procedure Push(constant Data : in integer);
      impure function Pop return integer;
      impure function IsEmpty return boolean;
 
   end protected;
end package DataStructures;

A declaração do pacote para o FIFO Linked List é mostrada no código acima. É um tipo protegido que possui as três funções; Push, Pop e IsEmpty. Eles são usados ​​para adicionar e remover elementos do FIFO, bem como para verificar se há zero elementos nele.
  -- Testbench FIFO that emulates the DUT
  shared variable fifo : LinkedList;

Os tipos protegidos são construções semelhantes a classes em VHDL. Criaremos um objeto da lista encadeada declarando uma variável compartilhada na região declarativa do testbench, conforme mostrado no código acima.

O modelo comportamental


Para emular totalmente o comportamento do ring buffer FIFO, declaramos dois novos sinais que espelham os sinais de saída do DUT. O primeiro sinal contém os dados de saída do modelo comportamental e o segundo é o sinal válido associado.
  -- Testbench FIFO signals
  signal fifo_out : integer;
  signal fifo_out_valid : std_logic := '0';

O código acima mostra a declaração dos dois sinais de saída do modelo comportamental. Não precisamos de nenhum sinal de entrada dedicado para o modelo comportamental, porque eles são os mesmos que estão conectados ao DUT. Estamos usando sinais para emular a saída do DUT porque isso nos permite coletar dados de cobertura facilmente, como veremos mais adiante neste artigo.
PROC_BEHAVIORAL_MODEL : process
begin
  wait until rising_edge(clk) and rst = '0';

  -- Emulate a write
  if wr_en = '1' and full = '0' then
    fifo.Push(to_integer(unsigned(wr_data)));
    report "Push " & integer'image(to_integer(unsigned(wr_data)));
  end if;
    
  -- Emulate a read
  if rd_en = '1' and empty = '0' then
    fifo_out <= fifo.Pop;
    fifo_out_valid <= '1';
  else
    fifo_out_valid <= '0';
  end if;
  
end process;

O processo que implementa o modelo comportamental do ring buffer FIFO é mostrado no código acima. Este processo será acionado em cada borda de subida do clock, se o sinal de reset não estiver ativo.

O modelo comportamental envia um novo valor para o FIFO do testbench sempre que o wr_en sinal é afirmado enquanto o full sinal é '0' . Da mesma forma, a lógica de leitura no processo de modelo comportamental funciona ouvindo o rd_en e empty sinais. Este último é controlado pelo DUT, mas verificaremos se está funcionando no próximo processo que criaremos.

Verificando as saídas


Toda a lógica responsável por verificar as saídas do DUT é reunida em um processo denominado «PROC_VERIFY». Estamos usando declarações assert para verificar se as saídas do DUT correspondem às do modelo comportamental. Também verificamos se o DUT e o modelo comportamental concordam quando o FIFO está vazio.

O código para o processo de verificação é mostrado abaixo.
PROC_VERIFY : process
begin
  wait until rising_edge(clk) and rst = '0';
  
  -- Check that DUT and TB FIFO are reporting empty simultaneously
  assert (empty = '1' and fifo.IsEmpty) or
         (empty = '0' and not fifo.IsEmpty)
    report "empty=" & std_logic'image(empty) 
      & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty)
    severity failure;

  -- Check that the valid signals are matching
  assert rd_valid = fifo_out_valid
    report "rd_valid=" & std_logic'image(rd_valid) 
      & " while fifo_out_valid=" & std_logic'image(fifo_out_valid)
    severity failure;

  -- Check that the output from the DUT matches the TB FIFO
  if rd_valid then
    assert fifo_out = to_integer(unsigned(rd_data))
      report "rd_data=" & integer'image(to_integer(unsigned(rd_data)))
        & " while fifo_out=" & integer'image(fifo_out)
      severity failure;
      report "Pop " & integer'image(fifo_out);
  end if;

end process;

O processo é acionado pela borda ascendente do relógio, como podemos ver na primeira linha de código. O DUT é um processo com clock, e espera-se que a lógica downstream que está conectada ao DUT também seja síncrona com o sinal de clock. Portanto, faz sentido verificar as saídas na borda ascendente do clock.

O segundo bloco de código verifica se o empty sinal proveniente do DUT é afirmado somente quando o FIFO do testbench está vazio. O DUT e o modelo comportamental precisam concordar que o FIFO está vazio ou não para que este teste seja aprovado.

Em seguida, segue uma comparação dos sinais válidos dos dados lidos. O DUT e o modelo comportamental devem produzir dados simultaneamente, caso contrário, há algo errado.

Finalmente, verificamos se os dados de saída do DUT correspondem ao próximo elemento que retiramos do FIFO do testbench. Isso, é claro, só acontece se o rd_valid sinal é afirmado, o que significa que o rd_data sinal pode ser amostrado.

Coleta de dados de cobertura


Para controlar o fluxo principal do testbench, criaremos um processo sequenciador. Esse processo inicializará os compartimentos de cobertura, executará os testes e interromperá o testbench quando todas as metas de cobertura forem atendidas. O código abaixo mostra o processo completo do «PROC_SEQUENCER».
PROC_SEQUENCER : process
begin

  -- Set up coverage bins
  bin1.AddBins("Write while empty", ONE_BIN);
  bin2.AddBins("Read while full", ONE_BIN);
  bin3.AddBins("Read and write while almost empty", ONE_BIN);
  bin4.AddBins("Read and write while almost full", ONE_BIN);
  bin5.AddBins("Read without write when almost empty", ONE_BIN);
  bin6.AddBins("Write without read when almost full", ONE_BIN);

  wait until rising_edge(clk);
  wait until rising_edge(clk);
  rst <= '0';
  wait until rising_edge(clk);

  loop
    wait until rising_edge(clk);

    -- Collect coverage data
    bin1.ICover(to_integer(wr_en = '1' and empty = '1'));
    bin2.ICover(to_integer(rd_en = '1' and full = '1'));
    bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and
                           empty = '0' and empty_next = '1'));
    bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and
                           full = '0' and full_next = '1'));
    bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and
                           empty = '0' and empty_next = '1'));
    bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and
                           full = '0' and full_next = '1'));

    -- Stop the test when all coverage goals have been met
    exit when
      bin1.IsCovered and
      bin2.IsCovered and
      bin3.IsCovered and
      bin4.IsCovered and
      bin5.IsCovered and
      bin6.IsCovered;
  end loop;
  
  report("Coverage goals met");

  -- Make sure that the DUT is empty before terminating the test
  wr_en <= force '0';
  rd_en <= force '1';
  loop
    wait until rising_edge(clk);
    exit when empty = '1';
  end loop;

  -- Print coverage data
  bin1.WriteBin;
  bin2.WriteBin;
  bin3.WriteBin;
  bin4.WriteBin;
  bin5.WriteBin;
  bin6.WriteBin;
  
  finish;
end process;

Primeiro, inicializamos os objetos bin de cobertura chamando o método AddBins procedimento sobre eles, como já discutimos anteriormente neste artigo. Então, após a liberação do reset, passamos a coletar os dados de cobertura. Em cada borda de subida do relógio, o código dentro da construção do loop será executado.

O primeiro bloco de código dentro do loop é para coletar dados de cobertura. Podemos chamar o ICover procedimento no compartimento para registrar uma ocorrência no ponto de cobertura que ele representa. Se fornecermos o parâmetro inteiro 0 , a chamada não terá efeito. Se usarmos o parâmetro inteiro 1 , contará como um acerto.

Existe apenas um «bin» dentro de cada um dos objetos bin de cobertura, porque os inicializamos usando o ONE_BIN constante. Este único compartimento pode ser alcançado chamando ICover(1) . Podemos registrar um acerto ou erro no ponto de cobertura convertendo nossas expressões booleanas para os inteiros 1 ou 0 usando o to_integer função

Após o registro dos dados de cobertura, verificamos se todas as metas de cobertura foram atingidas ligando para o IsCovered funcionar em todos os compartimentos. Em seguida, saímos do loop se todas as metas de cobertura foram atingidas.

Garantiremos que o DUT esteja vazio antes de encerrar o teste. Para conseguir isso, assumimos o controle dos processos de escritor e leitor, forçando o wr_en sinal para '0' e o rd_en sinal para '1' .

Por fim, imprimimos estatísticas de quantas vezes cada meta de cobertura foi atingida chamando o método WriteBin função em cada um dos compartimentos de cobertura. O finish palavra-chave no final do processo fará com que o simulador pare a simulação.

Executando o testbench


Você pode baixar todo o projeto ModelSim com todos os arquivos VHDL usando o formulário abaixo.





Após carregarmos o projeto executando o arquivo do que está incluído no Zip, podemos executar o testbench simplesmente digitando «runtb» no console do ModelSim. O tempo de execução do testbench será aleatório porque as metas de cobertura são atingidas aleatoriamente. No entanto, os resultados do teste são reproduzíveis porque, na verdade, é uma sequência pseudo-aleatória usada.

Não inicializamos uma semente em nosso código, o que significa que o valor de semente padrão será usado para o gerador pseudo-aleatório. É possível definir uma semente diferente chamando o InitSeed procedimento no RandomPType objeto, isso produzirá uma sequência aleatória diferente.

A saída do console


A saída impressa no console do ModelSim após darmos o comando «runtb» é mostrada abaixo. Haverá muitos empurrões aleatórios para e popping do FIFO do banco de testes enquanto a simulação estiver em execução.
VSIM 2> runtb
# ** Warning: Design size of 15929 statements or 2 leaf instances exceeds
#             ModelSim PE Student Edition recommended capacity.
# Expect performance to be quite adversely affected.
# ** Note: Push 34910
#    Time: 790 ns  Iteration: 0  Instance: /ring_buffer_tb
...
# ** Note: Pop 37937
#    Time: 83100 ns  Iteration: 0  Instance: /ring_buffer_tb
# ** Note: Pop 13898
#    Time: 83110 ns  Iteration: 0  Instance: /ring_buffer_tb
# %% WriteBin: 
# %% Write while empty  Bin:(1)   Count = 2  AtLeast = 1
# 
# %% WriteBin: 
# %% Read while full  Bin:(1)   Count = 3  AtLeast = 1
# 
# %% WriteBin: 
# %% Read and write while almost empty  Bin:(1)   Count = 106  AtLeast = 1
# 
# %% WriteBin: 
# %% Read and write while almost full  Bin:(1)   Count = 1  AtLeast = 1
# 
# %% WriteBin: 
# %% Read without write when almost empty  Bin:(1)   Count = 1  AtLeast = 1
# 
# %% WriteBin: 
# %% Write without read when almost full  Bin:(1)   Count = 3  AtLeast = 1
#
# Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127

As estatísticas de todos os compartimentos de cobertura são impressas quando todas as metas de cobertura são atingidas. Algumas das caixas foram atingidas apenas uma vez, enquanto uma foi atingida 106 vezes. Mas no final, cada caixa foi atingida pelo menos uma vez. Assim, podemos saber que todos os eventos para os quais definimos caixas de cobertura foram testados e verificados.

A forma de onda


Vamos examinar a forma de onda para ter uma ideia do que o testbench está fazendo. A imagem abaixo mostra a forma de onda com o fill_count sinal representado como um valor analógico. O FIFO está cheio quando o traço deste sinal está no topo e vazio quando está no fundo.



Como podemos ver na forma de onda, o buffer de anel está sendo preenchido e esvaziado aleatoriamente. No entanto, não programamos explicitamente essas inclinações e quedas no nível de preenchimento. No entanto, estamos vendo um padrão de interação de aparência orgânica com o DUT.

Mais sobre verificação aleatória restrita


A verificação aleatória restrita é uma boa estratégia de bancada de teste quando o vetor de teste tem muitas permutações para que um teste exaustivo seja prático. As interações aleatórias exibem um comportamento mais natural do que um teste de caso de canto direcionado teria feito, sem sacrificar a precisão.

Podemos ter certeza de que todos os casos de canto foram atendidos, desde que tenhamos configurado corretamente a coleta de dados de cobertura. O benefício adicional é que é mais provável que testes aleatórios exponham pontos fracos no DUT para os quais você não está testando especificamente. Contanto que você conheça todos os casos de canto, você pode criar testes direcionados para eles. Mas casos de canto são facilmente ignorados, e é aí que você pode se beneficiar da metodologia de verificação aleatória restrita.

Este artigo apenas arranhou a superfície do que você pode fazer com a verificação aleatória restrita. Recomendo a leitura dos documentos na página do GitHub do OSVVM para se aprofundar no assunto.

Também recomendo o curso Advanced VHDL Testbenches and Verification da SynthWorks, ao qual não sou afiliado. No entanto, participei da versão de 5 dias deste curso físico. O curso é ministrado por Jim Lewis, presidente do Grupo de Análise e Padronização VHDL (VASG). No geral, um ótimo investimento para qualquer empresa que queira levar seus testbenches VHDL para o próximo nível.





VHDL

  1. A cadência acelera a verificação de SoC de bilhões de portas
  2. Siemens adiciona ao Veloce para verificação assistida por hardware perfeita
  3. Synopsys permite projetos de múltiplas matrizes com HBM3 IP e verificação
  4. Controle de acesso com QR, RFID e verificação de temperatura
  5. O lado desconfortável, imprevisível e aleatório da manutenção
  6. Como gerar números aleatórios em Java
  7. Java 8 - Fluxos
  8. Controle de recursos de torno inclinado com gráficos de verificação
  9. Processamento isométrico diferencial e verificação de simulação de projeto de PCB de alta velocidade
  10. Programa de verificação de desempenho CAGI para compressores rotativos