Хабрахабр

Python и FPGA. Тестирование

В продолжение к первой статье, хочу на примере показать вариант работы с FPGA (ПЛИС) на python. В данной статье затрону подробнее аспект тестирования. Если фреймворк MyHDL позволяет людям, работающим на python, используя знакомый синтаксис и экосистему, заглянуть в мир FPGA, то опытным разработчикам ПЛИС смысл использования python не ясен. Парадигмы описания аппаратуры для MyHDL и Verilog похожи, а выбор в пользу определенного языка вопрос привычки и вкуса. За Verilog/VHDL выступает то, что на этих языках давно пишут прошивки, и по факту они являются стандартными для описания цифровой аппаратуры. Python, как новичок в этой сфере, может конкурировать в области написания тестового окружения. Значительную часть времени у FPGA разработчика занимает тестирование своих дизайнов. Далее я хочу на примере продемонстрировать как это делается в python с MyHDL.

Для простоты возьму память, общающуюся с другими устройствами через параллельный интерфейс (а не через последовательный, например I2C). Допустим, есть задача описать на ПЛИС некое устройство, работающее с памятью. Например отечественная 1645РУ1У и ее аналоги. Такие микросхемы не всегда бывают практичны в виду того, что для работы с ними требуется много пинов, с другой стороны обеспечивается более быстрый и упрощенный обмен информации.

Запись выглядит так: ПЛИС даёт 16-разрядный адрес ячейки, 8-бит данных, формирует сигнал на запись WE (write enable). Поскольку OE (output enable) и CE (chip enable) всегда разрешены, чтение происходит по смене адреса ячейки. Запись и чтение может производиться, как последовательно по несколько ячеек подряд, начиная с определённого адреса adr_start, записываемого по переднему фронту сигнала adr_write, так и по одной ячейке по произвольному адресу (random access).

На 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 не обязательно, этот шаг потребуется для прошивания ПЛИС.
После описания логики, следует провести верификацию проекта. Можно ограничиться, например тем, чтобы смоделировать входные воздействия и на временной диаграмме увидеть ответ модуля. Но при таком варианте сложней предсказать взаимодействие Вашего модуля с микросхемой памяти. Поэтому для полноценной проверки работы созданного устройства нужно создать модель памяти и протестировать взаимодействие между этими двумя устройствами.

Данные в котором хранятся как , а для этого случая {адрес: данные}. Поскольку работа происходит в python, за модель памяти сам собой напрашивается тип данный dictionary (словарь).

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

Для этих же целей подходит тип данных list (список), где у каждого элемента есть свои координаты, обозначающие расположение элемента в списке:

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

Далее можно создать второй testbench для проверки работы в режиме случайного доступа к памяти: test_random_access.py.

После чего обходим адреса в этом словаре и считываем информацию из памяти, занося ее в словарь temp_mem_read. Идея у второго теста схожая: записываем случайную информацию по случайному адресу и добавляем пару {адрес: данные} в словарь temp_mem_write. И в конце конструкцией 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 позволяет автоматизировать тестирование разработанных прошивок для ПЛИС и создавать практически любое тестовое окружение используя богатые возможности языка программирования python.

В статье рассмотрено:

  • создание модуля, работающего с памятью;
  • создание модели памяти;
  • создание тестового сценария;
  • автоматизация тестирования с фреймворком pytest.
Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»