Применение программных ЦПУ ускоряет процесс разработки ПЛИС за счет избежания этапов синтеза и размещения с трассировкой, которые сменяются компиляцией прошивки и обновлением потока битов. Для быстрого же обновления битовых потоков можно использовать либо официальную технику, которая не работает с вендорными ПЛИС, либо обходной путь. Обе этих техники мы и рассмотрим в данной статье.
План статьи
- Введение
- Общий способ вывода инициализированной ОЗУ
- Ручное инстанцирование блочной ОЗУ 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_designupdate_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_designDB_MEM_MIF = $(wildcard ./db/*mem*.mif)SRC_MEM_MIF = ../sw/mem_init_file.mifupdate_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
и доработайте. Пул-реквесты я
принимать готов.Заключение
Я уже пользуюсь этой техникой около двух лет. При этом наблюдается существенное сокращение этапов разработки, что еще больше подталкивает к переносу с оборудования на этот ЦПУ и другой важной функциональности, не завязанной на синхронизации.