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