Применение программных ЦПУ ускоряет процесс разработки ПЛИС за счет избежания этапов синтеза и размещения с трассировкой, которые сменяются компиляцией прошивки и обновлением потока битов. Для быстрого же обновления битовых потоков можно использовать либо официальную технику, которая не работает с вендорными ПЛИС, либо обходной путь. Обе этих техники мы и рассмотрим в данной статье.

План статьи



Введение


Логические элементы ПЛИС настраиваются с помощью потоков битов: таблицы поиска логических элементов получают содержимое, определяющее их поведение, переключатели в комплексных сетях создают нужную топологию маршрутизации, триггеры подключаются к правильным сетевым часам и т.д.

Большинство ПЛИС позволяют блочной ОЗУ получать в процессе настройки ненулевое содержимое.

Данная функция очень важна, если ПЛИС оборудована процессором с программным ядром, потому что это самый простой способ получать загрузочный код сразу после включения питания.

В собственных проектах я зачастую использую такие мини-ЦПУ при реализации контроллеров для всевозможных низкоскоростных протоколов вроде 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 и доработайте. Пул-реквесты я принимать готов.

Заключение


Я пользуюсь этой техникой уже около двух лет. При этом наблюдается существенное сокращение этапов разработки, что еще больше подталкивает к переносу с оборудования на этот ЦПУ и другой важной функциональности, не завязанной на синхронизации.