Допустим, есть задача описать на ПЛИС некое устройство, работающее с памятью. Для простоты возьму память, общающуюся с другими устройствами через параллельный интерфейс (а не через последовательный, например 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.
Комментарии (8)
QuokkaFPGA
03.03.2019 08:24Всем привет!
Внезапно засветили мою работу, спасибо! :)
Вот боевая репка github.com/EvgenyMuryshkin/QuokkaEvaluation
Пробуйте, если будут пожелания по фичам — обращайтесь.
Евгений.roboter
03.03.2019 12:59Привет, там есть Hello World но нету примера как всё это компилируется.
И насколько всё это применимо? всё можно описать? все возможности поддерживаются?QuokkaFPGA
03.03.2019 15:10+1Там есть cli, в ридми пример как его вызывать, надо поставить путь до твоего проекта.
Поддерживается не все, списка фич нет, к сожалению пока.
В примерах весь функционал который поддерживается.
СофтЦПУ в разработке, дин. памяти нету, все в блок рам лежит.
Генерируемый код не самый оптимальный. Все генерируется в конечный автомат (каждая строка в общем отдельное состояние).
Поддерживаются основные типы данных и плоские структуры.
Есть интеграция с ФПУ, но нету кастов в целые числа и недоделаны операции сравнения флоатов. Есть пример БПФ.
Есть продвинутые фичи lock и throw.
Есть аппаратная поддержка json-ish плоской структуры.
QuokkaFPGA
03.03.2019 15:14+1Ограниченно применимо, так скажем… если идет борьба за каждый такт и лут, то вам не к нам :) но всякие базовые протоколы я делал, я думаю что основная ниша это робототехника и управление множеством актуаторов, сенсоров и координация в устройстве. Со временем будет и клок аккурат код, если будет достаточная поддержка проекта.
QuokkaFPGA
03.03.2019 15:22+1У меня есть канал в тележке — QuokkaFPGA заходите и там можно в деталях обмуждать чтобы тут не засорять :)
roboter
А c# для FPGA ещё не завезли?
A__D
quokka-fpga.net/#
github.com/EvgenyMuryshkin?tab=repositories
lingvo
C# нет, но С, С++, System C уже завезли.