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 formal em VHDL usando PSL


Ao projetar VHDL para aplicativos FPGA de segurança crítica, não é suficiente escrever testbenches com o melhor esforço. Você deve apresentar prova de que o módulo funciona como pretendido e sem efeitos colaterais indesejáveis.

As técnicas de verificação formal podem ajudá-lo a mapear um requisito para um teste, provando que seu módulo VHDL está em conformidade com a especificação. É uma ferramenta instrumental para verificar aplicações de saúde ou obter a certificação DO-254 para soluções FPGA aerotransportadas.



Para desmistificar a verificação formal, VHDLwhiz pede a ajuda de Michael Finn Jørgensen para escrever este guest post. Michael tem experiência substancial no assunto e compartilha muitas dicas em sua página do GitHub.

O dispositivo em teste no exemplo para download deste artigo vem do tutorial existente:
Como fazer um AXI FIFO em bloco de RAM usando o handshake pronto/válido

Vou deixar Michael assumir a partir daqui e explicar a verificação formal para você no restante desta postagem do blog.

O que é verificação formal?


O objetivo da verificação formal (FV) é garantir que seu módulo funcione conforme o esperado!

FV é algo que você faz como parte de seu processo de desenvolvimento antes de sintetizar seu módulo. Às vezes é chamado de “Verificação de propriedades”, que é uma descrição melhor, eu acho.

Em FV você especifica as propriedades seu módulo deve ter, e então a ferramenta (chamada de “Solucionador de Satisfação”) irá provar que seu módulo satisfaz as propriedades para todas as sequências possíveis de entradas .

Em outras palavras, a ferramenta procurará qualquer transição de um válido state (onde todas as propriedades são satisfeitas) para um invalid estado (onde uma ou mais propriedades são violadas). Se não houver tal transição, ou seja, não há como fazer a transição para um estado inválido, as propriedades provaram ser sempre satisfeitas.

O desafio em FV é expressar as propriedades do seu módulo em uma linguagem que a ferramenta possa entender.

A verificação formal é uma alternativa aos testbenches escritos manualmente. O objetivo da verificação formal e dos testbenches manuais é eliminar bugs do design, mas a verificação formal faz isso examinando as propriedades e gerando muitos testbenches diferentes automaticamente.

As ferramentas usam dois métodos diferentes:

A prova de indução é mais difícil de satisfazer porque requer todos propriedades a serem declaradas, enquanto o BMC pode ser executado com apenas algumas propriedades e ainda fornecer resultados úteis.

Por que usar a verificação formal?


A verificação formal é muito boa para capturar bugs difíceis de encontrar! Isso porque a verificação formal pesquisa automaticamente todo o espaço de entradas possíveis.

Isso contrasta com a escrita tradicional em bancada que exige a criação manual de estímulos. É praticamente impossível explorar todo o espaço de estado usando testbenches escritos manualmente.

Além disso, quando a verificação formal encontrar um bug, ela gerará uma forma de onda muito curta mostrando o bug e fará isso muito mais rápido do que ao executar uma simulação baseada em um testbench escrito manualmente. Essa forma de onda curta é muito mais fácil de depurar do que uma forma de onda muito longa que normalmente surge da simulação.

No entanto, a verificação formal não é um substituto para a redação do testbench. Na minha experiência, a verificação formal é adequada para testes de unidade, enquanto é melhor fazer testes de integração usando testbenches feitos à mão. Isso porque o tempo de execução da verificação formal aumenta exponencialmente com o tamanho do módulo.

De fato, existe uma curva de aprendizado associada à verificação formal, mas vale a pena o tempo, e espero que esta postagem no blog ajude você a superar essa curva de aprendizado. Você concluirá seu design mais cedo e terá menos bugs!

Como escrever propriedades em PSL


Ao fazer a verificação formal, você deve expressar as propriedades do seu módulo usando a Property Specification Language (PSL). As propriedades geralmente estão localizadas em um arquivo separado com o final .psl , e esse arquivo é usado apenas durante a verificação formal.

Nesta seção, descreverei os principais recursos da linguagem PSL em termos gerais e, em uma seção posterior, mostrarei um exemplo específico.

Existem três declarações no PSL:

Você já deve estar familiarizado com a palavra-chave assert ao escrever testbenches. Essa mesma palavra-chave também é usada no PSL, mas com uma sintaxe ligeiramente diferente. O assert palavra-chave é usada para descrever propriedades que este módulo promete sempre satisfazer. Em particular, o assert A palavra-chave é aplicada às saídas do módulo, ou possivelmente também ao estado interno.

O assume palavra-chave é usada para descrever quaisquer requisitos que este módulo impõe às entradas. Em outras palavras, o assume palavra-chave é aplicada às entradas no módulo.

O cover palavra-chave é usada para descrever propriedades que devem ser possíveis de alcançar de alguma forma.

Você também pode escrever código VHDL regular no arquivo PSL, incluindo declarações e processos de sinal (tanto síncronos quanto combinatórios).

A primeira linha do arquivo PSL deve ser
vunit INSTANCE_NAME(ENTITY_NAME(ARCHITECTURE_NAME)) {

Aqui ENTITY_NAME e ARCHITECTURE_NAME consulte o módulo que você está verificando. O INSTANCE_NAME pode ser o que você quiser. O arquivo deve terminar com uma chave de fechamento:} .

A próxima linha no .psl arquivo deve ser:
default clock is rising_edge(clk);

Isso evita ter que se referir ao sinal de clock em cada uma das declarações de propriedade.

A sintaxe para o assert e assume declarações é:
LABEL : assert always {PRECONDITION} |-> {POSTCONDITION}
LABEL : assume always {PRECONDITION} |-> {POSTCONDITION}

Esta declaração é a seguinte:Se o PRECONDITION detém (em qualquer ciclo de clock) então o POSTCONDITION deve ser satisfeito no mesmo ciclo do relógio.

Existe outra forma comumente usada:
LABEL : assert always {PRECONDITION} |=> {POSTCONDITION}

Esta declaração é a seguinte:Se o PRECONDITION detém (em qualquer ciclo de clock) então o POSTCONDITION deve ser satisfeito no próximo ciclo do relógio.

Ambas as formas são amplamente utilizadas.

Dentro do PRE- e POST-CONDITIONS , você pode usar a palavra-chave stable . Esta palavra-chave significa:O valor em this ciclo de clock é o mesmo que o valor no anterior ciclo do relógio.

Dentro do PRE- e POST-CONDITIONS , você também pode usar sequências, escrevendo da seguinte forma:
{CONDITION_1 ; CONDITION_2}

Isso significa que CONDITION_1 deve ser satisfeito neste ciclo de clock e que CONDITION_2 deve ser satisfeito no próximo ciclo do relógio.

A declaração de capa tem a seguinte sintaxe:
LABEL : cover {CONDITION}

Veremos muitos exemplos de todas essas declarações no exemplo trabalhado mais adiante neste blog.

Instalando as ferramentas necessárias


A verificação formal é suportada pelos principais fornecedores de ferramentas, incluindo ModelSim. Infelizmente, a Student Edition do ModelSim NÃO suporta verificação formal. Na verdade, você recebe o seguinte erro:



Portanto, usar o ModelSim para verificação formal requer uma licença paga.

Em vez disso, existem ferramentas de código aberto (gratuitas) que permitem a verificação formal. Nesta seção, vou guiá-lo pelo processo de instalação dessas ferramentas.

Este guia pressupõe que você esteja executando em uma plataforma Linux. Se você estiver em uma plataforma Windows, recomendo usar o Windows Subsystem for Linux (WSL). O guia a seguir é verificado usando o Ubuntu 20.04 LTS.

Pré-requisitos


Primeiro, devemos atualizar o sistema para obter os patches mais recentes:
sudo apt update
sudo apt upgrade

Em seguida, devemos instalar alguns pacotes de desenvolvimento adicionais:
sudo apt install build-essential clang bison flex libreadline-dev \
                 gawk tcl-dev libffi-dev git mercurial graphviz   \
                 xdot pkg-config python python3 libftdi-dev gperf \
                 libboost-program-options-dev autoconf libgmp-dev \
                 cmake gnat gtkwave

Yosys et al.

git clone https://github.com/YosysHQ/yosys
cd yosys
make
sudo make install
cd ..

git clone https://github.com/YosysHQ/SymbiYosys
cd SymbiYosys
sudo make install
cd ..

git clone https://github.com/SRI-CSL/yices2
cd yices2
autoconf
./configure
make
sudo make install
cd ..

GHDL et al.


Existem binários pré-empacotados para GHDL, mas eu recomendo baixar os arquivos de origem mais recentes e compilá-los da seguinte forma:
git clone https://github.com/ghdl/ghdl
cd ghdl
./configure --prefix=/usr/local
make
sudo make install
cd ..

git clone https://github.com/ghdl/ghdl-yosys-plugin
cd ghdl-yosys-plugin
make
sudo make install
cd ..

Faça o download do projeto de exemplo


A próxima seção deste artigo orienta você na implementação da verificação formal para um projeto VHDL existente. Você pode baixar o código completo digitando seu endereço de e-mail no formulário abaixo.

Exemplo trabalhado com verificação formal


A seção a seguir descreverá como verificar formalmente o módulo axi_fifo descrito anteriormente neste blog.

Para começar a verificação formal, precisamos nos perguntar:quais propriedades o FIFO possui? Identifiquei quatro categorias de propriedades:

Discutirei cada uma dessas propriedades a seguir.

Redefinir manuseio


Primeiro, declaramos que o módulo espera que o reset seja declarado por um ciclo de clock:
f_reset : assume {rst};

Observe aqui a ausência da palavra-chave always . Sem o always palavra-chave, a instrução é válida apenas para o primeiro ciclo de clock.

É costume (e muito útil) dar um rótulo a cada declaração formal. Ao executar a verificação formal, a ferramenta fará referência a esses rótulos ao relatar quaisquer falhas. Eu uso a convenção de preceder todos esses rótulos com f_ .

Em seguida, afirmamos que o FIFO deve estar vazio após o reset:
f_after_reset_valid : assert always {rst} |=> {not out_valid};
f_after_reset_ready : assert always {rst} |=> {in_ready};
f_after_reset_head  : assert always {rst} |=> {count = 0};

Observe que é possível fazer referência a internos sinais no módulo e não apenas nas portas. Aqui fazemos referência a count , que é o nível de preenchimento atual, ou seja, o número de elementos atualmente no FIFO. Isso é possível porque nos referimos ao nome da arquitetura na primeira linha do arquivo PSL.

Alternativamente, poderíamos ter um processo separado no arquivo PSL contando o número de elementos que entram e saem do FIFO. Embora isso deva ser preferido, vou usar o sinal de contagem interno para manter esta postagem no blog curta e direta.

Sinalização FIFO cheia e vazia


A forma como o AXI FIFO sinaliza cheio e vazio é com o out_valid e in_ready sinais. O out_valid sinal é afirmado sempre que o FIFO não está vazio, e o in_ready sinal é afirmado sempre que o FIFO não está cheio. Vamos verificar essas propriedades:
f_empty : assert always {count = 0} |-> {not out_valid};
f_full : assert always {count = ram_depth-1} |-> {not in_ready};

Protocolo AXI


O handshake AXI válido/pronto afirma que a transferência de dados só acontece quando os dois valid e ready são afirmados simultaneamente. Um erro típico ao trabalhar com este protocolo é declarar válido (e acompanhar os dados) e não verificar pronto. Em outras palavras, se valid é afirmado e ready não é, então valid devem permanecer ativos no próximo ciclo de clock, e os dados devem permanecer inalterados. Isso se aplica tanto aos dados que entram no FIFO quanto aos dados que saem do FIFO. Para dados que entram no FIFO, usamos o assume palavra-chave, e para dados que saem do FIFO, usamos o assert palavra-chave.
f_in_data_stable : assume always
   {in_valid and not in_ready and not rst} |=>
   {stable(in_valid) and stable(in_data)};

f_out_data_stable : assert always
   {out_valid and not out_ready and not rst} |=>
   {stable(out_valid) and stable(out_data)};

Observe aqui o uso do stable palavra-chave em combinação com o |=> operador. Essas instruções fazem referência a dois ciclos de clock consecutivos. No primeiro ciclo de clock, ele verifica se valid é afirmado e ready não é afirmado. Em seguida, no próximo (segundo) ciclo de clock (indicado pelo |=> operador), os valores de valid e data deve ser o mesmo que o ciclo de clock anterior (ou seja, o primeiro).

Em segundo lugar, para o protocolo AXI, exigimos que o ready sinal é estável. O que isto significa é se o FIFO pode aceitar dados (ready é afirmado), mas nenhum dado é inserido (valid não é afirmado), então no próximo ciclo de clock ready deve permanecer afirmado.
f_out_ready_stable : assume always
   {out_ready and not out_valid and not rst} |=>
   {stable(out_ready)};

f_in_ready_stable : assert always
   {in_ready and not in_valid and not rst} |=>
   {stable(in_ready)};

Pedido FIFO


Outra propriedade importante de um FIFO é que os dados não são duplicados/excluídos/trocados. Expressar esta propriedade em PSL é bastante complexo, e esta é a parte mais difícil desta verificação formal. Vamos percorrer esta propriedade cuidadosamente passo a passo.

Podemos dizer em geral que se algum dado D1 entra no FIFO antes de algum outro dado D2, então no lado de saída o mesmo dado D1 deve sair do FIFO antes que D2 o faça.

Para expressar isso em PSL precisamos de alguns sinais auxiliares:f_sampled_in_d1 , f_sampled_in_d2 , f_sampled_out_d1 e f_sampled_out_d2 . Esses sinais são apagados no reset e ativados sempre que D1 ou D2 entra ou sai da FIFO. Uma vez ativados, esses sinais permanecem ativos.

Assim, expressamos a propriedade de ordenação FIFO em duas partes:Primeiro, uma suposição que uma vez que D2 entra no FIFO, então D1 já entrou anteriormente:
f_fifo_ordering_in : assume always
   {f_sampled_in_d2} |->
   {f_sampled_in_d1};

E em segundo lugar uma asserção que uma vez que D2 sai do FIFO, então D1 já saiu anteriormente:
f_fifo_ordering_out : assert always
   {f_sampled_out_d2} |->
   {f_sampled_out_d1};

Ainda precisamos declarar e preencher os sinais de amostragem mencionados acima. Fazemos isso da seguinte forma para os sinais de amostragem de entrada:
signal f_sampled_in_d1  : std_logic := '0';
signal f_sampled_in_d2  : std_logic := '0';

...

p_sampled : process (clk)
begin
   if rising_edge(clk) then
      if in_valid then
         if in_data = f_value_d1 then
            f_sampled_in_d1 <= '1';
         end if;
         if in_data = f_value_d2 then
            f_sampled_in_d2 <= '1';
         end if;
      end if;

      if out_valid then
         if out_data = f_value_d1 then
            f_sampled_out_d1 <= '1';
         end if;
         if out_data = f_value_d2 then
            f_sampled_out_d2 <= '1';
         end if;
      end if;

      if rst = '1' then
         f_sampled_in_d1  <= '0';
         f_sampled_in_d2  <= '0';
         f_sampled_out_d1 <= '0';
         f_sampled_out_d2 <= '0';
      end if;
   end if;
end process p_sampled;

Aqui estamos referenciando dois outros sinais, f_value_d1 e f_value_d2 . Eles contêm os valores de dados D1 e D2. Esses sinais podem conter qualquer sinal arbitrário valores, e há uma maneira especial de indicar isso para a ferramenta de verificação formal:
signal f_value_d1 : std_logic_vector(ram_width - 1 downto 0);
signal f_value_d2 : std_logic_vector(ram_width - 1 downto 0);
attribute anyconst : boolean;
attribute anyconst of f_value_d1 : signal is true;
attribute anyconst of f_value_d2 : signal is true;

O anyconst atributo é reconhecido pela ferramenta de verificação formal. Indica que a ferramenta pode inserir qualquer valor constante em seu lugar.

Com o acima, especificamos as propriedades do FIFO.

Executando verificação formal


Antes de podermos executar a ferramenta de verificação formal, precisamos escrever um pequeno script axi_fifo.sby :
[tasks]
bmc

[options]
bmc: mode bmc
bmc: depth 10

[engines]
smtbmc

[script]
ghdl --std=08 -gram_width=4 -gram_depth=8 axi_fifo.vhd axi_fifo.psl -e axi_fifo
prep -top axi_fifo

[files]
axi_fifo.psl
axi_fifo.vhd

A seção [tasks] afirma que queremos a Verificação de Modelo Limitado. A seção [options] especifica que o BMC deve ser executado por 10 ciclos de clock. O padrão é 20. A seção [engines] escolhe qual dos vários solucionadores diferentes usar. Pode haver diferenças na velocidade de execução dos diferentes motores possíveis, dependendo do seu projeto específico. Por enquanto, apenas deixe esse valor inalterado.

O [script] seção é a parte interessante. Aqui especificamos o padrão VHDL (2008), os valores dos genéricos, os arquivos a serem processados ​​e o nome da entidade de nível superior. Por fim, o [files] seção contém os nomes dos arquivos novamente.

Com este script pronto, podemos executar a verificação formal real usando este comando:
sby --yosys ";yosys -m ghdl"; -f axi_fifo.sby

A execução da ferramenta de verificação formal termina com a declaração sem cerimônia:
[axi_fifo_bmc] DONE (PASS, rc=0)

E isso significa apenas que todas as propriedades que especificamos são satisfeitas com todas as entradas arbitrárias por até 10 ciclos de clock. Aumentar a profundidade leva a tempos de execução mais longos para o solver. No entanto, observe que a profundidade é maior que a profundidade do FIFO, o que significa que o BMC encontrará uma situação FIFO completa para algumas sequências de entrada. Se tivéssemos escolhido apenas, digamos, 5 ciclos de clock, a verificação formal nunca encontraria um FIFO completo e não detectaria nenhum bug relacionado a isso.

É possível provar que as propriedades são satisfeitas para todos ciclos de clock usando a prova de indução. No entanto, isso requer mais trabalho escrevendo as propriedades. O desafio é que, até agora, acabamos de escrever algumas propriedades. Mas para indução, precisamos especificar todos propriedades (ou pelo menos o suficiente). Isso consumiria bastante tempo, então, em vez disso, discutirei uma estratégia alternativa para aumentar nossa confiança.

Mutação


Então, agora descrevemos algumas das propriedades que o axi_fifo módulo deve satisfazer, e nossa ferramenta rapidamente confirma essas propriedades, ou seja, prova que elas estão satisfeitas. Mas ainda podemos ter essa sensação desconfortável:temos certeza de que não há bugs? Nós realmente expressamos todas as propriedades do axi_fifo módulo?

Para ajudar a responder a essas perguntas e obter mais confiança na integridade das propriedades especificadas, podemos aplicar uma técnica chamada mutação :Propositalmente fazemos uma pequena mudança na implementação, ou seja, introduzimos deliberadamente um bug e, em seguida, vemos se a verificação formal detectará o bug!

Uma dessas mudanças pode ser remover parte da lógica que controla o out_valid sinal. Por exemplo, poderíamos comentar estas três linhas:
if count = 1 and read_while_write_p1 = '1' then
  out_valid_i <= '0';
end if;

Quando executamos a verificação formal agora recebemos uma falha com a mensagem
Assert failed in axi_fifo: i_axi_fifo.f_out_data_stable
Writing trace to VCD file: engine_0/trace.vcd

Usando a ferramenta GTKwave, podemos visualizar a forma de onda que a acompanha e se parece com o seguinte:



Aqui vemos que em 40 ns, o out_valid sinal deve ir para zero, mas isso não acontece. A asserção que falha é em 50 ns, onde out_valid permanece alto, mas o out_data alterações de sinal, indicando dados duplicados. Os dados duplicados não foram realmente transmitidos neste rastreamento específico porque out_ready é baixo em 40 ns, mas a verificação formal detecta o bug mesmo assim.

Declaração de capa


Vamos finalmente voltar para uma aplicação diferente da ferramenta de verificação formal:Declaração de capa. O objetivo da declaração de capa é verificar se existe uma sequência de entradas que satisfaça uma propriedade específica. Como estamos lidando com um FIFO, vamos ver se podemos preenchê-lo completamente e depois esvaziá-lo novamente. Isso pode ser expresso em uma única declaração de capa:
f_full_to_empty : cover {
   rst = '1';
   rst = '0'[*];
   rst = '0' and count = ram_depth-1;
   rst = '0'[*];
   rst = '0' and count = 0};

Isso é um bocado cheio! Observe os pontos e vírgulas dentro do {} . Para facilitar a leitura, coloquei cada seção em uma linha separada. Esta declaração de capa tem a seguinte redação:Procure uma sequência de entradas que satisfaça:

Portanto, a sintaxe [*] significa repetir (zero ou mais vezes) a condição anterior. Na linha 3, poderíamos ter removido a condição rst = '0' na frente de [*] . Os resultados serão os mesmos. A razão é que o verificador formal tentará encontrar o mais curto seqüência que satisfaça a declaração de capa, e afirmando reset enquanto preenche o FIFO apenas atrasará isso. No entanto, ao esvaziar o FIFO na linha 5, é essencial que o reset não seja ativado.

Para executar o verificador formal agora, precisamos fazer uma pequena modificação no script axi_fifo.sby . Substitua as seções superiores pelas seguintes:
[tasks]
bmc
cover

[options]
bmc: mode bmc
bmc: depth 10
cover: mode cover

Agora, o solver executará o BMC e, em seguida, executará a análise de cobertura. Na saída você verá estas duas linhas:
Reached cover statement at i_axi_fifo.f_full_to_empty in step 15.
Writing trace to VCD file: engine_0/trace5.vcd

Isso significa que a declaração de capa pode realmente ser satisfeita com uma sequência de 15 ciclos de clock, e o solver gerou uma forma de onda especificamente para esta declaração de capa:



Aqui vemos em 80 ns que o FIFO está cheio e in_ready é desativada. E em 150 ns o FIFO está vazio e out_valid é desativada. Observe como durante o período de 30 ns a 80 ns que out_ready é mantido baixo. Isso é necessário para garantir que o FIFO esteja sendo preenchido.

Se substituirmos a condição count = ram_depth-1 com count = ram_depth , a declaração de capa não pode ser satisfeita. A ferramenta então relata um erro.
Unreached cover statement at i_axi_fifo.f_full_to_empty.

Portanto, a mensagem de erro contém o rótulo da declaração de capa que falha. Essa é uma das razões pelas quais os rótulos são tão úteis! Interpretamos o erro acima como afirmando que o FIFO nunca pode conter ram_depth número de elementos. Isso é exatamente como afirmado na postagem original do blog AXI FIFO.

Para onde ir a partir daqui


VHDL

  1. Tutorial - Introdução ao VHDL
  2. Exemplos de conversões de VHDL
  3. Declaração de Procedimento - Exemplo de VHDL
  4. Registros - Exemplo de VHDL
  5. Assinado vs. Não assinado em VHDL
  6. Variáveis ​​- Exemplo de VHDL
  7. Smoking
  8. C# usando
  9. Como criar uma lista de strings em VHDL
  10. Usando uma fresadora como torno