Página principal

Hardware myhdl python

From CInLUG

Conteúdo

Introdução

Atualmente, o desenvolvimento de Hardware [1] tem sido muito mais intenso e rápido. Alguns técnicas surgiram ao longo dos anos, com a finalidade de acelerar o processo de desenvolvimento de Chips. O FPGA [2] - Hardware reprogramável criado pela Xilinx Inc [3] - foi uma delas. A facilidade e flexibilidade dos FPGAs são tão grandes que o atual desenvolvimento hardware utiliza FPGAs tanto como produto final, quanto como fase de desenvolvimento de grandes projetos que serão produzidos em silício.

Os FPGAs são programados através de linguagens de descrição de hardware ou HDLs [4] as quais descrevem formalmente o comportamento do dispositivo, onde as mais conhecidas são: VHDL [5] e Verilog [6]. Existem várias liguagens que abstraem VHDL ou Verilog visando facilitar ainda mais a vida do projetista.

Neste artigo falaremos de Python [7] como facilitador e teremos como foco uma biblioteca de desenvolvimento de hardware em Python chamada MyHDL [8]. Como diria o autor desta biblioteca: The goal of the MyHDL project is to empower hardware designers with the elegance and simplicity of the Python language Jan Decaluwe.


Preparando o ambiente

Para instalar MyHDL em qualquer máquina que rode Linux e que tenha Python >= 2.4 basta a execução dos seguintes passos:

  1. - Baixar o pacote myhdl para a instalação através deste link;
  2. - Executar os seguintes comandos:
   > cd <diretório onde encontra-se o pacote baixado>
   > tar -xvzf myhdl-x.x.x.tar.gz
   > cd myhdl-x.x.x
   > python setup.py install
   > cd myhdl/test
   > python test_all.py
   ...vai rodar uns 2 centos de testes...
   >

Pronto para o simples uso de MyHDL seu ambiente já está funcionando. Ao longo do texto algumas instruções de instalação serão passadas, para possibilitar o uso mais complexo de MyHDL.

Imagem:Important.png
Durante todo o texto estou supondo que você está usando Linux, nenhuma instrução de instalação do Windows, do Mac ou de qualquer outro sistema operacional é dada pelo simples fato de eu não saber ou não usar. Peço desculpas.

Estrutura

Bem...a figura abaixo mostra em resumo de como é o desenvolvimento de hardware usando Python MyHDL. O processo inicia-se quando o desenvolverdor (respeitando as regras de Engenharia de Software) escreve os testes unitários antes dos módulos (Artefato 1). Agora que os testes estão prontos e bem feitos, pode-se iniciar o desenvolvimento do módulo. Após a conclusão do módulo (Artefato 2), os testes unitários podem ser executados e avaliados, também podem ser feitas simulações as quais podem ter como resultado waveforms, valores impressos na tela, o preenchimento de arquivos, entres muitas outras possibilidades. Caso o os testes sejam mal sucedidos deve-se corrigir os erros e testar novamente (claro!!!), caso contrário pode-se passar para a próxima fase que é a conversão de Python para Verilog - esta fase é muito simples não necessitando de muito esforço. Agora, após a conversão, temos o código em verilog gerado (Artefato 3). Para testarmos o módulo em Verilog teremos que usar um artifício conhecido como Co-Simulação o qual nada mais é (neste caso) do que uma simulação ou execução de Python e Verilog em conjunto. Com a Co-Simulação poderemos rodar novamente os casos de testes (Artefato 1) ainda em Python para validar o novo código em Verilog (Artefato 3). Depois de validado o projeto está pronto para ser utilizado em FPGA. Pronto!


Imagem: Estrutura myhdl.png


Técnicas de modelagem

Para o desenvolvimento de hardware existem várias técnicas (níveis) de modelagem como: TL (Alto nível, serão necessários alguns refinamentos para chegar à sintese), RTL (baixo nível, facilmente sintetizável) e Estrutural (descrição de ligação entre módulos com sinais,bits...por aí) . A seguir explicaremos um pouco de cada dando uma idéia mais geral do que é cada etapa e como desenvolver em cada nível.

Imagem:Information.png
Todos os arquivos utilizados neste artigo estão "hosteados" em code.google com exceção de um que é o myhdl.vpi que deverá ser compilado, mas isso só será necessário caso você deseje executar uma Co-Simulação.


Sintaxe básica

Para introduzir os conceitos básicos de Python MyHDL falaremos um pouco da sintaxe. É bom salientar que em MyHDL não existem portas input e output. Pela lógica real, toda porta onde só se escreve é saída, de apenas leitura é entrada e de qualquer ação é entrada/saída. O código abaixo mostra a estrutura do código de um módulo chamado module. O decorator será o tipo de sensibilidade do processo onde temos:

  1. always_comb: sensível a qualquer entrada, também chamado de combinatorial;
  2. always: contém uma lista de sinais sensibilidade;
  3. instance: é o modo mais geral onde o desenvolvedor descreve dentro do módulo seu comportamento.
  1. def modulo(entradas...,saidas...):
  2.  
  3. @decorator
  4. def sub_processo():
  5. ...
  6. return sub_processo
  7.  
  8.  
  9. @decorator
  10. def sub_processo2():
  11. ...
  12. return sub_processo, sub_processo2
  13.  

Existem os tipos primitivos de Python e outros de MyHDL, o desenvolvimento tem que respeitar um conjunto restritivo de dados para que o código criado possa ser convertido para Verilog. Abaixo mostra-se um resumo do uso do intbv (Integer Bit Vector) um dos dados mais comuns:

  1. >>>from myhdl import intbv
  2. >>>a = intbv(0) # nesta caso o valor inicial de a será 0 e o tamanho do vetor é infinito e pode assumir qualquer valor.
  3. >>>b = intbv(1) # mesmo que acima,mas com o valor inicial igual 1
  4. >>>c = intbv(0, min=-100, max=200) # neste caso estamos definindo o intervalo de existência do sinal, mas sem falar nada sobre o tamanho.
  5. >>>d = intbv(0, min=-8, max=7) # mesmo que a, mas com a definição do tamanho do vetor sendo igual a 3. O intervalo de existência neste caso é de -8 a 7
  6. >>>e = d - 1 ; print hex(e) # e assume o valor resultado da operação que é -1 (lembre-se que python não tem frescura, sobrescreve mesmo!)
  7. -0x1L
  8. >>>e = intbv(d - 1)[3:]; print hex(e)
  9. 0x7L
  10. >>>
  11. >>>#Neste último comando, fazendo um cast para intbv()[3:] forçamos o resultado a ficar em "binário", gerando o complemento a dois do resultado.
  12. ...
  13. >>>
  14.  

Muitos outros detalhes sobre intbv podem ser vistos na documentação de MyHDL [9]. Observação... a lógica binária de verdade só é alcançada quando trabalhamos com vetores de range definidos, ou seja, tem que ter o mínimo e o máximo no construtor do signal.

Temos os sinais que ligam módulos, submódulos, entre outros que são descritos pela classe Signal.

  1. >>>from myhdl import Signal
  2. >>>a = Signal(0) #Nesta caso a é um Sinal que transmite inteiros de tamanho infinito que podem assumir qualquer valor.
  3. >>>b = Signal(0, delay=10) # o mesmo que o caso acima, porém o sinal tem um tempo de resposta de 10 alguma coisa (a unidade é abstraída).
  4. >>>c = Signal(intbv(0)[8:]) # Agora temos um que transmite vetores de bit com tamanho 8
  5. >>>c.next = 3 #maneira com que se deve setar um valor naquele sinal.
  6. >>>print c #o valor de c está em c.val o próximo valor de c é c.next
  7. 0
  8. >>>print c.next
  9. 3
  10. >>>
  11.  

RTL

É uma modelagem bem próxima da descrição real do hardware, é usado para síntese direta, ou seja, não é necessário mudar muita coisa para baixar no FPGA.


Lógica combinacional

Abaixo temos um exemplo simples de uma ULA que executa cálculos cada vez que alguma entrada muda.

  1. def ULA( a, b, sel, result ):
  2. """
  3. This module represents an ALU. It can
  4. execute severals operations.
  5. a,b - input value (8 bits signed interger)
  6. sel - selection (1 bits) {
  7. 0 - Additoin
  8. 1 - Subtraction
  9. result - output value (8 bits)
  10. """
  11. @always_comb
  12. def process():
  13. ###Addition
  14. if sel == 0x0:
  15. result.next = a + b
  16. ###Subtraction
  17. elif sel == 0x1:
  18. result.next = a - b
  19. else: raise Exception( "Error - Operation selected not implemented yet!" )
  20.  
  21. return process
  22.  


Simulação e verificação

Para simular o comportamento da ula poderemos gerar alguns estímulos conforme segue abaixo:

  1. def test_basic():
  2. a, b, result = [Signal( intbv( 0, min=-256, max=256 ) ) for i in range( 3 )]
  3. sel = Signal( intbv( 0 )[1:] )
  4. print "a b sel result"
  5. def test():
  6. for test in range( 5 ):
  7. a.next, b.next, sel.next = randrange( 10 ), randrange( 10 ), randrange( 2 )
  8. yield delay( 10 )
  9. print "%-6s%-6s%-6s%-6s" % ( hex( a ), hex( b ), hex( sel ), hex( result ) )
  10.  
  11. testando = test()
  12. ula = ULA( a, b, sel, result )
  13. sim = Simulation( ula, testando )
  14. sim.run( 100 )
  15.  
  16. if __name__ == '__main__':
  17. test_basic()
  18.  

O resultado gerado é:

   a     b     sel   result
   0x6   0x2   0x1   0x4L  
   0x6   0x5   0x0   0xBL  
   0x3   0x3   0x1   0x0L  
   0x0   0x9   0x0   0x9L  
   0x2   0x3   0x1   0xFFL 
   StopSimulation: No more events

Lógica seqüencial

Temos um registrador que executa ações quando um evento aconteceu com algum item da sua lista de sensibilidade.

  1. def registrador8b( clk, inp, enable, clear, output ):
  2. """
  3. Guarda vetores de oito bits
  4. clk:( 1 bit ) - clock do sistema
  5. inp: (vetor de bits tamanho 8) - entrada do registrador
  6. enable: (vetor de bits tamanho 8) - habilita o armazenamento da entrada
  7. clear: ( 1 bit ) - zera o registrador
  8. output: (vetor de bits tamanho 8) - saída, valor armazenado
  9. """
  10. @always( clk.posedge )
  11. def process():
  12. if clear == HIGH:
  13. output.next = 0
  14. else:
  15. if enable:
  16. output.next = inp
  17. return process
  18.  


Simulação e verificação

Neste caso, é um pouco mais complicado gerar um teste básico para colocar o registrador para funcionar, porque precisamos de um gerador de clock, um gerador de estímulos e um monitor (para imprimir os valores dos sinais). Então, chegamos ao trecho de código abaixo:

  1. def test_bench():
  2. clk, enable, clear = [Signal( intbv( 0 )[1:] ) for i in range( 3 )] # apenas replicando 3x
  3. inp, output = Signal( intbv( 0 )[8:] ), Signal( intbv( 0 )[8:] )
  4.  
  5. reg = registrador8b( clk, inp, enable, clear, output )
  6.  
  7. HALF_PERIOD = delay( 10 )
  8.  
  9. @always( HALF_PERIOD )
  10. def clk_gen():
  11. clk.next = not clk
  12.  
  13. @instance
  14. def stimgen():
  15. for i in range( 1000 ):
  16. inp.next, enable.next, clear.next = randrange( 256 ), randrange( 2 ), randrange( 2 )
  17. yield clk.negedge
  18. raise StopSimulation
  19.  
  20. @instance
  21. def monitor():
  22. print "input enable clear output"
  23. while 1:
  24. yield clk.posedge
  25. print "%-6s%-7s%-6s%-6s" % ( hex( inp ), enable, clear, hex( output ) )
  26. return clk_gen, stimgen, reg, monitor
  27.  

O resultado gerado é:

   input enable clear output
   0x8c  0      1     0x0L  
   0xe3  1      0     0x0L  
   0x69  0      1     0xe3  
   0x80  1      0     0x0L  
   0x2a  0      0     0x80  
   0x75  1      0     0x80  
   0x5b  0      1     0x75  
   0xab  1      1     0x0L  
   0xbc  0      0     0x0L  
   0x68  1      1     0x0L

Máquina de estados finita

Agora temos um módulo que representa o controle de um despertador. O controle é feito por uma máquina de estados finita a qual está descrita abaixo:

  1. HIGH, LOW = 1, 0
  2. t_State = enum( "ESPERANDO", "CONTANDO", "ARMAZENANDO", "TOCAR" )
  3.  
  4. def Despertador( clk, time, count , enable, state, reset, tocar ):
  5.  
  6. """ Despertador controlado por uma máquina de estados finita.
  7.  
  8. clk: (1 bit) - clock do sistema
  9. reset: (1 bit) - reset do sistema
  10. time: (32 bit) - tempo que será usador para tocar
  11. count: (32 bit) - valor atual do contador
  12. state: (estado) - estado atual do contador
  13. enable: (1 bit) - bit de habilitacao
  14. """
  15.  
  16. tmp = Signal( intbv( 0, min=0, max=10000 ) )
  17. end_time = Signal( intbv( 0 ) )
  18.  
  19. @always( clk.posedge, reset.posedge )
  20. def FSM():
  21. if reset == HIGH:
  22. count.next = 0
  23. tmp.next = 0
  24. time.next = 0
  25. state.next = t_State.ESPERANDO
  26. else:
  27. if state == t_State.ESPERANDO:
  28. tocar.next = 0
  29. if enable:
  30. end_time.next = time
  31. state.next = t_State.CONTANDO
  32.  
  33. elif state == t_State.CONTANDO:
  34. if tmp < 10:
  35. tmp.next = tmp + 1
  36. state.next = t_State.CONTANDO
  37. else:
  38. tmp.next = 0
  39. state.next = t_State.ARMAZENANDO
  40.  
  41. elif state == t_State.ARMAZENANDO:
  42. if count < end_time:
  43. count.next = count + 1
  44. state.next = t_State.CONTANDO
  45. else:
  46. count.next = 0
  47. state.next = t_State.TOCAR
  48.  
  49. elif state == t_State.TOCAR:
  50. tocar.next = 1
  51. state.next = t_State.TOCAR
  52. else:
  53. raise ValueError( "Undefined state" )
  54.  
  55. return FSM
  56.  


Simulação e verificação

A complexidade dos testes cresce com a complexidade do módulo. Abaixo temos um exemplo de uso do despertador. Observe na linha 39 o comando traceSignals, este gera um waveform da simulação possibilitando uma visualização mais detalhada do que acontece com os sinais.

  1. def testbench():
  2.  
  3. clk, enable, state, reset, tocar = [Signal( bool( 0 ) ) for i in range( 5 )]
  4. time, count = Signal( intbv( 0 )[32:] ), Signal( intbv( 0 )[32:] )
  5. state = Signal( t_State.ESPERANDO )
  6.  
  7.  
  8. desp = Despertador( clk, time, count, enable, state, reset, tocar )
  9.  
  10. @always( delay( 10 ) )
  11. def clkgen():
  12. clk.next = not clk
  13.  
  14. @instance
  15. def stimulus():
  16. reset.next = HIGH
  17. yield clk.posedge
  18. reset.next = LOW
  19. enable.next = HIGH
  20. time.next = intbv( 10 )[32:]
  21. yield clk.posedge
  22. enable.next = LOW
  23. while not tocar:
  24. yield clk.posedge
  25. yield clk.posedge
  26. raise StopSimulation
  27.  
  28. @instance
  29. def monitor():
  30. print "count tocar"
  31. yield reset.negedge
  32. while 1:
  33. yield count, tocar.posedge
  34. print "%-6s%-6s" % ( hex( count ), tocar )
  35.  
  36. return clkgen, stimulus, monitor, desp
  37.  
  38.  
  39. tb_fsm = traceSignals( testbench ) #Gera um arquivo .vcd de saída que contém o "waveform" da simulação.
  40. #Este aquivo pode ser visualizado com GTKWave.
  41. sim = Simulation( tb_fsm )
  42. sim.run()
  43.  

Os resultados gerados pela simples impressão das saídas são:

   count tocar
   0x1L  False 
   0x2L  False 
   0x3L  False 
   0x4L  False 
   0x5L  False 
   0x6L  False 
   0x7L  False 
   0x8L  False 
   0x9L  False 
   0xAL  False 
   0x0   False 
   0x0   1     

Podemos ver que neste caso foi gerado um arquivo .VCD que descreve o comportamento dos sinais durante a execução dos testes. Abaixo temos a visualização do arquivo .VCD gerado. Para visualizar o arquivo é preciso executar o comando:

   >gtkwave -L <nome do arquivo>.vcd
   ...ou...
   >gtkwave -v <nome do arquivo>.vcd


Image: Toque_despertar.png


Alto nível

A modelagem alto nível torna o desenvolvimento mais rápido e fácil, porém deixa o projeto menos sintetizável. Algumas formas de desenvolver em alto nível são encotradas na documentação de MyHDL, uma bem interessante é a descrição de uma memória simples cujo código está mostrado abaixo. Este nível de modelagem também dá suporte a uso de orientação a objetos e a tratamento de exceção.

  1. memory = {}
  2.  
  3. def sparseMemory( dout, din, addr, we, en, clk ):
  4. """
  5. Sparse memory model based on a dictionary.
  6.  
  7. Ports:
  8. dout -- data out
  9. din -- data in
  10. addr -- address bus
  11. we -- write enable: write if 1, read otherwise
  12. en -- interface enable: enabled if 1
  13. clk -- clock input
  14. """
  15.  
  16. @always( clk.posedge )
  17. def access():
  18. if en:
  19. if we:
  20. memory[int( addr.val )] = din.val
  21. else:
  22. try:
  23. dout.next = memory[int ( addr.val )]
  24. except KeyError:
  25. raise Error, "Uninitialized address %s" % hex( addr )
  26. return access
  27.  


Simulação e verificação

No final da simulação tentamos acessar um endereço (0x15) de memória ainda não alocada, por isso a exceção é gerada.

  = Memory ==============
  address        content
  0x1            0xFL
  0x2            0x10L
  0x3            0x11L
  0x4            0x12L
  0x5            0x13L
  0x6            0x14L
  0x7            0x15L
  0x8            0x16L
  0x9            0x17L
  0xa            0x18L
  Traceback (most recent call last):
  	File "/home/rjsp/workspace/artigo_myhdl/src/memory.py", line 102, in ?
  	sim.run()
  	File "/usr/lib/python2.4/site-packages/myhdl/_Simulation.py", line 132, in run
  	waiter.next(waiters, actives, exc)
  	File "/usr/lib/python2.4/site-packages/myhdl/_Waiter.py", line 140, in next
  	clause = self.generator.next()
  	File "/usr/lib/python2.4/site-packages/myhdl/_always.py", line 101, in genfunc
  	func()
  	File "/home/rjsp/workspace/artigo_myhdl/src/memory.py", line 46, in access
  	raise Error, "Uninitialized address %s" % hex( addr )
  __main__.Error: Uninitialized address 0x15L

Estrutural

Nesta seção, mostraremos a modelagem Estrutural a qual funciona baseada na idéia da construção de módulos usando módulos menores. Então abaixo vai um exemplo muito ilustrativo dessa idéia. Construiremos um shifter que desloca 8 bits juntando 4 que deslocam 2. Este exemplo é apenas ilustrativo, pois era muito mais fácil mudar o número de deslocamentos ao invés de montar tudo. Então:

  1. def shifter2x( s_input, s_output ):
  2. """Desloca o vetor de entrada de 2 bits
  3. s_input: (vetor de bits tamanho 8) - entrada do shifter
  4. s_output: (vetor de bits tamanho 8) - saída, valor deslocado
  5. """
  6. @always_comb
  7. def process():
  8. s_output.next = intbv(s_input << 2)[32:]
  9. return process
  10.  
  11. def shifter8x( s_input, s_output ):
  12. """Desloca o vetor de entrada de 8 bits
  13. s_input: (vetor de bits tamanho 8) - entrada do shifter
  14. s_output: (vetor de bits tamanho 8) - saída, valor deslocado
  15. """
  16. s_12, s_23, s_34 = [Signal( intbv( 0 )[32:] ) for i in range( 3 )]
  17. instance_1 = shifter2x( s_input, s_12 )
  18. instance_2 = shifter2x( s_12, s_23 )
  19. instance_3 = shifter2x( s_23, s_34 )
  20. instance_4 = shifter2x( s_34, s_output )
  21.  
  22. return instances()
  23.  

Testes unitários

Um aspecto muito interessante de MyHDL é o suporte ao Unittest Framework nativo de Python, uma ferramenta muito poderosa. No desenvolvimento de hardware os testes são muito importantes, grande parte do tempo da construção de um dispositivo é dedicada apenas a testes. Muitos desta área gostam do termo Test-Driven Development que nada mais é que o desenvolvimento dos casos testes antes para depois implementar o código necessário para passar nesses testes. Abaixo temos um exemplo de teste unitário onde o módulo que está sendo testado foi citado anteriormento neste artigo. Então temos:

  1. class shifters_unit_test( TestCase ):
  2.  
  3. def shifter_general_test_case( self, bits, obj_shifter ): #recebe o número de bits a serem deslocados e um objeto shifter
  4. """Deslocando bits 1000 vezes"""
  5. def test( s_input, s_output ):
  6. for i in range( 10 ):
  7. r_input = randrange( 10 ) #gera um número aleatório entre 0 e 10
  8. s_input.next = r_input
  9. yield delay( 10 )
  10. expected = r_input << bits #desloca a entrada e armazena para compara com o resultado que vai sair do módulo
  11. actual = s_output
  12. self.assertEqual( actual, intbv( expected )[32:] )
  13.  
  14. for width in range( 100 ):
  15. s_input, s_output = [Signal( intbv( 0 )[32:] ) for i in range( 2 )] #cria os sinais de entrada e saída do módulo a ser testado.
  16. shifter = obj_shifter( s_input, s_output )
  17. check = test( s_input, s_output )
  18. sim = Simulation( shifter, check )
  19. sim.run( quiet=1 )
  20.  
  21. def test_case_shifter2x( self ):
  22. """Deslocando 2 bits de números aleatórios 1000 vezes"""
  23. return self.shifter_general_test_case( 2, lambda a, b: shifter2x( a, b ) ) #dizemos aqui que o deslocamento é de 2 bits e o shifter é
  24. #do tipo shifter2x
  25. def test_case_shifter8x( self ):
  26. """Deslocando 8 bits de números aleatórios 1000 vezes"""
  27. return self.shifter_general_test_case( 8, lambda a, b: shifter8x( a, b ) ) #dizemos aqui que o deslocamento é de 8 bits e o shifter é
  28. #do tipo shifter8x
  29.  
  30. if __name__ == '__main__':
  31. unittest.main()
  32.  


Execução dos testes unitários

Quando os testes são executados as mensagens que abaixo aparecem:

   > python shifters_unit_test.py -v
   Deslocando 2 bits de números aleatórios 1000 vezes ... ok
   Deslocando 8 bits de números aleatórios 1000 vezes ... ok
   ----------------------------------------------------------------------
   Ran 2 tests in 2.970s
   OK
   >

Geração de código Verilog

A geração de código Verilog tem muitos poréns e porques.. A documentação é bem extensa, caso você leitor tenha algum interesse em aprofundar-se nesse tópico é bom dar uma olhada cuidadosa na documentação. Superficialmente, utilizaremos o registrador de 8 bit descrito anteriormente neste artigo para fazermos a conversão. Inicialmente é bem simples, apenas temos que definir os sinais e pronto. Então, segue abaixo o exemplo:

  1. HIGH = 1
  2. def registrador8b( clk, inp, enable, clear, output ):
  3. """
  4. Guarda vetores de oito bits
  5. clk:( 1 bit ) - clock do sistema
  6. inp: (vetor de bits tamanho 8) - entrada do registrador
  7. enable: (vetor de bits tamanho 8) - habilita o armazenamento da entrada
  8. clear: ( 1 bit ) - zera o registrador
  9. output: (vetor de bits tamanho 8) - saída, valor armazenado
  10. """
  11. @always( clk.posedge )
  12. def process():
  13. if clear == HIGH:
  14. output.next = intbv( 0 )[8:]
  15. else:
  16. if enable:
  17. output.next = inp
  18. return process
  19.  
  20. if __name__ == '__main__':
  21. clk, enable, clear = [Signal( bool( 0 ) ) for i in range( 3 )]
  22. inp, output = [Signal( intbv( 0 )[8:] ) for i in range( 2 )]
  23. reg = toVerilog( registrador8b, clk, inp, enable, clear, output )
  24.  


Resultados da conversão

Os resultados da conversão são o módulo em Verilog e uma interface que pode ser usada para Co-simulação (tópico que será explicado a seguir) e execução dos testes que ainda estão em python. O melhor de tudo é que você pode ter todos os testes ainda em Python sem precisar baixar o nível dos testes, ou seja, com os mesmos testcases pode-se testar o alto nível escrito em Python e o módulo baixo nível em Verilog. Com isso valida-se o código gerado, para então baixar em FPGA.


Módulo em Verilog

  1. module registrador8b (
  2. clk,
  3. inp,
  4. enable,
  5. clear,
  6. output
  7. );
  8. input clk;
  9. input [7:0] inp;
  10. input enable;
  11. input clear;
  12. output [7:0] output;
  13. reg [7:0] output;
  14.  
  15. always @(posedge clk) begin: _registrador8b_process
  16. if ((clear == 1)) begin
  17. output <= 8'h0;
  18. end
  19. else begin
  20. if (enable) begin
  21. output <= inp;
  22. end
  23. end
  24. end
  25. endmodule
  26.  


Interface de Co-Simulação em Verilog

  1. module tb_registrador8b;
  2.  
  3. reg clk;
  4. reg [7:0] inp;
  5. reg enable;
  6. reg clear;
  7. wire [7:0] output;
  8. initial begin
  9. $from_myhdl(
  10. clk,
  11. inp,
  12. enable,
  13. clear
  14. );
  15. $to_myhdl(
  16. output
  17. );
  18. end
  19. registrador8b dut(
  20. clk,
  21. inp,
  22. enable,
  23. clear,
  24. output
  25. );
  26. endmodule
  27.  

Co-simulação Python/Verilog

A união entre Co-Simulação e Python UnitTests gera uma possibilidade muito interessante para o desenvolvimento de Hardware. Um dispositivo pode ser desenvolvido baseado em test driven development de sua origem (em MyHDL) até a hora da sintetização (código já em Verilog) usando a mesma suite de testes, todos feitos em Python. Isso facilita bastante, pois não é necessário baixar o nível dos testes, ou seja, todos os testes desenvolvidos são utilizados até o fim do projeto. As suites de teste podem ser bem complexas, menos custosas e assim "mais facilmente" podem capturar erros.


Funcionamento do mecanismo de Co-Simulação

A Co-Simulação tem como base a idéia da união entre a simulação de Python e de Verilog. Neste contexto, o interessante é que um módulo desenvolvido em MyHDL pode ser testado em Python, depois, o código Verilog gerado pode ser testado com os mesmos testes... Pois é, usando a Co-Simulação, parte do dispositivo pode ficar em MyHDL e parte em Verilog. Desta forma, a tradução do módulo em MyHDL para Verilog pode ser incremental. Traduz-se uma pequena parte por vez e executam-se os testes novamente, no fim, todo o dispositivo estará em Verilog e testado!


Image: Myhdl_co-simulation.png


O suporte a co-simulação de MyHDL tem algumas restrições (melhor explicadas na documentação), onde as principais que eu identifiquei são:

  • Apenas HDL passivo pode ser co-simulado, a parte que está rodando na máquina Python (MyHDL) tem que ser mestre do tempo, ou seja, o clock nunca poderá ser gerado no simulador Verilog por exemplo. Enfim, não se pode impôr restrições de tempo dentro do simulador Verilog (eu quando li isso fiquei aliviado, pensei que era o contrário... ainda bem que é assim!!!) Com isso a idéia de test driven development funciona mesmo! Testado e aprovado!
  • Alguns sinais podem mudar do lado MyHDL entre a subida e a descida do clock, mas no lado Verilog as coisas só mudam ao fim do ciclo do clock.. Cuidado!


Pré-Co-Simulção

Para executar a Co-Simulação de MyHDL e Verilog é necessária a instalação de um Compilador e de um Simulador de Verilog, no meu caso eu usei o Icarus Verilog o qual já é os dois ao mesmo tempo (Compilador = iverilog e Simulador = vvp).

Para instalação do Icarus Verilog no Ubuntu:

   > sudo apt-get install verilog
   ...espera e pronto!...
   >

Para a instalação no Gentoo:

   > emerge =iverilog-0.8.4
   ...espare um "pouquinho" mais e pronto... 
   >

Falta apenas compilar uma coisinha... Dentro da pasta onde você descompactou a biblioteca MyHDL tem um arquivo que precisa ser compilado para ser usado na Co-Simulação. O arquivo que deve-se compilar é myhdl.c. Geralmente o caminho deste arquivo é: /myhdl-x.x.x/cosimulation/icarus/myhdl.c.

Para compilar e testar são necessários os seguintes comandos:

   > cd /myhdl-x.x.x/cosimulation/icarus/
   > make
   ...espare pouco, pronto!...para testar:
   > cd test
   > python test_all.py -v
   Check that only one bit changes in successive codewords ... ok
   Check that all codewords occur exactly once ... ok
   Check that the code is an original Gray code ... ok
   Check increment operation ... ok
   Check increment operation with suspended simulation runs ... ok
   dff test ... ok
   dff_clkout test ... ok
   dff test with simulation suspends ... ok
   dff_clkout test with simulation suspends ... ok
   ----------------------------------------------------------------------
   Ran 9 tests in 7.617s
   OK
   >

Agora você deve copiar o arquivo myhld.vpi para a pasta do seu projeto. Pronto, rápido e fácil!

Co-Simulando

Para rodar a Co-Simulação temos que alterar alguns pontos no unittest do módulo em MyHDL. Vamos usar uma ula como exemplo ilustrativo. Abaixo está o código do unittest da ula que funciona para as duas versões, tanto MyHDL quanto do módulo já convertido para Verilog. Observe que na linha 36 e na linha 54 estamos fazendo uso do compilador iverilog e do simulador vvp o qual usa aquele arquivo compilado na seção anterior. Observe também a presença da flag cosim que indica o uso de co-simulação ou não.

  1. def ula( a, b, sel, result ):
  2.  
  3. """
  4. This module represents an ALU. It can
  5. execute severals operations.
  6. a,b - input value (8 bits signed interger)
  7. sel - selection (1 bits) {
  8. 0 - Additoin
  9. 1 - Subtraction
  10. result - output value (8 bits)
  11.  
  12. """
  13.  
  14. @always_comb
  15. def process():
  16.  
  17. #Addition
  18. if sel == 0x0:
  19. result.next = a + b
  20. #Subtraction
  21. elif sel == 0x1:
  22. result.next = a - b #this is necessary because the negative valeu must be two complement.
  23. else: raise Exception( "Error - Operation selected not implemented yet!" )
  24.  
  25. return process
  26.  
  27. class ula_unit_test( TestCase ):
  28.  
  29. def general_test_case( self, op, function, cosim=False ):
  30. """Caso de teste geral
  31. op: (bit) - bit de selação da ula [0x0, 0x1];
  32. function: (objeto função nativo de pyhton) - É a função será executada pelo testcase.
  33. cosim: (bool) - uma flag que habilita a co-simulacao
  34. """
  35. if cosim:
  36. cmd = "iverilog -o ula.o ./ula.v ./tb_ula.v"
  37. os.system( cmd )
  38.  
  39. def test( a, b, sel, result ):
  40. for i in range( 10 ):
  41. r_a, r_b = randrange( 128 ), randrange( 128 )
  42. a.next, b.next = r_a, r_b
  43. sel.next = op
  44. yield delay( 10 )
  45. expected = function( r_a, r_b )
  46. actual = result
  47. self.assertEqual( actual.val , expected )
  48.  
  49. for width in range( 100 ):
  50. a, b, result = [Signal( intbv( 0, min=-256, max=256 ) ) for i in range( 3 )]
  51. sel = Signal( intbv( 0 )[1:] )
  52. sim = None
  53. if cosim:
  54. cosim = Cosimulation( "vvp -m ./myhdl.vpi ula.o", a=a, b=b, sel=sel, result=result ) #para MyHDL isso é o mesmo que um ula onde os
  55. #sinais estão sendo ligados com os sinais
  56. #declarados acima.
  57. sim = Simulation( cosim, test( a=a, b=b, sel=sel, result=result ) )
  58. else:
  59. sum = ula( a, b, sel, result )
  60. check = test( a, b, sel, result )
  61. sim = Simulation( sum, check )
  62. sim.run( quiet=1 )
  63.  
  64. def test_case_addition( self ):
  65. """Checando 1000 vezes a soma entre inteiros de 8 bits"""
  66. return self.general_test_case( 0x0, lambda a, b: a + b )
  67.  
  68. def test_case_subtraction( self ):
  69. """Checando 1000 vezes a subtracao entre inteiros de 8 bits"""
  70. return self.general_test_case( 0x1, lambda a, b: a - b )
  71.  
  72. def test_case_addition_verilog_co_simulation( self ):
  73. """Checando 1000 vezes a soma entre inteiros de 8 bits - verilog"""
  74. return self.general_test_case( 0x0, lambda a, b: a + b, True )
  75.  
  76. def test_case_subtraction_verilog_co_simulation( self ):
  77. """Checando 1000 vezes a subtracao entre inteiros de 8 bits - verilog"""
  78. return self.general_test_case( 0x1, lambda a, b: a - b, True )
  79.  
  80. if __name__ == '__main__':
  81. unittest.main()
  82.  


Resultado da Co-Simulação

Para Co-Simular basta executar:

   >python ula_unit_test.py -v
   Checando 1000 vezes a soma entre inteiros de 8 bits ... ok
   Checando 1000 vezes a soma entre inteiros de 8 bits - verilog ... ok
   Checando 1000 vezes a subtracao entre inteiros de 8 bits ... ok
   Checando 1000 vezes a subtracao entre inteiros de 8 bits - verilog ... ok
   ----------------------------------------------------------------------
   Ran 4 tests in 6.149s
   OK
   >

Pronto agora temos todo o processo em nossas mãos.

Conclusões

Depois de todo este artigo temos uma base interessante para começarmos a abstrair um pouco mais o desenvolvimento de hardware usando uma linguagem de alta produtividade como Python. Ao contrário de SystemC, MyHDL é produtivo e já tem nativo um tradutor de código para Verilog (uma possível feature do 0.6 é o toVHDL que gerará também código em VHDL). Um dos aspectos mais "inovadores" e interessantes deste projeto é a Co-Simulação a qual permite o uso de testes complexos (de rápido desenvolvimento -> baixo custo) que avaliam bem o comportamento do dispositivo. Abaixo temos um exemplo da aplicação de MyHDL onde quase todo o dispositivo já está rodando em Verilog, falta apenas traduzir e testar a memória.


Image:Myhdl_aplicacao.png


Com MyHDL é possível desenvolver (prototipar) hardware das mais variadas classes (de muito simples a muito complexos) sem muito "custo", já que MyHDL é LGPL (não custa algumas dezenas de milhares de dólares) e a produtividade de Python é muito alta. Qualquer pessoa que pense em abrir uma empresa de desenvolvimento eficiente de hardware não precisa mais de um capital inicial de 500 mil reais para produzir. Então, aproveite!

Grato,


Rodrigo Peixoto.

Referências

Autor: Rodrigo Peixoto

Imagem:Cc-small.png : Imagem:Cc-by.png Imagem:Cc-sa.png
Atribuição-Compatilhamento pela mesma licença 2.5


Imagem:Important.png
Este artigo pode sofrer alterações. Fique ligado!
Ferramentas pessoais
Vistas