Python和FPGA。 测试中

第一篇文章的续篇中,我想以python为例演示使用FPGA(FPGA)的示例。 本文将更详细地介绍测试方面。 如果MyHDL框架允许人们使用熟悉的语法和生态系统来研究python,那么有经验的FPGA开发人员将不了解使用python的含义。 MyHDL和Verilog的硬件描述范例相似,选择特定语言取决于习惯和口味。 Verilog / VHDL代表了使用这些语言编写固件很长时间的事实,并且事实上,它们是描述数字设备的标准。 作为该领域的新手,Python可以在编写测试环境中竞争。 FPGA开发人员的大部分时间都花在了测试他们的设计上。 接下来,我想以一个示例来演示如何使用MyHDL在python中完成此操作。

假设有一个任务描述一个与FPGA上的存储器一起工作的器件。 为简单起见,我将使用通过并行接口(而不是通过串行接口,例如I2C)与其他设备通信的内存。 考虑到这样的微电路并非总是实用的,因为需要与它们一起工作的许多引脚;另一方面,提供了更快和更容易的信息交换。 例如,家用1645RU1U及其类似物。



模块说明


记录如下所示:FPGA提供16位单元地址,8位数据,并生成写信号WE(写使能)。 由于OE(输出使能)和CE(芯片使能)始终处于启用状态,因此在更改单元地址时会发生读取。 可以从一个特定的adr_start地址开始(记录在adr_write信号的前沿)开始,并在一个行中的多个单元中依次进行写入和读取,也可以在任意地址处进行一个单元(随机访问)。

在MyHDL上,代码如下所示(写入和读取信号采用反向逻辑):

from myhdl import * @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 = 0 #    ,    else: mem_z.next = None #        data_out.next = data_memory we.next = 1 return write_data, write_start_adr 

如果使用以下功能转换为Verilog:

 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') 

然后我们得到以下内容:
 `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 

无需将项目转换为Verilog即可进行仿真;升级FPGA将需要此步骤。

造型


在逻辑描述之后,应验证项目。 您可以限制自己,例如,模拟输入影响并在时间图中查看模块的响应。 但是使用此选项,更难预测模块与内存芯片的交互。 因此,要完全验证创建的设备的运行,您需要创建一个内存模型并测试这两个设备之间的交互。

由于工作是在python中进行的,因此给定类型的字典(字典)建议将其用于内存模型。 数据以{key:value}的形式存储,在这种情况下为{address:data}。

 memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456 

出于相同的目的,列表数据类型是合适的,其中每个元素都有其自己的坐标,以指示元素在列表中的位置:

 memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456 

考虑到更大的可见性,使用字典来模拟内存似乎是可取的。

测试外壳的描述(在文件test_seq_access.py中)从信号的声明,初始状态的初始化以及将它们扔入上述内存驱动器功能开始:

 @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) 

下面介绍内存模型。 初始化初始状态,默认情况下,内存中会填充零值。 将内存模型限制为128个单元:

 memory = {i: intbv(0) for i in range(128)} 

并描述内存的行为:当WE处于低电平状态时,将该行中的值写入相应的内存地址,否则模型给出给定地址处的值:

 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 

然后,在同一功能中,您可以描述输入信号的行为(对于顺序写/读的情况):记录起始地址→记录8个信息单元→记录起始地址→读取8个记录的信息单元。

 @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 

运行模拟:

 tb = testbench() tb.config_sim(trace=True) tb.run_sim() 

启动程序后,在工作文件夹中生成testbench_seq_access.vcd文件,在gtkwave中将其打开:

 gtkwave testbench_seq_access.vcd 

我们看到了图片:



已成功读取记录的信息。

您可以通过将以下代码添加到testbench来查看内存的内容:

 for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) 

控制台中将显示以下内容:



测试中


此后,您可以使用数量更多的可写/可读单元格进行一些自动化测试。 为此,将几个测试周期和伪字典添加到testbench,在其中添加了书面和可读信息以及assert结构,如果两个字典不相等,则会导致错误:

 @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 

接下来,您可以创建第二个测试平台来测试随机访问模式下的操作:test_random_access.py。

第二个测试的想法是相似的:我们在随机地址处写入随机信息,并向temp_mem_write字典中添加一对{address:data}。 然后,我们遍历此字典中的地址,并从内存中读取信息,并将其输入到temp_mem_read字典中。 最后,assert构造检查两个字典的内容。

 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() 

Python有几个框架可以自动执行测试。 为了简单起见,我将使用pytest,它必须从pip安装:

 pip3 install pytest 

当从控制台启动“ pysest”命令时,框架将在工作文件夹中查找并执行所有文件名中带有“ test_ *”的文件。



测试成功完成。 我会在设备说明中故意犯一个错误:

 @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 

我运行测试:



如预期的那样,在两个测试中,都考虑了初始信息(零),即未记录新信息。

结论


将python与myHDL结合使用,可以使您自动为FPGA开发的固件进行测试,并使用python编程语言的丰富功能创建几乎任何测试环境。

本文认为:

  • 创建一个与内存兼容的模块;
  • 创建一个内存模型;
  • 测试案例创建;
  • 使用pytest框架进行自动化测试。

Source: https://habr.com/ru/post/zh-CN442010/


All Articles