В продолжение к первой статье, хочу на примере показать вариант работы с 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 код выглядит следующим образом (сигналы на запись и чтение приходят в обратной логике):
Если сконвертировать в Verilog при помощи функции:
то получится следующее:
Для моделирования конвертировать проект в Verilog не обязательно, этот шаг потребуется для прошивания ПЛИС.
После описания логики, следует провести верификацию проекта. Можно ограничиться, например тем, чтобы смоделировать входные воздействия и на временной диаграмме увидеть ответ модуля. Но при таком варианте сложней предсказать взаимодействие Вашего модуля с микросхемой памяти. Поэтому для полноценной проверки работы созданного устройства нужно создать модель памяти и протестировать взаимодействие между этими двумя устройствами.
Поскольку работа происходит в python, за модель памяти сам собой напрашивается тип данный dictionary (словарь). Данные в котором хранятся как {ключ: значение}, а для этого случая {адрес: данные}.
Для этих же целей подходит тип данных list (список), где у каждого элемента есть свои координаты, обозначающие расположение элемента в списке:
Использование словарей для имитации памяти выглядит более предпочтительным в виду большей наглядности.
Описание тестовой оболочки (в файле test_seq_access.py) начинается с объявления сигналов, инициализации начальных состояний и прокидывания их в вышеописанную функцию драйвера памяти:
Далее описывается модель памяти. Инициализируются начальные состояния, по умолчанию память заполняется нулевыми значении. Ограничим модель памяти 128 ячейками:
и опишем поведение памяти: когда WE в низком состоянии записываем значение в линии в соответствующий адрес памяти, в противном случае модель выдает значение по заданному адресу:
После, в этой же функции можно описать поведение входных сигналов (для случая последовательной записи/чтения): записывается начальный адрес → записываются 8 ячейки информации → записывается начальный адрес → читаются 8 записанных ячеек информации.
Запуск моделирования:
После запуска программы в рабочей папке сгенирируется файл testbench_seq_access.vcd, открываем его в gtkwave:
И видим картинку:

Записанная информация успешно прочиталась.
Увидеть содержимое памяти можно добавив в testbench следующий код:
В консоли появиться следующее:

После этого можно провести несколько автоматизированных тестов с увеличенным количеством записываемых/читаемых ячеек. Для этого в testbench добавляются несколько циклов проверки и фиктивные словари, куда складывается записываемая и читаемая информация и конструкция assert, которая вызывает ошибку в случае неравенства двух словарей:
Далее можно создать второй testbench для проверки работы в режиме случайного доступа к памяти: test_random_access.py.
Идея у второго теста схожая: записываем случайную информацию по случайному адресу и добавляем пару {адрес: данные} в словарь temp_mem_write. После чего обходим адреса в этом словаре и считываем информацию из памяти, занося ее в словарь temp_mem_read. И в конце конструкцией assert проверяем содержимое двух словарей.
Для автоматизации выполнения тестов у python есть несколько фреймоворков. Я возьму для простоты pytest, его надо ставить из pip:
При запуске из консоли команды «pysest», фреймворк найдёт и исполнит все файлы в рабочей папке, в названиях которых присутствует «test_*».

Тесты выполнены успешно. Нарочно сделаю ошибку в описании устройства:
Запускаю тесты:

Как и предполагалось, в обоих тестах считалась начальная информация (нули), то есть новая информация не записалась.
Использование python вместе с myHDL позволяет автоматизировать тестирование разработанных прошивок для ПЛИС и создавать практически любое тестовое окружение используя богатые возможности языка программирования python.
В статье рассмотрено:
Допустим, есть задача описать на ПЛИС некое устройство, работающее с памятью. Для простоты возьму память, общающуюся с другими устройствами через параллельный интерфейс (а не через последовательный, например 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_write. После чего обходим адреса в этом словаре и считываем информацию из памяти, занося ее в словарь 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 позволяет автоматизировать тестирование разработанных прошивок для ПЛИС и создавать практически любое тестовое окружение используя богатые возможности языка программирования python.
В статье рассмотрено:
- создание модуля, работающего с памятью;
- создание модели памяти;
- создание тестового сценария;
- автоматизация тестирования с фреймворком pytest.
