Em continuação ao
primeiro artigo , quero mostrar um exemplo de trabalho com FPGA (FPGA) em python como exemplo. Este artigo abordará o aspecto do teste em mais detalhes. Se a estrutura
MyHDL permitir que as pessoas que trabalham em python, usando a sintaxe e o ecossistema conhecidos, olhem para o mundo do FPGA, os desenvolvedores experientes de FPGA não entenderão o significado do uso de python. Os paradigmas de descrição de hardware para MyHDL e Verilog são semelhantes, e escolher uma linguagem específica é uma questão de hábito e gosto. Verilog / VHDL representa o fato de que o firmware foi escrito nesses idiomas por um longo tempo e, na verdade, eles são padrão na descrição de equipamentos digitais. O Python, como iniciante neste campo, pode competir na escrita de ambientes de teste. Uma parte significativa do tempo do desenvolvedor de FPGA é gasta testando seus designs. Em seguida, quero demonstrar com um exemplo como isso é feito em python com MyHDL.
Suponha que exista uma tarefa para descrever um dispositivo que trabalha com memória no FPGA. Por uma questão de simplicidade, utilizarei a memória que se comunica com outros dispositivos por meio de uma interface paralela (e não por uma serial, por exemplo, I2C). Tais microcircuitos nem sempre são práticos, pois são necessários muitos pinos para trabalhar com eles; por outro lado, é fornecida uma troca de informações mais rápida e fácil. Por exemplo, 1645RU1U doméstico e seus análogos.

Descrição do módulo
O registro fica assim: FPGA fornece um endereço de célula de 16 bits, dados de 8 bits, gera um sinal de gravação WE (ativação de gravação). Como o OE (saída habilitada) e CE (chip habilitado) estão sempre habilitados, a leitura ocorre quando o endereço da célula é alterado. A escrita e a leitura podem ser realizadas seqüencialmente em várias células seguidas, iniciando a partir de um endereço adr_start específico, gravado na extremidade anterior do sinal adr_write e uma célula em um endereço arbitrário (acesso aleatório).
No MyHDL, o código se parece com isso (os sinais de gravação e leitura são lógicos reversos):
from myhdl import * @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we):
Se convertido para Verilog usando a função:
def convert(hdl): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(0)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] inst = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) inst.convert(hdl=hdl) convert(hdl='Verilog')
então temos o seguinte:
`timescale 1ns/10ps module ram_driver ( data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we ); input [7:0] data_in; output [7:0] data_out; reg [7:0] data_out; output [15:0] adr; reg [15:0] adr; input [15:0] adr_start; input adr_write; inout [7:0] data_memory; wire [7:0] data_memory; input read; input write; output we; reg we; reg [7:0] mem_z; assign data_memory = mem_z; always @(write) begin: RAM_DRIVER_WRITE_DATA if ((!write)) begin mem_z <= data_in; we <= 0; end else begin mem_z <= 'bz; data_out <= data_memory; we <= 1; end end always @(posedge adr_write, posedge write, negedge read) begin: RAM_DRIVER_WRITE_START_ADR if (adr_write) begin adr <= adr_start; end else begin adr <= (adr + 1); end end endmodule
Não é necessário converter um projeto em Verilog para simulação, esta etapa será necessária para a atualização do FPGA.
Modelagem
Após a descrição da lógica, o projeto deve ser verificado. Você pode restringir-se, por exemplo, a simular influências de entrada e ver a resposta do módulo no diagrama de tempo. Mas com esta opção, é mais difícil prever a interação do seu módulo com um chip de memória. Portanto, para verificar completamente a operação do dispositivo criado, você precisa criar um modelo de memória e testar a interação entre esses dois dispositivos.
Como o trabalho ocorre em python, o tipo de dicionário fornecido (dicionário) se sugere para o modelo de memória. Os dados nos quais são armazenados como {key: value} e, neste caso, {address: data}.
memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456
Para a mesma finalidade, o tipo de dados da lista é adequado, onde cada elemento tem suas próprias coordenadas, indicando a localização do elemento na lista:
memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456
Usar dicionários para simular memória parece preferível em vista de maior visibilidade.
A descrição do shell de teste (no arquivo test_seq_access.py) começa com o anúncio dos sinais, a inicialização dos estados iniciais e seu lançamento na função de driver de memória descrita acima:
@block def testbench(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we)
A seguir, descreve o modelo de memória. Os estados iniciais são inicializados; por padrão, a memória é preenchida com valores zero. Limite o modelo de memória para 128 células:
memory = {i: intbv(0) for i in range(128)}
e descreva o comportamento da memória: quando WE no estado baixo, escreva o valor na linha no endereço de memória correspondente, caso contrário, o modelo fornecerá o valor no endereço fornecido:
mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None
Depois, na mesma função, você pode descrever o comportamento dos sinais de entrada (no caso de gravação / leitura seqüencial): o endereço inicial é gravado → 8 células de informação são gravadas → o endereço inicial é gravado → 8 células de informação gravadas são lidas.
@instance def stimul(): init_adr = random.randint(0, 50) # yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr # yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # 8 write.next = 0 data_in.next = random.randint(0, 100) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr # adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # read.next = 0 yield delay(100) read.next = 1 yield delay(100) raise StopSimulation return stimul, ram, access
Executar simulação:
tb = testbench() tb.config_sim(trace=True) tb.run_sim()
Após iniciar o programa, o arquivo testbench_seq_access.vcd é gerado na pasta de trabalho, abra-o em gtkwave:
gtkwave testbench_seq_access.vcd
E nós vemos a imagem:

A informação gravada foi lida com sucesso.
Você pode ver o conteúdo da memória adicionando o seguinte código ao testbench:
for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value))
O seguinte aparece no console:

Teste
Depois disso, você pode realizar vários testes automatizados com um número maior de células graváveis / legíveis. Para fazer isso, vários ciclos de teste e dicionários fictícios são adicionados ao testbench, onde as informações escritas e legíveis e a construção assert são adicionadas, o que causa um erro se dois dicionários não forem iguais:
@instance def stimul(): for time in range(100): temp_mem_write = {} temp_mem_read = {} init_adr = random.randint(0, 50) yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[i] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): read.next = 0 temp_mem_read[i] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, " " for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) raise StopSimulation return stimul, ram, access
Em seguida, você pode criar um segundo testbench para testar a operação no modo de acesso aleatório: test_random_access.py.
A idéia do segundo teste é semelhante: escrevemos informações aleatórias em um endereço aleatório e adicionamos alguns {address: data} ao dicionário temp_mem_write. Depois, contornamos os endereços deste dicionário e lemos as informações da memória, inserindo-as no dicionário temp_mem_read. E, finalmente, com a construção assert, verificamos o conteúdo de dois dicionários.
import random from myhdl import * from ram_driver import ram_driver @block def testbench_random_access(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) memory ={i:intbv(0) for i in range(128)} mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None @instance def stimul(): for time in range(10): temp_mem_write = {} temp_mem_read = {} yield delay(100) for i in range(64): write.next = 1 adr_write.next = 1 adr_start.next = random.randint(0, 126) yield delay(100) adr_write.next = 0 yield delay(100) write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[int(adr_start.val)] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) for key in temp_mem_write.keys(): adr_start.next = key adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) read.next = 0 temp_mem_read[key] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, ' random access' raise StopSimulation return stimul, ram, access tb = testbench_random_access() tb.config_sim(trace=True) tb.run_sim()
O Python possui várias estruturas para automatizar a execução do teste. Vou pegar pytest por simplicidade, ele deve ser instalado a partir do pip:
pip3 install pytest
Quando o comando "pysest" é iniciado a partir do console, a estrutura localiza e executa todos os arquivos na pasta de trabalho com "test_ *" em seus nomes.

Testes concluídos com sucesso. Cometerei um erro deliberado na descrição do dispositivo:
@block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): mem_z = data_memory.driver() @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: adr.next = adr_start else: adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 1 # , else: mem_z.next = None data_out.next = data_memory we.next = 1
Eu executo testes:

Como esperado, em ambos os testes, as informações iniciais (zeros) foram consideradas, ou seja, novas informações não foram registradas.
Conclusão
O uso de python junto com myHDL permite automatizar o teste de firmware desenvolvido para FPGAs e criar quase qualquer ambiente de teste usando os recursos avançados da linguagem de programação python.
O artigo considera:
- criando um módulo que funciona com memória;
- criando um modelo de memória;
- criação de caso de teste;
- automação de teste com a estrutura pytest.