Применение программных ЦПУ ускоряет процесс разработки ПЛИС за счет избежания этапов синтеза и размещения с трассировкой, которые сменяются компиляцией прошивки и обновлением потока битов. Для быстрого же обновления битовых потоков можно использовать либо официальную технику, которая не работает с вендорными ПЛИС, либо обходной путь. Обе этих техники мы и рассмотрим в данной статье.
План статьи
- Введение
- Общий способ вывода инициализированной ОЗУ
- Ручное инстанцирование блочной ОЗУ Intel
- Быстрое обновление битового потока после изменения файла MIF
- Быстрое обновление битового потока для общих случаев Verilog
- Мини ЦПУ: конкретный пример дизайна
- Заключение
Введение
Логические элементы ПЛИС настраиваются с помощью потоков битов: таблицы поиска логических элементов получают содержимое, определяющее их поведение, переключатели в комплексных сетях создают нужную топологию маршрутизации, триггеры подключаются к правильным сетевым часам и т.д.
Большинство ПЛИС позволяют блочной ОЗУ получать в процессе настройки ненулевое содержимое.
Данная функция очень важна, если ПЛИС оборудована процессором с программным ядром, потому что это самый простой способ получать загрузочный код сразу после включения питания.
В собственных проектах я зачастую использую такие мини-ЦПУ при реализации контроллеров для всевозможных низкоскоростных протоколов вроде I2C, SPI, Ethernet PHY MDIO и т.д. В этих случаях все прошивки предварительно подготавливаются в блочной ОЗУ. Я практически никогда не применяю внешние флеш-накопители для хранения прошивки просто потому, что обычно использую не настолько большой объем Си кода.
Применение программных ЦПУ вместо аппаратных ускоряет циклы разработки: если вы можете избежать синтеза, а также размещения и трассировки, заменив эти шаги на компиляцию прошивки и обновление потока битов, то вам удастся сэкономить минуты на этапах проектирования мелких схем или даже часы при разработке крупных.
И здесь возникает логичный вопрос:
Как можно быстро обновлять битовые потоки новым содержимым ОЗУ, не проходя через повторный синтез с последующим размещением и трассировкой?
В этой статье я опишу две техники:
- Официальную, которая требует ручного инстанцирования модели ОЗУ на ПЛИС Intel и использования HEX или MIF файла.
- Уловку, которая позволит задействовать в Verilog выведенную ОЗУ, инициализированную с помощью
$readmem(...)
.
В первой технике используется примитив Intel
altsyncram
, и она не работает с вендорными ПЛИС. Вторая же техника делает УРП (уровень регистровых передач) проекта совместимой с различными семействами ПЛИС.Я также приведу пример, в котором обе техники реализуются на минимальной, но при этом полезной системе ЦПУ.
Общий способ вывода инициализированной ОЗУ
Стандартный способ добавления ОЗУ в проект ПЛИС приблизительно выглядит так:
localparam mem_size_bytes = 2048;
// $clog2 требуется Verilog-2005 или новее...
localparam mem_addr_bits = $clog2(mem_size_bytes);
reg [7:0] mem[0:mem_size_bytes-1];
wire mem_wr;
reg [mem_addr_bits-1:0] mem_addr;
reg [7:0] mem_rdata;
reg [7:0] mem_wdata;
always @(posedge clk)
if (mem_wr) begin
mem[mem_addr] <= mem_wdata;
mem_rdata <= mem_wdata; // Это требуется некоторым //ОЗУ на ПЛИС Intel...
end
else
mem_rdata <= mem[mem_addr];
Любой компетентный инструмент синтеза ПЛИС выведет из этого кода блочную ОЗУ размером 2Кб.
Если вам нужно, чтобы содержимое ОЗУ инициализировалось после конфигурации, просто добавьте:
initial begin
$readmemh("mem_init_file.hex", mem);
end
Этот метод работает для симуляции и, опять же, большинство компетентных инструментов ПЛИС будут синтезировать поток битов, инициализирующий блочную ОЗУ с содержимым после конфигурирования. (Хотя разработчикам микросхем ASIC это совсем не понравится).
Здесь важно отметить, что в течение всего процесса, начиная с УРП и до потока битов, инструмент ПЛИС в ходе УРП -анализа и синтеза будет обрабатывать инструкцию
$readmemh()
. Эти шаги происходят в начале всего процесса создания потока битов. В результате простейшая реализация потребует перезапуска процесса в момент изменения содержимого mem_init_file.hex
. По крайней мере это точно касается Quartus… (Открытые инструменты синтеза ICE40/ECP5 и размещения с трассировкой не подпадают в категорию простейшей реализации).Должен быть способ получше…
Ручное инстанцирование блочной ОЗУ
Вместо того, чтобы оставлять процесс вывода ОЗУ из поведенческих блоков Verilog инструментам синтеза, можно явно инстанцировать примитивы ОЗУ в своем коде. Это окажется полезным по ряду причин.
Одна из них относится к случаям, когда нужно использовать очень специфичные возможности блочной ОЗУ конкретной ПЛИС.
Типичный пример — это, когда я хочу убедиться, что в блочной ОЗУ есть триггеры на входе (адрес, данные для записи, разрешение записи) и на выходе (данные для чтения).
Это увеличивает задержку чтения ОЗУ с одного до двух циклов, что во многих проектах не станет проблемой и может привести к существенному повышению тактовой частоты.
В коде ниже прописана ступень конвейера на выходе ОЗУ, которая затем используется в качестве операнда умножителя:
reg [7:0] mem[0:mem_size_bytes-1];
always @(posedge clk)
if (mem_wr_p0) begin
mem[mem_addr_p0] <= mem_wdata_p0;
mem_rdata_p1 <= mem_wdata_p1;
end
else
mem_rdata_p1 <= mem[mem_addr_p0];
// Дополнительная ступень конвейера для разрыва тракта синхронизации //между ОЗУ и входом умножителя
always @(posedge clk)
mem_rdata_p2 <= mem_rdata_p1;
always @(posedge clk)
result_p3 <= some_other_data_p2 * mem_rdata_p2;
Для подобных случаев выход ОЗУ ПЛИС может оказаться бессистемным, так как инструмент синтеза имеет 2 варианта реализации для регистра
rd_data_p2
.Он может использовать выход
FF
ОЗУ так:Либо использовать вход
FF
блока DSP так: Когда мне в таких ситуациях требуется тонкий контроль, я инстанцирую ОЗУ вручную с помощью примитивной ячейки ОЗУ Intel. Ранее приведенный поведенческий код УРП обретает частично структурированную форму:
Выделенная строка здесь ключевая: использование
“REGISTERED”
активирует вывод FF
ОЗУ и добавляет ступень конвейера. Новый код нельзя использовать с ПЛИС других вендоров, и даже его эмулирование становится затруднительным, так как модели симуляции Intel обычно зашифрованы и работают только с коммерческими инструментами симуляции Verilog вроде ModelSim или VCS.
Предлагаемый мной обходной путь состоит в применении
ifdef
, которая выбирает поведенческие блоки для симуляции, и altsyncram
для синтеза.Примитив
altsyncram
также содержит параметр init_file
: altsyncram #(
.operation_mode ("SINGLE_PORT"),
.width_a (8),
.width_ad_a (mem_addr_bits),
.outdata_reg_a ("REGISTERED"),
.init_file ("mem_init_file.mif") // <<<<<<<<<<
)
u_mem(
...
MIF означает «файл инициализации памяти» и представляет проприетарный текстовый формат файлов Intel. Я преобразую двоичные файлы в MIF с помощью собственного скрипта
create_mif.rb
.ОЗУ, выведенные Verilog, можно использовать с файлами MIF:
(* ram_init_file = "mem_init_file.mif" *) reg [7:0] mem[0:mem_size_bytes-1];
Но такая конструкция еще больше усложняет симуляцию, поскольку симуляторы не знают, что делать с Intel-атрибутом
ram_init_file
и просто его игнорируют.Быстрое обновление потока битов после изменения файла MIF
Красота использования
altsyncram
и файла MIF в том, что можно легко обновлять поток битов и изменять файл MIF, не начиная все сначала.Достаточно выполнить следующие шаги:
- заменить содержимое файла MIF;
- Quartus GUI: Processing -> Update Memory Initialization file.
Так вы загрузите обновленный файл во внутреннюю проектную базу данных Quartus.
- Quartus Gui: Processing -> Start -> Start Assembler.
Так вы создадите поток битов из внутренней проектной базы данных Quartus.
Вместо GUI для проделывания двух перечисленных шагов Quartus я задействую Makefile:
QUARTUS_DIR = /home/tom/altera/13.0sp1/quartus/bin/
DESIGN_NAME = my_design
update_ram: sw
$(QUARTUS_DIR)/quartus_cdb $(MY_DESIGN) -c $(MY_DESIGN) --update_mif
$(QUARTUS_DIR)/quartus_asm --read_settings_files=on --write_settings_files=off $(MY_DESIGN) -c $(MY_DESIGN)
sw:
cd ../sw && make
Правило
sw
пересобирает последнюю версию прошивки и создает новый файл MIF. quartus_cdb
обновляет проектную базу данных, а quartus_asm
создает новый битовый поток. Быстрое обновление битового потока для общих случаев Verilog
Чтобы обновить выведенную ОЗУ, которая была инициализирована с помощью
$readmemh()
, нужно самим взломать проектную базу данных Quartus. Это легче, чем кажется, потому что Quartus использует в БД формат файлов MIF.Шаги для обновления выведенной ОЗУ:
- найти в базе данных файл MIF, используемый для ОЗУ;
Я для этого вывожу все находящиеся в БД файлы MIF:
cd quartus/db
ll *.mif
- cоздать файл MIF для выведенной ОЗУ;
В Makefile для своей прошивки я всегда сразу собираю HEX файл (для использования
$readmemh()
) и файл MIF.- скопировать ваш файл MIF поверх аналогичного файла во внутренней БД;
- проделать два ранее описанных шага Quartus.
Так выглядит Makefile:
QUARTUS_DIR = /home/tom/altera/13.0sp1/quartus/bin/
DESIGN_NAME = my_design
DB_MEM_MIF = $(wildcard ./db/*mem*.mif)
SRC_MEM_MIF = ../sw/mem_init_file.mif
update_ram: sw $(DB_MEM_MIF)
$(QUARTUS_DIR)/quartus_cdb $(MY_DESIGN) -c $(MY_DESIGN) --update_mif
$(QUARTUS_DIR)/quartus_asm --read_settings_files=on --write_settings_files=off $(MY_DESIGN) -c $(MY_DESIGN)
$(DB_MEM_MIF): (SRC_MEM_MIF)
cp $< $@
sw:
cd ../sw && make
Самое главное здесь – это выбор верного файла MIF в базе данных. Имя этого файла будет соответствовать иерархическому размещению памяти в проекте, но при этом также обладать случайным hex-суффиксом.
Когда у вас несколько ОЗУ, которые должны обновляться подобным образом, нужно с осторожностью выбирать шаблон, но на деле это не сложно.
Мини ЦПУ: пример конкретного дизайна
Чтобы продемонстрировать только что описанные принципы, я создал небольшой, но в то же время нетривиальный пример, в котором есть ЦПУ VexRiscv, двухпортовая ОЗУ для хранения инструкций ЦПУ и данных, а также периферийные регистры для управления светодиодами и считывания кнопки. Соответствующий GitHub-репозиторий находится здесь.
Этот пример я протестировал на своей плате Arrow DECA FPGA, но его также легко портировать на другие платы ПЛИС Intel.
В нем есть есть `define для выбора между общим выводом ОЗУ и инстанцированием
altsyncram
.Makefile в каталоге
./quartus_max10_deca
показывает, как обновлять 4 ОЗУ, содержащих эту прошивку.Если у вас плата DECA, попробуйте:
- скомпилировать прошивку в каталоге
./sw
; - создать битовый поток;
- проверить, вращаются ли светодиоды в одном направлении;
- изменить define в прошивке, чтобы светодиоды стали вращаться в противоположном направлении;
- выполнить
make update_ram
в каталоге./quartus_max10_deca
, чтобы обновить битовый поток без повторной компиляции.
Если у вас другая плата ПЛИС на базе Intel, то скопируйте каталог
./quartus_max10_deca
и доработайте. Пул-реквесты я принимать готов.Заключение
Я пользуюсь этой техникой уже около двух лет. При этом наблюдается существенное сокращение этапов разработки, что еще больше подталкивает к переносу с оборудования на этот ЦПУ и другой важной функциональности, не завязанной на синхронизации.
flamehj
Резисторно-транзисторная логика?) Мне кажется тут имеется в виду RTL — register transfer level — уровень регистровых передач.
nullc0de
ЛОЛ. Резисторно-транзисторная логика это 5 балов… Человек переводивший статью явно не в теме FPGA. Я бы еще понял если бы RTL расшифровали как register transfer language. Но резисторно-транзисторная логика это прям определение на Нобелевскую премию.